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 1b39d2ea..d235ac0a 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 @@ -80,6 +80,7 @@ internal class AndroidARView( // Setting defaults private var enableRotation = false private var enablePans = false + private var enableScaling = false private var keepNodeSelected = true; private var footprintSelectionVisualizer = FootprintSelectionVisualizer() // Model builder @@ -302,7 +303,7 @@ internal class AndroidARView( MaterialFactory.makeTransparentWithColor(context, Color(255f, 255f, 255f, 0.3f)) .thenAccept { mat -> - footprintSelectionVisualizer.footprintRenderable = ShapeFactory.makeCylinder(0.7f,0.05f, Vector3(0f,0f,0f), mat) + footprintSelectionVisualizer.footprintRenderable = ShapeFactory.makeCylinder(0.7f,0.05f, Vector3(0f,0f,0.25f), mat) } transformationSystem = @@ -453,6 +454,7 @@ internal class AndroidARView( val argShowWorldOrigin: Boolean? = call.argument("showWorldOrigin") val argHandleTaps: Boolean? = call.argument("handleTaps") val argHandleRotation: Boolean? = call.argument("handleRotation") + val argHandleScaling: Boolean? = call.argument("handleScaling") val argHandlePans: Boolean? = call.argument("handlePans") val argShowAnimatedGuide: Boolean? = call.argument("showAnimatedGuide") @@ -463,8 +465,9 @@ internal class AndroidARView( 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) + //transformationSystem.selectNode(hitTestResult.node as TransformableNode) //} + if (hitTestResult.node != null && motionEvent?.action == MotionEvent.ACTION_DOWN) { objectManagerChannel.invokeMethod("onNodeTap", listOf(hitTestResult.node?.name)) } @@ -580,6 +583,12 @@ internal class AndroidARView( } else { enablePans = false } + if (argHandleScaling == + true) { // explicit comparison necessary because of nullable type + enableScaling = true + } else { + enableScaling = false + } result.success(null) } @@ -636,7 +645,7 @@ internal class AndroidARView( transformationSystem.selectNode(null) keepNodeSelected = true } - if (!enablePans && !enableRotation){ + if (!enablePans && !enableRotation && !enableScaling){ //unselect all nodes as we do not want the selection visualizer transformationSystem.selectNode(null) } @@ -645,28 +654,37 @@ internal class AndroidARView( 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) + modelBuilder.makeNodeFromGltf(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, enableScaling, 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) { + val mainHandler = Handler(viewContext.mainLooper) + val runnable = Runnable {sessionManagerChannel.invokeMethod("onError", listOf("1 LOADED RENDERABLE " + node.getRenderable()?.getMaterial().toString() )) } + mainHandler.post(runnable) + anchorNode.addChild(node) + completableFutureSuccess.complete(true) } else { completableFutureSuccess.complete(false) } } else { arSceneView.scene.addChild(node) + val mainHandler = Handler(viewContext.mainLooper) + val runnable = Runnable {sessionManagerChannel.invokeMethod("onError", listOf("2 LOADED RENDERABLE " + node )) } + mainHandler.post(runnable) + completableFutureSuccess.complete(true) } completableFutureSuccess.complete(false) @@ -674,14 +692,15 @@ internal class AndroidARView( .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)) } + 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) + modelBuilder.makeNodeFromGlb(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, enableScaling, 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 @@ -711,7 +730,7 @@ internal class AndroidARView( 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) // + modelBuilder.makeNodeFromGlb(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, enableScaling, 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 @@ -743,7 +762,7 @@ internal class AndroidARView( 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) + modelBuilder.makeNodeFromGltf(viewContext, transformationSystem, objectManagerChannel, enablePans, enableRotation, enableScaling, 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 @@ -776,7 +795,6 @@ internal class AndroidARView( } catch (e: java.lang.Exception) { completableFutureSuccess.completeExceptionally(e) } - return completableFutureSuccess } @@ -800,7 +818,7 @@ internal class AndroidARView( return true } if (motionEvent != null && motionEvent.action == MotionEvent.ACTION_DOWN) { - if (transformationSystem.selectedNode == null || (!enablePans && !enableRotation)){ + if (transformationSystem.selectedNode == null || (!enablePans && !enableRotation && !enableScaling)){ val allHitResults = frame?.hitTest(motionEvent) ?: listOf() val planeAndPointHitResults = allHitResults.filter { ((it.trackable is Plane) || (it.trackable is Point)) } diff --git a/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/ArModelBuilder.kt b/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/ArModelBuilder.kt index 455e808f..fd927d66 100644 --- a/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/ArModelBuilder.kt +++ b/android/src/main/kotlin/io/carius/lars/ar_flutter_plugin/ArModelBuilder.kt @@ -91,10 +91,9 @@ class ArModelBuilder { } // 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 { + fun makeNodeFromGltf(context: Context, transformationSystem: TransformationSystem, objectManagerChannel: MethodChannel, enablePans: Boolean, enableRotation: Boolean, enableScaling: Boolean, name: String, modelPath: String, transformation: ArrayList): CompletableFuture { val completableFutureNode: CompletableFuture = CompletableFuture() - - val gltfNode = CustomTransformableNode(transformationSystem, objectManagerChannel, enablePans, enableRotation) + val gltfNode = CustomTransformableNode(transformationSystem, objectManagerChannel, enablePans, enableRotation, enableScaling) ModelRenderable.builder() .setSource(context, RenderableSource.builder().setSource( @@ -118,14 +117,15 @@ class ArModelBuilder { 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 { + fun makeNodeFromGlb(context: Context, transformationSystem: TransformationSystem, objectManagerChannel: MethodChannel, enablePans: Boolean, enableRotation: Boolean, enableScaling: Boolean, name: String, modelPath: String, transformation: ArrayList): CompletableFuture { val completableFutureNode: CompletableFuture = CompletableFuture() - val gltfNode = CustomTransformableNode(transformationSystem, objectManagerChannel, enablePans, enableRotation) + val gltfNode = CustomTransformableNode(transformationSystem, objectManagerChannel, enablePans, enableRotation, enableScaling) //gltfNode.scaleController.isEnabled = false //gltfNode.translationController.isEnabled = false @@ -164,12 +164,14 @@ class ArModelBuilder { } } -class CustomTransformableNode(transformationSystem: TransformationSystem, objectManagerChannel: MethodChannel, enablePans: Boolean, enableRotation: Boolean) : +class CustomTransformableNode(transformationSystem: TransformationSystem, objectManagerChannel: MethodChannel, enablePans: Boolean, enableRotation: Boolean, enableScaling: Boolean) : TransformableNode(transformationSystem) { // private lateinit var customTranslationController: CustomTranslationController private lateinit var customRotationController: CustomRotationController + + private lateinit var customScaleController: CustomScaleController init { // Remove standard controllers @@ -197,6 +199,17 @@ class CustomTransformableNode(transformationSystem: TransformationSystem, object objectManagerChannel ) addTransformationController(customRotationController) + } + if (enableScaling) { + customScaleController = CustomScaleController( + this, + transformationSystem.pinchRecognizer, + objectManagerChannel + ) + + customScaleController.setMinScale(0.1f); + customScaleController.setMaxScale(4.0f); + addTransformationController(customScaleController) } } } @@ -246,3 +259,26 @@ class CustomRotationController(transformableNode: BaseTransformableNode, gesture super.onEndTransformation(gesture) } } + +class CustomScaleController(transformableNode: BaseTransformableNode, gestureRecognizer: PinchGestureRecognizer, objectManagerChannel: MethodChannel) : + ScaleController(transformableNode, gestureRecognizer) { + + val platformChannel: MethodChannel = objectManagerChannel + + override fun canStartTransformation(gesture: PinchGesture): Boolean { + platformChannel.invokeMethod("onScaleStart", transformableNode.name) + super.canStartTransformation(gesture) + return transformableNode.isSelected + } + + override fun onContinueTransformation(gesture: PinchGesture) { + platformChannel.invokeMethod("onScaleChange", transformableNode.name) + super.onContinueTransformation(gesture) + } + + override fun onEndTransformation(gesture: PinchGesture) { + val serializedLocalTransformation = serializeLocalTransformation(transformableNode) + platformChannel.invokeMethod("onScaleEnd", serializedLocalTransformation) + super.onEndTransformation(gesture) + } +} diff --git a/lib/managers/ar_object_manager.dart b/lib/managers/ar_object_manager.dart index d5e72d33..98803167 100644 --- a/lib/managers/ar_object_manager.dart +++ b/lib/managers/ar_object_manager.dart @@ -14,6 +14,9 @@ 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); +typedef NodeScaleStartHandler = void Function(String node); +typedef NodeScaleChangeHandler = void Function(String node); +typedef NodeScaleEndHandler = void Function(String node, Matrix4 transform); /// Manages the all node-related actions of an [ARView] class ARObjectManager { @@ -31,6 +34,9 @@ class ARObjectManager { NodeRotationStartHandler? onRotationStart; NodeRotationChangeHandler? onRotationChange; NodeRotationEndHandler? onRotationEnd; + NodeScaleStartHandler? onScaleStart; + NodeScaleChangeHandler? onScaleChange; + NodeScaleEndHandler? onScaleEnd; ARObjectManager(int id, {this.debug = false}) { _channel = MethodChannel('arobjects_$id'); @@ -103,6 +109,28 @@ class ARObjectManager { onRotationEnd!(tappedNodeName, transform); } break; + case 'onScaleStart': + if (onScaleStart != null) { + final tappedNode = call.arguments as String; + onScaleStart!(tappedNode); + } + break; + case 'onScaleChange': + if (onScaleChange != null) { + final tappedNode = call.arguments as String; + onScaleChange!(tappedNode); + } + break; + case 'onScaleEnd': + if (onScaleEnd != null) { + final tappedNodeName = call.arguments["name"] as String; + final transform = + MatrixConverter().fromJson(call.arguments['transform'] as List); + + // Notify callback + onScaleEnd!(tappedNodeName, transform); + } + break; default: if (debug) { print('Unimplemented method ${call.method} '); @@ -137,6 +165,7 @@ class ARObjectManager { return await _channel.invokeMethod('addNode', node.toMap()); } } on PlatformException catch (e) { + print("add exception"); return false; } } diff --git a/lib/managers/ar_session_manager.dart b/lib/managers/ar_session_manager.dart index 6ac28144..9637f040 100644 --- a/lib/managers/ar_session_manager.dart +++ b/lib/managers/ar_session_manager.dart @@ -84,6 +84,7 @@ class ARSessionManager { bool handleTaps = true, bool handlePans = false, // nodes are not draggable by default bool handleRotation = false, // nodes can not be rotated by default + bool handleScaling = false, // nodes can not be scaled by default }) { _channel.invokeMethod('init', { 'showAnimatedGuide': showAnimatedGuide, @@ -95,6 +96,7 @@ class ARSessionManager { 'handleTaps': handleTaps, 'handlePans': handlePans, 'handleRotation': handleRotation, + 'handleScaling': handleScaling, }); }