replate-camera 0.3.2 → 0.4.1
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/ios/ReplateCameraViewManager.swift +935 -774
- package/package.json +1 -1
|
@@ -5,115 +5,216 @@ import AVFoundation
|
|
|
5
5
|
import ImageIO
|
|
6
6
|
import MobileCoreServices
|
|
7
7
|
|
|
8
|
+
// MARK: - RCTViewManager
|
|
8
9
|
@objc(ReplateCameraViewManager)
|
|
9
10
|
class ReplateCameraViewManager: RCTViewManager {
|
|
11
|
+
override func view() -> ReplateCameraView {
|
|
12
|
+
return ReplateCameraView()
|
|
13
|
+
}
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
@objc override static func requiresMainQueueSetup() -> Bool {
|
|
17
|
-
return false
|
|
18
|
-
}
|
|
19
|
-
|
|
15
|
+
@objc override static func requiresMainQueueSetup() -> Bool {
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
20
18
|
}
|
|
21
19
|
|
|
20
|
+
// MARK: - UIImage Extension
|
|
22
21
|
extension UIImage {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
22
|
+
func rotate(radians: Float) -> UIImage {
|
|
23
|
+
var newSize = CGRect(origin: .zero, size: self.size)
|
|
24
|
+
.applying(CGAffineTransform(rotationAngle: CGFloat(radians))).size
|
|
25
|
+
newSize.width = floor(newSize.width)
|
|
26
|
+
newSize.height = floor(newSize.height)
|
|
27
|
+
|
|
28
|
+
UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
|
|
29
|
+
guard let context = UIGraphicsGetCurrentContext() else { return self }
|
|
30
|
+
|
|
31
|
+
context.translateBy(x: newSize.width / 2, y: newSize.height / 2)
|
|
32
|
+
context.rotate(by: CGFloat(radians))
|
|
33
|
+
self.draw(in: CGRect(x: -self.size.width / 2,
|
|
34
|
+
y: -self.size.height / 2,
|
|
35
|
+
width: self.size.width,
|
|
36
|
+
height: self.size.height))
|
|
37
|
+
|
|
38
|
+
let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
|
|
39
|
+
UIGraphicsEndImageContext()
|
|
40
|
+
|
|
41
|
+
return newImage
|
|
42
|
+
}
|
|
44
43
|
}
|
|
45
44
|
|
|
45
|
+
// MARK: - ReplateCameraView
|
|
46
46
|
class ReplateCameraView: UIView, ARSessionDelegate {
|
|
47
|
+
// MARK: - Static Properties
|
|
48
|
+
private static let lock = NSLock()
|
|
49
|
+
private static let arQueue = DispatchQueue(label: "com.replate.ar", qos: .userInteractive)
|
|
47
50
|
|
|
51
|
+
// AR View and Core Components
|
|
48
52
|
static var arView: ARView!
|
|
49
|
-
static var anchorEntity: AnchorEntity?
|
|
53
|
+
static var anchorEntity: AnchorEntity?
|
|
50
54
|
static var model: Entity!
|
|
51
|
-
static var
|
|
52
|
-
static var upperSpheresSet: [Bool] = [Bool](repeating: false, count: 72)
|
|
53
|
-
static var lowerSpheresSet: [Bool] = [Bool](repeating: false, count: 72)
|
|
54
|
-
static var totalPhotosTaken: Int = 0
|
|
55
|
-
static var photosFromDifferentAnglesTaken = 0
|
|
55
|
+
static var sessionId: UUID!
|
|
56
56
|
static var INSTANCE: ReplateCameraView!
|
|
57
|
+
|
|
58
|
+
// Scene Configuration
|
|
59
|
+
static var width = CGFloat(0)
|
|
60
|
+
static var height = CGFloat(0)
|
|
61
|
+
static var isPaused = false
|
|
62
|
+
|
|
63
|
+
// Sphere Properties
|
|
64
|
+
static var spheresModels: [ModelEntity] = []
|
|
65
|
+
static var upperSpheresSet = [Bool](repeating: false, count: 72)
|
|
66
|
+
static var lowerSpheresSet = [Bool](repeating: false, count: 72)
|
|
57
67
|
static var sphereRadius = Float(0.004)
|
|
58
68
|
static var spheresRadius = Float(0.13)
|
|
59
69
|
static var sphereAngle = Float(5)
|
|
60
70
|
static var spheresHeight = Float(0.10)
|
|
61
|
-
static var dragSpeed = CGFloat(7000)
|
|
62
|
-
static var isPaused = false
|
|
63
|
-
static var sessionId: UUID!
|
|
64
|
-
static var focusModel: Entity!
|
|
65
71
|
static var distanceBetweenCircles = Float(0.10)
|
|
66
|
-
|
|
72
|
+
|
|
73
|
+
// Focus and Navigation
|
|
74
|
+
static var focusModel: Entity!
|
|
75
|
+
static var circleInFocus = 0
|
|
76
|
+
static var dragSpeed = CGFloat(7000)
|
|
67
77
|
static var dotAnchors: [AnchorEntity] = []
|
|
68
|
-
static var width = CGFloat(0)
|
|
69
|
-
static var height = CGFloat(0)
|
|
70
|
-
private var isResetting = false // Flag to prevent infinite loop
|
|
71
|
-
private let resetSemaphore = DispatchSemaphore(value: 1) // Semaphore for synchronization
|
|
72
78
|
|
|
79
|
+
// Statistics
|
|
80
|
+
static var totalPhotosTaken = 0
|
|
81
|
+
static var photosFromDifferentAnglesTaken = 0
|
|
73
82
|
|
|
83
|
+
// Thread Safety
|
|
84
|
+
private static var isResetting = false
|
|
85
|
+
|
|
86
|
+
// MARK: - Initialization
|
|
74
87
|
override init(frame: CGRect) {
|
|
75
88
|
super.init(frame: frame)
|
|
76
|
-
|
|
77
|
-
// setupAR()
|
|
78
|
-
ReplateCameraView.INSTANCE = self
|
|
89
|
+
setupInitialState()
|
|
79
90
|
}
|
|
80
91
|
|
|
81
92
|
required init?(coder: NSCoder) {
|
|
82
93
|
super.init(coder: coder)
|
|
94
|
+
setupInitialState()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func setupInitialState() {
|
|
83
98
|
requestCameraPermissions()
|
|
84
99
|
ReplateCameraView.INSTANCE = self
|
|
85
|
-
// setupAR()
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
ReplateCameraView.arView.addGestureRecognizer(panGestureRecognizer)
|
|
94
|
-
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: ReplateCameraView.INSTANCE, action: #selector(ReplateCameraView.INSTANCE.handlePinch(_:)))
|
|
95
|
-
ReplateCameraView.arView.addGestureRecognizer(pinchGestureRecognizer)
|
|
96
|
-
}
|
|
102
|
+
// MARK: - Layout
|
|
103
|
+
override func layoutSubviews() {
|
|
104
|
+
super.layoutSubviews()
|
|
105
|
+
Self.lock.lock()
|
|
106
|
+
defer { Self.lock.unlock() }
|
|
97
107
|
|
|
98
|
-
|
|
108
|
+
ReplateCameraView.width = frame.width
|
|
109
|
+
ReplateCameraView.height = frame.height
|
|
110
|
+
}
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
// MARK: - AR Setup and Configuration
|
|
113
|
+
static func setupAR() {
|
|
114
|
+
arQueue.async {
|
|
115
|
+
guard let instance = INSTANCE else { return }
|
|
116
|
+
|
|
117
|
+
let configuration = ARWorldTrackingConfiguration()
|
|
118
|
+
configuration.isLightEstimationEnabled = true
|
|
119
|
+
configuration.planeDetection = .horizontal
|
|
120
|
+
|
|
121
|
+
DispatchQueue.main.async {
|
|
122
|
+
configureARView(configuration)
|
|
123
|
+
addRecognizers()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private static func configureARView(_ configuration: ARWorldTrackingConfiguration) {
|
|
129
|
+
configureRenderOptions()
|
|
130
|
+
configureVideoFormat(configuration)
|
|
131
|
+
|
|
132
|
+
arView.session.run(configuration)
|
|
133
|
+
arView.addCoaching()
|
|
134
|
+
sessionId = arView.session.identifier
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private static func configureRenderOptions() {
|
|
138
|
+
let renderOptions: [ARView.RenderOptions] = [
|
|
139
|
+
.disableMotionBlur,
|
|
140
|
+
.disableCameraGrain,
|
|
141
|
+
.disableAREnvironmentLighting,
|
|
142
|
+
.disableHDR,
|
|
143
|
+
.disableFaceMesh,
|
|
144
|
+
.disableGroundingShadows,
|
|
145
|
+
.disablePersonOcclusion
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
renderOptions.forEach { arView.renderOptions.insert($0) }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private static func configureVideoFormat(_ configuration: ARWorldTrackingConfiguration) {
|
|
152
|
+
if #available(iOS 16.0, *) {
|
|
153
|
+
configuration.videoFormat = ARWorldTrackingConfiguration.recommendedVideoFormatForHighResolutionFrameCapturing
|
|
154
|
+
?? ARWorldTrackingConfiguration.recommendedVideoFormatFor4KResolution
|
|
155
|
+
?? highestResolutionFormat()
|
|
106
156
|
} else {
|
|
107
|
-
|
|
157
|
+
configuration.videoFormat = highestResolutionFormat()
|
|
108
158
|
}
|
|
109
|
-
})
|
|
110
159
|
}
|
|
111
|
-
}
|
|
112
160
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
161
|
+
private static func highestResolutionFormat() -> ARConfiguration.VideoFormat {
|
|
162
|
+
return ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
|
|
163
|
+
let resolution1 = format1.imageResolution.width * format1.imageResolution.height
|
|
164
|
+
let resolution2 = format2.imageResolution.width * format2.imageResolution.height
|
|
165
|
+
return resolution1 < resolution2
|
|
166
|
+
})!
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// MARK: - Gesture Recognition
|
|
170
|
+
static func addRecognizers() {
|
|
171
|
+
guard let instance = INSTANCE else { return }
|
|
172
|
+
|
|
173
|
+
let recognizers = [
|
|
174
|
+
UITapGestureRecognizer(target: instance, action: #selector(instance.viewTapped)),
|
|
175
|
+
UIPanGestureRecognizer(target: instance, action: #selector(instance.handlePan)),
|
|
176
|
+
UIPinchGestureRecognizer(target: instance, action: #selector(instance.handlePinch))
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
recognizers.forEach { arView.addGestureRecognizer($0) }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@objc func viewTapped(_ recognizer: UITapGestureRecognizer) {
|
|
183
|
+
print("VIEW TAPPED")
|
|
184
|
+
let tapLocation: CGPoint = recognizer.location(in: ReplateCameraView.arView)
|
|
185
|
+
let estimatedPlane: ARRaycastQuery.Target = .estimatedPlane
|
|
186
|
+
let alignment: ARRaycastQuery.TargetAlignment = .horizontal
|
|
187
|
+
|
|
188
|
+
let result: [ARRaycastResult] = ReplateCameraView.arView.raycast(from: tapLocation,
|
|
189
|
+
allowing: estimatedPlane,
|
|
190
|
+
alignment: alignment)
|
|
191
|
+
|
|
192
|
+
guard let rayCast: ARRaycastResult = result.first
|
|
193
|
+
else {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
let anchor = AnchorEntity(world: rayCast.worldTransform)
|
|
197
|
+
print("ANCHOR FOUND\n", anchor.transform)
|
|
198
|
+
let callback = ReplateCameraController.anchorSetCallback
|
|
199
|
+
if (callback != nil) {
|
|
200
|
+
callback!([])
|
|
201
|
+
ReplateCameraController.anchorSetCallback = nil
|
|
202
|
+
}
|
|
203
|
+
if (ReplateCameraView.model == nil && ReplateCameraView.anchorEntity == nil) {
|
|
204
|
+
DispatchQueue.main.async{
|
|
205
|
+
for dot in ReplateCameraView.dotAnchors {
|
|
206
|
+
dot.removeFromParent()
|
|
207
|
+
ReplateCameraView.arView.scene.removeAnchor(dot)
|
|
208
|
+
}
|
|
209
|
+
ReplateCameraView.dotAnchors = []
|
|
210
|
+
}
|
|
211
|
+
ReplateCameraView.anchorEntity = anchor
|
|
212
|
+
createSpheres(y: ReplateCameraView.spheresHeight)
|
|
213
|
+
createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
|
|
214
|
+
createFocusSphere()
|
|
215
|
+
guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
|
|
216
|
+
ReplateCameraView.arView.scene.anchors.append(anchorEntity)
|
|
217
|
+
}
|
|
117
218
|
}
|
|
118
219
|
|
|
119
220
|
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
|
@@ -220,6 +321,82 @@ class ReplateCameraView: UIView, ARSessionDelegate {
|
|
|
220
321
|
}
|
|
221
322
|
}
|
|
222
323
|
|
|
324
|
+
// MARK: - Entity Management
|
|
325
|
+
|
|
326
|
+
func createFocusSphere() {
|
|
327
|
+
DispatchQueue.main.async {
|
|
328
|
+
let sphereRadius = ReplateCameraView.sphereRadius * 1.5
|
|
329
|
+
|
|
330
|
+
// Generate the first sphere mesh
|
|
331
|
+
let sphereMesh1 = MeshResource.generateSphere(radius: sphereRadius)
|
|
332
|
+
|
|
333
|
+
// Create the first sphere entity with initial material
|
|
334
|
+
let sphereEntity1 = ModelEntity(mesh: sphereMesh1, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
|
|
335
|
+
|
|
336
|
+
// Set the position for the first sphere entity
|
|
337
|
+
sphereEntity1.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight, z: 0)
|
|
338
|
+
|
|
339
|
+
// Generate the second sphere mesh
|
|
340
|
+
let sphereMesh2 = MeshResource.generateSphere(radius: sphereRadius)
|
|
341
|
+
|
|
342
|
+
// Create the second sphere entity with initial material
|
|
343
|
+
let sphereEntity2 = ModelEntity(mesh: sphereMesh2, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
|
|
344
|
+
|
|
345
|
+
// Set the position for the second sphere entity
|
|
346
|
+
sphereEntity2.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight + ReplateCameraView.distanceBetweenCircles, z: 0)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
// Update the material of the sphere entities
|
|
350
|
+
sphereEntity1.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
|
|
351
|
+
sphereEntity2.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
|
|
352
|
+
|
|
353
|
+
let baseOverlayEntity = self.loadModel(named: "center.obj")
|
|
354
|
+
baseOverlayEntity.scale *= 12
|
|
355
|
+
baseOverlayEntity.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.3), roughness: 1, isMetallic: false),
|
|
356
|
+
SimpleMaterial(color: .white.withAlphaComponent(0.7), roughness: 1, isMetallic: false),
|
|
357
|
+
SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)]
|
|
358
|
+
|
|
359
|
+
// Create a parent entity to hold both spheres
|
|
360
|
+
let parentEntity = Entity()
|
|
361
|
+
parentEntity.addChild(sphereEntity1)
|
|
362
|
+
parentEntity.addChild(sphereEntity2)
|
|
363
|
+
parentEntity.addChild(baseOverlayEntity)
|
|
364
|
+
|
|
365
|
+
// Set the focus model for the global state
|
|
366
|
+
ReplateCameraView.focusModel = parentEntity
|
|
367
|
+
|
|
368
|
+
// Safely add the parent entity to the anchor entity
|
|
369
|
+
ReplateCameraView.anchorEntity?.addChild(parentEntity)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func createSpheres(y: Float) {
|
|
374
|
+
DispatchQueue.main.async {
|
|
375
|
+
let radius = ReplateCameraView.spheresRadius
|
|
376
|
+
|
|
377
|
+
for i in 0..<72 {
|
|
378
|
+
let angle = Float(i) * (Float.pi / 180) * 5
|
|
379
|
+
let position = SIMD3(
|
|
380
|
+
radius * cos(angle),
|
|
381
|
+
y,
|
|
382
|
+
radius * sin(angle)
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
let sphere = self.createSphere(position: position)
|
|
386
|
+
ReplateCameraView.spheresModels.append(sphere)
|
|
387
|
+
ReplateCameraView.anchorEntity?.addChild(sphere)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
func createSphere(position: SIMD3<Float>) -> ModelEntity {
|
|
393
|
+
let sphereMesh = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius)
|
|
394
|
+
let material = SimpleMaterial(color: .white.withAlphaComponent(1), roughness: 1, isMetallic: false)
|
|
395
|
+
let sphere = ModelEntity(mesh: sphereMesh, materials: [material])
|
|
396
|
+
sphere.position = position
|
|
397
|
+
return sphere
|
|
398
|
+
}
|
|
399
|
+
|
|
223
400
|
func addDots(to planeAnchor: ARPlaneAnchor) {
|
|
224
401
|
print("Adding dots to plane anchor") // Debugging line
|
|
225
402
|
let center = planeAnchor.center
|
|
@@ -268,213 +445,85 @@ class ReplateCameraView: UIView, ARSessionDelegate {
|
|
|
268
445
|
return circleEntity
|
|
269
446
|
}
|
|
270
447
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
let baseOverlayMesh = MeshResource.generateBox(size: [ReplateCameraView.spheresRadius * 2, 0.01, ReplateCameraView.spheresRadius * 2], cornerRadius: 1)
|
|
277
|
-
let baseOverlayEntity = ModelEntity(mesh: baseOverlayMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)])
|
|
278
|
-
|
|
279
|
-
baseOverlayEntity.position = SIMD3(x: 0, y: 0.01, z: 0)
|
|
280
|
-
return baseOverlayEntity
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
@objc private func viewTapped(_ recognizer: UITapGestureRecognizer) {
|
|
285
|
-
print("VIEW TAPPED")
|
|
286
|
-
let tapLocation: CGPoint = recognizer.location(in: ReplateCameraView.arView)
|
|
287
|
-
let estimatedPlane: ARRaycastQuery.Target = .estimatedPlane
|
|
288
|
-
let alignment: ARRaycastQuery.TargetAlignment = .horizontal
|
|
448
|
+
// MARK: - Reset Functionality
|
|
449
|
+
@objc static func reset() {
|
|
450
|
+
arQueue.async {
|
|
451
|
+
Self.lock.lock()
|
|
452
|
+
defer { Self.lock.unlock() }
|
|
289
453
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
alignment: alignment)
|
|
454
|
+
if isResetting { return }
|
|
455
|
+
isResetting = true
|
|
293
456
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
// anchor.transform.rotation = simd_quatf(ix: 0, iy: 0, iz: 0, r: 1)
|
|
301
|
-
print("ANCHOR FOUND\n", anchor.transform)
|
|
302
|
-
let callback = ReplateCameraController.anchorSetCallback
|
|
303
|
-
if (callback != nil) {
|
|
304
|
-
callback!([])
|
|
305
|
-
ReplateCameraController.anchorSetCallback = nil
|
|
306
|
-
}
|
|
307
|
-
if (ReplateCameraView.model == nil && ReplateCameraView.anchorEntity == nil) {
|
|
308
|
-
DispatchQueue.main.async{
|
|
309
|
-
for dot in ReplateCameraView.dotAnchors {
|
|
310
|
-
dot.removeFromParent()
|
|
311
|
-
ReplateCameraView.arView.scene.removeAnchor(dot)
|
|
457
|
+
DispatchQueue.main.async {
|
|
458
|
+
tearDownARSession()
|
|
459
|
+
resetProperties()
|
|
460
|
+
setupNewARView()
|
|
461
|
+
isResetting = false
|
|
462
|
+
}
|
|
312
463
|
}
|
|
313
|
-
ReplateCameraView.dotAnchors = []
|
|
314
|
-
}
|
|
315
|
-
ReplateCameraView.anchorEntity = anchor
|
|
316
|
-
createSpheres(y: ReplateCameraView.spheresHeight)
|
|
317
|
-
createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
|
|
318
|
-
createFocusSphere()
|
|
319
|
-
|
|
320
|
-
//DEBUG MESHES
|
|
321
|
-
// let xAxis = MeshResource.generateBox(width: 2, height: 0.001, depth: 0.01)
|
|
322
|
-
// let xLineEntity = ModelEntity(mesh: xAxis)
|
|
323
|
-
// xLineEntity.setPosition(SIMD3<Float>(0, 0, 0), relativeTo: ReplateCameraView.anchorEntity)
|
|
324
|
-
// xLineEntity.model?.materials = [SimpleMaterial(color: .red, isMetallic: false)]
|
|
325
|
-
// ReplateCameraView.anchorEntity.addChild(xLineEntity)
|
|
326
|
-
// let xLineLeftSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
|
|
327
|
-
// let xLineLeftSphereEntity = ModelEntity(mesh: xLineLeftSphere)
|
|
328
|
-
// ReplateCameraView.anchorEntity.addChild(xLineLeftSphereEntity)
|
|
329
|
-
// xLineLeftSphereEntity.setPosition(SIMD3<Float>(-1, 0.002, 0), relativeTo: ReplateCameraView.anchorEntity)
|
|
330
|
-
// xLineLeftSphereEntity.model?.materials = [SimpleMaterial(color: .red, isMetallic: false)]
|
|
331
|
-
// let xLineRightSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
|
|
332
|
-
// let xLineRightSphereEntity = ModelEntity(mesh: xLineRightSphere)
|
|
333
|
-
// ReplateCameraView.anchorEntity.addChild(xLineRightSphereEntity)
|
|
334
|
-
// xLineRightSphereEntity.setPosition(SIMD3<Float>(1, 0.002, 0), relativeTo: ReplateCameraView.anchorEntity)
|
|
335
|
-
// xLineRightSphereEntity.model?.materials = [SimpleMaterial(color: .yellow, isMetallic: false)]
|
|
336
|
-
// let yAxis = MeshResource.generateBox(width: 0.01, height: 0.001, depth: 2)
|
|
337
|
-
// let yLineEntity = ModelEntity(mesh: yAxis)
|
|
338
|
-
// yLineEntity.setPosition(SIMD3<Float>(0, 0, 0), relativeTo: ReplateCameraView.anchorEntity)
|
|
339
|
-
// let yLineLeftSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
|
|
340
|
-
// let yLineLeftSphereEntity = ModelEntity(mesh: yLineLeftSphere)
|
|
341
|
-
// ReplateCameraView.anchorEntity.addChild(yLineLeftSphereEntity)
|
|
342
|
-
// yLineLeftSphereEntity.setPosition(SIMD3<Float>(0, 0.002, -1), relativeTo: ReplateCameraView.anchorEntity)
|
|
343
|
-
// yLineLeftSphereEntity.model?.materials = [SimpleMaterial(color: .systemPink, isMetallic: false)]
|
|
344
|
-
// let yLineRightSphere = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius * 5)
|
|
345
|
-
// let yLineRightSphereEntity = ModelEntity(mesh: yLineRightSphere)
|
|
346
|
-
// ReplateCameraView.anchorEntity.addChild(yLineRightSphereEntity)
|
|
347
|
-
// yLineRightSphereEntity.setPosition(SIMD3<Float>(0, 0.002, 1), relativeTo: ReplateCameraView.anchorEntity)
|
|
348
|
-
// yLineRightSphereEntity.model?.materials = [SimpleMaterial(color: .orange, isMetallic: false)]
|
|
349
|
-
// yLineEntity.model?.materials = [SimpleMaterial(color: .purple, isMetallic: false)]
|
|
350
|
-
// ReplateCameraView.anchorEntity.addChild(yLineEntity)
|
|
351
|
-
// let circleEntity = ModelEntity(mesh: MeshResource.generateBox(size: 2, cornerRadius: 1))
|
|
352
|
-
// circleEntity.setPosition(SIMD3<Float>(0, 0, 0), relativeTo: ReplateCameraView.anchorEntity)
|
|
353
|
-
// circleEntity.model?.materials = [SimpleMaterial(color: .yellow, isMetallic: false)]
|
|
354
|
-
|
|
355
|
-
guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
|
|
356
|
-
ReplateCameraView.arView.scene.anchors.append(anchorEntity)
|
|
357
464
|
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
func createFocusSphere() {
|
|
361
|
-
DispatchQueue.main.async {
|
|
362
|
-
let sphereRadius = ReplateCameraView.sphereRadius * 1.5
|
|
363
|
-
|
|
364
|
-
// Generate the first sphere mesh
|
|
365
|
-
let sphereMesh1 = MeshResource.generateSphere(radius: sphereRadius)
|
|
366
|
-
|
|
367
|
-
// Create the first sphere entity with initial material
|
|
368
|
-
let sphereEntity1 = ModelEntity(mesh: sphereMesh1, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
|
|
369
|
-
|
|
370
|
-
// Set the position for the first sphere entity
|
|
371
|
-
sphereEntity1.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight, z: 0)
|
|
372
|
-
|
|
373
|
-
// Generate the second sphere mesh
|
|
374
|
-
let sphereMesh2 = MeshResource.generateSphere(radius: sphereRadius)
|
|
375
|
-
|
|
376
|
-
// Create the second sphere entity with initial material
|
|
377
|
-
let sphereEntity2 = ModelEntity(mesh: sphereMesh2, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
|
|
378
|
-
|
|
379
|
-
// Set the position for the second sphere entity
|
|
380
|
-
sphereEntity2.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight + ReplateCameraView.distanceBetweenCircles, z: 0)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
// Update the material of the sphere entities
|
|
384
|
-
sphereEntity1.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
|
|
385
|
-
sphereEntity2.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
|
|
386
|
-
|
|
387
|
-
let baseOverlayEntity = self.loadModel(named: "center.obj")
|
|
388
|
-
baseOverlayEntity.scale *= 12
|
|
389
|
-
baseOverlayEntity.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.3), roughness: 1, isMetallic: false),
|
|
390
|
-
SimpleMaterial(color: .white.withAlphaComponent(0.7), roughness: 1, isMetallic: false),
|
|
391
|
-
SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)]
|
|
392
|
-
|
|
393
|
-
// Create a parent entity to hold both spheres
|
|
394
|
-
let parentEntity = Entity()
|
|
395
|
-
parentEntity.addChild(sphereEntity1)
|
|
396
|
-
parentEntity.addChild(sphereEntity2)
|
|
397
|
-
parentEntity.addChild(baseOverlayEntity)
|
|
398
465
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
466
|
+
private static func tearDownARSession() {
|
|
467
|
+
arView?.session.pause()
|
|
468
|
+
arView?.session.delegate = nil
|
|
469
|
+
arView?.scene.anchors.removeAll()
|
|
470
|
+
arView?.removeFromSuperview()
|
|
471
|
+
arView?.window?.resignKey()
|
|
404
472
|
}
|
|
405
|
-
}
|
|
406
473
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
474
|
+
private static func resetProperties() {
|
|
475
|
+
anchorEntity = nil
|
|
476
|
+
model = nil
|
|
477
|
+
spheresModels.removeAll()
|
|
478
|
+
upperSpheresSet = [Bool](repeating: false, count: 72)
|
|
479
|
+
lowerSpheresSet = [Bool](repeating: false, count: 72)
|
|
480
|
+
totalPhotosTaken = 0
|
|
481
|
+
photosFromDifferentAnglesTaken = 0
|
|
482
|
+
sphereRadius = 0.004
|
|
483
|
+
spheresRadius = 0.13
|
|
484
|
+
sphereAngle = 5
|
|
485
|
+
spheresHeight = 0.10
|
|
486
|
+
dragSpeed = 7000
|
|
487
|
+
dotAnchors.removeAll()
|
|
413
488
|
}
|
|
414
489
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
// Create sphere entity with the specified material
|
|
419
|
-
let sphereEntity = ModelEntity(mesh: sphereMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(1), roughness: 1, isMetallic: false)])
|
|
490
|
+
private static func setupNewARView() {
|
|
491
|
+
guard let instance = INSTANCE else { return }
|
|
420
492
|
|
|
421
|
-
|
|
422
|
-
|
|
493
|
+
arView = ARView(frame: CGRect(x: 0, y: 0, width: width, height: height))
|
|
494
|
+
arView.backgroundColor = instance.hexStringToUIColor(hexColor: "#32a852")
|
|
495
|
+
instance.addSubview(arView)
|
|
496
|
+
arView.session.delegate = instance
|
|
497
|
+
setupAR()
|
|
498
|
+
}
|
|
423
499
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
}
|
|
500
|
+
// MARK: - Utilities functions
|
|
501
|
+
func requestCameraPermissions() {
|
|
427
502
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
// let material = SimpleMaterial(color: .purple, isMetallic: true)
|
|
439
|
-
// sphereEntity.model?.materials[0] = material
|
|
440
|
-
// }
|
|
441
|
-
ReplateCameraView.spheresModels.append(sphereEntity)
|
|
442
|
-
ReplateCameraView.anchorEntity?.addChild(sphereEntity)
|
|
503
|
+
if AVCaptureDevice.authorizationStatus(for: .video) == .authorized {
|
|
504
|
+
print("Camera permissions already granted")
|
|
505
|
+
} else {
|
|
506
|
+
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
|
|
507
|
+
if granted {
|
|
508
|
+
print("Camera permissions granted")
|
|
509
|
+
} else {
|
|
510
|
+
print("Camera permissions denied")
|
|
511
|
+
}
|
|
512
|
+
})
|
|
443
513
|
}
|
|
444
514
|
}
|
|
445
515
|
|
|
446
|
-
func
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
ReplateCameraView.arView.renderOptions.insert(ARView.RenderOptions.disablePersonOcclusion)
|
|
458
|
-
configuration.planeDetection = ARWorldTrackingConfiguration.PlaneDetection.horizontal
|
|
459
|
-
if #available(iOS 16.0, *) {
|
|
460
|
-
print("recommendedVideoFormatForHighResolutionFrameCapturing")
|
|
461
|
-
configuration.videoFormat = ARWorldTrackingConfiguration.recommendedVideoFormatForHighResolutionFrameCapturing ?? ARWorldTrackingConfiguration.recommendedVideoFormatFor4KResolution ?? ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
|
|
462
|
-
let resolution1 = format1.imageResolution.width * format1.imageResolution.height
|
|
463
|
-
let resolution2 = format2.imageResolution.width * format2.imageResolution.height
|
|
464
|
-
return resolution1 < resolution2
|
|
465
|
-
})!
|
|
466
|
-
} else {
|
|
467
|
-
print("Alternative high resolution method")
|
|
468
|
-
let maxResolutionFormat = ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
|
|
469
|
-
let resolution1 = format1.imageResolution.width * format1.imageResolution.height
|
|
470
|
-
let resolution2 = format2.imageResolution.width * format2.imageResolution.height
|
|
471
|
-
return resolution1 < resolution2
|
|
472
|
-
})!
|
|
473
|
-
configuration.videoFormat = maxResolutionFormat
|
|
474
|
-
}
|
|
475
|
-
ReplateCameraView.arView.session.run(configuration)
|
|
476
|
-
ReplateCameraView.arView.addCoaching()
|
|
477
|
-
ReplateCameraView.sessionId = ReplateCameraView.arView.session.identifier
|
|
516
|
+
func loadModel(named name: String) -> ModelEntity {
|
|
517
|
+
do{
|
|
518
|
+
return try ModelEntity.loadModel(named: name)
|
|
519
|
+
}catch{
|
|
520
|
+
print("Cannot load model \(name)")
|
|
521
|
+
let baseOverlayMesh = MeshResource.generateBox(size: [ReplateCameraView.spheresRadius * 2, 0.01, ReplateCameraView.spheresRadius * 2], cornerRadius: 1)
|
|
522
|
+
let baseOverlayEntity = ModelEntity(mesh: baseOverlayMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)])
|
|
523
|
+
|
|
524
|
+
baseOverlayEntity.position = SIMD3(x: 0, y: 0.01, z: 0)
|
|
525
|
+
return baseOverlayEntity
|
|
526
|
+
}
|
|
478
527
|
}
|
|
479
528
|
|
|
480
529
|
@objc var color: String = "" {
|
|
@@ -524,73 +573,7 @@ class ReplateCameraView: UIView, ARSessionDelegate {
|
|
|
524
573
|
print("SESSION RESUMED")
|
|
525
574
|
}
|
|
526
575
|
|
|
527
|
-
func
|
|
528
|
-
DispatchQueue.global().async {
|
|
529
|
-
print("Entering reset. Waiting for semaphore.")
|
|
530
|
-
self.resetSemaphore.wait() // Wait for the semaphore
|
|
531
|
-
print("Obtained semaphore.")
|
|
532
|
-
if self.isResetting {
|
|
533
|
-
print("Signaling semaphore because reset is already in progress.")
|
|
534
|
-
self.resetSemaphore.signal() // Release the semaphore
|
|
535
|
-
return
|
|
536
|
-
}
|
|
537
|
-
self.isResetting = true
|
|
538
|
-
self.resetSemaphore.signal() // Release the semaphore
|
|
539
|
-
print("Resetting, releasing semaphore.")
|
|
540
|
-
|
|
541
|
-
if (!Thread.isMainThread) {
|
|
542
|
-
DispatchQueue.main.sync {
|
|
543
|
-
print("PROOOOOVA1")
|
|
544
|
-
// Pause the existing AR session
|
|
545
|
-
ReplateCameraView.arView?.session.pause()
|
|
546
|
-
ReplateCameraView.arView?.session.delegate = nil
|
|
547
|
-
ReplateCameraView.arView?.scene.anchors.removeAll()
|
|
548
|
-
|
|
549
|
-
// Perform UI updates and property resets on the main thread asynchronously
|
|
550
|
-
// Remove the existing ARView from the superview
|
|
551
|
-
ReplateCameraView.arView?.removeFromSuperview()
|
|
552
|
-
|
|
553
|
-
// Resign key window
|
|
554
|
-
ReplateCameraView.arView?.window?.resignKey()
|
|
555
|
-
print("PROOOOOOVA2")
|
|
556
|
-
|
|
557
|
-
// Reset the static properties
|
|
558
|
-
ReplateCameraView.anchorEntity = nil
|
|
559
|
-
ReplateCameraView.model = nil
|
|
560
|
-
ReplateCameraView.spheresModels = []
|
|
561
|
-
ReplateCameraView.upperSpheresSet = [Bool](repeating: false, count: 72)
|
|
562
|
-
ReplateCameraView.lowerSpheresSet = [Bool](repeating: false, count: 72)
|
|
563
|
-
ReplateCameraView.totalPhotosTaken = 0
|
|
564
|
-
ReplateCameraView.photosFromDifferentAnglesTaken = 0
|
|
565
|
-
ReplateCameraView.sphereRadius = Float(0.004)
|
|
566
|
-
ReplateCameraView.spheresRadius = Float(0.13)
|
|
567
|
-
ReplateCameraView.sphereAngle = Float(5)
|
|
568
|
-
ReplateCameraView.spheresHeight = Float(0.10)
|
|
569
|
-
ReplateCameraView.dragSpeed = CGFloat(7000)
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
ReplateCameraView.arView = ARView(frame: CGRect(x: 0, y: 0, width: ReplateCameraView.width, height: ReplateCameraView.height))
|
|
573
|
-
ReplateCameraView.arView.backgroundColor = self.hexStringToUIColor(hexColor: "#32a852")
|
|
574
|
-
print("PROOOOOOVA3")
|
|
575
|
-
|
|
576
|
-
// Add the new ARView to the view hierarchy
|
|
577
|
-
self.addSubview(ReplateCameraView.arView)
|
|
578
|
-
|
|
579
|
-
// Set the session delegate and run the session
|
|
580
|
-
ReplateCameraView.arView?.session.delegate = self
|
|
581
|
-
print("PROOOOOOVA")
|
|
582
|
-
self.setupAR()
|
|
583
|
-
|
|
584
|
-
self.resetSemaphore.wait() // Wait for the semaphore
|
|
585
|
-
self.isResetting = false
|
|
586
|
-
self.resetSemaphore.signal() // Release the semaphore
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
static func generateImpactFeedback(strength: UIImpactFeedbackGenerator.FeedbackStyle) {
|
|
576
|
+
func generateImpactFeedback(strength: UIImpactFeedbackGenerator.FeedbackStyle) {
|
|
594
577
|
do{
|
|
595
578
|
let impactFeedbackGenerator = try UIImpactFeedbackGenerator(style: strength)
|
|
596
579
|
impactFeedbackGenerator.prepare()
|
|
@@ -602,553 +585,733 @@ class ReplateCameraView: UIView, ARSessionDelegate {
|
|
|
602
585
|
|
|
603
586
|
}
|
|
604
587
|
|
|
605
|
-
@objc(ReplateCameraController)
|
|
606
|
-
class ReplateCameraController: NSObject {
|
|
607
|
-
|
|
608
|
-
static var completedTutorialCallback: RCTResponseSenderBlock?
|
|
609
|
-
static var anchorSetCallback: RCTResponseSenderBlock?
|
|
610
|
-
static var completedUpperSpheresCallback: RCTResponseSenderBlock?
|
|
611
|
-
static var completedLowerSpheresCallback: RCTResponseSenderBlock?
|
|
612
|
-
static var openedTutorialCallback: RCTResponseSenderBlock?
|
|
613
|
-
static var tooCloseCallback: RCTResponseSenderBlock?
|
|
614
|
-
static var tooFarCallback: RCTResponseSenderBlock?
|
|
615
|
-
|
|
616
|
-
@objc(registerOpenedTutorialCallback:)
|
|
617
|
-
func registerOpenedTutorialCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
|
|
618
|
-
ReplateCameraController.openedTutorialCallback = myCallback
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
@objc(registerCompletedTutorialCallback:)
|
|
622
|
-
func registerCompletedTutorialCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
|
|
623
|
-
ReplateCameraController.completedTutorialCallback = myCallback
|
|
624
|
-
}
|
|
625
588
|
|
|
626
|
-
@objc(registerAnchorSetCallback:)
|
|
627
|
-
func registerAnchorSetCallback(_ myCallback: @escaping RCTResponseSenderBlock) {
|
|
628
|
-
ReplateCameraController.anchorSetCallback = myCallback
|
|
629
|
-
}
|
|
630
589
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
590
|
+
// MARK: - Supporting Types
|
|
591
|
+
enum ARError: Error {
|
|
592
|
+
case noAnchor
|
|
593
|
+
case invalidAnchor
|
|
594
|
+
case notInFocus
|
|
595
|
+
case captureError
|
|
596
|
+
case tooManyImages
|
|
597
|
+
case processingError
|
|
598
|
+
case savingError
|
|
599
|
+
case transformError
|
|
600
|
+
case lightingError
|
|
601
|
+
case unknown
|
|
602
|
+
|
|
603
|
+
var localizedDescription: String {
|
|
604
|
+
switch self {
|
|
605
|
+
case .noAnchor: return "[ReplateCameraController] No anchor set yet"
|
|
606
|
+
case .invalidAnchor: return "[ReplateCameraController] AnchorNode is not valid"
|
|
607
|
+
case .notInFocus: return "[ReplateCameraController] Object not in focus"
|
|
608
|
+
case .captureError: return "[ReplateCameraController] Error capturing image"
|
|
609
|
+
case .tooManyImages: return "[ReplateCameraController] Too many images and the last one's not from a new angle"
|
|
610
|
+
case .processingError: return "[ReplateCameraController] Error processing image"
|
|
611
|
+
case .savingError: return "[ReplateCameraController] Error saving photo"
|
|
612
|
+
case .transformError: return "[ReplateCameraController] Camera transform data not available"
|
|
613
|
+
case .lightingError: return "[ReplateCameraController] Image too dark"
|
|
614
|
+
case .unknown: return "[ReplateCameraController] Unknown error occurred"
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
635
618
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
619
|
+
struct DeviceTargetInfo {
|
|
620
|
+
let isInFocus: Bool
|
|
621
|
+
let targetIndex: Int
|
|
622
|
+
let transform: simd_float4x4
|
|
623
|
+
let cameraPosition: SIMD3<Float>
|
|
624
|
+
let deviceDirection: SIMD3<Float>
|
|
640
625
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
626
|
+
var isValidTarget: Bool {
|
|
627
|
+
return targetIndex != -1
|
|
628
|
+
}
|
|
629
|
+
}
|
|
645
630
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
631
|
+
class SafeCallbackHandler {
|
|
632
|
+
private let resolver: RCTPromiseResolveBlock
|
|
633
|
+
private let rejecter: RCTPromiseRejectBlock
|
|
634
|
+
var hasCalledBack = false
|
|
635
|
+
private let lock = NSLock()
|
|
650
636
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
637
|
+
init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
|
638
|
+
self.resolver = resolver
|
|
639
|
+
self.rejecter = rejecter
|
|
640
|
+
}
|
|
655
641
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
642
|
+
func resolve(_ result: Any) {
|
|
643
|
+
lock.lock()
|
|
644
|
+
defer { lock.unlock() }
|
|
645
|
+
guard !hasCalledBack else {
|
|
646
|
+
print("rejecter: Callback already invoked.")
|
|
647
|
+
return
|
|
648
|
+
}
|
|
649
|
+
hasCalledBack = true
|
|
650
|
+
resolver(result)
|
|
651
|
+
}
|
|
660
652
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}
|
|
653
|
+
func reject(_ error: ARError) {
|
|
654
|
+
lock.lock()
|
|
655
|
+
defer { lock.unlock() }
|
|
665
656
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
657
|
+
guard !hasCalledBack else {
|
|
658
|
+
print("rejecter: Callback already invoked.")
|
|
659
|
+
return
|
|
660
|
+
}
|
|
661
|
+
hasCalledBack = true
|
|
662
|
+
rejecter(
|
|
663
|
+
String(describing: error),
|
|
664
|
+
error.localizedDescription,
|
|
665
|
+
NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
|
|
666
|
+
)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
670
669
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
670
|
+
// MARK: - Main Controller
|
|
671
|
+
@objc(ReplateCameraController)
|
|
672
|
+
class ReplateCameraController: NSObject {
|
|
673
|
+
// MARK: - Static Properties
|
|
674
|
+
private static let lock = NSLock()
|
|
675
|
+
private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
|
|
676
|
+
|
|
677
|
+
// Configuration Constants
|
|
678
|
+
private static let MIN_DISTANCE: Float = 0.15
|
|
679
|
+
private static let MAX_DISTANCE: Float = 0.45
|
|
680
|
+
private static let ANGLE_THRESHOLD: Float = 0.6
|
|
681
|
+
private static let TARGET_IMAGE_SIZE = CGSize(width: 1728, height: 1296)
|
|
682
|
+
private static let MIN_AMBIENT_INTENSITY: CGFloat = 650
|
|
683
|
+
|
|
684
|
+
// Callbacks
|
|
685
|
+
static var completedTutorialCallback: RCTResponseSenderBlock?
|
|
686
|
+
static var anchorSetCallback: RCTResponseSenderBlock?
|
|
687
|
+
static var completedUpperSpheresCallback: RCTResponseSenderBlock?
|
|
688
|
+
static var completedLowerSpheresCallback: RCTResponseSenderBlock?
|
|
689
|
+
static var openedTutorialCallback: RCTResponseSenderBlock?
|
|
690
|
+
static var tooCloseCallback: RCTResponseSenderBlock?
|
|
691
|
+
static var tooFarCallback: RCTResponseSenderBlock?
|
|
692
|
+
|
|
693
|
+
// MARK: - Callback Registration Methods
|
|
694
|
+
@objc(registerOpenedTutorialCallback:)
|
|
695
|
+
func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
696
|
+
Self.lock.lock()
|
|
697
|
+
defer { Self.lock.unlock() }
|
|
698
|
+
ReplateCameraController.openedTutorialCallback = callback
|
|
699
|
+
}
|
|
678
700
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
701
|
+
@objc(registerCompletedTutorialCallback:)
|
|
702
|
+
func registerCompletedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
703
|
+
Self.lock.lock()
|
|
704
|
+
defer { Self.lock.unlock() }
|
|
705
|
+
ReplateCameraController.completedTutorialCallback = callback
|
|
706
|
+
}
|
|
683
707
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
708
|
+
@objc(registerAnchorSetCallback:)
|
|
709
|
+
func registerAnchorSetCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
710
|
+
Self.lock.lock()
|
|
711
|
+
defer { Self.lock.unlock() }
|
|
712
|
+
ReplateCameraController.anchorSetCallback = callback
|
|
713
|
+
}
|
|
689
714
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
715
|
+
@objc(registerCompletedUpperSpheresCallback:)
|
|
716
|
+
func registerCompletedUpperSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
717
|
+
Self.lock.lock()
|
|
718
|
+
defer { Self.lock.unlock() }
|
|
719
|
+
ReplateCameraController.completedUpperSpheresCallback = callback
|
|
720
|
+
}
|
|
694
721
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
722
|
+
@objc(registerCompletedLowerSpheresCallback:)
|
|
723
|
+
func registerCompletedLowerSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
724
|
+
Self.lock.lock()
|
|
725
|
+
defer { Self.lock.unlock() }
|
|
726
|
+
ReplateCameraController.completedLowerSpheresCallback = callback
|
|
727
|
+
}
|
|
699
728
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
729
|
+
@objc(registerTooCloseCallback:)
|
|
730
|
+
func registerTooCloseCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
731
|
+
Self.lock.lock()
|
|
732
|
+
defer { Self.lock.unlock() }
|
|
733
|
+
ReplateCameraController.tooCloseCallback = callback
|
|
734
|
+
}
|
|
705
735
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
736
|
+
@objc(registerTooFarCallback:)
|
|
737
|
+
func registerTooFarCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
738
|
+
Self.lock.lock()
|
|
739
|
+
defer { Self.lock.unlock() }
|
|
740
|
+
ReplateCameraController.tooFarCallback = callback
|
|
741
|
+
}
|
|
709
742
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
743
|
+
// MARK: - Public Methods
|
|
744
|
+
@objc(getPhotosCount:rejecter:)
|
|
745
|
+
func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
746
|
+
Self.lock.lock()
|
|
747
|
+
let count = ReplateCameraView.totalPhotosTaken
|
|
748
|
+
Self.lock.unlock()
|
|
749
|
+
resolver(count)
|
|
750
|
+
}
|
|
713
751
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
}
|
|
752
|
+
@objc(isScanComplete:rejecter:)
|
|
753
|
+
func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
754
|
+
Self.lock.lock()
|
|
755
|
+
let isComplete = ReplateCameraView.photosFromDifferentAnglesTaken == 144
|
|
756
|
+
Self.lock.unlock()
|
|
757
|
+
resolver(isComplete)
|
|
721
758
|
}
|
|
722
759
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
}
|
|
760
|
+
@objc(getRemainingAnglesToScan:rejecter:)
|
|
761
|
+
func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
762
|
+
Self.lock.lock()
|
|
763
|
+
let remaining = 144 - ReplateCameraView.photosFromDifferentAnglesTaken
|
|
764
|
+
Self.lock.unlock()
|
|
765
|
+
resolver(remaining)
|
|
730
766
|
}
|
|
731
767
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
737
|
-
var anchorPosition: SIMD3<Float>? = nil
|
|
738
|
-
if(!Thread.isMainThread){
|
|
739
|
-
DispatchQueue.main.sync {
|
|
740
|
-
if isAnchorNodeValid(anchorNode) {
|
|
741
|
-
anchorPosition = anchorNode.position(relativeTo: nil)
|
|
742
|
-
print("Anchor position in world space: \(anchorPosition)")
|
|
743
|
-
} else {
|
|
744
|
-
print("AnchorNode is not valid.")
|
|
745
|
-
safeRejecter("001", "[ReplateCameraController] AnchorNode is not valid.", NSError(domain: "ReplateCameraController", code: 001, userInfo: nil))
|
|
746
|
-
return
|
|
747
|
-
}
|
|
768
|
+
@objc
|
|
769
|
+
func reset() {
|
|
770
|
+
DispatchQueue.main.async {
|
|
771
|
+
ReplateCameraView.reset()
|
|
748
772
|
}
|
|
749
|
-
|
|
750
|
-
guard let anchorPosition else {
|
|
751
|
-
safeRejecter("001", "[ReplateCameraController] No anchor set yet", NSError(domain: "ReplateCameraController", code: 001, userInfo: nil))
|
|
752
|
-
return
|
|
753
|
-
}
|
|
754
|
-
let spheresHeight = ReplateCameraView.spheresHeight
|
|
755
|
-
let distanceBetweenCircles = ReplateCameraView.distanceBetweenCircles
|
|
756
|
-
let point1Y = anchorPosition.y + spheresHeight
|
|
757
|
-
let point2Y = anchorPosition.y + distanceBetweenCircles + spheresHeight
|
|
758
|
-
let twoThirdsDistance = spheresHeight + distanceBetweenCircles + distanceBetweenCircles/5
|
|
759
|
-
var deviceTargetInFocus = -1
|
|
760
|
-
let angleThreshold: Float = 0.6
|
|
761
|
-
var relativeCameraTransform: simd_float4x4
|
|
762
|
-
if let cameraTransform = ReplateCameraView.arView.session.currentFrame?.camera.transform {
|
|
763
|
-
relativeCameraTransform = ReplateCameraController.getTransformRelativeToAnchor(anchor: anchorNode, cameraTransform: cameraTransform)
|
|
764
|
-
let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
|
|
765
|
-
let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
|
|
766
|
-
let directionToAnchor = normalize(anchorPosition - cameraPosition)
|
|
767
|
-
let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
|
|
773
|
+
}
|
|
768
774
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
} else {
|
|
775
|
-
deviceTargetInFocus = 1
|
|
776
|
-
print("Is pointing at second point")
|
|
777
|
-
}
|
|
778
|
-
} else {
|
|
779
|
-
print("Not pointing at anchor")
|
|
780
|
-
}
|
|
781
|
-
} else {
|
|
782
|
-
print("Camera transform data not available")
|
|
783
|
-
safeRejecter("002", "[ReplateCameraController] Camera transform data not available", NSError(domain: "ReplateCameraController", code: 002, userInfo: nil))
|
|
784
|
-
return
|
|
785
|
-
}
|
|
775
|
+
// MARK: - Photo Capture and Processing
|
|
776
|
+
@objc(takePhoto:resolver:rejecter:)
|
|
777
|
+
func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
|
778
|
+
Self.arQueue.async { [weak self] in
|
|
779
|
+
guard let self = self else { return }
|
|
786
780
|
|
|
787
|
-
|
|
781
|
+
let callbackHandler = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
|
|
788
782
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (material is SimpleMaterial && material != nil){
|
|
796
|
-
let simpleMaterial = material! as? SimpleMaterial
|
|
797
|
-
if #available(iOS 15.0, *) {
|
|
798
|
-
let newMaterial = SimpleMaterial(color: simpleMaterial?.color.tint.withAlphaComponent(CGFloat(opacity)) ?? SimpleMaterial.Color.white.withAlphaComponent(CGFloat(opacity)), roughness: 1, isMetallic: false)
|
|
799
|
-
entity.model?.materials[0] = newMaterial
|
|
800
|
-
} else {
|
|
801
|
-
// Fallback on earlier versions
|
|
802
|
-
}
|
|
803
|
-
}
|
|
783
|
+
do {
|
|
784
|
+
try self.validateAndProcessPhoto(unlimited: unlimited, callbackHandler: callbackHandler)
|
|
785
|
+
} catch let error as ARError {
|
|
786
|
+
callbackHandler.reject(error)
|
|
787
|
+
} catch {
|
|
788
|
+
callbackHandler.reject(.unknown)
|
|
804
789
|
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
if(deviceTargetInFocus != ReplateCameraView.circleInFocus){
|
|
808
|
-
setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
|
|
809
|
-
setOpacityToCircle(circleId: deviceTargetInFocus, opacity: 1)
|
|
810
|
-
ReplateCameraView.circleInFocus = deviceTargetInFocus
|
|
811
|
-
ReplateCameraView.generateImpactFeedback(strength: .heavy)
|
|
812
790
|
}
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private func validateAndProcessPhoto(unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
|
|
794
|
+
let anchorEntity = try getValidAnchorEntity()
|
|
795
|
+
let deviceTargetInfo = try getDeviceTargetInfo(anchorEntity: anchorEntity)
|
|
818
796
|
|
|
819
|
-
|
|
820
|
-
|
|
797
|
+
if deviceTargetInfo.isValidTarget {
|
|
798
|
+
try processTargetedDevice(deviceTargetInfo: deviceTargetInfo, unlimited: unlimited, callbackHandler: callbackHandler)
|
|
799
|
+
} else {
|
|
800
|
+
throw ARError.notInFocus
|
|
801
|
+
}
|
|
802
|
+
}
|
|
821
803
|
|
|
822
|
-
|
|
823
|
-
|
|
804
|
+
private func getValidAnchorEntity() throws -> AnchorEntity {
|
|
805
|
+
var anchorEntity: AnchorEntity?
|
|
806
|
+
if(Thread.isMainThread){
|
|
807
|
+
anchorEntity = ReplateCameraView.anchorEntity
|
|
824
808
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
809
|
+
}else{
|
|
810
|
+
DispatchQueue.main.sync {
|
|
811
|
+
anchorEntity = ReplateCameraView.anchorEntity
|
|
812
|
+
}
|
|
813
|
+
}
|
|
830
814
|
|
|
831
|
-
// Get the output CIImage
|
|
832
|
-
guard let scaledCIImage = scaleFilter.outputImage else {
|
|
833
|
-
safeRejecter("005", "[ReplateCameraController] Error scaling CIImage", NSError(domain: "ReplateCameraController", code: 005, userInfo: nil))
|
|
834
|
-
return
|
|
835
|
-
}
|
|
836
815
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
return
|
|
841
|
-
}
|
|
816
|
+
guard let anchor = anchorEntity else {
|
|
817
|
+
throw ARError.noAnchor
|
|
818
|
+
}
|
|
842
819
|
|
|
843
|
-
|
|
844
|
-
|
|
820
|
+
guard isAnchorNodeValid(anchor) else {
|
|
821
|
+
throw ARError.invalidAnchor
|
|
822
|
+
}
|
|
845
823
|
|
|
846
|
-
|
|
847
|
-
|
|
824
|
+
return anchor
|
|
825
|
+
}
|
|
848
826
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
827
|
+
private func getDeviceTargetInfo(anchorEntity: AnchorEntity) throws -> DeviceTargetInfo {
|
|
828
|
+
guard let frame = ReplateCameraView.arView.session.currentFrame else {
|
|
829
|
+
throw ARError.transformError
|
|
830
|
+
}
|
|
853
831
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
return
|
|
857
|
-
}
|
|
832
|
+
let cameraTransform = frame.camera.transform
|
|
833
|
+
let relativeCameraTransform = getTransformRelativeToAnchor(anchor: anchorEntity, cameraTransform: cameraTransform)
|
|
858
834
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
835
|
+
var anchorPosition: SIMD3<Float>!
|
|
836
|
+
if(Thread.isMainThread){
|
|
837
|
+
anchorPosition = anchorEntity.position(relativeTo: nil)
|
|
862
838
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
} else {
|
|
867
|
-
safeRejecter("006", "[ReplateCameraController] Error saving photo", NSError(domain: "ReplateCameraController", code: 006, userInfo: nil))
|
|
868
|
-
}
|
|
869
|
-
}
|
|
839
|
+
}else{
|
|
840
|
+
DispatchQueue.main.sync {
|
|
841
|
+
anchorPosition = anchorEntity.position(relativeTo: nil)
|
|
870
842
|
}
|
|
871
|
-
} else {
|
|
872
|
-
safeRejecter("007", "[ReplateCameraController] Object not in focus", NSError(domain: "ReplateCameraController", code: 007, userInfo: nil))
|
|
873
843
|
}
|
|
874
|
-
} catch {
|
|
875
|
-
print("Unexpected error occurred")
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
844
|
|
|
879
845
|
|
|
880
|
-
|
|
881
|
-
|
|
846
|
+
let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
|
|
847
|
+
let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
|
|
848
|
+
let directionToAnchor = normalize(anchorPosition - cameraPosition)
|
|
849
|
+
let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
|
|
882
850
|
|
|
883
|
-
|
|
884
|
-
}
|
|
851
|
+
let targetIndex = determineTargetIndex(angleToAnchor: angleToAnchor, relativeCameraTransform: relativeCameraTransform)
|
|
885
852
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
853
|
+
return DeviceTargetInfo(
|
|
854
|
+
isInFocus: angleToAnchor < Self.ANGLE_THRESHOLD,
|
|
855
|
+
targetIndex: targetIndex,
|
|
856
|
+
transform: relativeCameraTransform,
|
|
857
|
+
cameraPosition: cameraPosition,
|
|
858
|
+
deviceDirection: deviceDirection
|
|
859
|
+
)
|
|
890
860
|
}
|
|
891
861
|
|
|
892
|
-
|
|
893
|
-
|
|
862
|
+
private func determineTargetIndex(angleToAnchor: Float, relativeCameraTransform: simd_float4x4) -> Int {
|
|
863
|
+
guard angleToAnchor < Self.ANGLE_THRESHOLD else { return -1 }
|
|
894
864
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
\(cameraTransform.columns.2.x),\(cameraTransform.columns.2.y),\(cameraTransform.columns.2.z),\(cameraTransform.columns.2.w);
|
|
900
|
-
\(cameraTransform.columns.3.x),\(cameraTransform.columns.3.y),\(cameraTransform.columns.3.z),\(cameraTransform.columns.3.w)
|
|
901
|
-
"""
|
|
865
|
+
let spheresHeight = ReplateCameraView.spheresHeight
|
|
866
|
+
let distanceBetweenCircles = ReplateCameraView.distanceBetweenCircles
|
|
867
|
+
let twoThirdsDistance = spheresHeight + distanceBetweenCircles + distanceBetweenCircles/5
|
|
868
|
+
let cameraHeight = relativeCameraTransform.columns.3.y
|
|
902
869
|
|
|
903
|
-
|
|
904
|
-
|
|
870
|
+
return cameraHeight < twoThirdsDistance ? 0 : 1
|
|
871
|
+
}
|
|
905
872
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
873
|
+
private func processTargetedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
|
|
874
|
+
DispatchQueue.main.async { [weak self] in
|
|
875
|
+
guard let self = self else { return }
|
|
876
|
+
|
|
877
|
+
self.updateCircleFocus(targetIndex: deviceTargetInfo.targetIndex)
|
|
878
|
+
self.checkCameraDistance(deviceTargetInfo: deviceTargetInfo)
|
|
879
|
+
|
|
880
|
+
self.updateSpheres(
|
|
881
|
+
deviceTargetInfo: deviceTargetInfo,
|
|
882
|
+
cameraTransform: deviceTargetInfo.transform
|
|
883
|
+
) { success in
|
|
884
|
+
if !unlimited && !success {
|
|
885
|
+
callbackHandler.reject(.tooManyImages)
|
|
886
|
+
return
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
self.captureAndProcessImage(callbackHandler: callbackHandler)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
913
892
|
}
|
|
914
893
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
894
|
+
private func updateCircleFocus(targetIndex: Int) {
|
|
895
|
+
if targetIndex != ReplateCameraView.circleInFocus {
|
|
896
|
+
setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
|
|
897
|
+
setOpacityToCircle(circleId: targetIndex, opacity: 1)
|
|
898
|
+
ReplateCameraView.circleInFocus = targetIndex
|
|
899
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
|
|
900
|
+
}
|
|
919
901
|
}
|
|
920
902
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
903
|
+
private func checkCameraDistance(deviceTargetInfo: DeviceTargetInfo) {
|
|
904
|
+
let distance = isCameraWithinRange(
|
|
905
|
+
cameraTransform: deviceTargetInfo.transform,
|
|
906
|
+
anchorEntity: ReplateCameraView.anchorEntity!
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
switch distance {
|
|
910
|
+
case 1:
|
|
911
|
+
ReplateCameraController.tooFarCallback?([])
|
|
912
|
+
case -1:
|
|
913
|
+
ReplateCameraController.tooCloseCallback?([])
|
|
914
|
+
default:
|
|
915
|
+
break
|
|
916
|
+
}
|
|
917
|
+
}
|
|
925
918
|
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
919
|
+
// MARK: - Image Processing
|
|
920
|
+
private func captureAndProcessImage(callbackHandler: SafeCallbackHandler) {
|
|
921
|
+
guard let frame = ReplateCameraView.arView?.session.currentFrame else {
|
|
922
|
+
callbackHandler.reject(.captureError)
|
|
923
|
+
return
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Check lighting conditions
|
|
927
|
+
if let lightEstimate = frame.lightEstimate {
|
|
928
|
+
guard lightEstimate.ambientIntensity >= Self.MIN_AMBIENT_INTENSITY else {
|
|
929
|
+
callbackHandler.reject(.lightingError)
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
processAndSaveImage(frame.capturedImage, callbackHandler: callbackHandler)
|
|
930
935
|
}
|
|
931
|
-
var mutableMetadata = imageProperties
|
|
932
936
|
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
kCGImagePropertyExifUserComment: cameraTransform
|
|
936
|
-
]
|
|
937
|
+
private func processAndSaveImage(_ pixelBuffer: CVPixelBuffer, callbackHandler: SafeCallbackHandler) {
|
|
938
|
+
let ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
|
937
939
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
return
|
|
940
|
+
guard let resizedImage = resizeImage(ciImage, to: Self.TARGET_IMAGE_SIZE),
|
|
941
|
+
let cgImage = cgImage(from: resizedImage) else {
|
|
942
|
+
callbackHandler.reject(.processingError)
|
|
943
|
+
return
|
|
942
944
|
}
|
|
943
945
|
|
|
944
|
-
|
|
945
|
-
|
|
946
|
+
let uiImage = UIImage(cgImage: cgImage)
|
|
947
|
+
let rotatedImage = uiImage.rotate(radians: .pi / 2)
|
|
946
948
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
return nil
|
|
949
|
+
guard let savedURL = saveImageAsJPEG(rotatedImage) else {
|
|
950
|
+
callbackHandler.reject(.processingError)
|
|
951
|
+
return
|
|
951
952
|
}
|
|
952
953
|
|
|
953
|
-
|
|
954
|
-
return fileURL
|
|
954
|
+
callbackHandler.resolve(savedURL.absoluteString)
|
|
955
955
|
}
|
|
956
956
|
|
|
957
|
-
func isCameraWithinRange(cameraTransform: simd_float4x4, anchorEntity: AnchorEntity, minDistance: Float = 0.15, maxDistance: Float = 0.45) -> Int {
|
|
958
|
-
// Extract the camera's position from the cameraTransform (simd_float4x4)
|
|
959
|
-
let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
|
|
960
957
|
|
|
961
|
-
|
|
962
|
-
|
|
958
|
+
func resizeImage(_ image: CIImage, to targetSize: CGSize) -> CIImage? {
|
|
959
|
+
guard let scaleFilter = CIFilter(name: "CILanczosScaleTransform") else { return nil }
|
|
963
960
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
// Check if the distance is within the range
|
|
968
|
-
return distance <= minDistance ? -1 : distance >= maxDistance ? 1 : 0
|
|
969
|
-
}
|
|
961
|
+
scaleFilter.setValue(image, forKey: kCIInputImageKey)
|
|
962
|
+
scaleFilter.setValue(targetSize.width / image.extent.width, forKey: kCIInputScaleKey)
|
|
963
|
+
scaleFilter.setValue(1.0, forKey: kCIInputAspectRatioKey)
|
|
970
964
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
let dy = pos1.y - pos2.y
|
|
974
|
-
let dz = pos1.z - pos2.z
|
|
965
|
+
return scaleFilter.outputImage
|
|
966
|
+
}
|
|
975
967
|
|
|
976
|
-
//
|
|
977
|
-
|
|
978
|
-
|
|
968
|
+
// MARK: - Entity Management
|
|
969
|
+
func setOpacityToCircle(circleId: Int, opacity: Float) {
|
|
970
|
+
DispatchQueue.main.async {
|
|
971
|
+
for i in 0..<72 {
|
|
972
|
+
let offset = circleId == 0 ? 0 : 72
|
|
973
|
+
guard i + offset < ReplateCameraView.spheresModels.count else { continue }
|
|
979
974
|
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
if !completionCalled {
|
|
985
|
-
completionCalled = true
|
|
986
|
-
completion(result)
|
|
987
|
-
} else {
|
|
988
|
-
print("Completion already called")
|
|
989
|
-
}
|
|
975
|
+
let entity = ReplateCameraView.spheresModels[i + offset]
|
|
976
|
+
self.updateEntityMaterial(entity, opacity: opacity)
|
|
977
|
+
}
|
|
978
|
+
}
|
|
990
979
|
}
|
|
991
980
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
if (ReplateCameraView.spheresModels.count < 144) {
|
|
995
|
-
print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
|
|
996
|
-
callCompletion(false)
|
|
997
|
-
return
|
|
998
|
-
}
|
|
981
|
+
private func updateEntityMaterial(_ entity: ModelEntity, opacity: Float) {
|
|
982
|
+
guard let material = entity.model?.materials.first as? SimpleMaterial else { return }
|
|
999
983
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
984
|
+
if #available(iOS 15.0, *) {
|
|
985
|
+
let newMaterial = SimpleMaterial(
|
|
986
|
+
color: material.color.tint.withAlphaComponent(CGFloat(opacity)),
|
|
987
|
+
roughness: 1,
|
|
988
|
+
isMetallic: false
|
|
989
|
+
)
|
|
990
|
+
entity.model?.materials[0] = newMaterial
|
|
991
|
+
}
|
|
1004
992
|
}
|
|
1005
993
|
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
print("[updateSpheres] No current frame available.")
|
|
1009
|
-
callCompletion(false)
|
|
1010
|
-
return
|
|
1011
|
-
}
|
|
1012
|
-
let cameraDistance = isCameraWithinRange(cameraTransform: cameraTransform, anchorEntity: anchorNode)
|
|
1013
|
-
print("Camera distance: \(cameraDistance)")
|
|
1014
|
-
func showAlert(_ message: String) {
|
|
1015
|
-
DispatchQueue.main.async {
|
|
1016
|
-
let alert = UIAlertController(title: "Alert", message: message, preferredStyle: .alert)
|
|
1017
|
-
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
|
|
994
|
+
private func updateSpheres(deviceTargetInfo: DeviceTargetInfo, cameraTransform: simd_float4x4, completion: @escaping (Bool) -> Void) {
|
|
995
|
+
var completionCalled = false
|
|
1018
996
|
|
|
1019
|
-
|
|
1020
|
-
|
|
997
|
+
func safeCompletion(_ result: Bool) {
|
|
998
|
+
guard !completionCalled else {
|
|
999
|
+
print("Completion already called")
|
|
1000
|
+
return
|
|
1001
|
+
}
|
|
1002
|
+
completionCalled = true
|
|
1003
|
+
completion(result)
|
|
1021
1004
|
}
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
1005
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1006
|
+
// Validate spheres initialization
|
|
1007
|
+
guard ReplateCameraView.spheresModels.count >= 144 else {
|
|
1008
|
+
print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
|
|
1009
|
+
safeCompletion(false)
|
|
1010
|
+
return
|
|
1011
|
+
}
|
|
1030
1012
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1013
|
+
// Get anchor
|
|
1014
|
+
guard let anchorNode = ReplateCameraView.anchorEntity else {
|
|
1015
|
+
print("[updateSpheres] No anchor entity found.")
|
|
1016
|
+
safeCompletion(false)
|
|
1017
|
+
return
|
|
1018
|
+
}
|
|
1037
1019
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1020
|
+
// Calculate camera metrics
|
|
1021
|
+
let cameraDistance = isCameraWithinRange(
|
|
1022
|
+
cameraTransform: cameraTransform,
|
|
1023
|
+
anchorEntity: anchorNode
|
|
1024
|
+
)
|
|
1025
|
+
|
|
1026
|
+
// Calculate sphere index
|
|
1027
|
+
let angleDegrees = angleBetweenAnchorXAndCamera(
|
|
1028
|
+
anchor: anchorNode,
|
|
1029
|
+
cameraTransform: cameraTransform
|
|
1030
|
+
)
|
|
1031
|
+
let sphereIndex = max(Int(round(angleDegrees / 5.0)), 0) % 72
|
|
1032
|
+
|
|
1033
|
+
DispatchQueue.main.async {
|
|
1034
|
+
self.processSphereUpdate(
|
|
1035
|
+
sphereIndex: sphereIndex,
|
|
1036
|
+
targetIndex: deviceTargetInfo.targetIndex,
|
|
1037
|
+
completion: safeCompletion
|
|
1038
|
+
)
|
|
1039
|
+
}
|
|
1044
1040
|
}
|
|
1045
1041
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1042
|
+
private func processSphereUpdate(sphereIndex: Int, targetIndex: Int, completion: @escaping (Bool) -> Void) {
|
|
1043
|
+
var mesh: ModelEntity?
|
|
1044
|
+
var newAngle = false
|
|
1045
|
+
var callback: RCTResponseSenderBlock?
|
|
1050
1046
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1047
|
+
if targetIndex == 1 {
|
|
1048
|
+
guard sphereIndex < ReplateCameraView.upperSpheresSet.count else {
|
|
1049
|
+
print("[updateSpheres] Sphere index out of bounds")
|
|
1050
|
+
completion(false)
|
|
1051
|
+
return
|
|
1052
|
+
}
|
|
1055
1053
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1054
|
+
if !ReplateCameraView.upperSpheresSet[sphereIndex] {
|
|
1055
|
+
ReplateCameraView.upperSpheresSet[sphereIndex] = true
|
|
1056
|
+
ReplateCameraView.photosFromDifferentAnglesTaken += 1
|
|
1057
|
+
newAngle = true
|
|
1058
|
+
|
|
1059
|
+
guard 72 + sphereIndex < ReplateCameraView.spheresModels.count else {
|
|
1060
|
+
print("[updateSpheres] Upper spheresModels index out of range")
|
|
1061
|
+
completion(false)
|
|
1062
|
+
return
|
|
1063
|
+
}
|
|
1062
1064
|
|
|
1063
|
-
|
|
1064
|
-
ReplateCameraView.upperSpheresSet[sphereIndex] = true
|
|
1065
|
-
ReplateCameraView.photosFromDifferentAnglesTaken += 1
|
|
1066
|
-
newAngle = true
|
|
1065
|
+
mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
|
|
1067
1066
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1067
|
+
if ReplateCameraView.upperSpheresSet.allSatisfy({ $0 }) {
|
|
1068
|
+
callback = ReplateCameraController.completedUpperSpheresCallback
|
|
1069
|
+
ReplateCameraController.completedUpperSpheresCallback = nil
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
} else if targetIndex == 0 {
|
|
1073
|
+
guard sphereIndex < ReplateCameraView.lowerSpheresSet.count else {
|
|
1074
|
+
print("[updateSpheres] Lower sphere index out of range")
|
|
1075
|
+
completion(false)
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if !ReplateCameraView.lowerSpheresSet[sphereIndex] {
|
|
1080
|
+
ReplateCameraView.lowerSpheresSet[sphereIndex] = true
|
|
1081
|
+
ReplateCameraView.photosFromDifferentAnglesTaken += 1
|
|
1082
|
+
newAngle = true
|
|
1083
|
+
|
|
1084
|
+
guard sphereIndex < ReplateCameraView.spheresModels.count else {
|
|
1085
|
+
print("[updateSpheres] Lower spheresModels index out of range")
|
|
1086
|
+
completion(false)
|
|
1087
|
+
return
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
mesh = ReplateCameraView.spheresModels[sphereIndex]
|
|
1091
|
+
|
|
1092
|
+
if ReplateCameraView.lowerSpheresSet.allSatisfy({ $0 }) {
|
|
1093
|
+
callback = ReplateCameraController.completedLowerSpheresCallback
|
|
1094
|
+
ReplateCameraController.completedLowerSpheresCallback = nil
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1072
1097
|
}
|
|
1073
|
-
mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
|
|
1074
1098
|
|
|
1075
|
-
if
|
|
1076
|
-
|
|
1077
|
-
|
|
1099
|
+
if let mesh = mesh {
|
|
1100
|
+
let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
|
|
1101
|
+
mesh.model?.materials[0] = material
|
|
1102
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
|
|
1078
1103
|
}
|
|
1079
|
-
}
|
|
1080
|
-
} else if deviceTargetInFocus == 0 {
|
|
1081
|
-
if sphereIndex >= ReplateCameraView.lowerSpheresSet.count {
|
|
1082
|
-
print("[updateSpheres] Lower sphere index out of range. Index: \(sphereIndex), Count: \(ReplateCameraView.lowerSpheresSet.count)")
|
|
1083
|
-
callCompletion(false)
|
|
1084
|
-
return
|
|
1085
|
-
}
|
|
1086
1104
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
newAngle = true
|
|
1105
|
+
callback?([])
|
|
1106
|
+
completion(newAngle)
|
|
1107
|
+
}
|
|
1091
1108
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1109
|
+
// MARK: - Anchor Validation
|
|
1110
|
+
func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
|
|
1111
|
+
var isValid = false
|
|
1112
|
+
if(Thread.isMainThread){
|
|
1113
|
+
let transform = anchorNode.transform
|
|
1114
|
+
let position = transform.translation
|
|
1115
|
+
let rotation = transform.rotation
|
|
1116
|
+
let scale = transform.scale
|
|
1117
|
+
|
|
1118
|
+
isValid = !position.isNaN &&
|
|
1119
|
+
!rotation.isNaN &&
|
|
1120
|
+
scale != SIMD3<Float>(0, 0, 0) &&
|
|
1121
|
+
abs(length(rotation.vector) - 1.0) < 0.0001
|
|
1122
|
+
}else{
|
|
1123
|
+
DispatchQueue.main.sync {
|
|
1124
|
+
let transform = anchorNode.transform
|
|
1125
|
+
let position = transform.translation
|
|
1126
|
+
let rotation = transform.rotation
|
|
1127
|
+
let scale = transform.scale
|
|
1128
|
+
|
|
1129
|
+
isValid = !position.isNaN &&
|
|
1130
|
+
!rotation.isNaN &&
|
|
1131
|
+
scale != SIMD3<Float>(0, 0, 0) &&
|
|
1132
|
+
abs(length(rotation.vector) - 1.0) < 0.0001
|
|
1096
1133
|
}
|
|
1097
|
-
|
|
1134
|
+
}
|
|
1098
1135
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1136
|
+
return isValid
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// MARK: - Distance and Angle Calculations
|
|
1140
|
+
func isCameraWithinRange(cameraTransform: simd_float4x4, anchorEntity: AnchorEntity) -> Int {
|
|
1141
|
+
var distance: Float = 0
|
|
1142
|
+
|
|
1143
|
+
if(Thread.isMainThread){
|
|
1144
|
+
let cameraPosition = SIMD3<Float>(
|
|
1145
|
+
cameraTransform.columns.3.x,
|
|
1146
|
+
cameraTransform.columns.3.y,
|
|
1147
|
+
cameraTransform.columns.3.z
|
|
1148
|
+
)
|
|
1149
|
+
let anchorPosition = anchorEntity.transform.translation
|
|
1150
|
+
distance = distanceBetween(cameraPosition, anchorPosition)
|
|
1151
|
+
}else{
|
|
1152
|
+
if(Thread.isMainThread){
|
|
1153
|
+
let cameraPosition = SIMD3<Float>(
|
|
1154
|
+
cameraTransform.columns.3.x,
|
|
1155
|
+
cameraTransform.columns.3.y,
|
|
1156
|
+
cameraTransform.columns.3.z
|
|
1157
|
+
)
|
|
1158
|
+
let anchorPosition = anchorEntity.transform.translation
|
|
1159
|
+
distance = distanceBetween(cameraPosition, anchorPosition)
|
|
1160
|
+
}else{
|
|
1161
|
+
DispatchQueue.main.sync {
|
|
1162
|
+
let cameraPosition = SIMD3<Float>(
|
|
1163
|
+
cameraTransform.columns.3.x,
|
|
1164
|
+
cameraTransform.columns.3.y,
|
|
1165
|
+
cameraTransform.columns.3.z
|
|
1166
|
+
)
|
|
1167
|
+
let anchorPosition = anchorEntity.transform.translation
|
|
1168
|
+
distance = distanceBetween(cameraPosition, anchorPosition)
|
|
1169
|
+
}
|
|
1102
1170
|
}
|
|
1171
|
+
|
|
1103
1172
|
}
|
|
1173
|
+
return distance <= Self.MIN_DISTANCE ? -1 :
|
|
1174
|
+
distance >= Self.MAX_DISTANCE ? 1 : 0
|
|
1104
1175
|
}
|
|
1105
1176
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
mesh.model?.materials[0] = material
|
|
1110
|
-
ReplateCameraView.generateImpactFeedback(strength: .light)
|
|
1111
|
-
}
|
|
1177
|
+
func distanceBetween(_ pos1: SIMD3<Float>, _ pos2: SIMD3<Float>) -> Float {
|
|
1178
|
+
let difference = pos1 - pos2
|
|
1179
|
+
return sqrt(dot(difference, difference))
|
|
1112
1180
|
}
|
|
1113
1181
|
|
|
1114
|
-
//
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1182
|
+
// MARK: - Static Utility Methods
|
|
1183
|
+
func getTransformRelativeToAnchor(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> simd_float4x4 {
|
|
1184
|
+
var relativeTransform: simd_float4x4!
|
|
1185
|
+
if(Thread.isMainThread){
|
|
1186
|
+
let anchorTransform = anchor.transformMatrix(relativeTo: nil)
|
|
1187
|
+
relativeTransform = anchorTransform.inverse * cameraTransform
|
|
1188
|
+
}else{
|
|
1189
|
+
DispatchQueue.main.sync {
|
|
1190
|
+
let anchorTransform = anchor.transformMatrix(relativeTo: nil)
|
|
1191
|
+
relativeTransform = anchorTransform.inverse * cameraTransform
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
return relativeTransform
|
|
1195
|
+
}
|
|
1118
1196
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1197
|
+
func angleBetweenAnchorXAndCamera(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> Float {
|
|
1198
|
+
var angleDegrees: Float = 0
|
|
1199
|
+
if(Thread.isMainThread){
|
|
1200
|
+
let anchorTransform = anchor.transform.matrix
|
|
1201
|
+
let anchorPositionXZ = simd_float2(
|
|
1202
|
+
anchor.transform.translation.x,
|
|
1203
|
+
anchor.transform.translation.z
|
|
1204
|
+
)
|
|
1205
|
+
let relativeCameraPositionXZ = simd_float2(
|
|
1206
|
+
cameraTransform.columns.3.x,
|
|
1207
|
+
cameraTransform.columns.3.z
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
|
|
1211
|
+
let anchorXAxisXZ = simd_float2(
|
|
1212
|
+
anchorTransform.columns.0.x,
|
|
1213
|
+
anchorTransform.columns.0.z
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
let angle = atan2(directionXZ.y, directionXZ.x) -
|
|
1217
|
+
atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
|
|
1218
|
+
|
|
1219
|
+
angleDegrees = angle * (180.0 / .pi)
|
|
1220
|
+
if angleDegrees < 0 {
|
|
1221
|
+
angleDegrees += 360
|
|
1222
|
+
}
|
|
1223
|
+
}else{
|
|
1224
|
+
DispatchQueue.main.sync {
|
|
1225
|
+
let anchorTransform = anchor.transform.matrix
|
|
1226
|
+
let anchorPositionXZ = simd_float2(
|
|
1227
|
+
anchor.transform.translation.x,
|
|
1228
|
+
anchor.transform.translation.z
|
|
1229
|
+
)
|
|
1230
|
+
let relativeCameraPositionXZ = simd_float2(
|
|
1231
|
+
cameraTransform.columns.3.x,
|
|
1232
|
+
cameraTransform.columns.3.z
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
|
|
1236
|
+
let anchorXAxisXZ = simd_float2(
|
|
1237
|
+
anchorTransform.columns.0.x,
|
|
1238
|
+
anchorTransform.columns.0.z
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
let angle = atan2(directionXZ.y, directionXZ.x) -
|
|
1242
|
+
atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
|
|
1243
|
+
|
|
1244
|
+
angleDegrees = angle * (180.0 / .pi)
|
|
1245
|
+
if angleDegrees < 0 {
|
|
1246
|
+
angleDegrees += 360
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return angleDegrees
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
func cgImage(from ciImage: CIImage) -> CGImage? {
|
|
1254
|
+
let context = CIContext(options: nil)
|
|
1255
|
+
return context.createCGImage(ciImage, from: ciImage.extent)
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
func saveImageAsJPEG(_ image: UIImage) -> URL? {
|
|
1259
|
+
let cameraTransform = getCameraTransformString(from: ReplateCameraView.arView.session)
|
|
1260
|
+
|
|
1261
|
+
guard let imageData = image.jpegData(compressionQuality: 1),
|
|
1262
|
+
let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
|
|
1263
|
+
return nil
|
|
1264
|
+
}
|
|
1125
1265
|
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
let anchorPositionXZ = simd_float2(anchor.transform.translation.x, anchor.transform.translation.z)
|
|
1130
|
-
let relativeCameraPositionXZ = simd_float2(cameraTransform.columns.3.x, cameraTransform.columns.3.z)
|
|
1266
|
+
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
1267
|
+
let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
|
|
1268
|
+
let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
|
|
1131
1269
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1270
|
+
guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
|
|
1271
|
+
return nil
|
|
1272
|
+
}
|
|
1134
1273
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1274
|
+
var mutableMetadata = imageProperties
|
|
1275
|
+
mutableMetadata[kCGImagePropertyExifDictionary] = [
|
|
1276
|
+
kCGImagePropertyExifUserComment: cameraTransform
|
|
1277
|
+
]
|
|
1278
|
+
|
|
1279
|
+
guard let destination = CGImageDestinationCreateWithURL(
|
|
1280
|
+
fileURL as CFURL,
|
|
1281
|
+
kUTTypeJPEG,
|
|
1282
|
+
1,
|
|
1283
|
+
nil
|
|
1284
|
+
) else {
|
|
1285
|
+
return nil
|
|
1286
|
+
}
|
|
1137
1287
|
|
|
1138
|
-
|
|
1139
|
-
|
|
1288
|
+
CGImageDestinationAddImageFromSource(
|
|
1289
|
+
destination,
|
|
1290
|
+
source,
|
|
1291
|
+
0,
|
|
1292
|
+
mutableMetadata as CFDictionary
|
|
1293
|
+
)
|
|
1140
1294
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1295
|
+
guard CGImageDestinationFinalize(destination) else {
|
|
1296
|
+
return nil
|
|
1297
|
+
}
|
|
1143
1298
|
|
|
1144
|
-
|
|
1145
|
-
if angleDegrees < 0 {
|
|
1146
|
-
angleDegrees += 360
|
|
1299
|
+
return fileURL
|
|
1147
1300
|
}
|
|
1148
1301
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1302
|
+
func getCameraTransformString(from session: ARSession) -> String? {
|
|
1303
|
+
guard let currentFrame = session.currentFrame else {
|
|
1304
|
+
return nil
|
|
1305
|
+
}
|
|
1151
1306
|
|
|
1307
|
+
let transform = currentFrame.camera.transform
|
|
1308
|
+
return """
|
|
1309
|
+
\(transform.columns.0.x),\(transform.columns.0.y),\(transform.columns.0.z),\(transform.columns.0.w);
|
|
1310
|
+
\(transform.columns.1.x),\(transform.columns.1.y),\(transform.columns.1.z),\(transform.columns.1.w);
|
|
1311
|
+
\(transform.columns.2.x),\(transform.columns.2.y),\(transform.columns.2.z),\(transform.columns.2.w);
|
|
1312
|
+
\(transform.columns.3.x),\(transform.columns.3.y),\(transform.columns.3.z),\(transform.columns.3.w)
|
|
1313
|
+
"""
|
|
1314
|
+
}
|
|
1152
1315
|
}
|
|
1153
1316
|
|
|
1154
1317
|
extension ARView: ARCoachingOverlayViewDelegate {
|
|
@@ -1167,7 +1330,7 @@ extension ARView: ARCoachingOverlayViewDelegate {
|
|
|
1167
1330
|
// Set the delegate for any callbacks
|
|
1168
1331
|
coachingOverlay.delegate = self
|
|
1169
1332
|
coachingOverlay.setActive(true, animated: true)
|
|
1170
|
-
ReplateCameraView.generateImpactFeedback(strength: .light)
|
|
1333
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
|
|
1171
1334
|
let callback = ReplateCameraController.openedTutorialCallback
|
|
1172
1335
|
if (callback != nil) {
|
|
1173
1336
|
callback!([])
|
|
@@ -1179,15 +1342,13 @@ extension ARView: ARCoachingOverlayViewDelegate {
|
|
|
1179
1342
|
public func coachingOverlayViewDidDeactivate(
|
|
1180
1343
|
_ coachingOverlayView: ARCoachingOverlayView
|
|
1181
1344
|
) {
|
|
1182
|
-
print("DEACTIVATED")
|
|
1183
1345
|
let callback = ReplateCameraController.completedTutorialCallback
|
|
1184
1346
|
if (callback != nil) {
|
|
1185
1347
|
callback!([])
|
|
1186
1348
|
ReplateCameraController.completedTutorialCallback = nil
|
|
1187
1349
|
}
|
|
1188
|
-
ReplateCameraView.generateImpactFeedback(strength: .heavy)
|
|
1189
|
-
ReplateCameraView.
|
|
1190
|
-
print("CRASHED")
|
|
1350
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
|
|
1351
|
+
ReplateCameraView.addRecognizers()
|
|
1191
1352
|
}
|
|
1192
1353
|
}
|
|
1193
1354
|
|