diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreAugmentedImagesView.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreAugmentedImagesView.kt index 376f4e01..9cbf809e 100644 --- a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreAugmentedImagesView.kt +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreAugmentedImagesView.kt @@ -17,6 +17,7 @@ import com.google.ar.core.exceptions.CameraNotAvailableException import com.google.ar.core.exceptions.UnavailableException import com.google.ar.sceneform.AnchorNode import com.google.ar.sceneform.Scene +import com.google.ar.sceneform.ux.TransformationSystem import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreView.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreView.kt index 11ba9e5d..9768f6bd 100644 --- a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreView.kt +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/ArCoreView.kt @@ -14,17 +14,19 @@ import android.widget.Toast import com.difrancescogianmarco.arcore_flutter_plugin.flutter_models.FlutterArCoreHitTestResult import com.difrancescogianmarco.arcore_flutter_plugin.flutter_models.FlutterArCoreNode import com.difrancescogianmarco.arcore_flutter_plugin.flutter_models.FlutterArCorePose +import com.difrancescogianmarco.arcore_flutter_plugin.flutter_models.FlutterArCoreSceneSetup import com.difrancescogianmarco.arcore_flutter_plugin.models.RotatingNode import com.difrancescogianmarco.arcore_flutter_plugin.utils.ArCoreUtils +import com.difrancescogianmarco.arcore_flutter_plugin.utils.DecodableUtils import com.google.ar.core.* import com.google.ar.core.exceptions.CameraNotAvailableException import com.google.ar.core.exceptions.UnavailableException import com.google.ar.core.exceptions.UnavailableUserDeclinedInstallationException import com.google.ar.sceneform.* +import com.google.ar.sceneform.math.Vector3 import com.google.ar.sceneform.rendering.ModelRenderable import com.google.ar.sceneform.rendering.Texture import com.google.ar.sceneform.ux.AugmentedFaceNode -import io.flutter.app.FlutterApplication import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -32,16 +34,17 @@ import io.flutter.plugin.platform.PlatformView class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMessenger, id: Int, private val isAugmentedFaces: Boolean, private val debug: Boolean) : PlatformView, MethodChannel.MethodCallHandler { private val methodChannel: MethodChannel = MethodChannel(messenger, "arcore_flutter_plugin_$id") - // private val activity: Activity = (context.applicationContext as FlutterApplication).currentActivity + + private val TAG: String = ArCoreView::class.java.name lateinit var activityLifecycleCallbacks: Application.ActivityLifecycleCallbacks - private var installRequested: Boolean = false + private val coordinator by lazy { Coordinator(activity, ::onArTap, ::onNodeSelected, ::onNodeFocused, ::onNodeTapped) } private var mUserRequestedInstall = true - private val TAG: String = ArCoreView::class.java.name private var arSceneView: ArSceneView? = null private val gestureDetector: GestureDetector private val RC_PERMISSIONS = 0x123 private var sceneUpdateListener: Scene.OnUpdateListener private var faceSceneUpdateListener: Scene.OnUpdateListener + private var sceneOnPeekTouchListener: Scene.OnPeekTouchListener //AUGMENTEDFACE private var faceRegionsRenderable: ModelRenderable? = null @@ -88,6 +91,10 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess } } + sceneOnPeekTouchListener = Scene.OnPeekTouchListener { hitTestResult, motionEvent -> + coordinator.onTouch(hitTestResult, motionEvent) + } + faceSceneUpdateListener = Scene.OnUpdateListener { frameTime -> run { // if (faceRegionsRenderable == null || faceMeshTexture == null) { @@ -129,12 +136,19 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess setupLifeCycle(context) } - fun debugLog(message: String) { + fun debugLog(message: String?) { if (debug) { - Log.i(TAG, message) + Log.i(TAG, message ?: "null message") } } +/* private fun onArUpdate() { + val frame = arSceneView?.arFrame + val camera = frame?.camera + val state = camera?.trackingState + val reason = camera?.trackingFailureReason + }*/ + fun loadMesh(textureBytes: ByteArray?) { // Load the face regions renderable. @@ -160,7 +174,7 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "init" -> { - arScenViewInit(call, result, activity) + arScenViewInit(FlutterArCoreSceneSetup(call.arguments as Map), result) } "addArCoreNode" -> { debugLog(" addArCoreNode") @@ -169,7 +183,7 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess onAddNode(flutterNode, result) } "addArCoreNodeWithAnchor" -> { - debugLog(" addArCoreNode") + debugLog(" addArCoreNodeWithAnchor") val map = call.arguments as HashMap val flutterNode = FlutterArCoreNode(map) addNodeWithAnchor(flutterNode, result) @@ -179,19 +193,38 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess val map = call.arguments as HashMap removeNode(map["nodeName"] as String, result) } - "positionChanged" -> { - debugLog(" positionChanged") - + "positionConfigChanged" -> { + debugLog("positionConfigChanged") + val position =DecodableUtils.parseVector3(call.argument("position") as? HashMap) + coordinator.selectedNode?.apply { + localPosition = position + translationController.isEnabled = call.argument("enabled") as? Boolean ?: translationController.isEnabled + } + result.success(null) } - "rotationChanged" -> { - debugLog(" rotationChanged") - updateRotation(call, result) - + "scaleConfigChanged" -> { + debugLog(" scaleConfigChanged") + val scale = DecodableUtils.parseVector3(call.argument("scale") as? HashMap) + coordinator.focusedNode?.apply{ + scaleController.isEnabled = call.argument("enabled") as? Boolean ?: coordinator.focusedNode?.scaleController!!.isEnabled + parent?.apply { + localScale = scale + } + } + result.success(null) + } + "rotationConfigChanged" -> { + debugLog("rotationConfigChanged") + val rotation = DecodableUtils.parseQuaternion(call.argument("rotation") as? HashMap) + coordinator.selectedNode?.apply { + localRotation = rotation + rotationController.isEnabled = call.argument("enabled") as? Boolean ?: rotationController.isEnabled + } + result.success(null) } "updateMaterials" -> { debugLog(" updateMaterials") updateMaterials(call, result) - } "loadMesh" -> { val map = call.arguments as HashMap @@ -299,42 +332,29 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess } } - private fun arScenViewInit(call: MethodCall, result: MethodChannel.Result, context: Context) { + private fun arScenViewInit(flutterArCoreSceneSetup: FlutterArCoreSceneSetup, result: MethodChannel.Result) { debugLog("arScenViewInit") - val enableTapRecognizer: Boolean? = call.argument("enableTapRecognizer") - if (enableTapRecognizer != null && enableTapRecognizer) { - arSceneView - ?.scene - ?.setOnTouchListener { hitTestResult: HitTestResult, event: MotionEvent? -> - - if (hitTestResult.node != null) { - debugLog(" onNodeTap " + hitTestResult.node?.name) - debugLog(hitTestResult.node?.localPosition.toString()) - debugLog(hitTestResult.node?.worldPosition.toString()) - methodChannel.invokeMethod("onNodeTap", hitTestResult.node?.name) - return@setOnTouchListener true - } - return@setOnTouchListener gestureDetector.onTouchEvent(event) - } + + if (flutterArCoreSceneSetup.enableTapRecognizer == true) { + arSceneView?.scene?.addOnPeekTouchListener(sceneOnPeekTouchListener) } - val enableUpdateListener: Boolean? = call.argument("enableUpdateListener") - if (enableUpdateListener != null && enableUpdateListener) { + + if (flutterArCoreSceneSetup.enableUpdateListener == true) { // Set an update listener on the Scene that will hide the loading message once a Plane is // detected. arSceneView?.scene?.addOnUpdateListener(sceneUpdateListener) } - val enablePlaneRenderer: Boolean? = call.argument("enablePlaneRenderer") - if (enablePlaneRenderer != null && !enablePlaneRenderer) { - debugLog(" The plane renderer (enablePlaneRenderer) is set to " + enablePlaneRenderer.toString()) + if (flutterArCoreSceneSetup.enablePlaneRenderer == false) { + debugLog(" The plane renderer (enablePlaneRenderer) is set to false") arSceneView!!.planeRenderer.isVisible = false } - + result.success(null) } fun addNodeWithAnchor(flutterArCoreNode: FlutterArCoreNode, result: MethodChannel.Result) { - + debugLog("addNodeWithAnchor " + flutterArCoreNode.configuration.name) if (arSceneView == null) { return } @@ -344,18 +364,17 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess result.error("Make Renderable Error", t.localizedMessage, null) return@makeRenderable } - val myAnchor = arSceneView?.session?.createAnchor(Pose(flutterArCoreNode.getPosition(), flutterArCoreNode.getRotation())) - if (myAnchor != null) { - val anchorNode = AnchorNode(myAnchor) - anchorNode.name = flutterArCoreNode.name - anchorNode.renderable = renderable - - debugLog("addNodeWithAnchor inserted ${anchorNode.name}") - attachNodeToParent(anchorNode, flutterArCoreNode.parentNodeName) - for (node in flutterArCoreNode.children) { - node.parentNodeName = flutterArCoreNode.name - onAddNode(node, null) + val myAnchor = arSceneView?.session?.createAnchor(Pose(flutterArCoreNode.getPosition(), flutterArCoreNode.getRotation())) + myAnchor?.let { anchor -> + NodeFactory.makeTransformableNode(activity.applicationContext, flutterArCoreNode, debug, coordinator) { node, throwable -> + node?.let { baseNode -> + baseNode.attach(anchor, arSceneView!!.scene, true) + for (n in flutterArCoreNode.children) { + n.parentNodeName = flutterArCoreNode.configuration.name + onAddNode(n, null) + } + } } } result.success(null) @@ -368,25 +387,16 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess NodeFactory.makeNode(activity.applicationContext, flutterArCoreNode, debug) { node, throwable -> debugLog("onAddNode inserted ${node?.name}") - -/* if (flutterArCoreNode.parentNodeName != null) { - debugLog(flutterArCoreNode.parentNodeName); - val parentNode: Node? = arSceneView?.scene?.findByName(flutterArCoreNode.parentNodeName) - parentNode?.addChild(node) - } else { - debugLog("addNodeToSceneWithGeometry: NOT PARENT_NODE_NAME") - arSceneView?.scene?.addChild(node) - }*/ - if (node != null) { + node?.let { attachNodeToParent(node, flutterArCoreNode.parentNodeName) for (n in flutterArCoreNode.children) { - n.parentNodeName = flutterArCoreNode.name + n.parentNodeName = flutterArCoreNode.configuration.name onAddNode(n, null) } } - + result?.success(null) } - result?.success(null) + } fun attachNodeToParent(node: Node?, parentNodeName: String?) { @@ -509,54 +519,70 @@ class ArCoreView(val activity: Activity, context: Context, messenger: BinaryMess } fun onDestroy() { - if (arSceneView != null) { + if (arSceneView != null) { debugLog("Goodbye ARCore! Destroying the Activity now 7.") try { arSceneView?.scene?.removeOnUpdateListener(sceneUpdateListener) arSceneView?.scene?.removeOnUpdateListener(faceSceneUpdateListener) + arSceneView?.scene?.removeOnPeekTouchListener(sceneOnPeekTouchListener) debugLog("Goodbye arSceneView.") arSceneView?.destroy() arSceneView = null - }catch (e : Exception){ + } catch (e: Exception) { e.printStackTrace(); - } + } } } - /* private fun tryPlaceNode(tap: MotionEvent?, frame: Frame) { - if (tap != null && frame.camera.trackingState == TrackingState.TRACKING) { - for (hit in frame.hitTest(tap)) { - val trackable = hit.trackable - if (trackable is Plane && trackable.isPoseInPolygon(hit.hitPose)) { - // Create the Anchor. - val anchor = hit.createAnchor() - val anchorNode = AnchorNode(anchor) - anchorNode.setParent(arSceneView?.scene) - - ModelRenderable.builder() - .setSource(activity.applicationContext, Uri.parse("TocoToucan.sfb")) - .build() - .thenAccept { renderable -> - val node = Node() - node.renderable = renderable - anchorNode.addChild(node) - }.exceptionally { throwable -> - Log.e(TAG, "Unable to load Renderable.", throwable); - return@exceptionally null - } - } - } + private fun onArTap(motionEvent: MotionEvent) { + debugLog("onArTap") + val frame = arSceneView?.arFrame ?: return + if (frame.camera.trackingState != TrackingState.TRACKING) { + coordinator.selectNode(null) + return } - }*/ + frame.hitTest(motionEvent).firstOrNull { + val trackable = it.trackable + when { + trackable is Plane && trackable.isPoseInPolygon(it.hitPose) -> true + trackable is Point -> true + else -> false + } + }?.let { hit -> + val list = ArrayList>() + val distance: Float = hit.distance + val translation = hit.hitPose.translation + val rotation = hit.hitPose.rotationQuaternion + val flutterArCoreHitTestResult = FlutterArCoreHitTestResult(distance, translation, rotation) + val arguments = flutterArCoreHitTestResult.toHashMap() + list.add(arguments) + methodChannel.invokeMethod("onPlaneTap", list) + } ?: coordinator.selectNode(null) + } - /* fun updatePosition(call: MethodCall, result: MethodChannel.Result) { - val name = call.argument("name") - val node = arSceneView?.scene?.findByName(name) - node?.localPosition = parseVector3(call.arguments as HashMap) - result.success(null) - }*/ + //Called every frame + private fun onNodeUpdate(node: BaseNode?) { + //debugLog("onNodeUpdate: " + node?.name) + + } + + private fun onNodeSelected(old: BaseNode? = coordinator.selectedNode, new: BaseNode?) { + debugLog("onNodeSelected old: " + old?.name) + debugLog("onNodeSelected new: " + new?.name) + old?.onNodeUpdate = null + new?.onNodeUpdate = ::onNodeUpdate + } + + private fun onNodeFocused(node: BaseNode?) { + debugLog("onNodeFocused: " + node?.name) + } + + private fun onNodeTapped(node: Node?) { + debugLog("onNodeTapped: " + node?.name) + methodChannel.invokeMethod("onNodeTap", node?.name) + } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/BaseArCoreView.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/BaseArCoreView.kt index 963f63db..52b6f54c 100644 --- a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/BaseArCoreView.kt +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/BaseArCoreView.kt @@ -166,7 +166,7 @@ open class BaseArCoreView(val activity: Activity, context: Context, messenger: B if (node != null) { attachNodeToParent(node, flutterArCoreNode.parentNodeName) for (n in flutterArCoreNode.children) { - n.parentNodeName = flutterArCoreNode.name + n.parentNodeName = flutterArCoreNode.configuration.name onAddNode(n, null) } result?.success(null) diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/BaseNode.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/BaseNode.kt new file mode 100644 index 00000000..6fa26ec1 --- /dev/null +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/BaseNode.kt @@ -0,0 +1,164 @@ +package com.difrancescogianmarco.arcore_flutter_plugin + +import android.view.MotionEvent +import com.difrancescogianmarco.arcore_flutter_plugin.flutter_models.FlutterArCoreNodeConfiguration +import com.google.ar.core.Anchor +import com.google.ar.sceneform.* +import com.google.ar.sceneform.math.MathHelper +import com.google.ar.sceneform.math.Vector3 +import com.google.ar.sceneform.rendering.Renderable +import com.google.ar.sceneform.ux.* + +class BaseNode(coordinator: Coordinator, config: FlutterArCoreNodeConfiguration) : TransformableNode(coordinator) { + + init { + name = config.name + + localScale = config.currentScale + scaleController.apply { + isEnabled = config.scaleEnabled + minScale = config.minScale + maxScale = config.maxScale + } + + localPosition = config.currentPosition + translationController.apply { + isEnabled = config.translationEnabled + + } + + localRotation = config.currentRotation + rotationController.apply { + isEnabled = config.rotationEnabled + } + } + + override fun getTransformationSystem(): Coordinator = super.getTransformationSystem() as Coordinator + + + fun attach(anchor: Anchor, scene: Scene, focus: Boolean = false) { + setParent(AnchorNode(anchor).apply { setParent(scene) }) + if (focus) { + transformationSystem.focusNode(this) + } + } + + override fun setRenderable(renderable: Renderable?) { + super.setRenderable(renderable?.apply {}) + } + + override fun onTap(hitTestResult: HitTestResult?, motionEvent: MotionEvent?) { + super.onTap(hitTestResult, motionEvent) + if (isTransforming) return + transformationSystem.focusNode(this) + } + + var onNodeUpdate: ((BaseNode) -> Any)? = null + + override fun onUpdate(frameTime: FrameTime) { + onNodeUpdate?.invoke(this) + } +} + +// +//class CustomScaleController( +// transformableNode: BaseTransformableNode, +// gestureRecognizer: PinchGestureRecognizer +//) : +// BaseTransformationController(transformableNode, gestureRecognizer) { +// +// var minScale = DEFAULT_MIN_SCALE +// var maxScale = DEFAULT_MAX_SCALE +// var sensitivity = DEFAULT_SENSITIVITY +// var elasticity = DEFAULT_ELASTICITY +// +// private var currentScaleRatio: Float = 0.toFloat() +// +// private val scaleDelta: Float +// get() { +// val scaleDelta = maxScale - minScale +// +// if (scaleDelta <= 0.0f) { +// throw IllegalStateException("maxScale must be greater than minScale.") +// } +// +// return scaleDelta +// } +// +// private val clampedScaleRatio: Float +// get() = Math.min(1.0f, Math.max(0.0f, currentScaleRatio)) +// +// private val finalScale: Float +// get() { +// val elasticScaleRatio = clampedScaleRatio + elasticDelta +// return minScale + elasticScaleRatio * scaleDelta +// } +// +// private val elasticDelta: Float +// get() { +// val overRatio: Float +// if (currentScaleRatio > 1.0f) { +// overRatio = currentScaleRatio - 1.0f +// } else if (currentScaleRatio < 0.0f) { +// overRatio = currentScaleRatio +// } else { +// return 0.0f +// } +// +// return (1.0f - 1.0f / (Math.abs(overRatio) * elasticity + 1.0f)) * Math.signum(overRatio) +// } +// +// override fun onActivated(node: Node?) { +// super.onActivated(node) +// val scale = transformableNode.localScale +// currentScaleRatio = (scale.x - minScale) / scaleDelta +// } +// +// override fun onUpdated(node: Node?, frameTime: FrameTime?) { +// if (isTransforming) { +// return +// } +// +// val t = MathHelper.clamp(frameTime!!.deltaSeconds * LERP_SPEED, 0f, 1f) +// currentScaleRatio = MathHelper.lerp(currentScaleRatio, clampedScaleRatio, t) +// val finalScaleValue = finalScale +// val finalScale = Vector3(finalScaleValue, finalScaleValue, finalScaleValue) +// transformableNode.localScale = finalScale +// } +// +// public override fun canStartTransformation(gesture: PinchGesture): Boolean { +// return transformableNode.isSelected +// } +// +// fun getScaleRatio(): Float { +// return currentScaleRatio +// } +// +// fun updateScaleRatio(ratio: Float) { +// currentScaleRatio = ratio +// } +// +// public override fun onContinueTransformation(gesture: PinchGesture) { +// currentScaleRatio += gesture.gapDeltaInches() * sensitivity +// +// val finalScaleValue = finalScale +// val finalScale = Vector3(finalScaleValue, finalScaleValue, finalScaleValue) +// transformableNode.localScale = finalScale +// +// if (currentScaleRatio < -ELASTIC_RATIO_LIMIT || currentScaleRatio > 1.0f + ELASTIC_RATIO_LIMIT) { +// gesture.wasCancelled() +// } +// } +// +// public override fun onEndTransformation(gesture: PinchGesture) {} +// +// companion object { +// const val DEFAULT_MIN_SCALE = 0.75f +// const val DEFAULT_MAX_SCALE = 1.75f +// const val DEFAULT_SENSITIVITY = 0.75f +// const val DEFAULT_ELASTICITY = 0.15f +// +// private const val ELASTIC_RATIO_LIMIT = 0.8f +// private const val LERP_SPEED = 8.0f +// } +//} \ No newline at end of file diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/Coordinator.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/Coordinator.kt new file mode 100644 index 00000000..f0bf437f --- /dev/null +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/Coordinator.kt @@ -0,0 +1,83 @@ +package com.difrancescogianmarco.arcore_flutter_plugin + +import android.content.Context +import android.view.GestureDetector +import android.view.MotionEvent +import com.google.ar.sceneform.HitTestResult +import com.google.ar.sceneform.Node +import com.google.ar.sceneform.ux.BaseTransformableNode +import com.google.ar.sceneform.ux.SelectionVisualizer +import com.google.ar.sceneform.ux.TransformationSystem + +class Coordinator( + context: Context, + private val onArTap: (MotionEvent) -> Unit, + private val onNodeSelected: (old: BaseNode?, new: BaseNode?) -> Unit, + private val onNodeFocused: (nodes: BaseNode?) -> Unit, + private val onNodeTapped: (nodes: Node?) -> Unit +) : TransformationSystem( + context.resources.displayMetrics, SelectionNodeVisualizer(context) +) { + + override fun getSelectedNode(): BaseNode? = super.getSelectedNode() as? BaseNode + + private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(motionEvent: MotionEvent): Boolean { + onArTap(motionEvent) + return true + } + }) + + override fun getSelectionVisualizer(): SelectionNodeVisualizer { + return super.getSelectionVisualizer() as SelectionNodeVisualizer + } + + override fun setSelectionVisualizer(selectionVisualizer: SelectionVisualizer?) { + // Prevent changing the selection visualizer + } + + override fun onTouch(hitTestResult: HitTestResult?, motionEvent: MotionEvent?) { + super.onTouch(hitTestResult, motionEvent) + hitTestResult?.let{ + if (it.node == null) { + gestureDetector.onTouchEvent(motionEvent) + }else{ + onNodeTapped(it.node) + } + } + + } + + override fun selectNode(node: BaseTransformableNode?): Boolean { + val old = selectedNode + when (node) { + selectedNode -> return true /*ignored*/ + is BaseNode -> { + return super.selectNode(node).also { selected -> + if (!selected) return@also + onNodeSelected(old, node) + /*transfer current focus*/ + if (old != null && old == focusedNode) focusNode(node) + } + } + null -> { + return super.selectNode(null).also { + focusNode(null) + onNodeSelected(old, null) + } + } + } + return false + } + + var focusedNode: BaseNode? = null + private set + + fun focusNode(node: BaseNode?) { + if (node == focusedNode) return /*ignored*/ + focusedNode = node + if (node != null && node != selectedNode) selectNode(node) + onNodeFocused(node) + } + +} diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/NodeFactory.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/NodeFactory.kt index c8dc062c..8f923378 100644 --- a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/NodeFactory.kt +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/NodeFactory.kt @@ -4,15 +4,18 @@ import android.content.Context import android.util.Log import com.difrancescogianmarco.arcore_flutter_plugin.flutter_models.FlutterArCoreNode import com.google.ar.sceneform.Node +import com.google.ar.sceneform.ux.TransformableNode +import com.google.ar.sceneform.ux.TransformationSystem typealias NodeHandler = (Node?, Throwable?) -> Unit +typealias TransformableNodeHandler = (BaseNode?, Throwable?) -> Unit class NodeFactory { companion object { val TAG: String = NodeFactory::class.java.name - fun makeNode(context: Context, flutterNode: FlutterArCoreNode, debug: Boolean, handler: NodeHandler) { + fun makeNode(context: Context, flutterNode: FlutterArCoreNode, debug: Boolean, handler: NodeHandler) { if (debug) { Log.i(TAG, flutterNode.toString()) } @@ -21,8 +24,23 @@ class NodeFactory { if (renderable != null) { node.renderable = renderable handler(node, null) - }else{ - handler(null,t) + } else { + handler(null, t) + } + } + } + + fun makeTransformableNode(context: Context, flutterNode: FlutterArCoreNode, debug: Boolean, transformationSystem: Coordinator, handler: TransformableNodeHandler) { + if (debug) { + Log.i(TAG, flutterNode.toString()) + } + val node = flutterNode.buildTransformableNode(transformationSystem) + RenderableCustomFactory.makeRenderable(context, flutterNode) { renderable, t -> + if (renderable != null) { + node.renderable = renderable + handler(node, null) + } else { + handler(null, t) } } } diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/SelectionNodeVisualizer.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/SelectionNodeVisualizer.kt new file mode 100644 index 00000000..4181c21c --- /dev/null +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/SelectionNodeVisualizer.kt @@ -0,0 +1,44 @@ +package com.difrancescogianmarco.arcore_flutter_plugin + +import android.content.Context +import com.google.ar.sceneform.Node +import com.google.ar.sceneform.rendering.ModelRenderable +import com.google.ar.sceneform.ux.BaseTransformableNode +import com.google.ar.sceneform.ux.SelectionVisualizer + +class SelectionNodeVisualizer(context: Context) : SelectionVisualizer { + + interface Invisible + + private val node: Node = Node() + + var isEnabled: Boolean + get() = node.isEnabled + set(value) { + node.isEnabled = value + } + + init { + ModelRenderable.builder() + .setSource(context, R.raw.sceneform_footprint) + .build() + .thenAccept { + it.collisionShape = null + node.renderable = it.apply { collisionShape = null } + } + } + + override fun applySelectionVisual(node: BaseTransformableNode) { + when (node) { + is Invisible -> return + else -> this.node.setParent(node) + } + } + + override fun removeSelectionVisual(node: BaseTransformableNode) { + when (node) { + is Invisible -> return + else -> this.node.setParent(null) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreNode.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreNode.kt index 3622e607..d6734d21 100644 --- a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreNode.kt +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreNode.kt @@ -1,29 +1,21 @@ package com.difrancescogianmarco.arcore_flutter_plugin.flutter_models +import com.difrancescogianmarco.arcore_flutter_plugin.BaseNode +import com.difrancescogianmarco.arcore_flutter_plugin.Coordinator import com.difrancescogianmarco.arcore_flutter_plugin.models.RotatingNode -import com.difrancescogianmarco.arcore_flutter_plugin.utils.DecodableUtils.Companion.parseQuaternion -import com.difrancescogianmarco.arcore_flutter_plugin.utils.DecodableUtils.Companion.parseVector3 import com.google.ar.core.Pose import com.google.ar.sceneform.Node -import com.google.ar.sceneform.math.Quaternion -import com.google.ar.sceneform.math.Vector3 class FlutterArCoreNode(map: HashMap) { val dartType: String = map["dartType"] as String - val name: String = map["name"] as String val image: FlutterArCoreImage? = createArCoreImage(map["image"] as? HashMap) val objectUrl: String? = map["objectUrl"] as? String val object3DFileName: String? = map["object3DFileName"] as? String val shape: FlutterArCoreShape? = getShape(map["shape"] as? HashMap) - val position: Vector3 = parseVector3(map["position"] as? HashMap) ?: Vector3() - val scale: Vector3 = parseVector3(map["scale"] as? HashMap) - ?: Vector3(1.0F, 1.0F, 1.0F) - val rotation: Quaternion = parseQuaternion(map["rotation"] as? HashMap) - ?: Quaternion() - val degreesPerSecond: Float? = getDegreesPerSecond((map["degreesPerSecond"] as? Double)) + private val degreesPerSecond: Float? = getDegreesPerSecond((map["degreesPerSecond"] as? Double)) var parentNodeName: String? = map["parentNodeName"] as? String - + val configuration: FlutterArCoreNodeConfiguration = FlutterArCoreNodeConfiguration(map) val children: ArrayList = getChildrenFromMap(map["children"] as ArrayList>) private fun getChildrenFromMap(list: ArrayList>): ArrayList { @@ -31,27 +23,30 @@ class FlutterArCoreNode(map: HashMap) { } fun buildNode(): Node { - lateinit var node: Node - if (degreesPerSecond != null) { - node = RotatingNode(degreesPerSecond, true, 0.0f) + val node: Node = if (degreesPerSecond != null) { + RotatingNode(degreesPerSecond, true, 0.0f) } else { - node = Node() + Node() } - node.name = name - node.localPosition = position - node.localScale = scale - node.localRotation = rotation + node.name = configuration.name + node.localPosition = configuration.currentPosition + node.localScale = configuration.currentScale + node.localRotation = configuration.currentRotation return node } + fun buildTransformableNode(transformationSystem: Coordinator): BaseNode { + return BaseNode(transformationSystem, configuration) + } + fun getPosition(): FloatArray { - return floatArrayOf(position.x, position.y, position.z) + return floatArrayOf(configuration.currentPosition.x, configuration.currentPosition.y, configuration.currentPosition.z) } fun getRotation(): FloatArray { - return floatArrayOf(rotation.x, rotation.y, rotation.z, rotation.w) + return floatArrayOf(configuration.currentRotation.x, configuration.currentRotation.y, configuration.currentRotation.z, configuration.currentRotation.w) } fun getPose(): Pose { @@ -81,13 +76,13 @@ class FlutterArCoreNode(map: HashMap) { override fun toString(): String { return "dartType: $dartType\n" + - "name: $name\n" + + "name: ${configuration.name}\n" + "shape: ${shape.toString()}\n" + "object3DFileName: $object3DFileName \n" + "objectUrl: $objectUrl \n" + - "position: $position\n" + - "scale: $scale\n" + - "rotation: $rotation\n" + + "position: ${configuration.currentPosition}\n" + + "scale: ${configuration.currentScale}\n" + + "rotation: ${configuration.currentRotation}\n" + "parentNodeName: $parentNodeName" } diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreNodeConfiguration.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreNodeConfiguration.kt new file mode 100644 index 00000000..3a0f32ad --- /dev/null +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreNodeConfiguration.kt @@ -0,0 +1,31 @@ +package com.difrancescogianmarco.arcore_flutter_plugin.flutter_models + +import com.difrancescogianmarco.arcore_flutter_plugin.utils.DecodableUtils +import com.google.ar.sceneform.math.Quaternion +import com.google.ar.sceneform.math.Vector3 + +class FlutterArCoreNodeConfiguration(map: Map) { + + val name: String = map["name"] as String + + //ScaleController + val scaleEnabled = map["scaleGestureEnabled"] as? Boolean ?: true + val minScale = map["minScale"] as? Float ?: 0.25F + val maxScale = map["maxScale"] as? Float ?: 5.0F + + val scaleControllerNodeMap = map["scaleControllerNode"] as HashMap + val currentScale: Vector3 = DecodableUtils.parseVector3(scaleControllerNodeMap["scale"] as? HashMap) + ?: Vector3(1.0F, 1.0F, 1.0F) + + //TranslationController + val translationControllerNodeMap = map["translationControllerNode"] as HashMap + val translationEnabled = map["translationGestureEnabled"] as? Boolean ?: true + val currentPosition: Vector3 = DecodableUtils.parseVector3(translationControllerNodeMap["position"] as? HashMap) ?: Vector3() + + //RotationController + val rotationControllerNode = map["rotationControllerNode"] as HashMap + val rotationEnabled = map["rotationGestureEnabled"] as? Boolean ?: true + val currentRotation: Quaternion = DecodableUtils.parseQuaternion(rotationControllerNode["rotation"] as? HashMap) + ?: Quaternion() + +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreSceneSetup.kt b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreSceneSetup.kt new file mode 100644 index 00000000..ce2c8542 --- /dev/null +++ b/android/src/main/kotlin/com/difrancescogianmarco/arcore_flutter_plugin/flutter_models/FlutterArCoreSceneSetup.kt @@ -0,0 +1,10 @@ +package com.difrancescogianmarco.arcore_flutter_plugin.flutter_models + +class FlutterArCoreSceneSetup(map: Map) { + + + val enableTapRecognizer: Boolean? = map["enableTapRecognizer"] as? Boolean + val enableUpdateListener: Boolean? = map["enableUpdateListener"] as? Boolean + val enablePlaneRenderer: Boolean? = map["enablePlaneRenderer"] as? Boolean + +} \ No newline at end of file diff --git a/example/lib/home.dart b/example/lib/home.dart index 34d354b6..a270e4a3 100644 --- a/example/lib/home.dart +++ b/example/lib/home.dart @@ -3,6 +3,7 @@ import 'package:arcore_flutter_plugin_example/screens/augmented_images.dart'; import 'package:arcore_flutter_plugin_example/screens/image_object.dart'; import 'package:arcore_flutter_plugin_example/screens/matri_3d.dart'; import 'package:arcore_flutter_plugin_example/screens/multiple_augmented_images.dart'; +import 'package:arcore_flutter_plugin_example/screens/transformable_node.dart'; import 'package:flutter/material.dart'; import 'screens/hello_world.dart'; import 'screens/custom_object.dart'; @@ -105,6 +106,13 @@ class HomeScreen extends StatelessWidget { }, title: Text("Augmented Faces"), ), + ListTile( + onTap: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => TransformableNodeScreen())); + }, + title: Text("Transformable Node"), + ), ], ), ); diff --git a/example/lib/screens/transformable_node.dart b/example/lib/screens/transformable_node.dart new file mode 100644 index 00000000..601c859b --- /dev/null +++ b/example/lib/screens/transformable_node.dart @@ -0,0 +1,276 @@ +import 'dart:typed_data'; + +import 'package:arcore_flutter_plugin/arcore_flutter_plugin.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:vector_math/vector_math_64.dart' as vector; + +class TransformableNodeScreen extends StatefulWidget { + @override + _TransformableNodeState createState() => _TransformableNodeState(); +} + +class _TransformableNodeState extends State { + ArCoreController arCoreController; + String selectedNode; + final nodesMap = {}; + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Custom Object on plane detected'), + ), + body: Stack( + fit: StackFit.expand, + children: [ + ArCoreView( + onArCoreViewCreated: _onArCoreViewCreated, + enableTapRecognizer: true, + enableUpdateListener: true, + debug: true, + ), + Align( + alignment: Alignment.bottomCenter, + child: Container( + color: Colors.white, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(selectedNode ?? 'Unselected node'), + Row( + children: [ + Text('Scale'), + IconButton( + icon: Icon(Icons.remove), + onPressed: selectedNode != null + ? () { + if (nodesMap.containsKey(selectedNode)) { + final node = nodesMap[selectedNode]; + node.changeScale(node.scale - + vector.Vector3(1.0, 1.0, 1.0)); + + // final controller = nodesMap[selectedNode] + // .scaleControllerNode; + // controller.value = controller.value + // .copyWith( + // scale: controller.value.scale - + // vector.Vector3( + // 1.0, 1.0, 1.0)); + setState(() {}); + } + } + : null), + IconButton( + icon: Icon(Icons.add), + onPressed: selectedNode != null + ? () { + if (nodesMap.containsKey(selectedNode)) { + final node = nodesMap[selectedNode]; + node.changeScale(node.scale + + vector.Vector3(1.0, 1.0, 1.0)); + // final controller = nodesMap[selectedNode] + // .scaleControllerNode; + // controller.value = controller.value + // .copyWith( + // scale: controller.value.scale + + // vector.Vector3( + // 1.0, 1.0, 1.0)); + setState(() {}); + } + } + : null), + if (selectedNode != null && + nodesMap.containsKey(selectedNode)) ...[ + Checkbox( + value: nodesMap[selectedNode].scaleGestureEnabled, + onChanged: (value) { + nodesMap[selectedNode].scaleGestureEnabled = + value; + setState(() {}); + }, + ), + Text(nodesMap[selectedNode].scale.text), + ] + ], + ), + Row( + children: [ + Text('Position'), + IconButton( + icon: Icon(Icons.remove), + onPressed: selectedNode != null + ? () { + if (nodesMap.containsKey(selectedNode)) { + final node = nodesMap[selectedNode]; + node.changePosition(node.position - + vector.Vector3(0.3, 0.0, 0.0)); + + /* final controller = nodesMap[selectedNode] + .translationControllerNode; + controller.value = controller.value + .copyWith( + position: + controller.value.position - + vector.Vector3( + 0.3, 0.0, 0.0));*/ + setState(() {}); + } + } + : null), + IconButton( + icon: Icon(Icons.add), + onPressed: selectedNode != null + ? () { + if (nodesMap.containsKey(selectedNode)) { + final node = nodesMap[selectedNode]; + node.changePosition(node.position + + vector.Vector3(0.3, 0.0, 0.0)); + + // final controller = nodesMap[selectedNode] + // .translationControllerNode; + // controller.value = controller.value + // .copyWith( + // position: + // controller.value.position + + // vector.Vector3( + // 0.3, 0.0, 0.0)); + setState(() {}); + } + } + : null), + if (selectedNode != null && + nodesMap.containsKey(selectedNode)) ...[ + Checkbox( + value: + nodesMap[selectedNode].positionGestureEnabled, + onChanged: (value) { + nodesMap[selectedNode].positionGestureEnabled = + value; + setState(() {}); + }, + ), + Text(nodesMap[selectedNode].position.text), + ] + ], + ), + Row( + children: [ + Text('Rotation'), + IconButton( + icon: Icon(Icons.remove), + onPressed: selectedNode != null + ? () { + if (nodesMap.containsKey(selectedNode)) { + final node = nodesMap[selectedNode]; + node.changeRotation(node.rotation - + vector.Vector4(0.1, 0.1, 0.1, 0.1)); + setState(() {}); + // final controller = + // node.rotationControllerNode; + // controller.value = controller.value + // .copyWith( + // rotation: node.rotation - + // vector.Vector4( + // 0.1, 0.1, 0.1, 0.1)); + } + } + : null), + IconButton( + icon: Icon(Icons.add), + onPressed: selectedNode != null + ? () { + if (nodesMap.containsKey(selectedNode)) { + final node = nodesMap[selectedNode]; + node.changeRotation(node.rotation + + vector.Vector4(0.1, 0.1, 0.1, 0.1)); + setState(() {}); + } + } + : null), + if (selectedNode != null && + nodesMap.containsKey(selectedNode)) ...[ + Checkbox( + value: + nodesMap[selectedNode].rotationGestureEnabled, + onChanged: (value) { + nodesMap[selectedNode].rotationGestureEnabled = + value; + setState(() {}); + }, + ), + Text(nodesMap[selectedNode].rotation.text), + ] + ], + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + void _onArCoreViewCreated(ArCoreController controller) { + arCoreController = controller; + arCoreController.onPlaneTap = _handleOnPlaneTap; + arCoreController.onNodeTap = (node) { + print('TransformableNodeScreen: onNodeTap $node'); + setState(() { + selectedNode = node; + }); + }; + } + + Future _addSphere(ArCoreHitTestResult hit) async { + final ByteData textureBytes = await rootBundle.load('assets/earth.jpg'); + + final earthMaterial = ArCoreMaterial( + color: Color.fromARGB(120, 66, 134, 244), + textureBytes: textureBytes.buffer.asUint8List()); + + final earthShape = ArCoreSphere( + materials: [earthMaterial], + radius: 0.1, + ); + + final earth = ArCoreNode( + shape: earthShape, + scaleControllerNode: ScaleControllerNode( + scale: vector.Vector3(1.0, 1.0, 1.0), + ), + translationControllerNode: TranslationControllerNode( + position: hit.pose.translation + vector.Vector3(0.0, 1.0, 0.0), + ), + rotationControllerNode: RotationControllerNode( + rotation: hit.pose.rotation, + ), + ); + + nodesMap[earth.name] = earth; + + arCoreController.addArCoreNodeWithAnchor(earth); + } + + void _handleOnPlaneTap(List hits) { + final hit = hits.first; + _addSphere(hit); + } + + void onTapHandler(String name) { + print("Flutter: onNodeTap"); + showDialog( + context: context, + builder: (BuildContext context) => + AlertDialog(content: Text('onNodeTap on $name')), + ); + } + + @override + void dispose() { + arCoreController.dispose(); + super.dispose(); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock index f71733c9..57f8a9f8 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -64,13 +64,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.3" equatable: dependency: transitive description: @@ -206,5 +199,5 @@ packages: source: hosted version: "3.5.0" sdks: - dart: ">=2.4.0 <3.0.0" + dart: ">=2.7.0 <3.0.0" flutter: ">=1.10.0 <2.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 4420f974..a20f89e6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -3,16 +3,12 @@ description: Demonstrates how to use the arcore_flutter_plugin plugin. publish_to: 'none' environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.7.0 <3.0.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - dev_dependencies: flutter_test: sdk: flutter diff --git a/lib/arcore_flutter_plugin.dart b/lib/arcore_flutter_plugin.dart index 880de1b9..cd3b37f2 100644 --- a/lib/arcore_flutter_plugin.dart +++ b/lib/arcore_flutter_plugin.dart @@ -13,3 +13,4 @@ export 'package:arcore_flutter_plugin/src/arcore_plane.dart'; export 'package:arcore_flutter_plugin/src/arcore_image.dart'; export 'package:arcore_flutter_plugin/src/arcore_reference_node.dart'; export 'package:arcore_flutter_plugin/src/arcore_augmented_image.dart'; +export 'package:arcore_flutter_plugin/src/utils/vector_utils.dart'; diff --git a/lib/src/arcore_controller.dart b/lib/src/arcore_controller.dart index 2fd63952..8d83794a 100644 --- a/lib/src/arcore_controller.dart +++ b/lib/src/arcore_controller.dart @@ -141,7 +141,7 @@ class ArCoreController { return _channel.invokeMethod('getTrackingState'); } - addArCoreNodeToAugmentedImage(ArCoreNode node, int index, + Future addArCoreNodeToAugmentedImage(ArCoreNode node, int index, {String parentNodeName}) { assert(node != null); @@ -177,25 +177,71 @@ class ArCoreController { } void _addListeners(ArCoreNode node) { - node.position.addListener(() => _handlePositionChanged(node)); + node.translationControllerNode + .addListener(() => _handlePositionConfigChanged(node)); + node.scaleControllerNode.addListener(() => _handleScaleConfigChanged(node)); + node.rotationControllerNode + .addListener(() => _handleRotationConfigChanged(node)); node?.shape?.materials?.addListener(() => _updateMaterials(node)); - if (node is ArCoreRotatingNode) { - node.degreesPerSecond.addListener(() => _handleRotationChanged(node)); - } + // if (node is ArCoreRotatingNode) { + // node.degreesPerSecond.addListener(() => _handleRotationChanged(node)); + // } + } + +/* void _handleScaleChanged(ArCoreNode node) { + print('_handleScaleChanged: ${node.name}'); + _channel.invokeMethod( + 'scaleChanged', + _getHandlerParams( + node, + _getHandlerParams(node, { + 'scale': convertVector3ToMap(node.scale.value) + }))); } void _handlePositionChanged(ArCoreNode node) { - _channel.invokeMethod('positionChanged', - _getHandlerParams(node, convertVector3ToMap(node.position.value))); + print('_handlePositionChanged: ${node.name}'); + _channel.invokeMethod( + 'positionChanged', + _getHandlerParams( + node, + _getHandlerParams(node, { + 'position': convertVector3ToMap(node.position.value) + }))); + } + + void _handleRotationChanged(ArCoreNode node) { + print('_handleRotationChanged: ${node.name}'); + _channel.invokeMethod( + 'rotationChanged', + _getHandlerParams(node, { + 'rotation': convertVector4ToMap(node.rotation.value) + })); + }*/ + + void _handleRotationConfigChanged(ArCoreNode node) { + print('_handleRotationGestureChanged: ${node.name}'); + _channel.invokeMethod('rotationConfigChanged', + _getHandlerParams(node, node.rotationControllerNode?.value?.toMap())); } - void _handleRotationChanged(ArCoreRotatingNode node) { - _channel.invokeMethod('rotationChanged', - {'name': node.name, 'degreesPerSecond': node.degreesPerSecond.value}); + void _handleScaleConfigChanged(ArCoreNode node) { + print('_handleScaleConfigChanged: ${node.name}'); + _channel.invokeMethod('scaleConfigChanged', + _getHandlerParams(node, node.scaleControllerNode?.value?.toMap())); + } + + void _handlePositionConfigChanged(ArCoreNode node) { + print('_handlePositionConfigChanged: ${node.name}'); + _channel.invokeMethod( + 'positionConfigChanged', + _getHandlerParams( + node, node.translationControllerNode?.value?.toMap())); } void _updateMaterials(ArCoreNode node) { + print('_updateMaterials: ${node.name}'); _channel.invokeMethod( 'updateMaterials', _getHandlerParams(node, node.shape.toMap())); } @@ -204,6 +250,7 @@ class ArCoreController { ArCoreNode node, Map params) { final Map values = {'name': node.name} ..addAll(params); + values.removeWhere((k, v) => v == null); return values; } diff --git a/lib/src/arcore_node.dart b/lib/src/arcore_node.dart index 5688647e..17d202f4 100644 --- a/lib/src/arcore_node.dart +++ b/lib/src/arcore_node.dart @@ -1,5 +1,6 @@ import 'package:arcore_flutter_plugin/src/arcore_image.dart'; import 'package:arcore_flutter_plugin/src/utils/vector_utils.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart'; import 'package:arcore_flutter_plugin/src/utils/random_string.dart' @@ -8,42 +9,213 @@ import 'package:arcore_flutter_plugin/src/shape/arcore_shape.dart'; class ArCoreNode { ArCoreNode({ - this.shape, - this.image, + ScaleControllerNode scaleControllerNode, + TranslationControllerNode translationControllerNode, + RotationControllerNode rotationControllerNode, String name, Vector3 position, Vector3 scale, Vector4 rotation, + this.shape, + this.image, this.children = const [], - }) : name = name ?? random_string.randomString(), - position = ValueNotifier(position), - scale = ValueNotifier(scale), - rotation = ValueNotifier(rotation), - assert(!(shape != null && image != null)); + }) : name = name ?? 'node_${random_string.randomString(length: 6)}', + scaleControllerNode = ValueNotifier( + scaleControllerNode ?? ScaleControllerNode(scale: scale)), + translationControllerNode = ValueNotifier(translationControllerNode ?? + TranslationControllerNode(position: position)), + rotationControllerNode = ValueNotifier(rotationControllerNode ?? + RotationControllerNode(rotation: rotation)), + assert(!(shape != null && image != null)), + assert(!(scaleControllerNode != null && scale != null)), + assert(!(translationControllerNode != null && position != null)), + assert(!(rotationControllerNode != null && rotation != null)); final List children; final ArCoreShape shape; - final ValueNotifier position; + final String name; + + final ArCoreImage image; - final ValueNotifier scale; + final ValueNotifier scaleControllerNode; - final ValueNotifier rotation; + final ValueNotifier translationControllerNode; - final String name; + final ValueNotifier rotationControllerNode; - final ArCoreImage image; + Vector3 get position => translationControllerNode.value.position; + bool get positionGestureEnabled => translationControllerNode.value.enabled; + set positionGestureEnabled(bool value) { + translationControllerNode.value = + translationControllerNode.value.copyWith(enabled: value); + } + + Vector4 get rotation => rotationControllerNode.value.rotation; + bool get rotationGestureEnabled => rotationControllerNode.value.enabled; + set rotationGestureEnabled(bool value) { + rotationControllerNode.value = + rotationControllerNode.value.copyWith(enabled: value); + } + + Vector3 get scale => scaleControllerNode.value.scale; + bool get scaleGestureEnabled => scaleControllerNode.value.enabled; + set scaleGestureEnabled(bool value) { + scaleControllerNode.value = + scaleControllerNode.value.copyWith(enabled: value); + } + + void changeRotation(Vector4 newRotation) { + rotationControllerNode.value = + rotationControllerNode.value.copyWith(rotation: newRotation); + } + + void changePosition(Vector3 newPosition) { + translationControllerNode.value = + translationControllerNode.value.copyWith(position: newPosition); + } + + void changeScale(Vector3 newScale) { + scaleControllerNode.value = + scaleControllerNode.value.copyWith(scale: newScale); + } + + // factory ArCoreNode.fromMap(Map map) { + // return ArCoreNode( + // name: map['name'], + // position: map['position'], + // scale: map['scale'], + // rotation: map['rotation'], + // ); + // } Map toMap() => { 'dartType': runtimeType.toString(), 'shape': shape?.toMap(), - 'position': convertVector3ToMap(position.value), - 'scale': convertVector3ToMap(scale.value), - 'rotation': convertVector4ToMap(rotation.value), + 'scaleControllerNode': scaleControllerNode?.value?.toMap(), + 'translationControllerNode': translationControllerNode?.value?.toMap(), + 'rotationControllerNode': rotationControllerNode?.value?.toMap(), 'name': name, 'image': image?.toMap(), 'children': this.children.map((arCoreNode) => arCoreNode.toMap()).toList(), }..removeWhere((String k, dynamic v) => v == null); } + +class ScaleControllerNode extends Equatable { + final Vector3 scale; + final double minScale; + final bool enabled; + final double maxScale; + + ScaleControllerNode({ + @required this.scale, + this.minScale = 0.25, + this.maxScale = 5.0, + this.enabled = true, + }); + + Map toMap() { + return { + 'scale': convertVector3ToMap(this.scale), + 'minScale': this.minScale, + 'enabled': this.enabled, + 'maxScale': this.maxScale, + }; + } + + ScaleControllerNode copyWith({ + Vector3 scale, + double minScale, + bool enabled, + double maxScale, + }) { + if ((scale == null || identical(scale, this.scale)) && + (minScale == null || identical(minScale, this.minScale)) && + (enabled == null || identical(enabled, this.enabled)) && + (maxScale == null || identical(maxScale, this.maxScale))) { + return this; + } + + return new ScaleControllerNode( + scale: scale ?? this.scale, + minScale: minScale ?? this.minScale, + enabled: enabled ?? this.enabled, + maxScale: maxScale ?? this.maxScale, + ); + } + + @override + List get props => [scale, minScale, maxScale, enabled]; +} + +class TranslationControllerNode extends Equatable { + final Vector3 position; + final bool enabled; + + TranslationControllerNode({ + @required this.position, + this.enabled = true, + }); + + Map toMap() { + return { + 'position': convertVector3ToMap(this.position), + 'enabled': this.enabled, + }; + } + + TranslationControllerNode copyWith({ + Vector3 position, + bool enabled, + }) { + if ((position == null || identical(position, this.position)) && + (enabled == null || identical(enabled, this.enabled))) { + return this; + } + + return new TranslationControllerNode( + position: position ?? this.position, + enabled: enabled ?? this.enabled, + ); + } + + @override + List get props => [position, enabled]; +} + +class RotationControllerNode extends Equatable { + final Vector4 rotation; + final bool enabled; + + RotationControllerNode({ + @required this.rotation, + this.enabled = true, + }); + + Map toMap() { + return { + 'rotation': convertVector4ToMap(this.rotation), + 'enabled': this.enabled, + }; + } + + RotationControllerNode copyWith({ + Vector4 rotation, + bool enabled, + }) { + if ((rotation == null || identical(rotation, this.rotation)) && + (enabled == null || identical(enabled, this.enabled))) { + return this; + } + + return new RotationControllerNode( + rotation: rotation ?? this.rotation, + enabled: enabled ?? this.enabled, + ); + } + + @override + List get props => [rotation, enabled]; +} diff --git a/lib/src/arcore_reference_node.dart b/lib/src/arcore_reference_node.dart index d90fc35e..1c323f48 100644 --- a/lib/src/arcore_reference_node.dart +++ b/lib/src/arcore_reference_node.dart @@ -1,3 +1,4 @@ +import 'package:flutter/widgets.dart'; import 'package:vector_math/vector_math_64.dart'; import 'arcore_node.dart'; @@ -11,6 +12,9 @@ class ArCoreReferenceNode extends ArCoreNode { final String objectUrl; ArCoreReferenceNode({ + ScaleControllerNode scaleControllerNode, + TranslationControllerNode translationControllerNode, + RotationControllerNode rotationControllerNode, String name, this.object3DFileName, this.objectUrl, @@ -23,7 +27,10 @@ class ArCoreReferenceNode extends ArCoreNode { children: children, position: position, scale: scale, - rotation: rotation); + rotation: rotation, + scaleControllerNode: scaleControllerNode, + translationControllerNode: translationControllerNode, + rotationControllerNode: rotationControllerNode); @override Map toMap() => { diff --git a/lib/src/utils/vector_utils.dart b/lib/src/utils/vector_utils.dart index 6784e6cb..44c415db 100755 --- a/lib/src/utils/vector_utils.dart +++ b/lib/src/utils/vector_utils.dart @@ -6,3 +6,13 @@ Map convertVector3ToMap(Vector3 vector) => Map convertVector4ToMap(Vector4 vector) => vector == null ? null : {'x': vector.x, 'y': vector.y, 'z': vector.z, 'w': vector.w}; + +extension Vector3Ext on Vector3 { + String get text => + 'x: ${this.x.toStringAsFixed(2)} y: ${this.y.toStringAsFixed(2)} z: ${this.z.toStringAsFixed(2)}'; +} + +extension Vector4Ext on Vector4 { + String get text => + 'x: ${this.x.toStringAsFixed(2)} y: ${this.y.toStringAsFixed(2)} z: ${this.z.toStringAsFixed(2)} t: ${this.t.toStringAsFixed(2)}'; +} diff --git a/pubspec.yaml b/pubspec.yaml index f685bb09..d002ea01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.9 homepage: https://github.com/giandifra/arcore_flutter_plugin environment: - sdk: '>=2.1.0 <3.0.0' + sdk: '>=2.7.0 <3.0.0' flutter: '>=1.10.0 <2.0.0' dependencies: