diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..df53661 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +SmartNavigation \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4b11b6c..9ab3515 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -30,6 +30,9 @@ android { compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 + + // Add this to prevent build errors with ARCore + isCoreLibraryDesugaringEnabled = true // <--- ADD THIS } kotlinOptions { @@ -48,6 +51,17 @@ android { } dependencies { + // --- ADD ALL OF THESE --- // + // ARCore (SLAM) + implementation("com.google.ar:core:1.44.0") + implementation("com.google.ar.sceneform:core:1.17.1") + implementation("com.google.ar.sceneform.ux:sceneform-ux:1.17.1") + + // Desugaring, required for ARCore + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") + // --- END OF ADDITIONS --- // + + // Core Android implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -63,7 +77,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) - // CameraX for SLAM + // CameraX for SLAM (These are from your original file, now replaced by ARCore) implementation("androidx.camera:camera-core:1.3.3") implementation("androidx.camera:camera-camera2:1.3.3") implementation("androidx.camera:camera-lifecycle:1.3.3") @@ -87,4 +101,4 @@ dependencies { androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) -} +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ec51676..02982ad 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,7 +10,8 @@ - + + @@ -23,6 +24,10 @@ android:supportsRtl="true" android:theme="@style/Theme.SmartNavigation"> + + - + \ No newline at end of file diff --git a/app/src/main/java/com/example/smartnavigation/DeadReckoningScreen.kt b/app/src/main/java/com/example/smartnavigation/DeadReckoningScreen.kt index 0fea4c4..3c68d19 100644 --- a/app/src/main/java/com/example/smartnavigation/DeadReckoningScreen.kt +++ b/app/src/main/java/com/example/smartnavigation/DeadReckoningScreen.kt @@ -1,58 +1,67 @@ package com.example.smartnavigation - import android.content.Context import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.compose.material3.* -import androidx.compose.runtime.* import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.text.font.FontWeight -import kotlin.math.* +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.min +import kotlin.math.sin +import kotlin.math.sqrt + +// --- PDR Constants --- +private const val STRIDE_LENGTH_METERS = 0.7f // Average stride length +private const val STEP_THRESHOLD = 10.5f // Accelerometer magnitude to trigger a step +private const val STEP_DELAY_MS = 250 // Minimum time between steps @Composable -fun DeadReckoningScreen() { - val context = androidx.compose.ui.platform.LocalContext.current +fun DeadReckoningScreen(viewModel: SharedNavViewModel) { + val context = LocalContext.current val sensorManager = remember { context.getSystemService(Context.SENSOR_SERVICE) as SensorManager } - val position = remember { mutableStateListOf(0f, 0f, 0f) } - val velocity = remember { mutableStateListOf(0f, 0f, 0f) } - val pathPoints = remember { mutableStateListOf>() } + // Position is now only (X, Z) + val position = remember { mutableStateOf(0f to 0f) } + val pathPoints = remember { mutableStateListOf(0f to 0f) } var isRunning by remember { mutableStateOf(false) } + val pinPosition = viewModel.pinPosition.value + val sensorListener = remember { object : SensorEventListener { - var accelRaw = FloatArray(3) - var rotationVector = FloatArray(4) - var lastTimestamp = 0L - val alpha = 0.6f + private var rotationMatrix = FloatArray(9) + private var orientationAngles = FloatArray(3) + private var lastStepTime = 0L override fun onSensorChanged(event: SensorEvent) { - when (event.sensor.type) { - Sensor.TYPE_LINEAR_ACCELERATION -> { - val raw = event.values - for (i in 0..2) - accelRaw[i] = alpha * accelRaw[i] + (1 - alpha) * raw[i] - if (isRunning) integrate(event.timestamp) - } + if (!isRunning) return + when (event.sensor.type) { Sensor.TYPE_ROTATION_VECTOR -> { - if (event.values.size >= 4) - rotationVector = event.values.clone() - else { - val x = event.values[0] - val y = event.values[1] - val z = event.values[2] - rotationVector = floatArrayOf(x, y, z, sqrt(max(0f, 1f - x * x - y * y - z * z))) + SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) + SensorManager.getOrientation(rotationMatrix, orientationAngles) + } + Sensor.TYPE_ACCELEROMETER -> { + val (x, y, z) = event.values + val magnitude = sqrt(x * x + y * y + z * z) + + val currentTime = System.currentTimeMillis() + if (magnitude > STEP_THRESHOLD && (currentTime - lastStepTime) > STEP_DELAY_MS) { + lastStepTime = currentTime + onStepDetected() } } } @@ -60,49 +69,29 @@ fun DeadReckoningScreen() { override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} - private fun integrate(ts: Long) { - if (lastTimestamp == 0L) { - lastTimestamp = ts - return - } - val dt = (ts - lastTimestamp) / 1e9f - lastTimestamp = ts - if (dt <= 0f || dt > 0.5f) return - - val aWorld = deviceToWorldAccel(accelRaw, rotationVector) - for (i in 0..2) { - val ax = if (abs(aWorld[i]) < 0.02f) 0f else aWorld[i] - velocity[i] += ax * dt - position[i] += velocity[i] * dt - } - pathPoints.add(position[0] to position[1]) - if (pathPoints.size > 2000) pathPoints.removeAt(0) - } - - private fun deviceToWorldAccel(accel: FloatArray, q: FloatArray): FloatArray { - val qx = q[0].toDouble(); val qy = q[1].toDouble(); val qz = q[2].toDouble(); val qw = q[3].toDouble() - val vx = accel[0].toDouble(); val vy = accel[1].toDouble(); val vz = accel[2].toDouble() + private fun onStepDetected() { + // orientationAngles[0] is the azimuth (yaw) in radians + val heading = orientationAngles[0] - val ix = qw * vx + qy * vz - qz * vy - val iy = qw * vy + qz * vx - qx * vz - val iz = qw * vz + qx * vy - qy * vx - val iw = -qx * vx - qy * vy - qz * vz + // Update position based on heading and stride length + val (oldX, oldZ) = position.value + val newX = oldX + STRIDE_LENGTH_METERS * cos(heading) + val newZ = oldZ - STRIDE_LENGTH_METERS * sin(heading) // Subtract Z because screen Y is down - val wx = ix * qw - iw * qx + iy * qz - iz * qy - val wy = iy * qw - iw * qy + iz * qx - ix * qz - val wz = iz * qw - iw * qz + ix * qy - iy * qx - return floatArrayOf(wx.toFloat(), wy.toFloat(), wz.toFloat()) + position.value = newX to newZ + pathPoints.add(newX to newZ) + if (pathPoints.size > 2000) pathPoints.removeAt(0) } } } DisposableEffect(isRunning) { if (isRunning) { - sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)?.also { - sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_GAME) + sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.also { + sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_UI) } sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)?.also { - sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_GAME) + sensorManager.registerListener(sensorListener, it, SensorManager.SENSOR_DELAY_UI) } } else { sensorManager.unregisterListener(sensorListener) @@ -117,45 +106,87 @@ fun DeadReckoningScreen() { Button(onClick = { isRunning = true }, Modifier.weight(1f)) { Text("Start") } Button(onClick = { isRunning = false }, Modifier.weight(1f)) { Text("Stop") } Button(onClick = { - for (i in 0..2) { position[i] = 0f; velocity[i] = 0f } + position.value = 0f to 0f pathPoints.clear() + pathPoints.add(0f to 0f) // Reset path to origin }, Modifier.weight(1f)) { Text("Reset") } } Spacer(Modifier.height(10.dp)) - Text("Position: X=${position[0]} Y=${position[1]} Z=${position[2]}") - TrajectoryCanvas(points = pathPoints.toList()) + Text("DR Position: X=${"%.2f".format(position.value.first)} Z=${"%.2f".format(position.value.second)}") + + if (pinPosition != null) { + Text( + "SLAM Pin: X=${"%.2f".format(pinPosition.first)} Z=${"%.2f".format(pinPosition.second)}", + color = Color(0xFF006400) // Dark Green + ) + } else { + Text("No SLAM pin set. Go to SLAM screen to set one.", color = Color.Gray) + } + + TrajectoryCanvas( + points = pathPoints.toList(), + pin = pinPosition + ) } } @Composable -fun TrajectoryCanvas(points: List>) { +fun TrajectoryCanvas(points: List>, pin: Pair?) { Canvas( modifier = Modifier .fillMaxWidth() .height(380.dp) .padding(8.dp) ) { - if (points.isEmpty()) return@Canvas - val xs = points.map { it.first } - val ys = points.map { it.second } - val minX = xs.minOrNull() ?: 0f - val maxX = xs.maxOrNull() ?: 0f - val minY = ys.minOrNull() ?: 0f - val maxY = ys.maxOrNull() ?: 0f - val scale = min(size.width / (maxX - minX + 0.1f), size.height / (maxY - minY + 0.1f)) + if (points.isEmpty() && pin == null) return@Canvas + + val allPoints = points.toMutableList() + if (pin != null) { + allPoints.add(pin) + } + + val xs = allPoints.map { it.first } + val ys = allPoints.map { it.second } + val minX = (xs.minOrNull() ?: -1f) - 1f + val maxX = (xs.maxOrNull() ?: 1f) + 1f + val minY = (ys.minOrNull() ?: -1f) - 1f + val maxY = (ys.maxOrNull() ?: 1f) + 1f + + val rangeX = if (abs(maxX - minX) < 0.1f) 2f else (maxX - minX) + val rangeY = if (abs(maxY - minY) < 0.1f) 2f else (maxY - minY) + + val scale = min(size.width / rangeX, size.height / rangeY) * 0.9f val cx = size.width / 2f val cy = size.height / 2f + val midX = (minX + maxX) / 2f + val midY = (minY + maxY) / 2f + + // Draw the Dead Reckoning path for (i in 0 until points.size - 1) { val (x1, y1) = points[i] - val (x2, y2) = points[i + 1] + val (x2, y2) = points[i] drawLine( color = Color.Blue, - start = Offset(cx + (x1 - (minX + maxX) / 2f) * scale, cy - (y1 - (minY + maxY) / 2f) * scale), - end = Offset(cx + (x2 - (minX + maxX) / 2f) * scale, cy - (y2 - (minY + maxY) / 2f) * scale), + start = Offset(cx + (x1 - midX) * scale, cy + (y1 - midY) * scale), + end = Offset(cx + (x2 - midX) * scale, cy + (y2 - midY) * scale), strokeWidth = 4f ) } - val (lx, ly) = points.last() - drawCircle(Color.Red, 8f, Offset(cx + (lx - (minX + maxX) / 2f) * scale, cy - (ly - (minY + maxY) / 2f) * scale)) + + // Draw the current DR position + if (points.isNotEmpty()) { + val (lx, ly) = points.last() + drawCircle(Color.Red, 8f, Offset(cx + (lx - midX) * scale, cy + (ly - midY) * scale)) + } + + // Draw the "true" SLAM pin location + if (pin != null) { + val (px, py) = pin + val pinX = cx + (px - midX) * scale + val pinY = cy + (py - midY) * scale + drawCircle(Color(0xFF006400), 12f, Offset(pinX, pinY), style = Stroke(width = 5f)) + drawLine(Color(0xFF006400), Offset(pinX - 10, pinY), Offset(pinX + 10, pinY), strokeWidth = 5f) + drawLine(Color(0xFF006400), Offset(pinX, pinY - 10), Offset(pinX, pinY + 10), strokeWidth = 5f) + } } } diff --git a/app/src/main/java/com/example/smartnavigation/MainActivity.kt b/app/src/main/java/com/example/smartnavigation/MainActivity.kt index dc684d4..29b3302 100644 --- a/app/src/main/java/com/example/smartnavigation/MainActivity.kt +++ b/app/src/main/java/com/example/smartnavigation/MainActivity.kt @@ -9,20 +9,30 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel // <-- ADD THIS IMPORT +import androidx.navigation.NavController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.example.smartnavigation.ui.theme.SmartNavigationTheme // <-- Ensure this is imported class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - MaterialTheme { + // Use your project's theme + SmartNavigationTheme { val navController = rememberNavController() + + // Create the ViewModel that will be shared + val sharedViewModel: SharedNavViewModel = viewModel() + NavHost(navController, startDestination = "home") { composable("home") { HomeScreen(navController) } - composable("dead_reckoning") { DeadReckoningScreen() } - composable("slam") { SlamScreen() } + + // Pass the ViewModel to both screens + composable("dead_reckoning") { DeadReckoningScreen(sharedViewModel) } + composable("slam") { SlamScreen(sharedViewModel) } } } } @@ -30,7 +40,7 @@ class MainActivity : ComponentActivity() { } @Composable -fun HomeScreen(navController: androidx.navigation.NavController) { +fun HomeScreen(navController: NavController) { Surface(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier @@ -52,7 +62,7 @@ fun HomeScreen(navController: androidx.navigation.NavController) { Button( onClick = { navController.navigate("slam") }, modifier = Modifier.fillMaxWidth() - ) { Text("SLAM (Camera)") } + ) { Text("SLAM (AR Pin)") } // <-- Changed text } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartnavigation/SharedNavViewModel.kt b/app/src/main/java/com/example/smartnavigation/SharedNavViewModel.kt new file mode 100644 index 0000000..b77a9c3 --- /dev/null +++ b/app/src/main/java/com/example/smartnavigation/SharedNavViewModel.kt @@ -0,0 +1,22 @@ +package com.example.smartnavigation + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import com.google.ar.core.Anchor + +class SharedNavViewModel : ViewModel() { + val slamAnchor = mutableStateOf(null) + val pinPosition = mutableStateOf?>(null) + + fun setPin(anchor: Anchor) { + slamAnchor.value = anchor + // Store the X and Z position of the anchor + pinPosition.value = Pair(anchor.pose.tx(), anchor.pose.tz()) + } + + fun clearPin() { + slamAnchor.value?.detach() + slamAnchor.value = null + pinPosition.value = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/smartnavigation/SlamScreen.kt b/app/src/main/java/com/example/smartnavigation/SlamScreen.kt index e0b9b23..fa2ac60 100644 --- a/app/src/main/java/com/example/smartnavigation/SlamScreen.kt +++ b/app/src/main/java/com/example/smartnavigation/SlamScreen.kt @@ -1,60 +1,203 @@ package com.example.smartnavigation import android.Manifest -import androidx.camera.core.CameraSelector -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.camera.view.PreviewView +import android.view.View +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.Alignment -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.fragment.app.FragmentActivity import com.google.accompanist.permissions.* +import com.google.ar.core.Anchor +import com.google.ar.core.Plane +import com.google.ar.sceneform.AnchorNode +import com.google.ar.sceneform.FrameTime +import com.google.ar.sceneform.Scene +import com.google.ar.sceneform.math.Vector3 +import com.google.ar.sceneform.rendering.ModelRenderable +import com.google.ar.sceneform.ux.ArFragment +import com.google.ar.sceneform.ux.TransformableNode +import kotlin.math.atan2 +import kotlin.math.sqrt @OptIn(ExperimentalPermissionsApi::class) @Composable -fun SlamScreen() { +fun SlamScreen(viewModel: SharedNavViewModel) { val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA) val context = LocalContext.current - val lifecycleOwner = LocalLifecycleOwner.current + var arFragment: ArFragment? by remember { mutableStateOf(null) } + var pinModel: ModelRenderable? by remember { mutableStateOf(null) } + // --- Navigation State --- + var distanceToPin by remember { mutableStateOf(null) } + var directionAngle by remember { mutableStateOf(0f) } // Angle to rotate the arrow + + // Load the 3D model LaunchedEffect(Unit) { - if (!cameraPermission.status.isGranted) cameraPermission.launchPermissionRequest() + if (pinModel == null) { + ModelRenderable.builder() + .setSource(context, R.raw.pin_model) + .build() + .thenAccept { model -> pinModel = model } + .exceptionally { null } + } + } + + LaunchedEffect(Unit) { + if (!cameraPermission.status.isGranted) { + cameraPermission.launchPermissionRequest() + } } if (cameraPermission.status.isGranted) { - Column(Modifier.fillMaxSize()) { - Text( - "SLAM — Camera Visualization", - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier.padding(16.dp) - ) - CameraPreview(context, lifecycleOwner) + Box(Modifier.fillMaxSize()) { // Use Box to allow overlay + Column(Modifier.fillMaxSize()) { + Text( + "SLAM — Tap Floor to 'Pin' Location", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(16.dp) + ) + + Box(modifier = Modifier.weight(1f)) { + AndroidView( + factory = { ctx -> + val view = View.inflate(ctx, R.layout.ar_scene_layout, null) + val fragment = (view.context as FragmentActivity) + .supportFragmentManager + .findFragmentById(R.id.ar_fragment) as ArFragment + + arFragment = fragment + + // Add the Scene Update listener for navigation + fragment.arSceneView.scene.addOnUpdateListener(Scene.OnUpdateListener { + onSceneUpdate(it, viewModel, arFragment, onUpdate = { dist, angle -> + distanceToPin = dist + directionAngle = angle + }) + }) + + fragment.setOnTapArPlaneListener { hitResult, plane, _ -> + if (pinModel != null && plane.type == Plane.Type.HORIZONTAL_UPWARD_FACING) { + viewModel.clearPin() + removeAllAnchors(fragment) + val anchor = hitResult.createAnchor() + viewModel.setPin(anchor) + placePinObject(fragment, anchor, pinModel!!) + } + } + view + }, + modifier = Modifier.fillMaxSize() + ) + } + + Button( + onClick = { + viewModel.clearPin() + arFragment?.let { removeAllAnchors(it) } + distanceToPin = null // Clear navigation UI + }, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { Text("Clear Pin") } + } + + // --- Navigation Overlay UI --- + if (distanceToPin != null && viewModel.slamAnchor.value != null) { + NavigationOverlay(distance = distanceToPin!!, angle = directionAngle) + } } } else { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text("Camera permission required.") + Text("Camera permission required for SLAM.") } } } +// This function runs on every AR frame to calculate direction and distance +private fun onSceneUpdate( + frameTime: FrameTime, + viewModel: SharedNavViewModel, + arFragment: ArFragment?, + onUpdate: (distance: Float, angle: Float) -> Unit +) { + val pinAnchor = viewModel.slamAnchor.value ?: return + val camera = arFragment?.arSceneView?.scene?.camera ?: return + + // 1. Get positions + val pinPosition = pinAnchor.pose + val cameraPosition = camera.worldPosition + + // 2. Calculate distance (on the 2D floor plane) + val dx = pinPosition.tx() - cameraPosition.x + val dz = pinPosition.tz() - cameraPosition.z + val distance = sqrt(dx * dx + dz * dz) + + // 3. Calculate direction vector and angle + val directionToPin = Vector3(dx, 0f, dz).normalized() + val cameraForward = camera.forward.let { Vector3(it.x, 0f, it.z).normalized() } + + // Angle between camera forward and pin direction in degrees + val angleRad = atan2(cameraForward.cross(directionToPin).y, cameraForward.dot(directionToPin)) + val angleDeg = Math.toDegrees(angleRad.toDouble()).toFloat() + + onUpdate(distance, -angleDeg) // Negate because screen rotation is opposite +} + @Composable -fun CameraPreview(context: android.content.Context, lifecycleOwner: LifecycleOwner) { - val previewView = remember { PreviewView(context) } - LaunchedEffect(Unit) { - val cameraProviderFuture = ProcessCameraProvider.getInstance(context) - val cameraProvider = cameraProviderFuture.get() - val preview = androidx.camera.core.Preview.Builder().build().also { - it.setSurfaceProvider(previewView.surfaceProvider) +fun BoxScope.NavigationOverlay(distance: Float, angle: Float) { + Column( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 80.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "%.1f m".format(distance), + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + modifier = Modifier.padding(8.dp).background(Color.Black.copy(alpha = 0.5f), shape = MaterialTheme.shapes.medium).padding(horizontal = 8.dp, vertical = 4.dp) + ) + Spacer(Modifier.height(8.dp)) + Image( + imageVector = Icons.Default.ArrowUpward, + contentDescription = "Directional Arrow", + colorFilter = ColorFilter.tint(Color.White), + modifier = Modifier + .size(64.dp) + .rotate(angle) // Rotate the arrow to point the way + ) + } +} + +private fun removeAllAnchors(arFragment: ArFragment) { + arFragment.arSceneView.scene.children.toList().forEach { node -> + if (node is AnchorNode) { + node.anchor?.detach() + node.setParent(null) } - val selector = CameraSelector.DEFAULT_BACK_CAMERA - cameraProvider.unbindAll() - cameraProvider.bindToLifecycle(lifecycleOwner, selector, preview) } - AndroidView({ previewView }, modifier = Modifier.fillMaxSize()) } + +private fun placePinObject(arFragment: ArFragment, anchor: Anchor, model: ModelRenderable) { + val anchorNode = AnchorNode(anchor) + val node = TransformableNode(arFragment.transformationSystem) + node.renderable = model + node.setParent(anchorNode) + arFragment.arSceneView.scene.addChild(anchorNode) + node.select() +} \ No newline at end of file diff --git a/app/src/main/res/layout/ar_scene_layout.xml b/app/src/main/res/layout/ar_scene_layout.xml new file mode 100644 index 0000000..19b1c5d --- /dev/null +++ b/app/src/main/res/layout/ar_scene_layout.xml @@ -0,0 +1,12 @@ + + + + + +