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
[](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:

================================================
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