├── ios ├── Assets │ └── .gitkeep ├── Classes │ ├── DocumentMeasurePlugin.h │ ├── DocumentMeasurePlugin.m │ └── SwiftDocumentMeasurePlugin.swift ├── .gitignore └── document_measure.podspec ├── android ├── settings.gradle ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── arconsis │ │ └── documentmeasure │ │ └── DocumentMeasurePlugin.kt ├── .gitignore ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties └── build.gradle ├── analysis_options.yaml ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── .gitignore │ ├── Podfile.lock │ └── Podfile ├── assets │ └── images │ │ ├── tech_draw.png │ │ └── floorplan448x449mm.png ├── android │ ├── gradle.properties │ ├── .gitignore │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ └── values │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── arconsis │ │ │ │ │ │ └── documentmeasure_example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ └── build.gradle ├── lib │ ├── colors.dart │ └── main.dart ├── .metadata ├── README.md ├── .gitignore ├── test │ └── widget_test.dart ├── pubspec.yaml └── pubspec.lock ├── assets ├── gifs │ ├── 1_defaults.gif │ ├── 5_life_size.gif │ ├── 0_all_features.gif │ ├── 6_custom_delete.gif │ ├── 9_custom_lines.gif │ ├── 3_toggle_measure.gif │ ├── 4_toggle_distance.gif │ ├── 8_custom_distances.gif │ ├── 2_measurement_information.gif │ └── 7_custom_magnification_glass.gif └── images │ ├── example_portrait.png │ └── example_landscape.png ├── lib ├── src │ ├── util │ │ ├── colors.dart │ │ ├── logger.dart │ │ └── utils.dart │ ├── metadata │ │ ├── bloc │ │ │ ├── metadata_state.dart │ │ │ ├── metadata_bloc.dart │ │ │ └── metadata_event.dart │ │ └── repository │ │ │ └── metadata_repository.dart │ ├── measurement │ │ ├── drawing_holder.dart │ │ ├── bloc │ │ │ ├── magnification_bloc │ │ │ │ ├── magnification_event.dart │ │ │ │ ├── magnification_state.dart │ │ │ │ └── magnification_bloc.dart │ │ │ └── points_bloc │ │ │ │ ├── points_event.dart │ │ │ │ ├── points_state.dart │ │ │ │ └── points_bloc.dart │ │ ├── overlay │ │ │ ├── holder.dart │ │ │ ├── painters │ │ │ │ ├── measure_painter.dart │ │ │ │ ├── magnifying_painter.dart │ │ │ │ └── distance_painter.dart │ │ │ └── measure_area.dart │ │ └── repository │ │ │ └── measurement_repository.dart │ ├── scale_bloc │ │ ├── scale_state.dart │ │ ├── scale_event.dart │ │ └── scale_bloc.dart │ ├── input_bloc │ │ ├── input_event.dart │ │ ├── input_state.dart │ │ └── input_bloc.dart │ ├── style │ │ ├── distance_style.dart │ │ ├── magnification_style.dart │ │ └── point_style.dart │ ├── measurement_controller.dart │ └── measurement_information.dart └── document_measure.dart ├── .metadata ├── CHANGELOG.md ├── test ├── mocks │ └── test_mocks.dart ├── metadata │ ├── measurement_unit_test.dart │ ├── bloc │ │ └── metadata_bloc_test.dart │ └── repository │ │ └── metadata_repository_test.dart ├── measurement │ └── bloc │ │ ├── measure_bloc_test.dart │ │ └── points_bloc_test.dart ├── scale │ └── scale_bloc_test.dart ├── input │ └── input_bloc_test.dart └── measurements_widget_test.dart ├── pubspec.yaml ├── LICENSE └── .gitignore /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'measure' 2 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic/analysis_options.yaml -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/gifs/1_defaults.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/1_defaults.gif -------------------------------------------------------------------------------- /assets/gifs/5_life_size.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/5_life_size.gif -------------------------------------------------------------------------------- /assets/gifs/0_all_features.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/0_all_features.gif -------------------------------------------------------------------------------- /assets/gifs/6_custom_delete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/6_custom_delete.gif -------------------------------------------------------------------------------- /assets/gifs/9_custom_lines.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/9_custom_lines.gif -------------------------------------------------------------------------------- /assets/gifs/3_toggle_measure.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/3_toggle_measure.gif -------------------------------------------------------------------------------- /assets/gifs/4_toggle_distance.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/4_toggle_distance.gif -------------------------------------------------------------------------------- /assets/gifs/8_custom_distances.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/8_custom_distances.gif -------------------------------------------------------------------------------- /assets/images/example_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/images/example_portrait.png -------------------------------------------------------------------------------- /assets/images/example_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/images/example_landscape.png -------------------------------------------------------------------------------- /example/assets/images/tech_draw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/assets/images/tech_draw.png -------------------------------------------------------------------------------- /assets/gifs/2_measurement_information.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/2_measurement_information.gif -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /assets/gifs/7_custom_magnification_glass.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/assets/gifs/7_custom_magnification_glass.gif -------------------------------------------------------------------------------- /example/assets/images/floorplan448x449mm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/assets/images/floorplan448x449mm.png -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Classes/DocumentMeasurePlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface DocumentMeasurePlugin : NSObject 4 | @end 5 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PDDStudio/measurements/master/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/src/util/colors.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | const Color drawColor = Color(0xff1280b3); 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/arconsis/documentmeasure_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.arconsis.documentmeasure_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /example/lib/colors.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | const unselectedColor = Color(0xffeb743f); 7 | const selectedColor = Color(0xffb34512); 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 9f5ff2306bb3e30b2b98eee79cd231b1336f41f4 8 | channel: stable 9 | 10 | project_type: plugin 11 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 8af6b2f038c1172e61d418869363a28dffec3cb4 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /lib/src/metadata/bloc/metadata_state.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:equatable/equatable.dart'; 5 | 6 | class MetadataState extends Equatable { 7 | MetadataState(); 8 | 9 | @override 10 | List get props => []; 11 | } 12 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/document_measure.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | export 'src/measurement_controller.dart'; 5 | export 'src/measurement_information.dart'; 6 | export 'src/measurements_view.dart'; 7 | export 'src/style/distance_style.dart'; 8 | export 'src/style/magnification_style.dart'; 9 | export 'src/style/point_style.dart'; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1+2 2 | * Reformatted code 3 | 4 | ## 0.0.1+1 5 | * Refactored and improved code 6 | 7 | ## 0.0.1+0 8 | * Updated installation method in README 9 | 10 | ## 0.0.1 11 | ### Added Features 12 | * Measuring in Meter, Millimeter, Inch and Foot 13 | * Deleting points 14 | * Panning and zooming 15 | * Editing set points 16 | * Tolerance for measurements 17 | * Customizable magnification glass, points, lines, distance appearance and delete region 18 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/flutter_export_environment.sh -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # documentmeasure_example 2 | 3 | Demonstrates how to use the documentmeasure plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 13 | 14 | For help getting started with Flutter, view our 15 | [online documentation](https://flutter.dev/docs), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /ios/Classes/DocumentMeasurePlugin.m: -------------------------------------------------------------------------------- 1 | #import "DocumentMeasurePlugin.h" 2 | #if __has_include() 3 | #import 4 | #else 5 | // Support project import fallback if the generated compatibility header 6 | // is not copied when this plugin is created as a library. 7 | // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 8 | #import "document_measure-Swift.h" 9 | #endif 10 | 11 | @implementation DocumentMeasurePlugin 12 | + (void)registerWithRegistrar:(NSObject*)registrar { 13 | [SwiftDocumentMeasurePlugin registerWithRegistrar:registrar]; 14 | } 15 | @end 16 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Flutter Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | include ':app' 6 | 7 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 8 | def properties = new Properties() 9 | 10 | assert localPropertiesFile.exists() 11 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 12 | 13 | def flutterSdkPath = properties.getProperty("flutter.sdk") 14 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 15 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 16 | -------------------------------------------------------------------------------- /lib/src/measurement/drawing_holder.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/document_measure.dart'; 7 | import 'package:equatable/equatable.dart'; 8 | 9 | class DrawingHolder extends Equatable { 10 | final List points; 11 | final List distances; 12 | 13 | DrawingHolder(this.points, this.distances); 14 | 15 | @override 16 | String toString() { 17 | return super.toString() + ' points: $points, distances: $distances'; 18 | } 19 | 20 | @override 21 | List get props => [points, distances]; 22 | } 23 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | jcenter() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /lib/src/scale_bloc/scale_state.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:equatable/equatable.dart'; 7 | import 'package:flutter/widgets.dart'; 8 | 9 | class ScaleState extends Equatable { 10 | final Offset offset; 11 | final double scale; 12 | final Matrix4 transform; 13 | 14 | ScaleState(this.offset, this.scale, this.transform); 15 | 16 | @override 17 | List get props => [offset, scale, transform]; 18 | 19 | @override 20 | String toString() { 21 | return super.toString() + 22 | ' offset: $offset, scale: $scale, transform:\n$transform'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/measurement/bloc/magnification_bloc/magnification_event.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:equatable/equatable.dart'; 7 | 8 | abstract class MagnificationEvent extends Equatable {} 9 | 10 | class MagnificationShowEvent extends MagnificationEvent { 11 | final Offset position; 12 | 13 | MagnificationShowEvent(this.position); 14 | 15 | @override 16 | List get props => [position]; 17 | 18 | @override 19 | String toString() { 20 | return super.toString() + ' position: $position'; 21 | } 22 | } 23 | 24 | class MagnificationHideEvent extends MagnificationEvent { 25 | @override 26 | List get props => []; 27 | } 28 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | lib/generated_plugin_registrant.dart 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Exceptions to above rules. 43 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 44 | -------------------------------------------------------------------------------- /lib/src/input_bloc/input_event.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:equatable/equatable.dart'; 7 | 8 | abstract class InputEvent extends Equatable { 9 | final Offset position; 10 | 11 | InputEvent(this.position); 12 | 13 | @override 14 | List get props => [position]; 15 | 16 | @override 17 | String toString() { 18 | return super.toString() + ' position: $position'; 19 | } 20 | } 21 | 22 | class InputDownEvent extends InputEvent { 23 | InputDownEvent(Offset position) : super(position); 24 | } 25 | 26 | class InputMoveEvent extends InputEvent { 27 | InputMoveEvent(Offset position) : super(position); 28 | } 29 | 30 | class InputUpEvent extends InputEvent { 31 | InputUpEvent(Offset position) : super(position); 32 | } 33 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/measurement/overlay/holder.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/document_measure.dart'; 7 | import 'package:equatable/equatable.dart'; 8 | 9 | class Holder extends Equatable { 10 | final Offset start, end; 11 | final LengthUnit distance; 12 | 13 | Holder(this.start, this.end, {this.distance = const Millimeter(0)}); 14 | 15 | Holder.extend(Holder old, LengthUnit distance) 16 | : this(old.start, old.end, distance: distance); 17 | 18 | Holder.withDistance(this.start, this.end, this.distance); 19 | 20 | @override 21 | String toString() { 22 | return super.toString() + 23 | ' First Point: $start, Second Point: $end, Distance: $distance'; 24 | } 25 | 26 | @override 27 | List get props => [start, end, distance]; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/style/distance_style.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/src/util/colors.dart'; 7 | 8 | /// Style class for customizing the distances on the lines between two points. 9 | /// 10 | /// [showTolerance] will show up as for example 10.0±0.1mm instead of 10.0mm 11 | /// 12 | /// [numDecimalPlaces] will be used for both the distance and the tolerance, if displayed. 13 | /// [numDecimalPlaces] = 2 => 9.98mm or 9.98±0.12mm 14 | /// [numDecimalPlaces] = 3 => 9.987mm or 9.987±0.123mm 15 | class DistanceStyle { 16 | final Color textColor; 17 | 18 | final int numDecimalPlaces; 19 | final bool showTolerance; 20 | 21 | const DistanceStyle( 22 | {this.textColor = drawColor, 23 | this.numDecimalPlaces = 2, 24 | this.showTolerance = false}); 25 | } 26 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.arconsis.measure' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.3.70' 6 | ext.tt = 2 7 | repositories { 8 | google() 9 | jcenter() 10 | } 11 | 12 | dependencies { 13 | classpath 'com.android.tools.build:gradle:3.5.3' 14 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 15 | } 16 | } 17 | 18 | rootProject.allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | apply plugin: 'com.android.library' 26 | apply plugin: 'kotlin-android' 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | defaultConfig { 35 | minSdkVersion 16 36 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 37 | } 38 | lintOptions { 39 | disable 'InvalidPackage' 40 | } 41 | } 42 | 43 | dependencies { 44 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 45 | } 46 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import '../lib/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => 22 | widget is Text && widget.data.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /test/mocks/test_mocks.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:bloc_test/bloc_test.dart'; 4 | import 'package:document_measure/src/input_bloc/input_bloc.dart'; 5 | import 'package:document_measure/src/input_bloc/input_event.dart'; 6 | import 'package:document_measure/src/input_bloc/input_state.dart'; 7 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 8 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 9 | import 'package:mockito/mockito.dart'; 10 | 11 | class MockedMetadataRepository extends Mock implements MetadataRepository {} 12 | 13 | class MockedMeasurementRepository extends Mock 14 | implements MeasurementRepository {} 15 | 16 | class MockedInputBloc extends MockBloc 17 | implements InputBloc {} 18 | 19 | class MockedImage extends Mock implements Image { 20 | static final _mockedImage = MockedImage._private(); 21 | 22 | MockedImage._private(); 23 | 24 | static MockedImage get mock => _mockedImage; 25 | } 26 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: document_measure 2 | description: This package lets you measure distances in documents. The main use will probabily be technical documents and floorplans, but you are welcome to experiment with it. 3 | version: 0.0.1+2 4 | repository: "https://github.com/arconsis/measurements" 5 | issue_tracker: "https://github.com/arconsis/measurements/issues" 6 | 7 | environment: 8 | sdk: ">=2.6.0 <3.0.0" 9 | flutter: ^1.12.0 10 | 11 | dependencies: 12 | equatable: ^1.2.2 13 | flutter: 14 | sdk: flutter 15 | flutter_bloc: ^5.0.1 16 | get_it: ^4.0.4 17 | path_provider: ^1.6.11 18 | rxdart: ^0.24.1 19 | 20 | dev_dependencies: 21 | bloc_test: 6.0.1 22 | flutter_test: 23 | sdk: flutter 24 | mockito: 4.1.1 25 | pedantic: 1.9.0 26 | 27 | flutter: 28 | assets: 29 | - assets/images/example_portrait.png 30 | - assets/images/example_landscape.png 31 | 32 | plugin: 33 | platforms: 34 | android: 35 | package: com.arconsis.documentmeasure 36 | pluginClass: DocumentMeasurePlugin 37 | ios: 38 | pluginClass: DocumentMeasurePlugin 39 | 40 | -------------------------------------------------------------------------------- /lib/src/style/magnification_style.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/src/util/colors.dart'; 7 | 8 | /// Style class for customizing the appearance of the magnification glass. 9 | /// 10 | /// [magnificationColor] will be used for all lines and circles of the magnification glass. 11 | /// 12 | /// [magnificationRadius] is the inner radius of the outer circle around the magnified image. 13 | /// 14 | /// [outerCircleThickness] is added to the [magnificationRadius] and gives to total radius of the rendered magnification glass. 15 | class MagnificationStyle { 16 | final Color magnificationColor; 17 | 18 | final double magnificationRadius; 19 | final double outerCircleThickness; 20 | final double crossHairThickness; 21 | 22 | const MagnificationStyle( 23 | {this.magnificationColor = drawColor, 24 | this.magnificationRadius = 50, 25 | this.outerCircleThickness = 2, 26 | this.crossHairThickness = 0.0}); 27 | } 28 | -------------------------------------------------------------------------------- /ios/document_measure.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint documentmeasure.podspec' to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'document_measure' 7 | s.version = '0.0.1' 8 | s.summary = 'Flutter Document Measure Plugin' 9 | s.description = <<-DESC 10 | A widget that allows you to measure distances in documents. 11 | DESC 12 | s.homepage = 'https://github.com/arconsis/measurements' 13 | s.license = { :type => 'MIT', :file => '../LICENSE' } 14 | s.author = { 'arconsis IT-Solutions GmbH' => 'kontakt@arconsis.com' } 15 | s.source = { :http => 'https://github.com/arconsis/measurements' } 16 | s.source_files = 'Classes/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '8.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } 22 | s.swift_version = '5.0' 23 | end 24 | -------------------------------------------------------------------------------- /lib/src/measurement/bloc/points_bloc/points_event.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/document_measure.dart'; 7 | import 'package:equatable/equatable.dart'; 8 | 9 | abstract class PointsEvent extends Equatable { 10 | final List points; 11 | 12 | PointsEvent(this.points); 13 | 14 | @override 15 | List get props => [points]; 16 | 17 | @override 18 | String toString() { 19 | return super.toString() + ' points: $points'; 20 | } 21 | } 22 | 23 | class PointsOnlyEvent extends PointsEvent { 24 | PointsOnlyEvent(List points) : super(points); 25 | } 26 | 27 | class PointsAndDistancesEvent extends PointsEvent { 28 | final List distances; 29 | 30 | PointsAndDistancesEvent(List points, this.distances) : super(points); 31 | 32 | @override 33 | List get props => [points, distances]; 34 | 35 | @override 36 | String toString() { 37 | return super.toString() + ' distances: $distances'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 arconsis IT-Solutions GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/input_bloc/input_state.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:equatable/equatable.dart'; 7 | 8 | abstract class InputState extends Equatable { 9 | @override 10 | List get props => []; 11 | } 12 | 13 | abstract class InputPositionalState extends InputState { 14 | final Offset position; 15 | 16 | InputPositionalState(this.position); 17 | 18 | @override 19 | List get props => [position]; 20 | 21 | @override 22 | String toString() => super.toString() + ' position: $position'; 23 | } 24 | 25 | class InputStandardState extends InputPositionalState { 26 | InputStandardState(Offset position) : super(position); 27 | } 28 | 29 | class InputEndedState extends InputPositionalState { 30 | InputEndedState(Offset position) : super(position); 31 | } 32 | 33 | class InputDeleteRegionState extends InputPositionalState { 34 | InputDeleteRegionState(Offset position) : super(position); 35 | } 36 | 37 | class InputDeleteState extends InputState {} 38 | 39 | class InputEmptyState extends InputState {} 40 | -------------------------------------------------------------------------------- /lib/src/scale_bloc/scale_event.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | 7 | abstract class ScaleEvent {} 8 | 9 | class ScaleDoubleTapEvent extends ScaleEvent {} 10 | 11 | class ScaleOriginalEvent extends ScaleEvent {} 12 | 13 | class ScaleResetEvent extends ScaleEvent {} 14 | 15 | class ScaleCenterUpdatedEvent extends ScaleEvent {} 16 | 17 | abstract class ScalePositionEvent extends ScaleEvent implements Equatable { 18 | final Offset position; 19 | 20 | ScalePositionEvent(this.position); 21 | 22 | @override 23 | bool get stringify => false; 24 | } 25 | 26 | class ScaleStartEvent extends ScalePositionEvent { 27 | ScaleStartEvent(Offset position) : super(position); 28 | 29 | @override 30 | List get props => [position]; 31 | } 32 | 33 | class ScaleUpdateEvent extends ScalePositionEvent { 34 | final double scale; 35 | 36 | ScaleUpdateEvent(Offset position, this.scale) : super(position); 37 | 38 | @override 39 | List get props => [position, scale]; 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/util/logger.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | enum LogDistricts { 5 | MEASUREMENT_VIEW, 6 | METADATA_BLOC, 7 | SCALE_BLOC, 8 | METADATA_REPOSITORY, 9 | MEASURE_AREA, 10 | MEASURE_BLOC, 11 | POINTS_BLOC, 12 | MEASUREMENT_REPOSITORY, 13 | } 14 | 15 | class Logger { 16 | static final List _activeDistricts = [ 17 | // LogDistricts.MEASUREMENT_VIEW, 18 | // LogDistricts.METADATA_BLOC, 19 | // LogDistricts.SCALE_BLOC, 20 | // LogDistricts.METADATA_REPOSITORY, 21 | // 22 | // LogDistricts.MEASURE_AREA, 23 | // LogDistricts.MEASURE_BLOC, 24 | // LogDistricts.POINTS_BLOC, 25 | // LogDistricts.MEASUREMENT_REPOSITORY, 26 | ]; 27 | 28 | final LogDistricts district; 29 | 30 | Logger(this.district); 31 | 32 | String _districtName(LogDistricts district) { 33 | return district.toString().split('.')[1]; 34 | } 35 | 36 | void log(String message) { 37 | if (_activeDistricts.contains(district)) { 38 | print( 39 | '${DateTime.now()} measurements: (${_districtName(district)}) $message'); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/measurement/bloc/magnification_bloc/magnification_state.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:equatable/equatable.dart'; 7 | 8 | abstract class MagnificationState extends Equatable {} 9 | 10 | class MagnificationInactiveState extends MagnificationState { 11 | @override 12 | List get props => []; 13 | } 14 | 15 | class MagnificationActiveState extends MagnificationState { 16 | final Offset position; 17 | final Offset absolutePosition; 18 | final Offset magnificationOffset; 19 | final Image backgroundImage; 20 | final double imageScaleFactor; 21 | 22 | MagnificationActiveState(this.position, this.magnificationOffset, 23 | {this.absolutePosition, this.backgroundImage, this.imageScaleFactor}); 24 | 25 | @override 26 | List get props => [ 27 | position, 28 | absolutePosition, 29 | magnificationOffset, 30 | backgroundImage, 31 | imageScaleFactor 32 | ]; 33 | 34 | @override 35 | String toString() { 36 | return super.toString() + 37 | ' position: $position, absolutePosition: $absolutePosition, magnificationOffset: $magnificationOffset, backgroundImage: $backgroundImage, imageScaleFactor: $imageScaleFactor'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - document_measure (0.0.1): 3 | - Flutter 4 | - Flutter (1.0.0) 5 | - path_provider (0.0.1): 6 | - Flutter 7 | - path_provider_linux (0.0.1): 8 | - Flutter 9 | - path_provider_macos (0.0.1): 10 | - Flutter 11 | 12 | DEPENDENCIES: 13 | - document_measure (from `.symlinks/plugins/document_measure/ios`) 14 | - Flutter (from `Flutter`) 15 | - path_provider (from `.symlinks/plugins/path_provider/ios`) 16 | - path_provider_linux (from `.symlinks/plugins/path_provider_linux/ios`) 17 | - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`) 18 | 19 | EXTERNAL SOURCES: 20 | document_measure: 21 | :path: ".symlinks/plugins/document_measure/ios" 22 | Flutter: 23 | :path: Flutter 24 | path_provider: 25 | :path: ".symlinks/plugins/path_provider/ios" 26 | path_provider_linux: 27 | :path: ".symlinks/plugins/path_provider_linux/ios" 28 | path_provider_macos: 29 | :path: ".symlinks/plugins/path_provider_macos/ios" 30 | 31 | SPEC CHECKSUMS: 32 | document_measure: 1e686ea7f999df6b58189c3d3e7f56b9403560ec 33 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec 34 | path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c 35 | path_provider_linux: 4d630dc393e1f20364f3e3b4a2ff41d9674a84e4 36 | path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0 37 | 38 | PODFILE CHECKSUM: c34e2287a9ccaa606aeceab922830efb9a6ff69a 39 | 40 | COCOAPODS: 1.8.4 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | .dart_tool/ 24 | .flutter-plugins 25 | .packages 26 | .pub-cache/ 27 | .pub/ 28 | /build/ 29 | 30 | # Android related 31 | **/android/**/gradle-wrapper.jar 32 | **/android/.gradle 33 | **/android/captures/ 34 | **/android/gradlew 35 | **/android/gradlew.bat 36 | **/android/local.properties 37 | **/android/**/GeneratedPluginRegistrant.java 38 | 39 | # iOS/XCode related 40 | **/ios/**/*.mode1v3 41 | **/ios/**/*.mode2v3 42 | **/ios/**/*.moved-aside 43 | **/ios/**/*.pbxuser 44 | **/ios/**/*.perspectivev3 45 | **/ios/**/*sync/ 46 | **/ios/**/.sconsign.dblite 47 | **/ios/**/.tags* 48 | **/ios/**/.vagrant/ 49 | **/ios/**/DerivedData/ 50 | **/ios/**/Icon? 51 | **/ios/**/Pods/ 52 | **/ios/**/.symlinks/ 53 | **/ios/**/profile 54 | **/ios/**/xcuserdata 55 | **/ios/.generated/ 56 | **/ios/Flutter/App.framework 57 | **/ios/Flutter/Flutter.framework 58 | **/ios/Flutter/Generated.xcconfig 59 | **/ios/Flutter/app.flx 60 | **/ios/Flutter/app.zip 61 | **/ios/Flutter/flutter_assets/ 62 | **/ios/ServiceDefinitions.json 63 | **/ios/Runner/GeneratedPluginRegistrant.* 64 | 65 | # Exceptions to above rules. 66 | !**/ios/**/default.mode1v3 67 | !**/ios/**/default.mode2v3 68 | !**/ios/**/default.pbxuser 69 | !**/ios/**/default.perspectivev3 70 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | documentmeasure_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/src/metadata/bloc/metadata_bloc.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:async'; 5 | 6 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 7 | import 'package:document_measure/src/util/logger.dart'; 8 | import 'package:flutter_bloc/flutter_bloc.dart'; 9 | import 'package:get_it/get_it.dart'; 10 | 11 | import 'metadata_event.dart'; 12 | import 'metadata_state.dart'; 13 | 14 | class MetadataBloc extends Bloc { 15 | final _logger = Logger(LogDistricts.METADATA_BLOC); 16 | 17 | MetadataRepository _repository; 18 | 19 | MetadataBloc() : super(MetadataState()) { 20 | _repository = GetIt.I(); 21 | } 22 | 23 | @override 24 | void onEvent(MetadataEvent event) async { 25 | _logger.log('received event: $event'); 26 | 27 | if (event is MetadataStartedEvent) { 28 | _repository.registerStartupValuesChange( 29 | measurementInformation: event.measurementInformation, 30 | measure: event.measure, 31 | showDistance: event.showDistances, 32 | controller: event.controller, 33 | magnificationStyle: event.magnificationStyle, 34 | ); 35 | } else if (event is MetadataBackgroundEvent) { 36 | _repository.registerBackgroundChange(event.backgroundImage, event.size); 37 | } else if (event is MetadataDeleteRegionEvent) { 38 | _repository.registerDeleteRegion(event.position, event.deleteSize); 39 | } else if (event is MetadataScreenSizeEvent) { 40 | _repository.registerScreenSize(event.screenSize); 41 | } 42 | 43 | super.onEvent(event); 44 | } 45 | 46 | @override 47 | Stream mapEventToState(MetadataEvent event) async* {} 48 | } 49 | -------------------------------------------------------------------------------- /lib/src/style/point_style.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/src/util/colors.dart'; 7 | import 'package:equatable/equatable.dart'; 8 | 9 | abstract class LineType extends Equatable { 10 | final Color lineColor; 11 | 12 | const LineType(this.lineColor); 13 | } 14 | 15 | class SolidLine extends LineType { 16 | final double lineWidth; 17 | 18 | const SolidLine({this.lineWidth = 2, Color lineColor = drawColor}) 19 | : super(lineColor); 20 | 21 | @override 22 | List get props => [lineWidth, lineColor]; 23 | } 24 | 25 | /// [LineType] to render a dashed line, like - - - - 26 | /// 27 | /// [dashLength] is the length of one dash 1: - - 2: -- -- 28 | /// 29 | /// [dashDistance] is distance between two dashes 1: - - 2: - - 30 | class DashedLine extends LineType { 31 | final double dashWidth; 32 | final double dashLength; 33 | final double dashDistance; 34 | 35 | DashedLine( 36 | {this.dashWidth = 2, 37 | this.dashLength = 5, 38 | this.dashDistance = 5, 39 | Color lineColor = drawColor}) 40 | : super(lineColor); 41 | 42 | @override 43 | List get props => [dashWidth, dashLength, dashDistance, lineColor]; 44 | } 45 | 46 | /// Style class to customize the appearance of the placed points and lines between them. 47 | /// 48 | /// The lines can by styles in their respective constructors ([SolidLine], [DashedLine]). 49 | class PointStyle extends Equatable { 50 | final Color dotColor; 51 | final double dotRadius; 52 | 53 | final LineType lineType; 54 | 55 | const PointStyle( 56 | {this.dotColor = drawColor, 57 | this.dotRadius = 4, 58 | this.lineType = const SolidLine()}); 59 | 60 | @override 61 | List get props => [dotColor, dotRadius, lineType]; 62 | } 63 | -------------------------------------------------------------------------------- /ios/Classes/SwiftDocumentMeasurePlugin.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | public class SwiftDocumentMeasurePlugin: NSObject, FlutterPlugin { 5 | public static func register(with registrar: FlutterPluginRegistrar) { 6 | let channel = FlutterMethodChannel(name: "documentmeasure", binaryMessenger: registrar.messenger()) 7 | let instance = SwiftDocumentMeasurePlugin() 8 | registrar.addMethodCallDelegate(instance, channel: channel) 9 | } 10 | 11 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 12 | if (call.method == "getPhysicalPixelsPerInch") { 13 | var ppi: double_t 14 | 15 | switch UIDevice().name { 16 | case "iPhone X", "iPhone Xs", "iPhone 11 Pro", "iPhone 11 Pro Max": 17 | ppi = 458.0 18 | case "iPhone 6 Plus", "iPhone 6s Plus", "iPhone 7 Plus", "iPhone 8 Plus": 19 | ppi = 401.0 20 | case "iPhone 4s", "iPhone 5", "iPhone 5s", "iPhone 5c", "iPhone SE (1st generation)", "iPhone 6", "iPhone 6s", "iPhone 7", "iPhone 8", "iPhone 11", "iPhone Xʀ", "iPhone SE (2nd generation)", "iPad mini 2", "iPad mini 3", "iPad mini 4", "iPad mini (5th generation)": 21 | ppi = 326.0 22 | case "iPad Retina", "iPad Air", "iPad Air 2", "iPad Pro (9.7-inch)", "iPad Pro (12.9-inch)", "iPad (5th generation)", "iPad Pro (12.9-inch) (2nd generation)", "iPad (10.5-inch)", "iPad (6th generation)", "iPad (7th generation)", "iPad Pro (11-inch) (1st generation)", "iPad Pro (12.9-inch) (3rd generation)", "iPad Pro (11-inch) (2nd generation)", "iPad Pro (12.9-inch) (4th generation)", "iPad Air (3rd generation)": 23 | ppi = 264.0 24 | case "iPad 2": 25 | ppi = 132 26 | default: 27 | ppi = 326.0 28 | } 29 | 30 | result(ppi) 31 | } else { 32 | result(nil) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.arconsis.documentmeasure_example" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | } 47 | 48 | buildTypes { 49 | release { 50 | // TODO: Add your own signing config for the release build. 51 | // Signing with the debug keys for now, so `flutter run --release` works. 52 | signingConfig signingConfigs.debug 53 | } 54 | } 55 | } 56 | 57 | flutter { 58 | source '../..' 59 | } 60 | 61 | dependencies { 62 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 63 | } 64 | -------------------------------------------------------------------------------- /lib/src/util/utils.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:math'; 5 | import 'dart:ui'; 6 | 7 | import 'package:flutter/cupertino.dart'; 8 | 9 | import 'logger.dart'; 10 | 11 | extension IterableExtension on Iterable { 12 | void doInBetween(Function(T, T) function) { 13 | var iterator = this.iterator; 14 | iterator.moveNext(); 15 | 16 | T current = iterator.current, next; 17 | 18 | while (iterator.moveNext()) { 19 | next = iterator.current; 20 | 21 | function(current, next); 22 | 23 | current = next; 24 | } 25 | } 26 | 27 | void zip(Iterable iterable, Function(K, T) function) { 28 | Iterator thisIterator = iterator, otherIterator = iterable.iterator; 29 | 30 | while (thisIterator.moveNext() && otherIterator.moveNext()) { 31 | K thisCurrent = thisIterator.current; 32 | T otherCurrent = otherIterator.current; 33 | 34 | function(thisCurrent, otherCurrent); 35 | } 36 | } 37 | } 38 | 39 | extension NumberExtension on num { 40 | bool isInBounds(num lower, num upper) => this > lower && this < upper; 41 | 42 | num fit(num lower, num upper) => min(max(lower, this), upper); 43 | } 44 | 45 | extension OffsetExtension on Offset { 46 | Offset fitInto(Size mySize, Size bounds, Offset offset, Offset target, 47 | double threshold, double scale) { 48 | var currentOffset = this + offset; 49 | var thresholdOffset = min(mySize.width, mySize.height) * threshold; 50 | 51 | return Offset( 52 | (currentOffset.dx + target.dx).fit( 53 | -mySize.width + thresholdOffset, bounds.width - thresholdOffset), 54 | (currentOffset.dy + target.dy).fit( 55 | -mySize.height + thresholdOffset, bounds.height - thresholdOffset)); 56 | } 57 | } 58 | 59 | void measure(Logger logger, String text, Function() f) { 60 | final start = DateTime.now(); 61 | f(); 62 | logger.log(text + DateTime.now().difference(start).toString()); 63 | } 64 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: document_measure_example 2 | description: Demonstrates how to use the measurements plugin. 3 | publish_to: 'none' 4 | 5 | environment: 6 | sdk: ">=2.2.0 <3.0.0" 7 | 8 | dependencies: 9 | flutter: 10 | sdk: flutter 11 | document_measure: 12 | path: ../ 13 | 14 | dev_dependencies: 15 | flutter_test: 16 | sdk: flutter 17 | 18 | # For information on the generic Dart part of this file, see the 19 | # following page: https://dart.dev/tools/pub/pubspec 20 | 21 | # The following section is specific to Flutter. 22 | flutter: 23 | 24 | # The following line ensures that the Material Icons font is 25 | # included with your application, so that you can use the icons in 26 | # the material Icons class. 27 | uses-material-design: true 28 | 29 | # To add assets to your application, add an assets section, like this: 30 | # assets: 31 | # - images/a_dot_burr.jpeg 32 | # - images/a_dot_ham.jpeg 33 | assets: 34 | - assets/images/floorplan448x449mm.png 35 | - assets/images/tech_draw.png 36 | 37 | # An image asset can refer to one or more resolution-specific "variants", see 38 | # https://flutter.dev/assets-and-images/#resolution-aware. 39 | 40 | # For details regarding adding assets from package dependencies, see 41 | # https://flutter.dev/assets-and-images/#from-packages 42 | 43 | # To add custom fonts to your application, add a fonts section here, 44 | # in this "flutter" section. Each entry in this list should have a 45 | # "family" key with the font family name, and a "fonts" key with a 46 | # list giving the asset and other descriptors for the font. For 47 | # example: 48 | # fonts: 49 | # - family: Schyler 50 | # fonts: 51 | # - asset: fonts/Schyler-Regular.ttf 52 | # - asset: fonts/Schyler-Italic.ttf 53 | # style: italic 54 | # - family: Trajan Pro 55 | # fonts: 56 | # - asset: fonts/TrajanPro.ttf 57 | # - asset: fonts/TrajanPro_Bold.ttf 58 | # weight: 700 59 | # 60 | # For details regarding fonts from package dependencies, 61 | # see https://flutter.dev/custom-fonts/#from-packages 62 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/arconsis/documentmeasure/DocumentMeasurePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.arconsis.documentmeasure 2 | 3 | import android.content.res.Resources 4 | import androidx.annotation.NonNull 5 | import io.flutter.embedding.engine.plugins.FlutterPlugin 6 | import io.flutter.plugin.common.MethodCall 7 | import io.flutter.plugin.common.MethodChannel 8 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 9 | import io.flutter.plugin.common.PluginRegistry.Registrar 10 | 11 | /** MeasurePlugin */ 12 | class DocumentMeasurePlugin : FlutterPlugin, MethodCallHandler { 13 | override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 14 | val channel = MethodChannel(flutterPluginBinding.binaryMessenger, "documentmeasure") 15 | channel.setMethodCallHandler(DocumentMeasurePlugin()) 16 | } 17 | 18 | // This static function is optional and equivalent to onAttachedToEngine. It supports the old 19 | // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting 20 | // plugin registration via this function while apps migrate to use the new Android APIs 21 | // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. 22 | // 23 | // It is encouraged to share logic between onAttachedToEngine and registerWith to keep 24 | // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called 25 | // depending on the user's project. onAttachedToEngine or registerWith must both be defined 26 | // in the same class. 27 | companion object { 28 | @JvmStatic 29 | fun registerWith(registrar: Registrar) { 30 | val channel = MethodChannel(registrar.messenger(), "documentmeasure") 31 | channel.setMethodCallHandler(DocumentMeasurePlugin()) 32 | } 33 | } 34 | 35 | override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { 36 | } 37 | 38 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 39 | if (call.method == "getPhysicalPixelsPerInch") { 40 | result.success(Resources.getSystem().displayMetrics.xdpi) 41 | } else { 42 | result.notImplemented() 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/src/measurement/bloc/points_bloc/points_state.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/src/measurement/overlay/holder.dart'; 7 | import 'package:equatable/equatable.dart'; 8 | import 'package:flutter/gestures.dart'; 9 | 10 | abstract class PointsState extends Equatable {} 11 | 12 | class PointsEmptyState extends PointsState { 13 | @override 14 | List get props => null; 15 | } 16 | 17 | class PointsSingleState extends PointsState { 18 | final Offset point; 19 | 20 | PointsSingleState(this.point); 21 | 22 | @override 23 | List get props => [point]; 24 | 25 | @override 26 | String toString() { 27 | return super.toString() + ' point: $point'; 28 | } 29 | } 30 | 31 | class PointsOnlyState extends PointsState { 32 | final List points; 33 | 34 | PointsOnlyState(this.points); 35 | 36 | @override 37 | List get props => [points]; 38 | 39 | @override 40 | String toString() { 41 | return super.toString() + ' points: $points'; 42 | } 43 | } 44 | 45 | class PointsAndDistanceState extends PointsState { 46 | final List holders; 47 | final Offset viewCenter; 48 | final double tolerance; 49 | 50 | PointsAndDistanceState(this.holders, this.viewCenter, this.tolerance); 51 | 52 | @override 53 | List get props => [holders, viewCenter, tolerance]; 54 | 55 | @override 56 | String toString() { 57 | return super.toString() + 58 | ' drawingHolder: $holders, viewCenter: $viewCenter, tolerance: $tolerance'; 59 | } 60 | } 61 | 62 | class PointsAndDistanceActiveState extends PointsAndDistanceState { 63 | final List nullIndices; 64 | 65 | PointsAndDistanceActiveState(List holders, Offset viewCenter, 66 | double tolerance, this.nullIndices) 67 | : super(holders, viewCenter, tolerance); 68 | 69 | @override 70 | List get props => [holders, viewCenter, tolerance, nullIndices]; 71 | 72 | @override 73 | String toString() { 74 | return super.toString() + ' nullIndex: $nullIndices'; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/src/measurement/overlay/painters/measure_painter.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/src/style/point_style.dart'; 7 | import 'package:flutter/material.dart' as material; 8 | 9 | class MeasurePainter extends material.CustomPainter { 10 | final Offset start, end; 11 | final PointStyle style; 12 | final Paint dotPaint, pathPaint; 13 | 14 | final Path _drawPath = Path(); 15 | double _dotRadius; 16 | 17 | MeasurePainter( 18 | {@material.required this.start, 19 | @material.required this.end, 20 | @material.required this.style, 21 | @material.required this.dotPaint, 22 | @material.required this.pathPaint}) { 23 | _dotRadius = style.dotRadius; 24 | 25 | var lineType = style.lineType; 26 | _drawPath.reset(); 27 | _drawPath.moveTo(start.dx, start.dy); 28 | 29 | if (lineType is SolidLine) { 30 | _drawPath.lineTo(end.dx, end.dy); 31 | } else if (lineType is DashedLine) { 32 | var distance = (end - start).distance; 33 | 34 | var solidOffset = (end - start) * lineType.dashLength / distance; 35 | var emptyOffset = (end - start) * lineType.dashDistance / distance; 36 | var currentPosition = start; 37 | 38 | var numLines = 39 | (distance / (lineType.dashLength + lineType.dashDistance)).floor(); 40 | 41 | for (var i = 0; i < numLines; i++) { 42 | currentPosition += solidOffset; 43 | _drawPath.lineTo(currentPosition.dx, currentPosition.dy); 44 | currentPosition += emptyOffset; 45 | _drawPath.moveTo(currentPosition.dx, currentPosition.dy); 46 | } 47 | 48 | currentPosition += solidOffset; 49 | 50 | if ((currentPosition - start).distance > distance) { 51 | _drawPath.lineTo(end.dx, end.dy); 52 | } else { 53 | _drawPath.lineTo(currentPosition.dx, currentPosition.dy); 54 | } 55 | } else { 56 | throw UnimplementedError( 57 | 'This line type is not supported! Type was: $style'); 58 | } 59 | } 60 | 61 | @override 62 | void paint(Canvas canvas, Size size) { 63 | canvas.drawCircle(start, _dotRadius, dotPaint); 64 | canvas.drawCircle(end, _dotRadius, dotPaint); 65 | 66 | canvas.drawPath(_drawPath, pathPaint); 67 | } 68 | 69 | @override 70 | bool shouldRepaint(material.CustomPainter oldDelegate) { 71 | var old = oldDelegate as MeasurePainter; 72 | 73 | return old.start != start || old.end != end || old.style != style; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/measurement/overlay/painters/magnifying_painter.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui' as ui; 5 | 6 | import 'package:document_measure/src/style/magnification_style.dart'; 7 | import 'package:flutter/material.dart'; 8 | 9 | class MagnifyingPainter extends CustomPainter { 10 | final Offset fingerPosition; 11 | final ui.Image image; 12 | final MagnificationStyle style; 13 | 14 | final _drawPaint = Paint(); 15 | 16 | Offset _drawPosition; 17 | RRect _outerCircle, _innerCircle; 18 | Rect _imageTargetRect, _imageSourceRect; 19 | 20 | MagnifyingPainter( 21 | {@required this.fingerPosition, 22 | @required Offset absolutePosition, 23 | @required this.image, 24 | @required this.style, 25 | double imageScaleFactor, 26 | @required Offset magnificationOffset}) { 27 | _drawPosition = fingerPosition - magnificationOffset; 28 | 29 | var diameter = 2 * style.magnificationRadius; 30 | 31 | _innerCircle = getCircle(_drawPosition, style.magnificationRadius); 32 | _outerCircle = _innerCircle.inflate(style.outerCircleThickness); 33 | 34 | _imageSourceRect = Rect.fromCenter( 35 | center: absolutePosition * imageScaleFactor, 36 | width: diameter, 37 | height: diameter); 38 | _imageTargetRect = Rect.fromCenter( 39 | center: _drawPosition, width: diameter, height: diameter); 40 | 41 | _drawPaint.color = style.magnificationColor; 42 | _drawPaint.strokeWidth = style.crossHairThickness; 43 | } 44 | 45 | RRect getCircle(Offset position, double radius) { 46 | return RRect.fromRectAndRadius( 47 | Rect.fromCenter( 48 | center: position, width: radius * 2, height: radius * 2), 49 | Radius.circular(radius)); 50 | } 51 | 52 | @override 53 | void paint(Canvas canvas, Size size) { 54 | canvas.clipRRect(_outerCircle); 55 | canvas.drawImageRect(image, _imageSourceRect, _imageTargetRect, _drawPaint); 56 | 57 | canvas.drawDRRect(_outerCircle, _innerCircle, _drawPaint); 58 | canvas.drawLine( 59 | Offset(_drawPosition.dx - style.magnificationRadius, _drawPosition.dy), 60 | Offset(_drawPosition.dx + style.magnificationRadius, _drawPosition.dy), 61 | _drawPaint); 62 | canvas.drawLine( 63 | Offset(_drawPosition.dx, _drawPosition.dy - style.magnificationRadius), 64 | Offset(_drawPosition.dx, _drawPosition.dy + style.magnificationRadius), 65 | _drawPaint); 66 | } 67 | 68 | @override 69 | bool shouldRepaint(CustomPainter oldDelegate) { 70 | var old = oldDelegate as MagnifyingPainter; 71 | 72 | return old.fingerPosition != fingerPosition || old.image != image; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 23 | 27 | 32 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /lib/src/metadata/bloc/metadata_event.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui' as ui; 5 | 6 | import 'package:document_measure/document_measure.dart'; 7 | import 'package:equatable/equatable.dart'; 8 | import 'package:flutter/widgets.dart'; 9 | 10 | abstract class MetadataEvent extends Equatable {} 11 | 12 | class MetadataBackgroundEvent extends MetadataEvent { 13 | final ui.Image backgroundImage; 14 | final Size size; 15 | 16 | MetadataBackgroundEvent(this.backgroundImage, this.size); 17 | 18 | @override 19 | List get props => [backgroundImage, size]; 20 | 21 | @override 22 | String toString() { 23 | return super.toString() + ' size: $size, backgroundImage: $backgroundImage'; 24 | } 25 | } 26 | 27 | class MetadataStartedEvent extends MetadataEvent { 28 | final bool measure; 29 | final bool showDistances; 30 | final MeasurementInformation measurementInformation; 31 | final MagnificationStyle magnificationStyle; 32 | final MeasurementController controller; 33 | 34 | MetadataStartedEvent({ 35 | @required this.measure, 36 | @required this.showDistances, 37 | @required this.measurementInformation, 38 | @required this.magnificationStyle, 39 | @required this.controller, 40 | }); 41 | 42 | @override 43 | List get props => [ 44 | measurementInformation, 45 | controller, 46 | measure, 47 | showDistances, 48 | magnificationStyle, 49 | ]; 50 | 51 | @override 52 | String toString() { 53 | return super.toString() + 54 | ' MeasurementInformation: $measurementInformation, measure: $measure, showDistances: $showDistances, magnificationStyle: $magnificationStyle'; 55 | } 56 | } 57 | 58 | class MetadataOrientationEvent extends MetadataEvent { 59 | final Orientation orientation; 60 | 61 | MetadataOrientationEvent(this.orientation); 62 | 63 | @override 64 | List get props => [orientation]; 65 | 66 | @override 67 | String toString() { 68 | return super.toString() + ' orientation: $orientation'; 69 | } 70 | } 71 | 72 | class MetadataScreenSizeEvent extends MetadataEvent { 73 | final Size screenSize; 74 | 75 | MetadataScreenSizeEvent(this.screenSize); 76 | 77 | @override 78 | List get props => [screenSize]; 79 | 80 | @override 81 | String toString() { 82 | return super.toString() + ' screenSize: $screenSize'; 83 | } 84 | } 85 | 86 | class MetadataDeleteRegionEvent extends MetadataEvent { 87 | final Offset position; 88 | final Size deleteSize; 89 | 90 | MetadataDeleteRegionEvent(this.position, this.deleteSize); 91 | 92 | @override 93 | List get props => [position, deleteSize]; 94 | 95 | @override 96 | String toString() { 97 | return super.toString() + ' position: $position, deleteSize: $deleteSize'; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/src/input_bloc/input_bloc.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:async'; 5 | 6 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 7 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 8 | import 'package:flutter_bloc/flutter_bloc.dart'; 9 | import 'package:get_it/get_it.dart'; 10 | 11 | import 'input_event.dart'; 12 | import 'input_state.dart'; 13 | 14 | class InputBloc extends Bloc { 15 | final List _streamSubscription = []; 16 | 17 | MeasurementRepository _measurementRepository; 18 | MetadataRepository _metadataRepository; 19 | 20 | bool _measure = false; 21 | bool _delete = false; 22 | 23 | InputBloc() : super(InputEmptyState()) { 24 | _metadataRepository = GetIt.I(); 25 | _measurementRepository = GetIt.I(); 26 | 27 | _streamSubscription.add(_metadataRepository.measurement 28 | .listen((measure) => _measure = measure)); 29 | } 30 | 31 | @override 32 | void onEvent(InputEvent event) { 33 | if (_measure) { 34 | switch (event.runtimeType) { 35 | case InputDownEvent: 36 | if (_metadataRepository.isInDeleteRegion(event.position)) { 37 | _delete = false; 38 | } else { 39 | _delete = true; 40 | } 41 | 42 | _measurementRepository.registerDownEvent(event.position); 43 | break; 44 | case InputMoveEvent: 45 | _measurementRepository.registerMoveEvent(event.position); 46 | break; 47 | case InputUpEvent: 48 | if (_delete && _metadataRepository.isInDeleteRegion(event.position)) { 49 | _measurementRepository.removeCurrentPoint(); 50 | } else { 51 | _measurementRepository.registerUpEvent(event.position); 52 | } 53 | break; 54 | default: 55 | } 56 | } 57 | 58 | super.onEvent(event); 59 | } 60 | 61 | @override 62 | Future close() { 63 | _streamSubscription.forEach((subscription) => subscription.cancel()); 64 | return super.close(); 65 | } 66 | 67 | @override 68 | Stream mapEventToState(InputEvent event) async* { 69 | if (_measure) { 70 | if (_delete && _metadataRepository.isInDeleteRegion(event.position)) { 71 | if (event is InputMoveEvent || event is InputDownEvent) { 72 | yield InputDeleteRegionState(event.position); 73 | } else if (event is InputUpEvent) { 74 | yield InputDeleteState(); 75 | } 76 | } else { 77 | if (event is InputMoveEvent || event is InputDownEvent) { 78 | yield InputStandardState(event.position); 79 | } else if (event is InputUpEvent) { 80 | yield InputEndedState(event.position); 81 | } 82 | } 83 | } else { 84 | yield InputEmptyState(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /test/metadata/measurement_unit_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:document_measure/document_measure.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | final differenceThreshold = 0.0000001; 9 | 10 | group('Testing measurement conversions', () { 11 | group('Testing conversions from meter to others', () { 12 | final meter = Meter(1); 13 | 14 | test('meter to millimeter', () { 15 | expect(meter.convertToMillimeter().value, equals(1000)); 16 | }); 17 | 18 | test('meter to inch', () { 19 | expect(meter.convertToInch().value - 39.3700787, 20 | lessThan(differenceThreshold)); 21 | }); 22 | 23 | test('meter to foot', () { 24 | expect(meter.convertToFoot().value - 3.2808399, 25 | lessThan(differenceThreshold)); 26 | }); 27 | 28 | test('meter to meter', () { 29 | expect(meter.convertToMeter().value, equals(1)); 30 | }); 31 | }); 32 | 33 | group('Testing conversions from millimeter to others', () { 34 | final millimeter = Millimeter(1); 35 | 36 | test('millimeter to meter', () { 37 | expect(millimeter.convertToMeter().value, equals(0.001)); 38 | }); 39 | 40 | test('millimeter to inch', () { 41 | expect(millimeter.convertToInch().value - 0.0393700787, 42 | lessThan(differenceThreshold)); 43 | }); 44 | 45 | test('millimeter to foot', () { 46 | expect(millimeter.convertToFoot().value - 0.0032808399, 47 | lessThan(differenceThreshold)); 48 | }); 49 | 50 | test('millimeter to millimeter', () { 51 | expect(millimeter.convertToMillimeter().value, equals(1)); 52 | }); 53 | }); 54 | 55 | group('Testing conversions from inch to others', () { 56 | final inch = Inch(1); 57 | 58 | test('inch to meter', () { 59 | expect(inch.convertToMeter().value, equals(0.0254)); 60 | }); 61 | 62 | test('inch to millimeter', () { 63 | expect(inch.convertToMillimeter().value, equals(25.4)); 64 | }); 65 | 66 | test('inch to foot', () { 67 | expect(inch.convertToFoot().value - 0.0833333333, 68 | lessThan(differenceThreshold)); 69 | }); 70 | 71 | test('inch to inch', () { 72 | expect(inch.convertToInch().value, equals(1)); 73 | }); 74 | }); 75 | 76 | group('Testing conversions from foot to others', () { 77 | final foot = Foot(1); 78 | 79 | test('foot to meter', () { 80 | expect(foot.convertToMeter().value, equals(0.3048)); 81 | }); 82 | 83 | test('foot to millimeter', () { 84 | expect(foot.convertToMillimeter().value, equals(304.8)); 85 | }); 86 | 87 | test('foot to inch', () { 88 | expect(foot.convertToInch().value, equals(12)); 89 | }); 90 | 91 | test('foot to foot', () { 92 | expect(foot.convertToFoot().value, equals(1)); 93 | }); 94 | }); 95 | }); 96 | } 97 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/measurement_controller.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:rxdart/rxdart.dart'; 6 | 7 | /// Interface to offer zoom functionality. 8 | abstract class MeasurementFunction { 9 | /// Scales the document such that it appears in a scale of 1:1 on the device screen. 10 | /// Only works, when the resulting zoom factor is not too large (performance and other issues would arise). 11 | bool zoomToLifeSize(); 12 | 13 | /// When zoomed in this will reset the zoom to 1. 14 | bool resetZoom(); 15 | } 16 | 17 | /// Simple class that offers the list of distances between the set points and the tolerance under with the distances were calculated. 18 | class MeasurementValues extends Equatable { 19 | /// Distances in order of placed points. 20 | final List distances; 21 | 22 | /// Tolerance due to the size of one individual pixel. 23 | final double tolerance; 24 | 25 | MeasurementValues(this.distances, this.tolerance); 26 | 27 | @override 28 | List get props => [distances, tolerance]; 29 | } 30 | 31 | /// A controller that offers interaction with the displayed content for zooming in or out and retrieving the measured distances. 32 | /// 33 | /// You can get the latest distances and tolerance directly by calling [distances] and [tolerance] respectively 34 | /// or listen to the [MeasurementValues] stream via 35 | /// ```dart 36 | /// measurementController.measurements.listen((value) => ...); 37 | /// ``` 38 | class MeasurementController { 39 | final BehaviorSubject _currentValues = BehaviorSubject(); 40 | MeasurementFunction _function; 41 | 42 | MeasurementController(); 43 | 44 | /// Only for internal use. Don't use it, as that will break the update of distances. 45 | set measurementFunction(MeasurementFunction function) => _function = function; 46 | 47 | /// The stream of measurements the user takes. 48 | /// They will be in the selected unit of measurement in [MeasurementView]. 49 | Stream get measurements => _currentValues.stream; 50 | 51 | /// Returns the latest distances. 52 | List get distances => _currentValues.value?.distances; 53 | 54 | /// Only for internal use. Using it will return [distances] back to you in the [measurements] [Stream]. 55 | set distances(List distances) { 56 | if (_currentValues.value?.distances == distances) { 57 | return; 58 | } 59 | 60 | _currentValues.value = MeasurementValues(distances, tolerance); 61 | } 62 | 63 | /// Return the current tolerance. 64 | /// This might change as the user zooms in and out. 65 | double get tolerance => _currentValues.value?.tolerance; 66 | 67 | /// Only for internal use. Using it will return [tolerance] back to you in the [measurements] [Stream]. 68 | set tolerance(double tolerance) { 69 | if (_currentValues.value?.tolerance == tolerance) { 70 | return; 71 | } 72 | 73 | _currentValues.value = MeasurementValues(distances, tolerance); 74 | } 75 | 76 | /// Zoom the content to life-size if possible. 77 | /// When the resulting zoom would be too large the call will be ignored to avoid performance issues and other problems. 78 | bool zoomToLifeSize() => _function?.zoomToLifeSize(); 79 | 80 | /// Reset the zoom back to 1, which will fit the content into the view. 81 | bool resetZoom() => _function?.resetZoom(); 82 | 83 | void close() { 84 | _currentValues.close(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def parse_KV_file(file, separator='=') 14 | file_abs_path = File.expand_path(file) 15 | if !File.exists? file_abs_path 16 | return []; 17 | end 18 | generated_key_values = {} 19 | skip_line_start_symbols = ["#", "/"] 20 | File.foreach(file_abs_path) do |line| 21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } 22 | plugin = line.split(pattern=separator) 23 | if plugin.length == 2 24 | podname = plugin[0].strip() 25 | path = plugin[1].strip() 26 | podpath = File.expand_path("#{path}", file_abs_path) 27 | generated_key_values[podname] = podpath 28 | else 29 | puts "Invalid plugin specification: #{line}" 30 | end 31 | end 32 | generated_key_values 33 | end 34 | 35 | target 'Runner' do 36 | use_frameworks! 37 | use_modular_headers! 38 | 39 | # Flutter Pod 40 | 41 | copied_flutter_dir = File.join(__dir__, 'Flutter') 42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') 43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') 44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) 45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. 46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. 47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. 48 | 49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') 50 | unless File.exist?(generated_xcode_build_settings_path) 51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" 52 | end 53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) 54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; 55 | 56 | unless File.exist?(copied_framework_path) 57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) 58 | end 59 | unless File.exist?(copied_podspec_path) 60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) 61 | end 62 | end 63 | 64 | # Keep pod path relative so it can be checked into Podfile.lock. 65 | pod 'Flutter', :path => 'Flutter' 66 | 67 | # Plugin Pods 68 | 69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock 70 | # referring to absolute paths on developers' machines. 71 | system('rm -rf .symlinks') 72 | system('mkdir -p .symlinks/plugins') 73 | plugin_pods = parse_KV_file('../.flutter-plugins') 74 | plugin_pods.each do |name, path| 75 | symlink = File.join('.symlinks', 'plugins', name) 76 | File.symlink(path, symlink) 77 | pod name, :path => File.join(symlink, 'ios') 78 | end 79 | end 80 | 81 | post_install do |installer| 82 | installer.pods_project.targets.each do |target| 83 | target.build_configurations.each do |config| 84 | config.build_settings['ENABLE_BITCODE'] = 'NO' 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/src/measurement/overlay/painters/distance_painter.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:math'; 5 | import 'dart:ui'; 6 | 7 | import 'package:document_measure/document_measure.dart'; 8 | import 'package:flutter/material.dart' as material; 9 | 10 | class DistancePainter extends material.CustomPainter { 11 | static final double _log10 = log(10); 12 | static final double _offsetPerDigit = 4.57; 13 | 14 | final LengthUnit distance; 15 | final Offset viewCenter; 16 | 17 | Offset _zeroPoint; 18 | final Offset _zeroPointWithoutTolerance = Offset(-29, 0); 19 | final Offset _zeroPointWithTolerance = Offset(-47, 0); 20 | 21 | Paragraph _paragraph; 22 | double _radians; 23 | Offset _position; 24 | 25 | ParagraphBuilder paragraphBuilder = ParagraphBuilder( 26 | ParagraphStyle( 27 | textAlign: TextAlign.start, 28 | textDirection: TextDirection.ltr, 29 | maxLines: 1, 30 | height: 0.5, 31 | fontStyle: FontStyle.normal, 32 | ), 33 | ); 34 | 35 | DistancePainter( 36 | {@material.required Offset start, 37 | @material.required Offset end, 38 | @material.required this.distance, 39 | @material.required this.viewCenter, 40 | @material.required double tolerance, 41 | @material.required DistanceStyle style}) { 42 | if (style.showTolerance) { 43 | _zeroPoint = _zeroPointWithTolerance; 44 | } else { 45 | _zeroPoint = _zeroPointWithoutTolerance; 46 | } 47 | 48 | var distanceValue = distance.value; 49 | 50 | if (distanceValue > 0) { 51 | _zeroPoint -= Offset( 52 | ((log(distanceValue) / _log10).floor() - 1) * _offsetPerDigit, 0); 53 | } 54 | 55 | var difference = end - start; 56 | _position = start + difference / 2.0; 57 | _radians = difference.direction; 58 | 59 | if (_radians.abs() >= pi / 2.0) { 60 | _radians += pi; 61 | } 62 | 63 | var positionToCenter = viewCenter - _position; 64 | 65 | var offset = difference.normal(); 66 | offset *= offset.cosAlpha(positionToCenter).sign; 67 | 68 | paragraphBuilder.pushStyle(TextStyle(color: style.textColor)); 69 | if (style.showTolerance) { 70 | paragraphBuilder.addText( 71 | '${distanceValue?.toStringAsFixed(style.numDecimalPlaces)}±${tolerance.toStringAsFixed(style.numDecimalPlaces)}${distance.getAbbreviation()}'); 72 | } else { 73 | paragraphBuilder.addText( 74 | '${distanceValue?.toStringAsFixed(style.numDecimalPlaces)}${distance.getAbbreviation()}'); 75 | } 76 | 77 | _paragraph = paragraphBuilder.build(); 78 | _paragraph.layout(ParagraphConstraints(width: 300)); 79 | 80 | _position += offset * 12; 81 | } 82 | 83 | @override 84 | void paint(Canvas canvas, Size size) { 85 | canvas.translate(_position.dx, _position.dy); 86 | canvas.rotate(_radians); 87 | 88 | canvas.drawParagraph(_paragraph, _zeroPoint); 89 | } 90 | 91 | @override 92 | bool shouldRepaint(material.CustomPainter oldDelegate) { 93 | var old = oldDelegate as DistancePainter; 94 | 95 | return distance != old.distance || _position != old._position; 96 | } 97 | } 98 | 99 | extension OffsetExtension on Offset { 100 | Offset normal() { 101 | var normalized = normalize(); 102 | return Offset(-normalized.dy, normalized.dx); 103 | } 104 | 105 | Offset normalize() { 106 | return this / distance; 107 | } 108 | 109 | double cosAlpha(Offset other) { 110 | var thisNormalized = normalize(); 111 | var otherNormalized = other.normalize(); 112 | 113 | return thisNormalized.dx * otherNormalized.dx + 114 | thisNormalized.dy * otherNormalized.dy; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/measurement/bloc/measure_bloc_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:bloc_test/bloc_test.dart'; 7 | import 'package:document_measure/src/input_bloc/input_bloc.dart'; 8 | import 'package:document_measure/src/input_bloc/input_state.dart'; 9 | import 'package:document_measure/src/measurement/bloc/magnification_bloc/magnification_bloc.dart'; 10 | import 'package:document_measure/src/measurement/bloc/magnification_bloc/magnification_event.dart'; 11 | import 'package:document_measure/src/measurement/bloc/magnification_bloc/magnification_state.dart'; 12 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 13 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 14 | import 'package:flutter_test/flutter_test.dart'; 15 | import 'package:get_it/get_it.dart'; 16 | import 'package:mockito/mockito.dart'; 17 | 18 | import '../../mocks/test_mocks.dart'; 19 | 20 | void main() { 21 | group('Measure Bloc Unit Test', () { 22 | final imageScaleFactor = 3.0; 23 | 24 | MetadataRepository mockedMetadataRepository; 25 | MeasurementRepository mockedMeasurementRepository; 26 | InputBloc mockedInputBloc; 27 | 28 | setUp(() { 29 | mockedMetadataRepository = MockedMetadataRepository(); 30 | mockedMeasurementRepository = MockedMeasurementRepository(); 31 | mockedInputBloc = MockedInputBloc(); 32 | 33 | when(mockedMetadataRepository.viewSize) 34 | .thenAnswer((_) => Stream.fromIterable([Size(100, 200)])); 35 | when(mockedMetadataRepository.measurement) 36 | .thenAnswer((_) => Stream.fromIterable([true])); 37 | when(mockedMetadataRepository.magnificationCircleRadius) 38 | .thenAnswer((_) => Stream.fromIterable([10])); 39 | 40 | GetIt.I.registerSingleton(mockedMeasurementRepository); 41 | GetIt.I.registerSingleton(mockedMetadataRepository); 42 | 43 | whenListen(mockedInputBloc, Stream.fromIterable([])); 44 | }); 45 | 46 | tearDown(() { 47 | GetIt.I.unregister(instance: mockedMetadataRepository); 48 | GetIt.I.unregister(instance: mockedMeasurementRepository); 49 | mockedInputBloc.close(); 50 | }); 51 | 52 | blocTest( 53 | 'initial state', 54 | skip: 0, 55 | build: () async { 56 | when(mockedMetadataRepository.backgroundImage) 57 | .thenAnswer((_) => Stream.fromIterable([])); 58 | when(mockedMetadataRepository.imageScaleFactor) 59 | .thenAnswer((_) => Stream.fromIterable([])); 60 | 61 | return MagnificationBloc(mockedInputBloc); 62 | }, 63 | expect: [MagnificationInactiveState()], 64 | ); 65 | 66 | group('UI events', () { 67 | blocTest( 68 | 'stroke events', 69 | build: () async { 70 | when(mockedMetadataRepository.backgroundImage) 71 | .thenAnswer((_) => Stream.fromIterable([MockedImage.mock])); 72 | when(mockedMetadataRepository.imageScaleFactor) 73 | .thenAnswer((_) => Stream.fromIterable([imageScaleFactor])); 74 | 75 | return MagnificationBloc(mockedInputBloc); 76 | }, 77 | act: (bloc) async { 78 | bloc.add(MagnificationShowEvent(Offset(0, 0))); 79 | bloc.add(MagnificationShowEvent(Offset(10, 10))); 80 | bloc.add(MagnificationHideEvent()); 81 | }, 82 | expect: [ 83 | MagnificationActiveState(Offset(0, 0), Offset(-10, -50), 84 | backgroundImage: MockedImage.mock, 85 | imageScaleFactor: imageScaleFactor), 86 | MagnificationActiveState(Offset(10, 10), Offset(0, -50), 87 | backgroundImage: MockedImage.mock, 88 | imageScaleFactor: imageScaleFactor), 89 | MagnificationInactiveState() 90 | ], 91 | verify: (_) async { 92 | verifyInOrder([ 93 | mockedMeasurementRepository 94 | .convertIntoDocumentLocalTopLeftPosition(Offset(0, 0)), 95 | mockedMeasurementRepository 96 | .convertIntoDocumentLocalTopLeftPosition(Offset(10, 10)), 97 | ]); 98 | 99 | verifyNoMoreInteractions(mockedMeasurementRepository); 100 | }, 101 | ); 102 | }); 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /lib/src/measurement/bloc/points_bloc/points_bloc.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:async'; 5 | import 'dart:ui'; 6 | 7 | import 'package:document_measure/src/measurement/bloc/points_bloc/points_event.dart'; 8 | import 'package:document_measure/src/measurement/bloc/points_bloc/points_state.dart'; 9 | import 'package:document_measure/src/measurement/overlay/holder.dart'; 10 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 11 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 12 | import 'package:document_measure/src/util/logger.dart'; 13 | import 'package:document_measure/src/util/utils.dart'; 14 | import 'package:flutter_bloc/flutter_bloc.dart'; 15 | import 'package:get_it/get_it.dart'; 16 | 17 | import '../../drawing_holder.dart'; 18 | 19 | class PointsBloc extends Bloc { 20 | final _logger = Logger(LogDistricts.POINTS_BLOC); 21 | final List _streamSubscriptions = []; 22 | 23 | MeasurementRepository _measureRepository; 24 | MetadataRepository _metadataRepository; 25 | 26 | StreamSubscription _onlyPointsSubscription; 27 | StreamSubscription _pointsAndDistancesSubscription; 28 | 29 | Function(List) _pointsListener; 30 | Function(DrawingHolder) _pointsAndDistanceListener; 31 | 32 | Offset _viewCenter; 33 | double _tolerance; 34 | 35 | PointsBloc() : super(PointsEmptyState()) { 36 | _pointsListener = (points) => add(PointsOnlyEvent(points)); 37 | _pointsAndDistanceListener = (holder) => 38 | add(PointsAndDistancesEvent(holder.points, holder.distances)); 39 | 40 | _measureRepository = GetIt.I(); 41 | _metadataRepository = GetIt.I(); 42 | 43 | _streamSubscriptions 44 | .add(_metadataRepository.showDistances.listen((showDistances) { 45 | if (showDistances) { 46 | if (_pointsAndDistancesSubscription == null) { 47 | _onlyPointsSubscription?.cancel(); 48 | _onlyPointsSubscription = null; 49 | 50 | _pointsAndDistancesSubscription = _measureRepository.drawingHolder 51 | .listen(_pointsAndDistanceListener); 52 | } 53 | } else { 54 | if (_onlyPointsSubscription == null) { 55 | _pointsAndDistancesSubscription?.cancel(); 56 | _pointsAndDistancesSubscription = null; 57 | 58 | _onlyPointsSubscription = 59 | _measureRepository.points.listen(_pointsListener); 60 | } 61 | } 62 | })); 63 | 64 | _streamSubscriptions.add(_metadataRepository.viewCenter 65 | .listen((center) => _viewCenter = center)); 66 | _streamSubscriptions.add(_metadataRepository.tolerance 67 | .listen((tolerance) => _tolerance = tolerance)); 68 | } 69 | 70 | @override 71 | void onEvent(PointsEvent event) { 72 | _logger.log('received event: $event'); 73 | super.onEvent(event); 74 | } 75 | 76 | @override 77 | Future close() { 78 | _streamSubscriptions.forEach((subscription) => subscription.cancel()); 79 | _onlyPointsSubscription?.cancel(); 80 | _pointsAndDistancesSubscription?.cancel(); 81 | return super.close(); 82 | } 83 | 84 | @override 85 | Stream mapEventToState(PointsEvent event) async* { 86 | if (event.points.isEmpty) { 87 | yield PointsEmptyState(); 88 | } else if (event.points.length == 1) { 89 | yield PointsSingleState(event.points[0]); 90 | } else { 91 | if (event is PointsOnlyEvent) { 92 | yield PointsOnlyState(event.points); 93 | } else if (event is PointsAndDistancesEvent) { 94 | yield _mapMultiplePointsWithDistancesToState(event); 95 | } 96 | } 97 | } 98 | 99 | PointsState _mapMultiplePointsWithDistancesToState( 100 | PointsAndDistancesEvent event) { 101 | var holders = []; 102 | event.points.doInBetween((start, end) => holders.add(Holder(start, end))); 103 | event.distances.asMap().forEach((index, distance) => 104 | holders[index] = Holder.extend(holders[index], distance)); 105 | 106 | if (event.distances.contains(null)) { 107 | var nullIndices = []; 108 | nullIndices.add(event.distances.indexOf(null)); 109 | nullIndices.add(event.distances.lastIndexOf(null)); 110 | 111 | return PointsAndDistanceActiveState( 112 | holders, _viewCenter, _tolerance, nullIndices); 113 | } else if (event.points.length - 1 > event.distances.length) { 114 | return PointsAndDistanceActiveState( 115 | holders, _viewCenter, _tolerance, [event.distances.length]); 116 | } else { 117 | return PointsAndDistanceState(holders, _viewCenter, _tolerance); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:document_measure/document_measure.dart'; 5 | import 'package:flutter/cupertino.dart'; 6 | import 'package:flutter/material.dart'; 7 | 8 | import 'colors.dart'; 9 | 10 | class MetadataRepository {} 11 | 12 | void main() { 13 | runApp(MyApp()); 14 | } 15 | 16 | class MyApp extends StatefulWidget { 17 | @override 18 | _MyAppState createState() => _MyAppState(); 19 | } 20 | 21 | class _MyAppState extends State { 22 | static String originalTitle = 'Measurement app'; 23 | String title = originalTitle; 24 | bool measure = true; 25 | bool showDistanceOnLine = true; 26 | bool showTolerance = false; 27 | bool zoomed = false; 28 | 29 | List unitsOfMeasurement = [ 30 | Meter.asUnit(), 31 | Millimeter.asUnit(), 32 | Inch.asUnit(), 33 | Foot.asUnit() 34 | ]; 35 | int unitIndex = 0; 36 | 37 | MeasurementController controller; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | 43 | controller = MeasurementController(); 44 | } 45 | 46 | Color getButtonColor(bool selected) { 47 | if (selected) { 48 | return selectedColor; 49 | } else { 50 | return unselectedColor; 51 | } 52 | } 53 | 54 | @override 55 | Widget build(BuildContext context) { 56 | return MaterialApp( 57 | home: Scaffold( 58 | appBar: AppBar( 59 | backgroundColor: Color(0xff1280b3), 60 | title: Row( 61 | children: [ 62 | IconButton( 63 | onPressed: () => setState(() { 64 | measure = !measure; 65 | title = originalTitle; 66 | }), 67 | icon: Icon( 68 | Icons.straighten, 69 | color: getButtonColor( 70 | measure, 71 | ), 72 | ), 73 | ), 74 | IconButton( 75 | onPressed: () => 76 | setState(() => showDistanceOnLine = !showDistanceOnLine), 77 | icon: Icon( 78 | Icons.vertical_align_bottom, 79 | color: getButtonColor(showDistanceOnLine), 80 | ), 81 | ), 82 | SizedBox.fromSize( 83 | child: MaterialButton( 84 | shape: CircleBorder(), 85 | onPressed: () => 86 | setState(() => showTolerance = !showTolerance), 87 | child: Text('±'), 88 | textColor: getButtonColor(showTolerance), 89 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 90 | ), 91 | size: Size(52, 52), 92 | ), 93 | SizedBox.fromSize( 94 | child: MaterialButton( 95 | shape: CircleBorder(), 96 | onPressed: () => setState(() => 97 | unitIndex = (unitIndex + 1) % unitsOfMeasurement.length), 98 | child: Text(unitsOfMeasurement[unitIndex].getAbbreviation()), 99 | textColor: unselectedColor, 100 | materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, 101 | ), 102 | size: Size(64, 64), 103 | ), 104 | IconButton( 105 | onPressed: () { 106 | if (zoomed) { 107 | controller.resetZoom(); 108 | } else { 109 | controller.zoomToLifeSize(); 110 | } 111 | 112 | setState(() { 113 | zoomed = !zoomed; 114 | }); 115 | }, 116 | icon: 117 | Icon(Icons.zoom_out_map, color: getButtonColor(zoomed))), 118 | ], 119 | ), 120 | ), 121 | body: Center( 122 | child: Measurements( 123 | child: Image.asset( 124 | 'assets/images/floorplan448x449mm.png', 125 | ), 126 | measurementInformation: MeasurementInformation( 127 | scale: 1 / 50.0, 128 | documentWidthInLengthUnits: Millimeter(448), 129 | documentHeightInLengthUnits: Millimeter(449), 130 | targetLengthUnit: unitsOfMeasurement[unitIndex], 131 | ), 132 | controller: controller, 133 | showDistanceOnLine: showDistanceOnLine, 134 | distanceStyle: DistanceStyle( 135 | showTolerance: showTolerance, 136 | ), 137 | measure: measure, 138 | ), 139 | ), 140 | ), 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /test/metadata/bloc/metadata_bloc_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:bloc_test/bloc_test.dart'; 7 | import 'package:document_measure/document_measure.dart'; 8 | import 'package:document_measure/src/metadata/bloc/metadata_bloc.dart'; 9 | import 'package:document_measure/src/metadata/bloc/metadata_event.dart'; 10 | import 'package:document_measure/src/metadata/bloc/metadata_state.dart'; 11 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 12 | import 'package:flutter_test/flutter_test.dart'; 13 | import 'package:get_it/get_it.dart'; 14 | import 'package:mockito/mockito.dart'; 15 | import 'package:rxdart/rxdart.dart'; 16 | 17 | import '../../mocks/test_mocks.dart'; 18 | 19 | void main() { 20 | group('Metadata Bloc Unit Test', () { 21 | MetadataRepository mockedRepository; 22 | BehaviorSubject measurement; 23 | Image mockedImage; 24 | 25 | final measurementInformation = MeasurementInformation.dinA4(scale: 1.0); 26 | final measure = true; 27 | final showDistance = true; 28 | final magnificationStyle = MagnificationStyle(); 29 | final measurementController = MeasurementController(); 30 | 31 | final startedEvent = MetadataStartedEvent( 32 | measurementInformation: measurementInformation, 33 | measure: measure, 34 | showDistances: showDistance, 35 | magnificationStyle: magnificationStyle, 36 | controller: measurementController, 37 | ); 38 | 39 | setUp(() { 40 | mockedImage = MockedImage.mock; 41 | measurement = BehaviorSubject(); 42 | mockedRepository = MockedMetadataRepository(); 43 | GetIt.I.registerSingleton(mockedRepository); 44 | }); 45 | 46 | tearDown(() { 47 | GetIt.I.unregister(instance: mockedRepository); 48 | measurement?.close(); 49 | }); 50 | 51 | blocTest( 52 | 'initial state', 53 | skip: 0, 54 | build: () async => MetadataBloc(), 55 | expect: [MetadataState()], 56 | ); 57 | 58 | group('metadata events', () { 59 | blocTest( 60 | 'started event', 61 | build: () async => MetadataBloc(), 62 | act: (bloc) => bloc.add(startedEvent), 63 | verify: (MetadataBloc bloc) async { 64 | verify(mockedRepository.registerStartupValuesChange( 65 | measurementInformation: measurementInformation, 66 | measure: measure, 67 | showDistance: showDistance, 68 | magnificationStyle: magnificationStyle, 69 | controller: measurementController, 70 | )).called(1); 71 | }, 72 | ); 73 | 74 | blocTest( 75 | 'background event', 76 | skip: 0, 77 | build: () async => MetadataBloc(), 78 | act: (bloc) => 79 | bloc.add(MetadataBackgroundEvent(mockedImage, Size(300, 400))), 80 | verify: (MetadataBloc bloc) async { 81 | verify(mockedRepository.registerBackgroundChange( 82 | mockedImage, 83 | Size(300, 400), 84 | )).called(1); 85 | }, 86 | ); 87 | 88 | blocTest( 89 | 'delete region event', 90 | build: () async => MetadataBloc(), 91 | act: (bloc) => 92 | bloc.add(MetadataDeleteRegionEvent(Offset(10, 10), Size(10, 10))), 93 | verify: (MetadataBloc bloc) async { 94 | verify(mockedRepository.registerDeleteRegion( 95 | Offset(10, 10), Size(10, 10))); 96 | }, 97 | ); 98 | 99 | blocTest( 100 | 'started, background and delete event', 101 | build: () async { 102 | when(mockedRepository.measurement) 103 | .thenAnswer((_) => Stream.fromIterable([true])); 104 | 105 | return MetadataBloc(); 106 | }, 107 | act: (bloc) async { 108 | bloc.add(startedEvent); 109 | bloc.add(MetadataBackgroundEvent(mockedImage, Size(300, 400))); 110 | bloc.add(MetadataDeleteRegionEvent(Offset(10, 10), Size(10, 10))); 111 | }, 112 | verify: (MetadataBloc bloc) async { 113 | verifyInOrder([ 114 | mockedRepository.registerStartupValuesChange( 115 | measurementInformation: measurementInformation, 116 | measure: measure, 117 | showDistance: showDistance, 118 | magnificationStyle: magnificationStyle, 119 | controller: measurementController, 120 | ), 121 | mockedRepository.registerBackgroundChange( 122 | mockedImage, 123 | Size(300, 400), 124 | ), 125 | mockedRepository.registerDeleteRegion( 126 | Offset(10, 10), 127 | Size(10, 10), 128 | ), 129 | ]); 130 | }, 131 | ); 132 | }); 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/measurement/overlay/measure_area.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:document_measure/document_measure.dart'; 5 | import 'package:document_measure/src/measurement/bloc/magnification_bloc/magnification_bloc.dart'; 6 | import 'package:document_measure/src/measurement/bloc/magnification_bloc/magnification_state.dart'; 7 | import 'package:document_measure/src/measurement/bloc/points_bloc/points_bloc.dart'; 8 | import 'package:document_measure/src/measurement/bloc/points_bloc/points_state.dart'; 9 | import 'package:document_measure/src/util/utils.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter_bloc/flutter_bloc.dart'; 12 | 13 | import 'painters/distance_painter.dart'; 14 | import 'painters/magnifying_painter.dart'; 15 | import 'painters/measure_painter.dart'; 16 | 17 | class MeasureArea extends StatelessWidget { 18 | final PointStyle pointStyle; 19 | final MagnificationStyle magnificationStyle; 20 | final DistanceStyle distanceStyle; 21 | final Paint dotPaint = Paint(), pathPaint = Paint(); 22 | 23 | MeasureArea( 24 | {@required this.pointStyle, 25 | @required this.magnificationStyle, 26 | @required this.distanceStyle}) { 27 | var lineType = pointStyle.lineType; 28 | double strokeWidth; 29 | if (lineType is SolidLine) { 30 | strokeWidth = lineType.lineWidth; 31 | } else if (lineType is DashedLine) { 32 | strokeWidth = lineType.dashWidth; 33 | } else { 34 | throw UnimplementedError( 35 | 'This line type is not supported! Type was: $lineType'); 36 | } 37 | 38 | dotPaint.color = pointStyle.dotColor; 39 | 40 | pathPaint 41 | ..style = PaintingStyle.stroke 42 | ..color = pointStyle.lineType.lineColor 43 | ..strokeWidth = strokeWidth; 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | return Stack( 49 | fit: StackFit.expand, 50 | children: [ 51 | BlocBuilder( 52 | builder: (context, state) => _pointsOverlay(state), 53 | ), 54 | BlocBuilder( 55 | builder: (context, state) => _magnificationOverlay(state), 56 | ), 57 | ], 58 | ); 59 | } 60 | 61 | Stack _pointsOverlay(PointsState state) { 62 | var widgets = []; 63 | 64 | if (state is PointsSingleState) { 65 | widgets.add(_pointPainter(state.point, state.point)); 66 | } else if (state is PointsOnlyState) { 67 | widgets.addAll(_onlyPoints(state)); 68 | } else if (state is PointsAndDistanceActiveState) { 69 | widgets.addAll(_pointsAndDistancesWithSpace(state)); 70 | } else if (state is PointsAndDistanceState) { 71 | widgets.addAll(_pointsAndDistances(state)); 72 | } 73 | 74 | return Stack( 75 | children: widgets, 76 | ); 77 | } 78 | 79 | List _onlyPoints(PointsOnlyState state) { 80 | var widgets = []; 81 | 82 | state.points 83 | .doInBetween((start, end) => widgets.add(_pointPainter(start, end))); 84 | 85 | return widgets; 86 | } 87 | 88 | Iterable _pointsAndDistancesWithSpace( 89 | PointsAndDistanceActiveState state) { 90 | var widgets = []; 91 | 92 | state.holders.asMap().forEach((index, holder) { 93 | widgets.add(_pointPainter(holder.start, holder.end)); 94 | if (!state.nullIndices.contains(index)) { 95 | widgets.add(_distancePainter(holder.start, holder.end, holder.distance, 96 | state.tolerance, state.viewCenter)); 97 | } 98 | }); 99 | 100 | return widgets; 101 | } 102 | 103 | List _pointsAndDistances(PointsAndDistanceState state) { 104 | var widgets = []; 105 | 106 | state.holders.forEach((holder) { 107 | widgets.add(_pointPainter(holder.start, holder.end)); 108 | widgets.add(_distancePainter(holder.start, holder.end, holder.distance, 109 | state.tolerance, state.viewCenter)); 110 | }); 111 | 112 | return widgets; 113 | } 114 | 115 | CustomPaint _pointPainter(Offset first, Offset last) { 116 | return CustomPaint( 117 | foregroundPainter: MeasurePainter( 118 | start: first, 119 | end: last, 120 | style: pointStyle, 121 | dotPaint: dotPaint, 122 | pathPaint: pathPaint, 123 | ), 124 | ); 125 | } 126 | 127 | CustomPaint _distancePainter(Offset first, Offset last, LengthUnit distance, 128 | double tolerance, Offset viewCenter) { 129 | return CustomPaint( 130 | foregroundPainter: DistancePainter( 131 | start: first, 132 | end: last, 133 | distance: distance, 134 | tolerance: tolerance, 135 | viewCenter: viewCenter, 136 | style: distanceStyle, 137 | ), 138 | ); 139 | } 140 | 141 | Widget _magnificationOverlay(MagnificationState state) { 142 | if (state is MagnificationActiveState) { 143 | return CustomPaint( 144 | foregroundPainter: MagnifyingPainter( 145 | fingerPosition: state.position, 146 | absolutePosition: state.absolutePosition, 147 | image: state.backgroundImage, 148 | imageScaleFactor: state.imageScaleFactor, 149 | style: magnificationStyle, 150 | magnificationOffset: state.magnificationOffset, 151 | ), 152 | ); 153 | } 154 | 155 | return Opacity( 156 | opacity: 0.0, 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib/src/measurement/bloc/magnification_bloc/magnification_bloc.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:async'; 5 | import 'dart:ui'; 6 | 7 | import 'package:document_measure/src/input_bloc/input_bloc.dart'; 8 | import 'package:document_measure/src/input_bloc/input_state.dart'; 9 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 10 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 11 | import 'package:flutter_bloc/flutter_bloc.dart'; 12 | import 'package:get_it/get_it.dart'; 13 | 14 | import 'magnification_event.dart'; 15 | import 'magnification_state.dart'; 16 | 17 | class MagnificationBloc extends Bloc { 18 | final _defaultMagnificationOffset = Offset(0, 40); 19 | final InputBloc inputBloc; 20 | final List _streamSubscriptions = []; 21 | 22 | MeasurementRepository _measureRepository; 23 | MetadataRepository _metadataRepository; 24 | 25 | Image _backgroundImage; 26 | double _imageScaleFactor; 27 | Size _viewSize; 28 | double _magnificationRadius; 29 | Offset _magnificationOffset; 30 | 31 | MagnificationBloc(this.inputBloc) : super(MagnificationInactiveState()) { 32 | _measureRepository = GetIt.I(); 33 | _metadataRepository = GetIt.I(); 34 | 35 | _streamSubscriptions.add(_metadataRepository.backgroundImage 36 | .listen((image) => _backgroundImage = image)); 37 | _streamSubscriptions.add(_metadataRepository.imageScaleFactor 38 | .listen((factor) => _imageScaleFactor = factor)); 39 | _streamSubscriptions 40 | .add(_metadataRepository.viewSize.listen((size) => _viewSize = size)); 41 | _streamSubscriptions 42 | .add(_metadataRepository.magnificationCircleRadius.listen((radius) { 43 | _magnificationRadius = radius; 44 | _magnificationOffset = Offset(_defaultMagnificationOffset.dx, 45 | _defaultMagnificationOffset.dy + radius); 46 | })); 47 | 48 | _streamSubscriptions.add(inputBloc.listen((state) { 49 | if (state is InputStandardState) { 50 | add(MagnificationShowEvent(state.position)); 51 | } else if (state is InputEmptyState) { 52 | add(MagnificationHideEvent()); 53 | } else if (state is InputDeleteRegionState) { 54 | add(MagnificationHideEvent()); 55 | } else if (state is InputEndedState) { 56 | add(MagnificationHideEvent()); 57 | } else if (state is InputDeleteState) { 58 | add(MagnificationHideEvent()); 59 | } 60 | })); 61 | } 62 | 63 | @override 64 | Stream> 65 | transformTransitions( 66 | Stream> 67 | transitions) { 68 | return transitions 69 | .map((Transition transition) { 70 | final state = transition.nextState; 71 | if (state is MagnificationActiveState) { 72 | return Transition( 73 | currentState: transition.currentState, 74 | event: transition.event, 75 | nextState: MagnificationActiveState( 76 | state.position, 77 | state.magnificationOffset, 78 | absolutePosition: _measureRepository 79 | .convertIntoDocumentLocalTopLeftPosition(state.position), 80 | backgroundImage: _backgroundImage, 81 | imageScaleFactor: _imageScaleFactor, 82 | )); 83 | } else { 84 | return transition; 85 | } 86 | }); 87 | } 88 | 89 | @override 90 | Stream mapEventToState(MagnificationEvent event) async* { 91 | if (event is MagnificationShowEvent) { 92 | yield _mapMagnificationShowToState(event); 93 | } else if (event is MagnificationHideEvent) { 94 | yield MagnificationInactiveState(); 95 | } 96 | } 97 | 98 | @override 99 | Future close() { 100 | _streamSubscriptions.forEach((subscription) => subscription.cancel()); 101 | return super.close(); 102 | } 103 | 104 | MagnificationState _mapMagnificationShowToState( 105 | MagnificationShowEvent event) { 106 | var magnificationPosition = event.position - _magnificationOffset; 107 | 108 | if (_magnificationGlassFitsWithoutModification(magnificationPosition)) { 109 | return MagnificationActiveState(event.position, _magnificationOffset); 110 | } else { 111 | var modifiedOffset = _magnificationOffset; 112 | 113 | if (event.position.dy < _magnificationOffset.dy + _magnificationRadius) { 114 | modifiedOffset = Offset(modifiedOffset.dx, -modifiedOffset.dy); 115 | } 116 | 117 | if (event.position.dx < _magnificationRadius) { 118 | modifiedOffset = 119 | Offset(event.position.dx - _magnificationRadius, modifiedOffset.dy); 120 | } else if (event.position.dx > _viewSize.width - _magnificationRadius) { 121 | modifiedOffset = Offset( 122 | _magnificationRadius - (_viewSize.width - event.position.dx), 123 | modifiedOffset.dy); 124 | } 125 | 126 | return MagnificationActiveState(event.position, modifiedOffset); 127 | } 128 | } 129 | 130 | bool _magnificationGlassFitsWithoutModification( 131 | Offset magnificationPosition) => 132 | magnificationPosition > 133 | Offset(_magnificationRadius, _magnificationRadius) && 134 | magnificationPosition < 135 | Offset(_viewSize.width - _magnificationRadius, 136 | _viewSize.height - _magnificationRadius); 137 | } 138 | -------------------------------------------------------------------------------- /test/scale/scale_bloc_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:bloc_test/bloc_test.dart'; 5 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 6 | import 'package:document_measure/src/scale_bloc/scale_bloc.dart'; 7 | import 'package:document_measure/src/scale_bloc/scale_event.dart'; 8 | import 'package:document_measure/src/scale_bloc/scale_state.dart'; 9 | import 'package:flutter/cupertino.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | import 'package:get_it/get_it.dart'; 12 | import 'package:mockito/mockito.dart'; 13 | 14 | import '../mocks/test_mocks.dart'; 15 | 16 | void main() { 17 | group('Scale Bloc Test', () { 18 | MetadataRepository mockedMetadataRepository; 19 | final defaultOffset = Offset(25, 50); 20 | 21 | setUp(() { 22 | mockedMetadataRepository = MockedMetadataRepository(); 23 | 24 | when(mockedMetadataRepository.measurement) 25 | .thenAnswer((_) => Stream.fromIterable([false])); 26 | when(mockedMetadataRepository.viewSize) 27 | .thenAnswer((_) => Stream.fromIterable([Size(50, 100)])); 28 | when(mockedMetadataRepository.screenSize) 29 | .thenAnswer((_) => Stream.fromIterable([Size(100, 200)])); 30 | when(mockedMetadataRepository.zoomFactorForLifeSize) 31 | .thenAnswer((_) async => 2.0); 32 | when(mockedMetadataRepository.zoomFactorToFillScreen).thenReturn(5.0); 33 | when(mockedMetadataRepository.isDocumentWidthAlignedWithScreenWidth(any)) 34 | .thenReturn(true); 35 | 36 | GetIt.I.registerSingleton(mockedMetadataRepository); 37 | }); 38 | 39 | tearDown(() { 40 | GetIt.I.unregister(instance: mockedMetadataRepository); 41 | }); 42 | 43 | blocTest( 44 | 'initial state', 45 | build: () async => ScaleBloc(), 46 | skip: 0, 47 | expect: [ 48 | ScaleState(Offset(0, 0), 1.0, Matrix4.identity()), 49 | ScaleState(defaultOffset, 1.0, 50 | Matrix4.identity()..translate(defaultOffset.dx, defaultOffset.dy)), 51 | ], 52 | ); 53 | 54 | group('single touch events', () { 55 | blocTest( 56 | 'panning', 57 | build: () async => ScaleBloc(), 58 | act: (bloc) async { 59 | bloc.add(ScaleStartEvent(Offset(0, 0))); 60 | bloc.add(ScaleUpdateEvent(Offset(10, 0), 1.0)); 61 | }, 62 | expect: [ 63 | ScaleState( 64 | defaultOffset + Offset(10, 0), 65 | 1.0, 66 | Matrix4.identity() 67 | ..translate(defaultOffset.dx + 10.0, defaultOffset.dy)), 68 | ], 69 | ); 70 | 71 | blocTest( 72 | 'zooming', 73 | build: () async => ScaleBloc(), 74 | act: (bloc) async { 75 | bloc.add(ScaleStartEvent(Offset(0, 0))); 76 | bloc.add(ScaleUpdateEvent(Offset(10, 0), 2.0)); 77 | }, 78 | expect: [ 79 | ScaleState( 80 | defaultOffset, 81 | 2.0, 82 | Matrix4.identity() 83 | ..translate(defaultOffset.dx, defaultOffset.dy) 84 | ..scale(2.0)), 85 | ], 86 | ); 87 | 88 | blocTest( 89 | 'zooming out should clamp at 1.0', 90 | build: () async => ScaleBloc(), 91 | act: (bloc) async { 92 | bloc.add(ScaleStartEvent(Offset(0, 0))); 93 | bloc.add(ScaleUpdateEvent(Offset(10, 0), 0.12)); 94 | }, 95 | expect: [ 96 | ScaleState( 97 | defaultOffset, 98 | 1.0, 99 | Matrix4.identity() 100 | ..translate(defaultOffset.dx, defaultOffset.dy) 101 | ..scale(1.0)), 102 | ], 103 | ); 104 | 105 | blocTest( 106 | 'zoom and then pan', 107 | build: () async => ScaleBloc(), 108 | act: (bloc) async { 109 | bloc.add(ScaleStartEvent(Offset(0, 0))); 110 | bloc.add(ScaleUpdateEvent(Offset(10, 0), 2.0)); 111 | 112 | bloc.add(ScaleStartEvent(Offset(0, 0))); 113 | bloc.add(ScaleUpdateEvent(Offset(10, 0), 1.0)); 114 | }, 115 | expect: [ 116 | ScaleState( 117 | defaultOffset + Offset(10, 0), 118 | 2.0, 119 | Matrix4.identity() 120 | ..translate(defaultOffset.dx + 10, defaultOffset.dy) 121 | ..scale(2.0)), 122 | ], 123 | ); 124 | 125 | blocTest( 126 | 'zoom twice', 127 | build: () async => ScaleBloc(), 128 | act: (bloc) async { 129 | bloc.add(ScaleStartEvent(Offset(0, 0))); 130 | bloc.add(ScaleUpdateEvent(Offset(10, 0), 2.0)); 131 | 132 | bloc.add(ScaleStartEvent(Offset(0, 0))); 133 | bloc.add(ScaleUpdateEvent(Offset(10, 0), 3.0)); 134 | }, 135 | expect: [ 136 | ScaleState( 137 | defaultOffset, 138 | 6.0, 139 | Matrix4.identity() 140 | ..translate(defaultOffset.dx, defaultOffset.dy) 141 | ..scale(6.0)), 142 | ], 143 | ); 144 | }); 145 | 146 | group('double tap', () { 147 | blocTest( 148 | 'single double tap event', 149 | build: () async => ScaleBloc(), 150 | act: (bloc) async => bloc.add(ScaleDoubleTapEvent()), 151 | expect: [ 152 | ScaleState( 153 | defaultOffset, 154 | 5.0, 155 | Matrix4.identity() 156 | ..translate(defaultOffset.dx, defaultOffset.dy) 157 | ..scale(5.0)), 158 | ], 159 | ); 160 | }); 161 | }); 162 | } 163 | -------------------------------------------------------------------------------- /lib/src/scale_bloc/scale_bloc.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:async'; 5 | 6 | import 'package:document_measure/document_measure.dart'; 7 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 8 | import 'package:document_measure/src/util/logger.dart'; 9 | import 'package:document_measure/src/util/utils.dart'; 10 | import 'package:flutter/cupertino.dart'; 11 | import 'package:flutter/material.dart'; 12 | import 'package:flutter_bloc/flutter_bloc.dart'; 13 | import 'package:get_it/get_it.dart'; 14 | 15 | import 'scale_event.dart'; 16 | import 'scale_state.dart'; 17 | 18 | class ScaleBloc extends Bloc 19 | implements MeasurementFunction { 20 | final logger = Logger(LogDistricts.SCALE_BLOC); 21 | final List subscriptions = []; 22 | 23 | final double _minScale = 1.0; 24 | final double _maxScale = 10.0; 25 | 26 | MetadataRepository _metadataRepository; 27 | 28 | Offset _translateStart; 29 | Offset _workingTranslate = Offset(0, 0); 30 | Offset _currentTranslate = Offset(0, 0); 31 | 32 | Size _screenSize; 33 | Size _viewSize; 34 | Offset _defaultOffset = Offset(0, 0); 35 | 36 | double _currentScale = 1.0; 37 | double _accumulatedScale = 1.0; 38 | double _doubleTapScale = 1.0; 39 | double _originalScale; 40 | 41 | bool _measure; 42 | 43 | ScaleBloc() : super(ScaleState(Offset(0, 0), 1.0, Matrix4.identity())) { 44 | _metadataRepository = GetIt.I(); 45 | 46 | subscriptions.add(_metadataRepository.measurement 47 | .listen((measure) => _measure = measure)); 48 | subscriptions.add(_metadataRepository.viewSize.listen((size) { 49 | _viewSize = size; 50 | _updateDefaultOffset(); 51 | })); 52 | subscriptions.add(_metadataRepository.screenSize.listen((size) async { 53 | _screenSize = size; 54 | _updateDefaultOffset(); 55 | 56 | _doubleTapScale = _metadataRepository.zoomFactorToFillScreen; 57 | _originalScale = await _metadataRepository.zoomFactorForLifeSize; 58 | })); 59 | 60 | _metadataRepository.registerMeasurementFunction(this); 61 | } 62 | 63 | @override 64 | void onEvent(ScaleEvent event) { 65 | if (event is ScaleOriginalEvent && _originalScale != null) { 66 | _currentScale = _originalScale; 67 | _accumulatedScale = _currentScale; 68 | _registerResizing(); 69 | } else if (event is ScaleResetEvent) { 70 | _currentScale = 1.0; 71 | _accumulatedScale = _currentScale; 72 | _registerResizing(); 73 | } 74 | 75 | if (_measure) return; 76 | 77 | if (event is ScaleStartEvent) { 78 | _translateStart = event.position; 79 | 80 | _currentTranslate = _workingTranslate; 81 | _currentScale = _accumulatedScale; 82 | } else if (event is ScaleUpdateEvent) { 83 | if (event.scale == 1.0) { 84 | _workingTranslate = _currentTranslate.fitInto( 85 | _viewSize, 86 | _screenSize, 87 | _defaultOffset, 88 | event.position - _translateStart, 89 | 0.01, 90 | _accumulatedScale) - 91 | _defaultOffset; 92 | } else { 93 | _accumulatedScale = 94 | (_currentScale * event.scale).fit(_minScale, _maxScale); 95 | } 96 | } else if (event is ScaleDoubleTapEvent) { 97 | if (_currentScale == 1.0) { 98 | _currentScale = _doubleTapScale; 99 | } else { 100 | _currentScale = 1.0; 101 | } 102 | 103 | _currentTranslate = Offset(0, 0); 104 | 105 | _accumulatedScale = _currentScale; 106 | _workingTranslate = _currentTranslate; 107 | } 108 | 109 | _registerResizing(); 110 | super.onEvent(event); 111 | } 112 | 113 | @override 114 | Future close() { 115 | subscriptions.forEach((subscription) => subscription.cancel()); 116 | 117 | return super.close(); 118 | } 119 | 120 | @override 121 | Stream mapEventToState(ScaleEvent event) async* { 122 | final offset = _getTranslate(); 123 | 124 | if (event is ScaleOriginalEvent) { 125 | yield ScaleState( 126 | offset, 127 | _originalScale, 128 | Matrix4.identity() 129 | ..translate(offset.dx, offset.dy) 130 | ..scale(_originalScale), 131 | ); 132 | } else if (event is ScaleResetEvent) { 133 | yield ScaleState( 134 | _defaultOffset, 135 | 1.0, 136 | Matrix4.identity() 137 | ..translate(_defaultOffset.dx, _defaultOffset.dy) 138 | ..scale(1.0), 139 | ); 140 | } else { 141 | yield ScaleState( 142 | offset, 143 | _accumulatedScale, 144 | Matrix4.identity() 145 | ..translate(offset.dx, offset.dy) 146 | ..scale(_accumulatedScale), 147 | ); 148 | } 149 | } 150 | 151 | @override 152 | bool resetZoom() { 153 | add(ScaleResetEvent()); 154 | return true; 155 | } 156 | 157 | @override 158 | bool zoomToLifeSize() { 159 | if (!(_originalScale?.isInBounds(_minScale, _maxScale) ?? false)) 160 | return false; 161 | 162 | add(ScaleOriginalEvent()); 163 | return true; 164 | } 165 | 166 | void _registerResizing() => 167 | _metadataRepository.registerResizing(_getTranslate(), _accumulatedScale); 168 | 169 | Offset _getTranslate() => _defaultOffset + _workingTranslate; 170 | 171 | void _updateDefaultOffset() { 172 | if (_screenSize == null || _viewSize == null) return; 173 | 174 | _defaultOffset = Offset((_screenSize.width - _viewSize.width) / 2.0, 175 | (_screenSize.height - _viewSize.height) / 2.0); 176 | 177 | add(ScaleCenterUpdatedEvent()); 178 | _registerResizing(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /test/input/input_bloc_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:bloc_test/bloc_test.dart'; 7 | import 'package:document_measure/src/input_bloc/input_bloc.dart'; 8 | import 'package:document_measure/src/input_bloc/input_event.dart'; 9 | import 'package:document_measure/src/input_bloc/input_state.dart'; 10 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 11 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 12 | import 'package:flutter_test/flutter_test.dart'; 13 | import 'package:get_it/get_it.dart'; 14 | import 'package:mockito/mockito.dart'; 15 | 16 | import '../mocks/test_mocks.dart'; 17 | 18 | void main() { 19 | group('Input Bloc Test', () { 20 | MetadataRepository mockedMetadataRepository; 21 | MeasurementRepository mockedMeasurementRepository; 22 | 23 | setUp(() { 24 | mockedMetadataRepository = MockedMetadataRepository(); 25 | mockedMeasurementRepository = MockedMeasurementRepository(); 26 | 27 | when(mockedMetadataRepository.measurement) 28 | .thenAnswer((_) => Stream.fromIterable([])); 29 | 30 | GetIt.I.registerSingleton(mockedMetadataRepository); 31 | GetIt.I.registerSingleton(mockedMeasurementRepository); 32 | }); 33 | 34 | tearDown(() { 35 | GetIt.I.unregister(instance: mockedMeasurementRepository); 36 | GetIt.I.unregister(instance: mockedMetadataRepository); 37 | }); 38 | 39 | blocTest( 40 | 'initial state', 41 | build: () async => InputBloc(), 42 | skip: 0, 43 | expect: [InputEmptyState()], 44 | ); 45 | 46 | group('with measuring', () { 47 | var deleteRegion = Rect.fromPoints(Offset(10, 10), Offset(20, 20)); 48 | 49 | setUp(() { 50 | when(mockedMetadataRepository.measurement) 51 | .thenAnswer((_) => Stream.fromIterable([true])); 52 | when(mockedMetadataRepository.isInDeleteRegion(any)).thenAnswer( 53 | (realInvocation) => 54 | deleteRegion.contains(realInvocation.positionalArguments[0])); 55 | }); 56 | 57 | blocTest( 58 | 'down move up all outside of delete area', 59 | build: () async => InputBloc(), 60 | act: (bloc) async { 61 | bloc.add(InputDownEvent(Offset(50, 50))); 62 | bloc.add(InputMoveEvent(Offset(60, 60))); 63 | bloc.add(InputUpEvent(Offset(70, 70))); 64 | }, 65 | expect: [ 66 | InputStandardState(Offset(50, 50)), 67 | InputStandardState(Offset(60, 60)), 68 | InputEndedState(Offset(70, 70)), 69 | ], 70 | ); 71 | 72 | blocTest( 73 | 'down outside then move to delete area and up in there', 74 | build: () async => InputBloc(), 75 | act: (bloc) async { 76 | bloc.add(InputDownEvent(Offset(50, 50))); 77 | bloc.add(InputMoveEvent(Offset(15, 15))); 78 | bloc.add(InputUpEvent(Offset(15, 15))); 79 | }, 80 | expect: [ 81 | InputStandardState(Offset(50, 50)), 82 | InputDeleteRegionState(Offset(15, 15)), 83 | InputDeleteState(), 84 | ], 85 | ); 86 | 87 | blocTest( 88 | 'down move up all in delete area', 89 | build: () async => InputBloc(), 90 | act: (bloc) async { 91 | bloc.add(InputDownEvent(Offset(12, 12))); 92 | bloc.add(InputMoveEvent(Offset(15, 15))); 93 | bloc.add(InputUpEvent(Offset(16, 16))); 94 | }, 95 | expect: [ 96 | InputStandardState(Offset(12, 12)), 97 | InputStandardState(Offset(15, 15)), 98 | InputEndedState(Offset(16, 16)), 99 | ], 100 | ); 101 | 102 | blocTest( 103 | 'down in delete area then move out and up', 104 | build: () async => InputBloc(), 105 | act: (bloc) async { 106 | bloc.add(InputDownEvent(Offset(12, 12))); 107 | bloc.add(InputMoveEvent(Offset(60, 60))); 108 | bloc.add(InputUpEvent(Offset(70, 70))); 109 | }, 110 | expect: [ 111 | InputStandardState(Offset(12, 12)), 112 | InputStandardState(Offset(60, 60)), 113 | InputEndedState(Offset(70, 70)), 114 | ], 115 | ); 116 | 117 | blocTest( 118 | 'down in delete area then move outside and back in and up in delete area', 119 | build: () async => InputBloc(), 120 | act: (bloc) async { 121 | bloc.add(InputDownEvent(Offset(12, 12))); 122 | bloc.add(InputMoveEvent(Offset(60, 60))); 123 | bloc.add(InputMoveEvent(Offset(15, 15))); 124 | bloc.add(InputUpEvent(Offset(70, 70))); 125 | }, 126 | expect: [ 127 | InputStandardState(Offset(12, 12)), 128 | InputStandardState(Offset(60, 60)), 129 | InputStandardState(Offset(15, 15)), 130 | InputEndedState(Offset(70, 70)), 131 | ], 132 | ); 133 | }); 134 | 135 | group('without measuring', () { 136 | var deleteRegion = Rect.fromPoints(Offset(0, 0), Offset(0, 0)); 137 | 138 | setUp(() { 139 | when(mockedMetadataRepository.measurement) 140 | .thenAnswer((_) => Stream.fromIterable([false])); 141 | when(mockedMetadataRepository.isInDeleteRegion(any)).thenAnswer( 142 | (realInvocation) => 143 | deleteRegion.contains(realInvocation.positionalArguments[0])); 144 | }); 145 | 146 | blocTest( 147 | 'down move up should ignore all', 148 | build: () async => InputBloc(), 149 | act: (bloc) async { 150 | bloc.add(InputDownEvent(Offset(12, 12))); 151 | bloc.add(InputMoveEvent(Offset(15, 15))); 152 | bloc.add(InputUpEvent(Offset(70, 70))); 153 | }, 154 | expect: [ 155 | // Same states as initial state, therefore no will be emitted 156 | // InputEmptyState(), 157 | // InputEmptyState(), 158 | // InputEmptyState(), 159 | ], 160 | ); 161 | }); 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /test/metadata/repository/metadata_repository_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:async'; 5 | import 'dart:ui'; 6 | 7 | import 'package:document_measure/document_measure.dart'; 8 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 9 | import 'package:flutter/services.dart'; 10 | import 'package:flutter_test/flutter_test.dart'; 11 | import 'package:mockito/mockito.dart'; 12 | 13 | import '../../mocks/test_mocks.dart'; 14 | 15 | void main() { 16 | TestWidgetsFlutterBinding.ensureInitialized(); 17 | 18 | group('Metadata Repository Unit Test', () { 19 | final viewSize = Size(200, 300); 20 | final methodChannel = MethodChannel('documentmeasure'); 21 | final pixelPerInch = 10.0; 22 | 23 | final expectedMeasurement = true; 24 | final expectedShowDistance = true; 25 | final expectedController = MeasurementController(); 26 | final expectedMeasurementInformation = MeasurementInformation( 27 | documentWidthInLengthUnits: Inch(200), 28 | documentHeightInLengthUnits: Inch(200), 29 | scale: 4.0, 30 | targetLengthUnit: Inch.asUnit()); 31 | final expectedViewCenter = Offset(100, 150); 32 | final Image expectedImage = MockedImage.mock; 33 | final expectedMagnificationStyle = MagnificationStyle(); 34 | 35 | final expectedImageScaleFactor = 3.0; 36 | final expectedTransformationFactor = Inch(1 / 4); 37 | final expectedZoomFactor = 5 / 6; 38 | final expectedImageToDocumentFactor = expectedMeasurementInformation 39 | .documentWidthInLengthUnits.value 40 | .toDouble() / 41 | viewSize.width; 42 | 43 | MetadataRepository metadataRepository; 44 | 45 | setUpAll(() { 46 | methodChannel.setMockMethodCallHandler((call) async { 47 | if (call.method == 'getPhysicalPixelsPerInch') { 48 | return pixelPerInch; 49 | } else { 50 | return -1.0; 51 | } 52 | }); 53 | }); 54 | 55 | setUp(() { 56 | metadataRepository = MetadataRepository(); 57 | }); 58 | 59 | tearDown(() { 60 | metadataRepository.dispose(); 61 | }); 62 | 63 | tearDownAll(() { 64 | methodChannel.setMockMethodCallHandler(null); 65 | }); 66 | 67 | test('started', () { 68 | when((expectedImage as MockedImage).width).thenReturn(600); 69 | 70 | metadataRepository.registerStartupValuesChange( 71 | measurementInformation: expectedMeasurementInformation, 72 | measure: expectedMeasurement, 73 | showDistance: expectedShowDistance, 74 | magnificationStyle: expectedMagnificationStyle, 75 | controller: expectedController, 76 | ); 77 | 78 | metadataRepository.registerBackgroundChange(expectedImage, viewSize); 79 | 80 | metadataRepository.measurement 81 | .listen((actual) => expect(actual, expectedMeasurement)); 82 | metadataRepository.showDistances 83 | .listen((actual) => expect(actual, expectedShowDistance)); 84 | metadataRepository.controller 85 | .listen((actual) => expect(actual, expectedController)); 86 | metadataRepository.viewCenter 87 | .listen((actual) => expect(actual, expectedViewCenter)); 88 | metadataRepository.backgroundImage 89 | .listen((actual) => expect(actual, expectedImage)); 90 | 91 | metadataRepository.imageScaleFactor 92 | .listen((actual) => expect(actual, expectedImageScaleFactor)); 93 | metadataRepository.transformationFactor 94 | .listen((actual) => expect(actual, expectedTransformationFactor)); 95 | metadataRepository.imageToDocumentScaleFactor 96 | .listen((actual) => expect(actual, expectedImageToDocumentFactor)); 97 | }); 98 | 99 | test('started and updated view size', () { 100 | final updatedViewSize = Size(400, 100); 101 | 102 | when((expectedImage as MockedImage).width).thenReturn(600); 103 | 104 | metadataRepository.registerStartupValuesChange( 105 | measurementInformation: expectedMeasurementInformation, 106 | measure: expectedMeasurement, 107 | showDistance: expectedShowDistance, 108 | magnificationStyle: expectedMagnificationStyle, 109 | controller: expectedController, 110 | ); 111 | 112 | metadataRepository.registerBackgroundChange(expectedImage, viewSize); 113 | 114 | StreamSubscription subscription; 115 | subscription = 116 | metadataRepository.imageToDocumentScaleFactor.listen((actual) { 117 | expect(actual, expectedImageToDocumentFactor); 118 | subscription.cancel(); 119 | }); 120 | 121 | metadataRepository.registerBackgroundChange( 122 | expectedImage, updatedViewSize); 123 | metadataRepository.imageToDocumentScaleFactor.listen((actual) => expect( 124 | actual, 125 | expectedMeasurementInformation.documentHeightInLengthUnits.value 126 | .toDouble() / 127 | updatedViewSize.height)); 128 | }); 129 | 130 | group('original zoom factor', () { 131 | test('started without background and get zoom factor for original size', 132 | () async { 133 | metadataRepository.registerStartupValuesChange( 134 | measurementInformation: expectedMeasurementInformation, 135 | measure: expectedMeasurement, 136 | showDistance: expectedShowDistance, 137 | magnificationStyle: expectedMagnificationStyle, 138 | controller: expectedController, 139 | ); 140 | 141 | expect(await metadataRepository.zoomFactorForLifeSize, equals(1.0)); 142 | }); 143 | 144 | test('started and retrieve zoom factor for original size', () async { 145 | when((expectedImage as MockedImage).width).thenReturn(600); 146 | 147 | metadataRepository.registerStartupValuesChange( 148 | measurementInformation: expectedMeasurementInformation, 149 | measure: expectedMeasurement, 150 | showDistance: expectedShowDistance, 151 | magnificationStyle: expectedMagnificationStyle, 152 | controller: expectedController, 153 | ); 154 | 155 | metadataRepository.registerBackgroundChange(expectedImage, viewSize); 156 | metadataRepository.registerScreenSize(Size(200, 300)); 157 | 158 | expect(await metadataRepository.zoomFactorForLifeSize, 159 | equals(expectedZoomFactor)); 160 | }); 161 | }); 162 | }); 163 | } 164 | -------------------------------------------------------------------------------- /lib/src/measurement_information.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/widgets.dart'; 7 | 8 | /// The [LengthUnits] allow us to offer different units for measurement and changing between them. 9 | abstract class LengthUnit extends Equatable { 10 | final double value; 11 | 12 | const LengthUnit(this.value); 13 | 14 | @override 15 | String toString() => super.toString() + ' $value${getAbbreviation()}'; 16 | 17 | @override 18 | List get props => [value]; 19 | 20 | Millimeter convertToMillimeter() => Millimeter(value * millimeterFactor()); 21 | 22 | Meter convertToMeter() => Meter(value * meterFactor()); 23 | 24 | Inch convertToInch() => Inch(value * inchFactor()); 25 | 26 | Foot convertToFoot() => Foot(value * footFactor()); 27 | 28 | /// Returns the conversion factor of the current length unit to the target length [unit]. 29 | /// 30 | /// The individual lengths are ignored, only the types matter. 31 | /// ```dart 32 | /// Meter(10).factorTo(Millimeter(20)); // 1000 (millimeter per meter) 33 | /// ``` 34 | LengthUnit factorTo(LengthUnit unit) { 35 | switch (unit.runtimeType) { 36 | case Meter: 37 | return Meter(meterFactor()); 38 | case Millimeter: 39 | return Millimeter(millimeterFactor()); 40 | case Inch: 41 | return Inch(inchFactor()); 42 | case Foot: 43 | return Foot(footFactor()); 44 | default: 45 | return Meter(-1); 46 | } 47 | } 48 | 49 | /// Returns the current length expressed in the unit of the target length [unit]. 50 | /// 51 | /// The length of the target unit is ignored, only its type matters. 52 | /// ```dart 53 | /// Meter(10).convertTo(Millimeter(5)); // 10.000 (millimeter) 54 | /// ``` 55 | LengthUnit convertTo(LengthUnit unit) { 56 | switch (unit.runtimeType) { 57 | case Meter: 58 | return convertToMeter(); 59 | case Millimeter: 60 | return convertToMillimeter(); 61 | case Inch: 62 | return convertToInch(); 63 | case Foot: 64 | return convertToFoot(); 65 | default: 66 | return Meter(-1); 67 | } 68 | } 69 | 70 | double meterFactor(); 71 | 72 | double millimeterFactor(); 73 | 74 | double inchFactor(); 75 | 76 | double footFactor(); 77 | 78 | String getAbbreviation(); 79 | 80 | LengthUnit operator /(double value); 81 | 82 | LengthUnit operator *(double value); 83 | } 84 | 85 | class Meter extends LengthUnit { 86 | Meter.asUnit() : super(1); 87 | 88 | Meter(double meters) : super(meters); 89 | 90 | @override 91 | double footFactor() => 1 / 0.3048; 92 | 93 | @override 94 | double inchFactor() => 1 / 0.0254; 95 | 96 | @override 97 | double meterFactor() => 1; 98 | 99 | @override 100 | double millimeterFactor() => 1000; 101 | 102 | @override 103 | String getAbbreviation() => 'm'; 104 | 105 | @override 106 | Meter operator *(double value) => Meter(this.value * value); 107 | 108 | @override 109 | Meter operator /(double value) => Meter(this.value / value); 110 | } 111 | 112 | class Millimeter extends LengthUnit { 113 | const Millimeter.asUnit() : super(1); 114 | 115 | const Millimeter(double millimeters) : super(millimeters); 116 | 117 | @override 118 | double footFactor() => 1 / 304.8; 119 | 120 | @override 121 | double inchFactor() => 1 / 25.4; 122 | 123 | @override 124 | double meterFactor() => 1 / 1000; 125 | 126 | @override 127 | double millimeterFactor() => 1; 128 | 129 | @override 130 | String getAbbreviation() => 'mm'; 131 | 132 | @override 133 | Millimeter operator *(double value) => Millimeter(this.value * value); 134 | 135 | @override 136 | Millimeter operator /(double value) => Millimeter(this.value / value); 137 | } 138 | 139 | class Inch extends LengthUnit { 140 | Inch.asUnit() : super(1); 141 | 142 | Inch(double inches) : super(inches); 143 | 144 | @override 145 | double footFactor() => 1 / 12; 146 | 147 | @override 148 | double inchFactor() => 1; 149 | 150 | @override 151 | double meterFactor() => 0.0254; 152 | 153 | @override 154 | double millimeterFactor() => 25.4; 155 | 156 | @override 157 | String getAbbreviation() => 'in'; 158 | 159 | @override 160 | Inch operator *(double value) => Inch(this.value * value); 161 | 162 | @override 163 | Inch operator /(double value) => Inch(this.value / value); 164 | } 165 | 166 | class Foot extends LengthUnit { 167 | Foot.asUnit() : super(1); 168 | 169 | Foot(double feet) : super(feet); 170 | 171 | @override 172 | double footFactor() => 1; 173 | 174 | @override 175 | double inchFactor() => 12; 176 | 177 | @override 178 | double meterFactor() => 0.3048; 179 | 180 | @override 181 | double millimeterFactor() => 304.8; 182 | 183 | @override 184 | String getAbbreviation() => 'ft'; 185 | 186 | @override 187 | Foot operator *(double value) => Foot(this.value * value); 188 | 189 | @override 190 | Foot operator /(double value) => Foot(this.value / value); 191 | } 192 | 193 | /// This contains the information about the document that is being displayed. 194 | /// 195 | /// To change the result measurement unit change [targetLengthUnit]. 196 | class MeasurementInformation extends Equatable { 197 | /// The scale of the content in the displayed document. 198 | final double scale; 199 | 200 | /// The unit of measurement for the measurements of the user. 201 | final LengthUnit targetLengthUnit; 202 | 203 | final LengthUnit documentWidthInLengthUnits; 204 | final LengthUnit documentHeightInLengthUnits; 205 | 206 | const MeasurementInformation({ 207 | @required this.documentWidthInLengthUnits, 208 | @required this.documentHeightInLengthUnits, 209 | this.scale = 1.0, 210 | this.targetLengthUnit = const Millimeter.asUnit(), 211 | }); 212 | 213 | /// Default constructor for a DIN A4 document with a scale of 1 and Millimeters as the target unit. 214 | const MeasurementInformation.dinA4({ 215 | this.scale = 1.0, 216 | this.documentWidthInLengthUnits = const Millimeter(210.0), 217 | this.documentHeightInLengthUnits = const Millimeter(297.0), 218 | this.targetLengthUnit = const Millimeter.asUnit(), 219 | }); 220 | 221 | LengthUnit get documentToTargetFactor => 222 | documentWidthInLengthUnits.factorTo(targetLengthUnit); 223 | 224 | LengthUnit get documentWidthInUnitOfMeasurement => 225 | documentWidthInLengthUnits.convertTo(targetLengthUnit); 226 | 227 | @override 228 | List get props => 229 | [scale, documentWidthInLengthUnits, targetLengthUnit]; 230 | 231 | @override 232 | String toString() { 233 | return super.toString() + 234 | ' scale: $scale, documentWidth: $documentWidthInLengthUnits, documentHeight: $documentHeightInLengthUnits, targetLengthUnit: $targetLengthUnit'; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /test/measurement/bloc/points_bloc_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:math'; 5 | 6 | import 'package:bloc_test/bloc_test.dart'; 7 | import 'package:document_measure/document_measure.dart'; 8 | import 'package:document_measure/src/measurement/bloc/points_bloc/points_bloc.dart'; 9 | import 'package:document_measure/src/measurement/bloc/points_bloc/points_state.dart'; 10 | import 'package:document_measure/src/measurement/drawing_holder.dart'; 11 | import 'package:document_measure/src/measurement/overlay/holder.dart'; 12 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 13 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 14 | import 'package:flutter_test/flutter_test.dart'; 15 | import 'package:get_it/get_it.dart'; 16 | import 'package:mockito/mockito.dart'; 17 | 18 | import '../../mocks/test_mocks.dart'; 19 | 20 | void main() { 21 | group('Points Bloc Unit Test', () { 22 | MeasurementRepository measurementRepository; 23 | MetadataRepository metadataRepository; 24 | 25 | setUp(() { 26 | measurementRepository = MockedMeasurementRepository(); 27 | metadataRepository = MockedMetadataRepository(); 28 | 29 | when(metadataRepository.tolerance) 30 | .thenAnswer((_) => Stream.fromIterable([0.0])); 31 | when(metadataRepository.unitOfMeasurement) 32 | .thenAnswer((_) => Stream.fromIterable([Millimeter.asUnit()])); 33 | 34 | GetIt.I.registerSingleton(measurementRepository); 35 | GetIt.I.registerSingleton(metadataRepository); 36 | }); 37 | 38 | tearDown(() { 39 | GetIt.I.unregister(instance: measurementRepository); 40 | GetIt.I.unregister(instance: metadataRepository); 41 | }); 42 | 43 | blocTest( 44 | 'initial state', 45 | build: () async { 46 | when(metadataRepository.showDistances) 47 | .thenAnswer((_) => Stream.fromIterable([])); 48 | when(metadataRepository.viewCenter) 49 | .thenAnswer((_) => Stream.fromIterable([])); 50 | 51 | return PointsBloc(); 52 | }, 53 | skip: 0, 54 | expect: [ 55 | PointsEmptyState(), 56 | ], 57 | ); 58 | 59 | group('UI events', () { 60 | blocTest( 61 | 'no points', 62 | build: () async { 63 | when(metadataRepository.showDistances) 64 | .thenAnswer((_) => Stream.fromIterable([false])); 65 | when(metadataRepository.viewCenter) 66 | .thenAnswer((_) => Stream.fromIterable([])); 67 | 68 | when(measurementRepository.points) 69 | .thenAnswer((_) => Stream.fromIterable([ 70 | [Offset(10, 10)], 71 | [], 72 | ])); 73 | 74 | return PointsBloc(); 75 | }, 76 | wait: Duration(microseconds: 1), 77 | skip: 2, 78 | expect: [ 79 | PointsEmptyState(), 80 | ], 81 | ); 82 | 83 | blocTest( 84 | 'single point', 85 | build: () async { 86 | when(metadataRepository.showDistances) 87 | .thenAnswer((_) => Stream.fromIterable([false])); 88 | when(metadataRepository.viewCenter) 89 | .thenAnswer((_) => Stream.fromIterable([])); 90 | 91 | when(measurementRepository.points) 92 | .thenAnswer((_) => Stream.fromIterable([ 93 | [Offset(10, 10)] 94 | ])); 95 | 96 | return PointsBloc(); 97 | }, 98 | expect: [ 99 | PointsSingleState(Offset(10, 10)), 100 | ], 101 | ); 102 | 103 | blocTest( 104 | 'two points without distance', 105 | build: () async { 106 | when(metadataRepository.showDistances) 107 | .thenAnswer((_) => Stream.fromIterable([false])); 108 | when(metadataRepository.viewCenter) 109 | .thenAnswer((_) => Stream.fromIterable([])); 110 | 111 | when(measurementRepository.points) 112 | .thenAnswer((_) => Stream.fromIterable([ 113 | [Offset(10, 10), Offset(20, 20)] 114 | ])); 115 | 116 | return PointsBloc(); 117 | }, 118 | expect: [ 119 | PointsOnlyState([Offset(10, 10), Offset(20, 20)]) 120 | ], 121 | ); 122 | 123 | blocTest( 124 | 'two points with distance', 125 | build: () async { 126 | when(metadataRepository.showDistances) 127 | .thenAnswer((_) => Stream.fromIterable([true])); 128 | when(metadataRepository.viewCenter) 129 | .thenAnswer((_) => Stream.fromIterable([Offset(0, 0)])); 130 | 131 | when(measurementRepository.drawingHolder) 132 | .thenAnswer((_) => Stream.fromIterable([ 133 | DrawingHolder([Offset(10, 10), Offset(20, 20)], 134 | [Millimeter(sqrt(200))]) 135 | ])); 136 | 137 | return PointsBloc(); 138 | }, 139 | expect: [ 140 | PointsAndDistanceState([ 141 | Holder.withDistance( 142 | Offset(10, 10), Offset(20, 20), Millimeter(sqrt(200))) 143 | ], Offset(0, 0), 0.0) 144 | ], 145 | ); 146 | 147 | blocTest( 148 | 'active measurement with two points and distances', 149 | build: () async { 150 | when(metadataRepository.showDistances) 151 | .thenAnswer((_) => Stream.fromIterable([true])); 152 | when(metadataRepository.viewCenter) 153 | .thenAnswer((_) => Stream.fromIterable([Offset(0, 0)])); 154 | 155 | when(measurementRepository.drawingHolder) 156 | .thenAnswer((_) => Stream.fromIterable([ 157 | DrawingHolder([Offset(10, 10), Offset(20, 20)], [null]) 158 | ])); 159 | 160 | return PointsBloc(); 161 | }, 162 | expect: [ 163 | PointsAndDistanceActiveState( 164 | [Holder.withDistance(Offset(10, 10), Offset(20, 20), null)], 165 | Offset(0, 0), 166 | 0.0, 167 | [0, 0]), 168 | ], 169 | ); 170 | 171 | blocTest( 172 | 'active measurement on second last point with five points and distances', 173 | build: () async { 174 | when(metadataRepository.showDistances) 175 | .thenAnswer((_) => Stream.fromIterable([true])); 176 | when(metadataRepository.viewCenter) 177 | .thenAnswer((_) => Stream.fromIterable([Offset(0, 0)])); 178 | 179 | when(measurementRepository.drawingHolder) 180 | .thenAnswer((_) => Stream.fromIterable([ 181 | DrawingHolder([ 182 | Offset(10, 10), 183 | Offset(20, 20), 184 | Offset(20, 30), 185 | Offset(30, 30), 186 | Offset(10, 30) 187 | ], [ 188 | Millimeter(sqrt(200)), 189 | Millimeter(10), 190 | null, 191 | null 192 | ]) 193 | ])); 194 | 195 | return PointsBloc(); 196 | }, 197 | expect: [ 198 | PointsAndDistanceActiveState( 199 | [ 200 | Holder.withDistance( 201 | Offset(10, 10), Offset(20, 20), Millimeter(sqrt(200))), 202 | Holder.withDistance( 203 | Offset(20, 20), Offset(20, 30), Millimeter(10)), 204 | Holder.withDistance(Offset(20, 30), Offset(30, 30), null), 205 | Holder.withDistance(Offset(30, 30), Offset(10, 30), null) 206 | ], 207 | Offset(0, 0), 208 | 0.0, 209 | [2, 3], 210 | ), 211 | ], 212 | ); 213 | }); 214 | }); 215 | } 216 | -------------------------------------------------------------------------------- /lib/src/metadata/repository/metadata_repository.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:ui'; 5 | 6 | import 'package:document_measure/document_measure.dart'; 7 | import 'package:document_measure/src/util/logger.dart'; 8 | import 'package:flutter/services.dart'; 9 | import 'package:flutter/widgets.dart' as widget; 10 | import 'package:rxdart/subjects.dart'; 11 | 12 | class MetadataRepository { 13 | final _logger = Logger(LogDistricts.METADATA_REPOSITORY); 14 | 15 | final _enableMeasure = BehaviorSubject.seeded(false); 16 | final _showDistance = BehaviorSubject(); 17 | final _measurementInformation = BehaviorSubject(); 18 | final _unitOfMeasurement = BehaviorSubject(); 19 | final _magnificationRadius = BehaviorSubject(); 20 | final _controller = BehaviorSubject(); 21 | 22 | final _imageScaleFactor = BehaviorSubject(); 23 | final _imageToDocumentFactor = BehaviorSubject(); 24 | final _currentBackgroundImage = BehaviorSubject(); 25 | final _screenSize = BehaviorSubject(); 26 | final _viewSize = BehaviorSubject(); 27 | final _viewCenter = BehaviorSubject(); 28 | 29 | final _transformationFactor = BehaviorSubject(); 30 | final _tolerance = BehaviorSubject(); 31 | 32 | final _zoomLevel = BehaviorSubject.seeded(1.0); 33 | final _contentPosition = BehaviorSubject(); 34 | 35 | Rect _deleteRegion; 36 | 37 | MetadataRepository(); 38 | 39 | Stream get measurement => _enableMeasure.stream; 40 | 41 | Stream get showDistances => _showDistance.stream; 42 | 43 | Stream get transformationFactor => _transformationFactor.stream; 44 | 45 | Stream get controller => _controller.stream; 46 | 47 | Stream get unitOfMeasurement => _unitOfMeasurement.stream; 48 | 49 | Stream get zoom => _zoomLevel.stream; 50 | 51 | Stream get backgroundPosition => _contentPosition.stream; 52 | 53 | Stream get imageScaleFactor => _imageScaleFactor.stream; 54 | 55 | Stream get imageToDocumentScaleFactor => 56 | _imageToDocumentFactor.stream; 57 | 58 | Stream get backgroundImage => _currentBackgroundImage.stream; 59 | 60 | Stream get viewCenter => _viewCenter.stream; 61 | 62 | Stream get tolerance => _tolerance.stream; 63 | 64 | Stream get screenSize => _screenSize.stream; 65 | 66 | Stream get viewSize => _viewSize.stream; 67 | 68 | Stream get magnificationCircleRadius => _magnificationRadius.stream; 69 | 70 | Future get zoomFactorForLifeSize async { 71 | var pixelPerInch = await MethodChannel('documentmeasure') 72 | .invokeMethod('getPhysicalPixelsPerInch'); 73 | var screenSize = _screenSize.value; 74 | 75 | if (screenSize == null) return 1; 76 | 77 | var information = _measurementInformation.value; 78 | 79 | if (isDocumentWidthAlignedWithScreenWidth(screenSize)) { 80 | return information.documentWidthInLengthUnits.convertToInch().value * 81 | pixelPerInch / 82 | (screenSize.width * information.scale * window.devicePixelRatio); 83 | } else { 84 | return information.documentHeightInLengthUnits.convertToInch().value * 85 | pixelPerInch / 86 | (screenSize.height * information.scale * window.devicePixelRatio); 87 | } 88 | } 89 | 90 | double get zoomFactorToFillScreen { 91 | if (_screenSize.value == null) return 1.0; 92 | 93 | if (isDocumentWidthAlignedWithScreenWidth(_screenSize.value)) { 94 | return _screenSize.value.height / _screenSize.value.width; 95 | } else { 96 | return _screenSize.value.width / _screenSize.value.height; 97 | } 98 | } 99 | 100 | void registerStartupValuesChange({ 101 | @widget.required MeasurementInformation measurementInformation, 102 | @widget.required bool measure, 103 | @widget.required bool showDistance, 104 | @widget.required MagnificationStyle magnificationStyle, 105 | @widget.required MeasurementController controller, 106 | }) { 107 | _measurementInformation.value = measurementInformation; 108 | _unitOfMeasurement.value = measurementInformation.targetLengthUnit; 109 | _enableMeasure.value = measure; 110 | _showDistance.value = showDistance; 111 | _magnificationRadius.value = magnificationStyle.magnificationRadius + 112 | magnificationStyle.outerCircleThickness; 113 | _controller.value = controller; 114 | 115 | _updateTransformationFactor(); 116 | } 117 | 118 | void registerBackgroundChange(Image backgroundImage, Size size) { 119 | _currentBackgroundImage.value = backgroundImage; 120 | _viewSize.value = size; 121 | _viewCenter.value = Offset(size.width / 2, size.height / 2); 122 | _imageScaleFactor.value = backgroundImage.width / size.width; 123 | 124 | _logger.log( 125 | 'view size: ${_viewSize.value} view center: ${_viewCenter.value} image scale: ${_imageScaleFactor.value} image size $size'); 126 | 127 | _updateImageToDocumentFactor(size); 128 | _updateTransformationFactor(); 129 | } 130 | 131 | void registerResizing(Offset position, double zoom) { 132 | _logger.log('Offset: $position, zoom: $zoom'); 133 | _contentPosition.value = position; 134 | _zoomLevel.value = zoom; 135 | _updateTransformationFactor(); 136 | } 137 | 138 | void registerDeleteRegion(Offset position, Size size) => _deleteRegion = 139 | Rect.fromPoints(position, position + Offset(size.width, size.height)); 140 | 141 | void registerScreenSize(Size size) { 142 | _screenSize.value = size; 143 | _logger.log('_screenSize: ${_screenSize.value}'); 144 | } 145 | 146 | void registerMeasurementFunction(MeasurementFunction function) { 147 | _controller.value?.measurementFunction = function; 148 | } 149 | 150 | bool isInDeleteRegion(Offset position) => _deleteRegion.contains(position); 151 | 152 | bool isDocumentWidthAlignedWithScreenWidth(Size screenSize) { 153 | final documentAspectRatio = _getDocumentWidth() / _getDocumentHeight(); 154 | final backgroundAspectRatio = screenSize.width / screenSize.height; 155 | 156 | return documentAspectRatio > backgroundAspectRatio; 157 | } 158 | 159 | void dispose() { 160 | _enableMeasure.close(); 161 | _showDistance.close(); 162 | _measurementInformation.close(); 163 | _unitOfMeasurement.close(); 164 | _magnificationRadius.close(); 165 | _controller.close(); 166 | 167 | _currentBackgroundImage.close(); 168 | _imageScaleFactor.close(); 169 | _imageToDocumentFactor.close(); 170 | _screenSize.close(); 171 | _viewSize.close(); 172 | _viewCenter.close(); 173 | 174 | _transformationFactor.close(); 175 | _tolerance.close(); 176 | 177 | _contentPosition.close(); 178 | _zoomLevel.close(); 179 | } 180 | 181 | double _getDocumentWidth() => 182 | _measurementInformation.value.documentWidthInLengthUnits.value.toDouble(); 183 | 184 | double _getDocumentHeight() => 185 | _measurementInformation.value.documentHeightInLengthUnits.value 186 | .toDouble(); 187 | 188 | void _updateImageToDocumentFactor(Size viewSize) { 189 | if (_screenSize.value == null) return; 190 | 191 | if (isDocumentWidthAlignedWithScreenWidth(viewSize)) { 192 | _imageToDocumentFactor.value = _getDocumentWidth() / viewSize.width; 193 | } else { 194 | _imageToDocumentFactor.value = _getDocumentHeight() / viewSize.height; 195 | } 196 | } 197 | 198 | void _updateTransformationFactor() async { 199 | if (_zoomLevel.hasValue && 200 | _viewSize.hasValue && 201 | _measurementInformation.hasValue) { 202 | var zoomLevel = _zoomLevel.value; 203 | var viewWidth = _viewSize.value.width; 204 | var measurementInfo = _measurementInformation.value; 205 | 206 | _transformationFactor.value = 207 | measurementInfo.documentToTargetFactor / measurementInfo.scale; 208 | _tolerance.value = 209 | measurementInfo.documentWidthInUnitOfMeasurement.value / 210 | (measurementInfo.scale * viewWidth) / 211 | zoomLevel; 212 | 213 | _controller.value?.tolerance = _tolerance.value; 214 | 215 | _logger.log('tolerance is: ${_transformationFactor.value}'); 216 | _logger.log('updated transformationFactor'); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.0.13" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.6.0" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.4.1" 25 | bloc: 26 | dependency: transitive 27 | description: 28 | name: bloc 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "5.0.1" 32 | boolean_selector: 33 | dependency: transitive 34 | description: 35 | name: boolean_selector 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "2.0.0" 39 | charcode: 40 | dependency: transitive 41 | description: 42 | name: charcode 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.1.3" 46 | collection: 47 | dependency: transitive 48 | description: 49 | name: collection 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.14.12" 53 | convert: 54 | dependency: transitive 55 | description: 56 | name: convert 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "2.1.1" 60 | crypto: 61 | dependency: transitive 62 | description: 63 | name: crypto 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "2.1.4" 67 | cubit: 68 | dependency: transitive 69 | description: 70 | name: cubit 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "0.1.2" 74 | document_measure: 75 | dependency: "direct main" 76 | description: 77 | path: ".." 78 | relative: true 79 | source: path 80 | version: "0.0.1+1" 81 | equatable: 82 | dependency: transitive 83 | description: 84 | name: equatable 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "1.2.3" 88 | file: 89 | dependency: transitive 90 | description: 91 | name: file 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "5.2.1" 95 | flutter: 96 | dependency: "direct main" 97 | description: flutter 98 | source: sdk 99 | version: "0.0.0" 100 | flutter_bloc: 101 | dependency: transitive 102 | description: 103 | name: flutter_bloc 104 | url: "https://pub.dartlang.org" 105 | source: hosted 106 | version: "5.0.1" 107 | flutter_cubit: 108 | dependency: transitive 109 | description: 110 | name: flutter_cubit 111 | url: "https://pub.dartlang.org" 112 | source: hosted 113 | version: "0.1.1" 114 | flutter_test: 115 | dependency: "direct dev" 116 | description: flutter 117 | source: sdk 118 | version: "0.0.0" 119 | get_it: 120 | dependency: transitive 121 | description: 122 | name: get_it 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "4.0.4" 126 | image: 127 | dependency: transitive 128 | description: 129 | name: image 130 | url: "https://pub.dartlang.org" 131 | source: hosted 132 | version: "2.1.12" 133 | intl: 134 | dependency: transitive 135 | description: 136 | name: intl 137 | url: "https://pub.dartlang.org" 138 | source: hosted 139 | version: "0.16.1" 140 | matcher: 141 | dependency: transitive 142 | description: 143 | name: matcher 144 | url: "https://pub.dartlang.org" 145 | source: hosted 146 | version: "0.12.6" 147 | meta: 148 | dependency: transitive 149 | description: 150 | name: meta 151 | url: "https://pub.dartlang.org" 152 | source: hosted 153 | version: "1.1.8" 154 | nested: 155 | dependency: transitive 156 | description: 157 | name: nested 158 | url: "https://pub.dartlang.org" 159 | source: hosted 160 | version: "0.0.4" 161 | path: 162 | dependency: transitive 163 | description: 164 | name: path 165 | url: "https://pub.dartlang.org" 166 | source: hosted 167 | version: "1.6.4" 168 | path_provider: 169 | dependency: transitive 170 | description: 171 | name: path_provider 172 | url: "https://pub.dartlang.org" 173 | source: hosted 174 | version: "1.6.11" 175 | path_provider_linux: 176 | dependency: transitive 177 | description: 178 | name: path_provider_linux 179 | url: "https://pub.dartlang.org" 180 | source: hosted 181 | version: "0.0.1+2" 182 | path_provider_macos: 183 | dependency: transitive 184 | description: 185 | name: path_provider_macos 186 | url: "https://pub.dartlang.org" 187 | source: hosted 188 | version: "0.0.4+3" 189 | path_provider_platform_interface: 190 | dependency: transitive 191 | description: 192 | name: path_provider_platform_interface 193 | url: "https://pub.dartlang.org" 194 | source: hosted 195 | version: "1.0.2" 196 | petitparser: 197 | dependency: transitive 198 | description: 199 | name: petitparser 200 | url: "https://pub.dartlang.org" 201 | source: hosted 202 | version: "2.4.0" 203 | platform: 204 | dependency: transitive 205 | description: 206 | name: platform 207 | url: "https://pub.dartlang.org" 208 | source: hosted 209 | version: "2.2.1" 210 | plugin_platform_interface: 211 | dependency: transitive 212 | description: 213 | name: plugin_platform_interface 214 | url: "https://pub.dartlang.org" 215 | source: hosted 216 | version: "1.0.2" 217 | process: 218 | dependency: transitive 219 | description: 220 | name: process 221 | url: "https://pub.dartlang.org" 222 | source: hosted 223 | version: "3.0.13" 224 | provider: 225 | dependency: transitive 226 | description: 227 | name: provider 228 | url: "https://pub.dartlang.org" 229 | source: hosted 230 | version: "4.3.1" 231 | quiver: 232 | dependency: transitive 233 | description: 234 | name: quiver 235 | url: "https://pub.dartlang.org" 236 | source: hosted 237 | version: "2.1.3" 238 | rxdart: 239 | dependency: transitive 240 | description: 241 | name: rxdart 242 | url: "https://pub.dartlang.org" 243 | source: hosted 244 | version: "0.24.1" 245 | sky_engine: 246 | dependency: transitive 247 | description: flutter 248 | source: sdk 249 | version: "0.0.99" 250 | source_span: 251 | dependency: transitive 252 | description: 253 | name: source_span 254 | url: "https://pub.dartlang.org" 255 | source: hosted 256 | version: "1.7.0" 257 | stack_trace: 258 | dependency: transitive 259 | description: 260 | name: stack_trace 261 | url: "https://pub.dartlang.org" 262 | source: hosted 263 | version: "1.9.3" 264 | stream_channel: 265 | dependency: transitive 266 | description: 267 | name: stream_channel 268 | url: "https://pub.dartlang.org" 269 | source: hosted 270 | version: "2.0.0" 271 | string_scanner: 272 | dependency: transitive 273 | description: 274 | name: string_scanner 275 | url: "https://pub.dartlang.org" 276 | source: hosted 277 | version: "1.0.5" 278 | term_glyph: 279 | dependency: transitive 280 | description: 281 | name: term_glyph 282 | url: "https://pub.dartlang.org" 283 | source: hosted 284 | version: "1.1.0" 285 | test_api: 286 | dependency: transitive 287 | description: 288 | name: test_api 289 | url: "https://pub.dartlang.org" 290 | source: hosted 291 | version: "0.2.15" 292 | typed_data: 293 | dependency: transitive 294 | description: 295 | name: typed_data 296 | url: "https://pub.dartlang.org" 297 | source: hosted 298 | version: "1.1.6" 299 | vector_math: 300 | dependency: transitive 301 | description: 302 | name: vector_math 303 | url: "https://pub.dartlang.org" 304 | source: hosted 305 | version: "2.0.8" 306 | xdg_directories: 307 | dependency: transitive 308 | description: 309 | name: xdg_directories 310 | url: "https://pub.dartlang.org" 311 | source: hosted 312 | version: "0.1.0" 313 | xml: 314 | dependency: transitive 315 | description: 316 | name: xml 317 | url: "https://pub.dartlang.org" 318 | source: hosted 319 | version: "3.6.1" 320 | sdks: 321 | dart: ">=2.7.0 <3.0.0" 322 | flutter: ">=1.16.0 <2.0.0" 323 | -------------------------------------------------------------------------------- /lib/src/measurement/repository/measurement_repository.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'dart:math'; 5 | import 'dart:ui'; 6 | 7 | import 'package:document_measure/document_measure.dart'; 8 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 9 | import 'package:document_measure/src/util/logger.dart'; 10 | import 'package:document_measure/src/util/utils.dart'; 11 | import 'package:flutter/cupertino.dart'; 12 | import 'package:rxdart/rxdart.dart'; 13 | 14 | import '../drawing_holder.dart'; 15 | 16 | enum TouchState { 17 | FREE, 18 | DOWN, 19 | MOVE, 20 | UP, 21 | } 22 | 23 | class MeasurementRepository { 24 | final _logger = Logger(LogDistricts.MEASUREMENT_REPOSITORY); 25 | 26 | final _points = BehaviorSubject>.seeded([]); 27 | final _distances = BehaviorSubject>.seeded([]); 28 | final _drawingHolder = BehaviorSubject(); 29 | final MetadataRepository _metadataRepository; 30 | 31 | MeasurementController _controller; 32 | LengthUnit _transformationFactor; 33 | double _imageToDocumentScaleFactor = 1.0; 34 | 35 | int _currentIndex = -1; 36 | TouchState _currentState = TouchState.FREE; 37 | 38 | final List _absolutePoints = []; 39 | double _zoomLevel = 1.0; 40 | Offset _backgroundPosition = Offset(0, 0); 41 | Offset _viewCenterPosition = Offset(0, 0); 42 | 43 | MeasurementRepository(this._metadataRepository) { 44 | _metadataRepository.controller 45 | .listen((controller) => _controller = controller); 46 | _metadataRepository.viewCenter 47 | .listen((viewCenter) => _viewCenterPosition = viewCenter); 48 | _metadataRepository.imageToDocumentScaleFactor.listen((scaleFactor) { 49 | _imageToDocumentScaleFactor = scaleFactor; 50 | _publishPoints(); 51 | }); 52 | _metadataRepository.transformationFactor.listen((factor) { 53 | if (_transformationFactor != factor) { 54 | _transformationFactor = factor; 55 | _synchronizeDistances(); 56 | } 57 | }); 58 | _metadataRepository.zoom.listen((zoom) { 59 | _zoomLevel = zoom; 60 | _publishPoints(); 61 | }); 62 | _metadataRepository.backgroundPosition.listen((backgroundPosition) { 63 | _backgroundPosition = 64 | Offset(backgroundPosition.dx, -backgroundPosition.dy); 65 | _publishPoints(); 66 | }); 67 | } 68 | 69 | Stream> get points => _points.stream; 70 | 71 | Stream get drawingHolder => _drawingHolder.stream; 72 | 73 | void registerDownEvent(Offset globalPosition) { 74 | if (_currentState != TouchState.FREE) return; 75 | _currentState = TouchState.DOWN; 76 | 77 | var documentLocalCenteredPosition = 78 | _convertIntoDocumentLocalCenteredPosition( 79 | globalPosition, _viewCenterPosition); 80 | 81 | var closestIndex = _getClosestPointIndex(documentLocalCenteredPosition); 82 | 83 | if (closestIndex >= 0) { 84 | var closestPoint = _absolutePoints[closestIndex]; 85 | 86 | if ((_convertIntoGlobalPosition(closestPoint, _viewCenterPosition) - 87 | globalPosition) 88 | .distance > 89 | 40.0) { 90 | _currentIndex = _addNewPoint(documentLocalCenteredPosition); 91 | } else { 92 | _currentIndex = closestIndex; 93 | _updatePoint(documentLocalCenteredPosition); 94 | } 95 | } else { 96 | _currentIndex = _addNewPoint(documentLocalCenteredPosition); 97 | } 98 | 99 | _movementStarted(_currentIndex); 100 | } 101 | 102 | void registerMoveEvent(Offset position) { 103 | if (_currentState != TouchState.DOWN && _currentState != TouchState.MOVE) 104 | return; 105 | _currentState = TouchState.MOVE; 106 | 107 | _updatePoint(_convertIntoDocumentLocalCenteredPosition( 108 | position, _viewCenterPosition)); 109 | } 110 | 111 | void registerUpEvent(Offset position) { 112 | if (_currentState != TouchState.DOWN && _currentState != TouchState.MOVE) 113 | return; 114 | _currentState = TouchState.UP; 115 | 116 | _updatePoint(_convertIntoDocumentLocalCenteredPosition( 117 | position, _viewCenterPosition)); 118 | _movementFinished(); 119 | } 120 | 121 | void removeCurrentPoint() { 122 | if (_currentIndex >= 0) { 123 | _absolutePoints.removeAt(_currentIndex); 124 | _publishPoints(); 125 | 126 | _movementFinished(); 127 | } 128 | } 129 | 130 | void dispose() { 131 | _points.close(); 132 | _distances.close(); 133 | _drawingHolder.close(); 134 | } 135 | 136 | Offset convertIntoDocumentLocalTopLeftPosition(Offset position) { 137 | var documentLocalCenterPosition = _convertIntoDocumentLocalCenteredPosition( 138 | position, _viewCenterPosition) / 139 | _imageToDocumentScaleFactor; 140 | 141 | return Offset(documentLocalCenterPosition.dx + _viewCenterPosition.dx, 142 | _viewCenterPosition.dy - documentLocalCenterPosition.dy); 143 | } 144 | 145 | Offset _convertIntoDocumentLocalCenteredPosition( 146 | Offset position, Offset viewCenter) { 147 | return (Offset(position.dx - viewCenter.dx, viewCenter.dy - position.dy) - 148 | _backgroundPosition) / 149 | _zoomLevel * 150 | _imageToDocumentScaleFactor; 151 | } 152 | 153 | Offset _convertIntoGlobalPosition(Offset position, Offset viewCenter) { 154 | var scaledPosition = position / _imageToDocumentScaleFactor * _zoomLevel; 155 | 156 | return Offset(scaledPosition.dx + viewCenter.dx + _backgroundPosition.dx, 157 | viewCenter.dy - scaledPosition.dy - _backgroundPosition.dy); 158 | } 159 | 160 | List _getRelativePoints() { 161 | return _absolutePoints 162 | .map((point) => _convertIntoGlobalPosition(point, _viewCenterPosition)) 163 | .toList(); 164 | } 165 | 166 | void _publishPoints() { 167 | var relativePoints = _getRelativePoints(); 168 | 169 | _logger.log( 170 | '\nimageToDocumentScaleFactor: ${_imageToDocumentScaleFactor.toStringAsFixed(2)}, ' 171 | 'zoomLevel: ${_zoomLevel.toStringAsFixed(2)}, ' 172 | 'backgroundPosition: $_backgroundPosition, ' 173 | 'viewCenter: $_viewCenterPosition\n' 174 | 'absolute points: $_absolutePoints\n' 175 | 'relative points: $relativePoints'); 176 | 177 | _points.add(relativePoints); 178 | _drawingHolder.add(DrawingHolder(relativePoints, _distances.value)); 179 | } 180 | 181 | int _addNewPoint(Offset point) { 182 | _absolutePoints.add(point); 183 | _publishPoints(); 184 | 185 | _logger.log('added point: $_absolutePoints'); 186 | return _absolutePoints.length - 1; 187 | } 188 | 189 | void _updatePoint(Offset point) { 190 | if (_currentIndex >= 0) { 191 | _absolutePoints.setRange(_currentIndex, _currentIndex + 1, [point]); 192 | _publishPoints(); 193 | 194 | _logger.log('updated point $_currentIndex: $_absolutePoints'); 195 | } 196 | } 197 | 198 | int _getClosestPointIndex(Offset reference) { 199 | var index = 0; 200 | 201 | var sortedPoints = _absolutePoints 202 | .map((Offset point) => 203 | _CompareHolder(index++, (reference - point).distance)) 204 | .toList(); 205 | 206 | sortedPoints.sort((_CompareHolder a, _CompareHolder b) => 207 | a.distance.compareTo(b.distance)); 208 | 209 | return sortedPoints.isNotEmpty ? sortedPoints[0].index : -1; 210 | } 211 | 212 | void _publishDistances(List distances) { 213 | _distances.add(distances); 214 | _drawingHolder.add(DrawingHolder(_getRelativePoints(), distances)); 215 | } 216 | 217 | void _movementStarted(int index) { 218 | var distances = [..._distances.value]; 219 | 220 | distances.setRange( 221 | max(0, index - 1), min(distances.length, index + 1), [null, null]); 222 | _publishDistances(distances); 223 | 224 | _logger.log('started moving point with index: $index'); 225 | } 226 | 227 | void _movementFinished() { 228 | _currentIndex = -1; 229 | _synchronizeDistances(); 230 | _currentState = TouchState.FREE; 231 | } 232 | 233 | void _synchronizeDistances() { 234 | if (_transformationFactor != null && _absolutePoints.length >= 2) { 235 | var distances = []; 236 | _absolutePoints.doInBetween((start, end) => 237 | distances.add(_transformationFactor * (start - end).distance)); 238 | _publishDistances(distances); 239 | 240 | _controller?.distances = distances.map((unit) => unit.value).toList(); 241 | } else if (_absolutePoints.length == 1) { 242 | _publishDistances([]); 243 | _controller?.distances = []; 244 | } 245 | } 246 | } 247 | 248 | class _CompareHolder { 249 | double distance; 250 | int index; 251 | 252 | _CompareHolder(this.index, this.distance); 253 | } 254 | -------------------------------------------------------------------------------- /test/measurements_widget_test.dart: -------------------------------------------------------------------------------- 1 | /// Copyright (c) 2020 arconsis IT-Solutions GmbH 2 | /// Licensed under MIT (https://github.com/arconsis/measurements/blob/master/LICENSE) 3 | 4 | import 'package:document_measure/document_measure.dart'; 5 | import 'package:document_measure/src/measurement/drawing_holder.dart'; 6 | import 'package:document_measure/src/measurement/overlay/measure_area.dart'; 7 | import 'package:document_measure/src/measurement/repository/measurement_repository.dart'; 8 | import 'package:document_measure/src/metadata/repository/metadata_repository.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter/services.dart'; 11 | import 'package:flutter_test/flutter_test.dart'; 12 | import 'package:get_it/get_it.dart'; 13 | 14 | Type typeOf() => T; 15 | 16 | final imageWidth = 800.0; 17 | final imageHeight = 600.0; 18 | final imageWidget = Image.asset( 19 | 'assets/images/example_portrait.png', 20 | package: 'document_measure', 21 | width: imageWidth, 22 | height: imageHeight, 23 | ); 24 | 25 | Widget fillTemplate(Widget measurement) { 26 | return MaterialApp( 27 | home: Scaffold( 28 | body: measurement, 29 | ), 30 | ); 31 | } 32 | 33 | void main() { 34 | TestWidgetsFlutterBinding.ensureInitialized(); 35 | 36 | final controller = MeasurementController(); 37 | final measurementInformation = MeasurementInformation( 38 | documentWidthInLengthUnits: Millimeter(imageWidth * 2), 39 | documentHeightInLengthUnits: Millimeter(imageHeight * 2)); 40 | 41 | group('Measurement Widget Integration Test', () { 42 | MetadataRepository metadataRepository; 43 | MeasurementRepository measurementRepository; 44 | 45 | setUp(() { 46 | metadataRepository = MetadataRepository(); 47 | measurementRepository = MeasurementRepository(metadataRepository); 48 | 49 | GetIt.I.registerSingleton(metadataRepository); 50 | GetIt.I.registerSingleton(measurementRepository); 51 | }); 52 | 53 | tearDown(() { 54 | GetIt.I.unregister(instance: metadataRepository); 55 | GetIt.I.unregister(instance: measurementRepository); 56 | }); 57 | 58 | group('widget setup', () { 59 | testWidgets('measurement should show child also when measure is false', 60 | (WidgetTester tester) async { 61 | await tester.pumpWidget(fillTemplate(Measurements( 62 | child: imageWidget, 63 | ))); 64 | 65 | expect(find.byType(typeOf()), findsOneWidget); 66 | expect(find.byType(typeOf()), findsOneWidget); 67 | }); 68 | 69 | testWidgets( 70 | 'measurement should show child under measure area when measuring', 71 | (WidgetTester tester) async { 72 | await tester.pumpWidget(fillTemplate(Measurements( 73 | child: imageWidget, 74 | measure: true, 75 | ))); 76 | 77 | await tester.pump(); 78 | 79 | expect(find.byType(typeOf()), findsOneWidget); 80 | expect(find.byType(typeOf()), findsOneWidget); 81 | }); 82 | }); 83 | 84 | group('setting points', () { 85 | testWidgets('adding single point', (WidgetTester tester) async { 86 | await tester.pumpWidget(fillTemplate(Measurements( 87 | child: imageWidget, 88 | measure: true, 89 | ))); 90 | 91 | await tester.pumpAndSettle(); 92 | 93 | final gesture = await tester.startGesture(Offset(100, 100)); 94 | await gesture.up(); 95 | 96 | await tester.pumpAndSettle(); 97 | 98 | measurementRepository.points 99 | .listen((actual) => expect(actual, [Offset(100, 100)])); 100 | }); 101 | 102 | testWidgets('adding multiple points and getting distances', 103 | (WidgetTester tester) async { 104 | await tester.pumpWidget(fillTemplate(Measurements( 105 | child: imageWidget, 106 | measure: true, 107 | showDistanceOnLine: true, 108 | controller: controller, 109 | measurementInformation: measurementInformation, 110 | ))); 111 | 112 | await tester.pumpAndSettle(); 113 | 114 | final gesture = await tester.startGesture(Offset(100, 100)); 115 | await gesture.up(); 116 | 117 | await gesture.down(Offset(100, 300)); 118 | await gesture.up(); 119 | 120 | await gesture.down(Offset(300, 300)); 121 | await gesture.up(); 122 | 123 | await gesture.down(Offset(300, 100)); 124 | await gesture.up(); 125 | 126 | await tester.pumpAndSettle(); 127 | 128 | final expectedDrawingHolder = DrawingHolder([ 129 | Offset(100, 100), 130 | Offset(100, 300), 131 | Offset(300, 300), 132 | Offset(300, 100) 133 | ], [ 134 | Millimeter(400), 135 | Millimeter(400), 136 | Millimeter(400) 137 | ]); 138 | 139 | measurementRepository.drawingHolder 140 | .listen((actual) => expect(actual, expectedDrawingHolder)); 141 | expect(controller.distances, equals([400, 400, 400])); 142 | expect(controller.tolerance, equals(2)); 143 | }); 144 | 145 | testWidgets('add points without distances and then turn on distances', 146 | (WidgetTester tester) async { 147 | await tester.pumpWidget(fillTemplate(Measurements( 148 | child: imageWidget, 149 | measure: true, 150 | showDistanceOnLine: false, 151 | controller: controller, 152 | measurementInformation: measurementInformation, 153 | ))); 154 | 155 | await tester.pumpAndSettle(); 156 | 157 | final gesture = await tester.startGesture(Offset(100, 100)); 158 | await gesture.up(); 159 | 160 | await gesture.down(Offset(100, 300)); 161 | await gesture.up(); 162 | 163 | await gesture.down(Offset(300, 300)); 164 | await gesture.up(); 165 | 166 | await gesture.down(Offset(300, 100)); 167 | await gesture.up(); 168 | 169 | await tester.pumpAndSettle(); 170 | 171 | measurementRepository.points.listen((actual) => expectSync(actual, [ 172 | Offset(100, 100), 173 | Offset(100, 300), 174 | Offset(300, 300), 175 | Offset(300, 100) 176 | ])); 177 | expect(controller.distances, equals([400, 400, 400])); 178 | expect(controller.tolerance, equals(2)); 179 | 180 | await tester.pumpWidget(fillTemplate(Measurements( 181 | child: imageWidget, 182 | measure: true, 183 | showDistanceOnLine: true, 184 | measurementInformation: measurementInformation, 185 | ))); 186 | 187 | await tester.pumpAndSettle(); 188 | 189 | final expectedDrawingHolder = DrawingHolder([ 190 | Offset(100, 100), 191 | Offset(100, 300), 192 | Offset(300, 300), 193 | Offset(300, 100) 194 | ], [ 195 | Millimeter(400), 196 | Millimeter(400), 197 | Millimeter(400) 198 | ]); 199 | 200 | measurementRepository.drawingHolder 201 | .listen((actual) => expect(actual, expectedDrawingHolder)); 202 | expect(controller.distances, equals([400, 400, 400])); 203 | expect(controller.tolerance, equals(2)); 204 | }); 205 | 206 | testWidgets('adding multiple points and getting distances with set scale', 207 | (WidgetTester tester) async { 208 | await tester.pumpWidget(fillTemplate(Measurements( 209 | child: imageWidget, 210 | measure: true, 211 | showDistanceOnLine: true, 212 | controller: controller, 213 | measurementInformation: MeasurementInformation( 214 | documentWidthInLengthUnits: Millimeter(imageWidth), 215 | documentHeightInLengthUnits: Millimeter(imageHeight), 216 | scale: 2.0), 217 | ))); 218 | 219 | await tester.pumpAndSettle(); 220 | 221 | final gesture = await tester.startGesture(Offset(100, 100)); 222 | await gesture.up(); 223 | 224 | await gesture.down(Offset(100, 300)); 225 | await gesture.up(); 226 | 227 | await gesture.down(Offset(300, 300)); 228 | await gesture.up(); 229 | 230 | await gesture.down(Offset(300, 100)); 231 | await gesture.up(); 232 | 233 | await tester.pumpAndSettle(); 234 | 235 | final expectedDrawingHolder = DrawingHolder( 236 | [ 237 | Offset(100, 100), 238 | Offset(100, 300), 239 | Offset(300, 300), 240 | Offset(300, 100) 241 | ], 242 | [Millimeter(100), Millimeter(100), Millimeter(100)], 243 | ); 244 | 245 | measurementRepository.drawingHolder 246 | .listen((actual) => expect(actual, expectedDrawingHolder)); 247 | expect(controller.distances, equals([100, 100, 100])); 248 | expect(controller.tolerance, equals(0.5)); 249 | }); 250 | }); 251 | 252 | group('controller interaction', () { 253 | final channel = MethodChannel('measurements'); 254 | setUp(() { 255 | channel.setMockMethodCallHandler((call) async { 256 | if (call.method == 'getPhysicalPixelsPerInch') { 257 | return 4.0; 258 | } else { 259 | return -1.0; 260 | } 261 | }); 262 | }); 263 | 264 | testWidgets('set zoom to original size and reset zoom level', 265 | (WidgetTester tester) async { 266 | await tester.pumpWidget(fillTemplate(Measurements( 267 | child: imageWidget, 268 | measure: true, 269 | showDistanceOnLine: true, 270 | controller: controller, 271 | measurementInformation: MeasurementInformation( 272 | documentWidthInLengthUnits: Inch(imageWidth), 273 | documentHeightInLengthUnits: Inch(imageHeight), 274 | scale: 2.0), 275 | ))); 276 | 277 | await tester.pump(); 278 | 279 | controller.zoomToLifeSize(); 280 | await tester.pump(); 281 | 282 | controller.resetZoom(); 283 | await tester.pump(); 284 | }); 285 | }); 286 | }); 287 | } 288 | --------------------------------------------------------------------------------