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 @@
+
+
+
+
+
+