replate-camera 0.4.0 → 0.5.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.
|
@@ -1255,49 +1255,59 @@ class ReplateCameraController: NSObject {
|
|
|
1255
1255
|
return context.createCGImage(ciImage, from: ciImage.extent)
|
|
1256
1256
|
}
|
|
1257
1257
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
return fileURL
|
|
1258
|
+
func saveImageAsJPEG(_ image: UIImage) -> URL? {
|
|
1259
|
+
let cameraTransform = getCameraTransformString(from: ReplateCameraView.arView.session)
|
|
1260
|
+
let gravityVector = getGravityVectorString(from: ReplateCameraView.arView.session)
|
|
1261
|
+
|
|
1262
|
+
guard let imageData = image.jpegData(compressionQuality: 1),
|
|
1263
|
+
let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
|
|
1264
|
+
return nil
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
1268
|
+
let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
|
|
1269
|
+
let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
|
|
1270
|
+
|
|
1271
|
+
guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
|
|
1272
|
+
return nil
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
var mutableMetadata = imageProperties
|
|
1276
|
+
mutableMetadata[kCGImagePropertyExifDictionary] = [
|
|
1277
|
+
kCGImagePropertyExifUserComment: "\(cameraTransform ?? "")|\(gravityVector)"
|
|
1278
|
+
]
|
|
1279
|
+
|
|
1280
|
+
guard let destination = CGImageDestinationCreateWithURL(
|
|
1281
|
+
fileURL as CFURL,
|
|
1282
|
+
kUTTypeJPEG,
|
|
1283
|
+
1,
|
|
1284
|
+
nil
|
|
1285
|
+
) else {
|
|
1286
|
+
return nil
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
CGImageDestinationAddImageFromSource(
|
|
1290
|
+
destination,
|
|
1291
|
+
source,
|
|
1292
|
+
0,
|
|
1293
|
+
mutableMetadata as CFDictionary
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
guard CGImageDestinationFinalize(destination) else {
|
|
1297
|
+
return nil
|
|
1300
1298
|
}
|
|
1299
|
+
|
|
1300
|
+
return fileURL
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
func getGravityVectorString(from session: ARSession) -> String? {
|
|
1304
|
+
guard let currentFrame = session.currentFrame else {
|
|
1305
|
+
return nil
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
let gravityVector = currentFrame.camera.eulerAngles // gravity vector is in Euler angles (x, y, z)
|
|
1309
|
+
return "\(gravityVector.x),\(gravityVector.y),\(gravityVector.z)"
|
|
1310
|
+
}
|
|
1301
1311
|
|
|
1302
1312
|
func getCameraTransformString(from session: ARSession) -> String? {
|
|
1303
1313
|
guard let currentFrame = session.currentFrame else {
|
package/package.json
CHANGED
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
private func resizeImage(_ image: CIImage, to targetSize: CGSize) -> CIImage? {
|
|
2
|
-
guard let scaleFilter = CIFilter(name: "CILanczosScaleTransform") else { return nil }
|
|
3
|
-
|
|
4
|
-
scaleFilter.setValue(image, forKey: kCIInputImageKey)
|
|
5
|
-
scaleFilter.setValue(targetSize.width / image.extent.width, forKey: kCIInputScaleKey)
|
|
6
|
-
scaleFilter.setValue(1.0, forKey: kCIInputAspectRatioKey)
|
|
7
|
-
|
|
8
|
-
return scaleFilter.outputImage
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// MARK: - Entity Management
|
|
12
|
-
private func setOpacityToCircle(circleId: Int, opacity: Float) {
|
|
13
|
-
DispatchQueue.main.async {
|
|
14
|
-
for i in 0..<72 {
|
|
15
|
-
let offset = circleId == 0 ? 0 : 72
|
|
16
|
-
guard i + offset < ReplateCameraView.spheresModels.count else { continue }
|
|
17
|
-
|
|
18
|
-
let entity = ReplateCameraView.spheresModels[i + offset]
|
|
19
|
-
self.updateEntityMaterial(entity, opacity: opacity)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private func updateEntityMaterial(_ entity: ModelEntity, opacity: Float) {
|
|
25
|
-
guard let material = entity.model?.materials.first as? SimpleMaterial else { return }
|
|
26
|
-
|
|
27
|
-
if #available(iOS 15.0, *) {
|
|
28
|
-
let newMaterial = SimpleMaterial(
|
|
29
|
-
color: material.color.tint.withAlphaComponent(CGFloat(opacity)),
|
|
30
|
-
roughness: 1,
|
|
31
|
-
isMetallic: false
|
|
32
|
-
)
|
|
33
|
-
entity.model?.materials[0] = newMaterial
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private func updateSpheres(deviceTargetInfo: DeviceTargetInfo, cameraTransform: simd_float4x4, completion: @escaping (Bool) -> Void) {
|
|
38
|
-
var completionCalled = false
|
|
39
|
-
|
|
40
|
-
func safeCompletion(_ result: Bool) {
|
|
41
|
-
guard !completionCalled else {
|
|
42
|
-
print("Completion already called")
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
completionCalled = true
|
|
46
|
-
completion(result)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Validate spheres initialization
|
|
50
|
-
guard ReplateCameraView.spheresModels.count >= 144 else {
|
|
51
|
-
print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
|
|
52
|
-
safeCompletion(false)
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Get anchor
|
|
57
|
-
guard let anchorNode = ReplateCameraView.anchorEntity else {
|
|
58
|
-
print("[updateSpheres] No anchor entity found.")
|
|
59
|
-
safeCompletion(false)
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Calculate camera metrics
|
|
64
|
-
let cameraDistance = isCameraWithinRange(
|
|
65
|
-
cameraTransform: cameraTransform,
|
|
66
|
-
anchorEntity: anchorNode
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// Calculate sphere index
|
|
70
|
-
let angleDegrees = Self.angleBetweenAnchorXAndCamera(
|
|
71
|
-
anchor: anchorNode,
|
|
72
|
-
cameraTransform: cameraTransform
|
|
73
|
-
)
|
|
74
|
-
let sphereIndex = max(Int(round(angleDegrees / 5.0)), 0) % 72
|
|
75
|
-
|
|
76
|
-
DispatchQueue.main.async {
|
|
77
|
-
self.processSphereUpdate(
|
|
78
|
-
sphereIndex: sphereIndex,
|
|
79
|
-
targetIndex: deviceTargetInfo.targetIndex,
|
|
80
|
-
completion: safeCompletion
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private func processSphereUpdate(sphereIndex: Int, targetIndex: Int, completion: @escaping (Bool) -> Void) {
|
|
86
|
-
var mesh: ModelEntity?
|
|
87
|
-
var newAngle = false
|
|
88
|
-
var callback: RCTResponseSenderBlock?
|
|
89
|
-
|
|
90
|
-
if targetIndex == 1 {
|
|
91
|
-
guard sphereIndex < ReplateCameraView.upperSpheresSet.count else {
|
|
92
|
-
print("[updateSpheres] Sphere index out of bounds")
|
|
93
|
-
completion(false)
|
|
94
|
-
return
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if !ReplateCameraView.upperSpheresSet[sphereIndex] {
|
|
98
|
-
ReplateCameraView.upperSpheresSet[sphereIndex] = true
|
|
99
|
-
ReplateCameraView.photosFromDifferentAnglesTaken += 1
|
|
100
|
-
newAngle = true
|
|
101
|
-
|
|
102
|
-
guard 72 + sphereIndex < ReplateCameraView.spheresModels.count else {
|
|
103
|
-
print("[updateSpheres] Upper spheresModels index out of range")
|
|
104
|
-
completion(false)
|
|
105
|
-
return
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
|
|
109
|
-
|
|
110
|
-
if ReplateCameraView.upperSpheresSet.allSatisfy({ $0 }) {
|
|
111
|
-
callback = ReplateCameraController.completedUpperSpheresCallback
|
|
112
|
-
ReplateCameraController.completedUpperSpheresCallback = nil
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} else if targetIndex == 0 {
|
|
116
|
-
guard sphereIndex < ReplateCameraView.lowerSpheresSet.count else {
|
|
117
|
-
print("[updateSpheres] Lower sphere index out of range")
|
|
118
|
-
completion(false)
|
|
119
|
-
return
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if !ReplateCameraView.lowerSpheresSet[sphereIndex] {
|
|
123
|
-
ReplateCameraView.lowerSpheresSet[sphereIndex] = true
|
|
124
|
-
ReplateCameraView.photosFromDifferentAnglesTaken += 1
|
|
125
|
-
newAngle = true
|
|
126
|
-
|
|
127
|
-
guard sphereIndex < ReplateCameraView.spheresModels.count else {
|
|
128
|
-
print("[updateSpheres] Lower spheresModels index out of range")
|
|
129
|
-
completion(false)
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
mesh = ReplateCameraView.spheresModels[sphereIndex]
|
|
134
|
-
|
|
135
|
-
if ReplateCameraView.lowerSpheresSet.allSatisfy({ $0 }) {
|
|
136
|
-
callback = ReplateCameraController.completedLowerSpheresCallback
|
|
137
|
-
ReplateCameraController.completedLowerSpheresCallback = nil
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if let mesh = mesh {
|
|
143
|
-
let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
|
|
144
|
-
mesh.model?.materials[0] = material
|
|
145
|
-
ReplateCameraView.generateImpactFeedback(strength: .light)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
callback?([])
|
|
149
|
-
completion(newAngle)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// MARK: - Anchor Validation
|
|
153
|
-
private func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
|
|
154
|
-
var isValid = false
|
|
155
|
-
DispatchQueue.main.sync {
|
|
156
|
-
let transform = anchorNode.transform
|
|
157
|
-
let position = transform.translation
|
|
158
|
-
let rotation = transform.rotation
|
|
159
|
-
let scale = transform.scale
|
|
160
|
-
|
|
161
|
-
isValid = !position.isNaN &&
|
|
162
|
-
!rotation.isNaN &&
|
|
163
|
-
scale != SIMD3<Float>(0, 0, 0) &&
|
|
164
|
-
abs(length(rotation.vector) - 1.0) < 0.0001
|
|
165
|
-
}
|
|
166
|
-
return isValid
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// MARK: - Distance and Angle Calculations
|
|
170
|
-
private func isCameraWithinRange(cameraTransform: simd_float4x4, anchorEntity: AnchorEntity) -> Int {
|
|
171
|
-
var distance: Float = 0
|
|
172
|
-
|
|
173
|
-
DispatchQueue.main.sync {
|
|
174
|
-
let cameraPosition = SIMD3<Float>(
|
|
175
|
-
cameraTransform.columns.3.x,
|
|
176
|
-
cameraTransform.columns.3.y,
|
|
177
|
-
cameraTransform.columns.3.z
|
|
178
|
-
)
|
|
179
|
-
let anchorPosition = anchorEntity.transform.translation
|
|
180
|
-
distance = distanceBetween(cameraPosition, anchorPosition)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return distance <= Self.MIN_DISTANCE ? -1 :
|
|
184
|
-
distance >= Self.MAX_DISTANCE ? 1 : 0
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
private func distanceBetween(_ pos1: SIMD3<Float>, _ pos2: SIMD3<Float>) -> Float {
|
|
188
|
-
let difference = pos1 - pos2
|
|
189
|
-
return sqrt(dot(difference, difference))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// MARK: - Static Utility Methods
|
|
193
|
-
static func getTransformRelativeToAnchor(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> simd_float4x4 {
|
|
194
|
-
var relativeTransform: simd_float4x4!
|
|
195
|
-
DispatchQueue.main.sync {
|
|
196
|
-
let anchorTransform = anchor.transformMatrix(relativeTo: nil)
|
|
197
|
-
relativeTransform = anchorTransform.inverse * cameraTransform
|
|
198
|
-
}
|
|
199
|
-
return relativeTransform
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
static func angleBetweenAnchorXAndCamera(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> Float {
|
|
203
|
-
var angleDegrees: Float = 0
|
|
204
|
-
|
|
205
|
-
DispatchQueue.main.sync {
|
|
206
|
-
let anchorTransform = anchor.transform.matrix
|
|
207
|
-
let anchorPositionXZ = simd_float2(
|
|
208
|
-
anchor.transform.translation.x,
|
|
209
|
-
anchor.transform.translation.z
|
|
210
|
-
)
|
|
211
|
-
let relativeCameraPositionXZ = simd_float2(
|
|
212
|
-
cameraTransform.columns.3.x,
|
|
213
|
-
cameraTransform.columns.3.z
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
|
|
217
|
-
let anchorXAxisXZ = simd_float2(
|
|
218
|
-
anchorTransform.columns.0.x,
|
|
219
|
-
anchorTransform.columns.0.z
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
let angle = atan2(directionXZ.y, directionXZ.x) -
|
|
223
|
-
atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
|
|
224
|
-
|
|
225
|
-
angleDegrees = angle * (180.0 / .pi)
|
|
226
|
-
if angleDegrees < 0 {
|
|
227
|
-
angleDegrees += 360
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return angleDegrees
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
static func cgImage(from ciImage: CIImage) -> CGImage? {
|
|
235
|
-
let context = CIContext(options: nil)
|
|
236
|
-
return context.createCGImage(ciImage, from: ciImage.extent)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
static func saveImageAsJPEG(_ image: UIImage) -> URL? {
|
|
240
|
-
let cameraTransform = getCameraTransformString(from: ReplateCameraView.arView.session)
|
|
241
|
-
|
|
242
|
-
guard let imageData = image.jpegData(compressionQuality: 1),
|
|
243
|
-
let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
|
|
244
|
-
return nil
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
248
|
-
let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
|
|
249
|
-
let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
|
|
250
|
-
|
|
251
|
-
guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
|
|
252
|
-
return nil
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
var mutableMetadata = imageProperties
|
|
256
|
-
mutableMetadata[kCGImagePropertyExifDictionary] = [
|
|
257
|
-
kCGImagePropertyExifUserComment: cameraTransform
|
|
258
|
-
]
|
|
259
|
-
|
|
260
|
-
guard let destination = CGImageDestinationCreateWithURL(
|
|
261
|
-
fileURL as CFURL,
|
|
262
|
-
kUTTypeJPEG,
|
|
263
|
-
1,
|
|
264
|
-
nil
|
|
265
|
-
) else {
|
|
266
|
-
return nil
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
CGImageDestinationAddImageFromSource(
|
|
270
|
-
destination,
|
|
271
|
-
source,
|
|
272
|
-
0,
|
|
273
|
-
mutableMetadata as CFDictionary
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
guard CGImageDestinationFinalize(destination) else {
|
|
277
|
-
return nil
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return fileURL
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
static func getCameraTransformString(from session: ARSession) -> String? {
|
|
284
|
-
guard let currentFrame = session.currentFrame else {
|
|
285
|
-
return nil
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
let transform = currentFrame.camera.transform
|
|
289
|
-
return """
|
|
290
|
-
\(transform.columns.0.x),\(transform.columns.0.y),\(transform.columns.0.z),\(transform.columns.0.w);
|
|
291
|
-
\(transform.columns.1.x),\(transform.columns.1.y),\(transform.columns.1.z),\(transform.columns.1.w);
|
|
292
|
-
\(transform.columns.2.x),\(transform.columns.2.y),\(transform.columns.2.z),\(transform.columns.2.w);
|
|
293
|
-
\(transform.columns.3.x),\(transform.columns.3.y),\(transform.columns.3.z),\(transform.columns.3.w)
|
|
294
|
-
"""
|
|
295
|
-
}
|
|
296
|
-
}
|
|
@@ -1,363 +0,0 @@
|
|
|
1
|
-
import ARKit
|
|
2
|
-
import RealityKit
|
|
3
|
-
import UIKit
|
|
4
|
-
import AVFoundation
|
|
5
|
-
import ImageIO
|
|
6
|
-
import MobileCoreServices
|
|
7
|
-
|
|
8
|
-
guard !hasCalledBack else {
|
|
9
|
-
// MARK: - Supporting Types
|
|
10
|
-
enum ARError: Error {
|
|
11
|
-
case noAnchor
|
|
12
|
-
case invalidAnchor
|
|
13
|
-
case notInFocus
|
|
14
|
-
case captureError
|
|
15
|
-
case tooManyImages
|
|
16
|
-
case processingError
|
|
17
|
-
case savingError
|
|
18
|
-
case transformError
|
|
19
|
-
case lightingError
|
|
20
|
-
case unknown
|
|
21
|
-
|
|
22
|
-
var localizedDescription: String {
|
|
23
|
-
switch self {
|
|
24
|
-
case .noAnchor: return "[ReplateCameraController] No anchor set yet"
|
|
25
|
-
case .invalidAnchor: return "[ReplateCameraController] AnchorNode is not valid"
|
|
26
|
-
case .notInFocus: return "[ReplateCameraController] Object not in focus"
|
|
27
|
-
case .captureError: return "[ReplateCameraController] Error capturing image"
|
|
28
|
-
case .tooManyImages: return "[ReplateCameraController] Too many images and the last one's not from a new angle"
|
|
29
|
-
case .processingError: return "[ReplateCameraController] Error processing image"
|
|
30
|
-
case .savingError: return "[ReplateCameraController] Error saving photo"
|
|
31
|
-
case .transformError: return "[ReplateCameraController] Camera transform data not available"
|
|
32
|
-
case .lightingError: return "[ReplateCameraController] Image too dark"
|
|
33
|
-
case .unknown: return "[ReplateCameraController] Unknown error occurred"
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
struct DeviceTargetInfo {
|
|
39
|
-
let isInFocus: Bool
|
|
40
|
-
let targetIndex: Int
|
|
41
|
-
let transform: simd_float4x4
|
|
42
|
-
let cameraPosition: SIMD3<Float>
|
|
43
|
-
let deviceDirection: SIMD3<Float>
|
|
44
|
-
|
|
45
|
-
var isValidTarget: Bool {
|
|
46
|
-
return targetIndex != -1
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
class SafeCallbackHandler {
|
|
51
|
-
private let resolver: RCTPromiseResolveBlock
|
|
52
|
-
private let rejecter: RCTPromiseRejectBlock
|
|
53
|
-
private var hasCalledBack = false
|
|
54
|
-
private let lock = NSLock()
|
|
55
|
-
|
|
56
|
-
init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
|
57
|
-
self.resolver = resolver
|
|
58
|
-
self.rejecter = rejecter
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
func resolve(_ result: Any) {
|
|
62
|
-
lock.lock()
|
|
63
|
-
defer { lock.unlock() }
|
|
64
|
-
|
|
65
|
-
print("resolver: Callback already invoked.")
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
hasCalledBack = true
|
|
69
|
-
resolver(result)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
func reject(_ error: ARError) {
|
|
73
|
-
lock.lock()
|
|
74
|
-
defer { lock.unlock() }
|
|
75
|
-
|
|
76
|
-
guard !hasCalledBack else {
|
|
77
|
-
print("rejecter: Callback already invoked.")
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
hasCalledBack = true
|
|
81
|
-
rejecter(
|
|
82
|
-
String(describing: error),
|
|
83
|
-
error.localizedDescription,
|
|
84
|
-
NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
|
|
85
|
-
)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// MARK: - Main Controller
|
|
90
|
-
@objc(ReplateCameraController)
|
|
91
|
-
class ReplateCameraController: NSObject {
|
|
92
|
-
// MARK: - Static Properties
|
|
93
|
-
private static let lock = NSLock()
|
|
94
|
-
private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
|
|
95
|
-
|
|
96
|
-
// Configuration Constants
|
|
97
|
-
private static let MIN_DISTANCE: Float = 0.15
|
|
98
|
-
private static let MAX_DISTANCE: Float = 0.45
|
|
99
|
-
private static let ANGLE_THRESHOLD: Float = 0.6
|
|
100
|
-
private static let TARGET_IMAGE_SIZE = CGSize(width: 1728, height: 1296)
|
|
101
|
-
private static let MIN_AMBIENT_INTENSITY: CGFloat = 650
|
|
102
|
-
|
|
103
|
-
// Callbacks
|
|
104
|
-
static var completedTutorialCallback: RCTResponseSenderBlock?
|
|
105
|
-
static var anchorSetCallback: RCTResponseSenderBlock?
|
|
106
|
-
static var completedUpperSpheresCallback: RCTResponseSenderBlock?
|
|
107
|
-
static var completedLowerSpheresCallback: RCTResponseSenderBlock?
|
|
108
|
-
static var openedTutorialCallback: RCTResponseSenderBlock?
|
|
109
|
-
static var tooCloseCallback: RCTResponseSenderBlock?
|
|
110
|
-
static var tooFarCallback: RCTResponseSenderBlock?
|
|
111
|
-
|
|
112
|
-
// MARK: - Callback Registration Methods
|
|
113
|
-
@objc(registerOpenedTutorialCallback:)
|
|
114
|
-
func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
115
|
-
Self.lock.lock()
|
|
116
|
-
defer { Self.lock.unlock() }
|
|
117
|
-
ReplateCameraController.openedTutorialCallback = callback
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
@objc(registerCompletedTutorialCallback:)
|
|
121
|
-
func registerCompletedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
122
|
-
Self.lock.lock()
|
|
123
|
-
defer { Self.lock.unlock() }
|
|
124
|
-
ReplateCameraController.completedTutorialCallback = callback
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
@objc(registerAnchorSetCallback:)
|
|
128
|
-
func registerAnchorSetCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
129
|
-
Self.lock.lock()
|
|
130
|
-
defer { Self.lock.unlock() }
|
|
131
|
-
ReplateCameraController.anchorSetCallback = callback
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
@objc(registerCompletedUpperSpheresCallback:)
|
|
135
|
-
func registerCompletedUpperSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
136
|
-
Self.lock.lock()
|
|
137
|
-
defer { Self.lock.unlock() }
|
|
138
|
-
ReplateCameraController.completedUpperSpheresCallback = callback
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
@objc(registerCompletedLowerSpheresCallback:)
|
|
142
|
-
func registerCompletedLowerSpheresCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
143
|
-
Self.lock.lock()
|
|
144
|
-
defer { Self.lock.unlock() }
|
|
145
|
-
ReplateCameraController.completedLowerSpheresCallback = callback
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
@objc(registerTooCloseCallback:)
|
|
149
|
-
func registerTooCloseCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
150
|
-
Self.lock.lock()
|
|
151
|
-
defer { Self.lock.unlock() }
|
|
152
|
-
ReplateCameraController.tooCloseCallback = callback
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
@objc(registerTooFarCallback:)
|
|
156
|
-
func registerTooFarCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
157
|
-
Self.lock.lock()
|
|
158
|
-
defer { Self.lock.unlock() }
|
|
159
|
-
ReplateCameraController.tooFarCallback = callback
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// MARK: - Public Methods
|
|
163
|
-
@objc(getPhotosCount:rejecter:)
|
|
164
|
-
func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
165
|
-
Self.lock.lock()
|
|
166
|
-
let count = ReplateCameraView.totalPhotosTaken
|
|
167
|
-
Self.lock.unlock()
|
|
168
|
-
resolver(count)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
@objc(isScanComplete:rejecter:)
|
|
172
|
-
func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
173
|
-
Self.lock.lock()
|
|
174
|
-
let isComplete = ReplateCameraView.photosFromDifferentAnglesTaken == 144
|
|
175
|
-
Self.lock.unlock()
|
|
176
|
-
resolver(isComplete)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
@objc(getRemainingAnglesToScan:rejecter:)
|
|
180
|
-
func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
181
|
-
Self.lock.lock()
|
|
182
|
-
let remaining = 144 - ReplateCameraView.photosFromDifferentAnglesTaken
|
|
183
|
-
Self.lock.unlock()
|
|
184
|
-
resolver(remaining)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
@objc
|
|
188
|
-
func reset() {
|
|
189
|
-
DispatchQueue.main.async {
|
|
190
|
-
ReplateCameraView.INSTANCE.reset()
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// MARK: - Photo Capture and Processing
|
|
195
|
-
@objc(takePhoto:resolver:rejecter:)
|
|
196
|
-
func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
|
197
|
-
Self.arQueue.async { [weak self] in
|
|
198
|
-
guard let self = self else { return }
|
|
199
|
-
|
|
200
|
-
let callbackHandler = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
|
|
201
|
-
|
|
202
|
-
do {
|
|
203
|
-
try self.validateAndProcessPhoto(unlimited: unlimited, callbackHandler: callbackHandler)
|
|
204
|
-
} catch let error as ARError {
|
|
205
|
-
callbackHandler.reject(error)
|
|
206
|
-
} catch {
|
|
207
|
-
callbackHandler.reject(.unknown)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private func validateAndProcessPhoto(unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
|
|
213
|
-
let anchorEntity = try getValidAnchorEntity()
|
|
214
|
-
let deviceTargetInfo = try getDeviceTargetInfo(anchorEntity: anchorEntity)
|
|
215
|
-
|
|
216
|
-
if deviceTargetInfo.isValidTarget {
|
|
217
|
-
try processTargetedDevice(deviceTargetInfo: deviceTargetInfo, unlimited: unlimited, callbackHandler: callbackHandler)
|
|
218
|
-
} else {
|
|
219
|
-
throw ARError.notInFocus
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
private func getValidAnchorEntity() throws -> AnchorEntity {
|
|
224
|
-
var anchorEntity: AnchorEntity?
|
|
225
|
-
|
|
226
|
-
DispatchQueue.main.sync {
|
|
227
|
-
anchorEntity = ReplateCameraView.anchorEntity
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
guard let anchor = anchorEntity else {
|
|
231
|
-
throw ARError.noAnchor
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
guard isAnchorNodeValid(anchor) else {
|
|
235
|
-
throw ARError.invalidAnchor
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return anchor
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private func getDeviceTargetInfo(anchorEntity: AnchorEntity) throws -> DeviceTargetInfo {
|
|
242
|
-
guard let frame = ReplateCameraView.arView.session.currentFrame else {
|
|
243
|
-
throw ARError.transformError
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
let cameraTransform = frame.camera.transform
|
|
247
|
-
let relativeCameraTransform = Self.getTransformRelativeToAnchor(anchor: anchorEntity, cameraTransform: cameraTransform)
|
|
248
|
-
|
|
249
|
-
var anchorPosition: SIMD3<Float>!
|
|
250
|
-
DispatchQueue.main.sync {
|
|
251
|
-
anchorPosition = anchorEntity.position(relativeTo: nil)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
|
|
255
|
-
let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
|
|
256
|
-
let directionToAnchor = normalize(anchorPosition - cameraPosition)
|
|
257
|
-
let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
|
|
258
|
-
|
|
259
|
-
let targetIndex = determineTargetIndex(angleToAnchor: angleToAnchor, relativeCameraTransform: relativeCameraTransform)
|
|
260
|
-
|
|
261
|
-
return DeviceTargetInfo(
|
|
262
|
-
isInFocus: angleToAnchor < Self.ANGLE_THRESHOLD,
|
|
263
|
-
targetIndex: targetIndex,
|
|
264
|
-
transform: relativeCameraTransform,
|
|
265
|
-
cameraPosition: cameraPosition,
|
|
266
|
-
deviceDirection: deviceDirection
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private func determineTargetIndex(angleToAnchor: Float, relativeCameraTransform: simd_float4x4) -> Int {
|
|
271
|
-
guard angleToAnchor < Self.ANGLE_THRESHOLD else { return -1 }
|
|
272
|
-
|
|
273
|
-
let spheresHeight = ReplateCameraView.spheresHeight
|
|
274
|
-
let distanceBetweenCircles = ReplateCameraView.distanceBetweenCircles
|
|
275
|
-
let twoThirdsDistance = spheresHeight + distanceBetweenCircles + distanceBetweenCircles/5
|
|
276
|
-
let cameraHeight = relativeCameraTransform.columns.3.y
|
|
277
|
-
|
|
278
|
-
return cameraHeight < twoThirdsDistance ? 0 : 1
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private func processTargetedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
|
|
282
|
-
DispatchQueue.main.async { [weak self] in
|
|
283
|
-
guard let self = self else { return }
|
|
284
|
-
|
|
285
|
-
self.updateCircleFocus(targetIndex: deviceTargetInfo.targetIndex)
|
|
286
|
-
self.checkCameraDistance(deviceTargetInfo: deviceTargetInfo)
|
|
287
|
-
|
|
288
|
-
self.updateSpheres(
|
|
289
|
-
deviceTargetInfo: deviceTargetInfo,
|
|
290
|
-
cameraTransform: deviceTargetInfo.transform
|
|
291
|
-
) { success in
|
|
292
|
-
if !unlimited && !success {
|
|
293
|
-
callbackHandler.reject(.tooManyImages)
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
self.captureAndProcessImage(callbackHandler: callbackHandler)
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
private func updateCircleFocus(targetIndex: Int) {
|
|
303
|
-
if targetIndex != ReplateCameraView.circleInFocus {
|
|
304
|
-
setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
|
|
305
|
-
setOpacityToCircle(circleId: targetIndex, opacity: 1)
|
|
306
|
-
ReplateCameraView.circleInFocus = targetIndex
|
|
307
|
-
ReplateCameraView.generateImpactFeedback(strength: .heavy)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private func checkCameraDistance(deviceTargetInfo: DeviceTargetInfo) {
|
|
312
|
-
let distance = isCameraWithinRange(
|
|
313
|
-
cameraTransform: deviceTargetInfo.transform,
|
|
314
|
-
anchorEntity: ReplateCameraView.anchorEntity!
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
switch distance {
|
|
318
|
-
case 1:
|
|
319
|
-
ReplateCameraController.tooFarCallback?([])
|
|
320
|
-
case -1:
|
|
321
|
-
ReplateCameraController.tooCloseCallback?([])
|
|
322
|
-
default:
|
|
323
|
-
break
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// MARK: - Image Processing
|
|
328
|
-
private func captureAndProcessImage(callbackHandler: SafeCallbackHandler) {
|
|
329
|
-
guard let frame = ReplateCameraView.arView?.session.currentFrame else {
|
|
330
|
-
callbackHandler.reject(.captureError)
|
|
331
|
-
return
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// Check lighting conditions
|
|
335
|
-
if let lightEstimate = frame.lightEstimate {
|
|
336
|
-
guard lightEstimate.ambientIntensity >= Self.MIN_AMBIENT_INTENSITY else {
|
|
337
|
-
callbackHandler.reject(.lightingError)
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
processAndSaveImage(frame.capturedImage, callbackHandler: callbackHandler)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
private func processAndSaveImage(_ pixelBuffer: CVPixelBuffer, callbackHandler: SafeCallbackHandler) {
|
|
346
|
-
let ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
|
347
|
-
|
|
348
|
-
guard let resizedImage = resizeImage(ciImage, to: Self.TARGET_IMAGE_SIZE),
|
|
349
|
-
let cgImage = Self.cgImage(from: resizedImage),
|
|
350
|
-
let rotatedImage = UIImage(cgImage: cgImage).rotate(radians: .pi / 2),
|
|
351
|
-
let savedURL = Self.saveImageAsJPEG(rotatedImage) else {
|
|
352
|
-
callbackHandler.reject(.processingError)
|
|
353
|
-
return
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
callbackHandler.resolve(savedURL.absoluteString)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
private func resizeImage(_ image: CIImage, to targetSize: CGSize) -> CIImage? {
|
|
360
|
-
guard let scaleFilter = CIFilter(name: "CILanczosScaleTransform") else { return nil }
|
|
361
|
-
|
|
362
|
-
scaleFilter.setValue(image, forKey: kCIInputImageKey)
|
|
363
|
-
scaleFilter.setValue(targetSize.
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
@objc(ReplateCameraController)
|
|
2
|
-
class ReplateCameraController: NSObject {
|
|
3
|
-
// MARK: - Static Properties
|
|
4
|
-
private static let lock = NSLock()
|
|
5
|
-
private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
|
|
6
|
-
|
|
7
|
-
// Callback Properties
|
|
8
|
-
static var completedTutorialCallback: RCTResponseSenderBlock?
|
|
9
|
-
static var anchorSetCallback: RCTResponseSenderBlock?
|
|
10
|
-
static var completedUpperSpheresCallback: RCTResponseSenderBlock?
|
|
11
|
-
static var completedLowerSpheresCallback: RCTResponseSenderBlock?
|
|
12
|
-
static var openedTutorialCallback: RCTResponseSenderBlock?
|
|
13
|
-
static var tooCloseCallback: RCTResponseSenderBlock?
|
|
14
|
-
static var tooFarCallback: RCTResponseSenderBlock?
|
|
15
|
-
|
|
16
|
-
// Constants
|
|
17
|
-
private static let MIN_DISTANCE: Float = 0.15
|
|
18
|
-
private static let MAX_DISTANCE: Float = 0.45
|
|
19
|
-
private static let ANGLE_THRESHOLD: Float = 0.6
|
|
20
|
-
|
|
21
|
-
// MARK: - Callback Registration
|
|
22
|
-
@objc(registerOpenedTutorialCallback:)
|
|
23
|
-
func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
24
|
-
Self.lock.lock()
|
|
25
|
-
defer { Self.lock.unlock() }
|
|
26
|
-
ReplateCameraController.openedTutorialCallback = callback
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Similar registrations for other callbacks...
|
|
30
|
-
|
|
31
|
-
// MARK: - Public Methods
|
|
32
|
-
@objc(getPhotosCount:rejecter:)
|
|
33
|
-
func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
34
|
-
Self.lock.lock()
|
|
35
|
-
let count = ReplateCameraView.totalPhotosTaken
|
|
36
|
-
Self.lock.unlock()
|
|
37
|
-
resolver(count)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@objc(takePhoto:resolver:rejecter:)
|
|
41
|
-
func takePhoto(_ unlimited: Bool = false, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
|
42
|
-
Self.arQueue.async { [weak self] in
|
|
43
|
-
guard let self = self else { return }
|
|
44
|
-
|
|
45
|
-
let callbackHandler = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
|
|
46
|
-
|
|
47
|
-
do {
|
|
48
|
-
try self.validateAndProcessPhoto(unlimited: unlimited, callbackHandler: callbackHandler)
|
|
49
|
-
} catch let error as ARError {
|
|
50
|
-
callbackHandler.reject(error)
|
|
51
|
-
} catch {
|
|
52
|
-
callbackHandler.reject(.unknown)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// MARK: - Private Methods
|
|
58
|
-
private func validateAndProcessPhoto(unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
|
|
59
|
-
guard let anchorEntity = getValidAnchorEntity() else {
|
|
60
|
-
throw ARError.noAnchor
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let deviceTargetInfo = try getDeviceTargetInfo(anchorEntity: anchorEntity)
|
|
64
|
-
|
|
65
|
-
if deviceTargetInfo.isInFocus {
|
|
66
|
-
handleFocusedDevice(deviceTargetInfo: deviceTargetInfo, unlimited: unlimited, callbackHandler: callbackHandler)
|
|
67
|
-
} else {
|
|
68
|
-
throw ARError.notInFocus
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private func getValidAnchorEntity() -> AnchorEntity? {
|
|
73
|
-
var anchorEntity: AnchorEntity?
|
|
74
|
-
DispatchQueue.main.sync {
|
|
75
|
-
anchorEntity = ReplateCameraView.anchorEntity
|
|
76
|
-
if let anchor = anchorEntity {
|
|
77
|
-
guard isAnchorNodeValid(anchor) else {
|
|
78
|
-
return nil
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return anchorEntity
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
private func handleFocusedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) {
|
|
86
|
-
DispatchQueue.main.async {
|
|
87
|
-
self.updateCircleFocus(deviceTargetInfo: deviceTargetInfo)
|
|
88
|
-
|
|
89
|
-
self.updateSpheres(deviceTargetInfo: deviceTargetInfo) { success in
|
|
90
|
-
if !unlimited && !success {
|
|
91
|
-
callbackHandler.reject(.tooManyImages)
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
self.captureAndProcessImage(callbackHandler: callbackHandler)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private func captureAndProcessImage(callbackHandler: SafeCallbackHandler) {
|
|
101
|
-
guard let image = ReplateCameraView.arView?.session.currentFrame?.capturedImage else {
|
|
102
|
-
callbackHandler.reject(.captureError)
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
processAndSaveImage(image, callbackHandler: callbackHandler)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private func processAndSaveImage(_ pixelBuffer: CVPixelBuffer, callbackHandler: SafeCallbackHandler) {
|
|
110
|
-
// Image processing implementation...
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// MARK: - Entity Management
|
|
114
|
-
private func setOpacityToCircle(circleId: Int, opacity: Float) {
|
|
115
|
-
DispatchQueue.main.async {
|
|
116
|
-
for i in 0..<72 {
|
|
117
|
-
let offset = circleId == 0 ? 0 : 72
|
|
118
|
-
guard let entity = ReplateCameraView.spheresModels[safe: i + offset] else { continue }
|
|
119
|
-
|
|
120
|
-
self.updateEntityMaterial(entity, opacity: opacity)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
private func updateEntityMaterial(_ entity: ModelEntity, opacity: Float) {
|
|
126
|
-
guard let material = entity.model?.materials.first as? SimpleMaterial else { return }
|
|
127
|
-
|
|
128
|
-
if #available(iOS 15.0, *) {
|
|
129
|
-
let newMaterial = SimpleMaterial(
|
|
130
|
-
color: material.color.tint.withAlphaComponent(CGFloat(opacity)),
|
|
131
|
-
roughness: 1,
|
|
132
|
-
isMetallic: false
|
|
133
|
-
)
|
|
134
|
-
entity.model?.materials[0] = newMaterial
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// MARK: - Utility Methods
|
|
139
|
-
private func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
|
|
140
|
-
var isValid = false
|
|
141
|
-
DispatchQueue.main.sync {
|
|
142
|
-
let transform = anchorNode.transform
|
|
143
|
-
let position = transform.translation
|
|
144
|
-
let rotation = transform.rotation
|
|
145
|
-
let scale = transform.scale
|
|
146
|
-
|
|
147
|
-
isValid = !position.isNaN &&
|
|
148
|
-
!rotation.isNaN &&
|
|
149
|
-
scale != SIMD3<Float>(0, 0, 0) &&
|
|
150
|
-
abs(length(rotation.vector) - 1.0) < 0.0001
|
|
151
|
-
}
|
|
152
|
-
return isValid
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// MARK: - Supporting Types
|
|
157
|
-
extension ReplateCameraController {
|
|
158
|
-
struct DeviceTargetInfo {
|
|
159
|
-
let isInFocus: Bool
|
|
160
|
-
let targetIndex: Int
|
|
161
|
-
let transform: simd_float4x4
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
enum ARError: Error {
|
|
165
|
-
case noAnchor
|
|
166
|
-
case invalidAnchor
|
|
167
|
-
case notInFocus
|
|
168
|
-
case captureError
|
|
169
|
-
case tooManyImages
|
|
170
|
-
case unknown
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
class SafeCallbackHandler {
|
|
174
|
-
private let resolver: RCTPromiseResolveBlock
|
|
175
|
-
private let rejecter: RCTPromiseRejectBlock
|
|
176
|
-
private var hasCalledBack = false
|
|
177
|
-
private let lock = NSLock()
|
|
178
|
-
|
|
179
|
-
init(resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock) {
|
|
180
|
-
self.resolver = resolver
|
|
181
|
-
self.rejecter = rejecter
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
func resolve(_ result: Any) {
|
|
185
|
-
lock.lock()
|
|
186
|
-
defer { lock.unlock() }
|
|
187
|
-
|
|
188
|
-
guard !hasCalledBack else { return }
|
|
189
|
-
hasCalledBack = true
|
|
190
|
-
resolver(result)
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
func reject(_ error: ARError) {
|
|
194
|
-
lock.lock()
|
|
195
|
-
defer { lock.unlock() }
|
|
196
|
-
|
|
197
|
-
guard !hasCalledBack else { return }
|
|
198
|
-
hasCalledBack = true
|
|
199
|
-
rejecter(
|
|
200
|
-
String(describing: error),
|
|
201
|
-
error.localizedDescription,
|
|
202
|
-
NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// MARK: - Array Extension
|
|
209
|
-
extension Array {
|
|
210
|
-
subscript(safe index: Index) -> Element? {
|
|
211
|
-
indices.contains(index) ? self[index] : nil
|
|
212
|
-
}
|
|
213
|
-
}
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
import ARKit
|
|
2
|
-
import RealityKit
|
|
3
|
-
import UIKit
|
|
4
|
-
import AVFoundation
|
|
5
|
-
import ImageIO
|
|
6
|
-
import MobileCoreServices
|
|
7
|
-
|
|
8
|
-
// MARK: - RCTViewManager
|
|
9
|
-
@objc(ReplateCameraViewManager)
|
|
10
|
-
class ReplateCameraViewManager: RCTViewManager {
|
|
11
|
-
override func view() -> ReplateCameraView {
|
|
12
|
-
return ReplateCameraView()
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@objc override static func requiresMainQueueSetup() -> Bool {
|
|
16
|
-
return false
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// MARK: - UIImage Extension
|
|
21
|
-
extension UIImage {
|
|
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
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// MARK: - ReplateCameraView
|
|
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)
|
|
50
|
-
|
|
51
|
-
// AR View and Core Components
|
|
52
|
-
static var arView: ARView!
|
|
53
|
-
static var anchorEntity: AnchorEntity?
|
|
54
|
-
static var model: Entity!
|
|
55
|
-
static var sessionId: UUID!
|
|
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)
|
|
67
|
-
static var sphereRadius = Float(0.004)
|
|
68
|
-
static var spheresRadius = Float(0.13)
|
|
69
|
-
static var sphereAngle = Float(5)
|
|
70
|
-
static var spheresHeight = Float(0.10)
|
|
71
|
-
static var distanceBetweenCircles = Float(0.10)
|
|
72
|
-
|
|
73
|
-
// Focus and Navigation
|
|
74
|
-
static var focusModel: Entity!
|
|
75
|
-
static var circleInFocus = 0
|
|
76
|
-
static var dragSpeed = CGFloat(7000)
|
|
77
|
-
static var dotAnchors: [AnchorEntity] = []
|
|
78
|
-
|
|
79
|
-
// Statistics
|
|
80
|
-
static var totalPhotosTaken = 0
|
|
81
|
-
static var photosFromDifferentAnglesTaken = 0
|
|
82
|
-
|
|
83
|
-
// Thread Safety
|
|
84
|
-
private static var isResetting = false
|
|
85
|
-
|
|
86
|
-
// MARK: - Initialization
|
|
87
|
-
override init(frame: CGRect) {
|
|
88
|
-
super.init(frame: frame)
|
|
89
|
-
setupInitialState()
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
required init?(coder: NSCoder) {
|
|
93
|
-
super.init(coder: coder)
|
|
94
|
-
setupInitialState()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
private func setupInitialState() {
|
|
98
|
-
requestCameraPermissions()
|
|
99
|
-
ReplateCameraView.INSTANCE = self
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// MARK: - Layout
|
|
103
|
-
override func layoutSubviews() {
|
|
104
|
-
super.layoutSubviews()
|
|
105
|
-
Self.lock.lock()
|
|
106
|
-
defer { Self.lock.unlock() }
|
|
107
|
-
|
|
108
|
-
ReplateCameraView.width = frame.width
|
|
109
|
-
ReplateCameraView.height = frame.height
|
|
110
|
-
}
|
|
111
|
-
|
|
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()
|
|
156
|
-
} else {
|
|
157
|
-
configuration.videoFormat = highestResolutionFormat()
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
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
|
-
// MARK: - Entity Management
|
|
183
|
-
func createSpheres(y: Float) {
|
|
184
|
-
DispatchQueue.main.async {
|
|
185
|
-
let radius = ReplateCameraView.spheresRadius
|
|
186
|
-
|
|
187
|
-
for i in 0..<72 {
|
|
188
|
-
let angle = Float(i) * (Float.pi / 180) * 5
|
|
189
|
-
let position = SIMD3(
|
|
190
|
-
radius * cos(angle),
|
|
191
|
-
y,
|
|
192
|
-
radius * sin(angle)
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
let sphere = self.createSphere(position: position)
|
|
196
|
-
ReplateCameraView.spheresModels.append(sphere)
|
|
197
|
-
ReplateCameraView.anchorEntity?.addChild(sphere)
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
func createSphere(position: SIMD3<Float>) -> ModelEntity {
|
|
203
|
-
let sphereMesh = MeshResource.generateSphere(radius: ReplateCameraView.sphereRadius)
|
|
204
|
-
let material = SimpleMaterial(color: .white.withAlphaComponent(1), roughness: 1, isMetallic: false)
|
|
205
|
-
let sphere = ModelEntity(mesh: sphereMesh, materials: [material])
|
|
206
|
-
sphere.position = position
|
|
207
|
-
return sphere
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// MARK: - Reset Functionality
|
|
211
|
-
@objc static func reset() {
|
|
212
|
-
arQueue.async {
|
|
213
|
-
Self.lock.lock()
|
|
214
|
-
defer { Self.lock.unlock() }
|
|
215
|
-
|
|
216
|
-
if isResetting { return }
|
|
217
|
-
isResetting = true
|
|
218
|
-
|
|
219
|
-
DispatchQueue.main.async {
|
|
220
|
-
tearDownARSession()
|
|
221
|
-
resetProperties()
|
|
222
|
-
setupNewARView()
|
|
223
|
-
isResetting = false
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
private static func tearDownARSession() {
|
|
229
|
-
arView?.session.pause()
|
|
230
|
-
arView?.session.delegate = nil
|
|
231
|
-
arView?.scene.anchors.removeAll()
|
|
232
|
-
arView?.removeFromSuperview()
|
|
233
|
-
arView?.window?.resignKey()
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
private static func resetProperties() {
|
|
237
|
-
anchorEntity = nil
|
|
238
|
-
model = nil
|
|
239
|
-
spheresModels.removeAll()
|
|
240
|
-
upperSpheresSet = [Bool](repeating: false, count: 72)
|
|
241
|
-
lowerSpheresSet = [Bool](repeating: false, count: 72)
|
|
242
|
-
totalPhotosTaken = 0
|
|
243
|
-
photosFromDifferentAnglesTaken = 0
|
|
244
|
-
sphereRadius = 0.004
|
|
245
|
-
spheresRadius = 0.13
|
|
246
|
-
sphereAngle = 5
|
|
247
|
-
spheresHeight = 0.10
|
|
248
|
-
dragSpeed = 7000
|
|
249
|
-
dotAnchors.removeAll()
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
private static func setupNewARView() {
|
|
253
|
-
guard let instance = INSTANCE else { return }
|
|
254
|
-
|
|
255
|
-
arView = ARView(frame: CGRect(x: 0, y: 0, width: width, height: height))
|
|
256
|
-
arView.backgroundColor = instance.hexStringToUIColor(hexColor: "#32a852")
|
|
257
|
-
instance.addSubview(arView)
|
|
258
|
-
arView.session.delegate = instance
|
|
259
|
-
setupAR()
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Continue with the rest of your implementation...
|
|
263
|
-
// Including ARSessionDelegate methods, gesture handlers, and utility functions
|
|
264
|
-
}
|