replate-camera 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/replatecamera/ReplateCameraModule.kt +60 -0
- package/android/src/main/java/com/replatecamera/ReplateCameraPackage.kt +1 -1
- package/android/src/main/java/com/replatecamera/ReplateCameraView.kt +367 -113
- package/android/src/main/java/com/replatecamera/ReplateCameraViewManager.kt +3 -2
- package/ios/ReplateCameraViewManager.swift +2 -2
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -93,5 +93,6 @@ dependencies {
|
|
|
93
93
|
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
94
94
|
implementation 'com.google.ar:core:1.45.0'
|
|
95
95
|
implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.17.1'
|
|
96
|
+
implementation("androidx.exifinterface:exifinterface:1.3.7")
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
package com.replatecamera
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.*
|
|
4
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
5
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
6
|
+
import com.facebook.react.uimanager.common.UIManagerType
|
|
7
|
+
|
|
8
|
+
@ReactModule(name = ReplateCameraModule.NAME)
|
|
9
|
+
class ReplateCameraModule(private val reactContext: ReactApplicationContext) :
|
|
10
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
11
|
+
|
|
12
|
+
companion object {
|
|
13
|
+
const val NAME = "ReplateCameraModule"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
override fun getName(): String = NAME
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Example: takePhoto(...) from JS, returning a promise that resolves with the file URI or rejects on error.
|
|
20
|
+
*
|
|
21
|
+
* @param viewTag The React tag (integer) of the ReplateCameraView. From JS, you can get this via findNodeHandle(ref).
|
|
22
|
+
*/
|
|
23
|
+
@ReactMethod
|
|
24
|
+
fun takePhoto(viewTag: Int, promise: Promise) {
|
|
25
|
+
val cameraView = findCameraViewByTag(viewTag)
|
|
26
|
+
if (cameraView == null) {
|
|
27
|
+
promise.reject("VIEW_NOT_FOUND", "No ReplateCameraView found with tag $viewTag")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Call the Kotlin AR method:
|
|
32
|
+
cameraView.takePhoto { uri ->
|
|
33
|
+
if (uri != null) {
|
|
34
|
+
// resolve the promise with the URI string
|
|
35
|
+
promise.resolve(uri.toString())
|
|
36
|
+
} else {
|
|
37
|
+
promise.reject("PHOTO_ERROR", "Failed to capture photo.")
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Example: resetSession(...) from JS, no return value needed.
|
|
44
|
+
*/
|
|
45
|
+
@ReactMethod
|
|
46
|
+
fun resetSession(viewTag: Int) {
|
|
47
|
+
val cameraView = findCameraViewByTag(viewTag)
|
|
48
|
+
cameraView?.resetSession()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Helper function that looks up the ReplateCameraView instance by its React tag.
|
|
53
|
+
*/
|
|
54
|
+
private fun findCameraViewByTag(viewTag: Int): ReplateCameraView? {
|
|
55
|
+
// Use UIManagerHelper to look up the native view associated with 'viewTag'.
|
|
56
|
+
val uiManager = UIManagerHelper.getUIManager(reactContext, UIManagerType.DEFAULT /* 1 => default type for Fabric or not */)
|
|
57
|
+
val nativeView = uiManager?.resolveView(viewTag)
|
|
58
|
+
return nativeView as? ReplateCameraView
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -8,7 +8,7 @@ import com.facebook.react.uimanager.ViewManager
|
|
|
8
8
|
|
|
9
9
|
class ReplateCameraPackage : ReactPackage {
|
|
10
10
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
11
|
-
return
|
|
11
|
+
return listOf(ReplateCameraModule(reactContext))
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
@@ -4,105 +4,267 @@ import android.Manifest
|
|
|
4
4
|
import android.app.Activity
|
|
5
5
|
import android.content.Context
|
|
6
6
|
import android.content.pm.PackageManager
|
|
7
|
+
import android.graphics.*
|
|
8
|
+
import android.net.Uri
|
|
7
9
|
import android.os.Build
|
|
10
|
+
import android.os.Environment
|
|
11
|
+
import android.os.Handler
|
|
12
|
+
import android.os.Looper
|
|
8
13
|
import android.util.AttributeSet
|
|
14
|
+
import android.util.Log
|
|
9
15
|
import android.view.GestureDetector
|
|
10
16
|
import android.view.MotionEvent
|
|
17
|
+
import android.view.PixelCopy
|
|
11
18
|
import android.view.ScaleGestureDetector
|
|
12
19
|
import android.widget.FrameLayout
|
|
13
20
|
import androidx.annotation.RequiresApi
|
|
14
21
|
import androidx.core.app.ActivityCompat
|
|
15
22
|
import androidx.core.content.ContextCompat
|
|
23
|
+
import androidx.exifinterface.media.ExifInterface
|
|
16
24
|
import androidx.fragment.app.FragmentActivity
|
|
25
|
+
import com.google.ar.core.Anchor
|
|
17
26
|
import com.google.ar.core.HitResult
|
|
18
|
-
import com.google.ar.
|
|
27
|
+
import com.google.ar.core.Plane
|
|
28
|
+
import com.google.ar.core.TrackingState
|
|
29
|
+
import com.google.ar.sceneform.*
|
|
19
30
|
import com.google.ar.sceneform.math.Vector3
|
|
31
|
+
import com.google.ar.sceneform.rendering.Color
|
|
32
|
+
import com.google.ar.sceneform.rendering.Material
|
|
20
33
|
import com.google.ar.sceneform.rendering.MaterialFactory
|
|
21
34
|
import com.google.ar.sceneform.rendering.ShapeFactory
|
|
22
35
|
import com.google.ar.sceneform.ux.ArFragment
|
|
23
36
|
import com.google.ar.sceneform.ux.TransformableNode
|
|
24
|
-
import
|
|
37
|
+
import java.io.File
|
|
38
|
+
import java.io.FileOutputStream
|
|
39
|
+
import java.text.SimpleDateFormat
|
|
40
|
+
import java.util.Date
|
|
41
|
+
import kotlin.math.acos
|
|
42
|
+
import kotlin.math.atan2
|
|
43
|
+
import kotlin.math.cos
|
|
44
|
+
import kotlin.math.round
|
|
45
|
+
import kotlin.math.sin
|
|
25
46
|
import kotlin.math.sqrt
|
|
26
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Kotlin version of an ARCore/Sceneform view, mimicking much of the Swift+RealityKit approach.
|
|
50
|
+
* - Creates spheres in two circles around an anchor.
|
|
51
|
+
* - Allows pan/pinch to move/scale.
|
|
52
|
+
* - Captures screenshots with metadata placeholders.
|
|
53
|
+
* - Tracks gravity vector via SensorManager (TYPE_GRAVITY).
|
|
54
|
+
*
|
|
55
|
+
* Usage:
|
|
56
|
+
* 1) In your Activity/Fragment layout, add a <FrameLayout> that hosts this view.
|
|
57
|
+
* 2) Instantiate ReplateCameraView in code or via XML. The ARFragment is attached dynamically.
|
|
58
|
+
* 3) Call takePhoto() to save a screenshot to external files.
|
|
59
|
+
*/
|
|
27
60
|
@RequiresApi(Build.VERSION_CODES.N)
|
|
28
61
|
class ReplateCameraView @JvmOverloads constructor(
|
|
29
|
-
context: Context,
|
|
30
|
-
|
|
62
|
+
context: Context,
|
|
63
|
+
attrs: AttributeSet? = null,
|
|
64
|
+
defStyleAttr: Int = 0
|
|
65
|
+
) : FrameLayout(context, attrs, defStyleAttr), Scene.OnUpdateListener,
|
|
66
|
+
android.hardware.SensorEventListener {
|
|
67
|
+
|
|
68
|
+
companion object {
|
|
69
|
+
private const val TAG = "ReplateCameraView"
|
|
70
|
+
|
|
71
|
+
// Number of spheres per circle (mirrors Swift code's 72)
|
|
72
|
+
private const val TOTAL_SPHERES_PER_CIRCLE = 72
|
|
73
|
+
// Each sphere is placed 5 degrees apart => 360/5 = 72
|
|
74
|
+
private const val ANGLE_INCREMENT_DEGREES = 5f
|
|
75
|
+
|
|
76
|
+
// Initial defaults that match the Swift code
|
|
77
|
+
private const val DEFAULT_SPHERE_RADIUS = 0.004f
|
|
78
|
+
private const val DEFAULT_SPHERES_RADIUS = 0.13f
|
|
79
|
+
private const val DEFAULT_SPHERES_HEIGHT = 0.10f
|
|
80
|
+
private const val DEFAULT_DISTANCE_BETWEEN_CIRCLES = 0.10f
|
|
81
|
+
private const val DEFAULT_DRAG_SPEED = 7000f
|
|
82
|
+
private const val ANGLE_THRESHOLD = 0.6f // Radians
|
|
83
|
+
private const val MIN_DISTANCE = 0.15f
|
|
84
|
+
private const val MAX_DISTANCE = 0.65f
|
|
31
85
|
|
|
86
|
+
// Track whether each sphere index is "set" (mirroring Swift arrays)
|
|
87
|
+
private val upperSpheresSet = BooleanArray(TOTAL_SPHERES_PER_CIRCLE) { false }
|
|
88
|
+
private val lowerSpheresSet = BooleanArray(TOTAL_SPHERES_PER_CIRCLE) { false }
|
|
89
|
+
|
|
90
|
+
// Stats
|
|
91
|
+
var totalPhotosTaken = 0
|
|
92
|
+
var photosFromDifferentAnglesTaken = 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// AR Sceneform stuff
|
|
32
96
|
private lateinit var arFragment: ArFragment
|
|
33
97
|
private var anchorNode: AnchorNode? = null
|
|
34
|
-
|
|
98
|
+
|
|
99
|
+
// Spheres and a "focus node"
|
|
100
|
+
private val spheresModels = mutableListOf<TransformableNode>()
|
|
101
|
+
private var focusNode: Node? = null
|
|
102
|
+
|
|
103
|
+
// Scene geometry
|
|
104
|
+
private var sphereRadius = DEFAULT_SPHERE_RADIUS
|
|
105
|
+
private var spheresRadius = DEFAULT_SPHERES_RADIUS
|
|
106
|
+
private var sphereAngle = ANGLE_INCREMENT_DEGREES // If you want to “scale angle”
|
|
107
|
+
private var spheresHeight = DEFAULT_SPHERES_HEIGHT
|
|
108
|
+
private var distanceBetweenCircles = DEFAULT_DISTANCE_BETWEEN_CIRCLES
|
|
109
|
+
private var circleInFocus = 0 // 0 => lower circle, 1 => upper circle
|
|
110
|
+
private var dragSpeed = DEFAULT_DRAG_SPEED
|
|
111
|
+
|
|
112
|
+
// Gravity vector
|
|
113
|
+
private var gravityVector = mutableMapOf<String, Double>()
|
|
114
|
+
|
|
115
|
+
// Gesture detectors for custom pan/pinch (replicating Swift approach)
|
|
35
116
|
private var gestureDetector: GestureDetector
|
|
36
117
|
private var scaleDetector: ScaleGestureDetector
|
|
37
|
-
private var sphereRadius = 0.004f
|
|
38
|
-
private var sphereCircleRadius = 0.13f
|
|
39
|
-
private var dragSpeed = 0.005f
|
|
40
118
|
|
|
119
|
+
// ------------------------------------------------------------------------
|
|
120
|
+
// Init & Setup
|
|
121
|
+
// ------------------------------------------------------------------------
|
|
41
122
|
init {
|
|
42
|
-
setupArFragment()
|
|
43
123
|
requestCameraPermission()
|
|
124
|
+
setupArFragment()
|
|
125
|
+
|
|
44
126
|
gestureDetector = GestureDetector(context, GestureListener())
|
|
45
127
|
scaleDetector = ScaleGestureDetector(context, ScaleListener())
|
|
128
|
+
|
|
129
|
+
// Register a gravity sensor to replicate iOS's CMMotionManager
|
|
130
|
+
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE)
|
|
131
|
+
as? android.hardware.SensorManager
|
|
132
|
+
sensorManager?.getDefaultSensor(android.hardware.Sensor.TYPE_GRAVITY)?.also { gravitySensor ->
|
|
133
|
+
sensorManager.registerListener(this, gravitySensor, android.hardware.SensorManager.SENSOR_DELAY_GAME)
|
|
134
|
+
}
|
|
46
135
|
}
|
|
47
136
|
|
|
137
|
+
/**
|
|
138
|
+
* If camera permission not granted, request at runtime (for AR use).
|
|
139
|
+
*/
|
|
140
|
+
private fun requestCameraPermission() {
|
|
141
|
+
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
|
|
142
|
+
!= PackageManager.PERMISSION_GRANTED) {
|
|
143
|
+
ActivityCompat.requestPermissions(
|
|
144
|
+
context as Activity,
|
|
145
|
+
arrayOf(Manifest.permission.CAMERA),
|
|
146
|
+
0
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Programmatically attach an ArFragment into this FrameLayout.
|
|
153
|
+
* We also add ourselves as a Scene update listener.
|
|
154
|
+
*/
|
|
48
155
|
@RequiresApi(Build.VERSION_CODES.N)
|
|
49
156
|
private fun setupArFragment() {
|
|
50
157
|
arFragment = ArFragment()
|
|
51
|
-
val
|
|
52
|
-
fragmentManager.beginTransaction().replace(this.id, arFragment).commit()
|
|
158
|
+
val fm = (context as FragmentActivity).supportFragmentManager
|
|
53
159
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
160
|
+
fm.beginTransaction().replace(this.id, arFragment).commitAllowingStateLoss()
|
|
161
|
+
// Add an update listener
|
|
162
|
+
arFragment.arSceneView.scene.addOnUpdateListener(this)
|
|
58
163
|
|
|
59
|
-
|
|
164
|
+
// Listen for plane taps
|
|
165
|
+
arFragment.setOnTapArPlaneListener { hitResult: HitResult, plane: Plane, motionEvent: MotionEvent ->
|
|
60
166
|
if (anchorNode == null) {
|
|
61
167
|
anchorNode = createAnchorNode(hitResult)
|
|
62
|
-
|
|
168
|
+
createSpheresAtY(spheresHeight) // Lower circle
|
|
169
|
+
createSpheresAtY(distanceBetweenCircles + spheresHeight) // Upper circle
|
|
170
|
+
createFocusSphere()
|
|
63
171
|
}
|
|
64
172
|
}
|
|
65
173
|
}
|
|
66
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Creates and returns an AnchorNode from a tap HitResult.
|
|
177
|
+
*/
|
|
67
178
|
private fun createAnchorNode(hitResult: HitResult): AnchorNode {
|
|
68
|
-
val anchor = hitResult.createAnchor()
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
179
|
+
val anchor: Anchor = hitResult.createAnchor()
|
|
180
|
+
return AnchorNode(anchor).also { node ->
|
|
181
|
+
node.setParent(arFragment.arSceneView.scene)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ------------------------------------------------------------------------
|
|
186
|
+
// Scene OnUpdateListener => replicates Swift ARSessionDelegate
|
|
187
|
+
// ------------------------------------------------------------------------
|
|
188
|
+
override fun onUpdate(frameTime: FrameTime?) {
|
|
189
|
+
val frame = arFragment.arSceneView.arFrame ?: return
|
|
190
|
+
if (frame.camera.trackingState != TrackingState.TRACKING) return
|
|
191
|
+
|
|
192
|
+
// If you need per-frame logic, do it here.
|
|
193
|
+
// Example: check lighting, or do real-time distance checks, etc.
|
|
72
194
|
}
|
|
73
195
|
|
|
196
|
+
// ------------------------------------------------------------------------
|
|
197
|
+
// Spheres Logic
|
|
198
|
+
// ------------------------------------------------------------------------
|
|
74
199
|
@RequiresApi(Build.VERSION_CODES.N)
|
|
75
|
-
private fun
|
|
76
|
-
for (i in 0 until
|
|
77
|
-
val
|
|
78
|
-
val x =
|
|
79
|
-
val z =
|
|
80
|
-
createSphere(Vector3(x,
|
|
200
|
+
private fun createSpheresAtY(y: Float) {
|
|
201
|
+
for (i in 0 until TOTAL_SPHERES_PER_CIRCLE) {
|
|
202
|
+
val angleRad = Math.toRadians((i * ANGLE_INCREMENT_DEGREES).toDouble()).toFloat()
|
|
203
|
+
val x = spheresRadius * cos(angleRad.toDouble()).toFloat()
|
|
204
|
+
val z = spheresRadius * sin(angleRad.toDouble()).toFloat()
|
|
205
|
+
createSphere(Vector3(x, y, z))
|
|
81
206
|
}
|
|
82
207
|
}
|
|
83
208
|
|
|
84
209
|
@RequiresApi(Build.VERSION_CODES.N)
|
|
85
210
|
private fun createSphere(position: Vector3) {
|
|
86
211
|
MaterialFactory.makeOpaqueWithColor(context, Color(android.graphics.Color.WHITE))
|
|
87
|
-
.thenAccept { material ->
|
|
88
|
-
val
|
|
89
|
-
val
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
212
|
+
.thenAccept { material: Material ->
|
|
213
|
+
val sphereRenderable = ShapeFactory.makeSphere(sphereRadius, Vector3.zero(), material)
|
|
214
|
+
val sphereNode = TransformableNode(arFragment.transformationSystem)
|
|
215
|
+
sphereNode.renderable = sphereRenderable
|
|
216
|
+
sphereNode.worldPosition = position
|
|
217
|
+
sphereNode.setParent(anchorNode)
|
|
218
|
+
spheresModels.add(sphereNode)
|
|
93
219
|
}
|
|
94
220
|
}
|
|
95
221
|
|
|
96
|
-
|
|
97
|
-
|
|
222
|
+
@RequiresApi(Build.VERSION_CODES.N)
|
|
223
|
+
private fun createFocusSphere() {
|
|
224
|
+
// Parent node for the two green spheres + optional overlay
|
|
225
|
+
val parentNode = Node().apply { setParent(anchorNode) }
|
|
226
|
+
focusNode = parentNode
|
|
227
|
+
|
|
228
|
+
// Lower focus sphere
|
|
229
|
+
MaterialFactory.makeOpaqueWithColor(context, Color(0f, 1f, 0f))
|
|
230
|
+
.thenAccept { mat ->
|
|
231
|
+
val r = sphereRadius * 1.5f
|
|
232
|
+
val renderable = ShapeFactory.makeSphere(r, Vector3.zero(), mat)
|
|
233
|
+
val sphereN = Node()
|
|
234
|
+
sphereN.worldPosition = Vector3(0f, spheresHeight, 0f)
|
|
235
|
+
sphereN.renderable = renderable
|
|
236
|
+
parentNode.addChild(sphereN)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Upper focus sphere
|
|
240
|
+
MaterialFactory.makeOpaqueWithColor(context, Color(0f, 1f, 0f))
|
|
241
|
+
.thenAccept { mat ->
|
|
242
|
+
val r = sphereRadius * 1.5f
|
|
243
|
+
val renderable = ShapeFactory.makeSphere(r, Vector3.zero(), mat)
|
|
244
|
+
val sphereN = Node()
|
|
245
|
+
sphereN.worldPosition = Vector3(0f, spheresHeight + distanceBetweenCircles, 0f)
|
|
246
|
+
sphereN.renderable = renderable
|
|
247
|
+
parentNode.addChild(sphereN)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// If you have a custom "center.obj", load via ModelRenderable builder or Filament loader
|
|
251
|
+
// parentNode.addChild(loadedCustomOverlayNode)
|
|
98
252
|
}
|
|
99
253
|
|
|
254
|
+
// ------------------------------------------------------------------------
|
|
255
|
+
// Gestures (Mirror Swift's tap, pan, pinch)
|
|
256
|
+
// ------------------------------------------------------------------------
|
|
100
257
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
101
|
-
|
|
258
|
+
scaleDetector.onTouchEvent(event)
|
|
259
|
+
gestureDetector.onTouchEvent(event)
|
|
260
|
+
return true
|
|
102
261
|
}
|
|
103
262
|
|
|
104
263
|
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
|
105
|
-
|
|
264
|
+
|
|
265
|
+
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
|
266
|
+
Log.d(TAG, "Single tap up at x=${e.x}, y=${e.y}")
|
|
267
|
+
// If you want custom logic (like Swift's "viewTapped"), put it here
|
|
106
268
|
return true
|
|
107
269
|
}
|
|
108
270
|
|
|
@@ -112,105 +274,197 @@ class ReplateCameraView @JvmOverloads constructor(
|
|
|
112
274
|
distanceX: Float,
|
|
113
275
|
distanceY: Float
|
|
114
276
|
): Boolean {
|
|
115
|
-
// Assuming you have a reference to the AR scene view and the anchor entity
|
|
116
277
|
val sceneView = arFragment.arSceneView
|
|
117
|
-
val
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
val forward = Vector3(
|
|
129
|
-
-cameraTransform.x,
|
|
130
|
-
0f,
|
|
131
|
-
-cameraTransform.z
|
|
132
|
-
) // Assuming Y is up
|
|
133
|
-
val right = Vector3(
|
|
134
|
-
cameraTransform.x,
|
|
135
|
-
0f,
|
|
136
|
-
cameraTransform.z
|
|
137
|
-
) // Assuming Y is up
|
|
138
|
-
|
|
139
|
-
// Normalize the vectors
|
|
140
|
-
val forwardNormalized = normalize(forward)
|
|
141
|
-
val rightNormalized = normalize(right)
|
|
142
|
-
|
|
143
|
-
// Calculate the adjusted movement based on user input and camera orientation
|
|
144
|
-
val adjustedMovement = Vector3(
|
|
145
|
-
translationX * rightNormalized.x + translationY * forwardNormalized.x,
|
|
146
|
-
0f, // Assuming you want to keep the movement in the horizontal plane
|
|
147
|
-
-translationX * rightNormalized.z - translationY * forwardNormalized.z // Invert the z movement
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
// Update the position of the anchor entity
|
|
151
|
-
val initialPosition = anchorEntity.worldPosition
|
|
152
|
-
anchorEntity.worldPosition = Vector3.add(
|
|
153
|
-
initialPosition,
|
|
154
|
-
adjustedMovement.scaled(1f / dragSpeed)
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
// Reset translation
|
|
158
|
-
e2.setLocation(0f, 0f)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
278
|
+
val currentAnchor = anchorNode ?: return true
|
|
279
|
+
|
|
280
|
+
// Replicate Swift handlePan => anchorNode position changes based on camera orientation
|
|
281
|
+
val camera = sceneView.scene.camera
|
|
282
|
+
if (e2.action == MotionEvent.ACTION_MOVE && camera != null) {
|
|
283
|
+
val translationX = -distanceX
|
|
284
|
+
val translationY = -distanceY
|
|
285
|
+
|
|
286
|
+
// Sceneform camera: forward is -Z, right is +X
|
|
287
|
+
val cameraForward = Vector3(camera.forward.x, 0f, camera.forward.z)
|
|
288
|
+
val cameraRight = Vector3(camera.right.x, 0f, camera.right.z)
|
|
161
289
|
|
|
290
|
+
val forwardNorm = normalize(cameraForward)
|
|
291
|
+
val rightNorm = normalize(cameraRight)
|
|
292
|
+
|
|
293
|
+
// Similar to Swift: anchorEntity.position = initialPosition + adjustedMovement / dragSpeed
|
|
294
|
+
val adjustedMovement = Vector3(
|
|
295
|
+
translationX * rightNorm.x + translationY * forwardNorm.x,
|
|
296
|
+
0f,
|
|
297
|
+
translationX * rightNorm.z + translationY * forwardNorm.z
|
|
298
|
+
).scaled(1f / dragSpeed)
|
|
299
|
+
|
|
300
|
+
val initialPos = currentAnchor.worldPosition
|
|
301
|
+
currentAnchor.worldPosition = Vector3.add(initialPos, adjustedMovement)
|
|
302
|
+
}
|
|
162
303
|
return true
|
|
163
304
|
}
|
|
164
305
|
|
|
165
|
-
private fun normalize(
|
|
166
|
-
val length = sqrt(
|
|
167
|
-
return
|
|
306
|
+
private fun normalize(v: Vector3): Vector3 {
|
|
307
|
+
val length = sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
|
|
308
|
+
return if (length > 0.0001f) {
|
|
309
|
+
Vector3(v.x / length, v.y / length, v.z / length)
|
|
310
|
+
} else {
|
|
311
|
+
Vector3.zero()
|
|
312
|
+
}
|
|
168
313
|
}
|
|
169
314
|
}
|
|
170
315
|
|
|
171
316
|
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
172
317
|
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
// Calculate the scale factor from the detector
|
|
318
|
+
// Like Swift handlePinch => remove child spheres, scale, recreate
|
|
319
|
+
val currentAnchor = anchorNode ?: return false
|
|
177
320
|
val scale = detector.scaleFactor
|
|
178
321
|
|
|
179
|
-
// Remove all spheres
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
322
|
+
// Remove all existing spheres
|
|
323
|
+
spheresModels.forEach { node -> currentAnchor.removeChild(node) }
|
|
324
|
+
spheresModels.clear()
|
|
325
|
+
|
|
326
|
+
// Remove focus
|
|
327
|
+
focusNode?.let { currentAnchor.removeChild(it) }
|
|
328
|
+
focusNode = null
|
|
329
|
+
|
|
330
|
+
// Scale the radii
|
|
331
|
+
sphereRadius *= scale
|
|
332
|
+
spheresRadius *= scale
|
|
333
|
+
sphereAngle *= scale // if also scaling the angle
|
|
334
|
+
// Rebuild
|
|
335
|
+
createSpheresAtY(spheresHeight)
|
|
336
|
+
createSpheresAtY(distanceBetweenCircles + spheresHeight)
|
|
337
|
+
createFocusSphere()
|
|
338
|
+
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ------------------------------------------------------------------------
|
|
344
|
+
// Photo Capture Implementation (PixelCopy)
|
|
345
|
+
// ------------------------------------------------------------------------
|
|
346
|
+
/**
|
|
347
|
+
* Takes a screenshot of the ArSceneView and embeds the current gravityVector
|
|
348
|
+
* as a JSON string in the EXIF “UserComment” field.
|
|
349
|
+
*
|
|
350
|
+
* @param onPhotoSaved Callback that gives you the Uri of the saved file (or null on failure).
|
|
351
|
+
*/
|
|
352
|
+
fun takePhoto(onPhotoSaved: (Uri?) -> Unit = {}) {
|
|
353
|
+
val sceneView: ArSceneView = arFragment.arSceneView
|
|
354
|
+
val bitmap = Bitmap.createBitmap(sceneView.width, sceneView.height, Bitmap.Config.ARGB_8888)
|
|
355
|
+
|
|
356
|
+
PixelCopy.request(sceneView, bitmap, { copyResult ->
|
|
357
|
+
if (copyResult == PixelCopy.SUCCESS) {
|
|
358
|
+
// Save + embed EXIF
|
|
359
|
+
val savedUri = saveBitmapWithExif(bitmap, gravityVector)
|
|
360
|
+
if (savedUri != null) {
|
|
361
|
+
totalPhotosTaken += 1
|
|
362
|
+
Log.d(TAG, "Photo saved to: $savedUri")
|
|
363
|
+
onPhotoSaved(savedUri)
|
|
364
|
+
} else {
|
|
365
|
+
Log.e(TAG, "Failed to save photo.")
|
|
366
|
+
onPhotoSaved(null)
|
|
183
367
|
}
|
|
184
|
-
|
|
368
|
+
} else {
|
|
369
|
+
Log.e(TAG, "PixelCopy failed with code $copyResult")
|
|
370
|
+
onPhotoSaved(null)
|
|
371
|
+
}
|
|
372
|
+
}, Handler(Looper.getMainLooper()))
|
|
373
|
+
}
|
|
185
374
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Save the [bitmap] into JPEG. Then embed an EXIF “UserComment” that includes the gravity vector.
|
|
377
|
+
*/
|
|
378
|
+
private fun saveBitmapWithExif(
|
|
379
|
+
bitmap: Bitmap,
|
|
380
|
+
gravity: Map<String, Double>
|
|
381
|
+
): Uri? {
|
|
382
|
+
val picturesDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: return null
|
|
383
|
+
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
|
|
384
|
+
val file = File(picturesDir, "AR_Capture_$timeStamp.jpg")
|
|
190
385
|
|
|
191
|
-
|
|
192
|
-
|
|
386
|
+
return try {
|
|
387
|
+
// Write JPEG
|
|
388
|
+
FileOutputStream(file).use { out ->
|
|
389
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
|
|
390
|
+
out.flush()
|
|
193
391
|
}
|
|
194
392
|
|
|
195
|
-
|
|
393
|
+
// Now embed EXIF metadata
|
|
394
|
+
val exif = ExifInterface(file.absolutePath)
|
|
395
|
+
// Convert gravity vector to JSON-like string
|
|
396
|
+
val gx = gravity["x"] ?: 0.0
|
|
397
|
+
val gy = gravity["y"] ?: 0.0
|
|
398
|
+
val gz = gravity["z"] ?: 0.0
|
|
399
|
+
|
|
400
|
+
// Example JSON
|
|
401
|
+
// { "gravity": { "x":0.123, "y":-0.456, "z":9.81 } }
|
|
402
|
+
val gravityJson = """{ "gravity": { "x": $gx, "y": $gy, "z": $gz } }"""
|
|
403
|
+
|
|
404
|
+
exif.setAttribute(ExifInterface.TAG_USER_COMMENT, gravityJson)
|
|
405
|
+
// Save EXIF
|
|
406
|
+
exif.saveAttributes()
|
|
407
|
+
|
|
408
|
+
Uri.fromFile(file)
|
|
409
|
+
} catch (e: Exception) {
|
|
410
|
+
Log.e(TAG, "Error saving or updating EXIF: ${e.message}")
|
|
411
|
+
null
|
|
196
412
|
}
|
|
197
413
|
}
|
|
198
414
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
415
|
+
// ------------------------------------------------------------------------
|
|
416
|
+
// Gravity Sensor => Replicates CMMotionManager
|
|
417
|
+
// ------------------------------------------------------------------------
|
|
418
|
+
override fun onSensorChanged(event: android.hardware.SensorEvent?) {
|
|
419
|
+
if (event?.sensor?.type == android.hardware.Sensor.TYPE_GRAVITY) {
|
|
420
|
+
gravityVector["x"] = event.values[0].toDouble()
|
|
421
|
+
gravityVector["y"] = event.values[1].toDouble()
|
|
422
|
+
gravityVector["z"] = event.values[2].toDouble()
|
|
423
|
+
Log.d(TAG, "Gravity vector => x=${gravityVector["x"]}, y=${gravityVector["y"]}, z=${gravityVector["z"]}")
|
|
204
424
|
}
|
|
205
425
|
}
|
|
206
426
|
|
|
207
|
-
fun
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
427
|
+
override fun onAccuracyChanged(sensor: android.hardware.Sensor?, accuracy: Int) {
|
|
428
|
+
// Not used
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ------------------------------------------------------------------------
|
|
432
|
+
// Reset Logic
|
|
433
|
+
// ------------------------------------------------------------------------
|
|
434
|
+
/**
|
|
435
|
+
* Resets the AR session, removing anchors and restoring default geometry.
|
|
436
|
+
*/
|
|
437
|
+
fun resetSession() {
|
|
438
|
+
anchorNode?.let { node ->
|
|
439
|
+
node.children.forEach { child ->
|
|
440
|
+
node.removeChild(child)
|
|
441
|
+
}
|
|
442
|
+
arFragment.arSceneView.scene.removeChild(node)
|
|
443
|
+
}
|
|
444
|
+
anchorNode = null
|
|
445
|
+
spheresModels.clear()
|
|
446
|
+
focusNode = null
|
|
447
|
+
|
|
448
|
+
// Reset booleans
|
|
449
|
+
for (i in upperSpheresSet.indices) {
|
|
450
|
+
upperSpheresSet[i] = false
|
|
451
|
+
lowerSpheresSet[i] = false
|
|
214
452
|
}
|
|
453
|
+
totalPhotosTaken = 0
|
|
454
|
+
photosFromDifferentAnglesTaken = 0
|
|
455
|
+
|
|
456
|
+
// Reset geometry
|
|
457
|
+
sphereRadius = DEFAULT_SPHERE_RADIUS
|
|
458
|
+
spheresRadius = DEFAULT_SPHERES_RADIUS
|
|
459
|
+
sphereAngle = ANGLE_INCREMENT_DEGREES
|
|
460
|
+
spheresHeight = DEFAULT_SPHERES_HEIGHT
|
|
461
|
+
distanceBetweenCircles = DEFAULT_DISTANCE_BETWEEN_CIRCLES
|
|
462
|
+
circleInFocus = 0
|
|
463
|
+
dragSpeed = DEFAULT_DRAG_SPEED
|
|
464
|
+
|
|
465
|
+
// If you want to fully re-init the AR session, you could do:
|
|
466
|
+
// arFragment.arSceneView.session?.close()
|
|
467
|
+
// setupArFragment()
|
|
468
|
+
// But that may require re-attaching the fragment, so it depends on your usage.
|
|
215
469
|
}
|
|
216
470
|
}
|
|
@@ -8,11 +8,11 @@ import com.facebook.react.uimanager.SimpleViewManager
|
|
|
8
8
|
import com.facebook.react.uimanager.ThemedReactContext
|
|
9
9
|
import com.facebook.react.uimanager.annotations.ReactProp
|
|
10
10
|
|
|
11
|
-
class ReplateCameraViewManager : SimpleViewManager<
|
|
11
|
+
class ReplateCameraViewManager : SimpleViewManager<ReplateCameraView>() {
|
|
12
12
|
override fun getName() = "ReplateCameraView"
|
|
13
13
|
|
|
14
14
|
@RequiresApi(Build.VERSION_CODES.N)
|
|
15
|
-
override fun createViewInstance(reactContext: ThemedReactContext):
|
|
15
|
+
override fun createViewInstance(reactContext: ThemedReactContext): ReplateCameraView {
|
|
16
16
|
return ReplateCameraView(reactContext)
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -20,4 +20,5 @@ class ReplateCameraViewManager : SimpleViewManager<View>() {
|
|
|
20
20
|
fun setColor(view: View, color: String) {
|
|
21
21
|
view.setBackgroundColor(Color.parseColor(color))
|
|
22
22
|
}
|
|
23
|
+
|
|
23
24
|
}
|
|
@@ -704,8 +704,8 @@ class ReplateCameraController: NSObject {
|
|
|
704
704
|
private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
|
|
705
705
|
|
|
706
706
|
// Configuration Constants
|
|
707
|
-
private static let MIN_DISTANCE: Float = 0.
|
|
708
|
-
private static let MAX_DISTANCE: Float = 0.
|
|
707
|
+
private static let MIN_DISTANCE: Float = 0.25
|
|
708
|
+
private static let MAX_DISTANCE: Float = 0.55
|
|
709
709
|
private static let ANGLE_THRESHOLD: Float = 0.6
|
|
710
710
|
// private static let TARGET_IMAGE_SIZE = CGSize(width: 2048, height: 1556)
|
|
711
711
|
private static let TARGET_IMAGE_SIZE = CGSize(width: 3072, height: 2304)
|