diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 7197a723f..352562e72 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -29,8 +29,10 @@ diff --git a/arsceneview/src/main/java/io/github/sceneview/ar/ARSceneView.kt b/arsceneview/src/main/java/io/github/sceneview/ar/ARSceneView.kt index 94a9d42fc..b0aa7de9b 100644 --- a/arsceneview/src/main/java/io/github/sceneview/ar/ARSceneView.kt +++ b/arsceneview/src/main/java/io/github/sceneview/ar/ARSceneView.kt @@ -239,28 +239,28 @@ open class ARSceneView @JvmOverloads constructor( */ var onSessionUpdated: ((session: Session, frame: Frame) -> Unit)? = null, ) : SceneView( - context, - attrs, - defStyleAttr, - defStyleRes, - sharedEngine, - sharedModelLoader, - sharedMaterialLoader, - sharedEnvironmentLoader, - sharedScene, - sharedView, - sharedRenderer, - sharedCameraNode, - sharedMainLightNode, - sharedEnvironment, - isOpaque, - sharedCollisionSystem, - null, - viewNodeWindowManager, - onGestureListener, - onTouchEvent, - sharedActivity, - sharedLifecycle + context = context, + attrs = attrs, + defStyleAttr = defStyleAttr, + defStyleRes = defStyleRes, + sharedEngine = sharedEngine, + sharedModelLoader = sharedModelLoader, + sharedMaterialLoader = sharedMaterialLoader, + sharedEnvironmentLoader = sharedEnvironmentLoader, + sharedScene = sharedScene, + sharedView = sharedView, + sharedRenderer = sharedRenderer, + sharedCameraNode = sharedCameraNode, + sharedMainLightNode = sharedMainLightNode, + sharedEnvironment = sharedEnvironment, + isOpaque = isOpaque, + sharedCollisionSystem = sharedCollisionSystem, + cameraManipulator = null, + viewNodeWindowManager = viewNodeWindowManager, + onGestureListener = onGestureListener, + onTouchEvent = onTouchEvent, + sharedActivity = sharedActivity, + sharedLifecycle = sharedLifecycle ) { open val arCore = ARCore( onSessionCreated = ::onSessionCreated, diff --git a/samples/model-viewer-compose-camera-manipulator/.gitignore b/samples/model-viewer-compose-camera-manipulator/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/samples/model-viewer-compose-camera-manipulator/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/samples/model-viewer-compose-camera-manipulator/build.gradle b/samples/model-viewer-compose-camera-manipulator/build.gradle new file mode 100644 index 000000000..afd188f5d --- /dev/null +++ b/samples/model-viewer-compose-camera-manipulator/build.gradle @@ -0,0 +1,55 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + namespace 'io.github.sceneview.sample.modelviewer.compose.cameramanipulator' + + compileSdk 34 + + defaultConfig { + applicationId "io.github.sceneview.sample.modelviewer.compose.cameramanipulator" + minSdk 28 + targetSdk 34 + versionCode 1 + versionName "1.0.0" + } + + buildTypes { + release { + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.5.14' + } + androidResources { + noCompress 'filamat', 'ktx' + } +} + +dependencies { + implementation project(":samples:common") + + implementation "androidx.compose.ui:ui:1.6.7" + implementation "androidx.compose.foundation:foundation:1.6.7" + implementation 'androidx.activity:activity-compose:1.9.0' + implementation 'androidx.compose.material:material:1.6.7' + implementation "androidx.compose.ui:ui-tooling-preview:1.6.7" + implementation "androidx.navigation:navigation-compose:2.7.7" + debugImplementation "androidx.compose.ui:ui-tooling:1.6.7" + + // SceneView + releaseImplementation "io.github.sceneview:sceneview:2.2.1" + debugImplementation project(":sceneview") +} \ No newline at end of file diff --git a/samples/model-viewer-compose-camera-manipulator/damaged_helmet.glb b/samples/model-viewer-compose-camera-manipulator/damaged_helmet.glb new file mode 100644 index 000000000..2cee76d76 Binary files /dev/null and b/samples/model-viewer-compose-camera-manipulator/damaged_helmet.glb differ diff --git a/samples/model-viewer-compose-camera-manipulator/proguard-rules.pro b/samples/model-viewer-compose-camera-manipulator/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/samples/model-viewer-compose-camera-manipulator/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/samples/model-viewer-compose-camera-manipulator/src/main/AndroidManifest.xml b/samples/model-viewer-compose-camera-manipulator/src/main/AndroidManifest.xml new file mode 100644 index 000000000..31f0168dc --- /dev/null +++ b/samples/model-viewer-compose-camera-manipulator/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/model-viewer-compose-camera-manipulator/src/main/assets/environments/sky_2k.hdr b/samples/model-viewer-compose-camera-manipulator/src/main/assets/environments/sky_2k.hdr new file mode 100644 index 000000000..31843fc6a Binary files /dev/null and b/samples/model-viewer-compose-camera-manipulator/src/main/assets/environments/sky_2k.hdr differ diff --git a/samples/model-viewer-compose-camera-manipulator/src/main/assets/models/damaged_helmet.glb b/samples/model-viewer-compose-camera-manipulator/src/main/assets/models/damaged_helmet.glb new file mode 100644 index 000000000..2cee76d76 Binary files /dev/null and b/samples/model-viewer-compose-camera-manipulator/src/main/assets/models/damaged_helmet.glb differ diff --git a/samples/model-viewer-compose-camera-manipulator/src/main/java/io/github/sceneview/sample/modelviewer/compose/cameramanipulator/MainActivity.kt b/samples/model-viewer-compose-camera-manipulator/src/main/java/io/github/sceneview/sample/modelviewer/compose/cameramanipulator/MainActivity.kt new file mode 100644 index 000000000..b95f9b888 --- /dev/null +++ b/samples/model-viewer-compose-camera-manipulator/src/main/java/io/github/sceneview/sample/modelviewer/compose/cameramanipulator/MainActivity.kt @@ -0,0 +1,187 @@ +package io.github.sceneview.sample.modelviewer.compose.cameramanipulator + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.github.sceneview.Scene +import io.github.sceneview.collision.CollisionSystem +import io.github.sceneview.gesture.CameraGestureDetector +import io.github.sceneview.math.Position +import io.github.sceneview.math.toVector3 +import io.github.sceneview.node.CameraNode +import io.github.sceneview.node.ModelNode +import io.github.sceneview.rememberCameraManipulator +import io.github.sceneview.rememberCameraNode +import io.github.sceneview.rememberCollisionSystem +import io.github.sceneview.rememberEngine +import io.github.sceneview.rememberEnvironmentLoader +import io.github.sceneview.rememberModelLoader +import io.github.sceneview.rememberNode +import io.github.sceneview.rememberOnGestureListener +import io.github.sceneview.rememberView +import io.github.sceneview.sample.SceneviewTheme +import kotlin.math.sign + +class MainActivity : ComponentActivity() { + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + SceneviewTheme { + Box(modifier = Modifier.fillMaxSize()) { + val engine = rememberEngine() + val modelLoader = rememberModelLoader(engine) + val environmentLoader = rememberEnvironmentLoader(engine) + val view = rememberView(engine) + val collisionSystem = rememberCollisionSystem(view) + + val centerNode = rememberNode(engine) + + val cameraNode = rememberCameraNode(engine) { + position = Position(y = -0.5f, z = 2.0f) + lookAt(centerNode) + centerNode.addChildNode(this) + } + + val cameraManipulator = rememberCameraManipulator( + creator = { + AdvancedCameraManipulator( + cameraNode = cameraNode, + collisionSystem = collisionSystem, + orbitHomePosition = cameraNode.worldPosition, + targetPosition = centerNode.worldPosition + ) + } + ) + + Scene( + modifier = Modifier.fillMaxSize(), + engine = engine, + modelLoader = modelLoader, + view = view, + cameraNode = cameraNode, + cameraManipulator = cameraManipulator, + childNodes = listOf(centerNode, + rememberNode { + ModelNode( + modelInstance = modelLoader.createModelInstance( + assetFileLocation = "models/damaged_helmet.glb" + ), + scaleToUnits = 0.25f + ) + }), + collisionSystem = collisionSystem, + environment = environmentLoader.createHDREnvironment( + assetFileLocation = "environments/sky_2k.hdr" + )!!, + onGestureListener = rememberOnGestureListener( + onDoubleTap = { _, node -> + node?.apply { + scale *= 2.0f + } + } + ) + ) + Image( + modifier = Modifier + .width(192.dp) + .align(Alignment.BottomEnd) + .navigationBarsPadding() + .padding(16.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer.copy( + alpha = 0.5f + ), + shape = MaterialTheme.shapes.medium + ) + .padding(8.dp), + painter = painterResource(id = R.drawable.logo), + contentDescription = "Logo" + ) + TopAppBar( + title = { + Text( + text = stringResource(id = R.string.app_name) + ) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25f), + titleContentColor = MaterialTheme.colorScheme.onPrimary + + ) + ) + } + } + } + } +} + +/** + * Override default camera manipulator to achieve relative zooming based on distance from camera to + * target and separation between two fingers. + * + * Target is calculated as closest node to center of finger's position in direction of camera. + */ +class AdvancedCameraManipulator( + private val cameraNode: CameraNode, + private val collisionSystem: CollisionSystem, + orbitHomePosition: Position? = null, + targetPosition: Position? = null +): CameraGestureDetector.DefaultCameraManipulator( + orbitHomePosition = orbitHomePosition, + targetPosition = targetPosition +) { + private var scrollBeginCameraPosition = Position() + private var scrollBeginDistance: Float? = 0f + private var scrollBeginSeparation = 0f + + override fun scrollBegin(x: Int, y: Int, separation: Float) { + val hitResults = collisionSystem.hitTest(x.toFloat(), y.toFloat()) + scrollBeginDistance = hitResults.firstOrNull()?.node?.position?.let { + (cameraNode.position - it).toVector3().length() + } + scrollBeginCameraPosition = cameraNode.position + scrollBeginSeparation = separation + } + + override fun scrollUpdate(x: Int, y: Int, prevSeparation: Float, currSeparation: Float) { + val beginDistance = scrollBeginDistance + if (beginDistance == null) { + super.scrollUpdate(x, y, prevSeparation, currSeparation) + return + } + + val movedVector = (cameraNode.position - scrollBeginCameraPosition).toVector3() + val movedDirection = listOf( + movedVector.x.sign, + movedVector.y.sign, + movedVector.z.sign, + ).firstOrNull { it != 0f }?.sign ?: 1f + + val ratio = scrollBeginSeparation / currSeparation + val moved = movedVector.length() * movedDirection + val target = (beginDistance * ratio) + val adjust = target - (beginDistance - moved) + + manipulator.scroll(x, y, adjust) + } +} diff --git a/samples/model-viewer-compose-camera-manipulator/src/main/res/values/strings.xml b/samples/model-viewer-compose-camera-manipulator/src/main/res/values/strings.xml new file mode 100644 index 000000000..a1aeb96e7 --- /dev/null +++ b/samples/model-viewer-compose-camera-manipulator/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Model Viewer Compose Advanced Camera Manipulator + \ No newline at end of file diff --git a/sceneview/src/main/java/io/github/sceneview/Scene.kt b/sceneview/src/main/java/io/github/sceneview/Scene.kt index 33f927309..c3486d24a 100644 --- a/sceneview/src/main/java/io/github/sceneview/Scene.kt +++ b/sceneview/src/main/java/io/github/sceneview/Scene.kt @@ -32,6 +32,7 @@ import dev.romainguy.kotlin.math.Float2 import io.github.sceneview.collision.CollisionSystem import io.github.sceneview.collision.HitResult import io.github.sceneview.environment.Environment +import io.github.sceneview.gesture.CameraGestureDetector import io.github.sceneview.gesture.GestureDetector import io.github.sceneview.gesture.MoveGestureDetector import io.github.sceneview.gesture.RotateGestureDetector @@ -154,11 +155,9 @@ fun Scene( * position before any user gesture. * * Clients notify the camera manipulator of various mouse or touch events, then periodically - * call its getLookAt() method so that they can adjust their camera(s). Three modes are - * supported: ORBIT, MAP, and FREE_FLIGHT. To construct a manipulator instance, the desired mode - * is passed into the create method. + * call its getTransform() method so that they can adjust their camera(s). */ - cameraManipulator: Manipulator? = rememberCameraManipulator( + cameraManipulator: CameraGestureDetector.CameraManipulator? = rememberCameraManipulator( cameraNode.worldPosition ), /** @@ -256,6 +255,66 @@ fun Scene( } } +/** ## Deprecated: Use [CameraGestureDetector.DefaultCameraManipulator] + * + * Replace `manipulator = Manipulator.Builder().build()` with + * `cameraManipulator = CameraGestureDetector.DefaultCameraManipulator(manipulator = + * Manipulator.Builder().build())` + */ +@Composable +@Deprecated("Use CameraGestureDetector.DefaultCameraManipulator") +fun Scene( + modifier: Modifier = Modifier, + engine: Engine = rememberEngine(), + modelLoader: ModelLoader = rememberModelLoader(engine), + materialLoader: MaterialLoader = rememberMaterialLoader(engine), + environmentLoader: EnvironmentLoader = rememberEnvironmentLoader(engine), + view: View = rememberView(engine), + isOpaque: Boolean = true, + renderer: Renderer = rememberRenderer(engine), + scene: Scene = rememberScene(engine), + environment: Environment = rememberEnvironment(environmentLoader, isOpaque = isOpaque), + mainLightNode: LightNode? = rememberMainLightNode(engine), + cameraNode: CameraNode = rememberCameraNode(engine), + childNodes: List = rememberNodes(), + collisionSystem: CollisionSystem = rememberCollisionSystem(view), + manipulator: Manipulator, + viewNodeWindowManager: ViewNode2.WindowManager? = null, + onGestureListener: GestureDetector.OnGestureListener? = rememberOnGestureListener(), + onTouchEvent: ((e: MotionEvent, hitResult: HitResult?) -> Boolean)? = null, + activity: ComponentActivity? = LocalContext.current as? ComponentActivity, + lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle, + onFrame: ((frameTimeNanos: Long) -> Unit)? = null, + onViewCreated: (SceneView.() -> Unit)? = null, + onViewUpdated: (SceneView.() -> Unit)? = null +) { + Scene( + modifier = modifier, + engine = engine, + modelLoader = modelLoader, + materialLoader = materialLoader, + environmentLoader = environmentLoader, + view = view, + isOpaque = isOpaque, + renderer = renderer, + scene = scene, + environment = environment, + mainLightNode = mainLightNode, + cameraNode = cameraNode, + childNodes = childNodes, + collisionSystem = collisionSystem, + cameraManipulator = CameraGestureDetector.createDefaultCameraManipulator(manipulator), + viewNodeWindowManager = viewNodeWindowManager, + onGestureListener = onGestureListener, + onTouchEvent = onTouchEvent, + activity = activity, + lifecycle = lifecycle, + onFrame = onFrame, + onViewCreated = onViewCreated, + onViewUpdated = onViewUpdated + ) +} + @Composable fun rememberEngine( eglContextCreator: () -> EGLContext = { SceneView.createEglContext() }, @@ -515,7 +574,7 @@ fun rememberOnGestureListener( fun rememberCameraManipulator( orbitHomePosition: Position? = null, targetPosition: Position? = null, - creator: () -> Manipulator = { + creator: () -> CameraGestureDetector.CameraManipulator = { SceneView.createDefaultCameraManipulator(orbitHomePosition, targetPosition) } ) = remember(creator).also { collisionSystem -> diff --git a/sceneview/src/main/java/io/github/sceneview/SceneView.kt b/sceneview/src/main/java/io/github/sceneview/SceneView.kt index fea94e6f2..0cd3497e8 100644 --- a/sceneview/src/main/java/io/github/sceneview/SceneView.kt +++ b/sceneview/src/main/java/io/github/sceneview/SceneView.kt @@ -54,9 +54,6 @@ import io.github.sceneview.gesture.GestureDetector import io.github.sceneview.gesture.MoveGestureDetector import io.github.sceneview.gesture.RotateGestureDetector import io.github.sceneview.gesture.ScaleGestureDetector -import io.github.sceneview.gesture.orbitHomePosition -import io.github.sceneview.gesture.targetPosition -import io.github.sceneview.gesture.transform import io.github.sceneview.loaders.EnvironmentLoader import io.github.sceneview.loaders.MaterialLoader import io.github.sceneview.loaders.ModelLoader @@ -198,7 +195,8 @@ open class SceneView @JvmOverloads constructor( * supported: ORBIT, MAP, and FREE_FLIGHT. To construct a manipulator instance, the desired mode * is passed into the create method. */ - cameraManipulator: Manipulator? = createDefaultCameraManipulator(sharedCameraNode?.worldPosition), + cameraManipulator: CameraGestureDetector.CameraManipulator? = + createDefaultCameraManipulator(sharedCameraNode?.worldPosition), /** * Used for Node's that can display an Android [View] * @@ -226,6 +224,61 @@ open class SceneView @JvmOverloads constructor( sharedLifecycle: Lifecycle? = null, ) : SurfaceView(context, attrs, defStyleAttr, defStyleRes) { + /** ## Deprecated: Use [CameraGestureDetector.DefaultCameraManipulator] + * + * Replace `manipulator = Manipulator.Builder().build()` with + * `cameraManipulator = CameraGestureDetector.DefaultCameraManipulator(manipulator = + * Manipulator.Builder().build())` + */ + @Deprecated("Use CameraGestureDetector.DefaultCameraManipulator") + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, + sharedEngine: Engine? = null, + sharedModelLoader: ModelLoader? = null, + sharedMaterialLoader: MaterialLoader? = null, + sharedEnvironmentLoader: EnvironmentLoader? = null, + sharedScene: Scene? = null, + sharedView: View? = null, + sharedRenderer: Renderer? = null, + sharedCameraNode: CameraNode? = null, + sharedMainLightNode: LightNode? = null, + sharedEnvironment: Environment? = null, + isOpaque: Boolean = true, + sharedCollisionSystem: CollisionSystem? = null, + manipulator: Manipulator, + viewNodeWindowManager: ViewNode2.WindowManager? = null, + onGestureListener: GestureDetector.OnGestureListener? = null, + onTouchEvent: ((e: MotionEvent, hitResult: HitResult?) -> Boolean)? = null, + sharedActivity: ComponentActivity? = null, + sharedLifecycle: Lifecycle? = null, + ): this ( + context = context, + attrs = attrs, + defStyleAttr = defStyleAttr, + defStyleRes = defStyleRes, + sharedEngine = sharedEngine, + sharedModelLoader = sharedModelLoader, + sharedMaterialLoader = sharedMaterialLoader, + sharedEnvironmentLoader = sharedEnvironmentLoader, + sharedScene = sharedScene, + sharedView = sharedView, + sharedRenderer = sharedRenderer, + sharedCameraNode = sharedCameraNode, + sharedMainLightNode = sharedMainLightNode, + sharedEnvironment = sharedEnvironment, + isOpaque = isOpaque, + sharedCollisionSystem = sharedCollisionSystem, + cameraManipulator = CameraGestureDetector.createDefaultCameraManipulator(manipulator), + viewNodeWindowManager = viewNodeWindowManager, + onGestureListener = onGestureListener, + onTouchEvent = onTouchEvent, + sharedActivity = sharedActivity, + sharedLifecycle = sharedLifecycle, + ) + val engine = sharedEngine ?: createEglContext().let { defaultEglContext = it createEngine(it).also { defaultEngine = it } @@ -447,7 +500,7 @@ open class SceneView @JvmOverloads constructor( } var cameraGestureDetector: CameraGestureDetector? = - CameraGestureDetector(viewHeight = ::getHeight, manipulator = cameraManipulator) + CameraGestureDetector(viewHeight = ::getHeight, cameraManipulator = cameraManipulator) private set /** @@ -464,10 +517,10 @@ open class SceneView @JvmOverloads constructor( * supported: ORBIT, MAP, and FREE_FLIGHT. To construct a manipulator instance, the desired mode * is passed into the create method. */ - var cameraManipulator: Manipulator? - get() = cameraGestureDetector?.manipulator + var cameraManipulator: CameraGestureDetector.CameraManipulator? + get() = cameraGestureDetector?.cameraManipulator set(value) { - cameraGestureDetector?.manipulator = value + cameraGestureDetector?.cameraManipulator = value } protected open val activity: ComponentActivity? = sharedActivity @@ -676,7 +729,7 @@ open class SceneView @JvmOverloads constructor( if (lastTouchEvent != null) { manipulator.update(frameTimeNanos.intervalSeconds(lastFrameTimeNanos).toFloat()) // Extract the camera basis from the helper and push it to the Filament camera. - cameraNode.transform = manipulator.transform + cameraNode.transform = manipulator.getTransform() } } @@ -997,15 +1050,10 @@ open class SceneView @JvmOverloads constructor( fun createDefaultCameraManipulator( orbitHomePosition: Position? = null, targetPosition: Position? = null - ) = Manipulator.Builder() - .apply { - orbitHomePosition?.let { orbitHomePosition(it) } - targetPosition?.let { targetPosition(it) } - } -// .viewport(min(width, 1), min(height, 1)) - .orbitSpeed(0.005f, 0.005f) - .zoomSpeed(0.05f) - .build(Manipulator.Mode.ORBIT) + ) = CameraGestureDetector.DefaultCameraManipulator( + orbitHomePosition = orbitHomePosition, + targetPosition = targetPosition + ) @RequiresApi(Build.VERSION_CODES.P) diff --git a/sceneview/src/main/java/io/github/sceneview/gesture/CameraGestureDetector.kt b/sceneview/src/main/java/io/github/sceneview/gesture/CameraGestureDetector.kt index 784fdca05..884c6aab1 100644 --- a/sceneview/src/main/java/io/github/sceneview/gesture/CameraGestureDetector.kt +++ b/sceneview/src/main/java/io/github/sceneview/gesture/CameraGestureDetector.kt @@ -5,9 +5,10 @@ import com.google.android.filament.utils.Float2 import com.google.android.filament.utils.Manipulator import com.google.android.filament.utils.distance import com.google.android.filament.utils.mix +import io.github.sceneview.math.Position +import io.github.sceneview.math.Transform /** - * * Pan fixed version of the mostly duplicated com.google.android.filament.utils.GestureDetector * * Responds to Android touch events and manages a camera manipulator. @@ -16,7 +17,100 @@ import com.google.android.filament.utils.mix * Copied from * filament-utils-android/src/main/java/com/google/android/filament/utils/GestureDetector.kt */ -open class CameraGestureDetector(private val viewHeight: () -> Int, var manipulator: Manipulator?) { +open class CameraGestureDetector( + private val viewHeight: () -> Int, + var cameraManipulator: CameraManipulator?, +) { + /** + * ## Deprecated: Use CameraGestureDetector.CameraManipulator + * + * Replace `manipulator = Manipulator.Builder().build()` with + * `cameraManipulator = CameraGestureDetector.DefaultCameraManipulator(manipulator = + * Manipulator.Builder().build())` + */ + @Deprecated("Use CameraGestureDetector.CameraManipulator") + constructor( + viewHeight: () -> Int, + manipulator: Manipulator? + ): this( + viewHeight, + createDefaultCameraManipulator(manipulator) + ) + + interface CameraManipulator { + fun setViewport(width: Int, height: Int) + fun getTransform(): Transform + fun grabBegin(x: Int, y: Int, strafe: Boolean) + fun grabUpdate(x: Int, y: Int) + fun grabEnd() + fun scrollBegin(x: Int, y: Int, separation: Float) + fun scrollUpdate(x: Int, y: Int, prevSeparation: Float, currSeparation: Float) + fun scrollEnd() + fun update(deltaTime: Float) + } + + /** + * The first onTouch event will make the first manipulator build. So you can change the camera + * position before any user gesture. + * + * Clients notify the camera manipulator of various mouse or touch events, then periodically + * call its getLookAt() method so that they can adjust their camera(s). Three modes are + * supported: ORBIT, MAP, and FREE_FLIGHT. To construct a manipulator instance, the desired mode + * is passed into the create method. + */ + open class DefaultCameraManipulator( + protected val manipulator: Manipulator, + ): CameraManipulator { + private val kZoomSpeed = 1f / 10f + + constructor( + orbitHomePosition: Position? = null, + targetPosition: Position? = null + ) : this( + Manipulator.Builder() + .apply { + orbitHomePosition?.let { orbitHomePosition(it) } + targetPosition?.let { targetPosition(it) } + } +// .viewport(min(width, 1), min(height, 1)) + .orbitSpeed(0.005f, 0.005f) + .zoomSpeed(0.05f) + .build(Manipulator.Mode.ORBIT), + ) + + override fun setViewport(width: Int, height: Int) { + manipulator.setViewport(width, height) + } + + override fun getTransform(): Transform { + return manipulator.transform + } + + override fun grabBegin(x: Int, y: Int, strafe: Boolean) { + manipulator.grabBegin(x, y, strafe) + } + + override fun grabUpdate(x: Int, y: Int) { + manipulator.grabUpdate(x, y) + } + + override fun grabEnd() { + manipulator.grabEnd() + } + + override fun scrollBegin(x: Int, y: Int, separation: Float) {} + + override fun scrollUpdate(x: Int, y: Int, prevSeparation: Float, currSeparation: Float) { + manipulator.scroll(x, y, (prevSeparation - currSeparation) * kZoomSpeed) + } + + override fun scrollEnd() {} + + override fun update(deltaTime: Float) { + manipulator.update(deltaTime) + } + } + private enum class Gesture { NONE, ORBIT, PAN, ZOOM } // Simplified memento of MotionEvent, minimal but sufficient for our purposes. @@ -49,7 +143,6 @@ open class CameraGestureDetector(private val viewHeight: () -> Int, var manipula private val kGestureConfidenceCount = 2 private val kPanConfidenceDistance = 10 private val kZoomConfidenceDistance = 10 - private val kZoomSpeed = 1f / 10f var isPanEnabled: Boolean = true @@ -73,13 +166,13 @@ open class CameraGestureDetector(private val viewHeight: () -> Int, var manipula if (currentGesture == Gesture.ZOOM) { val d0 = previousTouch.separation val d1 = touch.separation - manipulator?.scroll(touch.x, touch.y, (d0 - d1) * kZoomSpeed) + cameraManipulator?.scrollUpdate(touch.x, touch.y, d0, d1) previousTouch = touch return } if (currentGesture != Gesture.NONE) { - manipulator?.grabUpdate(touch.x, touch.y) + cameraManipulator?.grabUpdate(touch.x, touch.y) return } @@ -95,19 +188,20 @@ open class CameraGestureDetector(private val viewHeight: () -> Int, var manipula } if (isOrbitGesture()) { - manipulator?.grabBegin(touch.x, touch.y, false) + cameraManipulator?.grabBegin(touch.x, touch.y, false) currentGesture = Gesture.ORBIT return } if (isZoomGesture()) { + cameraManipulator?.scrollBegin(touch.x, touch.y, touch.separation) currentGesture = Gesture.ZOOM previousTouch = touch return } if (isPanGesture()) { - manipulator?.grabBegin(touch.x, touch.y, true) + cameraManipulator?.grabBegin(touch.x, touch.y, true) currentGesture = Gesture.PAN return } @@ -124,7 +218,7 @@ open class CameraGestureDetector(private val viewHeight: () -> Int, var manipula tentativeOrbitEvents.clear() tentativeZoomEvents.clear() currentGesture = Gesture.NONE - manipulator?.grabEnd() + cameraManipulator?.grabEnd() } private fun isOrbitGesture(): Boolean { @@ -148,4 +242,16 @@ open class CameraGestureDetector(private val viewHeight: () -> Int, var manipula val newest = tentativeZoomEvents.last().separation return kotlin.math.abs(newest - oldest) > kZoomConfidenceDistance } + + companion object { + fun createDefaultCameraManipulator( + manipulator: Manipulator? = null, + ): DefaultCameraManipulator? { + if (manipulator == null) { + return null + } + + return DefaultCameraManipulator(manipulator) + } + } } \ No newline at end of file diff --git a/sceneview/src/main/java/io/github/sceneview/node/Node.kt b/sceneview/src/main/java/io/github/sceneview/node/Node.kt index 92df10866..1851455f6 100644 --- a/sceneview/src/main/java/io/github/sceneview/node/Node.kt +++ b/sceneview/src/main/java/io/github/sceneview/node/Node.kt @@ -32,7 +32,6 @@ import io.github.sceneview.collision.TransformProvider import io.github.sceneview.gesture.MoveGestureDetector import io.github.sceneview.gesture.RotateGestureDetector import io.github.sceneview.gesture.ScaleGestureDetector -import io.github.sceneview.gesture.transform import io.github.sceneview.managers.getParentOrNull import io.github.sceneview.managers.getTransform import io.github.sceneview.managers.getWorldTransform diff --git a/settings.gradle b/settings.gradle index 5f52e5d3c..d4ba98bf2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,4 +19,5 @@ include ':samples:ar-model-viewer-compose' include ':samples:ar-point-cloud' include ':samples:gltf-camera' include ':samples:model-viewer' -include ':samples:model-viewer-compose' \ No newline at end of file +include ':samples:model-viewer-compose' +include ':samples:model-viewer-compose-camera-manipulator'