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'