diff --git a/CHANGELOG.md b/CHANGELOG.md index 42e30719..9553480c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +##0.7.4 +* Uses New ARCore API 1.35.0 (ARCore API required to be enabled in Google Cloud Console) +* Place an anchor at point (X,Y) of screen programmatically + ##0.7.3 * Update the examples with null-safety diff --git a/README.md b/README.md index 2b7ecb6f..98d3e78c 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Or manually add this to your `pubspec.yaml` file (and run `flutter pub get`): ```yaml dependencies: - ar_flutter_plugin: ^0.7.3 + ar_flutter_plugin: ^0.7.4 ``` ### Importing diff --git a/android/build.gradle b/android/build.gradle index dbdff724..14660244 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'io.carius.lars.ar_flutter_plugin' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.7.10' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:8.5.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,26 +25,27 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 - + namespace 'io.carius.lars.ar_flutter_plugin' sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { + compileSdk 34 minSdkVersion 24 } } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "com.google.ar:core:1.22.0" + implementation "com.google.ar:core:1.35.0" - implementation 'com.google.ar.sceneform:core:1.15.0' - implementation 'com.google.ar.sceneform:assets:1.15.0' + implementation 'com.google.ar.sceneform:core:1.17.1' + implementation 'com.google.ar.sceneform:assets:1.17.1' 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 { @@ -53,8 +54,7 @@ afterEvaluate { for (def dependency : configuration.dependencies) { if (dependency.group == 'io.flutter' && dependency.name.startsWith('flutter_embedding') && - dependency.isTransitive()) - { + dependency.isTransitive()) { containsEmbeddingDependencies = true break } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 3c9d0852..9f9d85f5 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Mon Jul 15 18:10:20 EEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/AndroidARView.kt b/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/AndroidARView.kt index 6ffb177b..01240552 100644 --- a/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/AndroidARView.kt +++ b/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/AndroidARView.kt @@ -39,18 +39,9 @@ import com.google.ar.sceneform.rendering.* import android.view.ViewGroup import com.google.ar.core.TrackingState - - - - - - - - - - - - +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking internal class AndroidARView( @@ -116,6 +107,32 @@ internal class AndroidARView( result.error("Error", "could not get camera pose", null) } } + "placeGeospatial" -> { + + } + "placeBasedOnCoordinates" -> { + if (call.arguments is Map<*,*>) { + val cpuCoordinates = floatArrayOf((call.arguments as Map<*, *>)['x'] as Float, (call.arguments as Map<*, *>)['y'] as Float) + val viewCoordinates = FloatArray(2) + if (arSceneView.arFrame != null) { + arSceneView.arFrame!!.transformCoordinates2d( + Coordinates2d.IMAGE_PIXELS, + cpuCoordinates, + Coordinates2d.VIEW, + viewCoordinates + ) + val allHitResults = arSceneView.arFrame!!.hitTest(viewCoordinates[0], viewCoordinates[1]) + val planeAndPointHitResults = allHitResults.filter { ((it.trackable is Plane) || (it.trackable is Point)) } + val serializedPlaneAndPointHitResults: ArrayList> = + ArrayList(planeAndPointHitResults.map { serializeHitResult(it) }) + result.success(serializedPlaneAndPointHitResults) + } else { + result.success(null) + } + } else { + result.success(null) + } + } "snapshot" -> { var bitmap = Bitmap.createBitmap(arSceneView.width, arSceneView.height, Bitmap.Config.ARGB_8888); @@ -259,6 +276,19 @@ internal class AndroidARView( sessionManagerChannel.invokeMethod("onError", listOf("Error initializing cloud anchor mode: Session is null")) } } + "initGeospatialMode" -> { + 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") @@ -452,16 +482,23 @@ internal class AndroidARView( fun onDestroy() { try { - arSceneView.session?.close() - arSceneView.destroy() + if (arSceneView.session != null) { + for (anchor in arSceneView.session!!.allAnchors) { + anchor?.detach() + } + } + cloudAnchorHandler.dispose(); arSceneView.scene?.removeOnUpdateListener(sceneUpdateListener) arSceneView.scene?.removeOnPeekTouchListener(onNodeTapListener) + arSceneView.session?.close() + arSceneView.destroy() }catch (e : Exception){ e.printStackTrace(); } } private fun initializeARView(call: MethodCall, result: MethodChannel.Result) { + Log.d(TAG, "initializeARView") // Unpack call arguments val argShowFeaturePoints: Boolean? = call.argument("showFeaturePoints") val argPlaneDetectionConfig: Int? = call.argument("planeDetectionConfig") diff --git a/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/CloudAnchorHandler.kt b/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/CloudAnchorHandler.kt index 5e0724d4..b73cd667 100644 --- a/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/CloudAnchorHandler.kt +++ b/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/CloudAnchorHandler.kt @@ -15,7 +15,7 @@ internal class CloudAnchorHandler( arSession: Session ) { } private val TAG: String = CloudAnchorHandler::class.java.simpleName - private val pendingAnchors = HashMap>() + val pendingAnchors = HashMap>() private val session: Session = arSession @Synchronized @@ -52,6 +52,14 @@ internal class CloudAnchorHandler( arSession: Session ) { } } + @Synchronized + fun dispose() { + for (pendingAnchor in pendingAnchors) { + pendingAnchor.key.detach() + } + clearListeners() + } + // Remove all listeners @Synchronized fun clearListeners() { diff --git a/cloudAnchorSetup.md b/cloudAnchorSetup.md index 8115c41c..f283c073 100644 --- a/cloudAnchorSetup.md +++ b/cloudAnchorSetup.md @@ -5,9 +5,9 @@ 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: +The Google ARCore 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 +1. Activate the [ARCore API](https://console.cloud.google.com/apis/library/arcore) 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). diff --git a/ios/Classes/IosARView.swift b/ios/Classes/IosARView.swift index 49b239fc..3fd3dd54 100644 --- a/ios/Classes/IosARView.swift +++ b/ios/Classes/IosARView.swift @@ -4,6 +4,7 @@ import Foundation import ARKit import Combine import ARCoreCloudAnchors +import ARCoreGeospatial class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureRecognizerDelegate, ARSessionDelegate { let sceneView: ARSCNView @@ -72,7 +73,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco result(nil) } - func onSessionMethodCalled(_ call :FlutterMethodCall, _ result:FlutterResult) { + func onSessionMethodCalled(_ call :FlutterMethodCall, _ result:@escaping FlutterResult) { let arguments = call.arguments as? Dictionary switch call.method { @@ -81,6 +82,35 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco //result(nil) initializeARView(arguments: arguments!, result: result) break + case "placeBasedOnCoordinates": + let coordinates = CGPoint(x: CGFloat( arguments!["x"] as! Float), y: CGFloat( arguments!["y"] as! Float)); + let htResult = parseTouchLocation(touchLocation: coordinates, returnHitResult: true); + result(htResult); + break; + case "placeGeospatial": + do { + var coordinates = CLLocationCoordinate2D(); + coordinates.latitude = arguments!["lat"] as! Double; + coordinates.longitude = arguments!["lon"] as! Double; + try arcoreSession!.createAnchorOnTerrain( + coordinate: coordinates, + altitudeAboveTerrain: arguments!["alt"] as! Double, + eastUpSouthQAnchor: simd_quatf(vector: simd_float4(x: 0, y: 0, z: 0, w: 0)), + completionHandler: { anchor, state in + print(state.rawValue); + if (state == GARTerrainAnchorState.success) { + let newAnchor = ARAnchor(transform: anchor!.transform); + result(serializeAnchor(anchor: newAnchor, anchorNode: nil, ganchor: anchor!, name: arguments!["name"] as! String)); + } else { + result(nil); + } + } + ); + } catch { + print(error) + result(nil); + } + break; case "getCameraPose": if let cameraPose = sceneView.session.currentFrame?.camera.transform { result(serializeMatrix(cameraPose)) @@ -116,7 +146,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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"]) @@ -153,7 +183,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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"]) @@ -171,7 +201,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco break default: result(false) - + } } result(nil) @@ -181,20 +211,51 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco deleteAnchor(anchorName: name) } break + case "initGeoSpatialMode": + arcoreSession = try! GARSession.session() + + if (arcoreSession != nil){ + +// configuration.cloudAnchorMode = .enabled; + + if let token = JWTGenerator().generateWebToken(){ + arcoreSession!.setAuthToken(token) + +// cloudAnchorHandler = CloudAnchorHandler(session: arcoreSession!) +// arcoreSession!.delegate = cloudAnchorHandler + arcoreSession!.delegateQueue = DispatchQueue.main + + let configuration = GARSessionConfiguration(); + configuration.geospatialMode = .enabled; + arcoreSession?.setConfiguration(configuration, error: nil); + + 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 "initGoogleCloudAnchorMode": arcoreSession = try! GARSession.session() if (arcoreSession != nil){ - let configuration = GARSessionConfiguration(); - configuration.cloudAnchorMode = .enabled; - arcoreSession?.setConfiguration(configuration, error: nil); + +// configuration.cloudAnchorMode = .enabled; + if let token = JWTGenerator().generateWebToken(){ arcoreSession!.setAuthToken(token) - + cloudAnchorHandler = CloudAnchorHandler(session: arcoreSession!) arcoreSession!.delegate = cloudAnchorHandler arcoreSession!.delegateQueue = DispatchQueue.main + let configuration = GARSessionConfiguration(); + configuration.cloudAnchorMode = .enabled; + arcoreSession?.setConfiguration(configuration, error: nil); + arcoreMode = true } else { sessionManagerChannel.invokeMethod("onError", arguments: ["Error generating JWT, have you added cloudAnchorKey.json into the example/ios/Runner directory?"]) @@ -202,7 +263,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco } else { sessionManagerChannel.invokeMethod("onError", arguments: ["Error initializing Google AR Session"]) } - + break case "uploadAnchor": if let anchorName = arguments!["name"] as? String, let anchor = anchorCollection[anchorName] { @@ -233,18 +294,18 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco self.configuration.environmentTexturing = .automatic if let planeDetectionConfig = arguments["planeDetectionConfig"] as? Int { switch planeDetectionConfig { - case 1: + case 1: configuration.planeDetection = .horizontal - - case 2: + + case 2: if #available(iOS 11.3, *) { configuration.planeDetection = .vertical } - case 3: + case 3: if #available(iOS 11.3, *) { configuration.planeDetection = [.horizontal, .vertical] } - default: + default: configuration.planeDetection = [] } } @@ -281,7 +342,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco } } self.sceneView.debugOptions = ARSCNDebugOptions(rawValue: debugOptions) - + if let configHandleTaps = arguments["handleTaps"] as? Bool { if (configHandleTaps){ let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) @@ -298,7 +359,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco self.sceneView.gestureRecognizers?.append(panGestureRecognizer) } } - + if let configHandleRotation = arguments["handleRotation"] as? Bool { if (configHandleRotation){ let rotationGestureRecognizer = UIRotationGestureRecognizer(target: self, action: #selector(handleRotation(_:))) @@ -306,7 +367,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco self.sceneView.gestureRecognizers?.append(rotationGestureRecognizer) } } - + // Add coaching view if let configShowAnimatedGuide = arguments["showAnimatedGuide"] as? Bool { if configShowAnimatedGuide { @@ -336,13 +397,13 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco } } } - + // 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) @@ -353,7 +414,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco } 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) } @@ -362,7 +423,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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 { @@ -376,7 +437,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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 @@ -389,14 +450,13 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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) @@ -421,14 +481,13 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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) @@ -446,7 +505,7 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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 { @@ -455,14 +514,13 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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) @@ -488,14 +546,13 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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) @@ -510,47 +567,57 @@ class IosARView: NSObject, FlutterPlatformView, ARSCNViewDelegate, UIGestureReco 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) - + parseTouchLocation(touchLocation: recognizer.location(in: sceneView), returnHitResult: false); + } + + func parseTouchLocation(touchLocation: CGPoint, returnHitResult: Bool) -> Any? { 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) { + if (nodeHitResults.count != 0 && !returnHitResult) { self.objectManagerChannel.invokeMethod("onNodeTap", arguments: Array(Set(nodeHitResults))) // Chaining of Array and Set is used to remove duplicates - return + return nil; } - + let planeTypes: ARHitTestResult.ResultType if #available(iOS 11.3, *){ - planeTypes = ARHitTestResult.ResultType([.existingPlaneUsingGeometry, .featurePoint]) + planeTypes = ARHitTestResult.ResultType([.existingPlaneUsingGeometry, .featurePoint]); }else { - planeTypes = ARHitTestResult.ResultType([.existingPlaneUsingExtent, .featurePoint]) + planeTypes = ARHitTestResult.ResultType([.existingPlaneUsingExtent, .featurePoint]); } - - let planeAndPointHitResults = sceneView.hitTest(touchLocation, types: planeTypes) - + + 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) + + if (returnHitResult) { + return serializedPlaneAndPointHitResults; + } else { + self.sessionManagerChannel.invokeMethod("onPlaneOrPointTap", arguments: serializedPlaneAndPointHitResults) + } + } + return nil; } @objc func handlePan(_ recognizer: UIPanGestureRecognizer) { diff --git a/ios/Classes/JWTGenerator.swift b/ios/Classes/JWTGenerator.swift index f5c509b4..32a23799 100644 --- a/ios/Classes/JWTGenerator.swift +++ b/ios/Classes/JWTGenerator.swift @@ -36,7 +36,7 @@ class JWTGenerator { let exp: Date let aud: String } - let jwtTokenClaims = JWTTokenClaims(iss: clientEmail, sub: clientEmail, iat: Date(), exp: Date(timeIntervalSinceNow: 3600), aud: "https://arcorecloudanchor.googleapis.com/") + let jwtTokenClaims = JWTTokenClaims(iss: clientEmail, sub: clientEmail, iat: Date(), exp: Date(timeIntervalSinceNow: 3600), aud: "https://arcore.googleapis.com/") var jwtToken = JWT(header: jwtTokenHeader, claims: jwtTokenClaims) diff --git a/ios/ar_flutter_plugin.podspec b/ios/ar_flutter_plugin.podspec index 4af5e5f8..6fd35552 100644 --- a/ios/ar_flutter_plugin.podspec +++ b/ios/ar_flutter_plugin.podspec @@ -18,9 +18,9 @@ A Flutter plugin for shared AR experiences supporting Android and iOS. 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.32.0' + s.dependency 'ARCore', '~> 1.37.0' + s.dependency 'ARCore/CloudAnchors', '~> 1.37.0' + s.dependency 'ARCore/Geospatial', '~> 1.37.0' s.platform = :ios, '13.0' diff --git a/lib/managers/ar_anchor_manager.dart b/lib/managers/ar_anchor_manager.dart index 817bc27e..5d30b244 100644 --- a/lib/managers/ar_anchor_manager.dart +++ b/lib/managers/ar_anchor_manager.dart @@ -37,6 +37,11 @@ class ARAnchorManager { _channel.invokeMethod('initGoogleCloudAnchorMode', {}); } + /// Activates collaborative AR mode (using Google Cloud Anchors) + initGeoSpatialMode() async { + _channel.invokeMethod('initGeoSpatialMode', {}); + } + Future _platformCallHandler(MethodCall call) async { if (debug) { print('_platformCallHandler call ${call.method} ${call.arguments}'); diff --git a/lib/managers/ar_session_manager.dart b/lib/managers/ar_session_manager.dart index 20d3b83a..3ee655c0 100644 --- a/lib/managers/ar_session_manager.dart +++ b/lib/managers/ar_session_manager.dart @@ -29,8 +29,7 @@ class ARSessionManager { /// 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}) { + ARSessionManager(int id, this.buildContext, this.planeDetectionConfig, {this.debug = false}) { _channel = MethodChannel('arsession_$id'); _channel.setMethodCallHandler(_platformCallHandler); if (debug) { @@ -41,8 +40,7 @@ class ARSessionManager { /// 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', {}); + final serializedCameraPose = await _channel.invokeMethod>('getCameraPose', {}); return MatrixConverter().fromJson(serializedCameraPose!); } catch (e) { print('Error caught: ' + e.toString()); @@ -50,14 +48,41 @@ class ARSessionManager { } } + /// Places anchor in given 2D coordinates (starting from the top-left side of the screen) + Future placeBasedOnScreenCoordinates(double x, double y) async { + try { + return await _channel.invokeMethod>('placeBasedOnCoordinates', {"x": x, "y": y}); + } catch (e) { + print('Error caught: ' + e.toString()); + return null; + } + } + + /// Places anchor in lat-lon-alt coordinates using GeoSpatial API + Future placeGeospatial(double lat, double lon, double alt, String name) async { + try { + var result = await _channel.invokeMethod('placeGeospatial', { + "lat": lat, + "lon": lon, + "alt": alt, + "name": name, + }); + + var anchor = ARAnchor.fromJson(result); + return anchor; + } 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', { + final serializedCameraPose = await _channel.invokeMethod>('getAnchorPose', { "anchorId": anchor.name, }); return MatrixConverter().fromJson(serializedCameraPose!); @@ -68,8 +93,7 @@ class ARSessionManager { } /// Returns the distance in meters between @anchor1 and @anchor2. - Future getDistanceBetweenAnchors( - ARAnchor anchor1, ARAnchor anchor2) async { + Future getDistanceBetweenAnchors(ARAnchor anchor1, ARAnchor anchor2) async { var anchor1Pose = await getPose(anchor1); var anchor2Pose = await getPose(anchor2); var anchor1Translation = anchor1Pose?.getTranslation(); @@ -118,10 +142,8 @@ class ARSessionManager { case 'onPlaneOrPointTap': if (onPlaneOrPointTap != null) { final rawHitTestResults = call.arguments as List; - final serializedHitTestResults = rawHitTestResults - .map( - (hitTestResult) => Map.from(hitTestResult)) - .toList(); + final serializedHitTestResults = + rawHitTestResults.map((hitTestResult) => Map.from(hitTestResult)).toList(); final hitTestResults = serializedHitTestResults.map((e) { return ARHitTestResult.fromJson(e); }).toList(); @@ -172,15 +194,13 @@ class ARSessionManager { onError(String errorMessage) { ScaffoldMessenger.of(buildContext).showSnackBar(SnackBar( content: Text(errorMessage), - action: SnackBarAction( - label: 'HIDE', - onPressed: - ScaffoldMessenger.of(buildContext).hideCurrentSnackBar))); + 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 { + print('calling dispose from flutter'); try { await _channel.invokeMethod("dispose"); } catch (e) { diff --git a/lib/models/ar_anchor.dart b/lib/models/ar_anchor.dart index 4ab87e84..3cb197cd 100644 --- a/lib/models/ar_anchor.dart +++ b/lib/models/ar_anchor.dart @@ -20,7 +20,7 @@ abstract class ARAnchor { final String name; /// Constructs an [ARAnchor] from a serialized anchor object - factory ARAnchor.fromJson(Map arguments) { + factory ARAnchor.fromJson(Map arguments) { final type = arguments['type']; switch (type) { case 0: //(= AnchorType.plane) @@ -59,7 +59,7 @@ class ARPlaneAnchor extends ARAnchor { /// 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) => + static ARPlaneAnchor fromJson(Map json) => aRPlaneAnchorFromJson(json); @override @@ -67,7 +67,7 @@ class ARPlaneAnchor extends ARAnchor { } /// Constructs an [ARPlaneAnchor] from a serialized PlaneAnchor object -ARPlaneAnchor aRPlaneAnchorFromJson(Map json) { +ARPlaneAnchor aRPlaneAnchorFromJson(Map json) { return ARPlaneAnchor( transformation: const MatrixConverter().fromJson(json['transformation'] as List), @@ -99,14 +99,14 @@ class ARUnkownAnchor extends ARAnchor { {required AnchorType type, required Matrix4 transformation, String? name}) : super(type: type, transformation: transformation, name: name); - static ARUnkownAnchor fromJson(Map json) => + static ARUnkownAnchor fromJson(Map json) => aRUnkownAnchorFromJson(json); @override Map toJson() => aRUnkownAnchorToJson(this); } -ARUnkownAnchor aRUnkownAnchorFromJson(Map json) { +ARUnkownAnchor aRUnkownAnchorFromJson(Map json) { return ARUnkownAnchor( type: json['type'], transformation: diff --git a/pubspec.yaml b/pubspec.yaml index 8a25261f..88658a3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: ar_flutter_plugin description: Flutter Plugin for creating (collaborative) Augmented Reality experiences - Supports ARKit for iOS and ARCore for Android devices. -version: 0.7.3 +version: 0.7.4 homepage: https://lars.carius.io repository: https://github.com/CariusLars/ar_flutter_plugin @@ -8,13 +8,14 @@ environment: sdk: ">=2.16.1 <3.0.0" flutter: ">=1.20.0" + dependencies: flutter: sdk: flutter - permission_handler: ^10.1.0 - vector_math: ^2.1.1 - json_annotation: ^4.5.0 - geolocator: ^9.0.0 + permission_handler: ^11.3.1 + vector_math: ^2.1.4 + json_annotation: ^4.9.0 + geolocator: ^12.0.0 dev_dependencies: flutter_test: