diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..50a4c7b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: "fix: " +labels: bug +--- + +**Description** + +A clear and concise description of what the bug is. + +**Steps To Reproduce** + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected Behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Additional Context** + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/build.md b/.github/ISSUE_TEMPLATE/build.md new file mode 100644 index 0000000..0cf8e62 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/build.md @@ -0,0 +1,14 @@ +--- +name: Build System +about: Changes that affect the build system or external dependencies +title: "build: " +labels: build +--- + +**Description** + +Describe what changes need to be done to the build system and why. + +**Requirements** + +- [ ] The build system is passing diff --git a/.github/ISSUE_TEMPLATE/chore.md b/.github/ISSUE_TEMPLATE/chore.md new file mode 100644 index 0000000..498ebfd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/chore.md @@ -0,0 +1,14 @@ +--- +name: Chore +about: Other changes that don't modify src or test files +title: "chore: " +labels: chore +--- + +**Description** + +Clearly describe what change is needed and why. If this changes code then please use another issue type. + +**Requirements** + +- [ ] No functional changes to the code diff --git a/.github/ISSUE_TEMPLATE/ci.md b/.github/ISSUE_TEMPLATE/ci.md new file mode 100644 index 0000000..fa2dd9e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ci.md @@ -0,0 +1,14 @@ +--- +name: Continuous Integration +about: Changes to the CI configuration files and scripts +title: "ci: " +labels: ci +--- + +**Description** + +Describe what changes need to be done to the ci/cd system and why. + +**Requirements** + +- [ ] The ci system is passing diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..ec4bb38 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..f494a4d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,14 @@ +--- +name: Documentation +about: Improve the documentation so all collaborators have a common understanding +title: "docs: " +labels: documentation +--- + +**Description** + +Clearly describe what documentation you are looking to add or improve. + +**Requirements** + +- [ ] Requirements go here diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ddd2fcc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,18 @@ +--- +name: Feature Request +about: A new feature to be added to the project +title: "feat: " +labels: feature +--- + +**Description** + +Clearly describe what you are looking to add. The more context the better. + +**Requirements** + +- [ ] Checklist of requirements to be fulfilled + +**Additional Context** + +Add any other context or screenshots about the feature request go here. diff --git a/.github/ISSUE_TEMPLATE/performance.md b/.github/ISSUE_TEMPLATE/performance.md new file mode 100644 index 0000000..699b8d4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance.md @@ -0,0 +1,14 @@ +--- +name: Performance Update +about: A code change that improves performance +title: "perf: " +labels: performance +--- + +**Description** + +Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/ISSUE_TEMPLATE/refactor.md b/.github/ISSUE_TEMPLATE/refactor.md new file mode 100644 index 0000000..1626c57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor.md @@ -0,0 +1,14 @@ +--- +name: Refactor +about: A code change that neither fixes a bug nor adds a feature +title: "refactor: " +labels: refactor +--- + +**Description** + +Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/ISSUE_TEMPLATE/revert.md b/.github/ISSUE_TEMPLATE/revert.md new file mode 100644 index 0000000..9d121dc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/revert.md @@ -0,0 +1,16 @@ +--- +name: Revert Commit +about: Reverts a previous commit +title: "revert: " +labels: revert +--- + +**Description** + +Provide a link to a PR/Commit that you are looking to revert and why. + +**Requirements** + +- [ ] Change has been reverted +- [ ] No change in test coverage has happened +- [ ] A new ticket is created for any follow on work that needs to happen diff --git a/.github/ISSUE_TEMPLATE/style.md b/.github/ISSUE_TEMPLATE/style.md new file mode 100644 index 0000000..02244a7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/style.md @@ -0,0 +1,14 @@ +--- +name: Style Changes +about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) +title: "style: " +labels: style +--- + +**Description** + +Clearly describe what you are looking to change and why. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/ISSUE_TEMPLATE/test.md b/.github/ISSUE_TEMPLATE/test.md new file mode 100644 index 0000000..431a7ea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test.md @@ -0,0 +1,14 @@ +--- +name: Test +about: Adding missing tests or correcting existing tests +title: "test: " +labels: test +--- + +**Description** + +List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. + +**Requirements** + +- [ ] There is no drop in test coverage. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..2f4c71d --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ + + +## Description + + + +## Checklist + + +- [ ] My PR title is in the style of [conventional commits](https://www.conventionalcommits.org/) +- [ ] All public facing APIs are doccumented with [dartdoc](https://dart.dev/guides/language/effective-dart/documentation) +- [ ] I have added tests to cover my changes diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..63b035c --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,11 @@ +version: 2 +enable-beta-ecosystems: true +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + - package-ecosystem: "pub" + directory: "/" + schedule: + interval: "daily" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..e586e67 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,50 @@ +name: ci + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + semantic_pull_request: + uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 + + flutter-check: + name: Build Check + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: ๐Ÿ“š Checkout + uses: actions/checkout@v4 + + - name: ๐Ÿฆ Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + cache: true + + - name: โ“‚๏ธ Set up Melos + uses: bluefireteam/melos-action@v2 + + - name: ๐Ÿงช Run Analyze + run: melos run analyze + + - name: ๐Ÿ“ Run Test + run: melos run coverage + + - name: ๐Ÿ“Š Generate Coverage + id: coverage-report + uses: whynotmake-it/dart-coverage-assistant@efd2d5f7992843c0b42cb6566a7a047bd4970000 + with: + enforce_threshold: 'none' + enforce_forbidden_decrease: 'none' + lower_threshold: 50 + upper_threshold: 90 + \ No newline at end of file diff --git a/.gitignore b/.gitignore index d1eabbf..2e8b9f8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ -# Don't include platform stuff -/example/ios +# Don't include platform code in example /example/android +/example/ios +/example/linux /example/macos /example/web +/example/windows + # Miscellaneous *.class @@ -14,6 +17,8 @@ .buildlog/ .history .svn/ +.mason/ +migrate_working_dir/ # IntelliJ related *.iml @@ -21,61 +26,11 @@ *.iws .idea/ -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ +# See https://www.dartlang.org/guides/libraries/private-files -# Flutter/Dart/Pub related -**/doc/api/ +# Files and directories created by pub .dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies .packages -.pub-cache/ -.pub/ build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 +pubspec.lock +pubspec_overrides.yaml \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..05f13bd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "conventionalCommits.scopes": [ + "value_notifier_tools", + "melos", + "example" + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..befaea6 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,34 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build_runner build", + "type": "shell", + "command": "melos run build_runner", + "group": "build", "presentation": { + "reveal": "silent", + "group": "build" + } + }, + { + "label": "melos bullshit", + "type": "shell", + "group": "none", + "command": "melos bs", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": [] + }, + { + "label": "dart fix", + "group": "none", + "type": "shell", + "command": "melos run fix", + "presentation": { + "reveal": "silent" + }, + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index a621b74..5221ac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,130 +1,3 @@ -## 0.9.1 -- removed erroneous `simulatePressure` -## 0.9.0 -- Use `perfect_freehand` for much smoother lines (thanks to @mattrussell-sonocent) -- BREAKING: Removed line customization parameters from widgets +# 0.1.0+1 -## 0.4.0 -- Upgraded dependencies (thanks to @wxxedu) -- ``ScribbleNotifier.clear()`` clears the drawing without resetting anything else (color, width e.t.c) - -## 0.3.0 -- Upgraded dependencies -- Added ``ScribbleSketch`` widget for just displaying a sketch without input functionality, no notifier needed! -## 0.2.2 -- Upgraded dependencies - -## 0.2.1 -- Updated README to include the newest features -- Downgraded json_serializable due to a bug with freezed - -## 0.2.0 -#### BREAKING: -- Custom ScribbleNotifiers now need to provide a GlobalKey which is used in the renderImage() method to access Scribble's - RepaintBoundary -- Updated example to demonstrate image export. - -#### Image Export: -- You can now export the Scribble to an Image ```ByteData``` using the ScribbleNotifiers ``renderImage()`` method! - -#### Other Changes: -- The pressure on web is overridden so the cursor matches the selected pen width! -- ``ScribbleNotifier`` now extends ``ScribbleNotifierBase`` instead of implementing it as an interface. -- Updated dependencies - -## 0.1.3 -#### Filter for Pointers: -You can now switch between different ``ScribblePointerMode``s, even at runtime. - -This is very helpful for example, if Scribble lives inside a Scrollable and you want users to be able to navigate with their finger while drawing with their pen. - -Check the updated example to try it out! - -#### Other Changes: -* ``ScribbleNotifier`` now has the option to set the sketch from outside after it has been constructed using the ``setSketch()`` method. You can even choose whether you want it to be committed to the undo history. -* Added documentation to ``ScribbleState`` -* Updated example -* Updated dependencies - -## 0.1.2 - -* Removed the speed calculation using time due to precision issues - -## 0.1.1 -* Added scaleFactor to support zoomable canvases. This allows you to for example wrap the Scribble Widget in an -InteractiveViewer, so that users can draw finer details. - -## 0.1.0 - -* Points now remember their time to calculate speed more accurately -* Multiple fixes for drawing with real pens or touch -* ``ScribbleState`` can now be serialized to JSON - -### Breaking: - -* speedFactor's value should now be higher for the same effect, the default value has changed to 0.4 -* ``color`` property in state is now an int to allow for easy JSON -## 0.0.13 - -* Draw better line ends -* Removed marker-like blend mode for now due to performance and buggy rendering in some cases - -## 0.0.12 - -* Eraser keeps pen width and the other way around - -## 0.0.11 - -* Eraser doesn't autoselect anymore -* Undo doesn't undo color and stroke selection - -## 0.0.10 - -* Fixed stupid bug with pointer exit - -## 0.0.9 - -* Better behavior on pointer exit - -## 0.0.8 - -* Reduced Dependencies -* Replaced kimchi package with the better suited [history_state_notifier](https://pub.dev/packages/history_state_notifier) -* Fixed a bug with redo queue clearing - -## 0.0.7 - -* **BREAKING:** The ``drawPointer`` parameter is now called ``drawPen`` -* You can now obtain the current sketch from the notifier. - If you want to store it somewhere for example you can call ```toJson()``` on it. -* You can now pass a sketch to a ``ScribbleNotifier`` constructor to initialize it with an existing - drawing. -* Added ``ScribbleNotifierBase`` interface so you can write your own notifier that works with the ``Scribble``widget -* Added pressure curve support to the notifier -* Allows more customization in the scribble widget for how the lines are rendered - - -## 0.0.6 - -* Upped minimum flutter version to 2.5 - -## 0.0.5 - -* Back to flutter 2.2.3 - -## 0.0.4 - -* Upped minimum flutter version to 2.3 - -## 0.0.3 - -* meta dependency to hopefully work with analysis - -## 0.0.2 - -* Added documentation -* Fixed dependencies - -## 0.0.1 - -* Initial release +- feat: initial commit ๐ŸŽ‰ diff --git a/README.md b/README.md index 6b2e706..47b0a88 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,22 @@ # Scribble + +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) + + Scribble is a lightweight library for freehand drawing in Flutter supporting pressure, variable line width and more! -![scribble_demo](./scribble_demo.gif) +## Installation ๐Ÿ’ป -> Note: Scribble is still in development and will receive more features down the line! +**โ— In order to start using Scribble you must have the [Dart SDK][dart_install_link] installed on your machine.** + +Install via `dart pub add`: + +```sh +dart pub add scribble +``` + +--- ## Features @@ -13,13 +26,9 @@ Scribble is a lightweight library for freehand drawing in Flutter supporting pre * Choose which pointers can draw (touch, pen, mouse, etc.) * Lines get slimmer when the pen is moved more quickly * Line eraser support -* Full undo/redo support using [history_state_notifier](https://pub.dev/packages/history_state_notifier) +* Full undo/redo support using [value_notifier_tools](https://pub.dev/packages/value_notifier_tools) * Sketches are fully serializable to JSON - -## Pipeline - -* [X] Load sketches -* [X] PNG export +* Export Sketches to PNG ## Usage @@ -28,30 +37,16 @@ Scribble is a lightweight library for freehand drawing in Flutter supporting pre You can create a drawing surface by adding the ``Scribble`` widget to your widget tree and passing in a ``ScribbleNotifier``. -Where you manage this notifier is up to you, but since it is a ``StateNotifier``, it works amazingly -with [riverpod](https://pub.dev/packages/flutter_riverpod) for example. - -```dart -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -final scribbleStateProvider = -StateNotifierProvider.autoDispose( - (ref) => ScribbleNotifier(), -); -``` - -You can then pass the notifier to the scribble widget. - ```dart import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -class App extends ConsumerWidget { +class App extends StatelessWidget { @override - Widget build(BuildContext context, ScopedReader watch) { + Widget build(BuildContext context) { return Scaffold( body: Scribble( - notifier: watch(scribbleStateProvider.notifier), + notifier: notifier, ), ); } @@ -81,4 +76,12 @@ notifier.renderImage(pixelRatio: 2.0); As mentioned above, the package is still under development, but we already use it in the app we are currently developing. -Feel free to contribute, or open issues in our [GitHub repo](https://github.com/timcreatedit/scribble). \ No newline at end of file +Feel free to contribute, or open issues in our [GitHub repo](https://github.com/timcreatedit/scribble). + + +[dart_install_link]: https://dart.dev/get-dart +[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[mason_link]: https://github.com/felangel/mason +[very_good_ventures_link]: https://verygood.ventures diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..245ed9f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +include: package:lintervention/analysis_options.yaml diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 0000000..4926a19 --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,426 @@ +SF:lib/src/domain/model/sketch/sketch.dart +DA:19,2 +LF:1 +LH:1 +end_of_record +SF:lib/src/domain/model/sketch/sketch.g.dart +DA:9,2 +DA:10,1 +DA:11,3 +DA:12,1 +DA:15,0 +DA:16,0 +DA:17,0 +LF:7 +LH:4 +end_of_record +SF:lib/src/view/notifier/scribble_notifier.dart +DA:22,0 +DA:58,0 +DA:62,0 +DA:65,0 +DA:70,0 +DA:71,0 +DA:83,0 +DA:102,0 +DA:103,0 +DA:105,0 +DA:109,0 +DA:111,0 +DA:114,0 +DA:129,0 +DA:133,0 +DA:134,0 +DA:137,0 +DA:143,0 +DA:144,0 +DA:153,0 +DA:154,0 +DA:158,0 +DA:160,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:169,0 +DA:170,0 +DA:171,0 +DA:172,0 +DA:173,0 +DA:174,0 +DA:176,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:188,0 +DA:189,0 +DA:195,0 +DA:196,0 +DA:197,0 +DA:198,0 +DA:199,0 +DA:200,0 +DA:201,0 +DA:207,0 +DA:208,0 +DA:217,0 +DA:218,0 +DA:219,0 +DA:225,0 +DA:226,0 +DA:227,0 +DA:228,0 +DA:229,0 +DA:230,0 +DA:231,0 +DA:233,0 +DA:234,0 +DA:235,0 +DA:236,0 +DA:237,0 +DA:238,0 +DA:239,0 +DA:245,0 +DA:247,0 +DA:248,0 +DA:250,0 +DA:255,0 +DA:257,0 +DA:258,0 +DA:261,0 +DA:262,0 +DA:263,0 +DA:265,0 +DA:266,0 +DA:267,0 +DA:270,0 +DA:272,0 +DA:273,0 +DA:274,0 +DA:275,0 +DA:276,0 +DA:277,0 +DA:278,0 +DA:282,0 +DA:283,0 +DA:288,0 +DA:290,0 +DA:291,0 +DA:292,0 +DA:297,0 +DA:298,0 +DA:299,0 +DA:301,0 +DA:302,0 +DA:303,0 +DA:309,0 +DA:311,0 +DA:313,0 +DA:314,0 +DA:315,0 +DA:318,0 +DA:320,0 +DA:321,0 +DA:324,0 +DA:330,0 +DA:332,0 +DA:333,0 +DA:334,0 +DA:337,0 +DA:339,0 +DA:340,0 +DA:343,0 +DA:348,0 +DA:350,0 +DA:351,0 +DA:354,0 +DA:358,0 +DA:359,0 +DA:360,0 +DA:361,0 +DA:362,0 +DA:364,0 +DA:365,0 +DA:366,0 +DA:367,0 +DA:368,0 +DA:369,0 +DA:370,0 +DA:376,0 +DA:377,0 +DA:378,0 +DA:379,0 +DA:380,0 +DA:381,0 +DA:382,0 +DA:383,0 +DA:386,0 +DA:391,0 +DA:392,0 +DA:394,0 +DA:395,0 +DA:396,0 +DA:397,0 +DA:398,0 +DA:399,0 +DA:403,0 +DA:404,0 +DA:407,0 +DA:409,0 +DA:410,0 +LF:154 +LH:0 +end_of_record +SF:lib/src/view/scribble.dart +DA:19,0 +DA:39,0 +DA:41,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:68,0 +DA:70,0 +DA:71,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +DA:80,0 +LF:30 +LH:0 +end_of_record +SF:lib/src/view/scribble_sketch.dart +DA:13,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:32,0 +DA:33,0 +LF:6 +LH:0 +end_of_record +SF:lib/src/view/state/scribble.state.dart +DA:94,0 +DA:95,0 +DA:96,0 +DA:100,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:119,0 +DA:124,0 +LF:16 +LH:0 +end_of_record +SF:lib/src/view/state/scribble.state.g.dart +DA:9,0 +DA:10,0 +DA:11,0 +DA:12,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:31,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:62,0 +DA:64,0 +DA:65,0 +DA:66,0 +DA:67,0 +DA:70,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:79,0 +LF:49 +LH:0 +end_of_record +SF:lib/src/domain/model/point/point.dart +DA:18,1 +DA:21,2 +LF:2 +LH:2 +end_of_record +SF:lib/src/domain/model/point/point.g.dart +DA:9,2 +DA:10,2 +DA:11,2 +DA:12,2 +DA:15,0 +DA:16,0 +DA:17,0 +DA:18,0 +DA:19,0 +LF:9 +LH:4 +end_of_record +SF:lib/src/domain/model/sketch_line/sketch_line.dart +DA:25,1 +DA:26,1 +LF:2 +LH:2 +end_of_record +SF:lib/src/domain/model/sketch_line/sketch_line.g.dart +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,3 +DA:13,1 +DA:14,1 +DA:15,2 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +LF:12 +LH:7 +end_of_record +SF:lib/src/view/painting/point_to_offset_x.dart +DA:8,0 +LF:1 +LH:0 +end_of_record +SF:lib/src/view/painting/sketch_line_path_mixin.dart +DA:14,0 +DA:18,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:25,0 +DA:26,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:48,0 +DA:49,0 +DA:50,0 +LF:24 +LH:0 +end_of_record +SF:lib/src/view/painting/scribble_editing_painter.dart +DA:12,0 +DA:32,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:41,0 +DA:43,0 +DA:44,0 +DA:48,0 +DA:49,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:68,0 +DA:70,0 +LF:23 +LH:0 +end_of_record +SF:lib/src/view/painting/scribble_painter.dart +DA:8,0 +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:28,0 +DA:29,0 +DA:33,0 +DA:35,0 +DA:36,0 +LF:10 +LH:0 +end_of_record +SF:lib/src/view/pan_gesture_catcher.dart +DA:12,0 +DA:24,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:31,0 +DA:33,0 +DA:35,0 +DA:38,0 +DA:45,0 +DA:48,0 +DA:50,0 +DA:53,0 +DA:56,0 +DA:58,0 +LF:16 +LH:0 +end_of_record diff --git a/example/.gitignore b/example/.gitignore index 0fa6b67..29a3a50 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +migrate_working_dir/ # IntelliJ related *.iml @@ -26,14 +27,10 @@ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies -.packages .pub-cache/ .pub/ /build/ -# Web related -lib/generated_plugin_registrant.dart - # Symbolication related app.*.symbols diff --git a/example/.metadata b/example/.metadata index be0f63d..87b4b9a 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: 4cc385b4b84ac2f816d939a49ea1f328c4e0b48e - channel: stable + revision: "68bfaea224880b488c617afe30ab12091ea8fa4e" + channel: "stable" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e + base_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e + - platform: macos + create_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e + base_revision: 68bfaea224880b488c617afe30ab12091ea8fa4e + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/README.md b/example/README.md index d1099c1..1b7a4e3 100644 --- a/example/README.md +++ b/example/README.md @@ -1,10 +1,3 @@ -# Scribble Example +# example -An example that demonstrates how the scribble package can be used - -## Run the app - -1. Open a terminal -2. Navigate to this `example` directory -3. Run `flutter create .` -4. Run `flutter run` from your Terminal, or launch the project from your IDE! \ No newline at end of file +A new Flutter project. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 61b6c4d..f9b3034 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1,29 +1 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/example/lib/main.dart b/example/lib/main.dart index a116196..73ec47f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,29 +1,30 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; -import 'package:flutter_state_notifier/flutter_state_notifier.dart'; import 'package:scribble/scribble.dart'; +import 'package:value_notifier_tools/value_notifier_tools.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Scribble', - theme: ThemeData( - primarySwatch: Colors.blue, - ), + theme: ThemeData.from( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple)), home: const HomePage(title: 'Scribble'), ); } } class HomePage extends StatefulWidget { - const HomePage({Key? key, required this.title}) : super(key: key); + const HomePage({super.key, required this.title}); final String title; @@ -43,58 +44,132 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, appBar: AppBar( title: Text(widget.title), - leading: IconButton( - icon: const Icon(Icons.save), - tooltip: "Save to Image", - onPressed: () => _saveImage(context), - ), + actions: _buildActions(context), ), - body: SingleChildScrollView( - child: SizedBox( - height: MediaQuery.of(context).size.height * 2, - child: Stack( - children: [ - Scribble( - notifier: notifier, - drawPen: true, - ), - Positioned( - top: 16, - right: 16, - child: Column( - children: [ - _buildColorToolbar(context), - const Divider( - height: 32, - ), - _buildStrokeToolbar(context), - ], + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 64), + child: Column( + children: [ + Expanded( + child: Card( + clipBehavior: Clip.hardEdge, + margin: EdgeInsets.zero, + color: Colors.white, + surfaceTintColor: Colors.white, + child: Scribble( + notifier: notifier, + drawPen: true, ), - ) - ], + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + _buildColorToolbar(context), + const VerticalDivider(width: 32), + _buildStrokeToolbar(context), + const Expanded(child: SizedBox()), + _buildPointerModeSwitcher(context), + ], + ), + ) + ], + ), + ), + ); + } + + List _buildActions(context) { + return [ + ValueListenableBuilder( + valueListenable: notifier, + builder: (context, value, child) => IconButton( + icon: child as Icon, + tooltip: "Undo", + onPressed: notifier.canUndo ? notifier.undo : null, + ), + child: const Icon(Icons.undo), + ), + ValueListenableBuilder( + valueListenable: notifier, + builder: (context, value, child) => IconButton( + icon: child as Icon, + tooltip: "Redo", + onPressed: notifier.canRedo ? notifier.redo : null, + ), + child: const Icon(Icons.redo), + ), + IconButton( + icon: const Icon(Icons.clear), + tooltip: "Clear", + onPressed: notifier.clear, + ), + IconButton( + icon: const Icon(Icons.image), + tooltip: "Show PNG Image", + onPressed: () => _showImage(context), + ), + IconButton( + icon: const Icon(Icons.data_object), + tooltip: "Show JSON", + onPressed: () => _showJson(context), + ), + ]; + } + + void _showImage(BuildContext context) async { + final image = notifier.renderImage(); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Generated Image"), + content: SizedBox.expand( + child: FutureBuilder( + future: image, + builder: (context, snapshot) => snapshot.hasData + ? Image.memory(snapshot.data!.buffer.asUint8List()) + : const Center(child: CircularProgressIndicator()), ), ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text("Close"), + ) + ], ), ); } - Future _saveImage(BuildContext context) async { - final image = await notifier.renderImage(); + void _showJson(BuildContext context) { showDialog( context: context, builder: (context) => AlertDialog( - title: const Text("Your Image"), - content: Image.memory(image.buffer.asUint8List()), + title: const Text("Sketch as JSON"), + content: SizedBox.expand( + child: SelectableText( + jsonEncode(notifier.currentSketch.toJson()), + autofocus: true, + ), + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: const Text("Close"), + ) + ], ), ); } Widget _buildStrokeToolbar(BuildContext context) { - return StateNotifierBuilder( - stateNotifier: notifier, - builder: (context, state, _) => Column( + return ValueListenableBuilder( + valueListenable: notifier, + builder: (context, state, _) => Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -144,78 +219,56 @@ class _HomePageState extends State { } Widget _buildColorToolbar(BuildContext context) { - return StateNotifierBuilder( - stateNotifier: notifier, - builder: (context, state, _) => Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _buildUndoButton(context), - const Divider( - height: 4.0, - ), - _buildRedoButton(context), - const Divider( - height: 4.0, - ), - _buildClearButton(context), - const Divider( - height: 20.0, - ), - _buildPointerModeSwitcher(context, - penMode: - state.allowedPointersMode == ScribblePointerMode.penOnly), - const Divider( - height: 20.0, - ), - _buildEraserButton(context, isSelected: state is Erasing), - _buildColorButton(context, color: Colors.black, state: state), - _buildColorButton(context, color: Colors.red, state: state), - _buildColorButton(context, color: Colors.green, state: state), - _buildColorButton(context, color: Colors.blue, state: state), - _buildColorButton(context, color: Colors.yellow, state: state), - ], - ), + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _buildColorButton(context, color: Colors.black), + _buildColorButton(context, color: Colors.red), + _buildColorButton(context, color: Colors.green), + _buildColorButton(context, color: Colors.blue), + _buildColorButton(context, color: Colors.yellow), + _buildEraserButton(context), + ], ); } - Widget _buildPointerModeSwitcher(BuildContext context, - {required bool penMode}) { - return FloatingActionButton.small( - onPressed: () => notifier.setAllowedPointersMode( - penMode ? ScribblePointerMode.all : ScribblePointerMode.penOnly, - ), - tooltip: - "Switch drawing mode to " + (penMode ? "all pointers" : "pen only"), - child: AnimatedSwitcher( - duration: kThemeAnimationDuration, - child: !penMode - ? const Icon( - Icons.touch_app, - key: ValueKey(true), - ) - : const Icon( - Icons.do_not_touch, - key: ValueKey(false), + Widget _buildPointerModeSwitcher(BuildContext context) { + return ValueListenableBuilder( + valueListenable: notifier.select( + (value) => value.allowedPointersMode, + ), + builder: (context, value, child) { + return SegmentedButton( + multiSelectionEnabled: false, + emptySelectionAllowed: false, + onSelectionChanged: (v) => notifier.setAllowedPointersMode(v.first), + segments: const [ + ButtonSegment( + value: ScribblePointerMode.all, + icon: Icon(Icons.touch_app), + label: Text("All pointers"), ), - ), - ); + ButtonSegment( + value: ScribblePointerMode.penOnly, + icon: Icon(Icons.draw), + label: Text("Pen only"), + ), + ], + selected: {value}, + ); + }); } - Widget _buildEraserButton(BuildContext context, {required bool isSelected}) { - return Padding( - padding: const EdgeInsets.all(4), - child: FloatingActionButton.small( - tooltip: "Erase", - backgroundColor: const Color(0xFFF7FBFF), - elevation: isSelected ? 10 : 2, - shape: !isSelected - ? const CircleBorder() - : RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.remove, color: Colors.blueGrey), - onPressed: notifier.setEraser, + Widget _buildEraserButton(BuildContext context) { + return ValueListenableBuilder( + valueListenable: notifier.select((value) => value is Erasing), + builder: (context, value, child) => ColorButton( + color: Colors.transparent, + outlineColor: Colors.black, + isActive: value, + onPressed: () => notifier.setEraser(), + child: const Icon(Icons.cleaning_services), ), ); } @@ -223,61 +276,68 @@ class _HomePageState extends State { Widget _buildColorButton( BuildContext context, { required Color color, - required ScribbleState state, }) { - final isSelected = state is Drawing && state.selectedColor == color.value; - return Padding( - padding: const EdgeInsets.all(4), - child: FloatingActionButton.small( - backgroundColor: color, - elevation: isSelected ? 10 : 2, - shape: !isSelected - ? const CircleBorder() - : RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - child: Container(), - onPressed: () => notifier.setColor(color)), - ); - } - - Widget _buildUndoButton( - BuildContext context, - ) { - return FloatingActionButton.small( - tooltip: "Undo", - onPressed: notifier.canUndo ? notifier.undo : null, - disabledElevation: 0, - backgroundColor: notifier.canUndo ? Colors.blueGrey : Colors.grey, - child: const Icon( - Icons.undo_rounded, - color: Colors.white, + return ValueListenableBuilder( + valueListenable: notifier.select( + (value) => value is Drawing && value.selectedColor == color.value), + builder: (context, value, child) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: ColorButton( + color: color, + isActive: value, + onPressed: () => notifier.setColor(color), + ), ), ); } +} - Widget _buildRedoButton( - BuildContext context, - ) { - return FloatingActionButton.small( - tooltip: "Redo", - onPressed: notifier.canRedo ? notifier.redo : null, - disabledElevation: 0, - backgroundColor: notifier.canRedo ? Colors.blueGrey : Colors.grey, - child: const Icon( - Icons.redo_rounded, - color: Colors.white, - ), - ); - } +class ColorButton extends StatelessWidget { + const ColorButton({ + required this.color, + required this.isActive, + required this.onPressed, + this.outlineColor, + this.child, + super.key, + }); + + final Color color; + + final Color? outlineColor; + + final bool isActive; + + final VoidCallback onPressed; - Widget _buildClearButton(BuildContext context) { - return FloatingActionButton.small( - tooltip: "Clear", - onPressed: notifier.clear, - disabledElevation: 0, - backgroundColor: Colors.blueGrey, - child: const Icon(Icons.clear), + final Icon? child; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: kThemeAnimationDuration, + decoration: ShapeDecoration( + shape: CircleBorder( + side: BorderSide( + color: switch (isActive) { + true => outlineColor ?? color, + false => Colors.transparent, + }, + width: 2, + ), + ), + ), + child: IconButton( + style: FilledButton.styleFrom( + backgroundColor: color, + shape: const CircleBorder(), + side: isActive + ? const BorderSide(color: Colors.white, width: 2) + : const BorderSide(color: Colors.transparent), + ), + onPressed: onPressed, + icon: child ?? const SizedBox(), + ), ); } } diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index 9fb8d2d..0000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,224 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.9.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - characters: - dependency: transitive - description: - name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.16.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - fake_async: - dependency: transitive - description: - name: fake_async - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - flutter_state_notifier: - dependency: "direct main" - description: - name: flutter_state_notifier - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - freezed_annotation: - dependency: transitive - description: - name: freezed_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.3" - history_state_notifier: - dependency: transitive - description: - name: history_state_notifier - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.5" - json_annotation: - dependency: transitive - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "4.5.0" - lints: - dependency: transitive - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.12" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.5" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.0" - nested: - dependency: transitive - description: - name: nested - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - perfect_freehand: - dependency: transitive - description: - name: perfect_freehand - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - provider: - dependency: transitive - description: - name: provider - url: "https://pub.dartlang.org" - source: hosted - version: "6.0.0" - scribble: - dependency: "direct main" - description: - path: ".." - relative: true - source: path - version: "0.9.1" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - state_notifier: - dependency: transitive - description: - name: state_notifier - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.2+1" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.1" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.12" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" -sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=2.5.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 37eaf44..3a1c822 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,71 +1,22 @@ name: example -description: A new Flutter project. - +description: "An example project for scribble." publish_to: 'none' -version: 1.0.0+1 +version: 0.1.0 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: '>=3.3.2 <4.0.0' dependencies: flutter: sdk: flutter - - - cupertino_icons: ^1.0.2 - flutter_state_notifier: ^0.7.1 scribble: path: .. + value_notifier_tools: ^0.1.1 dev_dependencies: flutter_test: sdk: flutter + flutter_lints: ^3.0.0 - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^1.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/lib/scribble.dart b/lib/scribble.dart index c08ff98..05117e9 100644 --- a/lib/scribble.dart +++ b/lib/scribble.dart @@ -1,7 +1,8 @@ +/// Scribble is a lightweight library for freehand drawing in Flutter library scribble; -export 'package:scribble/src/model/sketch/sketch.dart'; -export 'package:scribble/src/scribble.dart'; -export 'package:scribble/src/scribble.notifier.dart'; -export 'package:scribble/src/scribble_sketch.dart'; -export 'package:scribble/src/state/scribble.state.dart'; +export 'package:scribble/src/domain/model/sketch/sketch.dart'; +export 'package:scribble/src/view/notifier/scribble_notifier.dart'; +export 'package:scribble/src/view/scribble.dart'; +export 'package:scribble/src/view/scribble_sketch.dart'; +export 'package:scribble/src/view/state/scribble.state.dart'; diff --git a/lib/src/core/pan_gesture_catcher.dart b/lib/src/core/pan_gesture_catcher.dart deleted file mode 100644 index 1af5f90..0000000 --- a/lib/src/core/pan_gesture_catcher.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; - -class GestureCatcher extends StatelessWidget { - const GestureCatcher({ - Key? key, - required this.pointerKindsToCatch, - required this.child, - }) : super(key: key); - - final Set pointerKindsToCatch; - final Widget child; - - @override - Widget build(BuildContext context) { - return RawGestureDetector( - key: ValueKey(pointerKindsToCatch), - gestures: { - GestureCatcherRecognizer: - GestureRecognizerFactoryWithHandlers( - () => GestureCatcherRecognizer( - debugOwner: this, - pointerKindsToCatch: pointerKindsToCatch, - ), - (GestureCatcherRecognizer instance) { - }, - ) - }, - child: child, - ); - } -} - -/// Catches movement both horizontally and vertically for a given set of -/// pointer kinds.. -/// -/// See also: -/// -/// * [ImmediateMultiDragGestureRecognizer], for a similar recognizer that -/// tracks each touch point independently. -/// * [DelayedMultiDragGestureRecognizer], for a similar recognizer that -/// tracks each touch point independently, but that doesn't start until -/// some time has passed. -class GestureCatcherRecognizer extends OneSequenceGestureRecognizer { - /// Create a gesture recognizer for tracking movement on a plane. - GestureCatcherRecognizer({ - required Set pointerKindsToCatch, - Object? debugOwner, - }) : super(debugOwner: debugOwner, supportedDevices: pointerKindsToCatch); - - @override - String get debugDescription => 'pan catcher'; - - @override - void didStopTrackingLastPointer(int pointer) { - } - - @override - void handleEvent(PointerEvent event) { - resolve(GestureDisposition.accepted); - } -} diff --git a/lib/src/model/sketch/point/point.dart b/lib/src/domain/model/point/point.dart similarity index 54% rename from lib/src/model/sketch/point/point.dart rename to lib/src/domain/model/point/point.dart index 8d8aaee..0f323ed 100644 --- a/lib/src/model/sketch/point/point.dart +++ b/lib/src/domain/model/point/point.dart @@ -1,22 +1,22 @@ -import 'dart:ui'; - import 'package:freezed_annotation/freezed_annotation.dart'; part 'point.freezed.dart'; - part 'point.g.dart'; -@freezed +/// {@template point} +/// Represents a point in a sketch with an x and y coordinate and an optional +/// pressure value. +/// {@endtemplate} +@Freezed() class Point with _$Point { - const Point._(); - + /// {@macro point} const factory Point( double x, double y, { - @Default(1) double pressure, + @Default(0.5) double pressure, }) = _Point; + const Point._(); + /// Constructs a point from a JSON object. factory Point.fromJson(Map json) => _$PointFromJson(json); - - Offset get asOffset => Offset(x, y); } diff --git a/lib/src/model/sketch/point/point.freezed.dart b/lib/src/domain/model/point/point.freezed.dart similarity index 58% rename from lib/src/model/sketch/point/point.freezed.dart rename to lib/src/domain/model/point/point.freezed.dart index b0c4015..bc354ce 100644 --- a/lib/src/model/sketch/point/point.freezed.dart +++ b/lib/src/domain/model/point/point.freezed.dart @@ -1,7 +1,7 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'point.dart'; @@ -12,7 +12,7 @@ part of 'point.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Point _$PointFromJson(Map json) { return _Point.fromJson(json); @@ -32,74 +32,80 @@ mixin _$Point { /// @nodoc abstract class $PointCopyWith<$Res> { factory $PointCopyWith(Point value, $Res Function(Point) then) = - _$PointCopyWithImpl<$Res>; + _$PointCopyWithImpl<$Res, Point>; + @useResult $Res call({double x, double y, double pressure}); } /// @nodoc -class _$PointCopyWithImpl<$Res> implements $PointCopyWith<$Res> { +class _$PointCopyWithImpl<$Res, $Val extends Point> + implements $PointCopyWith<$Res> { _$PointCopyWithImpl(this._value, this._then); - final Point _value; // ignore: unused_field - final $Res Function(Point) _then; + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + @pragma('vm:prefer-inline') @override $Res call({ - Object? x = freezed, - Object? y = freezed, - Object? pressure = freezed, + Object? x = null, + Object? y = null, + Object? pressure = null, }) { return _then(_value.copyWith( - x: x == freezed + x: null == x ? _value.x : x // ignore: cast_nullable_to_non_nullable as double, - y: y == freezed + y: null == y ? _value.y : y // ignore: cast_nullable_to_non_nullable as double, - pressure: pressure == freezed + pressure: null == pressure ? _value.pressure : pressure // ignore: cast_nullable_to_non_nullable as double, - )); + ) as $Val); } } /// @nodoc -abstract class _$$_PointCopyWith<$Res> implements $PointCopyWith<$Res> { - factory _$$_PointCopyWith(_$_Point value, $Res Function(_$_Point) then) = - __$$_PointCopyWithImpl<$Res>; +abstract class _$$PointImplCopyWith<$Res> implements $PointCopyWith<$Res> { + factory _$$PointImplCopyWith( + _$PointImpl value, $Res Function(_$PointImpl) then) = + __$$PointImplCopyWithImpl<$Res>; @override + @useResult $Res call({double x, double y, double pressure}); } /// @nodoc -class __$$_PointCopyWithImpl<$Res> extends _$PointCopyWithImpl<$Res> - implements _$$_PointCopyWith<$Res> { - __$$_PointCopyWithImpl(_$_Point _value, $Res Function(_$_Point) _then) - : super(_value, (v) => _then(v as _$_Point)); - - @override - _$_Point get _value => super._value as _$_Point; +class __$$PointImplCopyWithImpl<$Res> + extends _$PointCopyWithImpl<$Res, _$PointImpl> + implements _$$PointImplCopyWith<$Res> { + __$$PointImplCopyWithImpl( + _$PointImpl _value, $Res Function(_$PointImpl) _then) + : super(_value, _then); + @pragma('vm:prefer-inline') @override $Res call({ - Object? x = freezed, - Object? y = freezed, - Object? pressure = freezed, + Object? x = null, + Object? y = null, + Object? pressure = null, }) { - return _then(_$_Point( - x == freezed + return _then(_$PointImpl( + null == x ? _value.x : x // ignore: cast_nullable_to_non_nullable as double, - y == freezed + null == y ? _value.y : y // ignore: cast_nullable_to_non_nullable as double, - pressure: pressure == freezed + pressure: null == pressure ? _value.pressure : pressure // ignore: cast_nullable_to_non_nullable as double, @@ -109,11 +115,11 @@ class __$$_PointCopyWithImpl<$Res> extends _$PointCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$_Point extends _Point { - const _$_Point(this.x, this.y, {this.pressure = 1}) : super._(); +class _$PointImpl extends _Point { + const _$PointImpl(this.x, this.y, {this.pressure = 0.5}) : super._(); - factory _$_Point.fromJson(Map json) => - _$$_PointFromJson(json); + factory _$PointImpl.fromJson(Map json) => + _$$PointImplFromJson(json); @override final double x; @@ -129,49 +135,49 @@ class _$_Point extends _Point { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_Point && - const DeepCollectionEquality().equals(other.x, x) && - const DeepCollectionEquality().equals(other.y, y) && - const DeepCollectionEquality().equals(other.pressure, pressure)); + other is _$PointImpl && + (identical(other.x, x) || other.x == x) && + (identical(other.y, y) || other.y == y) && + (identical(other.pressure, pressure) || + other.pressure == pressure)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash( - runtimeType, - const DeepCollectionEquality().hash(x), - const DeepCollectionEquality().hash(y), - const DeepCollectionEquality().hash(pressure)); + int get hashCode => Object.hash(runtimeType, x, y, pressure); @JsonKey(ignore: true) @override - _$$_PointCopyWith<_$_Point> get copyWith => - __$$_PointCopyWithImpl<_$_Point>(this, _$identity); + @pragma('vm:prefer-inline') + _$$PointImplCopyWith<_$PointImpl> get copyWith => + __$$PointImplCopyWithImpl<_$PointImpl>(this, _$identity); @override Map toJson() { - return _$$_PointToJson(this); + return _$$PointImplToJson( + this, + ); } } abstract class _Point extends Point { const factory _Point(final double x, final double y, - {final double pressure}) = _$_Point; + {final double pressure}) = _$PointImpl; const _Point._() : super._(); - factory _Point.fromJson(Map json) = _$_Point.fromJson; + factory _Point.fromJson(Map json) = _$PointImpl.fromJson; @override - double get x => throw _privateConstructorUsedError; + double get x; @override - double get y => throw _privateConstructorUsedError; + double get y; @override - double get pressure => throw _privateConstructorUsedError; + double get pressure; @override @JsonKey(ignore: true) - _$$_PointCopyWith<_$_Point> get copyWith => + _$$PointImplCopyWith<_$PointImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/model/sketch/point/point.g.dart b/lib/src/domain/model/point/point.g.dart similarity index 65% rename from lib/src/model/sketch/point/point.g.dart rename to lib/src/domain/model/point/point.g.dart index 450094b..2b06066 100644 --- a/lib/src/model/sketch/point/point.g.dart +++ b/lib/src/domain/model/point/point.g.dart @@ -6,13 +6,14 @@ part of 'point.dart'; // JsonSerializableGenerator // ************************************************************************** -_$_Point _$$_PointFromJson(Map json) => _$_Point( +_$PointImpl _$$PointImplFromJson(Map json) => _$PointImpl( (json['x'] as num).toDouble(), (json['y'] as num).toDouble(), - pressure: (json['pressure'] as num?)?.toDouble() ?? 1, + pressure: (json['pressure'] as num?)?.toDouble() ?? 0.5, ); -Map _$$_PointToJson(_$_Point instance) => { +Map _$$PointImplToJson(_$PointImpl instance) => + { 'x': instance.x, 'y': instance.y, 'pressure': instance.pressure, diff --git a/lib/src/model/sketch/sketch.dart b/lib/src/domain/model/sketch/sketch.dart similarity index 52% rename from lib/src/model/sketch/sketch.dart rename to lib/src/domain/model/sketch/sketch.dart index cf95924..b20f9fa 100644 --- a/lib/src/model/sketch/sketch.dart +++ b/lib/src/domain/model/sketch/sketch.dart @@ -1,17 +1,20 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:scribble/src/model/sketch/sketch_line/sketch_line.dart'; +import 'package:scribble/src/domain/model/sketch_line/sketch_line.dart'; -export 'point/point.dart'; -export 'sketch_line/sketch_line.dart'; +export '../point/point.dart'; +export '../sketch_line/sketch_line.dart'; part 'sketch.freezed.dart'; part 'sketch.g.dart'; +/// Represents a sketch with a list of [SketchLine]s. @freezed class Sketch with _$Sketch { + /// Represents a sketch with a list of [SketchLine]s. const factory Sketch({ required List lines, }) = _Sketch; + /// Constructs a sketch from a JSON object. factory Sketch.fromJson(Map json) => _$SketchFromJson(json); } diff --git a/lib/src/model/sketch/sketch.freezed.dart b/lib/src/domain/model/sketch/sketch.freezed.dart similarity index 61% rename from lib/src/model/sketch/sketch.freezed.dart rename to lib/src/domain/model/sketch/sketch.freezed.dart index 586b914..286a8a4 100644 --- a/lib/src/model/sketch/sketch.freezed.dart +++ b/lib/src/domain/model/sketch/sketch.freezed.dart @@ -1,7 +1,7 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'sketch.dart'; @@ -12,7 +12,7 @@ part of 'sketch.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Sketch _$SketchFromJson(Map json) { return _Sketch.fromJson(json); @@ -30,54 +30,60 @@ mixin _$Sketch { /// @nodoc abstract class $SketchCopyWith<$Res> { factory $SketchCopyWith(Sketch value, $Res Function(Sketch) then) = - _$SketchCopyWithImpl<$Res>; + _$SketchCopyWithImpl<$Res, Sketch>; + @useResult $Res call({List lines}); } /// @nodoc -class _$SketchCopyWithImpl<$Res> implements $SketchCopyWith<$Res> { +class _$SketchCopyWithImpl<$Res, $Val extends Sketch> + implements $SketchCopyWith<$Res> { _$SketchCopyWithImpl(this._value, this._then); - final Sketch _value; // ignore: unused_field - final $Res Function(Sketch) _then; + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + @pragma('vm:prefer-inline') @override $Res call({ - Object? lines = freezed, + Object? lines = null, }) { return _then(_value.copyWith( - lines: lines == freezed + lines: null == lines ? _value.lines : lines // ignore: cast_nullable_to_non_nullable as List, - )); + ) as $Val); } } /// @nodoc -abstract class _$$_SketchCopyWith<$Res> implements $SketchCopyWith<$Res> { - factory _$$_SketchCopyWith(_$_Sketch value, $Res Function(_$_Sketch) then) = - __$$_SketchCopyWithImpl<$Res>; +abstract class _$$SketchImplCopyWith<$Res> implements $SketchCopyWith<$Res> { + factory _$$SketchImplCopyWith( + _$SketchImpl value, $Res Function(_$SketchImpl) then) = + __$$SketchImplCopyWithImpl<$Res>; @override + @useResult $Res call({List lines}); } /// @nodoc -class __$$_SketchCopyWithImpl<$Res> extends _$SketchCopyWithImpl<$Res> - implements _$$_SketchCopyWith<$Res> { - __$$_SketchCopyWithImpl(_$_Sketch _value, $Res Function(_$_Sketch) _then) - : super(_value, (v) => _then(v as _$_Sketch)); - - @override - _$_Sketch get _value => super._value as _$_Sketch; - +class __$$SketchImplCopyWithImpl<$Res> + extends _$SketchCopyWithImpl<$Res, _$SketchImpl> + implements _$$SketchImplCopyWith<$Res> { + __$$SketchImplCopyWithImpl( + _$SketchImpl _value, $Res Function(_$SketchImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') @override $Res call({ - Object? lines = freezed, + Object? lines = null, }) { - return _then(_$_Sketch( - lines: lines == freezed + return _then(_$SketchImpl( + lines: null == lines ? _value._lines : lines // ignore: cast_nullable_to_non_nullable as List, @@ -87,15 +93,16 @@ class __$$_SketchCopyWithImpl<$Res> extends _$SketchCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$_Sketch implements _Sketch { - const _$_Sketch({required final List lines}) : _lines = lines; +class _$SketchImpl implements _Sketch { + const _$SketchImpl({required final List lines}) : _lines = lines; - factory _$_Sketch.fromJson(Map json) => - _$$_SketchFromJson(json); + factory _$SketchImpl.fromJson(Map json) => + _$$SketchImplFromJson(json); final List _lines; @override List get lines { + if (_lines is EqualUnmodifiableListView) return _lines; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_lines); } @@ -106,10 +113,10 @@ class _$_Sketch implements _Sketch { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_Sketch && + other is _$SketchImpl && const DeepCollectionEquality().equals(other._lines, _lines)); } @@ -120,24 +127,27 @@ class _$_Sketch implements _Sketch { @JsonKey(ignore: true) @override - _$$_SketchCopyWith<_$_Sketch> get copyWith => - __$$_SketchCopyWithImpl<_$_Sketch>(this, _$identity); + @pragma('vm:prefer-inline') + _$$SketchImplCopyWith<_$SketchImpl> get copyWith => + __$$SketchImplCopyWithImpl<_$SketchImpl>(this, _$identity); @override Map toJson() { - return _$$_SketchToJson(this); + return _$$SketchImplToJson( + this, + ); } } abstract class _Sketch implements Sketch { - const factory _Sketch({required final List lines}) = _$_Sketch; + const factory _Sketch({required final List lines}) = _$SketchImpl; - factory _Sketch.fromJson(Map json) = _$_Sketch.fromJson; + factory _Sketch.fromJson(Map json) = _$SketchImpl.fromJson; @override - List get lines => throw _privateConstructorUsedError; + List get lines; @override @JsonKey(ignore: true) - _$$_SketchCopyWith<_$_Sketch> get copyWith => + _$$SketchImplCopyWith<_$SketchImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/model/sketch/sketch.g.dart b/lib/src/domain/model/sketch/sketch.g.dart similarity index 73% rename from lib/src/model/sketch/sketch.g.dart rename to lib/src/domain/model/sketch/sketch.g.dart index f84bba0..3bdcbcd 100644 --- a/lib/src/model/sketch/sketch.g.dart +++ b/lib/src/domain/model/sketch/sketch.g.dart @@ -6,12 +6,13 @@ part of 'sketch.dart'; // JsonSerializableGenerator // ************************************************************************** -_$_Sketch _$$_SketchFromJson(Map json) => _$_Sketch( +_$SketchImpl _$$SketchImplFromJson(Map json) => _$SketchImpl( lines: (json['lines'] as List) .map((e) => SketchLine.fromJson(e as Map)) .toList(), ); -Map _$$_SketchToJson(_$_Sketch instance) => { +Map _$$SketchImplToJson(_$SketchImpl instance) => + { 'lines': instance.lines.map((e) => e.toJson()).toList(), }; diff --git a/lib/src/model/sketch/sketch_line/sketch_line.dart b/lib/src/domain/model/sketch_line/sketch_line.dart similarity index 53% rename from lib/src/model/sketch/sketch_line/sketch_line.dart rename to lib/src/domain/model/sketch_line/sketch_line.dart index 7413475..20d6787 100644 --- a/lib/src/model/sketch/sketch_line/sketch_line.dart +++ b/lib/src/domain/model/sketch_line/sketch_line.dart @@ -1,18 +1,27 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:scribble/src/model/sketch/point/point.dart'; +import 'package:scribble/src/domain/model/point/point.dart'; part 'sketch_line.freezed.dart'; part 'sketch_line.g.dart'; +/// {@template sketch_line} +/// Represents a line in a sketch. +/// {@endtemplate} @freezed class SketchLine with _$SketchLine { + /// {@macro sketch_line} const factory SketchLine({ + /// The points that make up the line required List points, + + /// The color of the line in hexadecimal format (ARGB) required int color, + + /// The width of the line required double width, }) = _SketchLine; + /// Constructs a sketch line from a JSON object. factory SketchLine.fromJson(Map json) => _$SketchLineFromJson(json); - } diff --git a/lib/src/model/sketch/sketch_line/sketch_line.freezed.dart b/lib/src/domain/model/sketch_line/sketch_line.freezed.dart similarity index 58% rename from lib/src/model/sketch/sketch_line/sketch_line.freezed.dart rename to lib/src/domain/model/sketch_line/sketch_line.freezed.dart index d7b917c..0bfa22f 100644 --- a/lib/src/model/sketch/sketch_line/sketch_line.freezed.dart +++ b/lib/src/domain/model/sketch_line/sketch_line.freezed.dart @@ -1,7 +1,7 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'sketch_line.dart'; @@ -12,7 +12,7 @@ part of 'sketch_line.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); SketchLine _$SketchLineFromJson(Map json) { return _SketchLine.fromJson(json); @@ -20,8 +20,13 @@ SketchLine _$SketchLineFromJson(Map json) { /// @nodoc mixin _$SketchLine { + /// The points that make up the line List get points => throw _privateConstructorUsedError; + + /// The color of the line in hexadecimal format (ARGB) int get color => throw _privateConstructorUsedError; + + /// The width of the line double get width => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -34,77 +39,81 @@ mixin _$SketchLine { abstract class $SketchLineCopyWith<$Res> { factory $SketchLineCopyWith( SketchLine value, $Res Function(SketchLine) then) = - _$SketchLineCopyWithImpl<$Res>; + _$SketchLineCopyWithImpl<$Res, SketchLine>; + @useResult $Res call({List points, int color, double width}); } /// @nodoc -class _$SketchLineCopyWithImpl<$Res> implements $SketchLineCopyWith<$Res> { +class _$SketchLineCopyWithImpl<$Res, $Val extends SketchLine> + implements $SketchLineCopyWith<$Res> { _$SketchLineCopyWithImpl(this._value, this._then); - final SketchLine _value; // ignore: unused_field - final $Res Function(SketchLine) _then; + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + @pragma('vm:prefer-inline') @override $Res call({ - Object? points = freezed, - Object? color = freezed, - Object? width = freezed, + Object? points = null, + Object? color = null, + Object? width = null, }) { return _then(_value.copyWith( - points: points == freezed + points: null == points ? _value.points : points // ignore: cast_nullable_to_non_nullable as List, - color: color == freezed + color: null == color ? _value.color : color // ignore: cast_nullable_to_non_nullable as int, - width: width == freezed + width: null == width ? _value.width : width // ignore: cast_nullable_to_non_nullable as double, - )); + ) as $Val); } } /// @nodoc -abstract class _$$_SketchLineCopyWith<$Res> +abstract class _$$SketchLineImplCopyWith<$Res> implements $SketchLineCopyWith<$Res> { - factory _$$_SketchLineCopyWith( - _$_SketchLine value, $Res Function(_$_SketchLine) then) = - __$$_SketchLineCopyWithImpl<$Res>; + factory _$$SketchLineImplCopyWith( + _$SketchLineImpl value, $Res Function(_$SketchLineImpl) then) = + __$$SketchLineImplCopyWithImpl<$Res>; @override + @useResult $Res call({List points, int color, double width}); } /// @nodoc -class __$$_SketchLineCopyWithImpl<$Res> extends _$SketchLineCopyWithImpl<$Res> - implements _$$_SketchLineCopyWith<$Res> { - __$$_SketchLineCopyWithImpl( - _$_SketchLine _value, $Res Function(_$_SketchLine) _then) - : super(_value, (v) => _then(v as _$_SketchLine)); - - @override - _$_SketchLine get _value => super._value as _$_SketchLine; +class __$$SketchLineImplCopyWithImpl<$Res> + extends _$SketchLineCopyWithImpl<$Res, _$SketchLineImpl> + implements _$$SketchLineImplCopyWith<$Res> { + __$$SketchLineImplCopyWithImpl( + _$SketchLineImpl _value, $Res Function(_$SketchLineImpl) _then) + : super(_value, _then); + @pragma('vm:prefer-inline') @override $Res call({ - Object? points = freezed, - Object? color = freezed, - Object? width = freezed, + Object? points = null, + Object? color = null, + Object? width = null, }) { - return _then(_$_SketchLine( - points: points == freezed + return _then(_$SketchLineImpl( + points: null == points ? _value._points : points // ignore: cast_nullable_to_non_nullable as List, - color: color == freezed + color: null == color ? _value.color : color // ignore: cast_nullable_to_non_nullable as int, - width: width == freezed + width: null == width ? _value.width : width // ignore: cast_nullable_to_non_nullable as double, @@ -114,25 +123,32 @@ class __$$_SketchLineCopyWithImpl<$Res> extends _$SketchLineCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$_SketchLine implements _SketchLine { - const _$_SketchLine( +class _$SketchLineImpl implements _SketchLine { + const _$SketchLineImpl( {required final List points, required this.color, required this.width}) : _points = points; - factory _$_SketchLine.fromJson(Map json) => - _$$_SketchLineFromJson(json); + factory _$SketchLineImpl.fromJson(Map json) => + _$$SketchLineImplFromJson(json); + /// The points that make up the line final List _points; + + /// The points that make up the line @override List get points { + if (_points is EqualUnmodifiableListView) return _points; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_points); } + /// The color of the line in hexadecimal format (ARGB) @override final int color; + + /// The width of the line @override final double width; @@ -142,31 +158,31 @@ class _$_SketchLine implements _SketchLine { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$_SketchLine && + other is _$SketchLineImpl && const DeepCollectionEquality().equals(other._points, _points) && - const DeepCollectionEquality().equals(other.color, color) && - const DeepCollectionEquality().equals(other.width, width)); + (identical(other.color, color) || other.color == color) && + (identical(other.width, width) || other.width == width)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash( - runtimeType, - const DeepCollectionEquality().hash(_points), - const DeepCollectionEquality().hash(color), - const DeepCollectionEquality().hash(width)); + runtimeType, const DeepCollectionEquality().hash(_points), color, width); @JsonKey(ignore: true) @override - _$$_SketchLineCopyWith<_$_SketchLine> get copyWith => - __$$_SketchLineCopyWithImpl<_$_SketchLine>(this, _$identity); + @pragma('vm:prefer-inline') + _$$SketchLineImplCopyWith<_$SketchLineImpl> get copyWith => + __$$SketchLineImplCopyWithImpl<_$SketchLineImpl>(this, _$identity); @override Map toJson() { - return _$$_SketchLineToJson(this); + return _$$SketchLineImplToJson( + this, + ); } } @@ -174,19 +190,25 @@ abstract class _SketchLine implements SketchLine { const factory _SketchLine( {required final List points, required final int color, - required final double width}) = _$_SketchLine; + required final double width}) = _$SketchLineImpl; factory _SketchLine.fromJson(Map json) = - _$_SketchLine.fromJson; + _$SketchLineImpl.fromJson; @override - List get points => throw _privateConstructorUsedError; + + /// The points that make up the line + List get points; @override - int get color => throw _privateConstructorUsedError; + + /// The color of the line in hexadecimal format (ARGB) + int get color; @override - double get width => throw _privateConstructorUsedError; + + /// The width of the line + double get width; @override @JsonKey(ignore: true) - _$$_SketchLineCopyWith<_$_SketchLine> get copyWith => + _$$SketchLineImplCopyWith<_$SketchLineImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/model/sketch/sketch_line/sketch_line.g.dart b/lib/src/domain/model/sketch_line/sketch_line.g.dart similarity index 79% rename from lib/src/model/sketch/sketch_line/sketch_line.g.dart rename to lib/src/domain/model/sketch_line/sketch_line.g.dart index 6a461c7..2e5dbb5 100644 --- a/lib/src/model/sketch/sketch_line/sketch_line.g.dart +++ b/lib/src/domain/model/sketch_line/sketch_line.g.dart @@ -6,8 +6,8 @@ part of 'sketch_line.dart'; // JsonSerializableGenerator // ************************************************************************** -_$_SketchLine _$$_SketchLineFromJson(Map json) => - _$_SketchLine( +_$SketchLineImpl _$$SketchLineImplFromJson(Map json) => + _$SketchLineImpl( points: (json['points'] as List) .map((e) => Point.fromJson(e as Map)) .toList(), @@ -15,7 +15,7 @@ _$_SketchLine _$$_SketchLineFromJson(Map json) => width: (json['width'] as num).toDouble(), ); -Map _$$_SketchLineToJson(_$_SketchLine instance) => +Map _$$SketchLineImplToJson(_$SketchLineImpl instance) => { 'points': instance.points.map((e) => e.toJson()).toList(), 'color': instance.color, diff --git a/lib/src/scribble_editing_painter.dart b/lib/src/scribble_editing_painter.dart deleted file mode 100644 index e26cd61..0000000 --- a/lib/src/scribble_editing_painter.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:scribble/scribble.dart'; -import 'package:scribble/src/scribble_painter.dart'; - -class ScribbleEditingPainter extends CustomPainter with SketchLinePainter { - ScribbleEditingPainter({ - required this.state, - required this.drawPointer, - required this.drawEraser, - }); - - final ScribbleState state; - final bool drawPointer; - final bool drawEraser; - - @override - void paint(Canvas canvas, Size size) { - Paint paint = Paint()..style = PaintingStyle.fill; - - final activeLine = state.map( - drawing: (s) => s.activeLine, - erasing: (_) => null, - ); - if (activeLine != null) { - final path = getPathForLine(activeLine, scaleFactor: state.scaleFactor); - if (path != null) { - paint.color = Color(activeLine.color); - canvas.drawPath(path, paint); - } - } - - if (state.pointerPosition != null && (state is Drawing && drawPointer || state is Erasing && drawEraser)) { - paint.style = state.map( - drawing: (_) => PaintingStyle.fill, - erasing: (_) => PaintingStyle.stroke, - ); - paint.color = state.map( - drawing: (s) => Color(s.selectedColor), - erasing: (s) => const Color(0xFF000000), - ); - paint.strokeWidth = 1; - canvas.drawCircle( - state.pointerPosition!.asOffset, - state.selectedWidth / state.scaleFactor, - paint, - ); - } - } - - @override - bool shouldRepaint(ScribbleEditingPainter oldDelegate) { - return oldDelegate.state != state; - } -} diff --git a/lib/src/scribble_painter.dart b/lib/src/scribble_painter.dart deleted file mode 100644 index 98d5e63..0000000 --- a/lib/src/scribble_painter.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/rendering.dart'; -import 'package:perfect_freehand/perfect_freehand.dart' as pf; -import 'package:scribble/scribble.dart'; - -class ScribblePainter extends CustomPainter with SketchLinePainter { - ScribblePainter({ - required this.sketch, - required this.scaleFactor, - }); - - final Sketch sketch; - final double scaleFactor; - - List get lines => sketch.lines; - - @override - void paint(Canvas canvas, Size size) { - Paint paint = Paint()..style = PaintingStyle.fill; - - for (int i = 0; i < lines.length; ++i) { - final path = getPathForLine(lines[i]); - if (path == null) { - continue; - } - paint.color = Color(lines[i].color); - canvas.drawPath(path, paint); - } - } - - @override - bool shouldRepaint(ScribblePainter oldDelegate) { - return oldDelegate.sketch != sketch || oldDelegate.scaleFactor != scaleFactor; - } -} - -mixin SketchLinePainter { - Path? getPathForLine(SketchLine line, {double scaleFactor = 1.0}) { - final simulatePressure = line.points.isNotEmpty && line.points.every((p) => p.pressure == line.points.first.pressure); - final points = line.points.map((point) => pf.Point(point.x, point.y, point.pressure)).toList(); - final outlinePoints = pf.getStroke( - points, - size: line.width * 2 * scaleFactor, - simulatePressure: simulatePressure, - ); - if (outlinePoints.isEmpty) { - return null; - } else if (outlinePoints.length < 2) { - return Path() - ..addOval(Rect.fromCircle( - center: Offset(outlinePoints[0].x, outlinePoints[0].y), - radius: 1, - )); - } else { - final path = Path(); - path.moveTo(outlinePoints[0].x, outlinePoints[0].y); - for (int i = 1; i < outlinePoints.length - 1; ++i) { - final p0 = outlinePoints[i]; - final p1 = outlinePoints[i + 1]; - path.quadraticBezierTo( - p0.x, - p0.y, - (p0.x + p1.x) / 2, - (p0.y + p1.y) / 2, - ); - } - return path; - } - } -} diff --git a/lib/src/scribble.notifier.dart b/lib/src/view/notifier/scribble_notifier.dart similarity index 62% rename from lib/src/scribble.notifier.dart rename to lib/src/view/notifier/scribble_notifier.dart index a9e220c..670fc51 100644 --- a/lib/src/scribble.notifier.dart +++ b/lib/src/view/notifier/scribble_notifier.dart @@ -1,32 +1,52 @@ -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; -import 'package:history_state_notifier/history_state_notifier.dart'; -import 'package:scribble/src/model/sketch/sketch.dart'; -import 'package:scribble/src/state/scribble.state.dart'; -import 'package:state_notifier/state_notifier.dart'; - -abstract class ScribbleNotifierBase extends StateNotifier { - ScribbleNotifierBase(ScribbleState state) : super(state); - - /// You need to provide a key that the [RepointBoundary] can use so you can +import 'package:scribble/scribble.dart'; +import 'package:scribble/src/view/painting/point_to_offset_x.dart'; +import 'package:value_notifier_tools/value_notifier_tools.dart'; + +/// {@template scribble_notifier_base} +/// The base class for a notifier that controls the state of a [Scribble] +/// widget. +/// +/// This class is meant to be extended by a concrete implementation that +/// provides the actual behavior. +/// +/// See [ScribbleNotifier] for the default implementation. +/// {@endtemplate} +abstract class ScribbleNotifierBase extends ValueNotifier { + /// {@macro scribble_notifier_base} + ScribbleNotifierBase(super.state); + + /// You need to provide a key that the [RepaintBoundary] can use so you can /// access it from the [renderImage] method. GlobalKey get repaintBoundaryKey; + /// Should be called when the pointer hovers over the canvas with the + /// corresponding [event]. void onPointerHover(PointerHoverEvent event); + /// Should be called when the pointer is pressed down on the canvas with the + /// corresponding [event]. void onPointerDown(PointerDownEvent event); + /// Should be called when the pointer is moved on the canvas with the + /// corresponding [event]. void onPointerUpdate(PointerMoveEvent event); + /// Should be called when the pointer is lifted from the canvas with the + /// corresponding [event]. void onPointerUp(PointerUpEvent event); + /// Should be called when the pointer is canceled with the corresponding + /// [event]. void onPointerCancel(PointerCancelEvent event); + /// Should be called when the pointer exits the canvas with the corresponding + /// [event]. void onPointerExit(PointerExitEvent event); /// Used to render the image to ByteData which can then be stored or reused @@ -39,21 +59,27 @@ abstract class ScribbleNotifierBase extends StateNotifier { double pixelRatio = 1.0, ui.ImageByteFormat format = ui.ImageByteFormat.png, }) async { - final RenderRepaintBoundary? renderObject = - repaintBoundaryKey.currentContext?.findRenderObject() - as RenderRepaintBoundary?; + final renderObject = repaintBoundaryKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; if (renderObject == null) { throw StateError( - "Tried to convert Scribble to Image, but no valid RenderObject was found!"); + "Tried to convert Scribble to Image, but no valid RenderObject was " + "found!", + ); } final img = await renderObject.toImage(pixelRatio: pixelRatio); return (await img.toByteData(format: format))!; } } +/// {@template scribble_notifier} +/// The default implementation of a [ScribbleNotifierBase]. +/// /// This class controls the state and behavior for a [Scribble] widget. +/// {@endtemplate} class ScribbleNotifier extends ScribbleNotifierBase - with HistoryStateNotifierMixin { + with HistoryValueNotifierMixin { + /// {@macro scribble_notifier} ScribbleNotifier({ /// If you pass a sketch here, the notifier will use that sketch as a /// starting point. @@ -80,7 +106,7 @@ class ScribbleNotifier extends ScribbleNotifierBase allowedPointersMode: allowedPointersMode, ), ) { - state = ScribbleState.drawing( + value = ScribbleState.drawing( sketch: sketch ?? const Sketch(lines: []), selectedWidth: widths[0], allowedPointersMode: allowedPointersMode, @@ -100,7 +126,7 @@ class ScribbleNotifier extends ScribbleNotifierBase /// /// If you want to store it somewhere you can call ``.toJson()`` on it to /// receive a map. - Sketch get currentSketch => state.sketch; + Sketch get currentSketch => value.sketch; final GlobalKey _repaintBoundaryKey = GlobalKey(); @@ -110,10 +136,12 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Only apply the sketch from the undo history, otherwise keep current state @override @protected - ScribbleState transformHistoryState( - ScribbleState historyState, ScribbleState currentState) { + ScribbleState transformHistoryValue( + ScribbleState historyValue, + ScribbleState currentState, + ) { return currentState.copyWith( - sketch: historyState.sketch, + sketch: historyValue.sketch, ); } @@ -123,19 +151,19 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Per default, this state of the sketch gets added to the undo history. If /// this is not desired, set [addToUndoHistory] to ``false``. void setSketch({required Sketch sketch, bool addToUndoHistory = true}) { - final newState = state.copyWith( + final newState = value.copyWith( sketch: sketch, ); if (addToUndoHistory) { - state = newState; + value = newState; } else { - temporaryState = newState; + temporaryValue = newState; } } /// Clear the entire drawing. void clear() { - state = state.map( + value = value.map( drawing: (s) => ScribbleState.drawing( sketch: const Sketch(lines: []), selectedColor: s.selectedColor, @@ -158,25 +186,26 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Sets the width of the next line void setStrokeWidth(double strokeWidth) { - temporaryState = state.copyWith( + temporaryValue = value.copyWith( selectedWidth: strokeWidth, ); } /// Switches to eraser mode void setEraser() { - temporaryState = ScribbleState.erasing( - sketch: state.sketch, - selectedWidth: state.selectedWidth, - scaleFactor: state.scaleFactor, - allowedPointersMode: state.allowedPointersMode, - activePointerIds: state.activePointerIds, + temporaryValue = ScribbleState.erasing( + sketch: value.sketch, + selectedWidth: value.selectedWidth, + scaleFactor: value.scaleFactor, + allowedPointersMode: value.allowedPointersMode, + activePointerIds: value.activePointerIds, ); } - /// Sets the current mode of allowed pointers to the given [ScribblePointerMode] + /// Sets the current mode of allowed pointers to the given + /// [ScribblePointerMode] void setAllowedPointersMode(ScribblePointerMode allowedPointersMode) { - temporaryState = state.copyWith( + temporaryValue = value.copyWith( allowedPointersMode: allowedPointersMode, ); } @@ -184,17 +213,17 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Sets the zoom factor to allow for adjusting line width. /// /// If the factor is 2 for example, lines will be drawn half as thick as - /// actually selected to allow for drawing details. + /// actually selected to allow for drawing details. Has to be greater than 0. void setScaleFactor(double factor) { - assert(factor >= 0); - temporaryState = state.copyWith( + assert(factor > 0, "The scale factor must be greater than 0."); + temporaryValue = value.copyWith( scaleFactor: factor, ); } /// Sets the color of the pen to the given color. void setColor(Color color) { - temporaryState = state.map( + temporaryValue = value.map( drawing: (s) => ScribbleState.drawing( sketch: s.sketch, selectedColor: color.value, @@ -206,8 +235,8 @@ class ScribbleNotifier extends ScribbleNotifierBase selectedColor: color.value, selectedWidth: s.selectedWidth, allowedPointersMode: s.allowedPointersMode, - scaleFactor: state.scaleFactor, - activePointerIds: state.activePointerIds, + scaleFactor: value.scaleFactor, + activePointerIds: value.activePointerIds, ), ); } @@ -215,8 +244,8 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Used by the Listener callback to display the pen if desired @override void onPointerHover(PointerHoverEvent event) { - if (!state.supportedPointerKinds.contains(event.kind)) return; - temporaryState = state.copyWith( + if (!value.supportedPointerKinds.contains(event.kind)) return; + temporaryValue = value.copyWith( pointerPosition: event.distance > 10000 ? null : _getPointFromEvent(event), ); @@ -225,51 +254,52 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Used by the Listener callback to start drawing @override void onPointerDown(PointerDownEvent event) { - if (!state.supportedPointerKinds.contains(event.kind)) return; - ScribbleState s = state; + if (!value.supportedPointerKinds.contains(event.kind)) return; + var s = value; // Are there already pointers on the screen? - if (state.activePointerIds.isNotEmpty) { - s = state.map( - drawing: (s) => - // If the current line already contains something - (s.activeLine != null && s.activeLine!.points.length > 2) - ? _finishLineForState(s) - : s.copyWith( - activeLine: null, - ), - erasing: (s) => s); - } else if (state is Drawing) { - s = (state as Drawing).copyWith( + if (value.activePointerIds.isNotEmpty) { + s = value.map( + drawing: (s) => + // If the current line already contains something + (s.activeLine != null && s.activeLine!.points.length > 2) + ? _finishLineForState(s) + : s.copyWith( + activeLine: null, + ), + erasing: (s) => s, + ); + } else if (value is Drawing) { + s = (value as Drawing).copyWith( pointerPosition: _getPointFromEvent(event), activeLine: SketchLine( points: [_getPointFromEvent(event)], - color: (state as Drawing).selectedColor, - width: state.selectedWidth / state.scaleFactor, + color: (value as Drawing).selectedColor, + width: value.selectedWidth / value.scaleFactor, ), ); } - temporaryState = s.copyWith( - activePointerIds: [...state.activePointerIds, event.pointer], + temporaryValue = s.copyWith( + activePointerIds: [...value.activePointerIds, event.pointer], ); } /// Used by the Listener callback to update the drawing @override void onPointerUpdate(PointerMoveEvent event) { - if (!state.supportedPointerKinds.contains(event.kind)) return; - if (!state.active) { - temporaryState = state.copyWith( + if (!value.supportedPointerKinds.contains(event.kind)) return; + if (!value.active) { + temporaryValue = value.copyWith( pointerPosition: null, ); return; } - if (state is Drawing) { - temporaryState = _addPoint(event, state).copyWith( + if (value is Drawing) { + temporaryValue = _addPoint(event, value).copyWith( pointerPosition: _getPointFromEvent(event), ); - } else if (state is Erasing) { - temporaryState = _erasePoint(event).copyWith( + } else if (value is Erasing) { + temporaryValue = _erasePoint(event).copyWith( pointerPosition: _getPointFromEvent(event), ); } @@ -278,20 +308,20 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Used by the Listener callback to finish a line @override void onPointerUp(PointerUpEvent event) { - if (!state.supportedPointerKinds.contains(event.kind)) return; + if (!value.supportedPointerKinds.contains(event.kind)) return; final pos = - event.kind == PointerDeviceKind.mouse ? state.pointerPosition : null; - if (state is Drawing) { - state = _finishLineForState(_addPoint(event, state)).copyWith( + event.kind == PointerDeviceKind.mouse ? value.pointerPosition : null; + if (value is Drawing) { + value = _finishLineForState(_addPoint(event, value)).copyWith( pointerPosition: pos, activePointerIds: - state.activePointerIds.where((id) => id != event.pointer).toList(), + value.activePointerIds.where((id) => id != event.pointer).toList(), ); - } else if (state is Erasing) { - state = _erasePoint(event).copyWith( + } else if (value is Erasing) { + value = _erasePoint(event).copyWith( pointerPosition: pos, activePointerIds: - state.activePointerIds.where((id) => id != event.pointer).toList(), + value.activePointerIds.where((id) => id != event.pointer).toList(), ); } } @@ -299,29 +329,29 @@ class ScribbleNotifier extends ScribbleNotifierBase /// Used by the Listener callback to stop displaying the cursor @override void onPointerCancel(PointerCancelEvent event) { - if (!state.supportedPointerKinds.contains(event.kind)) return; - if (state is Drawing) { - state = _finishLineForState(_addPoint(event, state)).copyWith( + if (!value.supportedPointerKinds.contains(event.kind)) return; + if (value is Drawing) { + value = _finishLineForState(_addPoint(event, value)).copyWith( pointerPosition: null, activePointerIds: - state.activePointerIds.where((id) => id != event.pointer).toList(), + value.activePointerIds.where((id) => id != event.pointer).toList(), ); - } else if (state is Erasing) { - state = _erasePoint(event).copyWith( + } else if (value is Erasing) { + value = _erasePoint(event).copyWith( pointerPosition: null, activePointerIds: - state.activePointerIds.where((id) => id != event.pointer).toList(), + value.activePointerIds.where((id) => id != event.pointer).toList(), ); } } @override void onPointerExit(PointerExitEvent event) { - if (!state.supportedPointerKinds.contains(event.kind)) return; - temporaryState = _finishLineForState(state).copyWith( + if (!value.supportedPointerKinds.contains(event.kind)) return; + temporaryValue = _finishLineForState(value).copyWith( pointerPosition: null, activePointerIds: - state.activePointerIds.where((id) => id != event.pointer).toList(), + value.activePointerIds.where((id) => id != event.pointer).toList(), ); } @@ -344,11 +374,15 @@ class ScribbleNotifier extends ScribbleNotifierBase } ScribbleState _erasePoint(PointerEvent event) { - return state.copyWith.sketch( - lines: state.sketch.lines - .where((l) => l.points.every((p) => - (event.localPosition - p.asOffset).distance > - l.width + state.selectedWidth)) + return value.copyWith.sketch( + lines: value.sketch.lines + .where( + (l) => l.points.every( + (p) => + (event.localPosition - p.asOffset).distance > + l.width + value.selectedWidth, + ), + ) .toList(), ); } diff --git a/lib/src/view/painting/point_to_offset_x.dart b/lib/src/view/painting/point_to_offset_x.dart new file mode 100644 index 0000000..8a45180 --- /dev/null +++ b/lib/src/view/painting/point_to_offset_x.dart @@ -0,0 +1,9 @@ +import 'dart:ui'; + +import 'package:scribble/src/domain/model/sketch/sketch.dart'; + +/// Extension on [Point] to convert it to an [Offset]. +extension PointToOffsetX on Point { + /// Converts a [Point] to a [Offset]. + Offset get asOffset => Offset(x, y); +} diff --git a/lib/src/view/painting/scribble_editing_painter.dart b/lib/src/view/painting/scribble_editing_painter.dart new file mode 100644 index 0000000..c586553 --- /dev/null +++ b/lib/src/view/painting/scribble_editing_painter.dart @@ -0,0 +1,72 @@ +import 'package:flutter/rendering.dart'; +import 'package:scribble/scribble.dart'; +import 'package:scribble/src/view/painting/point_to_offset_x.dart'; +import 'package:scribble/src/view/painting/sketch_line_path_mixin.dart'; + +/// {@template scribble_editing_painter} +/// A painter for drawing the currently active line of a scribble sketch, as +/// well as the pointer when in drawing or erasing mode, if desired. +/// {@endtemplate} +class ScribbleEditingPainter extends CustomPainter with SketchLinePathMixin { + /// {@macro scribble_editing_painter} + ScribbleEditingPainter({ + required this.state, + required this.drawPointer, + required this.drawEraser, + }); + + /// The current state of the scribble sketch + final ScribbleState state; + + /// Whether to draw the pointer when in drawing mode. + /// + /// The pointer will be drawn as a filled circle with the currently selected + /// color. + final bool drawPointer; + + /// Whether to draw the pointer when in erasing mode + /// + /// The pointer will be drawn as a transparent circle with a black border. + final bool drawEraser; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..style = PaintingStyle.fill; + + final activeLine = state.map( + drawing: (s) => s.activeLine, + erasing: (_) => null, + ); + if (activeLine != null) { + final path = getPathForLine(activeLine, scaleFactor: state.scaleFactor); + if (path != null) { + paint.color = Color(activeLine.color); + canvas.drawPath(path, paint); + } + } + + if (state.pointerPosition != null && + (state is Drawing && drawPointer || state is Erasing && drawEraser)) { + paint + ..style = state.map( + drawing: (_) => PaintingStyle.fill, + erasing: (_) => PaintingStyle.stroke, + ) + ..color = state.map( + drawing: (s) => Color(s.selectedColor), + erasing: (s) => const Color(0xFF000000), + ) + ..strokeWidth = 1; + canvas.drawCircle( + state.pointerPosition!.asOffset, + state.selectedWidth / state.scaleFactor, + paint, + ); + } + } + + @override + bool shouldRepaint(ScribbleEditingPainter oldDelegate) { + return oldDelegate.state != state; + } +} diff --git a/lib/src/view/painting/scribble_painter.dart b/lib/src/view/painting/scribble_painter.dart new file mode 100644 index 0000000..4b0d210 --- /dev/null +++ b/lib/src/view/painting/scribble_painter.dart @@ -0,0 +1,38 @@ +import 'package:flutter/rendering.dart'; +import 'package:scribble/scribble.dart'; +import 'package:scribble/src/view/painting/sketch_line_path_mixin.dart'; + +/// A painter for drawing a scribble sketch. +class ScribblePainter extends CustomPainter with SketchLinePathMixin { + /// Creates a new [ScribblePainter] instance. + ScribblePainter({ + required this.sketch, + required this.scaleFactor, + }); + + /// The [Sketch] to draw. + final Sketch sketch; + + /// {@macro view.state.scribble_state.scale_factor} + final double scaleFactor; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..style = PaintingStyle.fill; + + for (var i = 0; i < sketch.lines.length; ++i) { + final path = getPathForLine(sketch.lines[i], scaleFactor: scaleFactor); + if (path == null) { + continue; + } + paint.color = Color(sketch.lines[i].color); + canvas.drawPath(path, paint); + } + } + + @override + bool shouldRepaint(ScribblePainter oldDelegate) { + return oldDelegate.sketch != sketch || + oldDelegate.scaleFactor != scaleFactor; + } +} diff --git a/lib/src/view/painting/sketch_line_path_mixin.dart b/lib/src/view/painting/sketch_line_path_mixin.dart new file mode 100644 index 0000000..72cc648 --- /dev/null +++ b/lib/src/view/painting/sketch_line_path_mixin.dart @@ -0,0 +1,56 @@ +import 'dart:ui'; + +import 'package:perfect_freehand/perfect_freehand.dart' as pf; +import 'package:scribble/src/domain/model/sketch/sketch.dart'; + +/// A mixin for generating a [Path] from a [SketchLine]. +/// +/// Provides the method [getPathForLine] which generates a smooth [Path] from a +/// [SketchLine]. +mixin SketchLinePathMixin { + /// Generates a [Path] from a [SketchLine]. + /// + /// The [scaleFactor] is used to scale the line width. + Path? getPathForLine( + SketchLine line, { + double scaleFactor = 1.0, + }) { + final simulatePressure = line.points.isNotEmpty && + line.points.every((p) => p.pressure == line.points.first.pressure); + final points = line.points + .map((point) => pf.PointVector(point.x, point.y, point.pressure)) + .toList(); + final outlinePoints = pf.getStroke( + points, + options: pf.StrokeOptions( + size: line.width * 2 * scaleFactor, + simulatePressure: simulatePressure, + ), + ); + if (outlinePoints.isEmpty) { + return null; + } else if (outlinePoints.length < 2) { + return Path() + ..addOval( + Rect.fromCircle( + center: Offset(outlinePoints[0].dx, outlinePoints[0].dy), + radius: 1, + ), + ); + } else { + final path = Path()..moveTo(outlinePoints[0].dx, outlinePoints[0].dy); + + for (var i = 1; i < outlinePoints.length - 1; i++) { + final p0 = outlinePoints[i]; + final p1 = outlinePoints[i + 1]; + path.quadraticBezierTo( + p0.dx, + p0.dy, + (p0.dx + p1.dx) / 2, + (p0.dy + p1.dy) / 2, + ); + } + return path; + } + } +} diff --git a/lib/src/view/pan_gesture_catcher.dart b/lib/src/view/pan_gesture_catcher.dart new file mode 100644 index 0000000..3aa4b63 --- /dev/null +++ b/lib/src/view/pan_gesture_catcher.dart @@ -0,0 +1,60 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +/// {@template gesture_catcher} +/// A widget that catches gestures for a given set of pointer kinds. +/// +/// Any gestures of pointers contained in [pointerKindsToCatch] will be caught +/// by this widget and not be passed to any other widgets in the widget tree. +/// {@endtemplate} +class GestureCatcher extends StatelessWidget { + /// {@macro gesture_catcher} + const GestureCatcher({ + required this.pointerKindsToCatch, + required this.child, + super.key, + }); + + /// The pointer kinds to catch. + final Set pointerKindsToCatch; + + /// The child widget. + final Widget child; + + @override + Widget build(BuildContext context) { + return RawGestureDetector( + key: ValueKey(pointerKindsToCatch), + gestures: { + _GestureCatcherRecognizer: + GestureRecognizerFactoryWithHandlers<_GestureCatcherRecognizer>( + () => _GestureCatcherRecognizer( + debugOwner: this, + pointerKindsToCatch: pointerKindsToCatch, + ), + (_GestureCatcherRecognizer instance) {}, + ), + }, + child: child, + ); + } +} + +class _GestureCatcherRecognizer extends OneSequenceGestureRecognizer { + /// Create a gesture recognizer for tracking movement on a plane. + _GestureCatcherRecognizer({ + required Set pointerKindsToCatch, + super.debugOwner, + }) : super(supportedDevices: pointerKindsToCatch); + + @override + String get debugDescription => 'pan catcher'; + + @override + void didStopTrackingLastPointer(int pointer) {} + + @override + void handleEvent(PointerEvent event) { + resolve(GestureDisposition.accepted); + } +} diff --git a/lib/src/scribble.dart b/lib/src/view/scribble.dart similarity index 59% rename from lib/src/scribble.dart rename to lib/src/view/scribble.dart index 8271f35..12a8d8c 100644 --- a/lib/src/scribble.dart +++ b/lib/src/view/scribble.dart @@ -2,19 +2,20 @@ import 'dart:ui'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_state_notifier/flutter_state_notifier.dart'; -import 'package:scribble/src/scribble.notifier.dart'; -import 'package:scribble/src/scribble_editing_painter.dart'; -import 'package:scribble/src/scribble_painter.dart'; -import 'package:scribble/src/state/scribble.state.dart'; - -import 'core/pan_gesture_catcher.dart'; +import 'package:scribble/src/view/notifier/scribble_notifier.dart'; +import 'package:scribble/src/view/painting/scribble_editing_painter.dart'; +import 'package:scribble/src/view/painting/scribble_painter.dart'; +import 'package:scribble/src/view/pan_gesture_catcher.dart'; +import 'package:scribble/src/view/state/scribble.state.dart'; +/// {@template scribble} /// This Widget represents a canvas on which users can draw with any pointer. /// /// You can control its behavior from code using the [notifier] instance you /// pass in. -class Scribble extends StatefulWidget { +/// {@endtemplate} +class Scribble extends StatelessWidget { + /// {@macro scribble} const Scribble({ /// The notifier that controls this canvas. required this.notifier, @@ -24,8 +25,8 @@ class Scribble extends StatefulWidget { /// Whether to draw the pointer when in erasing mode this.drawEraser = true, - Key? key, - }) : super(key: key); + super.key, + }); /// The notifier that controls this canvas. final ScribbleNotifierBase notifier; @@ -35,28 +36,22 @@ class Scribble extends StatefulWidget { /// Whether to draw the pointer when in erasing mode final bool drawEraser; - - @override - State createState() => _ScribbleState(); -} - -class _ScribbleState extends State { @override Widget build(BuildContext context) { - return StateNotifierBuilder( - stateNotifier: widget.notifier, + return ValueListenableBuilder( + valueListenable: notifier, builder: (context, state, _) { - final drawCurrentTool = widget.drawPen && state is Drawing || - widget.drawEraser && state is Erasing; + final drawCurrentTool = + drawPen && state is Drawing || drawEraser && state is Erasing; final child = SizedBox.expand( child: CustomPaint( foregroundPainter: ScribbleEditingPainter( state: state, - drawPointer: widget.drawPen, - drawEraser: widget.drawEraser, + drawPointer: drawPen, + drawEraser: drawEraser, ), child: RepaintBoundary( - key: widget.notifier.repaintBoundaryKey, + key: notifier.repaintBoundaryKey, child: CustomPaint( painter: ScribblePainter( sketch: state.sketch, @@ -76,13 +71,13 @@ class _ScribbleState extends State { .contains(PointerDeviceKind.mouse) ? SystemMouseCursors.none : MouseCursor.defer, - onExit: widget.notifier.onPointerExit, + onExit: notifier.onPointerExit, child: Listener( - onPointerDown: widget.notifier.onPointerDown, - onPointerMove: widget.notifier.onPointerUpdate, - onPointerUp: widget.notifier.onPointerUp, - onPointerHover: widget.notifier.onPointerHover, - onPointerCancel: widget.notifier.onPointerCancel, + onPointerDown: notifier.onPointerDown, + onPointerMove: notifier.onPointerUpdate, + onPointerUp: notifier.onPointerUp, + onPointerHover: notifier.onPointerHover, + onPointerCancel: notifier.onPointerCancel, child: child, ), ), diff --git a/lib/src/scribble_sketch.dart b/lib/src/view/scribble_sketch.dart similarity index 65% rename from lib/src/scribble_sketch.dart rename to lib/src/view/scribble_sketch.dart index 5ccc7bc..14a1f22 100644 --- a/lib/src/scribble_sketch.dart +++ b/lib/src/view/scribble_sketch.dart @@ -1,14 +1,20 @@ import 'package:flutter/widgets.dart'; -import 'package:scribble/scribble.dart'; -import 'package:scribble/src/scribble_painter.dart'; +import 'package:scribble/src/domain/model/sketch/sketch.dart'; +import 'package:scribble/src/view/painting/scribble_painter.dart'; +/// {@template scribble_sketch} /// A widget for displaying a scribble sketch without any input functionalities. +/// +/// The sketch is expected to not have any active line, i.e. all lines are +/// considered finished, the sketch is complete. +/// {@endtemplate} class ScribbleSketch extends StatelessWidget { + /// {@macro scribble_sketch} const ScribbleSketch({ - Key? key, required this.sketch, this.scaleFactor = 1, - }) : super(key: key); + super.key, + }); /// The sketch to display final Sketch sketch; diff --git a/lib/src/state/scribble.state.dart b/lib/src/view/state/scribble.state.dart similarity index 68% rename from lib/src/state/scribble.state.dart rename to lib/src/view/state/scribble.state.dart index 1bc43a5..eb93039 100644 --- a/lib/src/state/scribble.state.dart +++ b/lib/src/view/state/scribble.state.dart @@ -1,23 +1,36 @@ import 'dart:ui'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:scribble/src/model/sketch/sketch.dart'; +import 'package:scribble/src/domain/model/sketch/sketch.dart'; part 'scribble.state.freezed.dart'; part 'scribble.state.g.dart'; +/// Which pointers are allowed for drawing and will be captured by the scribble +/// widget. enum ScribblePointerMode { + /// Allow drawing with all pointers. all, + + /// Allow drawing with mouse only. mouseOnly, + + /// Allow drawing with pen only. + /// + /// This is useful if you want to place the scribble widget in an + /// `InteractiveViewer` for example, so that it can be zoomed in and out + /// without drawing on it. penOnly, + + /// Allow drawing with both mouse and pen. mouseAndPen, } +/// Represents the state of the scribble widget. @freezed -class ScribbleState with _$ScribbleState { - const ScribbleState._(); - +sealed class ScribbleState with _$ScribbleState { + /// The state of the scribble widget when it is being drawn on. const factory ScribbleState.drawing({ /// The current state of the sketch required Sketch sketch, @@ -42,13 +55,16 @@ class ScribbleState with _$ScribbleState { /// The current width of the pen @Default(5) double selectedWidth, + /// {@template view.state.scribble_state.scale_factor} /// How much the widget is scaled at the moment. /// /// Can be used if zoom functionality is needed /// (e.g. through InteractiveViewer) so that the pen width remains the same. + /// {@endtemplate} @Default(1) double scaleFactor, }) = Drawing; + /// The state of the scribble widget when the user is currently erasing. const factory ScribbleState.erasing({ /// The current state of the sketch required Sketch sketch, @@ -74,16 +90,26 @@ class ScribbleState with _$ScribbleState { @Default(1) double scaleFactor, }) = Erasing; + /// Constructs a [ScribbleState] from a JSON object. + factory ScribbleState.fromJson(Map json) => + _$ScribbleStateFromJson(json); + const ScribbleState._(); + + /// Returns whether the widget is currently active, meaning that only one + /// pointer is interacting with the widget. bool get active => activePointerIds.length <= 1; + /// Returns the list of lines that should be drawn on the canvas by + /// combining the sketches lines with the current active line if it exists. List get lines => map( - drawing: (d) => d.activeLine == null - ? sketch.lines - : [...sketch.lines, d.activeLine!], - erasing: (d) => d.sketch.lines); + drawing: (d) => d.activeLine == null + ? sketch.lines + : [...sketch.lines, d.activeLine!], + erasing: (d) => d.sketch.lines, + ); /// Returns a set of [PointerDeviceKind] that represents the currently - /// supported devices, depending on [state.allowedPointersMode]. + /// supported devices, depending on [ScribbleState.allowedPointersMode]. Set get supportedPointerKinds { switch (allowedPointersMode) { case ScribblePointerMode.all: @@ -103,7 +129,4 @@ class ScribbleState with _$ScribbleState { }; } } - - factory ScribbleState.fromJson(Map json) => - _$ScribbleStateFromJson(json); } diff --git a/lib/src/state/scribble.state.freezed.dart b/lib/src/view/state/scribble.state.freezed.dart similarity index 76% rename from lib/src/state/scribble.state.freezed.dart rename to lib/src/view/state/scribble.state.freezed.dart index 32be1ef..83d7937 100644 --- a/lib/src/state/scribble.state.freezed.dart +++ b/lib/src/view/state/scribble.state.freezed.dart @@ -1,7 +1,7 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'scribble.state.dart'; @@ -12,7 +12,7 @@ part of 'scribble.state.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); ScribbleState _$ScribbleStateFromJson(Map json) { switch (json['runtimeType']) { @@ -47,10 +47,12 @@ mixin _$ScribbleState { /// The current width of the pen double get selectedWidth => throw _privateConstructorUsedError; + /// {@template view.state.scribble_state.scale_factor} /// How much the widget is scaled at the moment. /// /// Can be used if zoom functionality is needed /// (e.g. through InteractiveViewer) so that the pen width remains the same. + /// {@endtemplate} double get scaleFactor => throw _privateConstructorUsedError; @optionalTypeArgs TResult when({ @@ -76,7 +78,7 @@ mixin _$ScribbleState { throw _privateConstructorUsedError; @optionalTypeArgs TResult? whenOrNull({ - TResult Function( + TResult? Function( Sketch sketch, SketchLine? activeLine, ScribblePointerMode allowedPointersMode, @@ -86,7 +88,7 @@ mixin _$ScribbleState { double selectedWidth, double scaleFactor)? drawing, - TResult Function( + TResult? Function( Sketch sketch, ScribblePointerMode allowedPointersMode, List activePointerIds, @@ -127,8 +129,8 @@ mixin _$ScribbleState { throw _privateConstructorUsedError; @optionalTypeArgs TResult? mapOrNull({ - TResult Function(Drawing value)? drawing, - TResult Function(Erasing value)? erasing, + TResult? Function(Drawing value)? drawing, + TResult? Function(Erasing value)? erasing, }) => throw _privateConstructorUsedError; @optionalTypeArgs @@ -148,7 +150,8 @@ mixin _$ScribbleState { abstract class $ScribbleStateCopyWith<$Res> { factory $ScribbleStateCopyWith( ScribbleState value, $Res Function(ScribbleState) then) = - _$ScribbleStateCopyWithImpl<$Res>; + _$ScribbleStateCopyWithImpl<$Res, ScribbleState>; + @useResult $Res call( {Sketch sketch, ScribblePointerMode allowedPointersMode, @@ -162,76 +165,82 @@ abstract class $ScribbleStateCopyWith<$Res> { } /// @nodoc -class _$ScribbleStateCopyWithImpl<$Res> +class _$ScribbleStateCopyWithImpl<$Res, $Val extends ScribbleState> implements $ScribbleStateCopyWith<$Res> { _$ScribbleStateCopyWithImpl(this._value, this._then); - final ScribbleState _value; // ignore: unused_field - final $Res Function(ScribbleState) _then; + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + @pragma('vm:prefer-inline') @override $Res call({ - Object? sketch = freezed, - Object? allowedPointersMode = freezed, - Object? activePointerIds = freezed, + Object? sketch = null, + Object? allowedPointersMode = null, + Object? activePointerIds = null, Object? pointerPosition = freezed, - Object? selectedWidth = freezed, - Object? scaleFactor = freezed, + Object? selectedWidth = null, + Object? scaleFactor = null, }) { return _then(_value.copyWith( - sketch: sketch == freezed + sketch: null == sketch ? _value.sketch : sketch // ignore: cast_nullable_to_non_nullable as Sketch, - allowedPointersMode: allowedPointersMode == freezed + allowedPointersMode: null == allowedPointersMode ? _value.allowedPointersMode : allowedPointersMode // ignore: cast_nullable_to_non_nullable as ScribblePointerMode, - activePointerIds: activePointerIds == freezed + activePointerIds: null == activePointerIds ? _value.activePointerIds : activePointerIds // ignore: cast_nullable_to_non_nullable as List, - pointerPosition: pointerPosition == freezed + pointerPosition: freezed == pointerPosition ? _value.pointerPosition : pointerPosition // ignore: cast_nullable_to_non_nullable as Point?, - selectedWidth: selectedWidth == freezed + selectedWidth: null == selectedWidth ? _value.selectedWidth : selectedWidth // ignore: cast_nullable_to_non_nullable as double, - scaleFactor: scaleFactor == freezed + scaleFactor: null == scaleFactor ? _value.scaleFactor : scaleFactor // ignore: cast_nullable_to_non_nullable as double, - )); + ) as $Val); } @override + @pragma('vm:prefer-inline') $SketchCopyWith<$Res> get sketch { return $SketchCopyWith<$Res>(_value.sketch, (value) { - return _then(_value.copyWith(sketch: value)); + return _then(_value.copyWith(sketch: value) as $Val); }); } @override + @pragma('vm:prefer-inline') $PointCopyWith<$Res>? get pointerPosition { if (_value.pointerPosition == null) { return null; } return $PointCopyWith<$Res>(_value.pointerPosition!, (value) { - return _then(_value.copyWith(pointerPosition: value)); + return _then(_value.copyWith(pointerPosition: value) as $Val); }); } } /// @nodoc -abstract class _$$DrawingCopyWith<$Res> +abstract class _$$DrawingImplCopyWith<$Res> implements $ScribbleStateCopyWith<$Res> { - factory _$$DrawingCopyWith(_$Drawing value, $Res Function(_$Drawing) then) = - __$$DrawingCopyWithImpl<$Res>; + factory _$$DrawingImplCopyWith( + _$DrawingImpl value, $Res Function(_$DrawingImpl) then) = + __$$DrawingImplCopyWithImpl<$Res>; @override + @useResult $Res call( {Sketch sketch, SketchLine? activeLine, @@ -250,55 +259,55 @@ abstract class _$$DrawingCopyWith<$Res> } /// @nodoc -class __$$DrawingCopyWithImpl<$Res> extends _$ScribbleStateCopyWithImpl<$Res> - implements _$$DrawingCopyWith<$Res> { - __$$DrawingCopyWithImpl(_$Drawing _value, $Res Function(_$Drawing) _then) - : super(_value, (v) => _then(v as _$Drawing)); - - @override - _$Drawing get _value => super._value as _$Drawing; +class __$$DrawingImplCopyWithImpl<$Res> + extends _$ScribbleStateCopyWithImpl<$Res, _$DrawingImpl> + implements _$$DrawingImplCopyWith<$Res> { + __$$DrawingImplCopyWithImpl( + _$DrawingImpl _value, $Res Function(_$DrawingImpl) _then) + : super(_value, _then); + @pragma('vm:prefer-inline') @override $Res call({ - Object? sketch = freezed, + Object? sketch = null, Object? activeLine = freezed, - Object? allowedPointersMode = freezed, - Object? activePointerIds = freezed, + Object? allowedPointersMode = null, + Object? activePointerIds = null, Object? pointerPosition = freezed, - Object? selectedColor = freezed, - Object? selectedWidth = freezed, - Object? scaleFactor = freezed, + Object? selectedColor = null, + Object? selectedWidth = null, + Object? scaleFactor = null, }) { - return _then(_$Drawing( - sketch: sketch == freezed + return _then(_$DrawingImpl( + sketch: null == sketch ? _value.sketch : sketch // ignore: cast_nullable_to_non_nullable as Sketch, - activeLine: activeLine == freezed + activeLine: freezed == activeLine ? _value.activeLine : activeLine // ignore: cast_nullable_to_non_nullable as SketchLine?, - allowedPointersMode: allowedPointersMode == freezed + allowedPointersMode: null == allowedPointersMode ? _value.allowedPointersMode : allowedPointersMode // ignore: cast_nullable_to_non_nullable as ScribblePointerMode, - activePointerIds: activePointerIds == freezed + activePointerIds: null == activePointerIds ? _value._activePointerIds : activePointerIds // ignore: cast_nullable_to_non_nullable as List, - pointerPosition: pointerPosition == freezed + pointerPosition: freezed == pointerPosition ? _value.pointerPosition : pointerPosition // ignore: cast_nullable_to_non_nullable as Point?, - selectedColor: selectedColor == freezed + selectedColor: null == selectedColor ? _value.selectedColor : selectedColor // ignore: cast_nullable_to_non_nullable as int, - selectedWidth: selectedWidth == freezed + selectedWidth: null == selectedWidth ? _value.selectedWidth : selectedWidth // ignore: cast_nullable_to_non_nullable as double, - scaleFactor: scaleFactor == freezed + scaleFactor: null == scaleFactor ? _value.scaleFactor : scaleFactor // ignore: cast_nullable_to_non_nullable as double, @@ -306,6 +315,7 @@ class __$$DrawingCopyWithImpl<$Res> extends _$ScribbleStateCopyWithImpl<$Res> } @override + @pragma('vm:prefer-inline') $SketchLineCopyWith<$Res>? get activeLine { if (_value.activeLine == null) { return null; @@ -319,8 +329,8 @@ class __$$DrawingCopyWithImpl<$Res> extends _$ScribbleStateCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$Drawing extends Drawing { - const _$Drawing( +class _$DrawingImpl extends Drawing { + const _$DrawingImpl( {required this.sketch, this.activeLine, this.allowedPointersMode = ScribblePointerMode.all, @@ -334,8 +344,8 @@ class _$Drawing extends Drawing { $type = $type ?? 'drawing', super._(); - factory _$Drawing.fromJson(Map json) => - _$$DrawingFromJson(json); + factory _$DrawingImpl.fromJson(Map json) => + _$$DrawingImplFromJson(json); /// The current state of the sketch @override @@ -360,6 +370,8 @@ class _$Drawing extends Drawing { @override @JsonKey() List get activePointerIds { + if (_activePointerIds is EqualUnmodifiableListView) + return _activePointerIds; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_activePointerIds); } @@ -378,10 +390,12 @@ class _$Drawing extends Drawing { @JsonKey() final double selectedWidth; + /// {@template view.state.scribble_state.scale_factor} /// How much the widget is scaled at the moment. /// /// Can be used if zoom functionality is needed /// (e.g. through InteractiveViewer) so that the pen width remains the same. + /// {@endtemplate} @override @JsonKey() final double scaleFactor; @@ -395,44 +409,45 @@ class _$Drawing extends Drawing { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$Drawing && - const DeepCollectionEquality().equals(other.sketch, sketch) && - const DeepCollectionEquality() - .equals(other.activeLine, activeLine) && - const DeepCollectionEquality() - .equals(other.allowedPointersMode, allowedPointersMode) && + other is _$DrawingImpl && + (identical(other.sketch, sketch) || other.sketch == sketch) && + (identical(other.activeLine, activeLine) || + other.activeLine == activeLine) && + (identical(other.allowedPointersMode, allowedPointersMode) || + other.allowedPointersMode == allowedPointersMode) && const DeepCollectionEquality() .equals(other._activePointerIds, _activePointerIds) && - const DeepCollectionEquality() - .equals(other.pointerPosition, pointerPosition) && - const DeepCollectionEquality() - .equals(other.selectedColor, selectedColor) && - const DeepCollectionEquality() - .equals(other.selectedWidth, selectedWidth) && - const DeepCollectionEquality() - .equals(other.scaleFactor, scaleFactor)); + (identical(other.pointerPosition, pointerPosition) || + other.pointerPosition == pointerPosition) && + (identical(other.selectedColor, selectedColor) || + other.selectedColor == selectedColor) && + (identical(other.selectedWidth, selectedWidth) || + other.selectedWidth == selectedWidth) && + (identical(other.scaleFactor, scaleFactor) || + other.scaleFactor == scaleFactor)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, - const DeepCollectionEquality().hash(sketch), - const DeepCollectionEquality().hash(activeLine), - const DeepCollectionEquality().hash(allowedPointersMode), + sketch, + activeLine, + allowedPointersMode, const DeepCollectionEquality().hash(_activePointerIds), - const DeepCollectionEquality().hash(pointerPosition), - const DeepCollectionEquality().hash(selectedColor), - const DeepCollectionEquality().hash(selectedWidth), - const DeepCollectionEquality().hash(scaleFactor)); + pointerPosition, + selectedColor, + selectedWidth, + scaleFactor); @JsonKey(ignore: true) @override - _$$DrawingCopyWith<_$Drawing> get copyWith => - __$$DrawingCopyWithImpl<_$Drawing>(this, _$identity); + @pragma('vm:prefer-inline') + _$$DrawingImplCopyWith<_$DrawingImpl> get copyWith => + __$$DrawingImplCopyWithImpl<_$DrawingImpl>(this, _$identity); @override @optionalTypeArgs @@ -463,7 +478,7 @@ class _$Drawing extends Drawing { @override @optionalTypeArgs TResult? whenOrNull({ - TResult Function( + TResult? Function( Sketch sketch, SketchLine? activeLine, ScribblePointerMode allowedPointersMode, @@ -473,7 +488,7 @@ class _$Drawing extends Drawing { double selectedWidth, double scaleFactor)? drawing, - TResult Function( + TResult? Function( Sketch sketch, ScribblePointerMode allowedPointersMode, List activePointerIds, @@ -535,8 +550,8 @@ class _$Drawing extends Drawing { @override @optionalTypeArgs TResult? mapOrNull({ - TResult Function(Drawing value)? drawing, - TResult Function(Erasing value)? erasing, + TResult? Function(Drawing value)? drawing, + TResult? Function(Erasing value)? erasing, }) { return drawing?.call(this); } @@ -556,7 +571,9 @@ class _$Drawing extends Drawing { @override Map toJson() { - return _$$DrawingToJson(this); + return _$$DrawingImplToJson( + this, + ); } } @@ -569,59 +586,62 @@ abstract class Drawing extends ScribbleState { final Point? pointerPosition, final int selectedColor, final double selectedWidth, - final double scaleFactor}) = _$Drawing; + final double scaleFactor}) = _$DrawingImpl; const Drawing._() : super._(); - factory Drawing.fromJson(Map json) = _$Drawing.fromJson; + factory Drawing.fromJson(Map json) = _$DrawingImpl.fromJson; @override /// The current state of the sketch - Sketch get sketch => throw _privateConstructorUsedError; + Sketch get sketch; /// The line that is currently being drawn - SketchLine? get activeLine => throw _privateConstructorUsedError; + SketchLine? get activeLine; @override /// Which pointers are allowed for drawing and will be captured by the /// scribble widget. - ScribblePointerMode get allowedPointersMode => - throw _privateConstructorUsedError; + ScribblePointerMode get allowedPointersMode; @override /// The ids of all supported pointers that are currently interacting with /// the widget. - List get activePointerIds => throw _privateConstructorUsedError; + List get activePointerIds; @override /// The current position of the pointer - Point? get pointerPosition => throw _privateConstructorUsedError; + Point? get pointerPosition; /// The color that is currently being drawn with - int get selectedColor => throw _privateConstructorUsedError; + int get selectedColor; @override /// The current width of the pen - double get selectedWidth => throw _privateConstructorUsedError; + double get selectedWidth; @override + /// {@template view.state.scribble_state.scale_factor} /// How much the widget is scaled at the moment. /// /// Can be used if zoom functionality is needed /// (e.g. through InteractiveViewer) so that the pen width remains the same. - double get scaleFactor => throw _privateConstructorUsedError; + /// {@endtemplate} + double get scaleFactor; @override @JsonKey(ignore: true) - _$$DrawingCopyWith<_$Drawing> get copyWith => + _$$DrawingImplCopyWith<_$DrawingImpl> get copyWith => throw _privateConstructorUsedError; } /// @nodoc -abstract class _$$ErasingCopyWith<$Res> +abstract class _$$ErasingImplCopyWith<$Res> implements $ScribbleStateCopyWith<$Res> { - factory _$$ErasingCopyWith(_$Erasing value, $Res Function(_$Erasing) then) = - __$$ErasingCopyWithImpl<$Res>; + factory _$$ErasingImplCopyWith( + _$ErasingImpl value, $Res Function(_$ErasingImpl) then) = + __$$ErasingImplCopyWithImpl<$Res>; @override + @useResult $Res call( {Sketch sketch, ScribblePointerMode allowedPointersMode, @@ -637,45 +657,45 @@ abstract class _$$ErasingCopyWith<$Res> } /// @nodoc -class __$$ErasingCopyWithImpl<$Res> extends _$ScribbleStateCopyWithImpl<$Res> - implements _$$ErasingCopyWith<$Res> { - __$$ErasingCopyWithImpl(_$Erasing _value, $Res Function(_$Erasing) _then) - : super(_value, (v) => _then(v as _$Erasing)); - - @override - _$Erasing get _value => super._value as _$Erasing; +class __$$ErasingImplCopyWithImpl<$Res> + extends _$ScribbleStateCopyWithImpl<$Res, _$ErasingImpl> + implements _$$ErasingImplCopyWith<$Res> { + __$$ErasingImplCopyWithImpl( + _$ErasingImpl _value, $Res Function(_$ErasingImpl) _then) + : super(_value, _then); + @pragma('vm:prefer-inline') @override $Res call({ - Object? sketch = freezed, - Object? allowedPointersMode = freezed, - Object? activePointerIds = freezed, + Object? sketch = null, + Object? allowedPointersMode = null, + Object? activePointerIds = null, Object? pointerPosition = freezed, - Object? selectedWidth = freezed, - Object? scaleFactor = freezed, + Object? selectedWidth = null, + Object? scaleFactor = null, }) { - return _then(_$Erasing( - sketch: sketch == freezed + return _then(_$ErasingImpl( + sketch: null == sketch ? _value.sketch : sketch // ignore: cast_nullable_to_non_nullable as Sketch, - allowedPointersMode: allowedPointersMode == freezed + allowedPointersMode: null == allowedPointersMode ? _value.allowedPointersMode : allowedPointersMode // ignore: cast_nullable_to_non_nullable as ScribblePointerMode, - activePointerIds: activePointerIds == freezed + activePointerIds: null == activePointerIds ? _value._activePointerIds : activePointerIds // ignore: cast_nullable_to_non_nullable as List, - pointerPosition: pointerPosition == freezed + pointerPosition: freezed == pointerPosition ? _value.pointerPosition : pointerPosition // ignore: cast_nullable_to_non_nullable as Point?, - selectedWidth: selectedWidth == freezed + selectedWidth: null == selectedWidth ? _value.selectedWidth : selectedWidth // ignore: cast_nullable_to_non_nullable as double, - scaleFactor: scaleFactor == freezed + scaleFactor: null == scaleFactor ? _value.scaleFactor : scaleFactor // ignore: cast_nullable_to_non_nullable as double, @@ -685,8 +705,8 @@ class __$$ErasingCopyWithImpl<$Res> extends _$ScribbleStateCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$Erasing extends Erasing { - const _$Erasing( +class _$ErasingImpl extends Erasing { + const _$ErasingImpl( {required this.sketch, this.allowedPointersMode = ScribblePointerMode.all, final List activePointerIds = const [], @@ -698,8 +718,8 @@ class _$Erasing extends Erasing { $type = $type ?? 'erasing', super._(); - factory _$Erasing.fromJson(Map json) => - _$$ErasingFromJson(json); + factory _$ErasingImpl.fromJson(Map json) => + _$$ErasingImplFromJson(json); /// The current state of the sketch @override @@ -720,6 +740,8 @@ class _$Erasing extends Erasing { @override @JsonKey() List get activePointerIds { + if (_activePointerIds is EqualUnmodifiableListView) + return _activePointerIds; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_activePointerIds); } @@ -750,38 +772,39 @@ class _$Erasing extends Erasing { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && - other is _$Erasing && - const DeepCollectionEquality().equals(other.sketch, sketch) && - const DeepCollectionEquality() - .equals(other.allowedPointersMode, allowedPointersMode) && + other is _$ErasingImpl && + (identical(other.sketch, sketch) || other.sketch == sketch) && + (identical(other.allowedPointersMode, allowedPointersMode) || + other.allowedPointersMode == allowedPointersMode) && const DeepCollectionEquality() .equals(other._activePointerIds, _activePointerIds) && - const DeepCollectionEquality() - .equals(other.pointerPosition, pointerPosition) && - const DeepCollectionEquality() - .equals(other.selectedWidth, selectedWidth) && - const DeepCollectionEquality() - .equals(other.scaleFactor, scaleFactor)); + (identical(other.pointerPosition, pointerPosition) || + other.pointerPosition == pointerPosition) && + (identical(other.selectedWidth, selectedWidth) || + other.selectedWidth == selectedWidth) && + (identical(other.scaleFactor, scaleFactor) || + other.scaleFactor == scaleFactor)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash( runtimeType, - const DeepCollectionEquality().hash(sketch), - const DeepCollectionEquality().hash(allowedPointersMode), + sketch, + allowedPointersMode, const DeepCollectionEquality().hash(_activePointerIds), - const DeepCollectionEquality().hash(pointerPosition), - const DeepCollectionEquality().hash(selectedWidth), - const DeepCollectionEquality().hash(scaleFactor)); + pointerPosition, + selectedWidth, + scaleFactor); @JsonKey(ignore: true) @override - _$$ErasingCopyWith<_$Erasing> get copyWith => - __$$ErasingCopyWithImpl<_$Erasing>(this, _$identity); + @pragma('vm:prefer-inline') + _$$ErasingImplCopyWith<_$ErasingImpl> get copyWith => + __$$ErasingImplCopyWithImpl<_$ErasingImpl>(this, _$identity); @override @optionalTypeArgs @@ -812,7 +835,7 @@ class _$Erasing extends Erasing { @override @optionalTypeArgs TResult? whenOrNull({ - TResult Function( + TResult? Function( Sketch sketch, SketchLine? activeLine, ScribblePointerMode allowedPointersMode, @@ -822,7 +845,7 @@ class _$Erasing extends Erasing { double selectedWidth, double scaleFactor)? drawing, - TResult Function( + TResult? Function( Sketch sketch, ScribblePointerMode allowedPointersMode, List activePointerIds, @@ -877,8 +900,8 @@ class _$Erasing extends Erasing { @override @optionalTypeArgs TResult? mapOrNull({ - TResult Function(Drawing value)? drawing, - TResult Function(Erasing value)? erasing, + TResult? Function(Drawing value)? drawing, + TResult? Function(Erasing value)? erasing, }) { return erasing?.call(this); } @@ -898,7 +921,9 @@ class _$Erasing extends Erasing { @override Map toJson() { - return _$$ErasingToJson(this); + return _$$ErasingImplToJson( + this, + ); } } @@ -909,43 +934,42 @@ abstract class Erasing extends ScribbleState { final List activePointerIds, final Point? pointerPosition, final double selectedWidth, - final double scaleFactor}) = _$Erasing; + final double scaleFactor}) = _$ErasingImpl; const Erasing._() : super._(); - factory Erasing.fromJson(Map json) = _$Erasing.fromJson; + factory Erasing.fromJson(Map json) = _$ErasingImpl.fromJson; @override /// The current state of the sketch - Sketch get sketch => throw _privateConstructorUsedError; + Sketch get sketch; @override /// Which pointers are allowed for drawing and will be captured by the /// scribble widget. - ScribblePointerMode get allowedPointersMode => - throw _privateConstructorUsedError; + ScribblePointerMode get allowedPointersMode; @override /// The ids of all supported pointers that are currently interacting with /// the widget. - List get activePointerIds => throw _privateConstructorUsedError; + List get activePointerIds; @override /// The current position of the pointer - Point? get pointerPosition => throw _privateConstructorUsedError; + Point? get pointerPosition; @override /// The current width of the pen - double get selectedWidth => throw _privateConstructorUsedError; + double get selectedWidth; @override /// How much the widget is scaled at the moment. /// /// Can be used if zoom functionality is needed /// (e.g. through InteractiveViewer) so that the pen width remains the same. - double get scaleFactor => throw _privateConstructorUsedError; + double get scaleFactor; @override @JsonKey(ignore: true) - _$$ErasingCopyWith<_$Erasing> get copyWith => + _$$ErasingImplCopyWith<_$ErasingImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/src/state/scribble.state.g.dart b/lib/src/view/state/scribble.state.g.dart similarity index 88% rename from lib/src/state/scribble.state.g.dart rename to lib/src/view/state/scribble.state.g.dart index fa1c863..93e287e 100644 --- a/lib/src/state/scribble.state.g.dart +++ b/lib/src/view/state/scribble.state.g.dart @@ -6,7 +6,8 @@ part of 'scribble.state.dart'; // JsonSerializableGenerator // ************************************************************************** -_$Drawing _$$DrawingFromJson(Map json) => _$Drawing( +_$DrawingImpl _$$DrawingImplFromJson(Map json) => + _$DrawingImpl( sketch: Sketch.fromJson(json['sketch'] as Map), activeLine: json['activeLine'] == null ? null @@ -27,11 +28,12 @@ _$Drawing _$$DrawingFromJson(Map json) => _$Drawing( $type: json['runtimeType'] as String?, ); -Map _$$DrawingToJson(_$Drawing instance) => { +Map _$$DrawingImplToJson(_$DrawingImpl instance) => + { 'sketch': instance.sketch.toJson(), 'activeLine': instance.activeLine?.toJson(), 'allowedPointersMode': - _$ScribblePointerModeEnumMap[instance.allowedPointersMode], + _$ScribblePointerModeEnumMap[instance.allowedPointersMode]!, 'activePointerIds': instance.activePointerIds, 'pointerPosition': instance.pointerPosition?.toJson(), 'selectedColor': instance.selectedColor, @@ -47,7 +49,8 @@ const _$ScribblePointerModeEnumMap = { ScribblePointerMode.mouseAndPen: 'mouseAndPen', }; -_$Erasing _$$ErasingFromJson(Map json) => _$Erasing( +_$ErasingImpl _$$ErasingImplFromJson(Map json) => + _$ErasingImpl( sketch: Sketch.fromJson(json['sketch'] as Map), allowedPointersMode: $enumDecodeNullable( _$ScribblePointerModeEnumMap, json['allowedPointersMode']) ?? @@ -64,10 +67,11 @@ _$Erasing _$$ErasingFromJson(Map json) => _$Erasing( $type: json['runtimeType'] as String?, ); -Map _$$ErasingToJson(_$Erasing instance) => { +Map _$$ErasingImplToJson(_$ErasingImpl instance) => + { 'sketch': instance.sketch.toJson(), 'allowedPointersMode': - _$ScribblePointerModeEnumMap[instance.allowedPointersMode], + _$ScribblePointerModeEnumMap[instance.allowedPointersMode]!, 'activePointerIds': instance.activePointerIds, 'pointerPosition': instance.pointerPosition?.toJson(), 'selectedWidth': instance.selectedWidth, diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..1428d21 --- /dev/null +++ b/melos.yaml @@ -0,0 +1,64 @@ +name: history_value_notifier_workspace + +packages: + - . + - packages/* + - example + +command: + version: + updateGitTagRefs: true + workspaceChangelog: false + +scripts: + analyze: + run: | + dart analyze . --fatal-infos + exec: + # We are setting the concurrency to 1 because a higher concurrency can crash + # the analysis server on low performance machines (like GitHub Actions). + concurrency: 1 + description: | + Run `dart analyze` in all packages. + - Note: you can also rely on your IDEs Dart Analysis / Issues window. + + test:select: + run: flutter test + exec: + failFast: true + concurrency: 6 + packageFilters: + dirExists: test + description: Run `flutter test test` for selected packages. + + test: + run: melos run test:select --no-select + description: Run all tests in this project. + + coverage:select: + run: | + flutter test --coverage + exec: + failFast: true + concurrency: 6 + packageFilters: + dirExists: test + description: Generate coverage for the selected package. + + coverage: + run: melos run coverage:select --no-select + description: Generate coverage for all packages. + + build_runner: + run: flutter pub run build_runner build --delete-conflicting-outputs + exec: + failFast: false + packageFilters: + dependsOn: build_runner + description: Run `flutter pub run build_runner build --delete-conflicting-outputs` in all packages. + + fix: + run: dart fix --apply + exec: + failFast: true + description: Run `dart fix --apply` in all packages. \ No newline at end of file diff --git a/packages/value_notifier_tools/.gitignore b/packages/value_notifier_tools/.gitignore new file mode 100644 index 0000000..8318291 --- /dev/null +++ b/packages/value_notifier_tools/.gitignore @@ -0,0 +1,27 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +.mason/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# See https://www.dartlang.org/guides/libraries/private-files + +# Files and directories created by pub +.dart_tool/ +.packages +build/ +pubspec.lock +pubspec_overrides.yaml \ No newline at end of file diff --git a/packages/value_notifier_tools/CHANGELOG.md b/packages/value_notifier_tools/CHANGELOG.md new file mode 100644 index 0000000..02867dc --- /dev/null +++ b/packages/value_notifier_tools/CHANGELOG.md @@ -0,0 +1,12 @@ +## 0.1.1 + + - **FIX**(value_notifier_tools): Added all classes to package exports. + - **FEAT**: use `HistoryValueNotifier` from new package. + - **FEAT**(value_notifier_tools): added `HistoryValueNotifier`. + - **FEAT**(value_notifier_tools): added where_value_notifier. + - **FEAT**(value_notifier_tools): added select value notifier. + - **DOCS**(value_notifier_tools): added README. + +# 0.1.0+1 + +- feat: initial commit ๐ŸŽ‰ diff --git a/packages/value_notifier_tools/LICENSE b/packages/value_notifier_tools/LICENSE new file mode 100644 index 0000000..b9cbdfa --- /dev/null +++ b/packages/value_notifier_tools/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Tim Lehmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/value_notifier_tools/README.md b/packages/value_notifier_tools/README.md new file mode 100644 index 0000000..d15e4cc --- /dev/null +++ b/packages/value_notifier_tools/README.md @@ -0,0 +1,173 @@ +# Value Notifier Tools + +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![melos](https://img.shields.io/badge/maintained%20with-melos-f700ff.svg?style=flat-square)](https://github.com/invertase/melos) +![coverage](coverage.svg) + +Helpful lightweight tools for working with `ValueNotifier`s + +## Installation ๐Ÿ’ป + +**โ— In order to start using History Value Notifier you must have the [Dart SDK][dart_install_link] installed on your machine.** + +Install via `dart pub add`: + +```sh +dart pub add value_notifier_tools +``` + +## Features +This package adds helpful tools for working with `ValueNotifier`s. Currently, it offers the following: + +* ๐Ÿ• [HistoryValueNotifier](#historyvaluenotifier) allows you to undo and redo changes to the state of the notifier. This is useful for implementing undo/redo functionality in your app. +* ๐ŸŽฏ [SelectValueNotifier](#selectvaluenotifier) allows you to select a subset of the state of a `ValueNotifier`. This is useful for when you only want to listen to a specific part of the state of and rebuild a widget when that changes. +* ๐Ÿ”Ž [WhereValueNotifier](#wherevaluenotifier) allows you to provide a custom predicate to determine whether a state change should be propagated to listeners. This is useful for when only certain state transitions should cause a rebuild. +* ๐Ÿชถ No dependencies on any other packages and super lightweight. +* ๐Ÿงฉ Easy to use and integrate into your existing projects. +* ๐Ÿงช 100% test coverage + + +## HistoryValueNotifier + +* โ†ฉ๏ธ Add `undo()` and `redo()` to `ValueNotifier` +* ๐Ÿ• Limit the size of your history +* ๐Ÿ’• Offers both a mixin that can be added to your existing `ValueNotifier`s and a class that you can extend +* ๐Ÿ”Ž Choose which states get stored to the history +* ๐Ÿ”„ Transform states before applying them from the history + +### Usage +Getting started is easy! There are three main ways in which you can add `HistoryValueNotifier` to your project: + +#### Use it as-is +If you don't need any extra functionality, you can use `HistoryValueNotifier` as-is. + +```dart +import 'package:history_value_notifier/history_value_notifier.dart'; + +final notifier = HistoryValueNotifier(0); +notifier.value = 1; +notifier.undo(); // 0 +notifier.redo(); // 1 +``` + +#### Upgrade an existing `ValueNotifier` + +```dart +class CounterNotifier extends ValueNotifier + with HistoryValueNotifierMixin { + CounterNotifier() : super(0) { + // This is how you limit the size of your history. + // Set it to null to keep all state (default) + maxHistoryLength = 30; + } + + void increment() => ++state; + + void decrement() => --state; + + // By using temporaryState setter, the change won't be stored in history + void reset() => temporaryState = 0; + + // You can override this function to apply a transformation to a state + // from the history before it gets applied. + @override + int transformHistoryState(int newState, int currentState) { + return newState; + } +} +``` + +#### Create a `HistoryValueNotifier` + +If you prefer to create a `HistoryValueNotifier` directly, you can do this instead: + +```dart +class CounterNotifier extends HistoryValueNotifier { + // ... Same as above +} +``` + +#### Use It! + +You can now use the full functionality of the `HistoryValueNotifier`! + +```dart +// Obtain a reference however you wish (Provider, GetIt, etc.) +final CounterNotifier notifier = context.read(counterNotifier); + +notifier.increment(); // 1 +notifier.undo(); // 0 +notifier.redo(); // 1 + +notifier.decrement(); // 0 +notifier.undo(); // 1 +notifier.canRedo // true +notifier.increment // 2 +notifier.canRedo // false + +// ... +``` + +## SelectValueNotifier + +* ๐ŸŽฏ Select a subset of the state of a `ValueNotifier` +* ๐Ÿงฉ Only listen to the parts of the state that you care about +* ๐Ÿƒ Micromanage rebuilds for maximum performance + +### Usage + +Using `SelectValueNotifier` is super easy: + +#### Use the convenient `select` extension method +This allows you to select a subset of the state of a `ValueNotifier` by providing a selector function anywhere you need. + +```dart +final notifier = ValueNotifier({'a': 1, 'b': 2}); + +return ValueListenableBuilder( + valueListenable: notifier.select((value) => value['a']), + builder: (context, value, child) { + return Text(value.toString()); + }, +); +``` +Your `selectNotifier` will now only notify listeners when the value of `'a'` changes. + +## WhereValueNotifier + +* ๐Ÿ”Ž Provide a custom predicate to determine whether a state change should be propagated to listeners +* ๐Ÿงฉ Only listen to the state changes that you care about + +### Usage +There are two main ways to use `WhereValueNotifier`: + + +#### Use the `where` extension method +This allows you to dynamically filter the state changes that you care about from another notifier. + +```dart +final notifier = ValueNotifier(0); + +return ValueListenableBuilder( + valueListenable: notifier.where((oldState, newState) => newState > oldState), + builder: (context, value, child) { + return Text(value.toString()); + }, +); +``` + +Now, your widget will only rebuild when the new state is greater than the old state. + +#### Extend it for your own custom classes +You can also extend it and provide your own `updateShouldNotify` function. + +```dart +class IncreasingValueNotifier extends WhereValueNotifier { + IncreasingValueNotifier(super.value); + + @override + bool updateShouldNotify(T oldState, T newState) { + return oldState < newState; + } +} +``` diff --git a/packages/value_notifier_tools/analysis_options.yaml b/packages/value_notifier_tools/analysis_options.yaml new file mode 100644 index 0000000..245ed9f --- /dev/null +++ b/packages/value_notifier_tools/analysis_options.yaml @@ -0,0 +1 @@ +include: package:lintervention/analysis_options.yaml diff --git a/packages/value_notifier_tools/coverage/lcov.info b/packages/value_notifier_tools/coverage/lcov.info new file mode 100644 index 0000000..d227f9c --- /dev/null +++ b/packages/value_notifier_tools/coverage/lcov.info @@ -0,0 +1,104 @@ +SF:lib/src/where_value_notifier/where_value_notifier_mixin.dart +DA:8,4 +DA:10,2 +DA:11,2 +DA:13,2 +DA:15,2 +DA:16,2 +DA:17,4 +LF:7 +LH:7 +end_of_record +SF:lib/src/where_value_notifier/where_value_notifier.dart +DA:28,2 +LF:1 +LH:1 +end_of_record +SF:lib/src/select_value_notifier/select_value_notifier.dart +DA:15,1 +DA:18,3 +DA:19,3 +DA:30,1 +DA:33,1 +DA:36,1 +DA:37,5 +DA:40,1 +DA:42,3 +DA:43,1 +DA:55,1 +DA:58,1 +LF:12 +LH:12 +end_of_record +SF:lib/src/where_value_notifier/where_value_notifier_from_parent.dart +DA:13,1 +DA:17,2 +DA:18,3 +DA:27,1 +DA:29,2 +DA:32,1 +DA:33,3 +DA:36,1 +DA:38,2 +DA:41,1 +DA:43,3 +DA:44,1 +DA:52,1 +DA:53,1 +LF:14 +LH:14 +end_of_record +SF:lib/src/history_value_notifier/history_value_notifier.dart +DA:17,1 +LF:1 +LH:1 +end_of_record +SF:lib/src/history_value_notifier/history_value_notifier_mixin.dart +DA:13,2 +DA:14,1 +DA:16,2 +DA:19,1 +DA:21,3 +DA:22,3 +DA:23,4 +DA:27,4 +DA:29,2 +DA:30,3 +DA:37,1 +DA:39,1 +DA:40,2 +DA:41,3 +DA:42,2 +DA:43,1 +DA:44,4 +DA:45,4 +DA:49,1 +DA:57,1 +DA:59,1 +DA:63,6 +DA:66,3 +DA:70,1 +DA:77,1 +DA:85,1 +DA:97,1 +DA:99,1 +DA:103,1 +DA:104,2 +DA:105,7 +DA:110,1 +DA:111,2 +DA:112,7 +DA:117,1 +DA:118,2 +DA:119,1 +DA:120,2 +DA:127,1 +DA:128,1 +DA:129,2 +DA:132,1 +DA:133,1 +DA:134,6 +DA:135,1 +LF:45 +LH:45 +end_of_record diff --git a/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier.dart b/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier.dart new file mode 100644 index 0000000..3514926 --- /dev/null +++ b/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:value_notifier_tools/src/history_value_notifier/history_value_notifier_mixin.dart'; + +/// {@template history_value_notifier} +/// Works like a [ValueNotifier] with the added benefit of maintaining an +/// internal undo history that can be navigated through. +/// +/// All states that get set using the [value] setter will automatically be +/// remembered for up to [maxHistoryLength] entries. If you want to update +/// the state without adding it to the history, use the [temporaryValue] setter +/// instead. +/// > Note: The initial undo history will start with the initial [value]. +/// {@endtemplate} +class HistoryValueNotifier extends ValueNotifier + with HistoryValueNotifierMixin { + /// {@macro history_value_notifier} + HistoryValueNotifier(super._value); +} diff --git a/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier_mixin.dart b/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier_mixin.dart new file mode 100644 index 0000000..c20e3ed --- /dev/null +++ b/packages/value_notifier_tools/lib/src/history_value_notifier/history_value_notifier_mixin.dart @@ -0,0 +1,138 @@ +import 'package:flutter/foundation.dart'; +import 'package:value_notifier_tools/src/history_value_notifier/history_value_notifier.dart'; + +/// Use this mixin on any [ValueNotifier] to add a history functionality to it. +/// +/// {@macro history_value_notifier} +mixin HistoryValueNotifierMixin on ValueNotifier { + int? _maxHistoryLength; + + /// How many values to keep track of. + /// + /// If null, all values will be stored. + int? get maxHistoryLength => _maxHistoryLength; + set maxHistoryLength(int? value) { + assert( + value == null || value >= 0, + "The maxHistoryLength can't be negative!", + ); + _maxHistoryLength = value; + if (value == null) return; + if (_undoHistory.length > value) { + _undoHistory = _undoHistory.sublist(0, value); + _undoIndex = _undoIndex >= value ? value - 1 : _undoIndex; + } + } + + bool get _keepAny => _maxHistoryLength == null || _maxHistoryLength! > 0; + + late List _undoHistory = [ + if (includeInitialValueInHistory && _keepAny) value, + ]; + + int _undoIndex = 0; + + /// Sets the value of this [HistoryValueNotifier], notifies listeners, + /// and adds the new value to the undo history. + @override + set value(T value) { + _internalClearRedoQueue(); + if (_undoHistory.isEmpty || + shouldInsertValueIntoQueue(value, _undoHistory[0])) { + _undoHistory.insert(0, value); + if (_maxHistoryLength != null && + _undoHistory.length > _maxHistoryLength!) { + _undoHistory = _undoHistory.sublist(0, _maxHistoryLength); + } + } + + super.value = value; + } + + /// Sets the current "value" of this [HistoryValueNotifier] **without** adding + /// the new value to the undo history. + /// + /// This is helpful for loading values or in general any other value that the + /// user should not be able to undo to. + @protected + set temporaryValue(T value) { + super.value = value; + } + + /// Whether currently an undo operation is possible. + bool get canUndo => _undoIndex + 1 < _undoHistory.length; + + /// Whether a redo operation is currently possible. + bool get canRedo => _undoIndex > 0; + + /// You can override this to prevent undo/redo operations in certain cases + /// (e.g. when in a loading value) + @protected + bool get allowOperations => true; + + /// Whether to include the initial [value] in the undo history. + /// + /// `true` by default, override and set to false if the initial value should + /// be treated like [temporaryValue]. + @protected + bool get includeInitialValueInHistory => true; + + /// You can override this function if you want to transform values from the + /// history before they get applied. + /// + /// This can be useful if your value contains values that aren't supposed + /// to be changed upon undoing for example. + @protected + T transformHistoryValue(T newValue, T currentValue) { + return newValue; + } + + /// You can override this function if you want to filter certain values before + /// adding them to the history. + /// + /// By default, this uses value equality, but you could for example always + /// return true in case your value doesn't support value equality. + /// [newValue] holds the value that's supposed to be added, [lastInQueue] is + /// the value that's currently last in the undo queue. + @protected + bool shouldInsertValueIntoQueue(T newValue, T lastInQueue) { + return newValue != lastInQueue; + } + + /// Returns to the previous value in the history. + void undo() { + if (canUndo && allowOperations) { + temporaryValue = transformHistoryValue(_undoHistory[++_undoIndex], value); + } + } + + /// Proceeds to the next value in the history. + void redo() { + if (canRedo && allowOperations) { + temporaryValue = transformHistoryValue(_undoHistory[--_undoIndex], value); + } + } + + /// Removes all history items from the queue. + void clearQueue() { + _undoHistory = []; + _undoIndex = 0; + temporaryValue = value; + } + + /// Removes all history items that happened after the current undo position. + /// + /// Internally this is used whenever a change occurs, but you might want to + /// use it for something else. + void clearRedoQueue() { + _internalClearRedoQueue(); + temporaryValue = value; + } + + void _internalClearRedoQueue() { + if (canRedo) { + _undoHistory = _undoHistory.sublist(_undoIndex, _undoHistory.length); + _undoIndex = 0; + } + } +} diff --git a/packages/value_notifier_tools/lib/src/select_value_notifier/select_value_notifier.dart b/packages/value_notifier_tools/lib/src/select_value_notifier/select_value_notifier.dart new file mode 100644 index 0000000..0f7877b --- /dev/null +++ b/packages/value_notifier_tools/lib/src/select_value_notifier/select_value_notifier.dart @@ -0,0 +1,63 @@ +import 'package:flutter/foundation.dart'; + +/// A function that maps a value of type [FromT] to a value of type [ToT]. +typedef Selector = ToT Function(FromT value); + +/// {@template selected_value_notifier} +/// A [ValueNotifier] that wraps another [ValueNotifier] and selects updates +/// based on a provided function. +/// +/// Updates will only be sent to listeners when the new value is different from +/// the previous value. +/// {@endtemplate} +class SelectValueNotifier extends ValueNotifier { + /// {@macro selected_value_notifier} + SelectValueNotifier({ + required this.parentNotifier, + required this.selector, + }) : super(selector(parentNotifier.value)) { + parentNotifier.addListener(_updateFromParent); + } + + /// The [ValueNotifier] that this [SelectValueNotifier] listens to and + /// filters updates from. + final ValueNotifier parentNotifier; + + /// A function that maps the value of the parent notifier to the value of this + /// notifier. + final Selector selector; + + @override + @protected + set value(ToT newValue) { + throw UnsupportedError('Cannot set value on SelectValueNotifier'); + } + + void _updateFromParent() { + super.value = selector(parentNotifier.value); + } + + @override + void dispose() { + parentNotifier.removeListener(_updateFromParent); + super.dispose(); + } +} + +/// An extension on [ValueNotifier] that provides a `select` method to create a +/// [SelectValueNotifier] from the notifier. +extension SelectValueNotifierX on ValueNotifier { + /// Selects updates from this notifier using the provided [selector] + /// function. + /// + /// The [selector] function is called whenever the parent notifier updates and + /// the result is used as the new value for the [SelectValueNotifier]. + SelectValueNotifier select( + ToT Function(FromT value) selector, + ) { + return SelectValueNotifier( + parentNotifier: this, + selector: selector, + ); + } +} diff --git a/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier.dart b/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier.dart new file mode 100644 index 0000000..c2b287e --- /dev/null +++ b/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier.dart @@ -0,0 +1,29 @@ +import 'package:flutter/foundation.dart'; +import 'package:value_notifier_tools/src/where_value_notifier/where_value_notifier_mixin.dart'; + +/// {@template where_value_notifier} +/// A [ValueNotifier] that provides a custom [updateShouldNotify] function to +/// determine whether the listener should be notified. +/// +/// Extend this to create a custom [ValueNotifier] that notifies listeners based +/// on the provided [updateShouldNotify] function. +/// +/// **Example:** +/// In this example, the MyValueNotifier notifies listeners when the new value +/// is less than the previous value. +/// ```dart +/// class MyValueNotifier extends WhereValueNotifier { +/// MyValueNotifier(int value) : super(value); +/// +/// @override +/// bool updateShouldNotify(int previous, int next) { +/// return previous > next; +/// } +/// } +/// ``` +/// {@endtemplate} +abstract class WhereValueNotifier extends ValueNotifier + with WhereValueNotifierMixin { + /// {@macro where_value_notifier} + WhereValueNotifier(super.value); +} diff --git a/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_from_parent.dart b/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_from_parent.dart new file mode 100644 index 0000000..9289b17 --- /dev/null +++ b/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_from_parent.dart @@ -0,0 +1,58 @@ +import 'package:flutter/foundation.dart'; +import 'package:value_notifier_tools/src/where_value_notifier/where_value_notifier.dart'; + +/// A function that compares the previous value with the new value and returns +/// whether the listener should be notified. +typedef WhereFilter = bool Function(T previous, T next); + +/// +/// A [WhereValueNotifier] that listens to a parent [ValueNotifier] and notifies +/// listeners based on the provided [updateShouldNotify] function. +class WhereValueNotifierFromParent extends WhereValueNotifier { + /// {@macro selected_value_notifier} + WhereValueNotifierFromParent({ + required this.parentNotifier, + required WhereFilter updateShouldNotify, + }) : filter = updateShouldNotify, + super(parentNotifier.value) { + parentNotifier.addListener(_parentListener); + } + + /// The parent notifier to listen to. + final ValueNotifier parentNotifier; + + /// The function that determines whether the listeners should be notified. + final WhereFilter filter; + + @override + set value(T newValue) { + parentNotifier.value = newValue; + } + + void _parentListener() { + super.value = parentNotifier.value; + } + + @override + bool updateShouldNotify(T previous, T next) { + return filter(previous, next); + } + + @override + void dispose() { + parentNotifier.removeListener(_parentListener); + super.dispose(); + } +} + +/// An extension on [ValueNotifier] that provides a `where` method to create a +/// [WhereValueNotifier] from the notifier. +extension SelectedValueNotifierX on ValueNotifier { + /// Creates a [WhereValueNotifier] from the notifier. + WhereValueNotifier where(WhereFilter updateShouldNotify) { + return WhereValueNotifierFromParent( + parentNotifier: this, + updateShouldNotify: updateShouldNotify, + ); + } +} diff --git a/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_mixin.dart b/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_mixin.dart new file mode 100644 index 0000000..67f6c52 --- /dev/null +++ b/packages/value_notifier_tools/lib/src/where_value_notifier/where_value_notifier_mixin.dart @@ -0,0 +1,24 @@ +import 'package:flutter/foundation.dart'; + +/// Use this mixin on any [ValueNotifier] to add a custom [updateShouldNotify] +/// function to determine whether the listeners should be notified. +/// +/// {@macro where_value_notifier} +mixin WhereValueNotifierMixin on ValueNotifier { + late T _value = super.value; + + @override + T get value => _value; + + @override + set value(T newValue) { + final previous = _value; + _value = newValue; + if (updateShouldNotify(previous, newValue)) notifyListeners(); + } + + /// The function that determines whether the listeners should be notified. + /// + /// If this function returns `true`, the listener will be notified. + bool updateShouldNotify(T previous, T next); +} diff --git a/packages/value_notifier_tools/lib/value_notifier_tools.dart b/packages/value_notifier_tools/lib/value_notifier_tools.dart new file mode 100644 index 0000000..e3cdab2 --- /dev/null +++ b/packages/value_notifier_tools/lib/value_notifier_tools.dart @@ -0,0 +1,9 @@ +/// Helpful lightweight tools for working with ValueNotifiers +library value_notifier_tools; + +export 'src/history_value_notifier/history_value_notifier.dart'; +export 'src/history_value_notifier/history_value_notifier_mixin.dart'; +export 'src/select_value_notifier/select_value_notifier.dart'; +export 'src/where_value_notifier/where_value_notifier.dart'; +export 'src/where_value_notifier/where_value_notifier_from_parent.dart'; +export 'src/where_value_notifier/where_value_notifier_mixin.dart'; diff --git a/packages/value_notifier_tools/pubspec.yaml b/packages/value_notifier_tools/pubspec.yaml new file mode 100644 index 0000000..e13ed05 --- /dev/null +++ b/packages/value_notifier_tools/pubspec.yaml @@ -0,0 +1,20 @@ +name: value_notifier_tools +description: Helpful lightweight tools for working with ValueNotifiers +version: 0.1.1 +repository: https://github.com/timcreatedit/scribble/tree/main/packages/value_notifier_tools +homepage: https://whynotmake.it + +environment: + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + lintervention: ^0.1.1 + mocktail: ^1.0.3 + diff --git a/packages/value_notifier_tools/test/src/history_value_notifier/history_value_notifier_test.dart b/packages/value_notifier_tools/test/src/history_value_notifier/history_value_notifier_test.dart new file mode 100644 index 0000000..db247a9 --- /dev/null +++ b/packages/value_notifier_tools/test/src/history_value_notifier/history_value_notifier_test.dart @@ -0,0 +1,193 @@ +// ignore_for_file: prefer_const_constructors +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:value_notifier_tools/src/history_value_notifier/history_value_notifier.dart'; + +class _MockNotifier extends HistoryValueNotifier with Mock { + _MockNotifier(super.value); +} + +void main() { + group('HistoryValueNotifier', () { + test('can be instantiated', () { + expect(HistoryValueNotifier(1), isNotNull); + expect(HistoryValueNotifier(1).value, 1); + }); + + group('as-is', () { + late HistoryValueNotifier sut; + + setUp(() { + sut = HistoryValueNotifier(0); + }); + test('can undo to initial state', () { + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, false); + sut.value = 1; + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + sut.undo(); + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, true); + sut.redo(); + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + }); + + test('clears redo queue when setting value while undone', () { + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, false); + sut.value = 1; + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + sut.undo(); + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, true); + sut.value = 2; + expect(sut.value, 2); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + }); + + test('clearRedoQueue works', () { + sut.value = 1; + expect(sut.value, 1); + sut.value = 2; + expect(sut.canUndo, true); + expect(sut.canRedo, false); + sut.undo(); + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, true); + sut.clearRedoQueue(); + expect(sut.canRedo, false); + }); + }); + + group('as extension', () { + late _MockNotifier sut; + + setUp(() { + sut = _MockNotifier(0); + }); + + test('can undo to initial state', () { + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, false); + sut.value = 1; + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + sut.undo(); + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, true); + sut.redo(); + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + }); + + test('clears redo queue when setting value while undone', () { + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, false); + sut.value = 1; + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + sut.undo(); + expect(sut.value, 0); + expect(sut.canUndo, false); + expect(sut.canRedo, true); + sut.value = 2; + expect(sut.value, 2); + expect(sut.canUndo, true); + expect(sut.canRedo, false); + }); + + test('clearRedoQueue works', () { + sut.value = 1; + expect(sut.value, 1); + sut.value = 2; + expect(sut.canUndo, true); + expect(sut.canRedo, false); + sut.undo(); + expect(sut.value, 1); + expect(sut.canUndo, true); + expect(sut.canRedo, true); + sut.clearRedoQueue(); + expect(sut.canRedo, false); + }); + + group('maxHistoryLength', () { + const changeCount = 10; + const maxHistoryLength = 5; + + test('drops entries that where there already', () async { + const changeCount = 10; + + for (var i = 0; i < changeCount; i++) { + sut.value++; + } + + expect(sut.canUndo, true); + expect(sut.canRedo, false); + + sut.maxHistoryLength = maxHistoryLength; + + expect(sut.canUndo, true); + expect(sut.canRedo, false); + + // We should be able to undo maxHistoryLength - 1 times + for (var i = 0; i < maxHistoryLength - 1; i++) { + expect(sut.canUndo, true, reason: 'iteration $i'); + expect(sut.value, changeCount - i); + sut.undo(); + } + expect(sut.canUndo, false); + }); + + test('only collects the last entries', () async { + sut.maxHistoryLength = maxHistoryLength; + for (var i = 0; i < changeCount; i++) { + sut.value++; + } + + expect(sut.canUndo, true); + expect(sut.canRedo, false); + + // We should be able to undo historyLength - 1 times + for (var i = 0; i < sut.maxHistoryLength! - 1; i++) { + expect(sut.canUndo, true, reason: 'iteration $i'); + expect(sut.value, changeCount - i); + sut.undo(); + } + expect(sut.canUndo, false); + }); + }); + + group('clear', () { + test('clears history', () { + sut + ..value = 1 + ..value = 2 + ..value = 3; + expect(sut.canUndo, true); + expect(sut.canRedo, false); + sut.clearQueue(); + expect(sut.canUndo, false); + expect(sut.canRedo, false); + }); + }); + }); + }); +} diff --git a/packages/value_notifier_tools/test/src/select_value_notifier/select_value_notifier_test.dart b/packages/value_notifier_tools/test/src/select_value_notifier/select_value_notifier_test.dart new file mode 100644 index 0000000..5eaac14 --- /dev/null +++ b/packages/value_notifier_tools/test/src/select_value_notifier/select_value_notifier_test.dart @@ -0,0 +1,88 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:value_notifier_tools/src/select_value_notifier/select_value_notifier.dart'; + +import '../../util/mock_listener.dart'; + +typedef _Model = ({int interesting, int uninteresting}); + +void main() { + group('SelectValueNotifier', () { + late ValueNotifier<_Model> notifier; + late MockListener notifierListener; + late SelectValueNotifier<_Model, int> sut; + late MockListener sutListener; + + setUp(() { + notifier = ValueNotifier((interesting: 0, uninteresting: 0)); + notifierListener = MockListener(); + notifier.addListener(notifierListener.call); + addTearDown(() => notifier.removeListener(notifierListener.call)); + + sut = SelectValueNotifier( + parentNotifier: notifier, + selector: (model) => model.interesting, + ); + sutListener = MockListener(); + + sut.addListener(sutListener.call); + }); + + group('notifyListeners()', () { + test( + 'should notify listeners when the new value is different from the ' + 'previous value', () { + notifier.value = (interesting: 1, uninteresting: 1); + verify(() => notifierListener.call()); + verify(() => sutListener.call()); + expect(sut.value, 1); + }); + + test( + 'should not notify listeners when the new value is the same as the ' + 'previous value', () { + notifier.value = (interesting: 0, uninteresting: 1); + verify(() => notifierListener.call()); + verifyNever(() => sutListener.call()); + expect(sut.value, 0); + }); + }); + + group('set value', () { + test('should throw an UnsupportedError', () { + // ignore: invalid_use_of_protected_member + expect(() => sut.value = 1, throwsUnsupportedError); + }); + }); + + group('dispose', () { + test('should remove the listener from the parent notifier', () { + notifier.value = (interesting: 1, uninteresting: 1); + verify(() => sutListener.call()); + sut.dispose(); + notifier.value = (interesting: 2, uninteresting: 1); + verifyNever(() => sutListener.call()); + }); + }); + }); + + group('SelectValueNotifierX', () { + late ValueNotifier<_Model> notifier; + + setUp(() { + notifier = ValueNotifier((interesting: 0, uninteresting: 0)); + }); + + group('select()', () { + test('should return a SelectValueNotifier that is set up correctly', () { + // ignore: omit_local_variable_types, prefer_function_declarations_over_variables + final Selector<_Model, int> selector = (model) => model.interesting; + final sut = notifier.select(selector); + + expect(sut.parentNotifier, notifier); + expect(sut.selector, selector); + }); + }); + }); +} diff --git a/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_from_parent_test.dart b/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_from_parent_test.dart new file mode 100644 index 0000000..7a9f356 --- /dev/null +++ b/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_from_parent_test.dart @@ -0,0 +1,109 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:value_notifier_tools/value_notifier_tools.dart'; + +import '../../util/mock_listener.dart'; + +class _MockUpdateShouldNotify extends Mock { + bool call(int previous, int next); +} + +void main() { + group('WhereValueNotifierFromParent', () { + late ValueNotifier notifier; + late MockListener notifierListener; + late WhereValueNotifierFromParent sut; + late MockListener sutListener; + late _MockUpdateShouldNotify updateShouldNotify; + setUp(() { + notifier = ValueNotifier(0); + notifierListener = MockListener(); + notifier.addListener(notifierListener.call); + addTearDown(() => notifier.removeListener(notifierListener.call)); + + updateShouldNotify = _MockUpdateShouldNotify(); + sut = WhereValueNotifierFromParent( + parentNotifier: notifier, + updateShouldNotify: updateShouldNotify.call, + ); + sutListener = MockListener(); + sut.addListener(sutListener.call); + addTearDown(() => sut.removeListener(sutListener.call)); + }); + + group('notifyListeners()', () { + test('should notify listeners when updateShouldNotify is true', () { + when(() => updateShouldNotify.call(any(), any())).thenReturn(true); + + notifier.value = -1; + verify(() => notifierListener.call()); + verify(() => sut.updateShouldNotify(0, -1)); + verify(() => sutListener.call()); + expect(sut.value, -1); + }); + + test('should not notify listeners when updateShouldNotify is false', () { + when(() => sut.updateShouldNotify(any(), any())).thenReturn(false); + + notifier.value = 1; + verify(() => notifierListener.call()); + verify(() => sut.updateShouldNotify(0, 1)); + verifyNever(() => sutListener.call()); + expect(sut.value, 1); + }); + }); + + group('set value', () { + test('should set the parent notifier value', () { + when(() => updateShouldNotify.call(any(), any())).thenReturn(true); + sut.value = 2; + expect(notifier.value, 2); + }); + + test('should notify listeners when updateShouldNotify is true', () { + when(() => updateShouldNotify.call(any(), any())).thenReturn(true); + + sut.value = -1; + verify(() => notifierListener.call()); + verify(() => sut.updateShouldNotify(0, -1)); + verify(() => sutListener.call()); + expect(sut.value, -1); + }); + + test('should not notify listeners when updateShouldNotify is false', () { + when(() => sut.updateShouldNotify(any(), any())).thenReturn(false); + + sut.value = 1; + verify(() => notifierListener.call()); + verify(() => sut.updateShouldNotify(0, 1)); + verifyNever(() => sutListener.call()); + expect(sut.value, 1); + }); + }); + + group('dispose()', () { + test('should remove the listener from the parent notifier', () { + when(() => updateShouldNotify.call(any(), any())).thenReturn(true); + + notifier.value = -1; + verify(() => notifierListener.call()); + verify(() => sutListener.call()); + sut.dispose(); + notifier.value = 2; + verifyNever(() => sutListener.call()); + }); + }); + + group('SelectedValueNotifierX', () { + test('should create a WhereValueNotifierFromParent', () { + when(() => updateShouldNotify.call(any(), any())).thenReturn(true); + + final where = notifier.where(updateShouldNotify.call); + expect(where, isA>()); + where.updateShouldNotify(0, 1); + verify(() => updateShouldNotify.call(0, 1)); + }); + }); + }); +} diff --git a/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_test.dart b/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_test.dart new file mode 100644 index 0000000..e9dfb49 --- /dev/null +++ b/packages/value_notifier_tools/test/src/where_value_notifier/where_value_notifier_test.dart @@ -0,0 +1,45 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:value_notifier_tools/src/where_value_notifier/where_value_notifier.dart'; + +import '../../util/mock_listener.dart'; + +class _TestNotifier extends WhereValueNotifier with Mock { + _TestNotifier(super.value); + + @override + bool updateShouldNotify(int previous, int next); +} + +void main() { + group('WhereValueNotifier', () { + late _TestNotifier sut; + late MockListener listener; + setUp(() { + sut = _TestNotifier(0); + listener = MockListener(); + sut.addListener(listener.call); + addTearDown(() => sut.removeListener(listener.call)); + }); + + group('notifyListeners()', () { + test('should notify listeners when updateShouldNotify is true', () { + when(() => sut.updateShouldNotify(any(), any())).thenReturn(true); + + sut.value = -1; + verify(() => listener.call()); + verify(() => sut.updateShouldNotify(0, -1)); + expect(sut.value, -1); + }); + + test('should not notify listeners when updateShouldNotify is false', () { + when(() => sut.updateShouldNotify(any(), any())).thenReturn(false); + + sut.value = 1; + verifyNever(() => listener.call()); + verify(() => sut.updateShouldNotify(0, 1)); + expect(sut.value, 1); + }); + }); + }); +} diff --git a/packages/value_notifier_tools/test/util/mock_listener.dart b/packages/value_notifier_tools/test/util/mock_listener.dart new file mode 100644 index 0000000..382779f --- /dev/null +++ b/packages/value_notifier_tools/test/util/mock_listener.dart @@ -0,0 +1,6 @@ +import 'package:mocktail/mocktail.dart'; + +/// A simple listener that can be used in tests to verify that it was called. +class MockListener extends Mock { + void call(); +} diff --git a/pubspec.lock b/pubspec.lock index ffa1ee6..0185c8c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,203 +5,239 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" source: hosted - version: "33.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "6.4.1" + ansi_styles: + dependency: transitive + description: + name: ansi_styles + sha256: "9c656cc12b3c27b17dd982b2cc5c0cfdfbdabd7bc8f3ae5e8542d9867b47ce8a" + url: "https://pub.dev" + source: hosted + version: "0.3.2+1" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" + url: "https://pub.dev" source: hosted - version: "2.1.8" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "7.3.0" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.9.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" + cli_launcher: + dependency: transitive + description: + name: cli_launcher + sha256: "5e7e0282b79e8642edd6510ee468ae2976d847a0a29b3916e85f5fa1bfe24005" + url: "https://pub.dev" + source: hosted + version: "0.3.1" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.4.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.10.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.18.0" + conventional_commit: + dependency: transitive + description: + name: conventional_commit + sha256: dec15ad1118f029c618651a4359eb9135d8b88f761aa24e4016d061cd45948f2 + url: "https://pub.dev" + source: hosted + version: "0.6.0+1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.6" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted version: "1.3.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" - flutter_state_notifier: - dependency: "direct main" - description: - name: flutter_state_notifier - url: "https://pub.dartlang.org" - source: hosted - version: "0.7.1" flutter_test: dependency: "direct dev" description: flutter @@ -211,205 +247,306 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + sha256: d25416cb7144576fa565d2c4f346f5db0d3fb5dc1dd162d2814408c32946456d + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.4.8" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.4.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "4.0.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.2" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" source: hosted - version: "2.1.0" - history_state_notifier: - dependency: "direct main" + version: "2.3.1" + http: + dependency: transitive description: - name: history_state_notifier - url: "https://pub.dartlang.org" + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" source: hosted - version: "0.0.5" + version: "1.2.1" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "0.7.1" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.8.1" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + url: "https://pub.dev" source: hosted - version: "6.1.5" - lints: + version: "6.7.1" + leak_tracker: dependency: transitive description: - name: lints - url: "https://pub.dartlang.org" + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lintervention: + dependency: "direct dev" + description: + name: lintervention + sha256: "1ad5a5da869cf5b67c98e311ab2f9f0d7253d53ced8688fde4ce1af2f253924e" + url: "https://pub.dev" + source: hosted + version: "0.1.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.2.0" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + melos: + dependency: "direct dev" + description: + name: melos + sha256: a0cb264096a315e4acdb66ae75ee594a76c97fe15ce9ae469f6c58c6c4b2be87 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "5.3.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.11.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" source: hosted - version: "1.0.0" - nested: + version: "1.0.5" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + mustache_template: dependency: transitive description: - name: nested - url: "https://pub.dartlang.org" + name: mustache_template + sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" source: hosted - version: "1.8.2" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "1.9.0" perfect_freehand: dependency: "direct main" description: name: perfect_freehand - url: "https://pub.dartlang.org" + sha256: "6ced289209b3b26dc23c8d21960ceff3d39442cebd8e12ce722c4385c63553e5" + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.3.2" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted - version: "1.5.0" - provider: + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + prompts: dependency: transitive description: - name: provider - url: "https://pub.dartlang.org" + name: prompts + sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1" + url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "2.0.0" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" + pub_updater: + dependency: transitive + description: + name: pub_updater + sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + pubspec: + dependency: transitive + description: + name: pubspec + sha256: f534a50a2b4d48dc3bc0ec147c8bd7c304280fff23b153f3f11803c4d49d927e + url: "https://pub.dev" + source: hosted + version: "2.3.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.3" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" sky_engine: dependency: transitive description: flutter @@ -419,114 +556,169 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.5.0" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.4" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - state_notifier: - dependency: "direct main" - description: - name: state_notifier - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.6.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" source: hosted - version: "1.3.0" - vector_math: + version: "1.3.2" + uri: + dependency: transitive + description: + name: uri + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + value_notifier_tools: dependency: "direct main" + description: + path: "packages/value_notifier_tools" + relative: true + source: path + version: "0.1.1" + vector_math: + dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" + very_good_analysis: + dependency: transitive + description: + name: very_good_analysis + sha256: "9ae7f3a3bd5764fb021b335ca28a34f040cd0ab6eec00a1b213b445dae58a4b8" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.4.4" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: c566f4f804215d84a7a2c377667f546c6033d5b34b4f9e60dfb09d17c4e97826 + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "2.2.0" sdks: - dart: ">=2.17.0-0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4cf8996..fde51dd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,31 +1,28 @@ name: scribble description: Scribble is a lightweight library for freehand drawing in Flutter - supporting pressure, variable line width and more! -version: 0.9.1 -repository: https://github.com/fyzo-dev/scribble -issue_tracker: https://github.com/fyzo-dev/scribble/issues +version: 0.1.0+1 +publish_to: none environment: - sdk: ">=2.12.0 <3.0.0" - flutter: ">=2.5.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" dependencies: flutter: sdk: flutter - flutter_state_notifier: ^0.7.1 - freezed_annotation: ^2.0.3 - history_state_notifier: ^0.0.5 - state_notifier: ^0.7.2+1 - vector_math: ^2.1.1 - perfect_freehand: ^1.0.4 + freezed_annotation: ^2.4.1 + perfect_freehand: ^2.3.2 + value_notifier_tools: ^0.1.1 dev_dependencies: - build_runner: ^2.1.8 - flutter_lints: ^1.0.4 + build_runner: ^2.4.9 flutter_test: sdk: flutter - freezed: ^2.0.3 - json_serializable: ^6.1.5 + freezed: ^2.4.7 + json_serializable: ^6.7.1 + lintervention: ^0.1.1 + melos: ^5.2.1 + mocktail: ^1.0.3 -# The following section is specific to Flutter. -flutter: null +flutter: + uses-material-design: true \ No newline at end of file diff --git a/test/src/domain/model/sketch/sketch_test.dart b/test/src/domain/model/sketch/sketch_test.dart new file mode 100644 index 0000000..6f546b4 --- /dev/null +++ b/test/src/domain/model/sketch/sketch_test.dart @@ -0,0 +1,2947 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:scribble/scribble.dart'; + +void main() { + group(".fromJson", () { + test('can deserialize example sketch', () async { + final json = jsonDecode(exampleJson) as Map; + final sketch = Sketch.fromJson(json); + expect(sketch.lines.length, 14); + expect(sketch.lines.where((l) => l.color == 0xFF000000).length, 10); + expect(sketch.lines.where((l) => l.color == Colors.red.value).length, 1); + expect( + sketch.lines.where((l) => l.color == Colors.green.value).length, + 1, + ); + expect(sketch.lines.where((l) => l.color == Colors.blue.value).length, 1); + expect( + sketch.lines.where((l) => l.color == Colors.yellow.value).length, + 1, + ); + }); + }); +} + +const exampleJson = ''' +{ + "lines" : [ + { + "points" : [ + { + "x" : 251.38671875, + "y" : 128.421875, + "pressure" : 0.5 + }, + { + "x" : 248.01953125, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 244.1484375, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 238.6640625, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 232.55859375, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 226.12109375, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 219.31640625, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 213.4140625, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 208.31640625, + "y" : 127.8359375, + "pressure" : 0.5 + }, + { + "x" : 203.2734375, + "y" : 127.98828125, + "pressure" : 0.5 + }, + { + "x" : 198.875, + "y" : 128.5234375, + "pressure" : 0.5 + }, + { + "x" : 195.66796875, + "y" : 129.55078125, + "pressure" : 0.5 + }, + { + "x" : 193.421875, + "y" : 130.83984375, + "pressure" : 0.5 + }, + { + "x" : 191.0859375, + "y" : 133.12109375, + "pressure" : 0.5 + }, + { + "x" : 190.39453125, + "y" : 135.1328125, + "pressure" : 0.5 + }, + { + "x" : 189.9609375, + "y" : 138.9609375, + "pressure" : 0.5 + }, + { + "x" : 189.26171875, + "y" : 142.29296875, + "pressure" : 0.5 + }, + { + "x" : 188.69921875, + "y" : 145.34765625, + "pressure" : 0.5 + }, + { + "x" : 188.1328125, + "y" : 148.65234375, + "pressure" : 0.5 + }, + { + "x" : 187.6875, + "y" : 152.09375, + "pressure" : 0.5 + }, + { + "x" : 187.515625, + "y" : 155.125, + "pressure" : 0.5 + }, + { + "x" : 187.515625, + "y" : 157.44921875, + "pressure" : 0.5 + }, + { + "x" : 187.515625, + "y" : 161.11328125, + "pressure" : 0.5 + }, + { + "x" : 187.66015625, + "y" : 163.1640625, + "pressure" : 0.5 + }, + { + "x" : 188.4765625, + "y" : 166.11328125, + "pressure" : 0.5 + }, + { + "x" : 190.84765625, + "y" : 171.08984375, + "pressure" : 0.5 + }, + { + "x" : 195.6328125, + "y" : 178.484375, + "pressure" : 0.5 + }, + { + "x" : 203.015625, + "y" : 187.28515625, + "pressure" : 0.5 + }, + { + "x" : 212.51953125, + "y" : 196.58203125, + "pressure" : 0.5 + }, + { + "x" : 222.53515625, + "y" : 204.8671875, + "pressure" : 0.5 + }, + { + "x" : 230.6875, + "y" : 210.8125, + "pressure" : 0.5 + }, + { + "x" : 237.16015625, + "y" : 214.859375, + "pressure" : 0.5 + }, + { + "x" : 243.859375, + "y" : 218.1953125, + "pressure" : 0.5 + }, + { + "x" : 250.48828125, + "y" : 220.578125, + "pressure" : 0.5 + }, + { + "x" : 256.796875, + "y" : 222.33984375, + "pressure" : 0.5 + }, + { + "x" : 263.20703125, + "y" : 224.33984375, + "pressure" : 0.5 + }, + { + "x" : 270.2734375, + "y" : 226.7890625, + "pressure" : 0.5 + }, + { + "x" : 277.28125, + "y" : 229.26171875, + "pressure" : 0.5 + }, + { + "x" : 282.484375, + "y" : 231.37890625, + "pressure" : 0.5 + }, + { + "x" : 286.7890625, + "y" : 233.37890625, + "pressure" : 0.5 + }, + { + "x" : 290.671875, + "y" : 235.3203125, + "pressure" : 0.5 + }, + { + "x" : 293.80078125, + "y" : 237.16796875, + "pressure" : 0.5 + }, + { + "x" : 296.53125, + "y" : 239.20703125, + "pressure" : 0.5 + }, + { + "x" : 299.23828125, + "y" : 241.7265625, + "pressure" : 0.5 + }, + { + "x" : 302.4453125, + "y" : 244.9609375, + "pressure" : 0.5 + }, + { + "x" : 305.953125, + "y" : 248.8671875, + "pressure" : 0.5 + }, + { + "x" : 309.25390625, + "y" : 252.7734375, + "pressure" : 0.5 + }, + { + "x" : 312.421875, + "y" : 256.91015625, + "pressure" : 0.5 + }, + { + "x" : 315.046875, + "y" : 260.9921875, + "pressure" : 0.5 + }, + { + "x" : 316.765625, + "y" : 264.0546875, + "pressure" : 0.5 + }, + { + "x" : 317.83984375, + "y" : 266.20703125, + "pressure" : 0.5 + }, + { + "x" : 318.328125, + "y" : 268.91796875, + "pressure" : 0.5 + }, + { + "x" : 317.6171875, + "y" : 271.2109375, + "pressure" : 0.5 + }, + { + "x" : 315.63671875, + "y" : 273.78515625, + "pressure" : 0.5 + }, + { + "x" : 312.546875, + "y" : 277.48046875, + "pressure" : 0.5 + }, + { + "x" : 308.99609375, + "y" : 281.91015625, + "pressure" : 0.5 + }, + { + "x" : 305.921875, + "y" : 286.015625, + "pressure" : 0.5 + }, + { + "x" : 303.703125, + "y" : 288.9453125, + "pressure" : 0.5 + }, + { + "x" : 302.2734375, + "y" : 290.69921875, + "pressure" : 0.5 + }, + { + "x" : 300.71484375, + "y" : 292.01953125, + "pressure" : 0.5 + }, + { + "x" : 298.37109375, + "y" : 292.50390625, + "pressure" : 0.5 + }, + { + "x" : 295.34375, + "y" : 293.5546875, + "pressure" : 0.5 + }, + { + "x" : 292.171875, + "y" : 295.28515625, + "pressure" : 0.5 + }, + { + "x" : 288.2734375, + "y" : 297.61328125, + "pressure" : 0.5 + }, + { + "x" : 283.984375, + "y" : 300.1796875, + "pressure" : 0.5 + }, + { + "x" : 280.04296875, + "y" : 302.5625, + "pressure" : 0.5 + }, + { + "x" : 277, + "y" : 304.0625, + "pressure" : 0.5 + }, + { + "x" : 273.64453125, + "y" : 305.34375, + "pressure" : 0.5 + }, + { + "x" : 269.0625, + "y" : 306.734375, + "pressure" : 0.5 + }, + { + "x" : 263.1328125, + "y" : 308.0078125, + "pressure" : 0.5 + }, + { + "x" : 255.5078125, + "y" : 309.16015625, + "pressure" : 0.5 + }, + { + "x" : 246.39453125, + "y" : 310.15625, + "pressure" : 0.5 + }, + { + "x" : 236.375, + "y" : 311.09765625, + "pressure" : 0.5 + }, + { + "x" : 226.4296875, + "y" : 311.53125, + "pressure" : 0.5 + }, + { + "x" : 217.31640625, + "y" : 311.53125, + "pressure" : 0.5 + }, + { + "x" : 209.0859375, + "y" : 311.53125, + "pressure" : 0.5 + }, + { + "x" : 201.58203125, + "y" : 311.39453125, + "pressure" : 0.5 + }, + { + "x" : 194.1015625, + "y" : 310.40234375, + "pressure" : 0.5 + }, + { + "x" : 186.015625, + "y" : 307.94921875, + "pressure" : 0.5 + }, + { + "x" : 178.15625, + "y" : 304.08984375, + "pressure" : 0.5 + }, + { + "x" : 172.1640625, + "y" : 300.1953125, + "pressure" : 0.5 + }, + { + "x" : 167.52734375, + "y" : 296.61328125, + "pressure" : 0.5 + }, + { + "x" : 164.140625, + "y" : 293.5, + "pressure" : 0.5 + }, + { + "x" : 163.12109375, + "y" : 290.671875, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 429.8359375, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 425.69140625, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 421.45703125, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 415.8671875, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 411.13671875, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 407.1640625, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 402.3359375, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 396.98046875, + "y" : 229.109375, + "pressure" : 0.5 + }, + { + "x" : 390.4609375, + "y" : 229.671875, + "pressure" : 0.5 + }, + { + "x" : 382.9375, + "y" : 232.16796875, + "pressure" : 0.5 + }, + { + "x" : 375.1953125, + "y" : 236.640625, + "pressure" : 0.5 + }, + { + "x" : 368.234375, + "y" : 241.7890625, + "pressure" : 0.5 + }, + { + "x" : 362.59765625, + "y" : 246.90234375, + "pressure" : 0.5 + }, + { + "x" : 358.078125, + "y" : 251.78125, + "pressure" : 0.5 + }, + { + "x" : 354.9453125, + "y" : 255.9765625, + "pressure" : 0.5 + }, + { + "x" : 352.72265625, + "y" : 259.71484375, + "pressure" : 0.5 + }, + { + "x" : 351.12890625, + "y" : 263.140625, + "pressure" : 0.5 + }, + { + "x" : 350.2734375, + "y" : 265.6875, + "pressure" : 0.5 + }, + { + "x" : 349.98828125, + "y" : 267.69140625, + "pressure" : 0.5 + }, + { + "x" : 350.6328125, + "y" : 272.265625, + "pressure" : 0.5 + }, + { + "x" : 352.98046875, + "y" : 276.3984375, + "pressure" : 0.5 + }, + { + "x" : 356.45703125, + "y" : 281.38671875, + "pressure" : 0.5 + }, + { + "x" : 360.0234375, + "y" : 286.5390625, + "pressure" : 0.5 + }, + { + "x" : 362.8671875, + "y" : 290.8046875, + "pressure" : 0.5 + }, + { + "x" : 364.58984375, + "y" : 293.18359375, + "pressure" : 0.5 + }, + { + "x" : 366.95703125, + "y" : 294.72265625, + "pressure" : 0.5 + }, + { + "x" : 369.81640625, + "y" : 294.984375, + "pressure" : 0.5 + }, + { + "x" : 376.92578125, + "y" : 294.984375, + "pressure" : 0.5 + }, + { + "x" : 387.6015625, + "y" : 294.984375, + "pressure" : 0.5 + }, + { + "x" : 400.12109375, + "y" : 294.984375, + "pressure" : 0.5 + }, + { + "x" : 414.05859375, + "y" : 294.984375, + "pressure" : 0.5 + }, + { + "x" : 425.6484375, + "y" : 294.84375, + "pressure" : 0.5 + }, + { + "x" : 433.58984375, + "y" : 294.2578125, + "pressure" : 0.5 + }, + { + "x" : 439.1484375, + "y" : 293.41796875, + "pressure" : 0.5 + }, + { + "x" : 443.3515625, + "y" : 292.2890625, + "pressure" : 0.5 + }, + { + "x" : 447.48046875, + "y" : 290.3828125, + "pressure" : 0.5 + }, + { + "x" : 452.1953125, + "y" : 287.7265625, + "pressure" : 0.5 + }, + { + "x" : 457.12109375, + "y" : 284.7421875, + "pressure" : 0.5 + }, + { + "x" : 462.0390625, + "y" : 281.765625, + "pressure" : 0.5 + }, + { + "x" : 466.38671875, + "y" : 279.16015625, + "pressure" : 0.5 + }, + { + "x" : 469.1640625, + "y" : 277.26171875, + "pressure" : 0.5 + }, + { + "x" : 471.1015625, + "y" : 275.671875, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 481.84375, + "y" : 220.3515625, + "pressure" : 0.5 + }, + { + "x" : 481.84375, + "y" : 225.75, + "pressure" : 0.5 + }, + { + "x" : 481.84375, + "y" : 235.33984375, + "pressure" : 0.5 + }, + { + "x" : 482.40234375, + "y" : 247.2265625, + "pressure" : 0.5 + }, + { + "x" : 484.0234375, + "y" : 259.1171875, + "pressure" : 0.5 + }, + { + "x" : 485.58203125, + "y" : 267.515625, + "pressure" : 0.5 + }, + { + "x" : 486.71484375, + "y" : 273.125, + "pressure" : 0.5 + }, + { + "x" : 487.87109375, + "y" : 277.17578125, + "pressure" : 0.5 + }, + { + "x" : 488.4375, + "y" : 279.2265625, + "pressure" : 0.5 + }, + { + "x" : 489.70703125, + "y" : 282.98046875, + "pressure" : 0.5 + }, + { + "x" : 490.40625, + "y" : 285.7890625, + "pressure" : 0.5 + }, + { + "x" : 491.29296875, + "y" : 288.8359375, + "pressure" : 0.5 + }, + { + "x" : 492.16796875, + "y" : 291.63671875, + "pressure" : 0.5 + }, + { + "x" : 492.78515625, + "y" : 293.703125, + "pressure" : 0.5 + }, + { + "x" : 493.3671875, + "y" : 295.85546875, + "pressure" : 0.5 + }, + { + "x" : 492.265625, + "y" : 292.65625, + "pressure" : 0.5 + }, + { + "x" : 491.078125, + "y" : 290.3203125, + "pressure" : 0.5 + }, + { + "x" : 488.48046875, + "y" : 283.796875, + "pressure" : 0.5 + }, + { + "x" : 485.13671875, + "y" : 274.35546875, + "pressure" : 0.5 + }, + { + "x" : 482.390625, + "y" : 264.54296875, + "pressure" : 0.5 + }, + { + "x" : 480.8203125, + "y" : 252.20703125, + "pressure" : 0.5 + }, + { + "x" : 480.56640625, + "y" : 239.078125, + "pressure" : 0.5 + }, + { + "x" : 480.56640625, + "y" : 228.625, + "pressure" : 0.5 + }, + { + "x" : 480.90234375, + "y" : 221.51953125, + "pressure" : 0.5 + }, + { + "x" : 481.73046875, + "y" : 216.66015625, + "pressure" : 0.5 + }, + { + "x" : 482.51953125, + "y" : 214.2421875, + "pressure" : 0.5 + }, + { + "x" : 484.6875, + "y" : 212.15625, + "pressure" : 0.5 + }, + { + "x" : 488.0859375, + "y" : 210.8515625, + "pressure" : 0.5 + }, + { + "x" : 493.640625, + "y" : 209.37109375, + "pressure" : 0.5 + }, + { + "x" : 500.91796875, + "y" : 207.9609375, + "pressure" : 0.5 + }, + { + "x" : 509.38671875, + "y" : 206.8828125, + "pressure" : 0.5 + }, + { + "x" : 518.25, + "y" : 206.28515625, + "pressure" : 0.5 + }, + { + "x" : 525.6328125, + "y" : 206.04296875, + "pressure" : 0.5 + }, + { + "x" : 531.3671875, + "y" : 206.04296875, + "pressure" : 0.5 + }, + { + "x" : 535.93359375, + "y" : 206.04296875, + "pressure" : 0.5 + }, + { + "x" : 538.55078125, + "y" : 206.0625, + "pressure" : 0.5 + }, + { + "x" : 540.37890625, + "y" : 206.9453125, + "pressure" : 0.5 + }, + { + "x" : 540.8515625, + "y" : 209.51171875, + "pressure" : 0.5 + }, + { + "x" : 540.8515625, + "y" : 212.3671875, + "pressure" : 0.5 + }, + { + "x" : 540.8515625, + "y" : 215.93359375, + "pressure" : 0.5 + }, + { + "x" : 540.30859375, + "y" : 219.1171875, + "pressure" : 0.5 + }, + { + "x" : 539.08203125, + "y" : 221.9140625, + "pressure" : 0.5 + }, + { + "x" : 537.22265625, + "y" : 224.546875, + "pressure" : 0.5 + }, + { + "x" : 534.52734375, + "y" : 227.49609375, + "pressure" : 0.5 + }, + { + "x" : 531.59765625, + "y" : 230.1796875, + "pressure" : 0.5 + }, + { + "x" : 527.765625, + "y" : 233.26171875, + "pressure" : 0.5 + }, + { + "x" : 523.78515625, + "y" : 236.3046875, + "pressure" : 0.5 + }, + { + "x" : 520.59375, + "y" : 238.38671875, + "pressure" : 0.5 + }, + { + "x" : 518.1875, + "y" : 239.71484375, + "pressure" : 0.5 + }, + { + "x" : 516.16796875, + "y" : 240.28125, + "pressure" : 0.5 + }, + { + "x" : 513.89453125, + "y" : 240.28125, + "pressure" : 0.5 + }, + { + "x" : 510.453125, + "y" : 241.3046875, + "pressure" : 0.5 + }, + { + "x" : 505.7265625, + "y" : 243.58203125, + "pressure" : 0.5 + }, + { + "x" : 500.69140625, + "y" : 245.96484375, + "pressure" : 0.5 + }, + { + "x" : 496.390625, + "y" : 247.85546875, + "pressure" : 0.5 + }, + { + "x" : 493.20703125, + "y" : 249.2890625, + "pressure" : 0.5 + }, + { + "x" : 491.34765625, + "y" : 250.09375, + "pressure" : 0.5 + }, + { + "x" : 494.01171875, + "y" : 250.62109375, + "pressure" : 0.5 + }, + { + "x" : 496.7890625, + "y" : 251.66015625, + "pressure" : 0.5 + }, + { + "x" : 499.4140625, + "y" : 252.82421875, + "pressure" : 0.5 + }, + { + "x" : 503.609375, + "y" : 254.74609375, + "pressure" : 0.5 + }, + { + "x" : 509.73828125, + "y" : 257.45703125, + "pressure" : 0.5 + }, + { + "x" : 518.52734375, + "y" : 261.6015625, + "pressure" : 0.5 + }, + { + "x" : 530.51953125, + "y" : 267.59765625, + "pressure" : 0.5 + }, + { + "x" : 543.96875, + "y" : 274.47265625, + "pressure" : 0.5 + }, + { + "x" : 556.94921875, + "y" : 281.83203125, + "pressure" : 0.5 + }, + { + "x" : 568.32421875, + "y" : 288.76171875, + "pressure" : 0.5 + }, + { + "x" : 576.05859375, + "y" : 293.38671875, + "pressure" : 0.5 + }, + { + "x" : 580.609375, + "y" : 296.2421875, + "pressure" : 0.5 + }, + { + "x" : 583.375, + "y" : 298.109375, + "pressure" : 0.5 + }, + { + "x" : 585.140625, + "y" : 299.421875, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 593.36328125, + "y" : 205.30859375, + "pressure" : 0.5 + }, + { + "x" : 593.61328125, + "y" : 212.31640625, + "pressure" : 0.5 + }, + { + "x" : 593.61328125, + "y" : 221.25390625, + "pressure" : 0.5 + }, + { + "x" : 594.58984375, + "y" : 233.94140625, + "pressure" : 0.5 + }, + { + "x" : 595.96875, + "y" : 246.05078125, + "pressure" : 0.5 + }, + { + "x" : 597.59765625, + "y" : 256.0703125, + "pressure" : 0.5 + }, + { + "x" : 599.3828125, + "y" : 264.45703125, + "pressure" : 0.5 + }, + { + "x" : 600.51953125, + "y" : 269.796875, + "pressure" : 0.5 + }, + { + "x" : 601.44921875, + "y" : 273.33203125, + "pressure" : 0.5 + }, + { + "x" : 601.99609375, + "y" : 275.51953125, + "pressure" : 0.5 + }, + { + "x" : 603.34375, + "y" : 279.125, + "pressure" : 0.5 + }, + { + "x" : 604.4375, + "y" : 281.703125, + "pressure" : 0.5 + }, + { + "x" : 605.39453125, + "y" : 284.21484375, + "pressure" : 0.5 + }, + { + "x" : 606.125, + "y" : 286.08203125, + "pressure" : 0.5 + }, + { + "x" : 607.17578125, + "y" : 288.0234375, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 636.6484375, + "y" : 199.66015625, + "pressure" : 0.5 + }, + { + "x" : 636.6484375, + "y" : 204.68359375, + "pressure" : 0.5 + }, + { + "x" : 636.84765625, + "y" : 216.26953125, + "pressure" : 0.5 + }, + { + "x" : 638.375, + "y" : 229.6953125, + "pressure" : 0.5 + }, + { + "x" : 640.97265625, + "y" : 243.08203125, + "pressure" : 0.5 + }, + { + "x" : 643.13671875, + "y" : 253.87109375, + "pressure" : 0.5 + }, + { + "x" : 644.8125, + "y" : 260.45703125, + "pressure" : 0.5 + }, + { + "x" : 645.87890625, + "y" : 265.12890625, + "pressure" : 0.5 + }, + { + "x" : 646.4765625, + "y" : 267.65625, + "pressure" : 0.5 + }, + { + "x" : 647.41015625, + "y" : 269.75390625, + "pressure" : 0.5 + }, + { + "x" : 648.23828125, + "y" : 271.75, + "pressure" : 0.5 + }, + { + "x" : 648.8203125, + "y" : 274.5234375, + "pressure" : 0.5 + }, + { + "x" : 649.125, + "y" : 277.40625, + "pressure" : 0.5 + }, + { + "x" : 649.54296875, + "y" : 279.51953125, + "pressure" : 0.5 + }, + { + "x" : 650.48046875, + "y" : 282.015625, + "pressure" : 0.5 + }, + { + "x" : 650.95703125, + "y" : 284.31640625, + "pressure" : 0.5 + }, + { + "x" : 651.62890625, + "y" : 286.4296875, + "pressure" : 0.5 + }, + { + "x" : 654.0703125, + "y" : 286.5859375, + "pressure" : 0.5 + }, + { + "x" : 657.87890625, + "y" : 285.67578125, + "pressure" : 0.5 + }, + { + "x" : 664.7890625, + "y" : 284.53125, + "pressure" : 0.5 + }, + { + "x" : 673.90625, + "y" : 283.0390625, + "pressure" : 0.5 + }, + { + "x" : 681.234375, + "y" : 281.6875, + "pressure" : 0.5 + }, + { + "x" : 686.3046875, + "y" : 280.26953125, + "pressure" : 0.5 + }, + { + "x" : 690.2578125, + "y" : 278.765625, + "pressure" : 0.5 + }, + { + "x" : 692.40625, + "y" : 277.42578125, + "pressure" : 0.5 + }, + { + "x" : 693.87109375, + "y" : 275, + "pressure" : 0.5 + }, + { + "x" : 693.98046875, + "y" : 272.515625, + "pressure" : 0.5 + }, + { + "x" : 693.765625, + "y" : 270.37890625, + "pressure" : 0.5 + }, + { + "x" : 693.6875, + "y" : 267.90625, + "pressure" : 0.5 + }, + { + "x" : 693.6875, + "y" : 265.29296875, + "pressure" : 0.5 + }, + { + "x" : 693.6875, + "y" : 263.01171875, + "pressure" : 0.5 + }, + { + "x" : 693.65234375, + "y" : 259.22265625, + "pressure" : 0.5 + }, + { + "x" : 692.5078125, + "y" : 253.6953125, + "pressure" : 0.5 + }, + { + "x" : 690.36328125, + "y" : 248.2578125, + "pressure" : 0.5 + }, + { + "x" : 688.26171875, + "y" : 244.4921875, + "pressure" : 0.5 + }, + { + "x" : 686.578125, + "y" : 242.1875, + "pressure" : 0.5 + }, + { + "x" : 684.73046875, + "y" : 241.05078125, + "pressure" : 0.5 + }, + { + "x" : 681.7890625, + "y" : 241.15234375, + "pressure" : 0.5 + }, + { + "x" : 678.796875, + "y" : 242.0546875, + "pressure" : 0.5 + }, + { + "x" : 676.78125, + "y" : 242.390625, + "pressure" : 0.5 + }, + { + "x" : 680.578125, + "y" : 238.8046875, + "pressure" : 0.5 + }, + { + "x" : 685.9296875, + "y" : 235.6640625, + "pressure" : 0.5 + }, + { + "x" : 692.8125, + "y" : 231.96875, + "pressure" : 0.5 + }, + { + "x" : 700.3671875, + "y" : 227.94921875, + "pressure" : 0.5 + }, + { + "x" : 706.7578125, + "y" : 224.47265625, + "pressure" : 0.5 + }, + { + "x" : 711.67578125, + "y" : 221.6171875, + "pressure" : 0.5 + }, + { + "x" : 715.71875, + "y" : 218.7265625, + "pressure" : 0.5 + }, + { + "x" : 718.04296875, + "y" : 216.72265625, + "pressure" : 0.5 + }, + { + "x" : 719.75, + "y" : 214.90234375, + "pressure" : 0.5 + }, + { + "x" : 719.8671875, + "y" : 212.1796875, + "pressure" : 0.5 + }, + { + "x" : 718.1796875, + "y" : 209.375, + "pressure" : 0.5 + }, + { + "x" : 714.6171875, + "y" : 205.8671875, + "pressure" : 0.5 + }, + { + "x" : 710.453125, + "y" : 202.67578125, + "pressure" : 0.5 + }, + { + "x" : 706.34375, + "y" : 200.234375, + "pressure" : 0.5 + }, + { + "x" : 702.69140625, + "y" : 198.53515625, + "pressure" : 0.5 + }, + { + "x" : 699.48828125, + "y" : 197.25390625, + "pressure" : 0.5 + }, + { + "x" : 696.421875, + "y" : 196.1640625, + "pressure" : 0.5 + }, + { + "x" : 692.2734375, + "y" : 194.578125, + "pressure" : 0.5 + }, + { + "x" : 686.59765625, + "y" : 192.5078125, + "pressure" : 0.5 + }, + { + "x" : 680.375, + "y" : 190.88671875, + "pressure" : 0.5 + }, + { + "x" : 674.41015625, + "y" : 189.83203125, + "pressure" : 0.5 + }, + { + "x" : 669.19921875, + "y" : 189.15625, + "pressure" : 0.5 + }, + { + "x" : 665.01953125, + "y" : 188.95703125, + "pressure" : 0.5 + }, + { + "x" : 661.21484375, + "y" : 188.95703125, + "pressure" : 0.5 + }, + { + "x" : 656.84765625, + "y" : 188.95703125, + "pressure" : 0.5 + }, + { + "x" : 652.46484375, + "y" : 188.95703125, + "pressure" : 0.5 + }, + { + "x" : 648.41015625, + "y" : 189.1796875, + "pressure" : 0.5 + }, + { + "x" : 644.91015625, + "y" : 189.71875, + "pressure" : 0.5 + }, + { + "x" : 642.625, + "y" : 190.19140625, + "pressure" : 0.5 + }, + { + "x" : 640.671875, + "y" : 190.65625, + "pressure" : 0.5 + }, + { + "x" : 637.68359375, + "y" : 190.859375, + "pressure" : 0.5 + }, + { + "x" : 635.23828125, + "y" : 190.859375, + "pressure" : 0.5 + }, + { + "x" : 632.421875, + "y" : 190.859375, + "pressure" : 0.5 + }, + { + "x" : 630.2578125, + "y" : 190.859375, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 735.640625, + "y" : 191.85546875, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 197.19140625, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 207.3515625, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 220.00390625, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 236.88671875, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 253.30078125, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 263.01171875, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 270.3359375, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 275.1953125, + "pressure" : 0.5 + }, + { + "x" : 735.8671875, + "y" : 277.4296875, + "pressure" : 0.5 + }, + { + "x" : 738.3359375, + "y" : 278.1953125, + "pressure" : 0.5 + }, + { + "x" : 744.2265625, + "y" : 278.1953125, + "pressure" : 0.5 + }, + { + "x" : 755.828125, + "y" : 278.1953125, + "pressure" : 0.5 + }, + { + "x" : 770.25, + "y" : 278.1953125, + "pressure" : 0.5 + }, + { + "x" : 785.2890625, + "y" : 278.1953125, + "pressure" : 0.5 + }, + { + "x" : 800.23046875, + "y" : 277.91015625, + "pressure" : 0.5 + }, + { + "x" : 810.90625, + "y" : 277.23828125, + "pressure" : 0.5 + }, + { + "x" : 817.84765625, + "y" : 276.01953125, + "pressure" : 0.5 + }, + { + "x" : 823.51171875, + "y" : 274.3203125, + "pressure" : 0.5 + }, + { + "x" : 826.94921875, + "y" : 272.5703125, + "pressure" : 0.5 + }, + { + "x" : 828.58203125, + "y" : 270.921875, + "pressure" : 0.5 + }, + { + "x" : 829.26171875, + "y" : 268.21484375, + "pressure" : 0.5 + }, + { + "x" : 829.26171875, + "y" : 262.62890625, + "pressure" : 0.5 + }, + { + "x" : 829.171875, + "y" : 258.72265625, + "pressure" : 0.5 + }, + { + "x" : 828.984375, + "y" : 255.859375, + "pressure" : 0.5 + }, + { + "x" : 828.73046875, + "y" : 253.78125, + "pressure" : 0.5 + }, + { + "x" : 826.94921875, + "y" : 251.1484375, + "pressure" : 0.5 + }, + { + "x" : 824.2265625, + "y" : 249.28515625, + "pressure" : 0.5 + }, + { + "x" : 820.109375, + "y" : 246.80859375, + "pressure" : 0.5 + }, + { + "x" : 815.87890625, + "y" : 244.60546875, + "pressure" : 0.5 + }, + { + "x" : 811.83203125, + "y" : 242.96484375, + "pressure" : 0.5 + }, + { + "x" : 808.04296875, + "y" : 241.59765625, + "pressure" : 0.5 + }, + { + "x" : 805.01171875, + "y" : 240.90234375, + "pressure" : 0.5 + }, + { + "x" : 801.375, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 796.4296875, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 790.19140625, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 783.80078125, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 778.09765625, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 773.58203125, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 771.0390625, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 768.54296875, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 765.90234375, + "y" : 240.73046875, + "pressure" : 0.5 + }, + { + "x" : 763.83203125, + "y" : 240.59765625, + "pressure" : 0.5 + }, + { + "x" : 769.14453125, + "y" : 239.9609375, + "pressure" : 0.5 + }, + { + "x" : 779.80078125, + "y" : 237.546875, + "pressure" : 0.5 + }, + { + "x" : 796.64453125, + "y" : 232.69140625, + "pressure" : 0.5 + }, + { + "x" : 815.82421875, + "y" : 226.9453125, + "pressure" : 0.5 + }, + { + "x" : 833.96484375, + "y" : 221.73828125, + "pressure" : 0.5 + }, + { + "x" : 847.22265625, + "y" : 217.77734375, + "pressure" : 0.5 + }, + { + "x" : 855.58984375, + "y" : 214.7421875, + "pressure" : 0.5 + }, + { + "x" : 861.16015625, + "y" : 212.4609375, + "pressure" : 0.5 + }, + { + "x" : 863.8671875, + "y" : 211.0234375, + "pressure" : 0.5 + }, + { + "x" : 865.4453125, + "y" : 208.98046875, + "pressure" : 0.5 + }, + { + "x" : 865.59375, + "y" : 206.3515625, + "pressure" : 0.5 + }, + { + "x" : 865.07421875, + "y" : 201.7578125, + "pressure" : 0.5 + }, + { + "x" : 862.62890625, + "y" : 195.6640625, + "pressure" : 0.5 + }, + { + "x" : 858.8359375, + "y" : 190.07421875, + "pressure" : 0.5 + }, + { + "x" : 854.58203125, + "y" : 185.66015625, + "pressure" : 0.5 + }, + { + "x" : 849.64453125, + "y" : 182.19140625, + "pressure" : 0.5 + }, + { + "x" : 844.9140625, + "y" : 180.1328125, + "pressure" : 0.5 + }, + { + "x" : 840.25, + "y" : 179.12890625, + "pressure" : 0.5 + }, + { + "x" : 834.62890625, + "y" : 178.62890625, + "pressure" : 0.5 + }, + { + "x" : 827.546875, + "y" : 178.4296875, + "pressure" : 0.5 + }, + { + "x" : 818.50390625, + "y" : 178.4296875, + "pressure" : 0.5 + }, + { + "x" : 808.50390625, + "y" : 178.4296875, + "pressure" : 0.5 + }, + { + "x" : 798.7109375, + "y" : 178.4296875, + "pressure" : 0.5 + }, + { + "x" : 789.984375, + "y" : 178.4296875, + "pressure" : 0.5 + }, + { + "x" : 782.99609375, + "y" : 178.4296875, + "pressure" : 0.5 + }, + { + "x" : 776.75, + "y" : 178.4296875, + "pressure" : 0.5 + }, + { + "x" : 770.921875, + "y" : 178.65234375, + "pressure" : 0.5 + }, + { + "x" : 766.28125, + "y" : 179.25390625, + "pressure" : 0.5 + }, + { + "x" : 763, + "y" : 180.01171875, + "pressure" : 0.5 + }, + { + "x" : 760.9296875, + "y" : 180.81640625, + "pressure" : 0.5 + }, + { + "x" : 758.75, + "y" : 182.44921875, + "pressure" : 0.5 + }, + { + "x" : 754.37890625, + "y" : 184.2734375, + "pressure" : 0.5 + }, + { + "x" : 750.9140625, + "y" : 185.40625, + "pressure" : 0.5 + }, + { + "x" : 747.4921875, + "y" : 186.46484375, + "pressure" : 0.5 + }, + { + "x" : 745.5, + "y" : 187.0703125, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 902.2890625, + "y" : 189.70703125, + "pressure" : 0.5 + }, + { + "x" : 902.03125, + "y" : 191.859375, + "pressure" : 0.5 + }, + { + "x" : 901.6484375, + "y" : 197.55859375, + "pressure" : 0.5 + }, + { + "x" : 899.62890625, + "y" : 209.62890625, + "pressure" : 0.5 + }, + { + "x" : 896.21484375, + "y" : 226.484375, + "pressure" : 0.5 + }, + { + "x" : 893.37890625, + "y" : 243.8515625, + "pressure" : 0.5 + }, + { + "x" : 892.31640625, + "y" : 255.28515625, + "pressure" : 0.5 + }, + { + "x" : 892.31640625, + "y" : 261.5078125, + "pressure" : 0.5 + }, + { + "x" : 892.31640625, + "y" : 266.3046875, + "pressure" : 0.5 + }, + { + "x" : 892.6953125, + "y" : 268.296875, + "pressure" : 0.5 + }, + { + "x" : 897.06640625, + "y" : 268.359375, + "pressure" : 0.5 + }, + { + "x" : 903.5859375, + "y" : 269.1875, + "pressure" : 0.5 + }, + { + "x" : 913.6015625, + "y" : 270.59765625, + "pressure" : 0.5 + }, + { + "x" : 924.18359375, + "y" : 272.4453125, + "pressure" : 0.5 + }, + { + "x" : 933.1484375, + "y" : 274.35546875, + "pressure" : 0.5 + }, + { + "x" : 939.34765625, + "y" : 275.46875, + "pressure" : 0.5 + }, + { + "x" : 942.69921875, + "y" : 276.19140625, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 966.49609375, + "y" : 200.828125, + "pressure" : 0.5 + }, + { + "x" : 966.49609375, + "y" : 208.34765625, + "pressure" : 0.5 + }, + { + "x" : 966.49609375, + "y" : 226.41796875, + "pressure" : 0.5 + }, + { + "x" : 966.49609375, + "y" : 251.2265625, + "pressure" : 0.5 + }, + { + "x" : 966.49609375, + "y" : 271.83203125, + "pressure" : 0.5 + }, + { + "x" : 966.49609375, + "y" : 286.38671875, + "pressure" : 0.5 + }, + { + "x" : 966.49609375, + "y" : 295.296875, + "pressure" : 0.5 + }, + { + "x" : 966.49609375, + "y" : 299.89453125, + "pressure" : 0.5 + }, + { + "x" : 972.47265625, + "y" : 300.40625, + "pressure" : 0.5 + }, + { + "x" : 980.83984375, + "y" : 300.0625, + "pressure" : 0.5 + }, + { + "x" : 989.83984375, + "y" : 300.0625, + "pressure" : 0.5 + }, + { + "x" : 997.171875, + "y" : 300.0625, + "pressure" : 0.5 + }, + { + "x" : 1002.6328125, + "y" : 300.0625, + "pressure" : 0.5 + }, + { + "x" : 1005.71484375, + "y" : 300.0625, + "pressure" : 0.5 + }, + { + "x" : 1007.82421875, + "y" : 299.94140625, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 975.96875, + "y" : 235.265625, + "pressure" : 0.5 + }, + { + "x" : 982.140625, + "y" : 235.265625, + "pressure" : 0.5 + }, + { + "x" : 992.63671875, + "y" : 235.265625, + "pressure" : 0.5 + }, + { + "x" : 1004.04296875, + "y" : 235.265625, + "pressure" : 0.5 + }, + { + "x" : 1013.06640625, + "y" : 235.265625, + "pressure" : 0.5 + }, + { + "x" : 1019.109375, + "y" : 235.265625, + "pressure" : 0.5 + }, + { + "x" : 1022.8671875, + "y" : 235.265625, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 978.14453125, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 982.51171875, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 991.06640625, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 1004.29296875, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 1017.33984375, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 1027.78515625, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 1035.0234375, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 1039.37109375, + "y" : 191.77734375, + "pressure" : 0.5 + }, + { + "x" : 1041.6328125, + "y" : 191.77734375, + "pressure" : 0.5 + } + ], + "color" : 4278190080, + "width" : 10 + }, + { + "points" : [ + { + "x" : 144.54296875, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 153.4140625, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 173.51953125, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 201.87890625, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 230.80078125, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 257.30859375, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 281.8359375, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 303.10546875, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 323.2265625, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 344.8359375, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 367.10546875, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 385.41015625, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 401.57421875, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 422.78125, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 442.484375, + "y" : 397.75, + "pressure" : 0.5 + }, + { + "x" : 459.3984375, + "y" : 397.4453125, + "pressure" : 0.5 + }, + { + "x" : 478.75, + "y" : 396.1953125, + "pressure" : 0.5 + }, + { + "x" : 496.08984375, + "y" : 394.640625, + "pressure" : 0.5 + }, + { + "x" : 511.65625, + "y" : 393.4296875, + "pressure" : 0.5 + }, + { + "x" : 529.50390625, + "y" : 392.15625, + "pressure" : 0.5 + }, + { + "x" : 550.31640625, + "y" : 390.65234375, + "pressure" : 0.5 + }, + { + "x" : 574.24609375, + "y" : 389.234375, + "pressure" : 0.5 + }, + { + "x" : 602.31640625, + "y" : 388.45703125, + "pressure" : 0.5 + }, + { + "x" : 632.43359375, + "y" : 388.265625, + "pressure" : 0.5 + }, + { + "x" : 662.375, + "y" : 388.265625, + "pressure" : 0.5 + }, + { + "x" : 693.21875, + "y" : 388.55078125, + "pressure" : 0.5 + }, + { + "x" : 725.52734375, + "y" : 389.640625, + "pressure" : 0.5 + }, + { + "x" : 758.03125, + "y" : 390.84765625, + "pressure" : 0.5 + }, + { + "x" : 797.45703125, + "y" : 392.16796875, + "pressure" : 0.5 + }, + { + "x" : 835.4765625, + "y" : 393.83984375, + "pressure" : 0.5 + }, + { + "x" : 874.89453125, + "y" : 396.1328125, + "pressure" : 0.5 + }, + { + "x" : 919.2265625, + "y" : 399.44140625, + "pressure" : 0.5 + }, + { + "x" : 964.10546875, + "y" : 403.66015625, + "pressure" : 0.5 + }, + { + "x" : 1002.12109375, + "y" : 407.11328125, + "pressure" : 0.5 + }, + { + "x" : 1023.1796875, + "y" : 408.71484375, + "pressure" : 0.5 + }, + { + "x" : 1036.48046875, + "y" : 409.8359375, + "pressure" : 0.5 + }, + { + "x" : 1045.203125, + "y" : 410.90234375, + "pressure" : 0.5 + }, + { + "x" : 1049.87109375, + "y" : 411.83203125, + "pressure" : 0.5 + } + ], + "color" : 4294198070, + "width" : 10 + }, + { + "points" : [ + { + "x" : 87.84375, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 90.1015625, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 98.1953125, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 119.21875, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 150.33984375, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 180.2109375, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 211.8828125, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 247.37890625, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 282.55859375, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 317.80078125, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 351.99609375, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 385.56640625, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 418.0703125, + "y" : 455.52734375, + "pressure" : 0.5 + }, + { + "x" : 452.60546875, + "y" : 455.68359375, + "pressure" : 0.5 + }, + { + "x" : 489.12890625, + "y" : 456.69140625, + "pressure" : 0.5 + }, + { + "x" : 529.953125, + "y" : 457.78515625, + "pressure" : 0.5 + }, + { + "x" : 573.80078125, + "y" : 458.02734375, + "pressure" : 0.5 + }, + { + "x" : 621.8671875, + "y" : 456.33984375, + "pressure" : 0.5 + }, + { + "x" : 671.36328125, + "y" : 453.17578125, + "pressure" : 0.5 + }, + { + "x" : 715.828125, + "y" : 450.87109375, + "pressure" : 0.5 + }, + { + "x" : 764.6796875, + "y" : 450.04296875, + "pressure" : 0.5 + }, + { + "x" : 816.48046875, + "y" : 452.01171875, + "pressure" : 0.5 + }, + { + "x" : 859.33203125, + "y" : 455.94140625, + "pressure" : 0.5 + }, + { + "x" : 897.88671875, + "y" : 460.53125, + "pressure" : 0.5 + }, + { + "x" : 936.4609375, + "y" : 465.3046875, + "pressure" : 0.5 + }, + { + "x" : 969.51171875, + "y" : 469.79296875, + "pressure" : 0.5 + }, + { + "x" : 995.38671875, + "y" : 474.125, + "pressure" : 0.5 + }, + { + "x" : 1013.5078125, + "y" : 477.3828125, + "pressure" : 0.5 + }, + { + "x" : 1027.7578125, + "y" : 479.83203125, + "pressure" : 0.5 + }, + { + "x" : 1041.015625, + "y" : 482.109375, + "pressure" : 0.5 + }, + { + "x" : 1052.9609375, + "y" : 484.30078125, + "pressure" : 0.5 + }, + { + "x" : 1063.9375, + "y" : 486.30859375, + "pressure" : 0.5 + }, + { + "x" : 1073.48046875, + "y" : 488.09765625, + "pressure" : 0.5 + }, + { + "x" : 1081.84765625, + "y" : 489.7734375, + "pressure" : 0.5 + }, + { + "x" : 1089.45703125, + "y" : 491.265625, + "pressure" : 0.5 + }, + { + "x" : 1097.453125, + "y" : 492.61328125, + "pressure" : 0.5 + }, + { + "x" : 1106.1875, + "y" : 494.01953125, + "pressure" : 0.5 + }, + { + "x" : 1114.51171875, + "y" : 495.6796875, + "pressure" : 0.5 + }, + { + "x" : 1120.7265625, + "y" : 497.1875, + "pressure" : 0.5 + }, + { + "x" : 1125.515625, + "y" : 498.27734375, + "pressure" : 0.5 + }, + { + "x" : 1129.09375, + "y" : 499.09765625, + "pressure" : 0.5 + }, + { + "x" : 1131.3203125, + "y" : 499.953125, + "pressure" : 0.5 + } + ], + "color" : 4283215696, + "width" : 10 + }, + { + "points" : [ + { + "x" : 45, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 47.19921875, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 57.63671875, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 80.4453125, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 114.54296875, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 161.18359375, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 215.58984375, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 274.38671875, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 336.0234375, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 395.21875, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 453.32421875, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 502.61328125, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 544.34765625, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 586.796875, + "y" : 529.48046875, + "pressure" : 0.5 + }, + { + "x" : 625.08984375, + "y" : 529.9453125, + "pressure" : 0.5 + }, + { + "x" : 658.23828125, + "y" : 531.1953125, + "pressure" : 0.5 + }, + { + "x" : 683.140625, + "y" : 533.07421875, + "pressure" : 0.5 + }, + { + "x" : 703.1796875, + "y" : 535.4140625, + "pressure" : 0.5 + }, + { + "x" : 721.48046875, + "y" : 537.5625, + "pressure" : 0.5 + }, + { + "x" : 735.83203125, + "y" : 539.02734375, + "pressure" : 0.5 + }, + { + "x" : 748.90234375, + "y" : 540.16015625, + "pressure" : 0.5 + }, + { + "x" : 762.69921875, + "y" : 540.87109375, + "pressure" : 0.5 + }, + { + "x" : 778.484375, + "y" : 541.0078125, + "pressure" : 0.5 + }, + { + "x" : 796.59765625, + "y" : 541.53125, + "pressure" : 0.5 + }, + { + "x" : 813.23046875, + "y" : 542.390625, + "pressure" : 0.5 + }, + { + "x" : 833.28515625, + "y" : 543.71875, + "pressure" : 0.5 + }, + { + "x" : 856.890625, + "y" : 546.01953125, + "pressure" : 0.5 + }, + { + "x" : 880.9453125, + "y" : 549.59375, + "pressure" : 0.5 + }, + { + "x" : 909.68359375, + "y" : 554.23828125, + "pressure" : 0.5 + }, + { + "x" : 941.38671875, + "y" : 559.08203125, + "pressure" : 0.5 + }, + { + "x" : 973.2265625, + "y" : 564.26953125, + "pressure" : 0.5 + }, + { + "x" : 998.9921875, + "y" : 568.25, + "pressure" : 0.5 + }, + { + "x" : 1019.7265625, + "y" : 570.18359375, + "pressure" : 0.5 + }, + { + "x" : 1036.890625, + "y" : 571.359375, + "pressure" : 0.5 + }, + { + "x" : 1047.54296875, + "y" : 571.85546875, + "pressure" : 0.5 + }, + { + "x" : 1055.83984375, + "y" : 571.85546875, + "pressure" : 0.5 + }, + { + "x" : 1064.10546875, + "y" : 571.85546875, + "pressure" : 0.5 + }, + { + "x" : 1073.73828125, + "y" : 572.0390625, + "pressure" : 0.5 + }, + { + "x" : 1085.93359375, + "y" : 572.34765625, + "pressure" : 0.5 + }, + { + "x" : 1098.15625, + "y" : 572.765625, + "pressure" : 0.5 + }, + { + "x" : 1108.1796875, + "y" : 573.05859375, + "pressure" : 0.5 + }, + { + "x" : 1115.2265625, + "y" : 573.05859375, + "pressure" : 0.5 + }, + { + "x" : 1118.890625, + "y" : 573.2265625, + "pressure" : 0.5 + }, + { + "x" : 1121.6953125, + "y" : 573.39453125, + "pressure" : 0.5 + }, + { + "x" : 1124.24609375, + "y" : 573.39453125, + "pressure" : 0.5 + }, + { + "x" : 1125.8671875, + "y" : 575.02734375, + "pressure" : 0.5 + } + ], + "color" : 4280391411, + "width" : 10 + }, + { + "points" : [ + { + "x" : 16.84765625, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 22.80859375, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 39.98046875, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 72.8125, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 118.05078125, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 169.734375, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 216.265625, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 266.79296875, + "y" : 593.47265625, + "pressure" : 0.5 + }, + { + "x" : 319.7421875, + "y" : 593.03515625, + "pressure" : 0.5 + }, + { + "x" : 358.90625, + "y" : 591.6484375, + "pressure" : 0.5 + }, + { + "x" : 405.79296875, + "y" : 589.58984375, + "pressure" : 0.5 + }, + { + "x" : 458.38671875, + "y" : 588.48046875, + "pressure" : 0.5 + }, + { + "x" : 500.421875, + "y" : 588.48046875, + "pressure" : 0.5 + }, + { + "x" : 550.19140625, + "y" : 588.48046875, + "pressure" : 0.5 + }, + { + "x" : 605.96875, + "y" : 589.1328125, + "pressure" : 0.5 + }, + { + "x" : 657.78125, + "y" : 590.7890625, + "pressure" : 0.5 + }, + { + "x" : 712.015625, + "y" : 592.5703125, + "pressure" : 0.5 + }, + { + "x" : 767.7421875, + "y" : 593.84375, + "pressure" : 0.5 + }, + { + "x" : 823.5390625, + "y" : 595.12890625, + "pressure" : 0.5 + }, + { + "x" : 879.20703125, + "y" : 596.9453125, + "pressure" : 0.5 + }, + { + "x" : 936.0859375, + "y" : 600.265625, + "pressure" : 0.5 + }, + { + "x" : 991.69921875, + "y" : 605.27734375, + "pressure" : 0.5 + }, + { + "x" : 1035.9921875, + "y" : 609.8359375, + "pressure" : 0.5 + }, + { + "x" : 1071.140625, + "y" : 612.98046875, + "pressure" : 0.5 + }, + { + "x" : 1105.32421875, + "y" : 615.48828125, + "pressure" : 0.5 + }, + { + "x" : 1130.77734375, + "y" : 617.69140625, + "pressure" : 0.5 + }, + { + "x" : 1149.078125, + "y" : 619.640625, + "pressure" : 0.5 + }, + { + "x" : 1166.15234375, + "y" : 621.54296875, + "pressure" : 0.5 + } + ], + "color" : 4294961979, + "width" : 10 + } + ] +} +''';