Repository: CariusLars/ar_flutter_plugin Branch: main Commit: 16fa29a8d30a Files: 97 Total size: 349.7 KB Directory structure: gitextract_cy7w_x8f/ ├── .gitignore ├── .metadata ├── CHANGELOG.md ├── LICENSE ├── README.md ├── android/ │ ├── .gitignore │ ├── build.gradle │ ├── gradle/ │ │ └── wrapper/ │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── settings.gradle │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── kotlin/ │ └── io/ │ └── carius/ │ └── lars/ │ └── ar_flutter_plugin/ │ ├── AndroidARView.kt │ ├── AndroidARViewFactory.kt │ ├── ArFlutterPlugin.kt │ ├── ArModelBuilder.kt │ ├── CloudAnchorHandler.kt │ └── Serialization/ │ ├── Deserializers.kt │ └── Serializers.kt ├── cloudAnchorSetup.md ├── example/ │ ├── .gitignore │ ├── .metadata │ ├── Models/ │ │ └── Chicken_01/ │ │ ├── Chicken_01.gltf │ │ └── license.txt │ ├── README.md │ ├── android/ │ │ ├── .gitignore │ │ ├── app/ │ │ │ ├── build.gradle │ │ │ └── src/ │ │ │ ├── debug/ │ │ │ │ └── AndroidManifest.xml │ │ │ ├── main/ │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── kotlin/ │ │ │ │ │ └── io/ │ │ │ │ │ └── carius/ │ │ │ │ │ └── lars/ │ │ │ │ │ └── ar_flutter_plugin_example/ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── res/ │ │ │ │ ├── drawable/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21/ │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values/ │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night/ │ │ │ │ └── styles.xml │ │ │ └── profile/ │ │ │ └── AndroidManifest.xml │ │ ├── build.gradle │ │ ├── gradle/ │ │ │ └── wrapper/ │ │ │ └── gradle-wrapper.properties │ │ ├── gradle.properties │ │ └── settings.gradle │ ├── ios/ │ │ ├── .gitignore │ │ ├── Flutter/ │ │ │ ├── AppFrameworkInfo.plist │ │ │ ├── Debug.xcconfig │ │ │ └── Release.xcconfig │ │ ├── Podfile │ │ ├── Runner/ │ │ │ ├── AppDelegate.swift │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ └── LaunchImage.imageset/ │ │ │ │ ├── Contents.json │ │ │ │ └── README.md │ │ │ ├── Base.lproj/ │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ │ ├── Info.plist │ │ │ └── Runner-Bridging-Header.h │ │ ├── Runner.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ ├── project.xcworkspace/ │ │ │ │ ├── contents.xcworkspacedata │ │ │ │ └── xcshareddata/ │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ │ └── xcshareddata/ │ │ │ └── xcschemes/ │ │ │ └── Runner.xcscheme │ │ └── Runner.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ ├── lib/ │ │ ├── examples/ │ │ │ ├── cloudanchorexample.dart │ │ │ ├── debugoptionsexample.dart │ │ │ ├── externalmodelmanagementexample.dart │ │ │ ├── localandwebobjectsexample.dart │ │ │ ├── objectgesturesexample.dart │ │ │ ├── objectsonplanesexample.dart │ │ │ └── screenshotexample.dart │ │ └── main.dart │ ├── pubspec.yaml │ └── test/ │ └── widget_test.dart ├── ios/ │ ├── .gitignore │ ├── Classes/ │ │ ├── ArFlutterPlugin.h │ │ ├── ArFlutterPlugin.m │ │ ├── ArModelBuilder.swift │ │ ├── CloudAnchorHandler.swift │ │ ├── IosARView.swift │ │ ├── IosARViewFactory.swift │ │ ├── JWTGenerator.swift │ │ ├── Serialization/ │ │ │ ├── Deserializers.swift │ │ │ └── Serializers.swift │ │ └── SwiftArFlutterPlugin.swift │ └── ar_flutter_plugin.podspec ├── lib/ │ ├── ar_flutter_plugin.dart │ ├── datatypes/ │ │ ├── anchor_types.dart │ │ ├── config_planedetection.dart │ │ ├── hittest_result_types.dart │ │ └── node_types.dart │ ├── managers/ │ │ ├── ar_anchor_manager.dart │ │ ├── ar_location_manager.dart │ │ ├── ar_object_manager.dart │ │ └── ar_session_manager.dart │ ├── models/ │ │ ├── ar_anchor.dart │ │ ├── ar_hittest_result.dart │ │ └── ar_node.dart │ ├── utils/ │ │ └── json_converters.dart │ └── widgets/ │ └── ar_view.dart ├── pubspec.yaml └── test/ └── ar_flutter_plugin_test.dart ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Credential files /example/ios/Runner/GoogleService-Info.plist /example/ios/Runner/cloudAnchorKey.json /example/android/app/google-services.json # Gitignore entries taken from https://github.com/flutter/flutter/blob/master/.gitignore : # Miscellaneous *.class *.lock *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # Visual Studio Code related .classpath .project .settings/ .vscode/ # Flutter repo-specific /bin/cache/ /bin/internal/bootstrap.bat /bin/internal/bootstrap.sh /bin/mingit/ /dev/benchmarks/mega_gallery/ /dev/bots/.recipe_deps /dev/bots/android_tools/ /dev/devicelab/ABresults*.json /dev/docs/doc/ /dev/docs/flutter.docs.zip /dev/docs/lib/ /dev/docs/pubspec.yaml /dev/integration_tests/**/xcuserdata /dev/integration_tests/**/Pods /packages/flutter/coverage/ version analysis_benchmark.json # packages file containing multi-root paths .packages.generated # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ .flutter-plugins .flutter-plugins-dependencies **/generated_plugin_registrant.dart .packages .pub-cache/ .pub/ build/ flutter_*.png linked_*.ds unlinked.ds unlinked_spec.ds # Android related **/android/**/gradle-wrapper.jar **/android/.gradle **/android/captures/ **/android/gradlew **/android/gradlew.bat **/android/local.properties **/android/**/GeneratedPluginRegistrant.java **/android/key.properties *.jks # iOS/XCode related **/ios/**/*.mode1v3 **/ios/**/*.mode2v3 **/ios/**/*.moved-aside **/ios/**/*.pbxuser **/ios/**/*.perspectivev3 **/ios/**/*sync/ **/ios/**/.sconsign.dblite **/ios/**/.tags* **/ios/**/.vagrant/ **/ios/**/DerivedData/ **/ios/**/Icon? **/ios/**/Pods/ **/ios/**/.symlinks/ **/ios/**/profile **/ios/**/xcuserdata **/ios/.generated/ **/ios/Flutter/.last_build_id **/ios/Flutter/App.framework **/ios/Flutter/Flutter.framework **/ios/Flutter/Flutter.podspec **/ios/Flutter/Generated.xcconfig **/ios/Flutter/app.flx **/ios/Flutter/app.zip **/ios/Flutter/flutter_assets/ **/ios/Flutter/flutter_export_environment.sh **/ios/ServiceDefinitions.json **/ios/Runner/GeneratedPluginRegistrant.* # macOS **/macos/Flutter/GeneratedPluginRegistrant.swift # Coverage coverage/ # Symbols app.*.symbols # Exceptions to above rules. !**/ios/**/default.mode1v3 !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock ================================================ FILE: .metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 4b50ca7f7fbf56be72e54cd200825b760416a356 channel: beta project_type: plugin ================================================ FILE: CHANGELOG.md ================================================ # Changelog ##0.7.3 * Update the examples with null-safety ## 0.7.2 * Fixes missing texturing on iOS ## 0.7.1 * Adds config to fix iOS cloud anchors not being able to upload ## 0.7.0 * Adds support to calculate distance between device and anchor and distance between two anchors ## 0.6.5 * Fixes the 'addNode' function to return true when a node is added to an anchor. ## 0.6.4 * Flutter 3 compatibility ## 0.6.3 * The function 'addNode' returned only true. You have now modified it to return false as well. * Prevent apps from turning off when errors other than those on your camera occur. ## 0.6.2 * Slight changes in ```AndroidARView``` dispose methods to prevent memory overflow issues when AR view is closed and reopened multiple times ## 0.6.1 * Adds ```dispose``` method to ```ARSessionManager``` to prevent memory overflow issues when AR view is closed and reopened multiple times ## 0.6.0 * Adds handling of two gestures: panning and rotating * Adds example showcasing handling and panning of nodes * Adds animated coaching overlay for finding planes (uses standard animation from SceneKit/Sceneform on iOS/Android respectively) - active by default, can be turned off using ```ARSessionManager``` * Updates ```ARCore/CloudAnchors``` to 1.26.0 on iOS * Removes overly restrictive permission for background location access on Android ## 0.5.1 * Removes overly restrictive background location permission on Android ## 0.5.0 * Adds new nodetypes ```fileSystemAppFolderGLB``` and ```fileSystemAppFolderGLTF2``` to load renderables from the device's local storage assigned to the current app * Extends the ```localandwebobjectsexample``` of the example app to showcase the new form of model loading * Adds snapshot functionality to the session manager to take screenshots of the ARView * Adds ```screenshotexample``` to the example app to showcase the snapshot functionality * Updates package versions of Flutter packages ```geolocator```, ```permission_handler```, ```vector_math```, and iOS package ```ARCore/CloudAnchors``` in the plugin * Updates package versions of Flutter packages ```firebase_core```, ```cloud_firestore```, ```geoflutterfire```, and ```FirebaseSDKVersion ``` on iOS in the example application ## 0.4.3 * Updates documentation after publishing to [pub.dev](https://pub.dev) ## 0.4.2 * Updates documentation * Deletes unnecessary files ## 0.4.1 * Adds External Model Management Example: A firebase database is used to store a list of 3D models (name, preview image, URI of the file location in a github repo). These models can be scrolled through and selected from in the example and can then be placed into the scene, uploaded through the Google Cloud Anchor API and downloaded on any other device with the app installed. The downloading user does not need to have the model "pre-installed", it is downloaded on the first occasion. ## 0.4.0 * Adds location manager which can be used to query the device's location (including permission and update handling) * Adds geoflutterfire to support uploading GPS coordinates alongside anchors and downloading anchors and objects by their location * Modifies cloud anchor example: Download button now queries and downloads all anchors along with the corresponding objects within a 100m radius of the device's current location * Bugfix: fixes bug on Android causing some examples to crash because the cloud anchor manager wasn't initialized ## 0.3.0 * BREAKING CHANGE: Converts plugin to adhere to Flutter null safety * Adds Cloud Anchor functionality (uploading and downloading anchors to/from the Google Cloud Anchor API), including keyless authentication * Adds Cloud Anchor example demonstrating how to use Firebase to manage cloud anchor IDs and corresponding data (e.g. on-tap texts) * Adds ```data``` member variable to ```ARNode``` as a flexible variable to hold any information associated with the node ## 0.2.1 * Bugfix: Handles singularities in affine transformation matrix deserialization on Android ## 0.2.0 * Adds AR Anchor as a common representation of anchors on all platforms * Implements AR Plane Anchor as subclass of AR Anchor: Plane Anchors can be created in Flutter, registered on the target platform and then be used to attach nodes to * Adds AR Hittest Result as a common representation of hittest results on all platforms. If the setting is activated in the session manager, taps on the platforms are registered and hit test results can be used to trigger callbacks in Flutter (example: hit result world coordinate transforms can be used to place anchors or nodes into the scene) * Adds option to trigger callbacks when nodes are tapped * Adds example to showcase hittests, creating and placing anchors and attaching nodes to anchors ## 0.1.0 * Adds AR Node as a common representation of nodes on all platforms * Custom objects (GLTF2 models from Flutter's asset folders or GLB models from the Internet) can be attached to nodes and are loaded / downloaded asynchronously during runtime to ensure maximum flexibility * AR Nodes (with attached objects) can be placed in the scene with respect to the world coordinate system, position, rotation and scale can be set and updated during runtime * Updates debug option functionality: options can be changed at runtime (see Debug Option example) * Updates examples to showcase current state of the plugin ## 0.0.1 * Coarse Plugin Architecture layed out * ARView supports iOS (ARKit) and Android (ARCore) devices * Camera Permission checks added * Debug options added on both platforms: Feature Points visualization, detected planes visualization and world origin visualization * Adds possibility to use own texture for plane visualization on both platforms ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Lars Carius Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ar_flutter_plugin [![pub package](https://img.shields.io/pub/v/ar_flutter_plugin.svg)](https://pub.dev/packages/ar_flutter_plugin) Flutter Plugin for (collaborative) Augmented Reality - Supports ARKit for iOS and ARCore for Android devices. Many thanks to Oleksandr Leuschenko for the [arkit_flutter_plugin](https://github.com/olexale/arkit_flutter_plugin) and to Gian Marco Di Francesco for the [arcore_flutter_plugin](https://github.com/giandifra/arcore_flutter_plugin) which both served as a great basis and starting point for this project. ## Getting Started ### Installing Add the Flutter package to your project by running: ```bash flutter pub add ar_flutter_plugin ``` Or manually add this to your `pubspec.yaml` file (and run `flutter pub get`): # ar_flutter_plugin package extension ```yaml dependencies: ar_flutter_plugin: ^0.7.3 ``` ### Importing Add this to your code: ```dart import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; ``` If you have problems with permissions on iOS (e.g. with the camera view not showing up even though camera access is allowed), add this to the ```podfile``` of your app's ```ios``` directory: ```pod post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| # Additional configuration options could already be set here # BEGINNING OF WHAT YOU SHOULD ADD config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera 'PERMISSION_CAMERA=1', ## dart: PermissionGroup.photos 'PERMISSION_PHOTOS=1', ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] 'PERMISSION_LOCATION=1', ## dart: PermissionGroup.sensors 'PERMISSION_SENSORS=1', ## dart: PermissionGroup.bluetooth 'PERMISSION_BLUETOOTH=1', # add additional permission groups if required ] # END OF WHAT YOU SHOULD ADD end end end ``` ### Example Applications To try out the plugin, it is best to have a look at one of the following examples implemented in the `Example` app: | Example Name | Description | Link to Code | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | Debug Options | Simple AR scene with toggles to visualize the world origin, feature points and tracked planes | [Debug Options Code](https://github.com/CariusLars/ar_flutter_plugin/blob/main/example/lib/examples/debugoptionsexample.dart) | | Local & Online Objects | AR scene with buttons to place GLTF objects from the flutter asset folders, GLB objects from the internet, or a GLB object from the app's Documents directory at a given position, rotation and scale. Additional buttons allow to modify scale, position and orientation with regard to the world origin after objects have been placed. | [Local & Online Objects Code](https://github.com/CariusLars/ar_flutter_plugin/blob/main/example/lib/examples/localandwebobjectsexample.dart) | | Objects & Anchors on Planes | AR Scene in which tapping on a plane creates an anchor with a 3D model attached to it | [Objects & Anchors on Planes Code](https://github.com/CariusLars/ar_flutter_plugin/blob/main/example/lib/examples/objectgesturesexample.dart) | | Object Transformation Gestures | Same as Objects & Anchors on Planes example, but objects can be panned and rotated using gestures after being placed | [Objects & Anchors on Planes Code](https://github.com/CariusLars/ar_flutter_plugin/blob/main/example/lib/examples/objectsonplanesexample.dart) | | | | Screenshots | Same as Objects & Anchors on Planes Example, but the snapshot function is used to take screenshots of the AR Scene | [Screenshots Code](https://github.com/CariusLars/ar_flutter_plugin/blob/main/example/lib/examples/screenshotexample.dart) | | Cloud Anchors | AR Scene in which objects can be placed, uploaded and downloaded, thus creating an interactive AR experience that can be shared between multiple devices. Currently, the example allows to upload the last placed object along with its anchor and download all anchors within a radius of 100m along with all the attached objects (independent of which device originally placed the objects). As sharing the objects is done by using the Google Cloud Anchor Service and Firebase, this requires some additional setup, please read [Getting Started with cloud anchors](cloudAnchorSetup.md) | [Cloud Anchors Code](https://github.com/CariusLars/ar_flutter_plugin/blob/main/example/lib/examples/cloudanchorexample.dart) | | External Object Management | Similar to the Cloud Anchors example, but contains UI to choose between different models. Rather than being hard-coded, an external database (Firestore) is used to manage the available models. As sharing the objects is done by using the Google Cloud Anchor Service and Firebase, this requires some additional setup, please read [Getting Started with cloud anchors](cloudAnchorSetup.md). Also make sure that in your Firestore database, the collection "models" contains some entries with the fields "name", "image", and "uri", where "uri" points to the raw file of a model in GLB format | [External Model Management Code](https://github.com/CariusLars/ar_flutter_plugin/blob/main/example/lib/examples/externalmodelmanagementexample.dart) | ## Contributing Contributions to this plugin are very welcome. To contribute code and discuss ideas, [create a pull request](https://github.com/CariusLars/ar_flutter_plugin/compare), [open an issue](https://github.com/CariusLars/ar_flutter_plugin/issues/new), or [start a discussion](https://github.com/CariusLars/ar_flutter_plugin/discussions). ## Plugin Architecture This is a rough sketch of the architecture the plugin implements: ![ar_plugin_architecture](./AR_Plugin_Architecture_highlevel.svg) ================================================ FILE: android/.gitignore ================================================ *.iml .gradle /local.properties /.idea/workspace.xml /.idea/libraries .DS_Store /build /captures ================================================ FILE: android/build.gradle ================================================ group 'io.carius.lars.ar_flutter_plugin' version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.3.50' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } rootProject.allprojects { repositories { google() jcenter() } } apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { minSdkVersion 24 } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.google.ar:core:1.22.0" implementation 'com.google.ar.sceneform:core:1.15.0' implementation 'com.google.ar.sceneform:assets:1.15.0' implementation 'com.google.android.gms:play-services-auth:16+' implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1' implementation 'androidx.appcompat:appcompat:1.3.0' } afterEvaluate { def containsEmbeddingDependencies = false for (def configuration : configurations.all) { for (def dependency : configuration.dependencies) { if (dependency.group == 'io.flutter' && dependency.name.startsWith('flutter_embedding') && dependency.isTransitive()) { containsEmbeddingDependencies = true break } } } if (!containsEmbeddingDependencies) { android { dependencies { def lifecycle_version = "1.1.1" compileOnly "android.arch.lifecycle:runtime:$lifecycle_version" compileOnly "android.arch.lifecycle:common:$lifecycle_version" compileOnly "android.arch.lifecycle:common-java8:$lifecycle_version" } } } } ================================================ FILE: android/gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip ================================================ FILE: android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: android/settings.gradle ================================================ rootProject.name = 'ar_flutter_plugin' ================================================ FILE: android/src/main/AndroidManifest.xml ================================================ ================================================ FILE: android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/AndroidARView.kt ================================================ package io.carius.lars.ar_flutter_plugin import android.app.Activity import android.app.Application import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.HandlerThread import android.util.Log import android.view.MotionEvent import android.view.PixelCopy import android.view.View import android.widget.Toast import com.google.ar.core.* import com.google.ar.core.exceptions.* import com.google.ar.sceneform.* import com.google.ar.sceneform.math.Vector3 import com.google.ar.sceneform.ux.* import io.carius.lars.ar_flutter_plugin.Serialization.deserializeMatrix4 import io.carius.lars.ar_flutter_plugin.Serialization.serializeAnchor import io.carius.lars.ar_flutter_plugin.Serialization.serializeHitResult import io.carius.lars.ar_flutter_plugin.Serialization.serializePose import io.flutter.FlutterInjector import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.platform.PlatformView import java.io.ByteArrayOutputStream import java.io.IOException import java.nio.FloatBuffer import java.util.concurrent.CompletableFuture import android.R import com.google.ar.sceneform.rendering.* import android.view.ViewGroup import com.google.ar.core.TrackingState internal class AndroidARView( val activity: Activity, context: Context, messenger: BinaryMessenger, id: Int, creationParams: Map? ) : PlatformView { // constants private val TAG: String = AndroidARView::class.java.name // Lifecycle variables private var mUserRequestedInstall = true lateinit var activityLifecycleCallbacks: Application.ActivityLifecycleCallbacks private val viewContext: Context // Platform channels private val sessionManagerChannel: MethodChannel = MethodChannel(messenger, "arsession_$id") private val objectManagerChannel: MethodChannel = MethodChannel(messenger, "arobjects_$id") private val anchorManagerChannel: MethodChannel = MethodChannel(messenger, "aranchors_$id") // UI variables private lateinit var arSceneView: ArSceneView private lateinit var transformationSystem: TransformationSystem private var showFeaturePoints = false private var showAnimatedGuide = false private lateinit var animatedGuide: View private var pointCloudNode = Node() private var worldOriginNode = Node() // Setting defaults private var enableRotation = false private var enablePans = false private var keepNodeSelected = true; private var footprintSelectionVisualizer = FootprintSelectionVisualizer() // Model builder private var modelBuilder = ArModelBuilder() // Cloud anchor handler private lateinit var cloudAnchorHandler: CloudAnchorHandler private lateinit var sceneUpdateListener: com.google.ar.sceneform.Scene.OnUpdateListener private lateinit var onNodeTapListener: com.google.ar.sceneform.Scene.OnPeekTouchListener // Method channel handlers private val onSessionMethodCall = object : MethodChannel.MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { Log.d(TAG, "AndroidARView onsessionmethodcall reveived a call!") when (call.method) { "init" -> { initializeARView(call, result) } "getAnchorPose" -> { val anchorNode = arSceneView.scene.findByName(call.argument("anchorId")) as AnchorNode? if (anchorNode != null) { result.success(serializePose(anchorNode.anchor!!.pose)) } else { result.error("Error", "could not get anchor pose", null) } } "getCameraPose" -> { val cameraPose = arSceneView.arFrame?.camera?.displayOrientedPose if (cameraPose != null) { result.success(serializePose(cameraPose!!)) } else { result.error("Error", "could not get camera pose", null) } } "snapshot" -> { var bitmap = Bitmap.createBitmap(arSceneView.width, arSceneView.height, Bitmap.Config.ARGB_8888); // Create a handler thread to offload the processing of the image. var handlerThread = HandlerThread("PixelCopier"); handlerThread.start(); // Make the request to copy. PixelCopy.request(arSceneView, bitmap, { copyResult:Int -> Log.d(TAG, "PIXELCOPY DONE") if (copyResult == PixelCopy.SUCCESS) { try { val mainHandler = Handler(context.mainLooper) val runnable = Runnable { val stream = ByteArrayOutputStream() bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream) val data = stream.toByteArray() result.success(data) } mainHandler.post(runnable) } catch (e: IOException) { result.error("e", e.message, e.stackTrace); } } else { result.error("e", "failed to take screenshot", null); } handlerThread.quitSafely(); }, Handler(handlerThread.looper)); } "dispose" -> { dispose() } else -> {} } } } private val onObjectMethodCall = object : MethodChannel.MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { Log.d(TAG, "AndroidARView onobjectmethodcall reveived a call!") when (call.method) { "init" -> { // objectManagerChannel.invokeMethod("onError", listOf("ObjectTEST from // Android")) } "addNode" -> { val dict_node: HashMap? = call.arguments as? HashMap dict_node?.let{ addNode(it).thenAccept{status: Boolean -> result.success(status) }.exceptionally { throwable -> result.error("e", throwable.message, throwable.stackTrace) null } } } "addNodeToPlaneAnchor" -> { val dict_node: HashMap? = call.argument>("node") val dict_anchor: HashMap? = call.argument>("anchor") if (dict_node != null && dict_anchor != null) { addNode(dict_node, dict_anchor).thenAccept{status: Boolean -> result.success(status) }.exceptionally { throwable -> result.error("e", throwable.message, throwable.stackTrace) null } } else { result.success(false) } } "removeNode" -> { val nodeName: String? = call.argument("name") nodeName?.let{ if (transformationSystem.selectedNode?.name == nodeName){ transformationSystem.selectNode(null) keepNodeSelected = true } val node = arSceneView.scene.findByName(nodeName) node?.let{ arSceneView.scene.removeChild(node) result.success(null) } } } "transformationChanged" -> { val nodeName: String? = call.argument("name") val newTransformation: ArrayList? = call.argument>("transformation") nodeName?.let{ name -> newTransformation?.let{ transform -> transformNode(name, transform) result.success(null) } } } else -> {} } } } private val onAnchorMethodCall = object : MethodChannel.MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "addAnchor" -> { val anchorType: Int? = call.argument("type") if (anchorType != null){ when(anchorType) { 0 -> { // Plane Anchor val transform: ArrayList? = call.argument>("transformation") val name: String? = call.argument("name") if ( name != null && transform != null){ result.success(addPlaneAnchor(transform, name)) } else { result.success(false) } } else -> result.success(false) } } else { result.success(false) } } "removeAnchor" -> { val anchorName: String? = call.argument("name") anchorName?.let{ name -> removeAnchor(name) } } "initGoogleCloudAnchorMode" -> { if (arSceneView.session != null) { val config = Config(arSceneView.session) config.cloudAnchorMode = Config.CloudAnchorMode.ENABLED config.updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE config.focusMode = Config.FocusMode.AUTO arSceneView.session?.configure(config) cloudAnchorHandler = CloudAnchorHandler(arSceneView.session!!) } else { sessionManagerChannel.invokeMethod("onError", listOf("Error initializing cloud anchor mode: Session is null")) } } "uploadAnchor" -> { val anchorName: String? = call.argument("name") val ttl: Int? = call.argument("ttl") anchorName?.let { val anchorNode = arSceneView.scene.findByName(anchorName) as AnchorNode? if (ttl != null) { cloudAnchorHandler.hostCloudAnchorWithTtl(anchorName, anchorNode!!.anchor, cloudAnchorUploadedListener(), ttl!!) } else { cloudAnchorHandler.hostCloudAnchor(anchorName, anchorNode!!.anchor, cloudAnchorUploadedListener()) } //Log.d(TAG, "---------------- HOSTING INITIATED ------------------") result.success(true) } } "downloadAnchor" -> { val anchorId: String? = call.argument("cloudanchorid") //Log.d(TAG, "---------------- RESOLVING INITIATED ------------------") anchorId?.let { cloudAnchorHandler.resolveCloudAnchor(anchorId, cloudAnchorDownloadedListener()) } } else -> {} } } } override fun getView(): View { return arSceneView } override fun dispose() { // Destroy AR session Log.d(TAG, "dispose called") try { onPause() onDestroy() ArSceneView.destroyAllResources() } catch (e: Exception) { e.printStackTrace() } } init { Log.d(TAG, "Initializing AndroidARView") viewContext = context arSceneView = ArSceneView(context) setupLifeCycle(context) sessionManagerChannel.setMethodCallHandler(onSessionMethodCall) objectManagerChannel.setMethodCallHandler(onObjectMethodCall) anchorManagerChannel.setMethodCallHandler(onAnchorMethodCall) //Original visualizer: com.google.ar.sceneform.ux.R.raw.sceneform_footprint MaterialFactory.makeTransparentWithColor(context, Color(255f, 255f, 255f, 0.3f)) .thenAccept { mat -> footprintSelectionVisualizer.footprintRenderable = ShapeFactory.makeCylinder(0.7f,0.05f, Vector3(0f,0f,0f), mat) } transformationSystem = TransformationSystem( activity.resources.displayMetrics, footprintSelectionVisualizer) onResume() // call onResume once to setup initial session // TODO: find out why this does not happen automatically } private fun setupLifeCycle(context: Context) { activityLifecycleCallbacks = object : Application.ActivityLifecycleCallbacks { override fun onActivityCreated( activity: Activity, savedInstanceState: Bundle? ) { Log.d(TAG, "onActivityCreated") } override fun onActivityStarted(activity: Activity) { Log.d(TAG, "onActivityStarted") } override fun onActivityResumed(activity: Activity) { Log.d(TAG, "onActivityResumed") onResume() } override fun onActivityPaused(activity: Activity) { Log.d(TAG, "onActivityPaused") onPause() } override fun onActivityStopped(activity: Activity) { Log.d(TAG, "onActivityStopped") // onStopped() onPause() } override fun onActivitySaveInstanceState( activity: Activity, outState: Bundle ) {} override fun onActivityDestroyed(activity: Activity) { Log.d(TAG, "onActivityDestroyed") // onPause() // onDestroy() } } activity.application.registerActivityLifecycleCallbacks(this.activityLifecycleCallbacks) } fun onResume() { // Create session if there is none if (arSceneView.session == null) { Log.d(TAG, "ARSceneView session is null. Trying to initialize") try { var session: Session? if (ArCoreApk.getInstance().requestInstall(activity, mUserRequestedInstall) == ArCoreApk.InstallStatus.INSTALL_REQUESTED) { Log.d(TAG, "Install of ArCore APK requested") session = null } else { session = Session(activity) } if (session == null) { // Ensures next invocation of requestInstall() will either return // INSTALLED or throw an exception. mUserRequestedInstall = false return } else { val config = Config(session) config.updateMode = Config.UpdateMode.LATEST_CAMERA_IMAGE config.focusMode = Config.FocusMode.AUTO session.configure(config) arSceneView.setupSession(session) } } catch (ex: UnavailableUserDeclinedInstallationException) { // Display an appropriate message to the user zand return gracefully. Toast.makeText( activity, "TODO: handle exception " + ex.localizedMessage, Toast.LENGTH_LONG) .show() return } catch (ex: UnavailableArcoreNotInstalledException) { Toast.makeText(activity, "Please install ARCore", Toast.LENGTH_LONG).show() return } catch (ex: UnavailableApkTooOldException) { Toast.makeText(activity, "Please update ARCore", Toast.LENGTH_LONG).show() return } catch (ex: UnavailableSdkTooOldException) { Toast.makeText(activity, "Please update this app", Toast.LENGTH_LONG).show() return } catch (ex: UnavailableDeviceNotCompatibleException) { Toast.makeText(activity, "This device does not support AR", Toast.LENGTH_LONG) .show() return } catch (e: Exception) { Toast.makeText(activity, "Failed to create AR session", Toast.LENGTH_LONG).show() return } } try { arSceneView.resume() } catch (ex: CameraNotAvailableException) { Log.d(TAG, "Unable to get camera" + ex) activity.finish() return } catch (e : Exception){ return } } fun onPause() { // hide instructions view if no longer required if (showAnimatedGuide){ val view = activity.findViewById(R.id.content) as ViewGroup view.removeView(animatedGuide) showAnimatedGuide = false } arSceneView.pause() } fun onDestroy() { try { arSceneView.session?.close() arSceneView.destroy() arSceneView.scene?.removeOnUpdateListener(sceneUpdateListener) arSceneView.scene?.removeOnPeekTouchListener(onNodeTapListener) }catch (e : Exception){ e.printStackTrace(); } } private fun initializeARView(call: MethodCall, result: MethodChannel.Result) { // Unpack call arguments val argShowFeaturePoints: Boolean? = call.argument("showFeaturePoints") val argPlaneDetectionConfig: Int? = call.argument("planeDetectionConfig") val argShowPlanes: Boolean? = call.argument("showPlanes") val argCustomPlaneTexturePath: String? = call.argument("customPlaneTexturePath") val argShowWorldOrigin: Boolean? = call.argument("showWorldOrigin") val argHandleTaps: Boolean? = call.argument("handleTaps") val argHandleRotation: Boolean? = call.argument("handleRotation") val argHandlePans: Boolean? = call.argument("handlePans") val argShowAnimatedGuide: Boolean? = call.argument("showAnimatedGuide") sceneUpdateListener = com.google.ar.sceneform.Scene.OnUpdateListener { frameTime: FrameTime -> onFrame(frameTime) } onNodeTapListener = com.google.ar.sceneform.Scene.OnPeekTouchListener { hitTestResult, motionEvent -> //if (hitTestResult.node != null){ //transformationSystem.selectionVisualizer.applySelectionVisual(hitTestResult.node as TransformableNode) //transformationSystem.selectNode(hitTestResult.node as TransformableNode) //} if (hitTestResult.node != null && motionEvent?.action == MotionEvent.ACTION_DOWN) { objectManagerChannel.invokeMethod("onNodeTap", listOf(hitTestResult.node?.name)) } transformationSystem.onTouch( hitTestResult, motionEvent ) } arSceneView.scene?.addOnUpdateListener(sceneUpdateListener) arSceneView.scene?.addOnPeekTouchListener(onNodeTapListener) // Configure Plane scanning guide if (argShowAnimatedGuide == true) { // explicit comparison necessary because of nullable type showAnimatedGuide = true val view = activity.findViewById(R.id.content) as ViewGroup animatedGuide = activity.layoutInflater.inflate(com.google.ar.sceneform.ux.R.layout.sceneform_plane_discovery_layout, null) view.addView(animatedGuide) } // Configure feature points if (argShowFeaturePoints == true) { // explicit comparison necessary because of nullable type arSceneView.scene.addChild(pointCloudNode) showFeaturePoints = true } else { showFeaturePoints = false while (pointCloudNode.children?.size ?: 0 > 0) { pointCloudNode.children?.first()?.setParent(null) } pointCloudNode.setParent(null) } // Configure plane detection val config = arSceneView.session?.config if (config == null) { sessionManagerChannel.invokeMethod("onError", listOf("session is null")) } when (argPlaneDetectionConfig) { 1 -> { config?.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL } 2 -> { config?.planeFindingMode = Config.PlaneFindingMode.VERTICAL } 3 -> { config?.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL } else -> { config?.planeFindingMode = Config.PlaneFindingMode.DISABLED } } arSceneView.session?.configure(config) // Configure whether or not detected planes should be shown arSceneView.planeRenderer.isVisible = if (argShowPlanes == true) true else false // Create custom plane renderer (use supplied texture & increase radius) argCustomPlaneTexturePath?.let { val loader: FlutterLoader = FlutterInjector.instance().flutterLoader() val key: String = loader.getLookupKeyForAsset(it) val sampler = Texture.Sampler.builder() .setMinFilter(Texture.Sampler.MinFilter.LINEAR) .setWrapMode(Texture.Sampler.WrapMode.REPEAT) .build() Texture.builder() .setSource(viewContext, Uri.parse(key)) .setSampler(sampler) .build() .thenAccept { texture: Texture? -> arSceneView.planeRenderer.material.thenAccept { material: Material -> material.setTexture(PlaneRenderer.MATERIAL_TEXTURE, texture) material.setFloat(PlaneRenderer.MATERIAL_SPOTLIGHT_RADIUS, 10f) } } // Set radius to render planes in arSceneView.scene.addOnUpdateListener { frameTime: FrameTime? -> val planeRenderer = arSceneView.planeRenderer planeRenderer.material.thenAccept { material: Material -> material.setFloat( PlaneRenderer.MATERIAL_SPOTLIGHT_RADIUS, 10f) // Sets the radius in which to visualize planes } } } // Configure world origin if (argShowWorldOrigin == true) { worldOriginNode = modelBuilder.makeWorldOriginNode(viewContext) arSceneView.scene.addChild(worldOriginNode) } else { worldOriginNode.setParent(null) } // Configure Tap handling if (argHandleTaps == true) { // explicit comparison necessary because of nullable type arSceneView.scene.setOnTouchListener{ hitTestResult: HitTestResult, motionEvent: MotionEvent? -> onTap(hitTestResult, motionEvent) } } // Configure gestures if (argHandleRotation == true) { // explicit comparison necessary because of nullable type enableRotation = true } else { enableRotation = false } if (argHandlePans == true) { // explicit comparison necessary because of nullable type enablePans = true } else { enablePans = false } result.success(null) } private fun onFrame(frameTime: FrameTime) { // hide instructions view if no longer required if (showAnimatedGuide && arSceneView.arFrame != null){ for (plane in arSceneView.arFrame!!.getUpdatedTrackables(Plane::class.java)) { if (plane.trackingState === TrackingState.TRACKING) { val view = activity.findViewById(R.id.content) as ViewGroup view.removeView(animatedGuide) showAnimatedGuide = false break } } } if (showFeaturePoints) { // remove points from last frame while (pointCloudNode.children?.size ?: 0 > 0) { pointCloudNode.children?.first()?.setParent(null) } var pointCloud = arSceneView.arFrame?.acquirePointCloud() // Access point cloud data (returns FloatBufferw with x,y,z coordinates and confidence // value). val points = pointCloud?.getPoints() ?: FloatBuffer.allocate(0) // Check if there are any feature points if (points.limit() / 4 >= 1) { for (index in 0 until points.limit() / 4) { // Add feature point to scene val featurePoint = modelBuilder.makeFeaturePointNode( viewContext, points.get(4 * index), points.get(4 * index + 1), points.get(4 * index + 2)) featurePoint.setParent(pointCloudNode) } } // Release resources pointCloud?.release() } val updatedAnchors = arSceneView.arFrame!!.updatedAnchors // Notify the cloudManager of all the updates. if (this::cloudAnchorHandler.isInitialized) {cloudAnchorHandler.onUpdate(updatedAnchors)} if (keepNodeSelected && transformationSystem.selectedNode != null && transformationSystem.selectedNode!!.isTransforming){ // If the selected node is currently transforming, we want to deselect it as soon as the transformation is done keepNodeSelected = false } if (!keepNodeSelected && transformationSystem.selectedNode != null && !transformationSystem.selectedNode!!.isTransforming){ // once the transformation is done, deselect the node and allow selection of another node transformationSystem.selectNode(null) keepNodeSelected = true } if (!enablePans && !enableRotation){ //unselect all nodes as we do not want the selection visualizer transformationSystem.selectNode(null) } } private fun addNode(dict_node: HashMap, dict_anchor: HashMap? = null): CompletableFuture{ val completableFutureSuccess: CompletableFuture = CompletableFuture() try { when (dict_node["type"] as Int) { 0 -> { // GLTF2 Model from Flutter asset folder // Get path to given Flutter asset val loader: FlutterLoader = FlutterInjector.instance().flutterLoader() val key: String = loader.getLookupKeyForAsset(dict_node["uri"] as String) // Add object to scene modelBuilder.makeNodeFromGltf(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, dict_node["name"] as String, key, dict_node["transformation"] as ArrayList) .thenAccept{node -> val anchorName: String? = dict_anchor?.get("name") as? String val anchorType: Int? = dict_anchor?.get("type") as? Int if (anchorName != null && anchorType != null) { val anchorNode = arSceneView.scene.findByName(anchorName) as AnchorNode? if (anchorNode != null) { anchorNode.addChild(node) completableFutureSuccess.complete(true) } else { completableFutureSuccess.complete(false) } } else { arSceneView.scene.addChild(node) completableFutureSuccess.complete(true) } completableFutureSuccess.complete(false) } .exceptionally { throwable -> // Pass error to session manager (this has to be done on the main thread if this activity) val mainHandler = Handler(viewContext.mainLooper) val runnable = Runnable {sessionManagerChannel.invokeMethod("onError", listOf("Unable to load renderable" + dict_node["uri"] as String)) } mainHandler.post(runnable) completableFutureSuccess.completeExceptionally(throwable) null // return null because java expects void return (in java, void has no instance, whereas in Kotlin, this closure returns a Unit which has one instance) } } 1 -> { // GLB Model from the web modelBuilder.makeNodeFromGlb(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, dict_node["name"] as String, dict_node["uri"] as String, dict_node["transformation"] as ArrayList) .thenAccept{node -> val anchorName: String? = dict_anchor?.get("name") as? String val anchorType: Int? = dict_anchor?.get("type") as? Int if (anchorName != null && anchorType != null) { val anchorNode = arSceneView.scene.findByName(anchorName) as AnchorNode? if (anchorNode != null) { anchorNode.addChild(node) completableFutureSuccess.complete(true) } else { completableFutureSuccess.complete(false) } } else { arSceneView.scene.addChild(node) completableFutureSuccess.complete(true) } completableFutureSuccess.complete(false) } .exceptionally { throwable -> // Pass error to session manager (this has to be done on the main thread if this activity) val mainHandler = Handler(viewContext.mainLooper) val runnable = Runnable {sessionManagerChannel.invokeMethod("onError", listOf("Unable to load renderable" + dict_node["uri"] as String)) } mainHandler.post(runnable) completableFutureSuccess.completeExceptionally(throwable) null // return null because java expects void return (in java, void has no instance, whereas in Kotlin, this closure returns a Unit which has one instance) } } 2 -> { // fileSystemAppFolderGLB val documentsPath = viewContext.getApplicationInfo().dataDir val assetPath = documentsPath + "/app_flutter/" + dict_node["uri"] as String modelBuilder.makeNodeFromGlb(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, dict_node["name"] as String, assetPath as String, dict_node["transformation"] as ArrayList) // .thenAccept{node -> val anchorName: String? = dict_anchor?.get("name") as? String val anchorType: Int? = dict_anchor?.get("type") as? Int if (anchorName != null && anchorType != null) { val anchorNode = arSceneView.scene.findByName(anchorName) as AnchorNode? if (anchorNode != null) { anchorNode.addChild(node) completableFutureSuccess.complete(true) } else { completableFutureSuccess.complete(false) } } else { arSceneView.scene.addChild(node) completableFutureSuccess.complete(true) } completableFutureSuccess.complete(false) } .exceptionally { throwable -> // Pass error to session manager (this has to be done on the main thread if this activity) val mainHandler = Handler(viewContext.mainLooper) val runnable = Runnable {sessionManagerChannel.invokeMethod("onError", listOf("Unable to load renderable " + dict_node["uri"] as String)) } mainHandler.post(runnable) completableFutureSuccess.completeExceptionally(throwable) null // return null because java expects void return (in java, void has no instance, whereas in Kotlin, this closure returns a Unit which has one instance) } } 3 -> { //fileSystemAppFolderGLTF2 // Get path to given Flutter asset val documentsPath = viewContext.getApplicationInfo().dataDir val assetPath = documentsPath + "/app_flutter/" + dict_node["uri"] as String // Add object to scene modelBuilder.makeNodeFromGltf(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, dict_node["name"] as String, assetPath, dict_node["transformation"] as ArrayList) .thenAccept{node -> val anchorName: String? = dict_anchor?.get("name") as? String val anchorType: Int? = dict_anchor?.get("type") as? Int if (anchorName != null && anchorType != null) { val anchorNode = arSceneView.scene.findByName(anchorName) as AnchorNode? if (anchorNode != null) { anchorNode.addChild(node) completableFutureSuccess.complete(true) } else { completableFutureSuccess.complete(false) } } else { arSceneView.scene.addChild(node) completableFutureSuccess.complete(true) } completableFutureSuccess.complete(false) } .exceptionally { throwable -> // Pass error to session manager (this has to be done on the main thread if this activity) val mainHandler = Handler(viewContext.mainLooper) val runnable = Runnable {sessionManagerChannel.invokeMethod("onError", listOf("Unable to load renderable" + dict_node["uri"] as String)) } mainHandler.post(runnable) completableFutureSuccess.completeExceptionally(throwable) null // return null because java expects void return (in java, void has no instance, whereas in Kotlin, this closure returns a Unit which has one instance) } } else -> { completableFutureSuccess.complete(false) } } } catch (e: java.lang.Exception) { completableFutureSuccess.completeExceptionally(e) } return completableFutureSuccess } private fun transformNode(name: String, transform: ArrayList) { val node = arSceneView.scene.findByName(name) node?.let { val transformTriple = deserializeMatrix4(transform) it.localScale = transformTriple.first it.localPosition = transformTriple.second it.localRotation = transformTriple.third //it.worldScale = transformTriple.first //it.worldPosition = transformTriple.second //it.worldRotation = transformTriple.third } } private fun onTap(hitTestResult: HitTestResult, motionEvent: MotionEvent?): Boolean { val frame = arSceneView.arFrame if (hitTestResult.node != null && motionEvent?.action == MotionEvent.ACTION_DOWN) { objectManagerChannel.invokeMethod("onNodeTap", listOf(hitTestResult.node?.name)) return true } if (motionEvent != null && motionEvent.action == MotionEvent.ACTION_DOWN) { if (transformationSystem.selectedNode == null || (!enablePans && !enableRotation)){ val allHitResults = frame?.hitTest(motionEvent) ?: listOf() val planeAndPointHitResults = allHitResults.filter { ((it.trackable is Plane) || (it.trackable is Point)) } val serializedPlaneAndPointHitResults: ArrayList> = ArrayList(planeAndPointHitResults.map { serializeHitResult(it) }) sessionManagerChannel.invokeMethod( "onPlaneOrPointTap", serializedPlaneAndPointHitResults ) return true } else { return false } } return false } private fun addPlaneAnchor(transform: ArrayList, name: String): Boolean { return try { val position = floatArrayOf(deserializeMatrix4(transform).second.x, deserializeMatrix4(transform).second.y, deserializeMatrix4(transform).second.z) val rotation = floatArrayOf(deserializeMatrix4(transform).third.x, deserializeMatrix4(transform).third.y, deserializeMatrix4(transform).third.z, deserializeMatrix4(transform).third.w) val anchor: Anchor = arSceneView.session!!.createAnchor(Pose(position, rotation)) val anchorNode = AnchorNode(anchor) anchorNode.name = name anchorNode.setParent(arSceneView.scene) true } catch (e: Exception) { false } } private fun removeAnchor(name: String) { val anchorNode = arSceneView.scene.findByName(name) as AnchorNode? anchorNode?.let{ // Remove corresponding anchor from tracking anchorNode.anchor?.detach() // Remove children for (node in anchorNode.children) { if (transformationSystem.selectedNode?.name == node.name){ transformationSystem.selectNode(null) keepNodeSelected = true } node.setParent(null) } // Remove anchor node anchorNode.setParent(null) } } private inner class cloudAnchorUploadedListener: CloudAnchorHandler.CloudAnchorListener { override fun onCloudTaskComplete(anchorName: String?, anchor: Anchor?) { val cloudState = anchor!!.cloudAnchorState if (cloudState.isError) { Log.e(TAG, "Error uploading anchor, state $cloudState") sessionManagerChannel.invokeMethod("onError", listOf("Error uploading anchor, state $cloudState")) return } // Swap old an new anchor of the respective AnchorNode val anchorNode = arSceneView.scene.findByName(anchorName) as AnchorNode? val oldAnchor = anchorNode?.anchor anchorNode?.anchor = anchor oldAnchor?.detach() val args = HashMap() args["name"] = anchorName args["cloudanchorid"] = anchor.cloudAnchorId anchorManagerChannel.invokeMethod("onCloudAnchorUploaded", args) } } private inner class cloudAnchorDownloadedListener: CloudAnchorHandler.CloudAnchorListener { override fun onCloudTaskComplete(anchorName: String?, anchor: Anchor?) { val cloudState = anchor!!.cloudAnchorState if (cloudState.isError) { Log.e(TAG, "Error downloading anchor, state $cloudState") sessionManagerChannel.invokeMethod("onError", listOf("Error downloading anchor, state $cloudState")) return } //Log.d(TAG, "---------------- RESOLVING SUCCESSFUL ------------------") val newAnchorNode = AnchorNode(anchor) // Register new anchor on the Flutter side of the plugin anchorManagerChannel.invokeMethod("onAnchorDownloadSuccess", serializeAnchor(newAnchorNode, anchor), object: MethodChannel.Result { override fun success(result: Any?) { newAnchorNode.name = result.toString() newAnchorNode.setParent(arSceneView.scene) //Log.d(TAG, "---------------- REGISTERING ANCHOR SUCCESSFUL ------------------") } override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { sessionManagerChannel.invokeMethod("onError", listOf("Error while registering downloaded anchor at the AR Flutter plugin: $errorMessage")) } override fun notImplemented() { sessionManagerChannel.invokeMethod("onError", listOf("Error while registering downloaded anchor at the AR Flutter plugin")) } }) } } } ================================================ FILE: android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/AndroidARViewFactory.kt ================================================ package io.carius.lars.ar_flutter_plugin import android.app.Activity import android.content.Context import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.platform.PlatformViewFactory class AndroidARViewFactory(val activity: Activity, val messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { val creationParams = args as Map? return AndroidARView(activity, context!!, messenger, viewId, creationParams) } } ================================================ FILE: android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/ArFlutterPlugin.kt ================================================ package io.carius.lars.ar_flutter_plugin import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.Result /** ArFlutterPlugin */ class ArFlutterPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { /// The MethodChannel that will the communication between Flutter and native Android /// /// This local reference serves to register the plugin with the Flutter Engine and unregister it /// when the Flutter Engine is detached from the Activity private lateinit var channel: MethodChannel private lateinit var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding override fun onAttachedToEngine( @NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding ) { channel = MethodChannel(flutterPluginBinding.binaryMessenger, "ar_flutter_plugin") channel.setMethodCallHandler(this) this.flutterPluginBinding = flutterPluginBinding } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { if (call.method == "getPlatformVersion") { result.success("Android ${android.os.Build.VERSION.RELEASE}") } else { result.notImplemented() } } override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { channel.setMethodCallHandler(null) } override fun onDetachedFromActivity() { channel.setMethodCallHandler(null) } override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { onAttachedToActivity(binding) } override fun onAttachedToActivity(binding: ActivityPluginBinding) { this.flutterPluginBinding.platformViewRegistry.registerViewFactory( "ar_flutter_plugin", AndroidARViewFactory(binding.activity, flutterPluginBinding.binaryMessenger)) } override fun onDetachedFromActivityForConfigChanges() { onDetachedFromActivity() } } ================================================ FILE: android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/ArModelBuilder.kt ================================================ package io.carius.lars.ar_flutter_plugin import android.R import android.app.Activity import android.content.Context import com.google.ar.sceneform.Node import com.google.ar.sceneform.math.Vector3 import com.google.ar.sceneform.math.Quaternion import com.google.ar.sceneform.assets.RenderableSource import java.util.concurrent.CompletableFuture import android.net.Uri import android.view.Gravity import android.widget.Toast import com.google.ar.core.* import com.google.ar.sceneform.ArSceneView import com.google.ar.sceneform.FrameTime import com.google.ar.sceneform.math.MathHelper import com.google.ar.sceneform.rendering.* import com.google.ar.sceneform.utilities.Preconditions import com.google.ar.sceneform.ux.* import io.carius.lars.ar_flutter_plugin.Serialization.* import io.flutter.FlutterInjector import io.flutter.embedding.engine.loader.FlutterLoader import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import java.security.AccessController // Responsible for creating Renderables and Nodes class ArModelBuilder { // Creates feature point node fun makeFeaturePointNode(context: Context, xPos: Float, yPos: Float, zPos: Float): Node { val featurePoint = Node() var cubeRenderable: ModelRenderable? = null MaterialFactory.makeOpaqueWithColor(context, Color(android.graphics.Color.YELLOW)) .thenAccept { material -> val vector3 = Vector3(0.01f, 0.01f, 0.01f) cubeRenderable = ShapeFactory.makeCube(vector3, Vector3(xPos, yPos, zPos), material) cubeRenderable?.isShadowCaster = false cubeRenderable?.isShadowReceiver = false } featurePoint.renderable = cubeRenderable return featurePoint } // Creates a coordinate system model at the world origin (X-axis: red, Y-axis: green, Z-axis:blue) // The code for this function is adapted from Alexander's stackoverflow answer (https://stackoverflow.com/questions/48908358/arcore-how-to-display-world-origin-or-axes-in-debug-mode) fun makeWorldOriginNode(context: Context): Node { val axisSize = 0.1f val axisRadius = 0.005f val rootNode = Node() val xNode = Node() val yNode = Node() val zNode = Node() rootNode.addChild(xNode) rootNode.addChild(yNode) rootNode.addChild(zNode) xNode.worldPosition = Vector3(axisSize / 2, 0f, 0f) xNode.worldRotation = Quaternion.axisAngle(Vector3(0f, 0f, 1f), 90f) yNode.worldPosition = Vector3(0f, axisSize / 2, 0f) zNode.worldPosition = Vector3(0f, 0f, axisSize / 2) zNode.worldRotation = Quaternion.axisAngle(Vector3(1f, 0f, 0f), 90f) MaterialFactory.makeOpaqueWithColor(context, Color(255f, 0f, 0f)) .thenAccept { redMat -> xNode.renderable = ShapeFactory.makeCylinder(axisRadius, axisSize, Vector3.zero(), redMat) } MaterialFactory.makeOpaqueWithColor(context, Color(0f, 255f, 0f)) .thenAccept { greenMat -> yNode.renderable = ShapeFactory.makeCylinder(axisRadius, axisSize, Vector3.zero(), greenMat) } MaterialFactory.makeOpaqueWithColor(context, Color(0f, 0f, 255f)) .thenAccept { blueMat -> zNode.renderable = ShapeFactory.makeCylinder(axisRadius, axisSize, Vector3.zero(), blueMat) } return rootNode } // Creates a node form a given gltf model path or URL. The gltf asset loading in Scenform is asynchronous, so the function returns a completable future of type Node fun makeNodeFromGltf(context: Context, transformationSystem: TransformationSystem, objectManagerChannel: MethodChannel, enablePans: Boolean, enableRotation: Boolean, name: String, modelPath: String, transformation: ArrayList): CompletableFuture { val completableFutureNode: CompletableFuture = CompletableFuture() val gltfNode = CustomTransformableNode(transformationSystem, objectManagerChannel, enablePans, enableRotation) ModelRenderable.builder() .setSource(context, RenderableSource.builder().setSource( context, Uri.parse(modelPath), RenderableSource.SourceType.GLTF2) .build()) .setRegistryId(modelPath) .build() .thenAccept{ renderable -> gltfNode.renderable = renderable gltfNode.name = name val transform = deserializeMatrix4(transformation) gltfNode.worldScale = transform.first gltfNode.worldPosition = transform.second gltfNode.worldRotation = transform.third completableFutureNode.complete(gltfNode) } .exceptionally { throwable -> completableFutureNode.completeExceptionally(throwable) null // return null because java expects void return (in java, void has no instance, whereas in Kotlin, this closure returns a Unit which has one instance) } return completableFutureNode } // Creates a node form a given glb model path or URL. The gltf asset loading in Sceneform is asynchronous, so the function returns a compleatable future of type Node fun makeNodeFromGlb(context: Context, transformationSystem: TransformationSystem, objectManagerChannel: MethodChannel, enablePans: Boolean, enableRotation: Boolean, name: String, modelPath: String, transformation: ArrayList): CompletableFuture { val completableFutureNode: CompletableFuture = CompletableFuture() val gltfNode = CustomTransformableNode(transformationSystem, objectManagerChannel, enablePans, enableRotation) //gltfNode.scaleController.isEnabled = false //gltfNode.translationController.isEnabled = false /*gltfNode.removeTransformationController(translationController) gltfNode.addTra val customTranslationController = DragController( gltfNode, transformationSystem.dragRecognizer, objectManagerChannel, transformationSystem )*/ ModelRenderable.builder() .setSource(context, RenderableSource.builder().setSource( context, Uri.parse(modelPath), RenderableSource.SourceType.GLB) .build()) .setRegistryId(modelPath) .build() .thenAccept{ renderable -> gltfNode.renderable = renderable gltfNode.name = name val transform = deserializeMatrix4(transformation) gltfNode.worldScale = transform.first gltfNode.worldPosition = transform.second gltfNode.worldRotation = transform.third completableFutureNode.complete(gltfNode) } .exceptionally{throwable -> completableFutureNode.completeExceptionally(throwable) null // return null because java expects void return (in java, void has no instance, whereas in Kotlin, this closure returns a Unit which has one instance) } return completableFutureNode } } class CustomTransformableNode(transformationSystem: TransformationSystem, objectManagerChannel: MethodChannel, enablePans: Boolean, enableRotation: Boolean) : TransformableNode(transformationSystem) { // private lateinit var customTranslationController: CustomTranslationController private lateinit var customRotationController: CustomRotationController init { // Remove standard controllers translationController.isEnabled = false rotationController.isEnabled = false scaleController.isEnabled = false removeTransformationController(translationController) removeTransformationController(rotationController) removeTransformationController(scaleController) // Add custom controllers if needed if (enablePans) { customTranslationController = CustomTranslationController( this, transformationSystem.dragRecognizer, objectManagerChannel ) addTransformationController(customTranslationController) } if (enableRotation) { customRotationController = CustomRotationController( this, transformationSystem.twistRecognizer, objectManagerChannel ) addTransformationController(customRotationController) } } } class CustomTranslationController(transformableNode: BaseTransformableNode, gestureRecognizer: DragGestureRecognizer, objectManagerChannel: MethodChannel) : TranslationController(transformableNode, gestureRecognizer) { val platformChannel: MethodChannel = objectManagerChannel override fun canStartTransformation(gesture: DragGesture): Boolean { platformChannel.invokeMethod("onPanStart", transformableNode.name) super.canStartTransformation(gesture) return transformableNode.isSelected } override fun onContinueTransformation(gesture: DragGesture) { platformChannel.invokeMethod("onPanChange", transformableNode.name) super.onContinueTransformation(gesture) } override fun onEndTransformation(gesture: DragGesture) { val serializedLocalTransformation = serializeLocalTransformation(transformableNode) platformChannel.invokeMethod("onPanEnd", serializedLocalTransformation) super.onEndTransformation(gesture) } } class CustomRotationController(transformableNode: BaseTransformableNode, gestureRecognizer: TwistGestureRecognizer, objectManagerChannel: MethodChannel) : RotationController(transformableNode, gestureRecognizer) { val platformChannel: MethodChannel = objectManagerChannel override fun canStartTransformation(gesture: TwistGesture): Boolean { platformChannel.invokeMethod("onRotationStart", transformableNode.name) super.canStartTransformation(gesture) return transformableNode.isSelected } override fun onContinueTransformation(gesture: TwistGesture) { platformChannel.invokeMethod("onRotationChange", transformableNode.name) super.onContinueTransformation(gesture) } override fun onEndTransformation(gesture: TwistGesture) { val serializedLocalTransformation = serializeLocalTransformation(transformableNode) platformChannel.invokeMethod("onRotationEnd", serializedLocalTransformation) super.onEndTransformation(gesture) } } ================================================ FILE: android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/CloudAnchorHandler.kt ================================================ package io.carius.lars.ar_flutter_plugin import com.google.ar.core.Anchor import com.google.ar.core.Anchor.CloudAnchorState import com.google.ar.core.Session import java.util.* // Class for handling logic regarding the Google Cloud Anchor API internal class CloudAnchorHandler( arSession: Session ) { // Listener that can be attached to hosing or resolving processes interface CloudAnchorListener { // Callback to invoke when cloud anchor task finishes fun onCloudTaskComplete(anchorName: String?, anchor: Anchor?) } private val TAG: String = CloudAnchorHandler::class.java.simpleName private val pendingAnchors = HashMap>() private val session: Session = arSession @Synchronized fun hostCloudAnchor(anchorName: String, anchor: Anchor?, listener: CloudAnchorListener?) { val newAnchor = session.hostCloudAnchor(anchor) // Register listener so it is invoked when the operation finishes pendingAnchors[newAnchor] = Pair(anchorName, listener) } @Synchronized fun hostCloudAnchorWithTtl(anchorName: String, anchor: Anchor?, listener: CloudAnchorListener?, ttl: Int) { val newAnchor = session.hostCloudAnchorWithTtl(anchor, ttl) // Register listener so it is invoked when the operation finishes pendingAnchors[newAnchor] = Pair(anchorName, listener) } @Synchronized fun resolveCloudAnchor(anchorId: String?, listener: CloudAnchorListener?) { val newAnchor = session.resolveCloudAnchor(anchorId) // Register listener so it is invoked when the operation finishes pendingAnchors[newAnchor] = Pair(null, listener) } // Updating function that should be called after each session.update call @Synchronized fun onUpdate(updatedAnchors: Collection) { for (anchor in updatedAnchors) { if (pendingAnchors.containsKey(anchor)) { if (anchor.cloudAnchorState != CloudAnchorState.NONE && anchor.cloudAnchorState != CloudAnchorState.TASK_IN_PROGRESS){ val element: Pair? = pendingAnchors.remove(anchor) element!!.second!!.onCloudTaskComplete(element.first, anchor) } } } } // Remove all listeners @Synchronized fun clearListeners() { pendingAnchors.clear() } } ================================================ FILE: android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/Serialization/Deserializers.kt ================================================ package io.carius.lars.ar_flutter_plugin.Serialization import com.google.ar.sceneform.math.Quaternion import com.google.ar.sceneform.math.Vector3 fun deserializeMatrix4(transform: ArrayList): Triple { val scale = Vector3() val position = Vector3() val rotation: Quaternion // Get the scale by calculating the length of each 3-dimensional column vector of the // transformation matrix // See // https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati for a mathematical explanation scale.x = Vector3(transform[0].toFloat(), transform[1].toFloat(), transform[2].toFloat()).length() scale.y = Vector3(transform[4].toFloat(), transform[5].toFloat(), transform[6].toFloat()).length() scale.z = Vector3(transform[8].toFloat(), transform[9].toFloat(), transform[10].toFloat()).length() // Get the translation by taking the last column of the transformation matrix // See // https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati for a mathematical explanation position.x = transform[12].toFloat() position.y = transform[13].toFloat() position.z = transform[14].toFloat() // Get the rotation matrix from the transformation matrix by normalizing with the scales // See // https://math.stackexchange.com/questions/237369/given-this-transformation-matrix-how-do-i-decompose-it-into-translation-rotati for a mathematical explanation val rowWiseMatrix = floatArrayOf( transform[0].toFloat() / scale.x, transform[4].toFloat() / scale.y, transform[8].toFloat() / scale.z, transform[1].toFloat() / scale.x, transform[5].toFloat() / scale.y, transform[9].toFloat() / scale.z, transform[2].toFloat() / scale.x, transform[6].toFloat() / scale.y, transform[10].toFloat() / scale.z) // Calculate the quaternion from the rotation matrix // See https://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToQuaternion/ for // a mathematical explanation val trace = rowWiseMatrix[0] + rowWiseMatrix[4] + rowWiseMatrix[8] var w = 0.0 var x = 0.0 var y = 0.0 var z = 0.0 if (trace > 0) { val scalefactor = Math.sqrt(trace + 1.0) * 2 w = 0.25 * scalefactor x = (rowWiseMatrix[7] - rowWiseMatrix[5]) / scalefactor y = (rowWiseMatrix[2] - rowWiseMatrix[6]) / scalefactor z = (rowWiseMatrix[3] - rowWiseMatrix[1]) / scalefactor } else if ((rowWiseMatrix[0] > rowWiseMatrix[4]) && (rowWiseMatrix[0] > rowWiseMatrix[8])) { val scalefactor = Math.sqrt(1.0 + rowWiseMatrix[0] - rowWiseMatrix[4] - rowWiseMatrix[8]) * 2 w = (rowWiseMatrix[7] - rowWiseMatrix[5]) / scalefactor x = 0.25 * scalefactor y = (rowWiseMatrix[1] + rowWiseMatrix[3]) / scalefactor z = (rowWiseMatrix[2] + rowWiseMatrix[6]) / scalefactor } else if (rowWiseMatrix[4] > rowWiseMatrix[8]) { val scalefactor = Math.sqrt(1.0 + rowWiseMatrix[4] - rowWiseMatrix[0] - rowWiseMatrix[8]) * 2 w = (rowWiseMatrix[2] - rowWiseMatrix[6]) / scalefactor x = (rowWiseMatrix[1] + rowWiseMatrix[3]) / scalefactor y = 0.25 * scalefactor z = (rowWiseMatrix[5] + rowWiseMatrix[7]) / scalefactor } else { val scalefactor = Math.sqrt(1.0 + rowWiseMatrix[8] - rowWiseMatrix[0] - rowWiseMatrix[4]) * 2 w = (rowWiseMatrix[3] - rowWiseMatrix[1]) / scalefactor x = (rowWiseMatrix[2] + rowWiseMatrix[6]) / scalefactor y = (rowWiseMatrix[5] + rowWiseMatrix[7]) / scalefactor z = 0.25 * scalefactor } val inputRotation = Quaternion(x.toFloat(), y.toFloat(), z.toFloat(), w.toFloat()) // Rotate by an additional 180 degrees around z and y to compensate for the different model // coordinate system definition used in Sceneform (in comparison to Scenekit and the definition // used for the Flutter API of this plugin) val correction_z = Quaternion(0.0f, 0.0f, 1.0f, 180f) val correction_y = Quaternion(0.0f, 1.0f, 0.0f, 180f) // Calculate resulting rotation quaternion by multiplying input and corrections rotation = Quaternion.multiply(Quaternion.multiply(inputRotation, correction_y), correction_z) return Triple(scale, position, rotation) } ================================================ FILE: android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/Serialization/Serializers.kt ================================================ package io.carius.lars.ar_flutter_plugin.Serialization import com.google.ar.core.* import com.google.ar.sceneform.AnchorNode import com.google.ar.sceneform.math.Matrix import com.google.ar.sceneform.math.Quaternion import com.google.ar.sceneform.math.Vector3 import com.google.ar.sceneform.ux.BaseTransformableNode import com.google.ar.sceneform.ux.TransformableNode fun serializeHitResult(hitResult: HitResult): HashMap { val serializedHitResult = HashMap() if (hitResult.trackable is Plane && (hitResult.trackable as Plane).isPoseInPolygon(hitResult.hitPose)) { serializedHitResult["type"] = 1 // Type plane } else if (hitResult.trackable is Point){ serializedHitResult["type"] = 2 // Type point } else { serializedHitResult["type"] = 0 // Type undefined } serializedHitResult["distance"] = hitResult.distance.toDouble() serializedHitResult["worldTransform"] = serializePose(hitResult.hitPose) return serializedHitResult } fun serializePose(pose: Pose): DoubleArray { val serializedPose = FloatArray(16) pose.toMatrix(serializedPose, 0) // copy into double Array val serializedPoseDouble = DoubleArray(serializedPose.size) for (i in serializedPose.indices) { serializedPoseDouble[i] = serializedPose[i].toDouble() } return serializedPoseDouble } fun serializePoseWithScale(pose: Pose, scale: Vector3): DoubleArray { val serializedPose = FloatArray(16) pose.toMatrix(serializedPose, 0) // copy into double Array val serializedPoseDouble = DoubleArray(serializedPose.size) for (i in serializedPose.indices) { serializedPoseDouble[i] = serializedPose[i].toDouble() if (i == 0 || i == 4 || i == 8){ serializedPoseDouble[i] = serializedPoseDouble[i] * scale.x } if (i == 1 || i == 5 || i == 9){ serializedPoseDouble[i] = serializedPoseDouble[i] * scale.y } if (i == 2 || i == 7 || i == 10){ serializedPoseDouble[i] = serializedPoseDouble[i] * scale.z } } return serializedPoseDouble } fun serializeAnchor(anchorNode: AnchorNode, anchor: Anchor?): HashMap { val serializedAnchor = HashMap() serializedAnchor["type"] = 0 // index for plane anchors serializedAnchor["name"] = anchorNode.name serializedAnchor["cloudanchorid"] = anchor?.cloudAnchorId serializedAnchor["transformation"] = if (anchor != null) serializePose(anchor.pose) else null serializedAnchor["childNodes"] = anchorNode.children.map { child -> child.name } return serializedAnchor } fun serializeLocalTransformation(node: BaseTransformableNode): HashMap{ val serializedLocalTransformation = HashMap() serializedLocalTransformation["name"] = node.name val transform = Pose(floatArrayOf(node.localPosition.x, node.localPosition.y, node.localPosition.z), floatArrayOf(node.localRotation.x, node.localRotation.y, node.localRotation.z, node.localRotation.w)) serializedLocalTransformation["transform"] = serializePoseWithScale(transform, node.localScale) return serializedLocalTransformation } ================================================ FILE: cloudAnchorSetup.md ================================================ # Getting Started with Cloud Anchors The usual method of authentication is setting an API key. However, this authentication method only allows to host anchors with a maximum lifetime of 24 hours. OAuth 2.0 authentication allows saving uploaded anchors for up to 365 days and needs to be set up to use the plugin. Follow the steps below to set up your application. ## Set up Google Cloud Anchor Service The Google Cloud Anchor API is used by the plugin to upload, store and download AR anchors. If your app uses the plugin's shared AR experience features, the following setup steps are required: 1. Activate the [Cloud Anchor API](https://console.cloud.google.com/apis/api/arcorecloudanchor.googleapis.com) in your [Google Cloud Console](https://console.cloud.google.com) for the respective project 2. Register the Android part of your Flutter Application * Perform the following steps to create a OAuth2 project (based on the [Android Cloud Anchors Developer Guide](https://developers.google.com/ar/develop/java/cloud-anchors/developer-guide-android?hl=en) and the [Guide for setting up OAuth 2.0](https://support.google.com/cloud/answer/6158849#zippy=)): * Go to the [Google Cloud Platform Console](https://console.cloud.google.com). * If the APIs & services page isn't already open, open the console left side menu and select APIs & services. * On the left, click Credentials. * Click New Credentials, then select OAuth client ID. * Select "Android" as the Application type * Fill in an arbitrary name and make sure the field "Package name" matches the package name in the ```AndroidManifest.xml``` of the Android part of your Flutter application * Fill in the SHA-1 certificate fingerprint. If you're still in development, you can get the debug keystore key for the Android app by executing ```keytool -keystore ~/.android/debug.keystore -list -v``` in your terminal * Click Create client ID * If this is your first time creating a client ID, you have to configure your consent screen by clicking Consent Screen. The following procedure explains how to set up the Consent screen. You won't be prompted to configure the consent screen after you do it the first time. * Go to the Google API Console [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) page. * Add required information like a product name and support email address. * Click Add Scope. * On the dialog that appears, select the ```ARCore Cloud Anchor API```scopes and any additional ones your project uses. Sensitive scopes display a lock icon next to the API name. (To select scopes for registration, you need to enable the API, like Drive or Gmail, from APIs & Services > API Library. You must select all scopes used by the project.) * Finish the remaining steps of the OAuth consent screen setup. * Enable keyless authentication in the Android part of your Flutter app (if you use the sample app of this plugin as a starting point, these steps are already done; the following steps are based on the [Android Cloud Anchors Developer Guide](https://developers.google.com/ar/develop/java/cloud-anchors/developer-guide-android?hl=en)): * Add the dependency ```implementation 'com.google.android.gms:play-services-auth:16+'``` to the ```build.gradle``` file * If you are using [ProGuard](https://www.guardsquare.com/en/products/proguard), add it to your app’s build.gradle file with ```java buildTypes { release { ... proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } ``` * And add the following to your app’s proguard-rules.pro file: ```java -keep class com.google.android.gms.common.** { *; } -keep class com.google.android.gms.auth.** { *; } -keep class com.google.android.gms.tasks.** { *; } ``` 3. Register the iOS part of your Flutter Application * Perform the following steps to create a Google Service account and signing key (based on the [iOS Cloud Anchor Developer Guide](https://developers.google.com/ar/develop/ios/cloud-anchors/developer-guide?hl=en)): * In the navigation menu of the Google Cloud Platform console, go to APIs & Services > Credentials. * Select the desired project, then click Create Credentials > Service account. * Under Service account details, type a name for the new account, then click Create. * On the Service account permissions page, go to the Select a role dropdown. Select Service Accounts > Service Account Token Creator, then click Continue. * On the Grant users access to this service account page, click Done. This takes you back to APIs & Services > Credentials. * On the Credentials page, scroll down to the Service Accounts section and click the name of the account you just created. * On the Service account details page, scroll down to the Keys section and select Add Key > Create new key. * Select JSON as the key type and click Create. This downloads a JSON file containing the private key to your machine. * Add the contents of the JSON file you just downloaded to the iOS part of your Flutter application: * Rename the file to ```cloudAnchorKey.json``` * Move or copy ```cloudAnchorKey.json``` into the example/ios/Runner directory. * Open Xcode, then right-click on Runner directory and select Add Files to "Runner". * Select ```cloudAnchorKey.json``` from the file manager. * A dialog will show up and ask you to select the targets, select the Runner target. ## Set up Firebase Google's Firebase cloud platform is used by the plugin's sample app to distribute and manage shared anchors and related content. If you want to use the included examples with shared AR experience features (e.g. the ```Cloud Anchors```example), the following setup steps are required (in your own apps, you can implement any method you like to distribute and manage the cloud anchor IDs that the plugin returns after uploading an anchor): 1. Create a new project in the [Firebase console](https://console.firebase.google.com/project/_/overview) 2. Register the Android part of your Flutter Application (based on the [FlutterFire Android Installation Guide](https://firebase.flutter.dev/docs/installation/android/)): * Add a new Android app to your project and make sure the ```Android package name``` matches your local project's package name which can be found within the ```AndroidManifest.xml``` * Fill in the debug signing certificate SHA-1 field. If you're still in development, you can get the debug keystore key for the Android app by executing ```keytool -keystore ~/.android/debug.keystore -list -v``` in your terminal * Once your Android app has been registered, download the configuration file from the Firebase Console (the file is called ```google-services.json```). Add this file into the ```android/app``` directory within your Flutter project * Add the dependency ```classpath 'com.google.gms:google-services:4.3.3'``` to the top-level ```build.gradle```file of the Android part of your Flutter application * Add ```apply plugin: 'com.google.gms.google-services'``` to the app-level ```build.gradle```file of the Android part of your Flutter application 3. Register the iOS part of your Flutter Application (based on the [FlutterFire iOS Installation Guide](https://firebase.flutter.dev/docs/installation/ios/)): * Add a new iOS app to your project and make sure the ```iOS bundle ID``` matches your local project bundle ID which can be found within the "General" tab when opening ```ios/Runner.xcworkspace``` with Xcode. * Download the file ```GoogleService-Info.plist``` * Move or copy ``` GoogleService-Info.plist``` into the example/ios/Runner directory. * Open Xcode, then right-click on Runner directory and select Add Files to "Runner". * Select ``` GoogleService-Info.plist``` from the file manager. * A dialog will show up and ask you to select the targets, select the Runner target. 4. Enable Cloud Firestore for the project you created in step 1 (head to https://console.firebase.google.com/project/INSERT_YOUR_FIREBASE_PROJECT_NAME_HERE/firestore) ## Set up Location Services * On the iOS part of your app, add the following to your Info.plist file (located under ios/Runner) in order to access the device's location: ``` NSLocationWhenInUseUsageDescription This app needs access to location when open. NSLocationAlwaysUsageDescription This app needs access to location when in the background. ``` ================================================ FILE: example/.gitignore ================================================ # Miscellaneous *.class *.log *.pyc *.swp .DS_Store .atom/ .buildlog/ .history .svn/ # IntelliJ related *.iml *.ipr *.iws .idea/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. #.vscode/ # Flutter/Dart/Pub related **/doc/api/ **/ios/Flutter/.last_build_id .dart_tool/ .flutter-plugins .flutter-plugins-dependencies .packages .pub-cache/ .pub/ /build/ # Web related lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols # Obfuscation related app.*.map.json # Android Studio will place build artifacts here /android/app/debug /android/app/profile /android/app/release ================================================ FILE: example/.metadata ================================================ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # # This file should be version controlled and should not be manually edited. version: revision: 4b50ca7f7fbf56be72e54cd200825b760416a356 channel: beta project_type: app ================================================ FILE: example/Models/Chicken_01/Chicken_01.gltf ================================================ { "accessors" : [ { "bufferView" : 0, "byteOffset" : 0, "componentType" : 5123, "count" : 558, "max" : [ 557 ], "min" : [ 0 ], "name" : "buffer-0-accessor-indices-buffer-0-mesh-0", "type" : "SCALAR" }, { "bufferView" : 2, "byteOffset" : 0, "componentType" : 5126, "count" : 558, "max" : [ 3.636018991470337, 152.5314636230469, 49.29042816162109 ], "min" : [ -87.20170593261719, 0, -49.29042816162109 ], "name" : "buffer-0-accessor-position-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 2, "byteOffset" : 6696, "componentType" : 5126, "count" : 558, "max" : [ 0.9146999716758728, 0.9926000237464905, 0.9937000274658203 ], "min" : [ -0.9735000133514404, -1, -0.9937000274658203 ], "name" : "buffer-0-accessor-normal-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 1, "byteOffset" : 0, "componentType" : 5126, "count" : 558, "max" : [ 1, 0 ], "min" : [ 0, -1 ], "name" : "buffer-0-accessor-texcoord-buffer-0-mesh-0", "type" : "VEC2" }, { "bufferView" : 3, "byteOffset" : 0, "componentType" : 5126, "count" : 0, "max" : [ 0, 0, 0, 0 ], "min" : [ 0, 0, 0, 0 ], "name" : "buffer-0-accessor-color-buffer-0-mesh-0", "type" : "VEC4" }, { "bufferView" : 0, "byteOffset" : 1116, "componentType" : 5123, "count" : 414, "max" : [ 413 ], "min" : [ 0 ], "name" : "buffer-0-accessor-indices-buffer-0-mesh-0", "type" : "SCALAR" }, { "bufferView" : 2, "byteOffset" : 13392, "componentType" : 5126, "count" : 414, "max" : [ 80.11344909667969, 176.5484161376953, 40.12937164306641 ], "min" : [ -75.36231994628906, 0, -40.12934494018555 ], "name" : "buffer-0-accessor-position-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 2, "byteOffset" : 18360, "componentType" : 5126, "count" : 414, "max" : [ 0.9840999841690063, 0.9950000047683716, 1 ], "min" : [ -0.942300021648407, -0.9502999782562256, -1 ], "name" : "buffer-0-accessor-normal-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 1, "byteOffset" : 4464, "componentType" : 5126, "count" : 414, "max" : [ 1, 0 ], "min" : [ 0, -1 ], "name" : "buffer-0-accessor-texcoord-buffer-0-mesh-0", "type" : "VEC2" }, { "bufferView" : 3, "byteOffset" : 0, "componentType" : 5126, "count" : 0, "max" : [ 0, 0, 0, 0 ], "min" : [ 0, 0, 0, 0 ], "name" : "buffer-0-accessor-color-buffer-0-mesh-0", "type" : "VEC4" }, { "bufferView" : 0, "byteOffset" : 1944, "componentType" : 5123, "count" : 144, "max" : [ 143 ], "min" : [ 0 ], "name" : "buffer-0-accessor-indices-buffer-0-mesh-0", "type" : "SCALAR" }, { "bufferView" : 2, "byteOffset" : 23328, "componentType" : 5126, "count" : 144, "max" : [ 0, 156.3618316650391, 14.85952568054199 ], "min" : [ -65.07901763916016, 0, -14.85947513580322 ], "name" : "buffer-0-accessor-position-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 2, "byteOffset" : 25056, "componentType" : 5126, "count" : 144, "max" : [ 0.9239000082015991, 0.9239000082015991, 1 ], "min" : [ -0.9239000082015991, -0.9239000082015991, -1 ], "name" : "buffer-0-accessor-normal-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 1, "byteOffset" : 7776, "componentType" : 5126, "count" : 144, "max" : [ 1, 0 ], "min" : [ 0, -1 ], "name" : "buffer-0-accessor-texcoord-buffer-0-mesh-0", "type" : "VEC2" }, { "bufferView" : 3, "byteOffset" : 0, "componentType" : 5126, "count" : 0, "max" : [ 0, 0, 0, 0 ], "min" : [ 0, 0, 0, 0 ], "name" : "buffer-0-accessor-color-buffer-0-mesh-0", "type" : "VEC4" }, { "bufferView" : 0, "byteOffset" : 2232, "componentType" : 5123, "count" : 504, "max" : [ 503 ], "min" : [ 0 ], "name" : "buffer-0-accessor-indices-buffer-0-mesh-0", "type" : "SCALAR" }, { "bufferView" : 2, "byteOffset" : 26784, "componentType" : 5126, "count" : 504, "max" : [ 0, 190.3115692138672, 10.57294273376465 ], "min" : [ -84.04743957519531, 0, -10.57290267944336 ], "name" : "buffer-0-accessor-position-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 2, "byteOffset" : 32832, "componentType" : 5126, "count" : 504, "max" : [ 0.9994000196456909, 0.998199999332428, 0.9961000084877014 ], "min" : [ -1, -0.9664000272750854, -0.9973000288009644 ], "name" : "buffer-0-accessor-normal-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 1, "byteOffset" : 8928, "componentType" : 5126, "count" : 504, "max" : [ 1, 0 ], "min" : [ 0, -1 ], "name" : "buffer-0-accessor-texcoord-buffer-0-mesh-0", "type" : "VEC2" }, { "bufferView" : 3, "byteOffset" : 0, "componentType" : 5126, "count" : 0, "max" : [ 0, 0, 0, 0 ], "min" : [ 0, 0, 0, 0 ], "name" : "buffer-0-accessor-color-buffer-0-mesh-0", "type" : "VEC4" }, { "bufferView" : 0, "byteOffset" : 3240, "componentType" : 5123, "count" : 324, "max" : [ 323 ], "min" : [ 0 ], "name" : "buffer-0-accessor-indices-buffer-0-mesh-0", "type" : "SCALAR" }, { "bufferView" : 2, "byteOffset" : 38880, "componentType" : 5126, "count" : 324, "max" : [ 6.280783176422119, 4.014040946960449, 51.71989440917969 ], "min" : [ -26.94760704040527, 0, -51.71989440917969 ], "name" : "buffer-0-accessor-position-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 2, "byteOffset" : 42768, "componentType" : 5126, "count" : 324, "max" : [ 0.9013000130653381, 0.9991000294685364, 0.9283000230789185 ], "min" : [ -0.9253000020980835, -0.9991000294685364, -0.9283000230789185 ], "name" : "buffer-0-accessor-normal-buffer-0-mesh-0", "type" : "VEC3" }, { "bufferView" : 1, "byteOffset" : 12960, "componentType" : 5126, "count" : 324, "max" : [ 1, 0 ], "min" : [ 0, -1 ], "name" : "buffer-0-accessor-texcoord-buffer-0-mesh-0", "type" : "VEC2" }, { "bufferView" : 3, "byteOffset" : 0, "componentType" : 5126, "count" : 0, "max" : [ 0, 0, 0, 0 ], "min" : [ 0, 0, 0, 0 ], "name" : "buffer-0-accessor-color-buffer-0-mesh-0", "type" : "VEC4" } ], "asset" : { "extras" : { "GOOGLE_processor_version" : 293297437 }, "generator" : "Obj2GltfConverter", "version" : "2.0" }, "bufferViews" : [ { "buffer" : 0, "byteLength" : 3888, "byteOffset" : 0, "byteStride" : 0, "name" : "buffer-0-bufferview-ushort", "target" : 34963 }, { "buffer" : 0, "byteLength" : 15552, "byteOffset" : 3888, "byteStride" : 8, "name" : "buffer-0-bufferview-vec2", "target" : 34962 }, { "buffer" : 0, "byteLength" : 46656, "byteOffset" : 19440, "byteStride" : 12, "name" : "buffer-0-bufferview-vec3", "target" : 34962 }, { "buffer" : 0, "byteLength" : 1, "byteOffset" : 0, "name" : "buffer-0-bufferview-vec4" } ], "buffers" : [ { "byteLength" : 66096, "name" : "buffer-0", "uri" : "Chicken_01.bin" } ], "cameras" : [ { "perspective" : { "yfov" : 0.7853981633974483, "znear" : 0.1000000014901161 }, "type" : "perspective" } ], "extensions" : { "KHR_lights_punctual" : { "lights" : [ { "color" : [ 1, 0.9333333333333333, 0.8666666666666667 ], "intensity" : 0.25, "name" : "headLight", "type" : "directional" }, { "color" : [ 1, 0.9333333333333333, 0.8666666666666667 ], "intensity" : 0.324999988079071, "name" : "keyLight", "type" : "directional" } ] } }, "extensionsUsed" : [ "GOOGLE_backgrounds", "GOOGLE_camera_settings", "KHR_lights_punctual" ], "extras" : { "GOOGLE_initial_camera_motion" : { "motionPath" : "FULL_ROTATION" }, "GOOGLE_lighting_rig" : { "disableShadows" : false } }, "materials" : [ { "alphaMode" : "OPAQUE", "doubleSided" : true, "extras" : { "__iag_" : true }, "name" : "FF9800", "pbrMetallicRoughness" : { "baseColorFactor" : [ 1, 0.355308982084, 0, 1 ], "metallicFactor" : 0, "roughnessFactor" : 0.7493216756721951 } }, { "alphaMode" : "OPAQUE", "doubleSided" : true, "extras" : { "__iag_" : true }, "name" : "FFFFFF", "pbrMetallicRoughness" : { "baseColorFactor" : [ 1, 1, 1, 1 ], "metallicFactor" : 0, "roughnessFactor" : 0.7493216756721951 } }, { "alphaMode" : "OPAQUE", "doubleSided" : true, "extras" : { "__iag_" : true }, "name" : "1A1A1A", "pbrMetallicRoughness" : { "baseColorFactor" : [ 0.010396045521, 0.010396045521, 0.010396045521, 1 ], "metallicFactor" : 0, "roughnessFactor" : 0.7493216756721951 } }, { "alphaMode" : "OPAQUE", "doubleSided" : true, "extras" : { "__iag_" : true }, "name" : "F44336", "pbrMetallicRoughness" : { "baseColorFactor" : [ 0.915586800769, 0.06903493502500001, 0.044844415225, 1 ], "metallicFactor" : 0, "roughnessFactor" : 0.7493216756721951 } }, { "alphaMode" : "OPAQUE", "doubleSided" : true, "extras" : { "__iag_" : true }, "name" : "455A64", "pbrMetallicRoughness" : { "baseColorFactor" : [ 0.07321786574399999, 0.124567349481, 0.153787112649, 1 ], "metallicFactor" : 0, "roughnessFactor" : 0.7493216756721951 } } ], "meshes" : [ { "name" : "buffer-0-mesh-0", "primitives" : [ { "attributes" : { "NORMAL" : 2, "POSITION" : 1, "TEXCOORD_0" : 3 }, "indices" : 0, "material" : 0, "mode" : 4 }, { "attributes" : { "NORMAL" : 7, "POSITION" : 6, "TEXCOORD_0" : 8 }, "indices" : 5, "material" : 1, "mode" : 4 }, { "attributes" : { "NORMAL" : 12, "POSITION" : 11, "TEXCOORD_0" : 13 }, "indices" : 10, "material" : 2, "mode" : 4 }, { "attributes" : { "NORMAL" : 17, "POSITION" : 16, "TEXCOORD_0" : 18 }, "indices" : 15, "material" : 3, "mode" : 4 }, { "attributes" : { "NORMAL" : 22, "POSITION" : 21, "TEXCOORD_0" : 23 }, "indices" : 20, "material" : 4, "mode" : 4 } ] } ], "nodes" : [ { "mesh" : 0, "name" : "node-0" }, { "children" : [ 2 ] }, { "children" : [ 3 ], "extras" : { "isTransformNode" : 1 }, "translation" : [ -10.33925953243352, 101.4766894350761, 0.4725487306714168 ] }, { "children" : [ 0 ], "translation" : [ 10.33925953243352, -101.4766894350761, -0.4725487306714168 ] }, { "camera" : 0, "children" : [ 5 ], "rotation" : [ 0.03161089902743668, 0.9405290359497446, 0.091286676120003, -0.3256879278712723 ], "translation" : [ -209.7092493748044, 164.720177932136, -253.0129743638226 ] }, { "extensions" : { "KHR_lights_punctual" : { "light" : 0 } }, "extras" : { "isHeadLight" : 1 }, "name" : "headLightNode", "translation" : [ -137.6435827985261, 68.82179139926303, 68.82179139926303 ] }, { "extensions" : { "KHR_lights_punctual" : { "light" : 1 } }, "extras" : { "isKeyLight" : 1 }, "name" : "keyLightNode", "rotation" : [ -0.7505326882858236, -0.3752663441429118, 0, 0.5439447166468927 ], "scale" : [ 0, 0, 139.0200186265113 ], "translation" : [ -169.8998424664395, 356.8894572597737, -139.0250173948788 ] } ], "scene" : 0, "scenes" : [ { "extensions" : { "GOOGLE_backgrounds" : { "color" : [ 0.803921568627451, 0.8627450980392157, 0.2235294117647059 ] }, "GOOGLE_camera_settings" : {} }, "extras" : { "GOOGLE_camera_index" : { "cameraIndex" : 0, "nodeIndex" : 4 }, "GOOGLE_geometry_data" : { "stats" : { "centroid" : [ -30.87982383992815, 78.84942000675105, -0.004998768367502245 ], "radius" : 137.6435827985261, "stdev" : 86.13574977146493 }, "visualCenterPoint" : [ -10.33925953243352, 101.4766894350761, 0.4725487306714168 ] }, "GOOGLE_hemi_light" : { "groundColor" : [ 0.803921568627451, 0.8627450980392157, 0.2235294117647059 ] } }, "name" : "scene-0", "nodes" : [ 1, 4, 6 ] } ] } ================================================ FILE: example/Models/Chicken_01/license.txt ================================================ Licensed under CC-BY 3.0 by "Poly by Google" (https://poly.google.com/user/4aEd8rQgKu2) ================================================ FILE: example/README.md ================================================ # ar_flutter_plugin_example Demonstrates how to use the ar_flutter_plugin plugin to create collaborative AR experiences. ## Contributing Contributions are very welcome. To contribute code and discuss ideas, [create a pull request](https://github.com/CariusLars/ar_flutter_plugin/compare) or [open an issue](https://github.com/CariusLars/ar_flutter_plugin/issues/new). ================================================ FILE: example/android/.gitignore ================================================ gradle-wrapper.jar /.gradle /captures/ /gradlew /gradlew.bat /local.properties GeneratedPluginRegistrant.java # Remember to never publicly share your keystore. # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app key.properties ================================================ FILE: example/android/app/build.gradle ================================================ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.carius.lars.ar_flutter_plugin_example" minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } buildTypes { debug { signingConfig signingConfigs.debug } release { signingConfig signingConfigs.debug } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } apply plugin: 'com.google.gms.google-services' ================================================ FILE: example/android/app/src/debug/AndroidManifest.xml ================================================ ================================================ FILE: example/android/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: example/android/app/src/main/kotlin/io/carius/lars/ar_flutter_plugin_example/MainActivity.kt ================================================ package io.carius.lars.ar_flutter_plugin_example import io.flutter.embedding.android.FlutterActivity class MainActivity: FlutterActivity() { } ================================================ FILE: example/android/app/src/main/res/drawable/launch_background.xml ================================================ ================================================ FILE: example/android/app/src/main/res/drawable-v21/launch_background.xml ================================================ ================================================ FILE: example/android/app/src/main/res/values/styles.xml ================================================ ================================================ FILE: example/android/app/src/main/res/values-night/styles.xml ================================================ ================================================ FILE: example/android/app/src/profile/AndroidManifest.xml ================================================ ================================================ FILE: example/android/build.gradle ================================================ buildscript { ext.kotlin_version = '1.6.10' repositories { google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:7.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.8' } } allprojects { repositories { google() jcenter() } } rootProject.buildDir = '../build' subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { project.evaluationDependsOn(':app') } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: example/android/gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip ================================================ FILE: example/android/gradle.properties ================================================ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true ================================================ FILE: example/android/settings.gradle ================================================ include ':app' def localPropertiesFile = new File(rootProject.projectDir, "local.properties") def properties = new Properties() assert localPropertiesFile.exists() localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } def flutterSdkPath = properties.getProperty("flutter.sdk") assert flutterSdkPath != null, "flutter.sdk not set in local.properties" apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" ================================================ FILE: example/ios/.gitignore ================================================ *.mode1v3 *.mode2v3 *.moved-aside *.pbxuser *.perspectivev3 **/*sync/ .sconsign.dblite .tags* **/.vagrant/ **/DerivedData/ Icon? **/Pods/ **/.symlinks/ profile xcuserdata **/.generated/ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ Flutter/flutter_export_environment.sh ServiceDefinitions.json Runner/GeneratedPluginRegistrant.* # Exceptions to above rules. !default.mode1v3 !default.mode2v3 !default.pbxuser !default.perspectivev3 ================================================ FILE: example/ios/Flutter/AppFrameworkInfo.plist ================================================ CFBundleDevelopmentRegion en CFBundleExecutable App CFBundleIdentifier io.flutter.flutter.app CFBundleInfoDictionaryVersion 6.0 CFBundleName App CFBundlePackageType FMWK CFBundleShortVersionString 1.0 CFBundleSignature ???? CFBundleVersion 1.0 MinimumOSVersion 13.0 ================================================ FILE: example/ios/Flutter/Debug.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" ================================================ FILE: example/ios/Flutter/Release.xcconfig ================================================ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" ================================================ FILE: example/ios/Podfile ================================================ # Uncomment this line to define a global platform for your project # platform :ios, '9.0' # Override firebase SDK version for compatibility reasons $FirebaseSDKVersion = '8.7.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' project 'Runner', { 'Debug' => :debug, 'Profile' => :release, 'Release' => :release, } def flutter_root generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist?(generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" end require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '8.7.0' # This line improves build times (see https://firebase.flutter.dev/docs/overview/) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ '$(inherited)', ## dart: PermissionGroup.camera 'PERMISSION_CAMERA=1', ## dart: PermissionGroup.photos 'PERMISSION_PHOTOS=1', ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] 'PERMISSION_LOCATION=1', ## dart: PermissionGroup.sensors 'PERMISSION_SENSORS=1', ## dart: PermissionGroup.bluetooth 'PERMISSION_BLUETOOTH=1', ] end end end ================================================ FILE: example/ios/Runner/AppDelegate.swift ================================================ import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } ================================================ FILE: example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "Icon-App-1024x1024@1x.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "filename" : "LaunchImage.png", "scale" : "1x" }, { "idiom" : "universal", "filename" : "LaunchImage@2x.png", "scale" : "2x" }, { "idiom" : "universal", "filename" : "LaunchImage@3x.png", "scale" : "3x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md ================================================ # Launch Screen Assets You can customize the launch screen with your own desired assets by replacing the image files in this directory. 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. ================================================ FILE: example/ios/Runner/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: example/ios/Runner/Base.lproj/Main.storyboard ================================================ ================================================ FILE: example/ios/Runner/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName ar_flutter_plugin_example CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS NSCameraUsageDescription UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIViewControllerBasedStatusBarAppearance NSLocationWhenInUseUsageDescription This app needs access to location when open. NSLocationAlwaysUsageDescription This app needs access to location when in the background. CADisableMinimumFrameDurationOnPhone ================================================ FILE: example/ios/Runner/Runner-Bridging-Header.h ================================================ #import "GeneratedPluginRegistrant.h" ================================================ FILE: example/ios/Runner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 51; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; EF5AB5D5261C9BD900A60388 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = EF5AB5D4261C9BD900A60388 /* GoogleService-Info.plist */; }; EF5AB736261F5EA600A60388 /* cloudAnchorKey.json in Resources */ = {isa = PBXBuildFile; fileRef = EF5AB735261F5EA600A60388 /* cloudAnchorKey.json */; }; FC3B1579A1F9BBE343269DEF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A1F47AAC378FB2E799B13D8 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 0181363DB89EBAFA9858B1A3 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3A1F47AAC378FB2E799B13D8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D424E5FE727DFB8DCB6BF1D8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; EF5AB5D4261C9BD900A60388 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; EF5AB5D6261DA4D800A60388 /* ARCoreCloudAnchors.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ARCoreCloudAnchors.framework; path = Pods/ARCore/CloudAnchors/Frameworks/ARCoreCloudAnchors.framework; sourceTree = ""; }; EF5AB5D8261DA54800A60388 /* ARCore */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ARCore; path = Pods/ARCore; sourceTree = ""; }; EF5AB735261F5EA600A60388 /* cloudAnchorKey.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = cloudAnchorKey.json; sourceTree = ""; }; F6A0B71292B6059CA94A3F18 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( FC3B1579A1F9BBE343269DEF /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 4E95B0BED358A7C62F0A1158 /* Frameworks */ = { isa = PBXGroup; children = ( EF5AB5D8261DA54800A60388 /* ARCore */, EF5AB5D6261DA4D800A60388 /* ARCoreCloudAnchors.framework */, 3A1F47AAC378FB2E799B13D8 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, ); name = Flutter; sourceTree = ""; }; 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, 9B7184BFA5C5D8AB5FFB1988 /* Pods */, 4E95B0BED358A7C62F0A1158 /* Frameworks */, ); sourceTree = ""; }; 97C146EF1CF9000F007C117D /* Products */ = { isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, ); name = Products; sourceTree = ""; }; 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( EF5AB735261F5EA600A60388 /* cloudAnchorKey.json */, EF5AB5D4261C9BD900A60388 /* GoogleService-Info.plist */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; 9B7184BFA5C5D8AB5FFB1988 /* Pods */ = { isa = PBXGroup; children = ( D424E5FE727DFB8DCB6BF1D8 /* Pods-Runner.debug.xcconfig */, F6A0B71292B6059CA94A3F18 /* Pods-Runner.release.xcconfig */, 0181363DB89EBAFA9858B1A3 /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( B6180CDA50A54DB55045479E /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 94B0826727C2A055D78A9B66 /* [CP] Embed Pods Frameworks */, C226EEF590F236CBFE1746B9 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, EF5AB736261F5EA600A60388 /* cloudAnchorKey.json in Resources */, EF5AB5D5261C9BD900A60388 /* GoogleService-Info.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 94B0826727C2A055D78A9B66 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; B6180CDA50A54DB55045479E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; C226EEF590F236CBFE1746B9 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Profile; }; 249021D4217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 5TD7B79GK6; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Pods/ARCore/CloudAnchors/Frameworks", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = io.carius.lars.arFlutterPluginExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; name = Release; }; 97C147061CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 5TD7B79GK6; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Pods/ARCore/CloudAnchors/Frameworks", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = io.carius.lars.arFlutterPluginExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; }; 97C147071CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = 5TD7B79GK6; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Pods/ARCore/CloudAnchors/Frameworks", ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 0.0.1; PRODUCT_BUNDLE_IDENTIFIER = io.carius.lars.arFlutterPluginExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, 97C147041CF9000F007C117D /* Release */, 249021D3217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147061CF9000F007C117D /* Debug */, 97C147071CF9000F007C117D /* Release */, 249021D4217E4FDB00AE95B9 /* Profile */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings ================================================ PreviewsEnabled ================================================ FILE: example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme ================================================ ================================================ FILE: example/ios/Runner.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: example/lib/examples/cloudanchorexample.dart ================================================ import 'dart:convert'; import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:flutter/material.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; import 'package:ar_flutter_plugin/datatypes/node_types.dart'; import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:ar_flutter_plugin/models/ar_hittest_result.dart'; import 'package:vector_math/vector_math_64.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:geoflutterfire/geoflutterfire.dart'; import 'package:geolocator/geolocator.dart'; class CloudAnchorWidget extends StatefulWidget { CloudAnchorWidget({Key? key}) : super(key: key); @override _CloudAnchorWidgetState createState() => _CloudAnchorWidgetState(); } class _CloudAnchorWidgetState extends State { // Firebase stuff bool _initialized = false; bool _error = false; FirebaseManager firebaseManager = FirebaseManager(); Map anchorsInDownloadProgress = Map(); ARSessionManager? arSessionManager; ARObjectManager? arObjectManager; ARAnchorManager? arAnchorManager; ARLocationManager? arLocationManager; List nodes = []; List anchors = []; String lastUploadedAnchor = ""; bool readyToUpload = false; bool readyToDownload = true; @override void initState() { firebaseManager.initializeFlutterFire().then((value) => setState(() { _initialized = value; _error = !value; })); super.initState(); } @override void dispose() { super.dispose(); arSessionManager!.dispose(); } @override Widget build(BuildContext context) { // Show error message if initialization failed if (_error) { return Scaffold( appBar: AppBar( title: const Text('Cloud Anchors'), ), body: Container( child: Center( child: Column( children: [ Text("Firebase initialization failed"), ElevatedButton( child: Text("Retry"), onPressed: () => {initState()}) ], )))); } // Show a loader until FlutterFire is initialized if (!_initialized) { return Scaffold( appBar: AppBar( title: const Text('Cloud Anchors'), ), body: Container( child: Center( child: Column(children: [ CircularProgressIndicator(), Text("Initializing Firebase") ])))); } return Scaffold( appBar: AppBar( title: const Text('Cloud Anchors'), ), body: Container( child: Stack(children: [ ARView( onARViewCreated: onARViewCreated, planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical, ), Align( alignment: FractionalOffset.bottomCenter, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onRemoveEverything, child: Text("Remove Everything")), ]), ), Align( alignment: FractionalOffset.topCenter, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Visibility( visible: readyToUpload, child: ElevatedButton( onPressed: onUploadButtonPressed, child: Text("Upload"))), Visibility( visible: readyToDownload, child: ElevatedButton( onPressed: onDownloadButtonPressed, child: Text("Download"))), ]), ) ]))); } void onARViewCreated( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager) { this.arSessionManager = arSessionManager; this.arObjectManager = arObjectManager; this.arAnchorManager = arAnchorManager; this.arLocationManager = arLocationManager; this.arSessionManager!.onInitialize( showFeaturePoints: false, showPlanes: true, customPlaneTexturePath: "Images/triangle.png", showWorldOrigin: true, ); this.arObjectManager!.onInitialize(); this.arAnchorManager!.initGoogleCloudAnchorMode(); this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped; this.arObjectManager!.onNodeTap = onNodeTapped; this.arAnchorManager!.onAnchorUploaded = onAnchorUploaded; this.arAnchorManager!.onAnchorDownloaded = onAnchorDownloaded; this .arLocationManager !.startLocationUpdates() .then((value) => null) .onError((error, stackTrace) { switch (error.toString()) { case 'Location services disabled': { showAlertDialog( context, "Action Required", "To use cloud anchor functionality, please enable your location services", "Settings", this.arLocationManager!.openLocationServicesSettings, "Cancel"); break; } case 'Location permissions denied': { showAlertDialog( context, "Action Required", "To use cloud anchor functionality, please allow the app to access your device's location", "Retry", this.arLocationManager!.startLocationUpdates, "Cancel"); break; } case 'Location permissions permanently denied': { showAlertDialog( context, "Action Required", "To use cloud anchor functionality, please allow the app to access your device's location", "Settings", this.arLocationManager!.openAppPermissionSettings, "Cancel"); break; } default: { this.arSessionManager!.onError(error.toString()); break; } } this.arSessionManager!.onError(error.toString()); }); } Future onRemoveEverything() async { anchors.forEach((anchor) { this.arAnchorManager!.removeAnchor(anchor); }); anchors = []; if (lastUploadedAnchor != "") { setState(() { readyToDownload = true; readyToUpload = false; }); } else { setState(() { readyToDownload = true; readyToUpload = false; }); } } Future onNodeTapped(List nodeNames) async { var foregroundNode = nodes.firstWhere((element) => element.name == nodeNames.first); this.arSessionManager!.onError(foregroundNode.data!["onTapText"]); } Future onPlaneOrPointTapped( List hitTestResults) async { var singleHitTestResult = hitTestResults.firstWhere( (hitTestResult) => hitTestResult.type == ARHitTestResultType.plane); if (singleHitTestResult != null) { var newAnchor = ARPlaneAnchor( transformation: singleHitTestResult.worldTransform, ttl: 2); bool? didAddAnchor = await this.arAnchorManager!.addAnchor(newAnchor); if (didAddAnchor ?? false) { this.anchors.add(newAnchor); // Add note to anchor var newNode = ARNode( type: NodeType.webGLB, uri: "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb", scale: Vector3(0.2, 0.2, 0.2), position: Vector3(0.0, 0.0, 0.0), rotation: Vector4(1.0, 0.0, 0.0, 0.0), data: {"onTapText": "Ouch, that hurt!"}); bool? didAddNodeToAnchor = await this.arObjectManager!.addNode(newNode, planeAnchor: newAnchor); if (didAddNodeToAnchor ?? false) { this.nodes.add(newNode); setState(() { readyToUpload = true; }); } else { this.arSessionManager!.onError("Adding Node to Anchor failed"); } } else { this.arSessionManager!.onError("Adding Anchor failed"); } } } Future onUploadButtonPressed() async { this.arAnchorManager!.uploadAnchor(this.anchors.last); setState(() { readyToUpload = false; }); } onAnchorUploaded(ARAnchor anchor) { // Upload anchor information to firebase firebaseManager.uploadAnchor(anchor, currentLocation: this.arLocationManager!.currentLocation); // Upload child nodes to firebase if (anchor is ARPlaneAnchor) { anchor.childNodes.forEach((nodeName) => firebaseManager.uploadObject( nodes.firstWhere((element) => element.name == nodeName))); } setState(() { readyToDownload = true; readyToUpload = false; }); this.arSessionManager!.onError("Upload successful"); } ARAnchor onAnchorDownloaded(MapserializedAnchor) { final anchor = ARPlaneAnchor.fromJson(anchorsInDownloadProgress[serializedAnchor["cloudanchorid"]] as Map); anchorsInDownloadProgress.remove(anchor.cloudanchorid); this.anchors.add(anchor); // Download nodes attached to this anchor firebaseManager.getObjectsFromAnchor(anchor, (snapshot) { snapshot.docs.forEach((objectDoc) { ARNode object = ARNode.fromMap(objectDoc.data() as Map); arObjectManager!.addNode(object, planeAnchor: anchor); this.nodes.add(object); }); }); return anchor; } Future onDownloadButtonPressed() async { //this.arAnchorManager.downloadAnchor(lastUploadedAnchor); //firebaseManager.downloadLatestAnchor((snapshot) { // final cloudAnchorId = snapshot.docs.first.get("cloudanchorid"); // anchorsInDownloadProgress[cloudAnchorId] = snapshot.docs.first.data(); // arAnchorManager.downloadAnchor(cloudAnchorId); //}); // Get anchors within a radius of 100m of the current device's location if (this.arLocationManager!.currentLocation != null) { firebaseManager.downloadAnchorsByLocation((snapshot) { final cloudAnchorId = snapshot.get("cloudanchorid"); anchorsInDownloadProgress[cloudAnchorId] = snapshot.data() as Map; arAnchorManager!.downloadAnchor(cloudAnchorId); }, this.arLocationManager!.currentLocation, 0.1); setState(() { readyToDownload = false; }); } else { this .arSessionManager! .onError("Location updates not running, can't download anchors"); } } void showAlertDialog(BuildContext context, String title, String content, String buttonText, Function buttonFunction, String cancelButtonText) { // set up the buttons Widget cancelButton = ElevatedButton( child: Text(cancelButtonText), onPressed: () { Navigator.of(context).pop(); }, ); Widget actionButton = ElevatedButton( child: Text(buttonText), onPressed: () { buttonFunction(); Navigator.of(context).pop(); }, ); // set up the AlertDialog AlertDialog alert = AlertDialog( title: Text(title), content: Text(content), actions: [ cancelButton, actionButton, ], ); // show the dialog showDialog( context: context, builder: (BuildContext context) { return alert; }, ); } } // Class for managing interaction with Firebase (in your own app, this can be put in a separate file to keep everything clean and tidy) typedef FirebaseListener = void Function(QuerySnapshot snapshot); typedef FirebaseDocumentStreamListener = void Function( DocumentSnapshot snapshot); class FirebaseManager { FirebaseFirestore? firestore; Geoflutterfire? geo; CollectionReference? anchorCollection; CollectionReference? objectCollection; // Firebase initialization function Future initializeFlutterFire() async { try { // Wait for Firebase to initialize await Firebase.initializeApp(); geo = Geoflutterfire(); firestore = FirebaseFirestore.instance; anchorCollection = FirebaseFirestore.instance.collection('anchors'); objectCollection = FirebaseFirestore.instance.collection('objects'); return true; } catch (e) { return false; } } void uploadAnchor(ARAnchor anchor, {Position? currentLocation}) { if (firestore == null) return; var serializedAnchor = anchor.toJson(); var expirationTime = DateTime.now().millisecondsSinceEpoch / 1000 + serializedAnchor["ttl"] * 24 * 60 * 60; serializedAnchor["expirationTime"] = expirationTime; // Add location if (currentLocation != null) { GeoFirePoint myLocation = geo!.point( latitude: currentLocation.latitude, longitude: currentLocation.longitude); serializedAnchor["position"] = myLocation.data; } anchorCollection! .add(serializedAnchor) .then((value) => print("Successfully added anchor: " + serializedAnchor["name"])) .catchError((error) => print("Failed to add anchor: $error")); } void uploadObject(ARNode node) { if (firestore == null) return; var serializedNode = node.toMap(); objectCollection! .add(serializedNode) .then((value) => print("Successfully added object: " + serializedNode["name"])) .catchError((error) => print("Failed to add object: $error")); } void downloadLatestAnchor(FirebaseListener listener) { anchorCollection! .orderBy("expirationTime", descending: false) .limitToLast(1) .get() .then((value) => listener(value)) .catchError( (error) => (error) => print("Failed to download anchor: $error")); } void downloadAnchorsByLocation(FirebaseDocumentStreamListener listener, Position location, double radius) { GeoFirePoint center = geo!.point(latitude: location.latitude, longitude: location.longitude); Stream> stream = geo! .collection(collectionRef: anchorCollection!) .within(center: center, radius: radius, field: 'position'); stream.listen((List documentList) { documentList.forEach((element) { listener(element); }); }); } void downloadAnchorsByChannel() {} void getObjectsFromAnchor(ARPlaneAnchor anchor, FirebaseListener listener) { objectCollection! .where("name", whereIn: anchor.childNodes) .get() .then((value) => listener(value)) .catchError((error) => print("Failed to download objects: $error")); } void deleteExpiredDatabaseEntries() { WriteBatch batch = FirebaseFirestore.instance.batch(); anchorCollection! .where("expirationTime", isLessThan: DateTime.now().millisecondsSinceEpoch / 1000) .get() .then((anchorSnapshot) => anchorSnapshot.docs.forEach((anchorDoc) { // Delete all objects attached to the expired anchor objectCollection! .where("name", arrayContainsAny: anchorDoc.get("childNodes")) .get() .then((objectSnapshot) => objectSnapshot.docs.forEach( (objectDoc) => batch.delete(objectDoc.reference))); // Delete the expired anchor batch.delete(anchorDoc.reference); })); batch.commit(); } } ================================================ FILE: example/lib/examples/debugoptionsexample.dart ================================================ import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:flutter/material.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; class DebugOptionsWidget extends StatefulWidget { DebugOptionsWidget({Key? key}) : super(key: key); @override _DebugOptionsWidgetState createState() => _DebugOptionsWidgetState(); } class _DebugOptionsWidgetState extends State { ARSessionManager? arSessionManager; ARObjectManager? arObjectManager; bool _showFeaturePoints = false; bool _showPlanes = false; bool _showWorldOrigin = false; bool _showAnimatedGuide = true; String _planeTexturePath = "Images/triangle.png"; bool _handleTaps = false; @override void dispose() { super.dispose(); arSessionManager!.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Debug Options'), ), body: Container( child: Stack(children: [ ARView( onARViewCreated: onARViewCreated, planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical, showPlatformType: true, ), Align( alignment: FractionalOffset.bottomRight, child: Container( width: MediaQuery.of(context).size.width * 0.5, color: Color(0xFFFFFFF).withOpacity(0.5), child: Column( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ SwitchListTile( title: const Text('Feature Points'), value: _showFeaturePoints, onChanged: (bool value) { setState(() { _showFeaturePoints = value; updateSessionSettings(); }); }, ), SwitchListTile( title: const Text('Planes'), value: _showPlanes, onChanged: (bool value) { setState(() { _showPlanes = value; updateSessionSettings(); }); }, ), SwitchListTile( title: const Text('World Origin'), value: _showWorldOrigin, onChanged: (bool value) { setState(() { _showWorldOrigin = value; updateSessionSettings(); }); }, ), ], ), ), ), ]))); } void onARViewCreated( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager) { this.arSessionManager = arSessionManager; this.arObjectManager = arObjectManager; this.arSessionManager!.onInitialize( showFeaturePoints: _showFeaturePoints, showPlanes: _showPlanes, customPlaneTexturePath: _planeTexturePath, showWorldOrigin: _showWorldOrigin, showAnimatedGuide: _showAnimatedGuide, handleTaps: _handleTaps, ); this.arObjectManager!.onInitialize(); } void updateSessionSettings() { this.arSessionManager!.onInitialize( showFeaturePoints: _showFeaturePoints, showPlanes: _showPlanes, customPlaneTexturePath: _planeTexturePath, showWorldOrigin: _showWorldOrigin, ); } } ================================================ FILE: example/lib/examples/externalmodelmanagementexample.dart ================================================ import 'dart:convert'; import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:flutter/material.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; import 'package:ar_flutter_plugin/datatypes/node_types.dart'; import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:ar_flutter_plugin/models/ar_hittest_result.dart'; import 'package:vector_math/vector_math_64.dart' as VectorMath; import 'package:firebase_core/firebase_core.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:geoflutterfire/geoflutterfire.dart'; import 'package:geolocator/geolocator.dart'; class ExternalModelManagementWidget extends StatefulWidget { ExternalModelManagementWidget({Key? key}) : super(key: key); @override _ExternalModelManagementWidgetState createState() => _ExternalModelManagementWidgetState(); } class _ExternalModelManagementWidgetState extends State { // Firebase stuff bool _initialized = false; bool _error = false; FirebaseManager firebaseManager = FirebaseManager(); Map anchorsInDownloadProgress = Map(); ARSessionManager? arSessionManager; ARObjectManager? arObjectManager; ARAnchorManager? arAnchorManager; ARLocationManager? arLocationManager; List nodes = []; List anchors = []; String lastUploadedAnchor = ""; AvailableModel selectedModel = AvailableModel( "Duck", "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb", ""); bool readyToUpload = false; bool readyToDownload = true; bool modelChoiceActive = false; @override void initState() { firebaseManager.initializeFlutterFire().then((value) => setState(() { _initialized = value; _error = !value; })); super.initState(); } @override void dispose() { super.dispose(); arSessionManager!.dispose(); } @override Widget build(BuildContext context) { // Show error message if initialization failed if (_error) { return Scaffold( appBar: AppBar( title: const Text('External Model Management'), ), body: Container( child: Center( child: Column( children: [ Text("Firebase initialization failed"), ElevatedButton( child: Text("Retry"), onPressed: () => {initState()}) ], )))); } // Show a loader until FlutterFire is initialized if (!_initialized) { return Scaffold( appBar: AppBar( title: const Text('External Model Management'), ), body: Container( child: Center( child: Column(children: [ CircularProgressIndicator(), Text("Initializing Firebase") ])))); } return Scaffold( appBar: AppBar( title: const Text('External Model Management'), actions: [ IconButton( icon: Icon(Icons.pets), onPressed: () { setState(() { modelChoiceActive = !modelChoiceActive; }); }, ), ]), body: Container( child: Stack(children: [ ARView( onARViewCreated: onARViewCreated, planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical, ), Align( alignment: FractionalOffset.bottomCenter, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onRemoveEverything, child: Text("Remove Everything")), ]), ), Align( alignment: FractionalOffset.topCenter, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Visibility( visible: readyToUpload, child: ElevatedButton( onPressed: onUploadButtonPressed, child: Text("Upload"))), Visibility( visible: readyToDownload, child: ElevatedButton( onPressed: onDownloadButtonPressed, child: Text("Download"))), ]), ), Align( alignment: FractionalOffset.centerLeft, child: Visibility( visible: modelChoiceActive, child: ModelSelectionWidget( onTap: onModelSelected, firebaseManager: this.firebaseManager))) ]))); } void onARViewCreated( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager) { this.arSessionManager = arSessionManager; this.arObjectManager = arObjectManager; this.arAnchorManager = arAnchorManager; this.arLocationManager = arLocationManager; this.arSessionManager!.onInitialize( showFeaturePoints: false, showPlanes: true, customPlaneTexturePath: "Images/triangle.png", showWorldOrigin: true, ); this.arObjectManager!.onInitialize(); this.arAnchorManager!.initGoogleCloudAnchorMode(); this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped; this.arObjectManager!.onNodeTap = onNodeTapped; this.arAnchorManager!.onAnchorUploaded = onAnchorUploaded; this.arAnchorManager!.onAnchorDownloaded = onAnchorDownloaded; this .arLocationManager! .startLocationUpdates() .then((value) => null) .onError((error, stackTrace) { switch (error.toString()) { case 'Location services disabled': { showAlertDialog( context, "Action Required", "To use cloud anchor functionality, please enable your location services", "Settings", this.arLocationManager!.openLocationServicesSettings, "Cancel"); break; } case 'Location permissions denied': { showAlertDialog( context, "Action Required", "To use cloud anchor functionality, please allow the app to access your device's location", "Retry", this.arLocationManager!.startLocationUpdates, "Cancel"); break; } case 'Location permissions permanently denied': { showAlertDialog( context, "Action Required", "To use cloud anchor functionality, please allow the app to access your device's location", "Settings", this.arLocationManager!.openAppPermissionSettings, "Cancel"); break; } default: { this.arSessionManager!.onError(error.toString()); break; } } this.arSessionManager!.onError(error.toString()); }); } void onModelSelected(AvailableModel model) { this.selectedModel = model; this.arSessionManager!.onError(model.name + " selected"); setState(() { modelChoiceActive = false; }); } Future onRemoveEverything() async { anchors.forEach((anchor) { this.arAnchorManager!.removeAnchor(anchor); }); anchors = []; if (lastUploadedAnchor != "") { setState(() { readyToDownload = true; readyToUpload = false; }); } else { setState(() { readyToDownload = true; readyToUpload = false; }); } } Future onNodeTapped(List nodeNames) async { var foregroundNode = nodes.firstWhere((element) => element.name == nodeNames.first); this.arSessionManager!.onError(foregroundNode.data!["onTapText"]); } Future onPlaneOrPointTapped( List hitTestResults) async { var singleHitTestResult = hitTestResults.firstWhere( (hitTestResult) => hitTestResult.type == ARHitTestResultType.plane); if (singleHitTestResult != null) { var newAnchor = ARPlaneAnchor( transformation: singleHitTestResult.worldTransform, ttl: 2); bool? didAddAnchor = await this.arAnchorManager!.addAnchor(newAnchor); if (didAddAnchor!) { this.anchors.add(newAnchor); // Add note to anchor var newNode = ARNode( type: NodeType.webGLB, uri: this.selectedModel.uri, scale: VectorMath.Vector3(0.2, 0.2, 0.2), position: VectorMath.Vector3(0.0, 0.0, 0.0), rotation: VectorMath.Vector4(1.0, 0.0, 0.0, 0.0), data: {"onTapText": "I am a " + this.selectedModel.name}); bool? didAddNodeToAnchor = await this.arObjectManager!.addNode(newNode, planeAnchor: newAnchor); if (didAddNodeToAnchor!) { this.nodes.add(newNode); setState(() { readyToUpload = true; }); } else { this.arSessionManager!.onError("Adding Node to Anchor failed"); } } else { this.arSessionManager!.onError("Adding Anchor failed"); } } } Future onUploadButtonPressed() async { this.arAnchorManager!.uploadAnchor(this.anchors.last); setState(() { readyToUpload = false; }); } onAnchorUploaded(ARAnchor anchor) { // Upload anchor information to firebase firebaseManager.uploadAnchor(anchor, currentLocation: this.arLocationManager!.currentLocation); // Upload child nodes to firebase if (anchor is ARPlaneAnchor) { anchor.childNodes.forEach((nodeName) => firebaseManager.uploadObject( nodes.firstWhere((element) => element.name == nodeName))); } setState(() { readyToDownload = true; readyToUpload = false; }); this.arSessionManager!.onError("Upload successful"); } ARAnchor onAnchorDownloaded(Map serializedAnchor) { final anchor = ARPlaneAnchor.fromJson(anchorsInDownloadProgress[serializedAnchor["cloudanchorid"]] as Map); anchorsInDownloadProgress.remove(anchor.cloudanchorid); this.anchors.add(anchor); // Download nodes attached to this anchor firebaseManager.getObjectsFromAnchor(anchor, (snapshot) { snapshot.docs.forEach((objectDoc) { ARNode object = ARNode.fromMap(objectDoc.data() as Map); arObjectManager!.addNode(object, planeAnchor: anchor); this.nodes.add(object); }); }); return anchor; } Future onDownloadButtonPressed() async { //this.arAnchorManager.downloadAnchor(lastUploadedAnchor); //firebaseManager.downloadLatestAnchor((snapshot) { // final cloudAnchorId = snapshot.docs.first.get("cloudanchorid"); // anchorsInDownloadProgress[cloudAnchorId] = snapshot.docs.first.data(); // arAnchorManager.downloadAnchor(cloudAnchorId); //}); // Get anchors within a radius of 100m of the current device's location if (this.arLocationManager!.currentLocation != null) { firebaseManager.downloadAnchorsByLocation((snapshot) { final cloudAnchorId = snapshot.get("cloudanchorid"); anchorsInDownloadProgress[cloudAnchorId] = snapshot.data() as Map; arAnchorManager!.downloadAnchor(cloudAnchorId); }, this.arLocationManager!.currentLocation, 0.1); setState(() { readyToDownload = false; }); } else { this .arSessionManager! .onError("Location updates not running, can't download anchors"); } } void showAlertDialog(BuildContext context, String title, String content, String buttonText, Function buttonFunction, String cancelButtonText) { // set up the buttons Widget cancelButton = ElevatedButton( child: Text(cancelButtonText), onPressed: () { Navigator.of(context).pop(); }, ); Widget actionButton = ElevatedButton( child: Text(buttonText), onPressed: () { buttonFunction(); Navigator.of(context).pop(); }, ); // set up the AlertDialog AlertDialog alert = AlertDialog( title: Text(title), content: Text(content), actions: [ cancelButton, actionButton, ], ); // show the dialog showDialog( context: context, builder: (BuildContext context) { return alert; }, ); } } // Class for managing interaction with Firebase (in your own app, this can be put in a separate file to keep everything clean and tidy) typedef FirebaseListener = void Function(QuerySnapshot snapshot); typedef FirebaseDocumentStreamListener = void Function( DocumentSnapshot snapshot); class FirebaseManager { FirebaseFirestore? firestore; Geoflutterfire? geo; CollectionReference? anchorCollection; CollectionReference? objectCollection; CollectionReference? modelCollection; // Firebase initialization function Future initializeFlutterFire() async { try { // Wait for Firebase to initialize await Firebase.initializeApp(); geo = Geoflutterfire(); firestore = FirebaseFirestore.instance; anchorCollection = FirebaseFirestore.instance.collection('anchors'); objectCollection = FirebaseFirestore.instance.collection('objects'); modelCollection = FirebaseFirestore.instance.collection('models'); return true; } catch (e) { return false; } } void uploadAnchor(ARAnchor anchor, {Position? currentLocation}) { if (firestore == null) return; var serializedAnchor = anchor.toJson(); var expirationTime = DateTime.now().millisecondsSinceEpoch / 1000 + serializedAnchor["ttl"] * 24 * 60 * 60; serializedAnchor["expirationTime"] = expirationTime; // Add location if (currentLocation != null) { GeoFirePoint myLocation = geo!.point( latitude: currentLocation.latitude, longitude: currentLocation.longitude); serializedAnchor["position"] = myLocation.data; } anchorCollection! .add(serializedAnchor) .then((value) => print("Successfully added anchor: " + serializedAnchor["name"])) .catchError((error) => print("Failed to add anchor: $error")); } void uploadObject(ARNode node) { if (firestore == null) return; var serializedNode = node.toMap(); objectCollection! .add(serializedNode) .then((value) => print("Successfully added object: " + serializedNode["name"])) .catchError((error) => print("Failed to add object: $error")); } void downloadLatestAnchor(FirebaseListener listener) { anchorCollection! .orderBy("expirationTime", descending: false) .limitToLast(1) .get() .then((value) => listener(value)) .catchError( (error) => (error) => print("Failed to download anchor: $error")); } void downloadAnchorsByLocation(FirebaseDocumentStreamListener listener, Position location, double radius) { GeoFirePoint center = geo!.point(latitude: location.latitude, longitude: location.longitude); Stream> stream = geo! .collection(collectionRef: anchorCollection!) .within(center: center, radius: radius, field: 'position'); stream.listen((List documentList) { documentList.forEach((element) { listener(element); }); }); } void downloadAnchorsByChannel() {} void getObjectsFromAnchor(ARPlaneAnchor anchor, FirebaseListener listener) { objectCollection! .where("name", whereIn: anchor.childNodes) .get() .then((value) => listener(value)) .catchError((error) => print("Failed to download objects: $error")); } void deleteExpiredDatabaseEntries() { WriteBatch batch = FirebaseFirestore.instance.batch(); anchorCollection! .where("expirationTime", isLessThan: DateTime.now().millisecondsSinceEpoch / 1000) .get() .then((anchorSnapshot) => anchorSnapshot.docs.forEach((anchorDoc) { // Delete all objects attached to the expired anchor objectCollection! .where("name", arrayContainsAny: anchorDoc.get("childNodes")) .get() .then((objectSnapshot) => objectSnapshot.docs.forEach( (objectDoc) => batch.delete(objectDoc.reference))); // Delete the expired anchor batch.delete(anchorDoc.reference); })); batch.commit(); } void downloadAvailableModels(FirebaseListener listener) { modelCollection! .get() .then((value) => listener(value)) .catchError((error) => print("Failed to download objects: $error")); } } class AvailableModel { String name; String uri; String image; AvailableModel(this.name, this.uri, this.image); } class ModelSelectionWidget extends StatefulWidget { final Function onTap; final FirebaseManager firebaseManager; ModelSelectionWidget({required this.onTap, required this.firebaseManager}); @override _ModelSelectionWidgetState createState() => _ModelSelectionWidgetState(); } class _ModelSelectionWidgetState extends State { List models = []; String? selected; @override void initState() { super.initState(); widget.firebaseManager.downloadAvailableModels((snapshot) { snapshot.docs.forEach((element) { setState(() { models.add(AvailableModel(element.get("name"), element.get("uri"), element.get("image").first["downloadURL"])); }); }); }); } @override Widget build(BuildContext context) { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ DecoratedBox( decoration: BoxDecoration( color: Colors.white, border: Border.all( color: Colors.white, style: BorderStyle.solid, width: 4.0, ), borderRadius: BorderRadius.all(Radius.circular(5)), shape: BoxShape.rectangle, boxShadow: const [ BoxShadow( color: Color(0x66000000), blurRadius: 10.0, spreadRadius: 4.0, ) ], ), child: Text('Choose a Model', style: DefaultTextStyle.of(context) .style .apply(fontSizeFactor: 2.0)), ), Container( height: MediaQuery.of(context).size.width * 0.65, child: ListView.builder( itemCount: models.length, scrollDirection: Axis.horizontal, itemBuilder: (context, index) { return GestureDetector( onTap: () { widget.onTap(models[index]); }, child: Card( elevation: 4.0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all( Radius.circular(5), ), ), child: Column( children: [ Padding( padding: EdgeInsets.all(20), child: Image.network(models[index].image)), Text( models[index].name, style: DefaultTextStyle.of(context) .style .apply(fontSizeFactor: 2.0), ) ], ), ), ); }, ), ) ]); } } ================================================ FILE: example/lib/examples/localandwebobjectsexample.dart ================================================ import 'dart:io'; import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; import 'package:ar_flutter_plugin/datatypes/node_types.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; import 'dart:math'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_archive/flutter_archive.dart'; class LocalAndWebObjectsWidget extends StatefulWidget { LocalAndWebObjectsWidget({Key? key}) : super(key: key); @override _LocalAndWebObjectsWidgetState createState() => _LocalAndWebObjectsWidgetState(); } class _LocalAndWebObjectsWidgetState extends State { ARSessionManager? arSessionManager; ARObjectManager? arObjectManager; //String localObjectReference; ARNode? localObjectNode; //String webObjectReference; ARNode? webObjectNode; ARNode? fileSystemNode; HttpClient? httpClient; @override void dispose() { super.dispose(); arSessionManager!.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Local & Web Objects'), ), body: Container( child: Stack(children: [ ARView( onARViewCreated: onARViewCreated, planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical, ), Align( alignment: FractionalOffset.bottomCenter, child: Column(mainAxisAlignment: MainAxisAlignment.end, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onFileSystemObjectAtOriginButtonPressed, child: Text("Add/Remove Filesystem\nObject at Origin")), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onLocalObjectAtOriginButtonPressed, child: Text("Add/Remove Local\nObject at Origin")), ElevatedButton( onPressed: onWebObjectAtOriginButtonPressed, child: Text("Add/Remove Web\nObject at Origin")), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onLocalObjectShuffleButtonPressed, child: Text("Shuffle Local\nobject at Origin")), ElevatedButton( onPressed: onWebObjectShuffleButtonPressed, child: Text("Shuffle Web\nObject at Origin")), ], ) ])) ]))); } void onARViewCreated( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager) { this.arSessionManager = arSessionManager; this.arObjectManager = arObjectManager; this.arSessionManager!.onInitialize( showFeaturePoints: false, showPlanes: true, customPlaneTexturePath: "Images/triangle.png", showWorldOrigin: true, handleTaps: false, ); this.arObjectManager!.onInitialize(); //Download model to file system httpClient = new HttpClient(); _downloadFile( "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb", "LocalDuck.glb"); // Alternative to use type fileSystemAppFolderGLTF2: //_downloadAndUnpack( // "https://drive.google.com/uc?export=download&id=1fng7yiK0DIR0uem7XkV2nlPSGH9PysUs", // "Chicken_01.zip"); } Future _downloadFile(String url, String filename) async { var request = await httpClient!.getUrl(Uri.parse(url)); var response = await request.close(); var bytes = await consolidateHttpClientResponseBytes(response); String dir = (await getApplicationDocumentsDirectory()).path; File file = new File('$dir/$filename'); await file.writeAsBytes(bytes); print("Downloading finished, path: " + '$dir/$filename'); return file; } Future _downloadAndUnpack(String url, String filename) async { var request = await httpClient!.getUrl(Uri.parse(url)); var response = await request.close(); var bytes = await consolidateHttpClientResponseBytes(response); String dir = (await getApplicationDocumentsDirectory()).path; File file = new File('$dir/$filename'); await file.writeAsBytes(bytes); print("Downloading finished, path: " + '$dir/$filename'); // To print all files in the directory: print(Directory(dir).listSync()); try { await ZipFile.extractToDirectory( zipFile: File('$dir/$filename'), destinationDir: Directory(dir)); print("Unzipping successful"); } catch (e) { print("Unzipping failed: " + e.toString()); } } Future onLocalObjectAtOriginButtonPressed() async { if (this.localObjectNode != null) { this.arObjectManager!.removeNode(this.localObjectNode!); this.localObjectNode = null; } else { var newNode = ARNode( type: NodeType.localGLTF2, uri: "Models/Chicken_01/Chicken_01.gltf", scale: Vector3(0.2, 0.2, 0.2), position: Vector3(0.0, 0.0, 0.0), rotation: Vector4(1.0, 0.0, 0.0, 0.0)); bool? didAddLocalNode = await this.arObjectManager!.addNode(newNode); this.localObjectNode = (didAddLocalNode!) ? newNode : null; } } Future onWebObjectAtOriginButtonPressed() async { if (this.webObjectNode != null) { this.arObjectManager!.removeNode(this.webObjectNode!); this.webObjectNode = null; } else { var newNode = ARNode( type: NodeType.webGLB, uri: "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb", scale: Vector3(0.2, 0.2, 0.2)); bool? didAddWebNode = await this.arObjectManager!.addNode(newNode); this.webObjectNode = (didAddWebNode!) ? newNode : null; } } Future onFileSystemObjectAtOriginButtonPressed() async { if (this.fileSystemNode != null) { this.arObjectManager!.removeNode(this.fileSystemNode!); this.fileSystemNode = null; } else { var newNode = ARNode( type: NodeType.fileSystemAppFolderGLB, uri: "LocalDuck.glb", scale: Vector3(0.2, 0.2, 0.2)); //Alternative to use type fileSystemAppFolderGLTF2: //var newNode = ARNode( // type: NodeType.fileSystemAppFolderGLTF2, // uri: "Chicken_01.gltf", // scale: Vector3(0.2, 0.2, 0.2)); bool? didAddFileSystemNode = await this.arObjectManager!.addNode(newNode); this.fileSystemNode = (didAddFileSystemNode!) ? newNode : null; } } Future onLocalObjectShuffleButtonPressed() async { if (this.localObjectNode != null) { var newScale = Random().nextDouble() / 3; var newTranslationAxis = Random().nextInt(3); var newTranslationAmount = Random().nextDouble() / 3; var newTranslation = Vector3(0, 0, 0); newTranslation[newTranslationAxis] = newTranslationAmount; var newRotationAxisIndex = Random().nextInt(3); var newRotationAmount = Random().nextDouble(); var newRotationAxis = Vector3(0, 0, 0); newRotationAxis[newRotationAxisIndex] = 1.0; final newTransform = Matrix4.identity(); newTransform.setTranslation(newTranslation); newTransform.rotate(newRotationAxis, newRotationAmount); newTransform.scale(newScale); this.localObjectNode!.transform = newTransform; } } Future onWebObjectShuffleButtonPressed() async { if (this.webObjectNode != null) { var newScale = Random().nextDouble() / 3; var newTranslationAxis = Random().nextInt(3); var newTranslationAmount = Random().nextDouble() / 3; var newTranslation = Vector3(0, 0, 0); newTranslation[newTranslationAxis] = newTranslationAmount; var newRotationAxisIndex = Random().nextInt(3); var newRotationAmount = Random().nextDouble(); var newRotationAxis = Vector3(0, 0, 0); newRotationAxis[newRotationAxisIndex] = 1.0; final newTransform = Matrix4.identity(); newTransform.setTranslation(newTranslation); newTransform.rotate(newRotationAxis, newRotationAmount); newTransform.scale(newScale); this.webObjectNode!.transform = newTransform; } } } ================================================ FILE: example/lib/examples/objectgesturesexample.dart ================================================ import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:flutter/material.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; import 'package:ar_flutter_plugin/datatypes/node_types.dart'; import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:ar_flutter_plugin/models/ar_hittest_result.dart'; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; import 'dart:math'; class ObjectGesturesWidget extends StatefulWidget { ObjectGesturesWidget({Key? key}) : super(key: key); @override _ObjectGesturesWidgetState createState() => _ObjectGesturesWidgetState(); } class _ObjectGesturesWidgetState extends State { ARSessionManager? arSessionManager; ARObjectManager? arObjectManager; ARAnchorManager? arAnchorManager; List nodes = []; List anchors = []; @override void dispose() { super.dispose(); arSessionManager!.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Object Transformation Gestures'), ), body: Container( child: Stack(children: [ ARView( onARViewCreated: onARViewCreated, planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical, ), Align( alignment: FractionalOffset.bottomCenter, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onRemoveEverything, child: Text("Remove Everything")), ]), ) ]))); } void onARViewCreated( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager) { this.arSessionManager = arSessionManager; this.arObjectManager = arObjectManager; this.arAnchorManager = arAnchorManager; this.arSessionManager!.onInitialize( showFeaturePoints: false, showPlanes: true, customPlaneTexturePath: "Images/triangle.png", showWorldOrigin: true, handlePans: true, handleRotation: true, ); this.arObjectManager!.onInitialize(); this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped; this.arObjectManager!.onPanStart = onPanStarted; this.arObjectManager!.onPanChange = onPanChanged; this.arObjectManager!.onPanEnd = onPanEnded; this.arObjectManager!.onRotationStart = onRotationStarted; this.arObjectManager!.onRotationChange = onRotationChanged; this.arObjectManager!.onRotationEnd = onRotationEnded; } Future onRemoveEverything() async { /*nodes.forEach((node) { this.arObjectManager.removeNode(node); });*/ anchors.forEach((anchor) { this.arAnchorManager!.removeAnchor(anchor); }); anchors = []; } Future onPlaneOrPointTapped( List hitTestResults) async { var singleHitTestResult = hitTestResults.firstWhere( (hitTestResult) => hitTestResult.type == ARHitTestResultType.plane); if (singleHitTestResult != null) { var newAnchor = ARPlaneAnchor(transformation: singleHitTestResult.worldTransform); bool? didAddAnchor = await this.arAnchorManager!.addAnchor(newAnchor); if (didAddAnchor!) { this.anchors.add(newAnchor); // Add note to anchor var newNode = ARNode( type: NodeType.webGLB, uri: "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb", scale: Vector3(0.2, 0.2, 0.2), position: Vector3(0.0, 0.0, 0.0), rotation: Vector4(1.0, 0.0, 0.0, 0.0)); bool? didAddNodeToAnchor = await this.arObjectManager!.addNode(newNode, planeAnchor: newAnchor); if (didAddNodeToAnchor!) { this.nodes.add(newNode); } else { this.arSessionManager!.onError("Adding Node to Anchor failed"); } } else { this.arSessionManager!.onError("Adding Anchor failed"); } } } onPanStarted(String nodeName) { print("Started panning node " + nodeName); } onPanChanged(String nodeName) { print("Continued panning node " + nodeName); } onPanEnded(String nodeName, Matrix4 newTransform) { print("Ended panning node " + nodeName); final pannedNode = this.nodes.firstWhere((element) => element.name == nodeName); /* * Uncomment the following command if you want to keep the transformations of the Flutter representations of the nodes up to date * (e.g. if you intend to share the nodes through the cloud) */ //pannedNode.transform = newTransform; } onRotationStarted(String nodeName) { print("Started rotating node " + nodeName); } onRotationChanged(String nodeName) { print("Continued rotating node " + nodeName); } onRotationEnded(String nodeName, Matrix4 newTransform) { print("Ended rotating node " + nodeName); final rotatedNode = this.nodes.firstWhere((element) => element.name == nodeName); /* * Uncomment the following command if you want to keep the transformations of the Flutter representations of the nodes up to date * (e.g. if you intend to share the nodes through the cloud) */ //rotatedNode.transform = newTransform; } } ================================================ FILE: example/lib/examples/objectsonplanesexample.dart ================================================ import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:flutter/material.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; import 'package:ar_flutter_plugin/datatypes/node_types.dart'; import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:ar_flutter_plugin/models/ar_hittest_result.dart'; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; import 'dart:math'; class ObjectsOnPlanesWidget extends StatefulWidget { ObjectsOnPlanesWidget({Key? key}) : super(key: key); @override _ObjectsOnPlanesWidgetState createState() => _ObjectsOnPlanesWidgetState(); } class _ObjectsOnPlanesWidgetState extends State { ARSessionManager? arSessionManager; ARObjectManager? arObjectManager; ARAnchorManager? arAnchorManager; List nodes = []; List anchors = []; @override void dispose() { super.dispose(); arSessionManager!.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Anchors & Objects on Planes'), ), body: Container( child: Stack(children: [ ARView( onARViewCreated: onARViewCreated, planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical, ), Align( alignment: FractionalOffset.bottomCenter, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onRemoveEverything, child: Text("Remove Everything")), ]), ) ]))); } void onARViewCreated( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager) { this.arSessionManager = arSessionManager; this.arObjectManager = arObjectManager; this.arAnchorManager = arAnchorManager; this.arSessionManager!.onInitialize( showFeaturePoints: false, showPlanes: true, customPlaneTexturePath: "Images/triangle.png", showWorldOrigin: true, ); this.arObjectManager!.onInitialize(); this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped; this.arObjectManager!.onNodeTap = onNodeTapped; } Future onRemoveEverything() async { /*nodes.forEach((node) { this.arObjectManager.removeNode(node); });*/ anchors.forEach((anchor) { this.arAnchorManager!.removeAnchor(anchor); }); anchors = []; } Future onNodeTapped(List nodes) async { var number = nodes.length; this.arSessionManager!.onError("Tapped $number node(s)"); } Future onPlaneOrPointTapped( List hitTestResults) async { var singleHitTestResult = hitTestResults.firstWhere( (hitTestResult) => hitTestResult.type == ARHitTestResultType.plane); if (singleHitTestResult != null) { var newAnchor = ARPlaneAnchor(transformation: singleHitTestResult.worldTransform); bool? didAddAnchor = await this.arAnchorManager!.addAnchor(newAnchor); if (didAddAnchor!) { this.anchors.add(newAnchor); // Add note to anchor var newNode = ARNode( type: NodeType.webGLB, uri: "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb", scale: Vector3(0.2, 0.2, 0.2), position: Vector3(0.0, 0.0, 0.0), rotation: Vector4(1.0, 0.0, 0.0, 0.0)); bool? didAddNodeToAnchor = await this.arObjectManager!.addNode(newNode, planeAnchor: newAnchor); if (didAddNodeToAnchor!) { this.nodes.add(newNode); } else { this.arSessionManager!.onError("Adding Node to Anchor failed"); } } else { this.arSessionManager!.onError("Adding Anchor failed"); } /* // To add a node to the tapped position without creating an anchor, use the following code (Please mind: the function onRemoveEverything has to be adapted accordingly!): var newNode = ARNode( type: NodeType.localGLTF2, uri: "Models/Chicken_01/Chicken_01.gltf", scale: Vector3(0.2, 0.2, 0.2), transformation: singleHitTestResult.worldTransform); bool didAddWebNode = await this.arObjectManager.addNode(newNode); if (didAddWebNode) { this.nodes.add(newNode); }*/ } } } ================================================ FILE: example/lib/examples/screenshotexample.dart ================================================ import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:flutter/material.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; import 'package:ar_flutter_plugin/datatypes/node_types.dart'; import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:ar_flutter_plugin/models/ar_hittest_result.dart'; import 'package:vector_math/vector_math_64.dart'; class ScreenshotWidget extends StatefulWidget { const ScreenshotWidget({Key? key}) : super(key: key); @override _ScreenshotWidgetState createState() => _ScreenshotWidgetState(); } class _ScreenshotWidgetState extends State { ARSessionManager? arSessionManager; ARObjectManager? arObjectManager; ARAnchorManager? arAnchorManager; List nodes = []; List anchors = []; @override void dispose() { super.dispose(); arSessionManager!.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Screenshots'), ), body: Container( child: Stack(children: [ ARView( onARViewCreated: onARViewCreated, planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical, ), Align( alignment: FractionalOffset.bottomCenter, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ ElevatedButton( onPressed: onRemoveEverything, child: const Text("Remove Everything")), ElevatedButton( onPressed: onTakeScreenshot, child: const Text("Take Screenshot")), ]), ) ]))); } void onARViewCreated( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager) { this.arSessionManager = arSessionManager; this.arObjectManager = arObjectManager; this.arAnchorManager = arAnchorManager; this.arSessionManager!.onInitialize( showFeaturePoints: false, showPlanes: true, customPlaneTexturePath: "Images/triangle.png", showWorldOrigin: true, ); this.arObjectManager!.onInitialize(); this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped; this.arObjectManager!.onNodeTap = onNodeTapped; } Future onRemoveEverything() async { /*nodes.forEach((node) { this.arObjectManager.removeNode(node); });*/ // anchors.forEach((anchor) for (var anchor in anchors) { arAnchorManager!.removeAnchor(anchor); }; anchors = []; } Future onTakeScreenshot() async { var image = await arSessionManager!.snapshot(); await showDialog( context: context, builder: (_) => Dialog( child: Container( decoration: BoxDecoration( image: DecorationImage(image: image, fit: BoxFit.cover)), ), )); } Future onNodeTapped(List nodes) async { var number = nodes.length; arSessionManager!.onError("Tapped $number node(s)"); } Future onPlaneOrPointTapped( List hitTestResults) async { var singleHitTestResult = hitTestResults.firstWhere( (hitTestResult) => hitTestResult.type == ARHitTestResultType.plane); if (singleHitTestResult != null) { var newAnchor = ARPlaneAnchor(transformation: singleHitTestResult.worldTransform); bool? didAddAnchor = await arAnchorManager!.addAnchor(newAnchor); if (didAddAnchor != null && didAddAnchor) { anchors.add(newAnchor); // Add note to anchor var newNode = ARNode( type: NodeType.webGLB, uri: "https://github.com/KhronosGroup/glTF-Sample-Models/raw/master/2.0/Duck/glTF-Binary/Duck.glb", scale: Vector3(0.2, 0.2, 0.2), position: Vector3(0.0, 0.0, 0.0), rotation: Vector4(1.0, 0.0, 0.0, 0.0)); bool? didAddNodeToAnchor = await arObjectManager!.addNode(newNode, planeAnchor: newAnchor); if (didAddNodeToAnchor != null && didAddNodeToAnchor) { nodes.add(newNode); } else { arSessionManager!.onError("Adding Node to Anchor failed"); } } else { arSessionManager!.onError("Adding Anchor failed"); } /* // To add a node to the tapped position without creating an anchor, use the following code (Please mind: the function onRemoveEverything has to be adapted accordingly!): var newNode = ARNode( type: NodeType.localGLTF2, uri: "Models/Chicken_01/Chicken_01.gltf", scale: Vector3(0.2, 0.2, 0.2), transformation: singleHitTestResult.worldTransform); bool didAddWebNode = await this.arObjectManager.addNode(newNode); if (didAddWebNode) { this.nodes.add(newNode); }*/ } } } ================================================ FILE: example/lib/main.dart ================================================ import 'package:ar_flutter_plugin_example/examples/externalmodelmanagementexample.dart'; import 'package:ar_flutter_plugin_example/examples/objectsonplanesexample.dart'; import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/services.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; import 'package:ar_flutter_plugin_example/examples/cloudanchorexample.dart'; import 'package:ar_flutter_plugin_example/examples/localandwebobjectsexample.dart'; import 'package:ar_flutter_plugin_example/examples/debugoptionsexample.dart'; import 'examples/objectgesturesexample.dart'; import 'examples/screenshotexample.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(MyApp()); } class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); } class _MyAppState extends State { String _platformVersion = 'Unknown'; static const String _title = 'AR Plugin Demo'; @override void initState() { super.initState(); initPlatformState(); } // Platform messages are asynchronous, so we initialize in an async method. Future initPlatformState() async { String platformVersion; // Platform messages may fail, so we use a try/catch PlatformException. try { platformVersion = await ArFlutterPlugin.platformVersion; } on PlatformException { platformVersion = 'Failed to get platform version.'; } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. if (!mounted) return; setState(() { _platformVersion = platformVersion; }); } @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: const Text(_title), ), body: Column(children: [ Text('Running on: $_platformVersion\n'), Expanded( child: ExampleList(), ), ]), ), ); } } class ExampleList extends StatelessWidget { ExampleList({Key? key}) : super(key: key); @override Widget build(BuildContext context) { final examples = [ Example( 'Debug Options', 'Visualize feature points, planes and world coordinate system', () => Navigator.push(context, MaterialPageRoute(builder: (context) => DebugOptionsWidget()))), Example( 'Local & Online Objects', 'Place 3D objects from Flutter assets and the web into the scene', () => Navigator.push( context, MaterialPageRoute( builder: (context) => LocalAndWebObjectsWidget()))), Example( 'Anchors & Objects on Planes', 'Place 3D objects on detected planes using anchors', () => Navigator.push( context, MaterialPageRoute( builder: (context) => ObjectsOnPlanesWidget()))), Example( 'Object Transformation Gestures', 'Rotate and Pan Objects', () => Navigator.push(context, MaterialPageRoute(builder: (context) => ObjectGesturesWidget()))), Example( 'Screenshots', 'Place 3D objects on planes and take screenshots', () => Navigator.push(context, MaterialPageRoute(builder: (context) => ScreenshotWidget()))), Example( 'Cloud Anchors', 'Place and retrieve 3D objects using the Google Cloud Anchor API', () => Navigator.push(context, MaterialPageRoute(builder: (context) => CloudAnchorWidget()))), Example( 'External Model Management', 'Similar to Cloud Anchors example, but uses external database to choose from available 3D models', () => Navigator.push( context, MaterialPageRoute( builder: (context) => ExternalModelManagementWidget()))) ]; return ListView( children: examples.map((example) => ExampleCard(example: example)).toList(), ); } } class ExampleCard extends StatelessWidget { ExampleCard({Key? key, required this.example}) : super(key: key); final Example example; @override build(BuildContext context) { return Card( child: InkWell( splashColor: Colors.blue.withAlpha(30), onTap: () { example.onTap(); }, child: ListTile( title: Text(example.name), subtitle: Text(example.description), ), ), ); } } class Example { const Example(this.name, this.description, this.onTap); final String name; final String description; final Function onTap; } ================================================ FILE: example/pubspec.yaml ================================================ name: ar_flutter_plugin_example description: Demonstrates how to use the ar_flutter_plugin plugin. # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: sdk: ">=2.16.1 <3.0.0" dependencies: flutter: sdk: flutter ar_flutter_plugin: # When depending on this package from a real application you should use: # ar_flutter_plugin: ^x.y.z # See https://dart.dev/tools/pub/dependencies#version-constraints # The example app is bundled with the plugin so we use a path dependency on # the parent directory to use the current plugin's version. path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 # Firebase dependencies to shared and manage cloud anchor IDs and related content firebase_core: "^1.6.0" cloud_firestore: "^2.5.3" geoflutterfire: "^3.0.1" path_provider: ^2.0.3 flutter_archive: ^4.0.1 dev_dependencies: flutter_test: sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - Images/triangle.png - Models/Chicken_01/ # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see # https://flutter.dev/assets-and-images/#from-packages # To add custom fonts to your application, add a fonts section here, # in this "flutter" section. Each entry in this list should have a # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic # - family: Trajan Pro # fonts: # - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro_Bold.ttf # weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages ================================================ FILE: example/test/widget_test.dart ================================================ // This is a basic Flutter widget test. // // To perform an interaction with a widget in your test, use the WidgetTester // utility that Flutter provides. For example, you can send tap and scroll // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ar_flutter_plugin_example/main.dart'; void main() { testWidgets('Verify Platform version', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); // Verify that platform version is retrieved. expect( find.byWidgetPredicate( (Widget widget) => widget is Text && widget.data!.startsWith('Running on:'), ), findsOneWidget, ); }); } ================================================ FILE: ios/.gitignore ================================================ .idea/ .vagrant/ .sconsign.dblite .svn/ .DS_Store *.swp profile DerivedData/ build/ GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m .generated/ *.pbxuser *.mode1v3 *.mode2v3 *.perspectivev3 !default.pbxuser !default.mode1v3 !default.mode2v3 !default.perspectivev3 xcuserdata *.moved-aside *.pyc *sync/ Icon? .tags* /Flutter/Generated.xcconfig /Flutter/flutter_export_environment.sh ================================================ FILE: ios/Classes/ArFlutterPlugin.h ================================================ #import @interface ArFlutterPlugin : NSObject @end ================================================ FILE: ios/Classes/ArFlutterPlugin.m ================================================ #import "ArFlutterPlugin.h" #if __has_include() #import #else // Support project import fallback if the generated compatibility header // is not copied when this plugin is created as a library. // https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816 #import "ar_flutter_plugin-Swift.h" #endif @implementation ArFlutterPlugin + (void)registerWithRegistrar:(NSObject*)registrar { [SwiftArFlutterPlugin registerWithRegistrar:registrar]; } @end ================================================ FILE: ios/Classes/ArModelBuilder.swift ================================================ import UIKit import Foundation import ARKit import GLTFSceneKit import Combine // Responsible for creating Renderables and Nodes class ArModelBuilder: NSObject { func makePlane(anchor: ARPlaneAnchor, flutterAssetFile: String?) -> SCNNode { let plane = SCNPlane(width: CGFloat(anchor.extent.x), height: CGFloat(anchor.extent.z)) //Create material let material = SCNMaterial() let opacity: CGFloat if let textureSourcePath = flutterAssetFile { // Use given asset as plane texture let key = FlutterDartProject.lookupKey(forAsset: textureSourcePath) if let image = UIImage(named: key, in: Bundle.main,compatibleWith: nil){ // Asset was found so we can use it material.diffuse.contents = image material.diffuse.wrapS = .repeat material.diffuse.wrapT = .repeat plane.materials = [material] opacity = 1.0 } else { // Use standard planes opacity = 0.3 } } else { // Use standard planes opacity = 0.3 } let planeNode = SCNNode(geometry: plane) planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z) // rotate plane by 90 degrees to match the anchor (planes are vertical by default) planeNode.eulerAngles.x = -.pi / 2 planeNode.opacity = opacity return planeNode } func updatePlaneNode(planeNode: SCNNode, anchor: ARPlaneAnchor){ if let plane = planeNode.geometry as? SCNPlane { // Update plane dimensions plane.width = CGFloat(anchor.extent.x) plane.height = CGFloat(anchor.extent.z) // Update texture of planes let imageSize: Float = 65 // in mm let repeatAmount: Float = 1000 / imageSize //how often per meter we need to repeat the image if let gridMaterial = plane.materials.first { gridMaterial.diffuse.contentsTransform = SCNMatrix4MakeScale(anchor.extent.x * repeatAmount, anchor.extent.z * repeatAmount, 1) } } planeNode.position = SCNVector3Make(anchor.center.x, 0, anchor.center.z) } // Creates a node from a given gltf2 (.gltf) model in the Flutter assets folder func makeNodeFromGltf(name: String, modelPath: String, transformation: Array?) -> SCNNode? { var scene: SCNScene let node: SCNNode = SCNNode() do { let sceneSource = try GLTFSceneSource(named: modelPath) scene = try sceneSource.scene() for child in scene.rootNode.childNodes { child.scale = SCNVector3(0.01,0.01,0.01) // Compensate for the different model dimension definitions in iOS and Android (meters vs. millimeters) //child.eulerAngles.z = -.pi // Compensate for the different model coordinate definitions in iOS and Android //child.eulerAngles.y = -.pi // Compensate for the different model coordinate definitions in iOS and Android node.addChildNode(child.flattenedClone()) } node.name = name if let transform = transformation { node.transform = deserializeMatrix4(transform) } return node } catch { print("\(error.localizedDescription)") return nil } } // Creates a node from a given gltf2 (.gltf) model in the Flutter assets folder func makeNodeFromFileSystemGltf(name: String, modelPath: String, transformation: Array?) -> SCNNode? { var scene: SCNScene let node: SCNNode = SCNNode() do { let sceneSource = try GLTFSceneSource(path: modelPath) scene = try sceneSource.scene() for child in scene.rootNode.childNodes { child.scale = SCNVector3(0.01,0.01,0.01) // Compensate for the different model dimension definitions in iOS and Android (meters vs. millimeters) //child.eulerAngles.z = -.pi // Compensate for the different model coordinate definitions in iOS and Android //child.eulerAngles.y = -.pi // Compensate for the different model coordinate definitions in iOS and Android node.addChildNode(child.flattenedClone()) } node.name = name if let transform = transformation { node.transform = deserializeMatrix4(transform) } return node } catch { print("\(error.localizedDescription)") return nil } } // Creates a node from a given glb model in the app's documents directory func makeNodeFromFileSystemGLB(name: String, modelPath: String, transformation: Array?) -> SCNNode? { var scene: SCNScene let node: SCNNode = SCNNode() do { let sceneSource = try GLTFSceneSource(path: modelPath) scene = try sceneSource.scene() for child in scene.rootNode.childNodes { child.scale = SCNVector3(0.01,0.01,0.01) // Compensate for the different model dimension definitions in iOS and Android (meters vs. millimeters) //child.eulerAngles.z = -.pi // Compensate for the different model coordinate definitions in iOS and Android //child.eulerAngles.y = -.pi // Compensate for the different model coordinate definitions in iOS and Android node.addChildNode(child.flattenedClone()) } node.name = name if let transform = transformation { node.transform = deserializeMatrix4(transform) } return node } catch { print("\(error.localizedDescription)") return nil } } // Creates a node form a given glb model path func makeNodeFromWebGlb(name: String, modelURL: String, transformation: Array?) -> Future { return Future {promise in var node: SCNNode? = SCNNode() let handler: (URL?, URLResponse?, Error?) -> Void = {(url: URL?, urlResponse: URLResponse?, error: Error?) -> Void in // If response code is not 200, link was invalid, so return if ((urlResponse as? HTTPURLResponse)?.statusCode != 200) { print("makeNodeFromWebGltf received non-200 response code") node = nil promise(.success(node)) } else { guard let fileURL = url else { return } do { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let documentsDirectory = paths[0] let targetURL = documentsDirectory.appendingPathComponent(urlResponse!.url!.lastPathComponent) try? FileManager.default.removeItem(at: targetURL) //remove item if it's already there try FileManager.default.copyItem(at: fileURL, to: targetURL) do { let sceneSource = GLTFSceneSource(url: targetURL) let scene = try sceneSource.scene() for child in scene.rootNode.childNodes { child.scale = SCNVector3(0.01,0.01,0.01) // Compensate for the different model dimension definitions in iOS and Android (meters vs. millimeters) //child.eulerAngles.z = -.pi // Compensate for the different model coordinate definitions in iOS and Android //child.eulerAngles.y = -.pi // Compensate for the different model coordinate definitions in iOS and Android node?.addChildNode(child) } node?.name = name if let transform = transformation { node?.transform = deserializeMatrix4(transform) } /*node?.scale = worldScale node?.position = worldPosition node?.worldOrientation = worldRotation*/ } catch { print("\(error.localizedDescription)") node = nil } // Delete file to avoid cluttering device storage (at some point, caching can be included) try FileManager.default.removeItem(at: targetURL) promise(.success(node)) } catch { node = nil promise(.success(node)) } } } let downloadTask = URLSession.shared.downloadTask(with: URL(string: modelURL)!, completionHandler: handler) downloadTask.resume() } } } ================================================ FILE: ios/Classes/CloudAnchorHandler.swift ================================================ import Foundation import ARCoreCloudAnchors // Listener that can be attached to hosing or resolving processes protocol CloudAnchorListener { // Callback to invoke when cloud anchor task finishes func onCloudTaskComplete(anchorName: String?, anchor: GARAnchor?) } // Class for handling logic regarding the Google Cloud Anchor API class CloudAnchorHandler: NSObject, GARSessionDelegate { private var session: GARSession private var pendingAnchors = [GARAnchor: (String?, CloudAnchorListener?)]() init(session: GARSession){ self.session = session } func hostCloudAnchor(anchorName: String, anchor: ARAnchor, listener: CloudAnchorListener?) { do { let newAnchor = try self.session.hostCloudAnchor(anchor) // Register listener so it is invoked when the operation finishes pendingAnchors[newAnchor] = (anchorName, listener) } catch { print(error) } } func hostCloudAnchorWithTtl(anchorName: String, anchor: ARAnchor, listener: CloudAnchorListener?, ttl: Int) { do { let newAnchor = try self.session.hostCloudAnchor(anchor, ttlDays: ttl) // Register listener so it is invoked when the operation finishes pendingAnchors[newAnchor] = (anchorName, listener) } catch { print(error) } } func resolveCloudAnchor(anchorId: String, listener: CloudAnchorListener?) { do { let newAnchor = try self.session.resolveCloudAnchor(anchorId) // Register listener so it is invoked when the operation finishes pendingAnchors[newAnchor] = (nil, listener) } catch { print(error) } } func session(_ session: GARSession, didHost anchor: GARAnchor) { pendingAnchors[anchor]?.1?.onCloudTaskComplete(anchorName: pendingAnchors[anchor]?.0, anchor: anchor) } func session(_ session: GARSession, didFailToHost anchor: GARAnchor) { pendingAnchors[anchor]?.1?.onCloudTaskComplete(anchorName: pendingAnchors[anchor]?.0, anchor: anchor) } func session(_ session: GARSession, didResolve anchor: GARAnchor) { pendingAnchors[anchor]?.1?.onCloudTaskComplete(anchorName: pendingAnchors[anchor]?.0, anchor: anchor) } func session(_ session: GARSession, didFailToResolve anchor: GARAnchor) { pendingAnchors[anchor]?.1?.onCloudTaskComplete(anchorName: pendingAnchors[anchor]?.0, anchor: anchor) } // Remove all listeners func clearListeners() { pendingAnchors.removeAll() } } ================================================ FILE: ios/Classes/IosARView.swift ================================================ import Flutter import UIKit import Foundation import ARKit import Combine import ARCoreCloudAnchors class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureRecognizerDelegate, ARSessionDelegate { let sceneView: ARSCNView let coachingView: ARCoachingOverlayView let sessionManagerChannel: FlutterMethodChannel let objectManagerChannel: FlutterMethodChannel let anchorManagerChannel: FlutterMethodChannel var showPlanes = false var customPlaneTexturePath: String? = nil private var trackedPlanes = [UUID: (SCNNode, SCNNode)]() let modelBuilder = ArModelBuilder() var cancellableCollection = Set() //Used to store all cancellables in (needed for working with Futures) var anchorCollection = [String: ARAnchor]() //Used to bookkeep all anchors created by Flutter calls private var cloudAnchorHandler: CloudAnchorHandler? = nil private var arcoreSession: GARSession? = nil private var arcoreMode: Bool = false private var configuration: ARWorldTrackingConfiguration! private var tappedPlaneAnchorAlignment = ARPlaneAnchor.Alignment.horizontal // default alignment private var panStartLocation: CGPoint? private var panCurrentLocation: CGPoint? private var panCurrentVelocity: CGPoint? private var panCurrentTranslation: CGPoint? private var rotationStartLocation: CGPoint? private var rotation: CGFloat? private var rotationVelocity: CGFloat? private var panningNode: SCNNode? private var panningNodeCurrentWorldLocation: SCNVector3? init( frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?, binaryMessenger messenger: FlutterBinaryMessenger ) { self.sceneView = ARSCNView(frame: frame) self.coachingView = ARCoachingOverlayView(frame: frame) self.sessionManagerChannel = FlutterMethodChannel(name: "arsession_\(viewId)", binaryMessenger: messenger) self.objectManagerChannel = FlutterMethodChannel(name: "arobjects_\(viewId)", binaryMessenger: messenger) self.anchorManagerChannel = FlutterMethodChannel(name: "aranchors_\(viewId)", binaryMessenger: messenger) super.init() let configuration = ARWorldTrackingConfiguration() // Create default configuration before initializeARView is called self.sceneView.delegate = self self.coachingView.delegate = self self.sceneView.session.run(configuration) self.sceneView.session.delegate = self self.sessionManagerChannel.setMethodCallHandler(self.onSessionMethodCalled) self.objectManagerChannel.setMethodCallHandler(self.onObjectMethodCalled) self.anchorManagerChannel.setMethodCallHandler(self.onAnchorMethodCalled) } func view() -> UIView { return self.sceneView } func onDispose(_ result:FlutterResult) { sceneView.session.pause() self.sessionManagerChannel.setMethodCallHandler(nil) self.objectManagerChannel.setMethodCallHandler(nil) self.anchorManagerChannel.setMethodCallHandler(nil) result(nil) } func onSessionMethodCalled(_ call :FlutterMethodCall, _ result:FlutterResult) { let arguments = call.arguments as? Dictionary switch call.method { case "init": //self.sessionManagerChannel.invokeMethod("onError", arguments: ["SessionTEST from iOS"]) //result(nil) initializeARView(arguments: arguments!, result: result) break case "getCameraPose": if let cameraPose = sceneView.session.currentFrame?.camera.transform { result(serializeMatrix(cameraPose)) } else { result(FlutterError()) } break case "getAnchorPose": if let cameraPose = anchorCollection[arguments?["anchorId"] as! String]?.transform { result(serializeMatrix(cameraPose)) } else { result(FlutterError()) } break case "snapshot": // call the SCNView Snapshot method and return the Image let snapshotImage = sceneView.snapshot() if let bytes = snapshotImage.pngData() { let data = FlutterStandardTypedData(bytes:bytes) result(data) } else { result(nil) } case "dispose": onDispose(result) result(nil) break default: result(FlutterMethodNotImplemented) break } } func onObjectMethodCalled(_ call :FlutterMethodCall, _ result: @escaping FlutterResult) { let arguments = call.arguments as? Dictionary switch call.method { case "init": self.objectManagerChannel.invokeMethod("onError", arguments: ["ObjectTEST from iOS"]) result(nil) break case "addNode": addNode(dict_node: arguments!).sink(receiveCompletion: {completion in }, receiveValue: { val in result(val) }).store(in: &self.cancellableCollection) break case "addNodeToPlaneAnchor": if let dict_node = arguments!["node"] as? Dictionary, let dict_anchor = arguments!["anchor"] as? Dictionary { addNode(dict_node: dict_node, dict_anchor: dict_anchor).sink(receiveCompletion: {completion in }, receiveValue: { val in result(val) }).store(in: &self.cancellableCollection) } break case "removeNode": if let name = arguments!["name"] as? String { sceneView.scene.rootNode.childNode(withName: name, recursively: true)?.removeFromParentNode() } break case "transformationChanged": if let name = arguments!["name"] as? String, let transform = arguments!["transformation"] as? Array { transformNode(name: name, transform: transform) result(nil) } break default: result(FlutterMethodNotImplemented) break } } func onAnchorMethodCalled(_ call :FlutterMethodCall, _ result: @escaping FlutterResult) { let arguments = call.arguments as? Dictionary switch call.method { case "init": self.objectManagerChannel.invokeMethod("onError", arguments: ["ObjectTEST from iOS"]) result(nil) break case "addAnchor": if let type = arguments!["type"] as? Int { switch type { case 0: //Plane Anchor if let transform = arguments!["transformation"] as? Array, let name = arguments!["name"] as? String { addPlaneAnchor(transform: transform, name: name) result(true) } result(false) break default: result(false) } } result(nil) break case "removeAnchor": if let name = arguments!["name"] as? String { deleteAnchor(anchorName: name) } break case "initGoogleCloudAnchorMode": arcoreSession = try! GARSession.session() if (arcoreSession != nil){ let configuration = GARSessionConfiguration(); configuration.cloudAnchorMode = .enabled; arcoreSession?.setConfiguration(configuration, error: nil); if let token = JWTGenerator().generateWebToken(){ arcoreSession!.setAuthToken(token) cloudAnchorHandler = CloudAnchorHandler(session: arcoreSession!) arcoreSession!.delegate = cloudAnchorHandler arcoreSession!.delegateQueue = DispatchQueue.main arcoreMode = true } else { sessionManagerChannel.invokeMethod("onError", arguments: ["Error generating JWT, have you added cloudAnchorKey.json into the example/ios/Runner directory?"]) } } else { sessionManagerChannel.invokeMethod("onError", arguments: ["Error initializing Google AR Session"]) } break case "uploadAnchor": if let anchorName = arguments!["name"] as? String, let anchor = anchorCollection[anchorName] { print("---------------- HOSTING INITIATED ------------------") if let ttl = arguments!["ttl"] as? Int { cloudAnchorHandler?.hostCloudAnchorWithTtl(anchorName: anchorName, anchor: anchor, listener: cloudAnchorUploadedListener(parent: self), ttl: ttl) } else { cloudAnchorHandler?.hostCloudAnchor(anchorName: anchorName, anchor: anchor, listener: cloudAnchorUploadedListener(parent: self)) } } result(true) break case "downloadAnchor": if let anchorId = arguments!["cloudanchorid"] as? String { print("---------------- RESOLVING INITIATED ------------------") cloudAnchorHandler?.resolveCloudAnchor(anchorId: anchorId, listener: cloudAnchorDownloadedListener(parent: self)) } break default: result(FlutterMethodNotImplemented) break } } func initializeARView(arguments: Dictionary, result: FlutterResult){ // Set plane detection configuration self.configuration = ARWorldTrackingConfiguration() self.configuration.environmentTexturing = .automatic if let planeDetectionConfig = arguments["planeDetectionConfig"] as? Int { switch planeDetectionConfig { case 1: configuration.planeDetection = .horizontal case 2: if #available(iOS 11.3, *) { configuration.planeDetection = .vertical } case 3: if #available(iOS 11.3, *) { configuration.planeDetection = [.horizontal, .vertical] } default: configuration.planeDetection = [] } } // Set plane rendering options if let configShowPlanes = arguments["showPlanes"] as? Bool { showPlanes = configShowPlanes if (showPlanes){ // Visualize currently tracked planes for plane in trackedPlanes.values { plane.0.addChildNode(plane.1) } } else { // Remove currently visualized planes for plane in trackedPlanes.values { plane.1.removeFromParentNode() } } } if let configCustomPlaneTexturePath = arguments["customPlaneTexturePath"] as? String { customPlaneTexturePath = configCustomPlaneTexturePath } // Set debug options var debugOptions = ARSCNDebugOptions().rawValue if let showFeaturePoints = arguments["showFeaturePoints"] as? Bool { if (showFeaturePoints) { debugOptions |= ARSCNDebugOptions.showFeaturePoints.rawValue } } if let showWorldOrigin = arguments["showWorldOrigin"] as? Bool { if (showWorldOrigin) { debugOptions |= ARSCNDebugOptions.showWorldOrigin.rawValue } } self.sceneView.debugOptions = ARSCNDebugOptions(rawValue: debugOptions) if let configHandleTaps = arguments["handleTaps"] as? Bool { if (configHandleTaps){ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) tapGestureRecognizer.delegate = self self.sceneView.gestureRecognizers?.append(tapGestureRecognizer) } } if let configHandlePans = arguments["handlePans"] as? Bool { if (configHandlePans){ let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) panGestureRecognizer.maximumNumberOfTouches = 1 panGestureRecognizer.delegate = self self.sceneView.gestureRecognizers?.append(panGestureRecognizer) } } if let configHandleRotation = arguments["handleRotation"] as? Bool { if (configHandleRotation){ let rotationGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:))) rotationGestureRecognizer.delegate = self self.sceneView.gestureRecognizers?.append(rotationGestureRecognizer) } } // Add coaching view if let configShowAnimatedGuide = arguments["showAnimatedGuide"] as? Bool { if configShowAnimatedGuide { if self.sceneView.superview != nil && self.coachingView.superview == nil { self.sceneView.addSubview(self.coachingView) // self.coachingView.translatesAutoresizingMaskIntoConstraints = false self.coachingView.autoresizingMask = [ .flexibleWidth, .flexibleHeight ] self.coachingView.session = self.sceneView.session self.coachingView.activatesAutomatically = true if configuration.planeDetection == .horizontal { self.coachingView.goal = .horizontalPlane }else{ self.coachingView.goal = .verticalPlane } // TODO: look into constraints issue. This causes a crash: /** Terminating app due to uncaught exception 'NSGenericException', reason: 'Unable to activate constraint with anchors and because they have no common ancestor. Does the constraint or its anchors reference items in different view hierarchies? That's illegal.' */ // NSLayoutConstraint.activate([ // self.coachingView.centerXAnchor.constraint(equalTo: self.sceneView.superview!.centerXAnchor), // self.coachingView.centerYAnchor.constraint(equalTo: self.sceneView.superview!.centerYAnchor), // self.coachingView.widthAnchor.constraint(equalTo: self.sceneView.superview!.widthAnchor), // self.coachingView.heightAnchor.constraint(equalTo: self.sceneView.superview!.heightAnchor) // ]) } } } // Update session configuration self.sceneView.session.run(configuration) } func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if let planeAnchor = anchor as? ARPlaneAnchor{ let plane = modelBuilder.makePlane(anchor: planeAnchor, flutterAssetFile: customPlaneTexturePath) trackedPlanes[anchor.identifier] = (node, plane) if (showPlanes) { node.addChildNode(plane) } } } func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) { if let planeAnchor = anchor as? ARPlaneAnchor, let plane = trackedPlanes[anchor.identifier] { modelBuilder.updatePlaneNode(planeNode: plane.1, anchor: planeAnchor) } } func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) { trackedPlanes.removeValue(forKey: anchor.identifier) } func session(_ session: ARSession, didUpdate frame: ARFrame) { if (arcoreMode) { do { try arcoreSession!.update(frame) } catch { print(error) } } } func addNode(dict_node: Dictionary, dict_anchor: Dictionary? = nil) -> Future { return Future {promise in switch (dict_node["type"] as! Int) { case 0: // GLTF2 Model from Flutter asset folder // Get path to given Flutter asset let key = FlutterDartProject.lookupKey(forAsset: dict_node["uri"] as! String) // Add object to scene if let node: SCNNode = self.modelBuilder.makeNodeFromGltf(name: dict_node["name"] as! String, modelPath: key, transformation: dict_node["transformation"] as? Array) { if let anchorName = dict_anchor?["name"] as? String, let anchorType = dict_anchor?["type"] as? Int { switch anchorType{ case 0: //PlaneAnchor if let anchor = self.anchorCollection[anchorName]{ // Attach node to the top-level node of the specified anchor self.sceneView.node(for: anchor)?.addChildNode(node) promise(.success(true)) } else { promise(.success(false)) } default: promise(.success(false)) } } else { // Attach to top-level node of the scene self.sceneView.scene.rootNode.addChildNode(node) promise(.success(true)) } promise(.success(false)) } else { self.sessionManagerChannel.invokeMethod("onError", arguments: ["Unable to load renderable \(dict_node["uri"] as! String)"]) promise(.success(false)) } break case 1: // GLB Model from the web // Add object to scene self.modelBuilder.makeNodeFromWebGlb(name: dict_node["name"] as! String, modelURL: dict_node["uri"] as! String, transformation: dict_node["transformation"] as? Array) .sink(receiveCompletion: { completion in print("Async Model Downloading Task completed: ", completion) }, receiveValue: { val in if let node: SCNNode = val { if let anchorName = dict_anchor?["name"] as? String, let anchorType = dict_anchor?["type"] as? Int { switch anchorType{ case 0: //PlaneAnchor if let anchor = self.anchorCollection[anchorName]{ // Attach node to the top-level node of the specified anchor self.sceneView.node(for: anchor)?.addChildNode(node) promise(.success(true)) } else { promise(.success(false)) } default: promise(.success(false)) } } else { // Attach to top-level node of the scene self.sceneView.scene.rootNode.addChildNode(node) promise(.success(true)) } promise(.success(false)) } else { self.sessionManagerChannel.invokeMethod("onError", arguments: ["Unable to load renderable \(dict_node["name"] as! String)"]) promise(.success(false)) } }).store(in: &self.cancellableCollection) break case 2: // GLB Model from the app's documents folder // Get path to given file system asset let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let documentsDirectory = paths[0] let targetPath = documentsDirectory.appendingPathComponent(dict_node["uri"] as! String).path // Add object to scene if let node: SCNNode = self.modelBuilder.makeNodeFromFileSystemGLB(name: dict_node["name"] as! String, modelPath: targetPath, transformation: dict_node["transformation"] as? Array) { if let anchorName = dict_anchor?["name"] as? String, let anchorType = dict_anchor?["type"] as? Int { switch anchorType{ case 0: //PlaneAnchor if let anchor = self.anchorCollection[anchorName]{ // Attach node to the top-level node of the specified anchor self.sceneView.node(for: anchor)?.addChildNode(node) promise(.success(true)) } else { promise(.success(false)) } default: promise(.success(false)) } } else { // Attach to top-level node of the scene self.sceneView.scene.rootNode.addChildNode(node) promise(.success(true)) } promise(.success(false)) } else { self.sessionManagerChannel.invokeMethod("onError", arguments: ["Unable to load renderable \(dict_node["uri"] as! String)"]) promise(.success(false)) } break case 3: //fileSystemAppFolderGLTF2 // Get path to given file system asset let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) let documentsDirectory = paths[0] let targetPath = documentsDirectory.appendingPathComponent(dict_node["uri"] as! String).path // Add object to scene if let node: SCNNode = self.modelBuilder.makeNodeFromFileSystemGltf(name: dict_node["name"] as! String, modelPath: targetPath, transformation: dict_node["transformation"] as? Array) { if let anchorName = dict_anchor?["name"] as? String, let anchorType = dict_anchor?["type"] as? Int { switch anchorType{ case 0: //PlaneAnchor if let anchor = self.anchorCollection[anchorName]{ // Attach node to the top-level node of the specified anchor self.sceneView.node(for: anchor)?.addChildNode(node) promise(.success(true)) } else { promise(.success(false)) } default: promise(.success(false)) } } else { // Attach to top-level node of the scene self.sceneView.scene.rootNode.addChildNode(node) promise(.success(true)) } promise(.success(false)) } else { self.sessionManagerChannel.invokeMethod("onError", arguments: ["Unable to load renderable \(dict_node["uri"] as! String)"]) promise(.success(false)) } break default: promise(.success(false)) } } } func transformNode(name: String, transform: Array) { let node = sceneView.scene.rootNode.childNode(withName: name, recursively: true) node?.transform = deserializeMatrix4(transform) } @objc func handleTap(_ recognizer: UITapGestureRecognizer) { guard let sceneView = recognizer.view as? ARSCNView else { return } let touchLocation = recognizer.location(in: sceneView) let allHitResults = sceneView.hitTest(touchLocation, options: [SCNHitTestOption.searchMode : SCNHitTestSearchMode.closest.rawValue]) // Because 3D model loading can lead to composed nodes, we have to traverse through a node's parent until the parent node with the name assigned by the Flutter API is found let nodeHitResults: Array = allHitResults.compactMap { nearestParentWithNameStart(node: $0.node, characters: "[#")?.name } if (nodeHitResults.count != 0) { self.objectManagerChannel.invokeMethod("onNodeTap", arguments: Array(Set(nodeHitResults))) // Chaining of Array and Set is used to remove duplicates return } let planeTypes: ARHitTestResult.ResultType if #available(iOS 11.3, *){ planeTypes = ARHitTestResult.ResultType([.existingPlaneUsingGeometry, .featurePoint]) }else { planeTypes = ARHitTestResult.ResultType([.existingPlaneUsingExtent, .featurePoint]) } let planeAndPointHitResults = sceneView.hitTest(touchLocation, types: planeTypes) // store the alignment of the tapped plane anchor so we can refer to is later when transforming the node if planeAndPointHitResults.count > 0, let hitAnchor = planeAndPointHitResults.first?.anchor as? ARPlaneAnchor { self.tappedPlaneAnchorAlignment = hitAnchor.alignment } let serializedPlaneAndPointHitResults = planeAndPointHitResults.map{serializeHitResult($0)} if (serializedPlaneAndPointHitResults.count != 0) { self.sessionManagerChannel.invokeMethod("onPlaneOrPointTap", arguments: serializedPlaneAndPointHitResults) } } @objc func handlePan(_ recognizer: UIPanGestureRecognizer) { guard let sceneView = recognizer.view as? ARSCNView else { return } // State Begins if recognizer.state == UIGestureRecognizer.State.began { panStartLocation = recognizer.location(in: sceneView) if let startLocation = panStartLocation { let allHitResults = sceneView.hitTest(startLocation, options: [SCNHitTestOption.searchMode : SCNHitTestSearchMode.closest.rawValue]) // Because 3D model loading can lead to composed nodes, we have to traverse through a node's parent until the parent node with the name assigned by the Flutter API is found let nodeHitResults: Array = allHitResults.compactMap { if let nearestNode = nearestParentWithNameStart(node: $0.node, characters: "[#") { panningNode = nearestNode return nearestNode.name }else{ return nil } } if (nodeHitResults.count != 0 && panningNode != nil) { panningNodeCurrentWorldLocation = panningNode!.worldPosition self.objectManagerChannel.invokeMethod("onPanStart", arguments: panningNode!.name) // Chaining of Array and Set is used to remove duplicates return } } } // State Changes if(recognizer.state == UIGestureRecognizer.State.changed) { // the velocity of the gesture is how fast it is moving. This can be used to translate the position of the node. panCurrentVelocity = recognizer.velocity(in: sceneView) panCurrentLocation = recognizer.location(in: sceneView) panCurrentTranslation = recognizer.translation(in: sceneView) if let panLoc = panCurrentLocation, let panNode = panningNode { if let query = sceneView.raycastQuery(from: panLoc, allowing: .estimatedPlane, alignment: .any) { guard let result = self.sceneView.session.raycast(query).first else { return } let posX = result.worldTransform.columns.3.x let posY = result.worldTransform.columns.3.y let posZ = result.worldTransform.columns.3.z panNode.worldPosition = SCNVector3(posX, posY, posZ) } self.objectManagerChannel.invokeMethod("onPanChange", arguments: panNode.name) } } // State Ended if(recognizer.state == UIGestureRecognizer.State.ended) { // kill variables panStartLocation = nil panCurrentLocation = nil self.objectManagerChannel.invokeMethod("onPanEnd", arguments: serializeLocalTransformation(node: panningNode)) panningNode = nil } } @objc func handleRotation(_ recognizer: UIRotationGestureRecognizer) { guard let sceneView = recognizer.view as? ARSCNView else { return } // State Begins if recognizer.state == UIGestureRecognizer.State.began { rotationStartLocation = recognizer.location(in: sceneView) if let startLocation = rotationStartLocation { let allHitResults = sceneView.hitTest(startLocation, options: [SCNHitTestOption.searchMode : SCNHitTestSearchMode.closest.rawValue]) // Because 3D model loading can lead to composed nodes, we have to traverse through a node's parent until the parent node with the name assigned by the Flutter API is found let nodeHitResults: Array = allHitResults.compactMap { if let nearestNode = nearestParentWithNameStart(node: $0.node, characters: "[#") { panningNode = nearestNode return nearestNode.name }else{ return nil } } if (nodeHitResults.count != 0 && panningNode != nil) { self.objectManagerChannel.invokeMethod("onRotationStart", arguments: panningNode!.name) // Chaining of Array and Set is used to remove duplicates return } } } // State Changes if(recognizer.state == UIGestureRecognizer.State.changed) { // the velocity of the gesture is how fast it is moving. This can be used to translate the position of the node. rotation = recognizer.rotation rotationVelocity = recognizer.velocity if let r = rotationVelocity, let panNode = panningNode { // velocity needs to be reduced substantially otherwise the rotation change seems too fast as radians; also needs inverting to match the movement of the fingers as they rotate on the screen let r2 = (r*0.01) * -1 let nodeRotation = panNode.rotation let rotation: SCNQuaternion! let planeAlignment = self.tappedPlaneAnchorAlignment if planeAlignment == .horizontal { rotation = SCNQuaternion(x: 0, y: 1, z: 0, w: nodeRotation.w+Float(r2)) // quickest way to convert screen into world positions (meters) }else{ rotation = SCNQuaternion(x: 0, y: 0, z: 1, w: nodeRotation.w+Float(r2)) // quickest way to convert screen into world positions (meters) } panNode.rotation = rotation self.objectManagerChannel.invokeMethod("onRotationChange", arguments: panNode.name) } // update position of panning node if it has been created // panningNode.position + the gesture delta } // State Ended if(recognizer.state == UIGestureRecognizer.State.ended) { // kill variables rotation = nil rotationVelocity = nil self.objectManagerChannel.invokeMethod("onRotationEnd", arguments: serializeLocalTransformation(node: panningNode)) panningNode = nil } } // Recursive helper function to traverse a node's parents until a node with a name starting with the specified characters is found func nearestParentWithNameStart(node: SCNNode?, characters: String) -> SCNNode? { if let nodeNamePrefix = node?.name?.prefix(characters.count) { if (nodeNamePrefix == characters) { return node } } if let parent = node?.parent { return nearestParentWithNameStart(node: parent, characters: characters) } return nil } func addPlaneAnchor(transform: Array, name: String){ let arAnchor = ARAnchor(transform: simd_float4x4(deserializeMatrix4(transform))) anchorCollection[name] = arAnchor sceneView.session.add(anchor: arAnchor) // Ensure root node is added to anchor before any other function can run (if this isn't done, addNode could fail because anchor does not have a root node yet). // The root node is added to the anchor as soon as the async rendering loop runs once, more specifically the function "renderer(_:nodeFor:)" while (sceneView.node(for: arAnchor) == nil) { usleep(1) // wait 1 millionth of a second } } func deleteAnchor(anchorName: String) { if let anchor = anchorCollection[anchorName]{ // Delete all child nodes if var attachedNodes = sceneView.node(for: anchor)?.childNodes { attachedNodes.removeAll() } // Remove anchor sceneView.session.remove(anchor: anchor) // Update bookkeeping anchorCollection.removeValue(forKey: anchorName) } } private class cloudAnchorUploadedListener: CloudAnchorListener { private var parent: IosARView init(parent: IosARView) { self.parent = parent } func onCloudTaskComplete(anchorName: String?, anchor: GARAnchor?) { if let cloudState = anchor?.cloudState { if (cloudState == GARCloudAnchorState.success) { var args = Dictionary() args["name"] = anchorName args["cloudanchorid"] = anchor?.cloudIdentifier parent.anchorManagerChannel.invokeMethod("onCloudAnchorUploaded", arguments: args) } else { print("Error uploading anchor, state: \(parent.decodeCloudAnchorState(state: cloudState))") parent.sessionManagerChannel.invokeMethod("onError", arguments: ["Error uploading anchor, state: \(parent.decodeCloudAnchorState(state: cloudState))"]) return } } } } private class cloudAnchorDownloadedListener: CloudAnchorListener { private var parent: IosARView init(parent: IosARView) { self.parent = parent } func onCloudTaskComplete(anchorName: String?, anchor: GARAnchor?) { if let cloudState = anchor?.cloudState { if (cloudState == GARCloudAnchorState.success) { let newAnchor = ARAnchor(transform: anchor!.transform) // Register new anchor on the Flutter side of the plugin parent.anchorManagerChannel.invokeMethod("onAnchorDownloadSuccess", arguments: serializeAnchor(anchor: newAnchor, anchorNode: nil, ganchor: anchor!, name: anchorName), result: { result in if let anchorName = result as? String { self.parent.sceneView.session.add(anchor: newAnchor) self.parent.anchorCollection[anchorName] = newAnchor } else { self.parent.sessionManagerChannel.invokeMethod("onError", arguments: ["Error while registering downloaded anchor at the AR Flutter plugin"]) } }) } else { print("Error downloading anchor, state \(cloudState)") parent.sessionManagerChannel.invokeMethod("onError", arguments: ["Error downloading anchor, state \(cloudState)"]) return } } } } func decodeCloudAnchorState(state: GARCloudAnchorState) -> String { switch state { case .errorCloudIdNotFound: return "Cloud anchor id not found" case .errorHostingDatasetProcessingFailed: return "Dataset processing failed, feature map insufficient" case .errorHostingServiceUnavailable: return "Hosting service unavailable" case .errorInternal: return "Internal error" case .errorNotAuthorized: return "Authentication failed: Not Authorized" case .errorResolvingSdkVersionTooNew: return "Resolving Sdk version too new" case .errorResolvingSdkVersionTooOld: return "Resolving Sdk version too old" case .errorResourceExhausted: return " Resource exhausted" case .none: return "Empty state" case .taskInProgress: return "Task in progress" case .success: return "Success" case .errorServiceUnavailable: return "Cloud Anchor Service unavailable" case .errorResolvingLocalizationNoMatch: return "No match" @unknown default: return "Unknown" } } } // ---------------------- ARCoachingOverlayViewDelegate --------------------------------------- extension IosARView: ARCoachingOverlayViewDelegate { func coachingOverlayViewWillActivate(_ coachingOverlayView: ARCoachingOverlayView){ // use this delegate method to hide anything in the UI that could cover the coaching overlay view } func coachingOverlayViewDidRequestSessionReset(_ coachingOverlayView: ARCoachingOverlayView) { // Reset the session. self.sceneView.session.run(configuration, options: [.resetTracking]) } } ================================================ FILE: ios/Classes/IosARViewFactory.swift ================================================ import Flutter import UIKit class IosARViewFactory: NSObject, FlutterPlatformViewFactory { private var messenger: FlutterBinaryMessenger init(messenger: FlutterBinaryMessenger) { self.messenger = messenger super.init() } func create( withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any? ) -> FlutterPlatformView { return IosARView( frame: frame, viewIdentifier: viewId, arguments: args, binaryMessenger: messenger) } } ================================================ FILE: ios/Classes/JWTGenerator.swift ================================================ // // JWTGenerator.swift // ar_flutter_plugin // // Created by Lars Carius on 08.04.21. // import Foundation import SwiftJWT class JWTGenerator { func generateWebToken() -> String? { if let path = Bundle.main.path(forResource: "cloudAnchorKey", ofType: "json") { do { let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) if let jsonResult = jsonResult as? Dictionary, let type = jsonResult["type"] as? String, let projectId = jsonResult["project_id"] as? String, let privateKeyId = jsonResult["private_key_id"] as? String, let privateKey = jsonResult["private_key"] as? String, let clientEmail = jsonResult["client_email"] as? String, let clientId = jsonResult["client_id"] as? String, let authUri = jsonResult["auth_uri"] as? String, let tokenUri = jsonResult["token_uri"] as? String, let authProviderX509CertUrl = jsonResult["auth_provider_x509_cert_url"] as? String, let clientX509CertUrl = jsonResult["client_x509_cert_url"] as? String{ let jwtTokenHeader = Header(typ: type, jku: tokenUri, kid: privateKeyId, x5u: authProviderX509CertUrl, x5c: [clientX509CertUrl]) struct JWTTokenClaims: Claims { let iss: String let sub: String let iat: Date let exp: Date let aud: String } let jwtTokenClaims = JWTTokenClaims(iss: clientEmail, sub: clientEmail, iat: Date(), exp: Date(timeIntervalSinceNow: 3600), aud: "https://arcorecloudanchor.googleapis.com/") var jwtToken = JWT(header: jwtTokenHeader, claims: jwtTokenClaims) // Sign Token let jwtSigner = JWTSigner.rs256(privateKey: privateKey.data(using: String.Encoding.ascii)!) let signedJwtToken = try jwtToken.sign(using: jwtSigner) return signedJwtToken } } catch { print("Error generating JWT") } } return nil } } ================================================ FILE: ios/Classes/Serialization/Deserializers.swift ================================================ // The code in this file is adapted from Oleksandr Leuschenko' ARKit Flutter Plugin (https://github.com/olexale/arkit_flutter_plugin) import ARKit func deserializeVector3(_ coords: Array) -> SCNVector3 { let point = SCNVector3(coords[0], coords[1], coords[2]) return point } func deserializeVector4(_ coords: Array) -> SCNVector4 { let point = SCNVector4(coords[0], coords[1], coords[2], coords[3]) return point } func deserializeMatrix4(_ c: Array) -> SCNMatrix4 { let coords = c.map({ Float(truncating: $0 )}) let matrix = SCNMatrix4(m11: coords[0], m12: coords[1],m13: coords[2], m14: coords[3], m21: coords[4], m22: coords[5], m23: coords[6], m24: coords[7], m31: coords[8], m32: coords[9], m33: coords[10], m34: coords[11], m41: coords[12], m42: coords[13], m43: coords[14], m44: coords[15]) return matrix } ================================================ FILE: ios/Classes/Serialization/Serializers.swift ================================================ import Foundation import ARKit import ARCoreCloudAnchors func serializeHitResult(_ result: ARHitTestResult) -> Dictionary { var hitResult = Dictionary(minimumCapacity: 3) if (result.type == .existingPlaneUsingExtent || result.type == .existingPlaneUsingGeometry || result.type == .existingPlane) { hitResult["type"] = 1 // Type plane } else if (result.type == .featurePoint) { hitResult["type"] = 2 // Type point } else { hitResult["type"] = 0 // Type undefined } hitResult["distance"] = result.distance hitResult["worldTransform"] = serializeMatrix(result.worldTransform) return hitResult } // The following code is adapted from Oleksandr Leuschenko' ARKit Flutter Plugin (https://github.com/olexale/arkit_flutter_plugin) func serializeMatrix(_ matrix: simd_float4x4) -> Array { return [matrix.columns.0, matrix.columns.1, matrix.columns.2, matrix.columns.3].flatMap { serializeArray($0) } } func serializeArray(_ array: simd_float4) -> Array { return [array[0], array[1], array[2], array[3]] } func serializeAnchor(anchor: ARAnchor, anchorNode: SCNNode?, ganchor: GARAnchor, name: String?) -> Dictionary { var serializedAnchor = Dictionary() serializedAnchor["type"] = 0 // index for plane anchors serializedAnchor["name"] = name serializedAnchor["cloudanchorid"] = ganchor.cloudIdentifier serializedAnchor["transformation"] = serializeMatrix(anchor.transform) serializedAnchor["childNodes"] = anchorNode?.childNodes.map{$0.name} return serializedAnchor } func serializeLocalTransformation(node: SCNNode?) -> Dictionary { var serializedLocalTransformation = Dictionary() let transform: [Float?] = [node?.transform.m11, node?.transform.m12, node?.transform.m13, node?.transform.m14, node?.transform.m21, node?.transform.m22, node?.transform.m23, node?.transform.m24, node?.transform.m31, node?.transform.m32, node?.transform.m33, node?.transform.m34, node?.transform.m41, node?.transform.m42, node?.transform.m43, node?.transform.m44] serializedLocalTransformation["name"] = node?.name serializedLocalTransformation["transform"] = transform return serializedLocalTransformation } ================================================ FILE: ios/Classes/SwiftArFlutterPlugin.swift ================================================ import Flutter import UIKit public class SwiftArFlutterPlugin: NSObject, FlutterPlugin { public static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: "ar_flutter_plugin", binaryMessenger: registrar.messenger()) let instance = SwiftArFlutterPlugin() registrar.addMethodCallDelegate(instance, channel: channel) let factory = IosARViewFactory(messenger: registrar.messenger()) registrar.register(factory, withId: "ar_flutter_plugin") } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { result("iOS " + UIDevice.current.systemVersion) } } ================================================ FILE: ios/ar_flutter_plugin.podspec ================================================ # # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. # Run `pod lib lint ar_flutter_plugin.podspec` to validate before publishing. # Pod::Spec.new do |s| s.name = 'ar_flutter_plugin' s.version = '0.6.2' s.summary = 'A Flutter plugin for shared AR experiences.' s.description = <<-DESC A Flutter plugin for shared AR experiences supporting Android and iOS. DESC s.homepage = 'https://lars.carius.io' s.license = { :file => '../LICENSE' } s.author = { 'Lars Carius' => 'carius.lars@gmail.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' s.dependency 'GLTFSceneKit' s.dependency 'SwiftJWT' s.static_framework = true #s.dependency 'ARCore/CloudAnchors', '~> 1.12.0' #s.dependency 'ARCore', '~> 1.2.0' s.dependency 'ARCore/CloudAnchors', '~> 1.33.0' # Updated from 1.32 to 1.33 to support Apple Silicon, info here: https://github.com/google-ar/arcore-ios-sdk/issues/59#issuecomment-1219756010 s.platform = :ios, '13.0' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } s.swift_version = '5.0' end ================================================ FILE: lib/ar_flutter_plugin.dart ================================================ export 'package:ar_flutter_plugin/widgets/ar_view.dart'; import 'dart:async'; import 'package:flutter/services.dart'; class ArFlutterPlugin { static const MethodChannel _channel = const MethodChannel('ar_flutter_plugin'); /// Private constructor to prevent accidental instantiation of the Plugin using the implicit default constructor ArFlutterPlugin._(); static Future get platformVersion async { final String version = await _channel.invokeMethod('getPlatformVersion'); return version; } } ================================================ FILE: lib/datatypes/anchor_types.dart ================================================ /// Determines which types of anchors the plugin supports enum AnchorType { plane, //Additional anchor Types can be added here (e.g. imageAnchor, faceAnchor, bodyAnchor,etc.) } ================================================ FILE: lib/datatypes/config_planedetection.dart ================================================ /// Determines which types of planes ARCore and ARKit should track enum PlaneDetectionConfig { none, horizontal, vertical, horizontalAndVertical, } ================================================ FILE: lib/datatypes/hittest_result_types.dart ================================================ /// Determines which types of hit results the plugin supports enum ARHitTestResultType { undefined, plane, point, } ================================================ FILE: lib/datatypes/node_types.dart ================================================ /// Determines which types of nodes the plugin supports enum NodeType { localGLTF2, // Node with renderable with fileending .gltf in the Flutter asset folder webGLB, // Node with renderable with fileending .glb loaded from the internet during runtime fileSystemAppFolderGLB, // Node with renderable with fileending .glb in the documents folder of the current app fileSystemAppFolderGLTF2, // Node with renderable with fileending .gltf in the documents folder of the current app } ================================================ FILE: lib/managers/ar_anchor_manager.dart ================================================ import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; // Type definitions to enforce a consistent use of the API typedef AnchorUploadedHandler = void Function(ARAnchor arAnchor); typedef AnchorDownloadedHandler = ARAnchor Function( Map serializedAnchor); /// Handles all anchor-related functionality of an [ARView], including configuration and usage of collaborative sessions class ARAnchorManager { /// Platform channel used for communication from and to [ARAnchorManager] late MethodChannel _channel; /// Debugging status flag. If true, all platform calls are printed. Defaults to false. final bool debug; /// Reference to all anchors that are being uploaded to the google cloud anchor API List pendingAnchors = []; /// Callback that is triggered once an anchor has successfully been uploaded to the google cloud anchor API AnchorUploadedHandler? onAnchorUploaded; /// Callback that is triggered once an anchor has successfully been downloaded from the google cloud anchor API and resolved within the current scene AnchorDownloadedHandler? onAnchorDownloaded; ARAnchorManager(int id, {this.debug = false}) { _channel = MethodChannel('aranchors_$id'); _channel.setMethodCallHandler(_platformCallHandler); if (debug) { print("ARAnchorManager initialized"); } } /// Activates collaborative AR mode (using Google Cloud Anchors) initGoogleCloudAnchorMode() async { _channel.invokeMethod('initGoogleCloudAnchorMode', {}); } Future _platformCallHandler(MethodCall call) async { if (debug) { print('_platformCallHandler call ${call.method} ${call.arguments}'); } try { switch (call.method) { case 'onError': print(call.arguments); break; case 'onCloudAnchorUploaded': final name = call.arguments["name"]; final cloudanchorid = call.arguments["cloudanchorid"]; print( "UPLOADED ANCHOR WITH ID: " + cloudanchorid + ", NAME: " + name); final currentAnchor = pendingAnchors.where((element) => element.name == name).first; // Update anchor with cloud anchor ID (currentAnchor as ARPlaneAnchor).cloudanchorid = cloudanchorid; // Remove anchor from list of pending anchors pendingAnchors.remove(currentAnchor); // Notify callback if (onAnchorUploaded != null) { onAnchorUploaded!(currentAnchor); } break; case "onAnchorDownloadSuccess": final serializedAnchor = call.arguments; if (onAnchorDownloaded != null) { ARAnchor anchor = onAnchorDownloaded!( Map.from(serializedAnchor)); return anchor.name; } else { return serializedAnchor["name"]; } default: if (debug) { print('Unimplemented method ${call.method} '); } } } catch (e) { print('Error caught: ' + e.toString()); } return Future.value(); } /// Add given anchor to the underlying AR scene Future addAnchor(ARAnchor anchor) async { try { return await _channel.invokeMethod('addAnchor', anchor.toJson()); } on PlatformException catch (e) { return false; } } /// Remove given anchor and all its children from the AR Scene removeAnchor(ARAnchor anchor) { _channel.invokeMethod('removeAnchor', {'name': anchor.name}); } /// Upload given anchor from the underlying AR scene to the Google Cloud Anchor API Future uploadAnchor(ARAnchor anchor) async { try { final response = await _channel.invokeMethod('uploadAnchor', anchor.toJson()); pendingAnchors.add(anchor); return response; } on PlatformException catch (e) { return false; } } /// Try to download anchor with the given ID from the Google Cloud Anchor API and add it to the scene Future downloadAnchor(String cloudanchorid) async { print("TRYING TO DOWNLOAD ANCHOR WITH ID " + cloudanchorid); _channel .invokeMethod('downloadAnchor', {"cloudanchorid": cloudanchorid}); } } ================================================ FILE: lib/managers/ar_location_manager.dart ================================================ import 'dart:async'; import 'package:geolocator/geolocator.dart'; /// Can be used to get the current location of the device, update it and handle location permissions class ARLocationManager { late Position currentLocation; late StreamSubscription locationStream; /// Returns the last known location of the device or an error, if permissions don't suffice. Automatically queries user permission if possible Future getLastKnownPosition() async { bool serviceEnabled; LocationPermission permission; // Test if location services are enabled. serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // Location services are not enabled don't continue // accessing the position and request users of the // App to enable the location services. return Future.error('Location services disabled'); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Permissions are denied, next time you could try // requesting permissions again (this is also where // Android's shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. return Future.error('Location permissions denied'); } } if (permission == LocationPermission.deniedForever) { // Permissions are denied forever, handle appropriately. return Future.error('Location permissions permanently denied'); } // When we reach here, permissions are granted and we can // continue accessing the last known position of the device. return await Geolocator.getLastKnownPosition(); } /// Starts high precision location updates to keep track of the device's position. Returns true or an error, if permissions don't suffice. Automatically queries user permission if possible Future startLocationUpdates() async { bool serviceEnabled; LocationPermission permission; // Test if location services are enabled. serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // Location services are not enabled don't continue // accessing the position and request users of the // App to enable the location services. return Future.error('Location services disabled'); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Permissions are denied, next time you could try // requesting permissions again (this is also where // Android's shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. return Future.error('Location permissions denied'); } } if (permission == LocationPermission.deniedForever) { // Permissions are denied forever, handle appropriately. return Future.error('Location permissions permanently denied'); } // When we reach here, permissions are granted and we can // continue accessing the position of the device. locationStream = Geolocator.getPositionStream(locationSettings: LocationSettings(accuracy: LocationAccuracy.high)) .listen((Position position) { //print(position.latitude.toString() + ', ' + position.longitude.toString()); currentLocation = position; }); return true; } /// Stops the high-precision location updates void stopLocationUpdates() { locationStream.cancel(); } /// Opens the settings of the current application void openAppPermissionSettings() async { await Geolocator.openAppSettings(); } /// Opens the device settings where location services can be enabled void openLocationServicesSettings() async { await Geolocator.openLocationSettings(); } /// Determines the current position of the device. /// /// When the location services are not enabled or permissions /// are denied the `Future` will return an error. Future _determinePosition() async { bool serviceEnabled; LocationPermission permission; // Test if location services are enabled. serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { // Location services are not enabled don't continue // accessing the position and request users of the // App to enable the location services. return Future.error('Location services disabled'); } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { // Permissions are denied, next time you could try // requesting permissions again (this is also where // Android's shouldShowRequestPermissionRationale // returned true. According to Android guidelines // your App should show an explanatory UI now. return Future.error('Location permissions denied'); } } if (permission == LocationPermission.deniedForever) { // Permissions are denied forever, handle appropriately. return Future.error('Location permissions permanently denied'); } // When we reach here, permissions are granted and we can // continue accessing the position of the device. return await Geolocator.getCurrentPosition(); } } ================================================ FILE: lib/managers/ar_object_manager.dart ================================================ import 'dart:typed_data'; import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:ar_flutter_plugin/utils/json_converters.dart'; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; // Type definitions to enforce a consistent use of the API typedef NodeTapResultHandler = void Function(List nodes); typedef NodePanStartHandler = void Function(String node); typedef NodePanChangeHandler = void Function(String node); typedef NodePanEndHandler = void Function(String node, Matrix4 transform); typedef NodeRotationStartHandler = void Function(String node); typedef NodeRotationChangeHandler = void Function(String node); typedef NodeRotationEndHandler = void Function(String node, Matrix4 transform); /// Manages the all node-related actions of an [ARView] class ARObjectManager { /// Platform channel used for communication from and to [ARObjectManager] late MethodChannel _channel; /// Debugging status flag. If true, all platform calls are printed. Defaults to false. final bool debug; /// Callback function that is invoked when the platform detects a tap on a node NodeTapResultHandler? onNodeTap; NodePanStartHandler? onPanStart; NodePanChangeHandler? onPanChange; NodePanEndHandler? onPanEnd; NodeRotationStartHandler? onRotationStart; NodeRotationChangeHandler? onRotationChange; NodeRotationEndHandler? onRotationEnd; ARObjectManager(int id, {this.debug = false}) { _channel = MethodChannel('arobjects_$id'); _channel.setMethodCallHandler(_platformCallHandler); if (debug) { print("ARObjectManager initialized"); } } Future _platformCallHandler(MethodCall call) { if (debug) { print('_platformCallHandler call ${call.method} ${call.arguments}'); } try { switch (call.method) { case 'onError': print(call.arguments); break; case 'onNodeTap': if (onNodeTap != null) { final tappedNodes = call.arguments as List; onNodeTap!(tappedNodes .map((tappedNode) => tappedNode.toString()) .toList()); } break; case 'onPanStart': if (onPanStart != null) { final tappedNode = call.arguments as String; // Notify callback onPanStart!(tappedNode); } break; case 'onPanChange': if (onPanChange != null) { final tappedNode = call.arguments as String; // Notify callback onPanChange!(tappedNode); } break; case 'onPanEnd': if (onPanEnd != null) { final tappedNodeName = call.arguments["name"] as String; final transform = MatrixConverter().fromJson(call.arguments['transform'] as List); // Notify callback onPanEnd!(tappedNodeName, transform); } break; case 'onRotationStart': if (onRotationStart != null) { final tappedNode = call.arguments as String; onRotationStart!(tappedNode); } break; case 'onRotationChange': if (onRotationChange != null) { final tappedNode = call.arguments as String; onRotationChange!(tappedNode); } break; case 'onRotationEnd': if (onRotationEnd != null) { final tappedNodeName = call.arguments["name"] as String; final transform = MatrixConverter().fromJson(call.arguments['transform'] as List); // Notify callback onRotationEnd!(tappedNodeName, transform); } break; default: if (debug) { print('Unimplemented method ${call.method} '); } } } catch (e) { print('Error caught: ' + e.toString()); } return Future.value(); } /// Sets up the AR Object Manager onInitialize() { _channel.invokeMethod('init', {}); } /// Add given node to the given anchor of the underlying AR scene (or to its top-level if no anchor is given) and listen to any changes made to its transformation Future addNode(ARNode node, {ARPlaneAnchor? planeAnchor}) async { try { node.transformNotifier.addListener(() { _channel.invokeMethod('transformationChanged', { 'name': node.name, 'transformation': MatrixValueNotifierConverter().toJson(node.transformNotifier) }); }); if (planeAnchor != null) { planeAnchor.childNodes.add(node.name); return await _channel.invokeMethod('addNodeToPlaneAnchor', {'node': node.toMap(), 'anchor': planeAnchor.toJson()}); } else { return await _channel.invokeMethod('addNode', node.toMap()); } } on PlatformException catch (e) { return false; } } /// Remove given node from the AR Scene removeNode(ARNode node) { _channel.invokeMethod('removeNode', {'name': node.name}); } } ================================================ FILE: lib/managers/ar_session_manager.dart ================================================ import 'dart:math' show sqrt; import 'dart:typed_data'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; import 'package:ar_flutter_plugin/models/ar_anchor.dart'; import 'package:ar_flutter_plugin/models/ar_hittest_result.dart'; import 'package:ar_flutter_plugin/utils/json_converters.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:vector_math/vector_math_64.dart'; // Type definitions to enforce a consistent use of the API typedef ARHitResultHandler = void Function(List hits); /// Manages the session configuration, parameters and events of an [ARView] class ARSessionManager { /// Platform channel used for communication from and to [ARSessionManager] late MethodChannel _channel; /// Debugging status flag. If true, all platform calls are printed. Defaults to false. final bool debug; /// Context of the [ARView] widget that this manager is attributed to final BuildContext buildContext; /// Determines the types of planes ARCore and ARKit should show final PlaneDetectionConfig planeDetectionConfig; /// Receives hit results from user taps with tracked planes or feature points late ARHitResultHandler onPlaneOrPointTap; ARSessionManager(int id, this.buildContext, this.planeDetectionConfig, {this.debug = false}) { _channel = MethodChannel('arsession_$id'); _channel.setMethodCallHandler(_platformCallHandler); if (debug) { print("ARSessionManager initialized"); } } /// Returns the camera pose in Matrix4 format with respect to the world coordinate system of the [ARView] Future getCameraPose() async { try { final serializedCameraPose = await _channel.invokeMethod>('getCameraPose', {}); return MatrixConverter().fromJson(serializedCameraPose!); } catch (e) { print('Error caught: ' + e.toString()); return null; } } /// Returns the given anchor pose in Matrix4 format with respect to the world coordinate system of the [ARView] Future getPose(ARAnchor anchor) async { try { if (anchor.name.isEmpty) { throw Exception("Anchor can not be resolved. Anchor name is empty."); } final serializedCameraPose = await _channel.invokeMethod>('getAnchorPose', { "anchorId": anchor.name, }); return MatrixConverter().fromJson(serializedCameraPose!); } catch (e) { print('Error caught: ' + e.toString()); return null; } } /// Returns the distance in meters between @anchor1 and @anchor2. Future getDistanceBetweenAnchors( ARAnchor anchor1, ARAnchor anchor2) async { var anchor1Pose = await getPose(anchor1); var anchor2Pose = await getPose(anchor2); var anchor1Translation = anchor1Pose?.getTranslation(); var anchor2Translation = anchor2Pose?.getTranslation(); if (anchor1Translation != null && anchor2Translation != null) { return getDistanceBetweenVectors(anchor1Translation, anchor2Translation); } else { return null; } } /// Returns the distance in meters between @anchor and device's camera. Future getDistanceFromAnchor(ARAnchor anchor) async { Matrix4? cameraPose = await getCameraPose(); Matrix4? anchorPose = await getPose(anchor); Vector3? cameraTranslation = cameraPose?.getTranslation(); Vector3? anchorTranslation = anchorPose?.getTranslation(); if (anchorTranslation != null && cameraTranslation != null) { return getDistanceBetweenVectors(anchorTranslation, cameraTranslation); } else { return null; } } /// Returns the distance in meters between @vector1 and @vector2. double getDistanceBetweenVectors(Vector3 vector1, Vector3 vector2) { num dx = vector1.x - vector2.x; num dy = vector1.y - vector2.y; num dz = vector1.z - vector2.z; double distance = sqrt(dx * dx + dy * dy + dz * dz); return distance; } Future _platformCallHandler(MethodCall call) { if (debug) { print('_platformCallHandler call ${call.method} ${call.arguments}'); } try { switch (call.method) { case 'onError': if (onError != null) { onError(call.arguments[0]); print(call.arguments); } break; case 'onPlaneOrPointTap': if (onPlaneOrPointTap != null) { final rawHitTestResults = call.arguments as List; final serializedHitTestResults = rawHitTestResults .map( (hitTestResult) => Map.from(hitTestResult)) .toList(); final hitTestResults = serializedHitTestResults.map((e) { return ARHitTestResult.fromJson(e); }).toList(); onPlaneOrPointTap(hitTestResults); } break; case 'dispose': _channel.invokeMethod("dispose"); break; default: if (debug) { print('Unimplemented method ${call.method} '); } } } catch (e) { print('Error caught: ' + e.toString()); } return Future.value(); } /// Function to initialize the platform-specific AR view. Can be used to initially set or update session settings. /// [customPlaneTexturePath] refers to flutter assets from the app that is calling this function, NOT to assets within this plugin. Make sure /// the assets are correctly registered in the pubspec.yaml of the parent app (e.g. the ./example app in this plugin's repo) onInitialize({ bool showAnimatedGuide = true, bool showFeaturePoints = false, bool showPlanes = true, String? customPlaneTexturePath, bool showWorldOrigin = false, bool handleTaps = true, bool handlePans = false, // nodes are not draggable by default bool handleRotation = false, // nodes can not be rotated by default }) { _channel.invokeMethod('init', { 'showAnimatedGuide': showAnimatedGuide, 'showFeaturePoints': showFeaturePoints, 'planeDetectionConfig': planeDetectionConfig.index, 'showPlanes': showPlanes, 'customPlaneTexturePath': customPlaneTexturePath, 'showWorldOrigin': showWorldOrigin, 'handleTaps': handleTaps, 'handlePans': handlePans, 'handleRotation': handleRotation, }); } /// Displays the [errorMessage] in a snackbar of the parent widget onError(String errorMessage) { ScaffoldMessenger.of(buildContext).showSnackBar(SnackBar( content: Text(errorMessage), action: SnackBarAction( label: 'HIDE', onPressed: ScaffoldMessenger.of(buildContext).hideCurrentSnackBar))); } /// Dispose the AR view on the platforms to pause the scenes and disconnect the platform handlers. /// You should call this before removing the AR view to prevent out of memory erros dispose() async { try { await _channel.invokeMethod("dispose"); } catch (e) { print(e); } } /// Returns a future ImageProvider that contains a screenshot of the current AR Scene Future snapshot() async { final result = await _channel.invokeMethod('snapshot'); return MemoryImage(result!); } } ================================================ FILE: lib/models/ar_anchor.dart ================================================ import 'package:ar_flutter_plugin/datatypes/anchor_types.dart'; import 'package:ar_flutter_plugin/models/ar_node.dart'; import 'package:ar_flutter_plugin/utils/json_converters.dart'; import 'package:vector_math/vector_math_64.dart'; import 'package:flutter/widgets.dart'; /// Object attached to a tracked physical entity of the AR environment (can be initialized with a world transformation) abstract class ARAnchor { ARAnchor({ required this.type, required this.transformation, String? name, }) : name = name ?? UniqueKey().toString(); /// Specifies the [AnchorType] of this [ARAnchor] final AnchorType type; /// Determines the name of the [ARAnchor] /// Will be autogenerated if not defined. final String name; /// Constructs an [ARAnchor] from a serialized anchor object factory ARAnchor.fromJson(Map arguments) { final type = arguments['type']; switch (type) { case 0: //(= AnchorType.plane) return ARPlaneAnchor.fromJson(arguments); } return ARUnkownAnchor.fromJson(arguments); } /// Defines the anchor’s rotation, translation and scale in world coordinates. final Matrix4 transformation; /// Serializes an [ARAnchor] Map toJson(); } /// An [ARAnchor] fixed to a tracked plane class ARPlaneAnchor extends ARAnchor { ARPlaneAnchor({ required Matrix4 transformation, String? name, List? childNodes, String? cloudanchorid, int? ttl, }) : childNodes = childNodes ?? [], cloudanchorid = cloudanchorid ?? null, ttl = ttl ?? 1, super( type: AnchorType.plane, transformation: transformation, name: name); /// Names of ARNodes attached to this [APlaneRAnchor] List childNodes; /// ID associated with the anchor after uploading it to the google cloud anchor API String? cloudanchorid; /// Time to live of the anchor: Determines how long the anchor is stored once it is uploaded to the google cloud anchor API (optional, defaults to 1 day (24hours)) int? ttl; static ARPlaneAnchor fromJson(Map json) => aRPlaneAnchorFromJson(json); @override Map toJson() => aRPlaneAnchorToJson(this); } /// Constructs an [ARPlaneAnchor] from a serialized PlaneAnchor object ARPlaneAnchor aRPlaneAnchorFromJson(Map json) { return ARPlaneAnchor( transformation: const MatrixConverter().fromJson(json['transformation'] as List), name: json['name'] as String, childNodes: json['childNodes'] ?.map((child) => child.toString()) ?.toList() ?.cast(), cloudanchorid: json['cloudanchorid'] as String?, ttl: json['ttl'] as int?, ); } /// Serializes an [ARPlaneAnchor] Map aRPlaneAnchorToJson(ARPlaneAnchor instance) { return { 'type': instance.type.index, 'transformation': MatrixConverter().toJson(instance.transformation), 'name': instance.name, 'childNodes': instance.childNodes, 'cloudanchorid': instance.cloudanchorid, 'ttl': instance.ttl, }; } /// An [ARAnchor] type that is not supported yet class ARUnkownAnchor extends ARAnchor { ARUnkownAnchor( {required AnchorType type, required Matrix4 transformation, String? name}) : super(type: type, transformation: transformation, name: name); static ARUnkownAnchor fromJson(Map json) => aRUnkownAnchorFromJson(json); @override Map toJson() => aRUnkownAnchorToJson(this); } ARUnkownAnchor aRUnkownAnchorFromJson(Map json) { return ARUnkownAnchor( type: json['type'], transformation: const MatrixConverter().fromJson(json['transformation'] as List), name: json['name'] as String, ); } Map aRUnkownAnchorToJson(ARUnkownAnchor instance) { return { 'type': instance.type.index, 'transformation': MatrixConverter().toJson(instance.transformation), 'name': instance.name, }; } ================================================ FILE: lib/models/ar_hittest_result.dart ================================================ // The code in this file is adapted from Oleksandr Leuschenko' ARKit Flutter Plugin (https://github.com/olexale/arkit_flutter_plugin) import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart'; import 'package:ar_flutter_plugin/utils/json_converters.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:vector_math/vector_math_64.dart'; /// A result (type, distance from the camera, world transformation) of an intersection found during a hit-test. class ARHitTestResult { ARHitTestResult( this.type, this.distance, this.worldTransform, ); /// The type of the hit-test result. final ARHitTestResultType type; /// The distance from the camera to the intersection in meters. final double distance; /// The transformation matrix that defines the intersection’s rotation, translation and scale /// relative to the world. final Matrix4 worldTransform; /// Instantiates am [ARHitTestResult] from a serialized ARHitTestResult static ARHitTestResult fromJson(Map json) => _$ARHitTestResultFromJson(json); /// Serializes the [ARHitTestResult] Map toJson() => _$ARHitTestResultToJson(this); } /// Instantiates am [ARHitTestResult] from a serialized ARHitTestResult ARHitTestResult _$ARHitTestResultFromJson(Map json) { return ARHitTestResult( const ARHitTestResultTypeConverter().fromJson(json['type'] as int), (json['distance'] as num).toDouble(), const MatrixConverter().fromJson(json['worldTransform'] as List), ); } /// Serializes the [ARHitTestResult] Map _$ARHitTestResultToJson(ARHitTestResult instance) { final val = {}; void writeNotNull(String key, dynamic value) { if (value != null) { val[key] = value; } } writeNotNull( 'type', const ARHitTestResultTypeConverter().toJson(instance.type)); val['distance'] = instance.distance; writeNotNull('worldTransform', const MatrixConverter().toJson(instance.worldTransform)); return val; } /// Helper class to convert the type of an [ARHitTestResult] from its integer representation to the [ARHitTestResultType] and vice versa class ARHitTestResultTypeConverter implements JsonConverter { const ARHitTestResultTypeConverter(); /// Converts the type of an [ARHitTestResult] from its integer representation to the [ARHitTestResultType] @override ARHitTestResultType fromJson(int json) { switch (json) { case 1: return ARHitTestResultType.plane; case 2: return ARHitTestResultType.point; default: return ARHitTestResultType.undefined; } } /// Converts the type of an [ARHitTestResult] from its [ARHitTestResultType] to an integer representation @override int toJson(ARHitTestResultType object) { switch (object) { case ARHitTestResultType.plane: return 1; case ARHitTestResultType.point: return 2; default: return 0; } } } ================================================ FILE: lib/models/ar_node.dart ================================================ // The code in this file is adapted from Oleksandr Leuschenko' ARKit Flutter Plugin (https://github.com/olexale/arkit_flutter_plugin) import 'package:ar_flutter_plugin/utils/json_converters.dart'; import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart'; import 'package:json_annotation/json_annotation.dart'; import 'dart:math' as math; import 'package:ar_flutter_plugin/datatypes/node_types.dart'; /// ARNode is the model class for node-tree objects. /// It encapsulates the position, rotations, and other transforms of a node, which define a coordinate system. /// The coordinate systems of all the sub-nodes are relative to the one of their parent node. class ARNode { ARNode({ required this.type, required this.uri, String? name, Vector3? position, Vector3? scale, Vector4? rotation, Vector3? eulerAngles, Matrix4? transformation, Map? data, }) : name = name ?? UniqueKey().toString(), transformNotifier = ValueNotifier(createTransformMatrix( transformation, position, scale, rotation, eulerAngles)), data = data ?? null; /// Specifies the receiver's [NodeType] NodeType type; /// Specifies the path to the 3D model used for the [ARNode]. Depending on the [type], this is either a relative path or an URL to an online asset String uri; /// Determines the receiver's transform. /// The transform is the combination of the position, rotation and scale defined below. /// So when the transform is set, the receiver's position, rotation and scale are changed to match the new transform. Matrix4 get transform => transformNotifier.value; set transform(Matrix4 matrix) { transformNotifier.value = matrix; } /// Determines the receiver's position. Vector3 get position => transform.getTranslation(); set position(Vector3 value) { final old = Matrix4.fromFloat64List(transform.storage); final newT = old.clone(); newT.setTranslation(value); transform = newT; } /// Determines the receiver's scale. Vector3 get scale => transform.matrixScale; set scale(Vector3 value) { transform = Matrix4.compose(position, Quaternion.fromRotation(rotation), value); } /// Determines the receiver's rotation. Matrix3 get rotation => transform.getRotation(); set rotation(Matrix3 value) { transform = Matrix4.compose(position, Quaternion.fromRotation(value), scale); } set rotationFromQuaternion(Quaternion value) { transform = Matrix4.compose(position, value, scale); } /// Determines the receiver's euler angles. /// The order of components in this vector matches the axes of rotation: /// 1. Pitch (the x component) is the rotation about the node's x-axis (in radians) /// 2. Yaw (the y component) is the rotation about the node's y-axis (in radians) /// 3. Roll (the z component) is the rotation about the node's z-axis (in radians) Vector3 get eulerAngles => transform.matrixEulerAngles; set eulerAngles(Vector3 value) { final old = Matrix4.fromFloat64List(transform.storage); final newT = old.clone(); newT.matrixEulerAngles = value; transform = newT; } final ValueNotifier transformNotifier; /// Determines the name of the receiver. /// Will be autogenerated if not defined. final String name; /// Holds any data attached to the node, especially useful when uploading serialized nodes to the cloud. This data is not shared with the underlying platform Map? data; static const _matrixValueNotifierConverter = MatrixValueNotifierConverter(); Map toMap() => { 'type': type.index, 'uri': uri, 'transformation': _matrixValueNotifierConverter.toJson(transformNotifier), 'name': name, 'data': data, }..removeWhere((String k, dynamic v) => v == null); static ARNode fromMap(Map map) { return ARNode( type: NodeType.values[map["type"]], uri: map["uri"] as String, name: map["name"] as String, transformation: MatrixConverter().fromJson(map["transformation"]), data: Map.from(map["data"])); } } /// Helper function to create a Matrix4 from either a given matrix or from position, scale and rotation relative to the origin Matrix4 createTransformMatrix(Matrix4? origin, Vector3? position, Vector3? scale, Vector4? rotation, Vector3? eulerAngles) { final transform = origin ?? Matrix4.identity(); if (position != null) { transform.setTranslation(position); } if (rotation != null) { transform.rotate( Vector3(rotation[0], rotation[1], rotation[2]), rotation[3]); } if (eulerAngles != null) { transform.matrixEulerAngles = eulerAngles; } if (scale != null) { transform.scale(scale); } else { transform.scale(1.0); } return transform; } extension Matrix4Extenstion on Matrix4 { Vector3 get matrixScale { final scale = Vector3.zero(); decompose(Vector3.zero(), Quaternion(0, 0, 0, 0), scale); return scale; } Vector3 get matrixEulerAngles { final q = Quaternion(0, 0, 0, 0); decompose(Vector3.zero(), q, Vector3.zero()); final t = q.x; q.x = q.y; q.y = t; final angles = Vector3.zero(); // roll (x-axis rotation) final sinrCosp = 2 * (q.w * q.x + q.y * q.z); final cosrCosp = 1 - 2 * (q.x * q.x + q.y * q.y); angles[0] = math.atan2(sinrCosp, cosrCosp); // pitch (y-axis rotation) final sinp = 2 * (q.w * q.y - q.z * q.x); if (sinp.abs() >= 1) { angles[1] = _copySign(math.pi / 2, sinp); // use 90 degrees if out of range } else { angles[1] = math.asin(sinp); } // yaw (z-axis rotation) final sinyCosp = 2 * (q.w * q.z + q.x * q.y); final cosyCosp = 1 - 2 * (q.y * q.y + q.z * q.z); angles[2] = math.atan2(sinyCosp, cosyCosp); return angles; } set matrixEulerAngles(Vector3 angles) { final translation = Vector3.zero(); final scale = Vector3.zero(); decompose(translation, Quaternion(0, 0, 0, 0), scale); final r = Quaternion.euler(angles[0], angles[1], angles[2]); setFromTranslationRotationScale(translation, r, scale); } } // https://scidart.org/docs/scidart/numdart/copySign.html double _copySign(double magnitude, double sign) { // The highest order bit is going to be zero if the // highest order bit of m and s is the same and one otherwise. // So (m^s) will be positive if both m and s have the same sign // and negative otherwise. /*final long m = Double.doubleToRawLongBits(magnitude); // don't care about NaN final long s = Double.doubleToRawLongBits(sign); if ((m^s) >= 0) { return magnitude; } return -magnitude; // flip sign*/ if (sign == 0.0 || sign.isNaN || magnitude.sign == sign.sign) { return magnitude; } return -magnitude; // flip sign } class MatrixValueNotifierConverter implements JsonConverter, List> { const MatrixValueNotifierConverter(); @override ValueNotifier fromJson(List json) { return ValueNotifier(Matrix4.fromList(json.cast())); } @override List toJson(ValueNotifier matrix) { final list = List.filled(16, 0.0); matrix.value.copyIntoArray(list); return list; } } ================================================ FILE: lib/utils/json_converters.dart ================================================ // The code in this file is adapted from Oleksandr Leuschenko' ARKit Flutter Plugin (https://github.com/olexale/arkit_flutter_plugin) import 'package:json_annotation/json_annotation.dart'; import 'package:vector_math/vector_math_64.dart'; class MatrixConverter implements JsonConverter> { const MatrixConverter(); @override Matrix4 fromJson(List json) { return Matrix4.fromList(json.cast()); } @override List toJson(Matrix4 matrix) { final list = List.filled(16, 0.0); matrix.copyIntoArray(list); return list; } } ================================================ FILE: lib/widgets/ar_view.dart ================================================ import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_location_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:ar_flutter_plugin/managers/ar_session_manager.dart'; import 'package:ar_flutter_plugin/managers/ar_object_manager.dart'; import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart'; // Type definitions to enforce a consistent use of the API typedef ARViewCreatedCallback = void Function( ARSessionManager arSessionManager, ARObjectManager arObjectManager, ARAnchorManager arAnchorManager, ARLocationManager arLocationManager); /// Factory method for creating a platform-dependent AR view abstract class PlatformARView { factory PlatformARView(TargetPlatform platform) { switch (platform) { case TargetPlatform.android: return AndroidARView(); case TargetPlatform.iOS: return IosARView(); default: throw FlutterError; } } Widget build( {@required BuildContext context, @required ARViewCreatedCallback arViewCreatedCallback, @required PlaneDetectionConfig planeDetectionConfig}); /// Callback function that is executed once the view is established void onPlatformViewCreated(int id); } /// Instantiates [ARSessionManager], [ARObjectManager] and returns them to the widget instantiating the [ARView] using the [arViewCreatedCallback] createManagers( int id, BuildContext? context, ARViewCreatedCallback? arViewCreatedCallback, PlaneDetectionConfig? planeDetectionConfig) { if (context == null || arViewCreatedCallback == null || planeDetectionConfig == null) { return; } arViewCreatedCallback(ARSessionManager(id, context, planeDetectionConfig), ARObjectManager(id), ARAnchorManager(id), ARLocationManager()); } /// Android-specific implementation of [PlatformARView] /// Uses Hybrid Composition to increase peformance on Android 9 and below (https://flutter.dev/docs/development/platform-integration/platform-views) class AndroidARView implements PlatformARView { late BuildContext? _context; late ARViewCreatedCallback? _arViewCreatedCallback; late PlaneDetectionConfig? _planeDetectionConfig; @override void onPlatformViewCreated(int id) { print("Android platform view created!"); createManagers(id, _context, _arViewCreatedCallback, _planeDetectionConfig); } @override Widget build( {BuildContext? context, ARViewCreatedCallback? arViewCreatedCallback, PlaneDetectionConfig? planeDetectionConfig}) { _context = context; _arViewCreatedCallback = arViewCreatedCallback; _planeDetectionConfig = planeDetectionConfig; // This is used in the platform side to register the view. final String viewType = 'ar_flutter_plugin'; // Pass parameters to the platform side. final Map creationParams = {}; return AndroidView( viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), onPlatformViewCreated: onPlatformViewCreated, ); } } /// iOS-specific implementation of [PlatformARView] class IosARView implements PlatformARView { BuildContext? _context; ARViewCreatedCallback? _arViewCreatedCallback; PlaneDetectionConfig? _planeDetectionConfig; @override void onPlatformViewCreated(int id) { print("iOS platform view created!"); createManagers(id, _context, _arViewCreatedCallback, _planeDetectionConfig); } @override Widget build( {BuildContext? context, ARViewCreatedCallback? arViewCreatedCallback, PlaneDetectionConfig? planeDetectionConfig}) { _context = context; _arViewCreatedCallback = arViewCreatedCallback; _planeDetectionConfig = planeDetectionConfig; // This is used in the platform side to register the view. final String viewType = 'ar_flutter_plugin'; // Pass parameters to the platform side. final Map creationParams = {}; return UiKitView( viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), onPlatformViewCreated: onPlatformViewCreated, ); } } /// If camera permission is granted, [ARView] creates a platform-dependent view from the factory method [PlatformARView]. To instantiate an [ARView], /// the calling widget needs to pass the callback function [onARViewCreated] to which the function [createManagers] returns managers such as the /// [ARSessionManager] and the [ARObjectManager]. [planeDetectionConfig] is passed to the constructor to determine which types of planes the underlying /// AR frameworks should track (defaults to none). /// If camera permission is not given, the user is prompted to grant it. To modify the UI of the prompts, the following named parameters can be used: /// [permissionPromptDescription], [permissionPromptButtonText] and [permissionPromptParentalRestriction]. class ARView extends StatefulWidget { final String permissionPromptDescription; final String permissionPromptButtonText; final String permissionPromptParentalRestriction; /// Function to be called when the AR View is created final ARViewCreatedCallback onARViewCreated; /// Configures the type of planes ARCore and ARKit should track. defaults to none final PlaneDetectionConfig planeDetectionConfig; /// Configures whether or not to display the device's platform type above the AR view. Defaults to false final bool showPlatformType; ARView( {Key? key, required this.onARViewCreated, this.planeDetectionConfig = PlaneDetectionConfig.none, this.showPlatformType = false, this.permissionPromptDescription = "Camera permission must be given to the app for AR functions to work", this.permissionPromptButtonText = "Grant Permission", this.permissionPromptParentalRestriction = "Camera permission is restriced by the OS, please check parental control settings"}) : super(key: key); @override _ARViewState createState() => _ARViewState( showPlatformType: this.showPlatformType, permissionPromptDescription: this.permissionPromptDescription, permissionPromptButtonText: this.permissionPromptButtonText, permissionPromptParentalRestriction: this.permissionPromptParentalRestriction); } class _ARViewState extends State { PermissionStatus _cameraPermission = PermissionStatus.denied; bool showPlatformType; String permissionPromptDescription; String permissionPromptButtonText; String permissionPromptParentalRestriction; _ARViewState( {required this.showPlatformType, required this.permissionPromptDescription, required this.permissionPromptButtonText, required this.permissionPromptParentalRestriction}); @override void initState() { super.initState(); initCameraPermission(); } initCameraPermission() async { requestCameraPermission(); } requestCameraPermission() async { final cameraPermission = await Permission.camera.request(); setState(() { _cameraPermission = cameraPermission; }); } requestCameraPermissionFromSettings() async { final cameraPermission = await Permission.camera.request(); if (cameraPermission == PermissionStatus.permanentlyDenied) { openAppSettings(); } setState(() { _cameraPermission = cameraPermission; }); } @override build(BuildContext context) { switch (_cameraPermission) { case (PermissionStatus .limited): //iOS-specific: permissions granted for this specific application case (PermissionStatus.granted): { return Column(children: [ if (showPlatformType) Text(Theme.of(context).platform.toString()), Expanded( child: PlatformARView(Theme.of(context).platform).build( context: context, arViewCreatedCallback: widget.onARViewCreated, planeDetectionConfig: widget.planeDetectionConfig)), ]); } case (PermissionStatus.denied): { return Center( child: Column( children: [ Text(permissionPromptDescription), ElevatedButton( child: Text(permissionPromptButtonText), onPressed: () async => {await requestCameraPermission()}) ], )); } case (PermissionStatus .permanentlyDenied): //Android-specific: User needs to open Settings to give permissions { return Center( child: Column( children: [ Text(permissionPromptDescription), ElevatedButton( child: Text(permissionPromptButtonText), onPressed: () async => {await requestCameraPermissionFromSettings()}) ], )); } case (PermissionStatus.restricted): { //iOS only return Center(child: Text(permissionPromptParentalRestriction)); } default: return Text('something went wrong'); } } } ================================================ FILE: pubspec.yaml ================================================ name: ar_flutter_plugin description: Flutter Plugin for creating (collaborative) Augmented Reality experiences - Supports ARKit for iOS and ARCore for Android devices. version: 0.7.3 homepage: https://lars.carius.io repository: https://github.com/CariusLars/ar_flutter_plugin environment: sdk: ">=2.16.1 <3.0.0" flutter: ">=1.20.0" dependencies: flutter: sdk: flutter permission_handler: ^10.1.0 vector_math: ^2.1.1 json_annotation: ^4.5.0 geolocator: ^9.0.0 dev_dependencies: flutter_test: sdk: flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: # This section identifies this Flutter project as a plugin project. # The 'pluginClass' and Android 'package' identifiers should not ordinarily # be modified. They are used by the tooling to maintain consistency when # adding or updating assets for this project. plugin: platforms: android: package: io.carius.lars.ar_flutter_plugin pluginClass: ArFlutterPlugin ios: pluginClass: ArFlutterPlugin ================================================ FILE: test/ar_flutter_plugin_test.dart ================================================ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ar_flutter_plugin/ar_flutter_plugin.dart'; void main() { const MethodChannel channel = MethodChannel('ar_flutter_plugin'); TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { return '42'; }); }); tearDown(() { channel.setMockMethodCallHandler(null); }); test('getPlatformVersion', () async { expect(await ArFlutterPlugin.platformVersion, '42'); }); }