replate-camera 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios/ReplateCameraViewManager.swift +651 -1339
- package/package.json +1 -1
|
@@ -12,7 +12,6 @@ class ReplateCameraViewManager: RCTViewManager {
|
|
|
12
12
|
override func view() -> ReplateCameraView {
|
|
13
13
|
return ReplateCameraView()
|
|
14
14
|
}
|
|
15
|
-
|
|
16
15
|
@objc override static func requiresMainQueueSetup() -> Bool {
|
|
17
16
|
return false
|
|
18
17
|
}
|
|
@@ -28,690 +27,327 @@ extension UIImage {
|
|
|
28
27
|
|
|
29
28
|
UIGraphicsBeginImageContextWithOptions(newSize, false, self.scale)
|
|
30
29
|
guard let context = UIGraphicsGetCurrentContext() else { return self }
|
|
31
|
-
|
|
32
|
-
context.translateBy(x: newSize.width / 2, y: newSize.height / 2)
|
|
30
|
+
context.translateBy(x: newSize.width/2, y: newSize.height/2)
|
|
33
31
|
context.rotate(by: CGFloat(radians))
|
|
34
|
-
self.draw(in: CGRect(x: -self.size.width
|
|
35
|
-
y: -self.size.height
|
|
32
|
+
self.draw(in: CGRect(x: -self.size.width/2,
|
|
33
|
+
y: -self.size.height/2,
|
|
36
34
|
width: self.size.width,
|
|
37
35
|
height: self.size.height))
|
|
38
|
-
|
|
39
36
|
let newImage = UIGraphicsGetImageFromCurrentImageContext() ?? self
|
|
40
37
|
UIGraphicsEndImageContext()
|
|
41
|
-
|
|
42
38
|
return newImage
|
|
43
39
|
}
|
|
44
40
|
}
|
|
45
41
|
|
|
42
|
+
// MARK: - Safe Array Access
|
|
43
|
+
extension Array {
|
|
44
|
+
subscript(safe index: Index) -> Element? {
|
|
45
|
+
return indices.contains(index) ? self[index] : nil
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
46
49
|
// MARK: - ReplateCameraView
|
|
47
50
|
class ReplateCameraView: UIView, ARSessionDelegate {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
super.init(frame: frame)
|
|
92
|
-
setupInitialState()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
required init?(coder: NSCoder) {
|
|
96
|
-
super.init(coder: coder)
|
|
97
|
-
setupInitialState()
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private func setupInitialState() {
|
|
101
|
-
requestCameraPermissions()
|
|
102
|
-
ReplateCameraView.INSTANCE = self
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// MARK: - Layout
|
|
51
|
+
// MARK: Static
|
|
52
|
+
private static let lock = NSLock()
|
|
53
|
+
private static let arQueue = DispatchQueue(label: "com.replate.ar", qos: .userInteractive)
|
|
54
|
+
static var arView: ARView!
|
|
55
|
+
static var anchorEntity: AnchorEntity?
|
|
56
|
+
static var sessionId: UUID!
|
|
57
|
+
static var motionManager: CMMotionManager!
|
|
58
|
+
static var gravityVector: [String: Double] = [:]
|
|
59
|
+
static var INSTANCE: ReplateCameraView!
|
|
60
|
+
static var width: CGFloat = 0
|
|
61
|
+
static var height: CGFloat = 0
|
|
62
|
+
|
|
63
|
+
// Sphere state
|
|
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 spheresRadius: Float = 0.13
|
|
68
|
+
static var spheresHeight: Float = 0.10
|
|
69
|
+
static var distanceBetweenCircles: Float = 0.10
|
|
70
|
+
static var dotAnchors: [AnchorEntity] = []
|
|
71
|
+
|
|
72
|
+
// Gesture throttle
|
|
73
|
+
private var lastPinchTime: TimeInterval = 0
|
|
74
|
+
|
|
75
|
+
// Cached mesh + material
|
|
76
|
+
private static let baseSphereMesh = MeshResource.generateSphere(radius: 0.004)
|
|
77
|
+
private static let baseSphereMaterial = SimpleMaterial(color: .white, roughness: 1, isMetallic: false)
|
|
78
|
+
|
|
79
|
+
// MARK: Init
|
|
80
|
+
override init(frame: CGRect) {
|
|
81
|
+
super.init(frame: frame)
|
|
82
|
+
setupInitialState()
|
|
83
|
+
}
|
|
84
|
+
required init?(coder: NSCoder) {
|
|
85
|
+
super.init(coder: coder)
|
|
86
|
+
setupInitialState()
|
|
87
|
+
}
|
|
88
|
+
private func setupInitialState() {
|
|
89
|
+
requestCameraPermissions()
|
|
90
|
+
ReplateCameraView.INSTANCE = self
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// MARK: Layout
|
|
106
94
|
override func layoutSubviews() {
|
|
107
95
|
super.layoutSubviews()
|
|
108
|
-
Self.lock.lock()
|
|
109
|
-
defer { Self.lock.unlock() }
|
|
110
|
-
|
|
96
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
111
97
|
ReplateCameraView.width = frame.width
|
|
112
98
|
ReplateCameraView.height = frame.height
|
|
113
99
|
}
|
|
114
100
|
|
|
115
|
-
// MARK:
|
|
101
|
+
// MARK: AR Setup
|
|
116
102
|
static func setupAR() {
|
|
117
103
|
arQueue.async {
|
|
118
|
-
guard let
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
configuration.planeDetection = .horizontal
|
|
123
|
-
|
|
104
|
+
guard let inst = INSTANCE else { return }
|
|
105
|
+
let config = ARWorldTrackingConfiguration()
|
|
106
|
+
config.isLightEstimationEnabled = true
|
|
107
|
+
config.planeDetection = .horizontal
|
|
124
108
|
DispatchQueue.main.async {
|
|
125
|
-
configureARView(
|
|
109
|
+
configureARView(config)
|
|
126
110
|
addRecognizers()
|
|
127
111
|
}
|
|
128
112
|
}
|
|
129
113
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
configureRenderOptions()
|
|
133
|
-
configureVideoFormat(configuration)
|
|
134
|
-
|
|
135
|
-
arView.session.run(configuration)
|
|
114
|
+
private static func configureARView(_ config: ARWorldTrackingConfiguration) {
|
|
115
|
+
arView.session.run(config)
|
|
136
116
|
arView.addCoaching()
|
|
137
117
|
sessionId = arView.session.identifier
|
|
138
118
|
}
|
|
139
|
-
|
|
140
|
-
private static func configureRenderOptions() {
|
|
141
|
-
let renderOptions: [ARView.RenderOptions] = [
|
|
142
|
-
.disableMotionBlur,
|
|
143
|
-
.disableCameraGrain,
|
|
144
|
-
.disableAREnvironmentLighting,
|
|
145
|
-
.disableHDR,
|
|
146
|
-
.disableFaceMesh,
|
|
147
|
-
.disableGroundingShadows,
|
|
148
|
-
.disablePersonOcclusion
|
|
149
|
-
]
|
|
150
|
-
|
|
151
|
-
renderOptions.forEach { arView.renderOptions.insert($0) }
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private static func configureVideoFormat(_ configuration: ARWorldTrackingConfiguration) {
|
|
155
|
-
if #available(iOS 16.0, *) {
|
|
156
|
-
configuration.videoFormat = ARWorldTrackingConfiguration.recommendedVideoFormatForHighResolutionFrameCapturing
|
|
157
|
-
?? ARWorldTrackingConfiguration.recommendedVideoFormatFor4KResolution
|
|
158
|
-
?? highestResolutionFormat()
|
|
159
|
-
} else {
|
|
160
|
-
configuration.videoFormat = highestResolutionFormat()
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private static func highestResolutionFormat() -> ARConfiguration.VideoFormat {
|
|
165
|
-
return ARWorldTrackingConfiguration.supportedVideoFormats.max(by: { format1, format2 in
|
|
166
|
-
let resolution1 = format1.imageResolution.width * format1.imageResolution.height
|
|
167
|
-
let resolution2 = format2.imageResolution.width * format2.imageResolution.height
|
|
168
|
-
return resolution1 < resolution2
|
|
169
|
-
})!
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// MARK: - Gesture Recognition
|
|
173
119
|
static func addRecognizers() {
|
|
174
|
-
guard let
|
|
175
|
-
|
|
176
|
-
let
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
alignment: alignment)
|
|
194
|
-
|
|
195
|
-
guard let rayCast: ARRaycastResult = result.first
|
|
196
|
-
else {
|
|
197
|
-
return
|
|
198
|
-
}
|
|
199
|
-
let anchor = AnchorEntity(world: rayCast.worldTransform)
|
|
200
|
-
print("ANCHOR FOUND\n", anchor.transform)
|
|
201
|
-
let callback = ReplateCameraController.anchorSetCallback
|
|
202
|
-
if (callback != nil) {
|
|
203
|
-
callback!([])
|
|
204
|
-
ReplateCameraController.anchorSetCallback = nil
|
|
205
|
-
}
|
|
206
|
-
if (ReplateCameraView.model == nil && ReplateCameraView.anchorEntity == nil) {
|
|
207
|
-
DispatchQueue.main.async{
|
|
208
|
-
for dot in ReplateCameraView.dotAnchors {
|
|
209
|
-
dot.removeFromParent()
|
|
210
|
-
ReplateCameraView.arView.scene.removeAnchor(dot)
|
|
211
|
-
}
|
|
212
|
-
ReplateCameraView.dotAnchors = []
|
|
213
|
-
}
|
|
214
|
-
ReplateCameraView.anchorEntity = anchor
|
|
215
|
-
createSpheres(y: ReplateCameraView.spheresHeight)
|
|
216
|
-
createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
|
|
217
|
-
createFocusSphere()
|
|
218
|
-
guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
|
|
219
|
-
ReplateCameraView.arView.scene.anchors.append(anchorEntity)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
|
224
|
-
print("handle pan")
|
|
225
|
-
guard let sceneView = gestureRecognizer.view as? ARView else {
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
guard let anchorEntity = ReplateCameraView.anchorEntity else { return }
|
|
229
|
-
let cameraTransform = sceneView.cameraTransform.matrix // Get the 4x4 matrix
|
|
230
|
-
print("passed guard")
|
|
231
|
-
|
|
232
|
-
if gestureRecognizer.state == .changed {
|
|
233
|
-
print("triggered")
|
|
234
|
-
let translation = gestureRecognizer.translation(in: sceneView)
|
|
235
|
-
print(translation)
|
|
236
|
-
|
|
237
|
-
// Extract forward and right vectors from the camera transform matrix
|
|
238
|
-
let forward = SIMD3<Float>(-cameraTransform.columns.2.x, 0, -cameraTransform.columns.2.z) // Assuming Y is up
|
|
239
|
-
let right = SIMD3<Float>(cameraTransform.columns.0.x, 0, cameraTransform.columns.0.z) // Assuming Y is up
|
|
240
|
-
|
|
241
|
-
// Normalize the vectors
|
|
242
|
-
let forwardNormalized = normalize(forward)
|
|
243
|
-
let rightNormalized = normalize(right)
|
|
244
|
-
|
|
245
|
-
// Calculate the adjusted movement based on user input and camera orientation
|
|
246
|
-
// Invert the vertical translation (y-axis)
|
|
247
|
-
let adjustedMovement = SIMD3<Float>(
|
|
248
|
-
x: Float(translation.x) * rightNormalized.x + Float(translation.y) * forwardNormalized.x,
|
|
249
|
-
y: 0, // Assuming you want to keep the movement in the horizontal plane
|
|
250
|
-
z: -Float(translation.x) * rightNormalized.z - Float(translation.y) * forwardNormalized.z // Invert the z movement
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
let initialPosition = anchorEntity.position
|
|
254
|
-
ReplateCameraView.anchorEntity?.position = initialPosition + adjustedMovement / Float(ReplateCameraView.dragSpeed)
|
|
255
|
-
|
|
256
|
-
gestureRecognizer.setTranslation(.zero, in: sceneView)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
|
|
261
|
-
guard let sceneView = gestureRecognizer.view as? ARView else {
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
switch gestureRecognizer.state {
|
|
265
|
-
case .changed:
|
|
266
|
-
// Ensure execution on the main thread
|
|
267
|
-
DispatchQueue.main.async {
|
|
268
|
-
// Calculate the scale based on the gesture recognizer's scale
|
|
269
|
-
let scale = Float(gestureRecognizer.scale)
|
|
270
|
-
|
|
271
|
-
// Ensure anchor entity is not nil before proceeding
|
|
272
|
-
guard let anchorEntity = ReplateCameraView.anchorEntity else {
|
|
273
|
-
print("[handlePinch] Anchor entity is nil.")
|
|
274
|
-
return
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Remove all child entities safely
|
|
278
|
-
ReplateCameraView.spheresModels.forEach { entity in
|
|
279
|
-
anchorEntity.removeChild(entity)
|
|
280
|
-
}
|
|
281
|
-
if let focusModel = ReplateCameraView.focusModel {
|
|
282
|
-
anchorEntity.removeChild(focusModel)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Clear spheres models array
|
|
286
|
-
ReplateCameraView.spheresModels = []
|
|
287
|
-
|
|
288
|
-
// Update the scales
|
|
289
|
-
ReplateCameraView.sphereRadius *= scale
|
|
290
|
-
ReplateCameraView.spheresRadius *= scale
|
|
291
|
-
ReplateCameraView.sphereAngle *= scale
|
|
292
|
-
|
|
293
|
-
// Recreate spheres and the focus sphere
|
|
294
|
-
self.createSpheres(y: ReplateCameraView.spheresHeight)
|
|
295
|
-
self.createSpheres(y: ReplateCameraView.distanceBetweenCircles + ReplateCameraView.spheresHeight)
|
|
296
|
-
self.createFocusSphere()
|
|
297
|
-
|
|
298
|
-
// Update the material of the spheres based on their state
|
|
299
|
-
for i in 0..<72 {
|
|
300
|
-
let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
|
|
301
|
-
if ReplateCameraView.upperSpheresSet[i] {
|
|
302
|
-
if 72 + i < ReplateCameraView.spheresModels.count {
|
|
303
|
-
let entity = ReplateCameraView.spheresModels[72 + i]
|
|
304
|
-
entity.model?.materials[0] = material
|
|
305
|
-
} else {
|
|
306
|
-
print("[handlePinch] Upper sphere index out of bounds: \(72 + i)")
|
|
120
|
+
guard let inst = INSTANCE else { return }
|
|
121
|
+
let tap = UITapGestureRecognizer(target: inst, action: #selector(inst.viewTapped(_:)))
|
|
122
|
+
let pan = UIPanGestureRecognizer(target: inst, action: #selector(inst.handlePan(_:)))
|
|
123
|
+
let pinch = UIPinchGestureRecognizer(target: inst, action: #selector(inst.handlePinch(_:)))
|
|
124
|
+
[tap, pan, pinch].forEach { arView.addGestureRecognizer($0) }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// MARK: Gestures
|
|
128
|
+
@objc private func viewTapped(_ g: UITapGestureRecognizer) {
|
|
129
|
+
let loc = g.location(in: ReplateCameraView.arView)
|
|
130
|
+
let results = ReplateCameraView.arView.raycast(from: loc,
|
|
131
|
+
allowing: .estimatedPlane,
|
|
132
|
+
alignment: .horizontal)
|
|
133
|
+
guard let hit = results.first else { return }
|
|
134
|
+
let anchor = AnchorEntity(world: hit.worldTransform)
|
|
135
|
+
DispatchQueue.main.async {
|
|
136
|
+
ReplateCameraView.dotAnchors.forEach {
|
|
137
|
+
$0.removeFromParent()
|
|
138
|
+
ReplateCameraView.arView.scene.removeAnchor($0)
|
|
307
139
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
140
|
+
ReplateCameraView.dotAnchors = []
|
|
141
|
+
ReplateCameraView.anchorEntity = anchor
|
|
142
|
+
createSpheres(y: spheresHeight)
|
|
143
|
+
createSpheres(y: distanceBetweenCircles + spheresHeight)
|
|
144
|
+
ReplateCameraView.arView.scene.anchors.append(anchor)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@objc private func handlePan(_ g: UIPanGestureRecognizer) {
|
|
149
|
+
guard let v = g.view as? ARView,
|
|
150
|
+
let a = ReplateCameraView.anchorEntity else { return }
|
|
151
|
+
if g.state == .changed {
|
|
152
|
+
let t = g.translation(in: v)
|
|
153
|
+
let m = v.cameraTransform.matrix
|
|
154
|
+
let forward = normalize(SIMD3(-m.columns.2.x, 0, -m.columns.2.z))
|
|
155
|
+
let right = normalize(SIMD3( m.columns.0.x, 0, m.columns.0.z))
|
|
156
|
+
let move = SIMD3<Float>(
|
|
157
|
+
Float( t.x)*right.x + Float( t.y)*forward.x,
|
|
158
|
+
0,
|
|
159
|
+
-Float( t.x)*right.z - Float( t.y)*forward.z
|
|
160
|
+
) / 7000
|
|
161
|
+
a.position += move
|
|
162
|
+
g.setTranslation(.zero, in: v)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@objc private func handlePinch(_ g: UIPinchGestureRecognizer) {
|
|
167
|
+
let now = CACurrentMediaTime()
|
|
168
|
+
guard now - lastPinchTime > 0.05, let a = ReplateCameraView.anchorEntity else { return }
|
|
169
|
+
lastPinchTime = now
|
|
170
|
+
if g.state == .changed {
|
|
171
|
+
let s = Float(g.scale)
|
|
172
|
+
a.scale *= SIMD3(repeating: s)
|
|
173
|
+
g.scale = 1.0
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// MARK: Sphere Creation
|
|
178
|
+
private static func createSpheres(y: Float) {
|
|
179
|
+
guard let anchor = anchorEntity else { return }
|
|
180
|
+
var batch: [ModelEntity] = []
|
|
181
|
+
for i in 0..<72 {
|
|
182
|
+
let angle = Float(i) * (Float.pi/36)
|
|
183
|
+
let pos = SIMD3(
|
|
184
|
+
spheresRadius * cos(angle),
|
|
185
|
+
y,
|
|
186
|
+
spheresRadius * sin(angle)
|
|
187
|
+
)
|
|
188
|
+
let sphere = ModelEntity(mesh: baseSphereMesh,
|
|
189
|
+
materials: [baseSphereMaterial])
|
|
190
|
+
sphere.position = pos
|
|
191
|
+
batch.append(sphere)
|
|
192
|
+
}
|
|
193
|
+
spheresModels.append(contentsOf: batch)
|
|
194
|
+
batch.forEach { anchor.addChild($0) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// MARK: Dot Grid
|
|
198
|
+
func addDots(to planeAnchor: ARPlaneAnchor) {
|
|
199
|
+
let center = planeAnchor.center
|
|
200
|
+
let extent = planeAnchor.extent
|
|
201
|
+
let spacing: Float = 0.05
|
|
202
|
+
var positions: [SIMD3<Float>] = []
|
|
203
|
+
for x in stride(from: -extent.x/2, through: extent.x/2, by: spacing) {
|
|
204
|
+
for z in stride(from: -extent.z/2, through: extent.z/2, by: spacing) {
|
|
205
|
+
positions.append(SIMD3(center.x + x, 0, center.z + z))
|
|
315
206
|
}
|
|
316
|
-
}
|
|
317
207
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// MARK: - Entity Management
|
|
328
|
-
|
|
329
|
-
func createFocusSphere() {
|
|
330
|
-
DispatchQueue.main.async {
|
|
331
|
-
let sphereRadius = ReplateCameraView.sphereRadius * 1.5
|
|
332
|
-
|
|
333
|
-
// Generate the first sphere mesh
|
|
334
|
-
let sphereMesh1 = MeshResource.generateSphere(radius: sphereRadius)
|
|
335
|
-
|
|
336
|
-
// Create the first sphere entity with initial material
|
|
337
|
-
let sphereEntity1 = ModelEntity(mesh: sphereMesh1, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
|
|
338
|
-
|
|
339
|
-
// Set the position for the first sphere entity
|
|
340
|
-
sphereEntity1.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight, z: 0)
|
|
341
|
-
|
|
342
|
-
// Generate the second sphere mesh
|
|
343
|
-
let sphereMesh2 = MeshResource.generateSphere(radius: sphereRadius)
|
|
344
|
-
|
|
345
|
-
// Create the second sphere entity with initial material
|
|
346
|
-
let sphereEntity2 = ModelEntity(mesh: sphereMesh2, materials: [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)])
|
|
347
|
-
|
|
348
|
-
// Set the position for the second sphere entity
|
|
349
|
-
sphereEntity2.position = SIMD3(x: 0, y: ReplateCameraView.spheresHeight + ReplateCameraView.distanceBetweenCircles, z: 0)
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
// Update the material of the sphere entities
|
|
353
|
-
sphereEntity1.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
|
|
354
|
-
sphereEntity2.model?.materials = [SimpleMaterial(color: .green.withAlphaComponent(1), roughness: 1, isMetallic: false)]
|
|
355
|
-
|
|
356
|
-
let baseOverlayEntity = self.loadModel(named: "center.obj")
|
|
357
|
-
baseOverlayEntity.scale *= 12
|
|
358
|
-
baseOverlayEntity.model?.materials = [SimpleMaterial(color: .white.withAlphaComponent(0.3), roughness: 1, isMetallic: false),
|
|
359
|
-
SimpleMaterial(color: .white.withAlphaComponent(0.7), roughness: 1, isMetallic: false),
|
|
360
|
-
SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)]
|
|
361
|
-
|
|
362
|
-
// Create a parent entity to hold both spheres
|
|
363
|
-
let parentEntity = Entity()
|
|
364
|
-
parentEntity.addChild(sphereEntity1)
|
|
365
|
-
parentEntity.addChild(sphereEntity2)
|
|
366
|
-
parentEntity.addChild(baseOverlayEntity)
|
|
367
|
-
|
|
368
|
-
// Set the focus model for the global state
|
|
369
|
-
ReplateCameraView.focusModel = parentEntity
|
|
370
|
-
|
|
371
|
-
// Safely add the parent entity to the anchor entity
|
|
372
|
-
ReplateCameraView.anchorEntity?.addChild(parentEntity)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
func createSpheres(y: Float) {
|
|
377
|
-
DispatchQueue.main.async {
|
|
378
|
-
let radius = ReplateCameraView.spheresRadius
|
|
379
|
-
|
|
380
|
-
for i in 0..<72 {
|
|
381
|
-
let angle = Float(i) * (Float.pi / 180) * 5
|
|
382
|
-
let position = SIMD3(
|
|
383
|
-
radius * cos(angle),
|
|
384
|
-
y,
|
|
385
|
-
radius * sin(angle)
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
let sphere = self.createSphere(position: position)
|
|
389
|
-
ReplateCameraView.spheresModels.append(sphere)
|
|
390
|
-
ReplateCameraView.anchorEntity?.addChild(sphere)
|
|
208
|
+
DispatchQueue.main.asyncAfter(deadline: .now()+1.0) {
|
|
209
|
+
for pos in positions {
|
|
210
|
+
let dotAnchor = AnchorEntity(world: planeAnchor.transform)
|
|
211
|
+
let dot = self.createDot(at: pos)
|
|
212
|
+
dotAnchor.addChild(dot)
|
|
213
|
+
ReplateCameraView.arView.scene.addAnchor(dotAnchor)
|
|
214
|
+
ReplateCameraView.dotAnchors.append(dotAnchor)
|
|
391
215
|
}
|
|
392
216
|
}
|
|
393
217
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
let
|
|
397
|
-
let
|
|
398
|
-
let
|
|
399
|
-
|
|
400
|
-
return
|
|
218
|
+
func createDot(at position: SIMD3<Float>) -> ModelEntity {
|
|
219
|
+
let size: Float = 0.005
|
|
220
|
+
let mesh = MeshResource.generateBox(size: [size, 0.0001, size], cornerRadius: size/2)
|
|
221
|
+
let mat = SimpleMaterial(color: .white, roughness: 1, isMetallic: false)
|
|
222
|
+
let e = ModelEntity(mesh: mesh, materials: [mat])
|
|
223
|
+
e.position = position
|
|
224
|
+
return e
|
|
401
225
|
}
|
|
402
226
|
|
|
403
|
-
|
|
404
|
-
print("Adding dots to plane anchor") // Debugging line
|
|
405
|
-
let center = planeAnchor.center
|
|
406
|
-
let extent = planeAnchor.extent
|
|
407
|
-
|
|
408
|
-
var dotPositions: [SIMD3<Float>] = []
|
|
409
|
-
let dotSpacing: Float = 0.05 // Adjust the spacing of dots (smaller value for more dots)
|
|
410
|
-
|
|
411
|
-
for x in stride(from: -extent.x / 2, through: extent.x / 2, by: dotSpacing) {
|
|
412
|
-
for z in stride(from: -extent.z / 2, through: extent.z / 2, by: dotSpacing) {
|
|
413
|
-
let position = SIMD3<Float>(x + center.x, 0, z + center.z)
|
|
414
|
-
dotPositions.append(position)
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now().advanced(by: DispatchTimeInterval.milliseconds(1000)), execute: {
|
|
419
|
-
// Add the dots to the ARView
|
|
420
|
-
for position in dotPositions {
|
|
421
|
-
let dotAnchor = AnchorEntity(world: planeAnchor.transform)
|
|
422
|
-
let dot = self.createDot(at: position) // Assuming you're using the circle function
|
|
423
|
-
dot.position.y = 0 // Ensure the dot position matches the plane's height
|
|
424
|
-
dotAnchor.addChild(dot)
|
|
425
|
-
ReplateCameraView.arView.scene.addAnchor(dotAnchor)
|
|
426
|
-
ReplateCameraView.dotAnchors.append(dotAnchor)
|
|
427
|
-
}
|
|
428
|
-
})
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
func createDot(at position: SIMD3<Float>) -> ModelEntity {
|
|
433
|
-
// Define the dimensions of the box
|
|
434
|
-
let width: Float = 0.005 // 1 cm width
|
|
435
|
-
let height: Float = 0.0001 // Very small height to make it almost flat
|
|
436
|
-
let depth: Float = width // 1 cm depth
|
|
437
|
-
let cornerRadius: Float = width/2.0 // Half of the width to make it look like a circle
|
|
438
|
-
|
|
439
|
-
// Generate a box with rounded corners
|
|
440
|
-
let cylinderMesh = MeshResource.generateBox(size: [width, height, depth], cornerRadius: 1)
|
|
441
|
-
|
|
442
|
-
// Create the material
|
|
443
|
-
let material = SimpleMaterial(color: .white, roughness: 1, isMetallic: false)
|
|
444
|
-
|
|
445
|
-
// Create the entity
|
|
446
|
-
let circleEntity = ModelEntity(mesh: cylinderMesh, materials: [material])
|
|
447
|
-
circleEntity.position = position
|
|
448
|
-
return circleEntity
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// MARK: - Reset Functionality
|
|
227
|
+
// MARK: Reset
|
|
452
228
|
@objc static func reset() {
|
|
453
229
|
arQueue.async {
|
|
454
|
-
|
|
455
|
-
defer { Self.lock.unlock() }
|
|
456
|
-
|
|
457
|
-
if isResetting { return }
|
|
458
|
-
isResetting = true
|
|
459
|
-
|
|
230
|
+
lock.lock(); defer { lock.unlock() }
|
|
460
231
|
DispatchQueue.main.async {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
232
|
+
arView.session.pause()
|
|
233
|
+
arView.session.delegate = nil
|
|
234
|
+
arView.scene.anchors.removeAll()
|
|
235
|
+
arView.removeFromSuperview()
|
|
236
|
+
anchorEntity = nil
|
|
237
|
+
spheresModels.removeAll()
|
|
238
|
+
upperSpheresSet = [Bool](repeating: false, count: 72)
|
|
239
|
+
lowerSpheresSet = [Bool](repeating: false, count: 72)
|
|
240
|
+
dotAnchors.removeAll()
|
|
241
|
+
INSTANCE.setupNewARView()
|
|
465
242
|
}
|
|
466
243
|
}
|
|
467
244
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
arView
|
|
471
|
-
arView
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
private static func resetProperties() {
|
|
478
|
-
anchorEntity = nil
|
|
479
|
-
model = nil
|
|
480
|
-
spheresModels.removeAll()
|
|
481
|
-
upperSpheresSet = [Bool](repeating: false, count: 72)
|
|
482
|
-
lowerSpheresSet = [Bool](repeating: false, count: 72)
|
|
483
|
-
totalPhotosTaken = 0
|
|
484
|
-
photosFromDifferentAnglesTaken = 0
|
|
485
|
-
sphereRadius = 0.004
|
|
486
|
-
spheresRadius = 0.13
|
|
487
|
-
sphereAngle = 5
|
|
488
|
-
spheresHeight = 0.10
|
|
489
|
-
dragSpeed = 7000
|
|
490
|
-
dotAnchors.removeAll()
|
|
491
|
-
gravityVector = [:]
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
private static func setupNewARView() {
|
|
495
|
-
guard let instance = INSTANCE else { return }
|
|
496
|
-
|
|
497
|
-
arView = ARView(frame: CGRect(x: 0, y: 0, width: width, height: height))
|
|
498
|
-
arView.backgroundColor = instance.hexStringToUIColor(hexColor: "#32a852")
|
|
499
|
-
instance.addSubview(arView)
|
|
500
|
-
arView.session.delegate = instance
|
|
501
|
-
motionManager = CMMotionManager()
|
|
502
|
-
if motionManager.isDeviceMotionAvailable {
|
|
503
|
-
ReplateCameraView.INSTANCE.startDeviceMotionUpdates()
|
|
504
|
-
} else {
|
|
505
|
-
print("Device motion is not available")
|
|
245
|
+
private func setupNewARView() {
|
|
246
|
+
ReplateCameraView.arView = ARView(frame: bounds)
|
|
247
|
+
addSubview(ReplateCameraView.arView)
|
|
248
|
+
ReplateCameraView.arView.session.delegate = self
|
|
249
|
+
ReplateCameraView.motionManager = CMMotionManager()
|
|
250
|
+
if ReplateCameraView.motionManager.isDeviceMotionAvailable {
|
|
251
|
+
startDeviceMotionUpdates()
|
|
506
252
|
}
|
|
507
|
-
setupAR()
|
|
253
|
+
ReplateCameraView.setupAR()
|
|
508
254
|
}
|
|
509
255
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
print("Camera permissions already granted")
|
|
515
|
-
} else {
|
|
516
|
-
AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
|
|
517
|
-
if granted {
|
|
518
|
-
print("Camera permissions granted")
|
|
519
|
-
} else {
|
|
520
|
-
print("Camera permissions denied")
|
|
256
|
+
// MARK: Permissions & Motion
|
|
257
|
+
func requestCameraPermissions() {
|
|
258
|
+
if AVCaptureDevice.authorizationStatus(for: .video) != .authorized {
|
|
259
|
+
AVCaptureDevice.requestAccess(for: .video) { _ in }
|
|
521
260
|
}
|
|
522
|
-
})
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
func loadModel(named name: String) -> ModelEntity {
|
|
527
|
-
do{
|
|
528
|
-
return try ModelEntity.loadModel(named: name)
|
|
529
|
-
}catch{
|
|
530
|
-
print("Cannot load model \(name)")
|
|
531
|
-
let baseOverlayMesh = MeshResource.generateBox(size: [ReplateCameraView.spheresRadius * 2, 0.01, ReplateCameraView.spheresRadius * 2], cornerRadius: 1)
|
|
532
|
-
let baseOverlayEntity = ModelEntity(mesh: baseOverlayMesh, materials: [SimpleMaterial(color: .white.withAlphaComponent(0.5), roughness: 1, isMetallic: false)])
|
|
533
|
-
|
|
534
|
-
baseOverlayEntity.position = SIMD3(x: 0, y: 0.01, z: 0)
|
|
535
|
-
return baseOverlayEntity
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
@objc var color: String = "" {
|
|
540
|
-
didSet {
|
|
541
|
-
self.backgroundColor = hexStringToUIColor(hexColor: color)
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
func hexStringToUIColor(hexColor: String) -> UIColor {
|
|
546
|
-
let stringScanner = Scanner(string: hexColor)
|
|
547
|
-
|
|
548
|
-
if (hexColor.hasPrefix("#")) {
|
|
549
|
-
stringScanner.scanLocation = 1
|
|
550
261
|
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: 1)
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
internal func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
|
|
562
|
-
print("Planes detected: \(anchors.count)") // Debugging line
|
|
563
|
-
for anchor in anchors {
|
|
564
|
-
if let planeAnchor = anchor as? ARPlaneAnchor {
|
|
565
|
-
print("Adding dots to plane")
|
|
566
|
-
if (ReplateCameraView.spheresModels.isEmpty && ReplateCameraView.dotAnchors.isEmpty) {
|
|
567
|
-
addDots(to: planeAnchor)
|
|
262
|
+
func startDeviceMotionUpdates() {
|
|
263
|
+
ReplateCameraView.motionManager.deviceMotionUpdateInterval = 0.1
|
|
264
|
+
ReplateCameraView.motionManager.startDeviceMotionUpdates(to: .main) { m, _ in
|
|
265
|
+
if let g = m?.gravity {
|
|
266
|
+
ReplateCameraView.gravityVector = ["x": g.x, "y": g.y, "z": g.z]
|
|
267
|
+
}
|
|
568
268
|
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
func session(_ session: ARSession, didUpdate frame: ARFrame) {
|
|
574
|
-
// Handle AR frame updates
|
|
575
|
-
// You can perform actions here, such as updating the AR content based on the camera frame
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
func sessionWasInterrupted(_ session: ARSession) {
|
|
579
|
-
print("SESSION INTERRUPTED")
|
|
580
|
-
ReplateCameraView.motionManager.stopDeviceMotionUpdates()
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
func sessionInterruptionEnded(_ session: ARSession) {
|
|
584
|
-
print("SESSION RESUMED")
|
|
585
|
-
ReplateCameraView.INSTANCE.startDeviceMotionUpdates()
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
func generateImpactFeedback(strength: UIImpactFeedbackGenerator.FeedbackStyle) {
|
|
589
|
-
do{
|
|
590
|
-
let impactFeedbackGenerator = try UIImpactFeedbackGenerator(style: strength)
|
|
591
|
-
impactFeedbackGenerator.prepare()
|
|
592
|
-
impactFeedbackGenerator.impactOccurred()
|
|
593
|
-
}catch{
|
|
594
|
-
print("Error when sending feedback")
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
func startDeviceMotionUpdates() {
|
|
599
|
-
ReplateCameraView.motionManager.deviceMotionUpdateInterval = 0.1 // Update interval in seconds
|
|
600
|
-
ReplateCameraView.motionManager.startDeviceMotionUpdates(to: .main) { (deviceMotion, error) in
|
|
601
|
-
if let deviceMotion = deviceMotion {
|
|
602
|
-
let gravity = deviceMotion.gravity
|
|
603
|
-
ReplateCameraView.gravityVector = [
|
|
604
|
-
"x": gravity.x,
|
|
605
|
-
"y": gravity.y,
|
|
606
|
-
"z": gravity.z
|
|
607
|
-
]
|
|
608
|
-
print("Gravity vector: x = \(gravity.x), y = \(gravity.y), z = \(gravity.z)")
|
|
609
|
-
}
|
|
610
269
|
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
270
|
}
|
|
614
271
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
// MARK: - Supporting Types
|
|
272
|
+
// MARK: - ARError
|
|
618
273
|
enum ARError: Error {
|
|
619
|
-
case noAnchor
|
|
620
|
-
case
|
|
621
|
-
case
|
|
622
|
-
case captureError
|
|
623
|
-
case tooManyImages
|
|
624
|
-
case processingError
|
|
625
|
-
case savingError
|
|
626
|
-
case transformError
|
|
627
|
-
case lightingError
|
|
628
|
-
case notInRange
|
|
629
|
-
case unknown
|
|
630
|
-
|
|
274
|
+
case noAnchor, invalidAnchor, notInFocus, captureError
|
|
275
|
+
case tooManyImages, processingError, savingError
|
|
276
|
+
case transformError, lightingError, notInRange, unknown
|
|
631
277
|
var localizedDescription: String {
|
|
632
278
|
switch self {
|
|
633
279
|
case .noAnchor: return "[ReplateCameraController] No anchor set yet"
|
|
634
280
|
case .invalidAnchor: return "[ReplateCameraController] AnchorNode is not valid"
|
|
635
281
|
case .notInFocus: return "[ReplateCameraController] Object not in focus"
|
|
636
282
|
case .captureError: return "[ReplateCameraController] Error capturing image"
|
|
637
|
-
case .tooManyImages: return "[ReplateCameraController] Too many images
|
|
283
|
+
case .tooManyImages: return "[ReplateCameraController] Too many images / angle repeat"
|
|
638
284
|
case .processingError: return "[ReplateCameraController] Error processing image"
|
|
639
285
|
case .savingError: return "[ReplateCameraController] Error saving photo"
|
|
640
|
-
case .transformError: return "[ReplateCameraController] Camera transform
|
|
286
|
+
case .transformError: return "[ReplateCameraController] Camera transform unavailable"
|
|
641
287
|
case .lightingError: return "[ReplateCameraController] Image too dark"
|
|
642
288
|
case .notInRange: return "[ReplateCameraController] Camera not in range"
|
|
643
|
-
case .unknown: return "[ReplateCameraController] Unknown error
|
|
289
|
+
case .unknown: return "[ReplateCameraController] Unknown error"
|
|
644
290
|
}
|
|
645
291
|
}
|
|
646
292
|
}
|
|
647
293
|
|
|
294
|
+
// MARK: - DeviceTargetInfo
|
|
648
295
|
struct DeviceTargetInfo {
|
|
649
296
|
let isInFocus: Bool
|
|
650
297
|
let targetIndex: Int
|
|
651
298
|
let transform: simd_float4x4
|
|
652
299
|
let cameraPosition: SIMD3<Float>
|
|
653
300
|
let deviceDirection: SIMD3<Float>
|
|
654
|
-
|
|
655
|
-
var isValidTarget: Bool {
|
|
656
|
-
return targetIndex != -1
|
|
657
|
-
}
|
|
301
|
+
let distance: Float
|
|
302
|
+
var isValidTarget: Bool { targetIndex != -1 }
|
|
658
303
|
}
|
|
659
304
|
|
|
305
|
+
// MARK: - SafeCallbackHandler
|
|
660
306
|
class SafeCallbackHandler {
|
|
661
307
|
private let resolver: RCTPromiseResolveBlock
|
|
662
308
|
private let rejecter: RCTPromiseRejectBlock
|
|
663
|
-
var hasCalledBack = false
|
|
309
|
+
private var hasCalledBack = false
|
|
664
310
|
private let lock = NSLock()
|
|
665
|
-
|
|
666
|
-
|
|
311
|
+
init(resolver: @escaping RCTPromiseResolveBlock,
|
|
312
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
667
313
|
self.resolver = resolver
|
|
668
314
|
self.rejecter = rejecter
|
|
669
315
|
}
|
|
670
|
-
|
|
671
316
|
func resolve(_ result: Any) {
|
|
672
|
-
lock.lock()
|
|
673
|
-
|
|
674
|
-
guard !hasCalledBack else {
|
|
675
|
-
print("rejecter: Callback already invoked.")
|
|
676
|
-
return
|
|
677
|
-
}
|
|
317
|
+
lock.lock(); defer { lock.unlock() }
|
|
318
|
+
guard !hasCalledBack else { return }
|
|
678
319
|
hasCalledBack = true
|
|
679
320
|
resolver(result)
|
|
680
321
|
}
|
|
681
|
-
|
|
682
322
|
func reject(_ error: ARError) {
|
|
683
|
-
lock.lock()
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
guard !hasCalledBack else {
|
|
687
|
-
print("rejecter: Callback already invoked.")
|
|
688
|
-
return
|
|
689
|
-
}
|
|
323
|
+
lock.lock(); defer { lock.unlock() }
|
|
324
|
+
guard !hasCalledBack else { return }
|
|
690
325
|
hasCalledBack = true
|
|
691
|
-
rejecter(
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
NSError(domain: "ReplateCameraController", code: 0, userInfo: nil)
|
|
695
|
-
)
|
|
326
|
+
rejecter(String(describing: error),
|
|
327
|
+
error.localizedDescription,
|
|
328
|
+
NSError(domain: "ReplateCameraController", code: 0))
|
|
696
329
|
}
|
|
697
330
|
}
|
|
698
331
|
|
|
699
|
-
// MARK: -
|
|
332
|
+
// MARK: - ReplateCameraController
|
|
700
333
|
@objc(ReplateCameraController)
|
|
701
334
|
class ReplateCameraController: NSObject {
|
|
702
|
-
// MARK: - Static Properties
|
|
703
335
|
private static let lock = NSLock()
|
|
704
336
|
private static let arQueue = DispatchQueue(label: "com.replate.ar.controller", qos: .userInteractive)
|
|
337
|
+
private static let imageProcessingQueue = DispatchQueue(label: "com.replate.ar.image", qos: .userInitiated, attributes: .concurrent)
|
|
705
338
|
|
|
706
|
-
// Configuration Constants
|
|
707
339
|
private static let MIN_DISTANCE: Float = 0.25
|
|
708
340
|
private static let MAX_DISTANCE: Float = 0.55
|
|
709
341
|
private static let ANGLE_THRESHOLD: Float = 0.6
|
|
710
|
-
// private static let TARGET_IMAGE_SIZE = CGSize(width: 2048, height: 1556)
|
|
711
342
|
private static let TARGET_IMAGE_SIZE = CGSize(width: 3072, height: 2304)
|
|
712
343
|
private static let MIN_AMBIENT_INTENSITY: CGFloat = 650
|
|
713
344
|
|
|
714
|
-
|
|
345
|
+
private var lastCaptureTime: TimeInterval = 0
|
|
346
|
+
private var captureCount: Int = 0
|
|
347
|
+
private lazy var imageProcessingContext: CIContext = {
|
|
348
|
+
CIContext(options: [.useSoftwareRenderer: false, .priorityRequestLow: true])
|
|
349
|
+
}()
|
|
350
|
+
|
|
715
351
|
static var completedTutorialCallback: RCTResponseSenderBlock?
|
|
716
352
|
static var anchorSetCallback: RCTResponseSenderBlock?
|
|
717
353
|
static var completedUpperSpheresCallback: RCTResponseSenderBlock?
|
|
@@ -720,846 +356,522 @@ class ReplateCameraController: NSObject {
|
|
|
720
356
|
static var tooCloseCallback: RCTResponseSenderBlock?
|
|
721
357
|
static var tooFarCallback: RCTResponseSenderBlock?
|
|
722
358
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
func registerOpenedTutorialCallback(_ callback: @escaping RCTResponseSenderBlock) {
|
|
726
|
-
Self.lock.lock()
|
|
727
|
-
defer { Self.lock.unlock() }
|
|
728
|
-
ReplateCameraController.openedTutorialCallback = callback
|
|
359
|
+
override init() {
|
|
360
|
+
super.init()
|
|
729
361
|
}
|
|
730
362
|
|
|
731
|
-
|
|
732
|
-
func
|
|
733
|
-
Self.lock.lock()
|
|
734
|
-
|
|
735
|
-
ReplateCameraController.completedTutorialCallback = callback
|
|
363
|
+
// MARK: Callback Registration
|
|
364
|
+
@objc func registerOpenedTutorialCallback(_ cb: @escaping RCTResponseSenderBlock) {
|
|
365
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
366
|
+
Self.openedTutorialCallback = cb
|
|
736
367
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
Self.lock.lock()
|
|
741
|
-
defer { Self.lock.unlock() }
|
|
742
|
-
ReplateCameraController.anchorSetCallback = callback
|
|
368
|
+
@objc func registerCompletedTutorialCallback(_ cb: @escaping RCTResponseSenderBlock) {
|
|
369
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
370
|
+
Self.completedTutorialCallback = cb
|
|
743
371
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
Self.lock.lock()
|
|
748
|
-
defer { Self.lock.unlock() }
|
|
749
|
-
ReplateCameraController.completedUpperSpheresCallback = callback
|
|
372
|
+
@objc func registerAnchorSetCallback(_ cb: @escaping RCTResponseSenderBlock) {
|
|
373
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
374
|
+
Self.anchorSetCallback = cb
|
|
750
375
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
Self.lock.lock()
|
|
755
|
-
defer { Self.lock.unlock() }
|
|
756
|
-
ReplateCameraController.completedLowerSpheresCallback = callback
|
|
376
|
+
@objc func registerCompletedUpperSpheresCallback(_ cb: @escaping RCTResponseSenderBlock) {
|
|
377
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
378
|
+
Self.completedUpperSpheresCallback = cb
|
|
757
379
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
Self.lock.lock()
|
|
762
|
-
defer { Self.lock.unlock() }
|
|
763
|
-
ReplateCameraController.tooCloseCallback = callback
|
|
380
|
+
@objc func registerCompletedLowerSpheresCallback(_ cb: @escaping RCTResponseSenderBlock) {
|
|
381
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
382
|
+
Self.completedLowerSpheresCallback = cb
|
|
764
383
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
Self.lock.lock()
|
|
769
|
-
defer { Self.lock.unlock() }
|
|
770
|
-
ReplateCameraController.tooFarCallback = callback
|
|
384
|
+
@objc func registerTooCloseCallback(_ cb: @escaping RCTResponseSenderBlock) {
|
|
385
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
386
|
+
Self.tooCloseCallback = cb
|
|
771
387
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
776
|
-
Self.lock.lock()
|
|
777
|
-
let count = ReplateCameraView.totalPhotosTaken
|
|
778
|
-
Self.lock.unlock()
|
|
779
|
-
resolver(count)
|
|
388
|
+
@objc func registerTooFarCallback(_ cb: @escaping RCTResponseSenderBlock) {
|
|
389
|
+
Self.lock.lock(); defer { Self.lock.unlock() }
|
|
390
|
+
Self.tooFarCallback = cb
|
|
780
391
|
}
|
|
781
392
|
|
|
782
|
-
|
|
783
|
-
func
|
|
784
|
-
Self.lock.lock()
|
|
785
|
-
|
|
786
|
-
Self.lock.unlock()
|
|
787
|
-
resolver(isComplete)
|
|
393
|
+
// MARK: Status Queries
|
|
394
|
+
@objc func getPhotosCount(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
395
|
+
Self.lock.lock(); let c = ReplateCameraView.totalPhotosTaken; Self.lock.unlock()
|
|
396
|
+
resolver(c)
|
|
788
397
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
Self.lock.lock()
|
|
793
|
-
let remaining = 144 - ReplateCameraView.photosFromDifferentAnglesTaken
|
|
794
|
-
Self.lock.unlock()
|
|
795
|
-
resolver(remaining)
|
|
398
|
+
@objc func isScanComplete(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
399
|
+
Self.lock.lock(); let done = ReplateCameraView.photosFromDifferentAnglesTaken == 144; Self.lock.unlock()
|
|
400
|
+
resolver(done)
|
|
796
401
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
402
|
+
@objc func getRemainingAnglesToScan(_ resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) {
|
|
403
|
+
Self.lock.lock(); let rem = 144 - ReplateCameraView.photosFromDifferentAnglesTaken; Self.lock.unlock()
|
|
404
|
+
resolver(rem)
|
|
405
|
+
}
|
|
406
|
+
@objc func reset() {
|
|
407
|
+
DispatchQueue.main.async { ReplateCameraView.reset() }
|
|
803
408
|
}
|
|
804
409
|
|
|
805
|
-
// MARK:
|
|
806
|
-
@objc(
|
|
807
|
-
|
|
410
|
+
// MARK: Photo Capture
|
|
411
|
+
@objc func takePhoto(_ unlimited: Bool = false,
|
|
412
|
+
resolver: @escaping RCTPromiseResolveBlock,
|
|
413
|
+
rejecter: @escaping RCTPromiseRejectBlock) {
|
|
808
414
|
Self.arQueue.async { [weak self] in
|
|
809
415
|
guard let self = self else { return }
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
416
|
+
let cb = SafeCallbackHandler(resolver: resolver, rejecter: rejecter)
|
|
417
|
+
|
|
418
|
+
var pixelBuffer: CVPixelBuffer?
|
|
419
|
+
var camTransform: simd_float4x4?
|
|
420
|
+
var lightEst: ARLightEstimate?
|
|
421
|
+
DispatchQueue.main.sync {
|
|
422
|
+
if let frame = ReplateCameraView.arView.session.currentFrame {
|
|
423
|
+
pixelBuffer = frame.capturedImage
|
|
424
|
+
camTransform = frame.camera.transform
|
|
425
|
+
lightEst = frame.lightEstimate
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
guard let buf = pixelBuffer, let camT = camTransform else {
|
|
429
|
+
cb.reject(.captureError); return
|
|
430
|
+
}
|
|
813
431
|
do {
|
|
814
|
-
try self.
|
|
815
|
-
|
|
816
|
-
|
|
432
|
+
try self.validateAndProcessPhotoWithData(
|
|
433
|
+
pixelBuffer: buf,
|
|
434
|
+
transform: camT,
|
|
435
|
+
lightEstimate: lightEst,
|
|
436
|
+
unlimited: unlimited,
|
|
437
|
+
callback: cb
|
|
438
|
+
)
|
|
439
|
+
} catch let e as ARError {
|
|
440
|
+
cb.reject(e)
|
|
817
441
|
} catch {
|
|
818
|
-
|
|
442
|
+
cb.reject(.unknown)
|
|
819
443
|
}
|
|
820
444
|
}
|
|
821
445
|
}
|
|
822
446
|
|
|
823
|
-
private func
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
private func getValidAnchorEntity() throws -> AnchorEntity {
|
|
835
|
-
var anchorEntity: AnchorEntity?
|
|
836
|
-
if(Thread.isMainThread){
|
|
837
|
-
anchorEntity = ReplateCameraView.anchorEntity
|
|
838
|
-
|
|
839
|
-
}else{
|
|
840
|
-
DispatchQueue.main.sync {
|
|
841
|
-
anchorEntity = ReplateCameraView.anchorEntity
|
|
447
|
+
private func validateAndProcessPhotoWithData(pixelBuffer: CVPixelBuffer,
|
|
448
|
+
transform camT: simd_float4x4,
|
|
449
|
+
lightEstimate le: ARLightEstimate?,
|
|
450
|
+
unlimited: Bool,
|
|
451
|
+
callback cb: SafeCallbackHandler) throws {
|
|
452
|
+
let anchor = try getValidAnchorEntity()
|
|
453
|
+
let info = try getDeviceTargetInfoFromTransform(anchorEntity: anchor,
|
|
454
|
+
cameraTransform: camT)
|
|
455
|
+
guard info.isValidTarget else { throw ARError.notInFocus }
|
|
456
|
+
if let light = le, light.ambientIntensity < Self.MIN_AMBIENT_INTENSITY {
|
|
457
|
+
throw ARError.lightingError
|
|
842
458
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
throw ARError.
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
guard isAnchorNodeValid(anchor) else {
|
|
851
|
-
throw ARError.invalidAnchor
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
return anchor
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
private func getDeviceTargetInfo(anchorEntity: AnchorEntity) throws -> DeviceTargetInfo {
|
|
858
|
-
guard let frame = ReplateCameraView.arView.session.currentFrame else {
|
|
859
|
-
throw ARError.transformError
|
|
459
|
+
if info.distance < Self.MIN_DISTANCE {
|
|
460
|
+
DispatchQueue.main.async {
|
|
461
|
+
Self.tooCloseCallback?([]); Self.tooCloseCallback = nil
|
|
462
|
+
}
|
|
463
|
+
throw ARError.notInRange
|
|
860
464
|
}
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
465
|
+
if info.distance > Self.MAX_DISTANCE {
|
|
466
|
+
DispatchQueue.main.async {
|
|
467
|
+
Self.tooFarCallback?([]); Self.tooFarCallback = nil
|
|
468
|
+
}
|
|
469
|
+
throw ARError.notInRange
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// compute sphereIndex & newAngle
|
|
473
|
+
let angle = angleBetweenAnchorXAndCamera(anchor: anchor, cameraTransform: camT)
|
|
474
|
+
let sphereIndex = max(Int(round(angle/5.0)),0) % 72
|
|
475
|
+
let newAngle = (info.targetIndex==1
|
|
476
|
+
? !ReplateCameraView.upperSpheresSet[sphereIndex]
|
|
477
|
+
: !ReplateCameraView.lowerSpheresSet[sphereIndex])
|
|
478
|
+
|
|
479
|
+
if !unlimited && !newAngle { throw ARError.tooManyImages }
|
|
480
|
+
|
|
481
|
+
Self.imageProcessingQueue.async {
|
|
482
|
+
self.processAndSaveImageConcurrently(buf, callback: cb) { success in
|
|
483
|
+
guard success else { return }
|
|
484
|
+
DispatchQueue.main.async {
|
|
485
|
+
self.updateCircleFocus(targetIndex: info.targetIndex)
|
|
486
|
+
self.updateSpheresAfterCapture(
|
|
487
|
+
targetIndex: info.targetIndex,
|
|
488
|
+
sphereIndex: sphereIndex,
|
|
489
|
+
isNewAngle: newAngle
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
872
493
|
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
let cameraPosition = SIMD3<Float>(cameraTransform.columns.3.x, cameraTransform.columns.3.y, cameraTransform.columns.3.z)
|
|
877
|
-
let deviceDirection = normalize(SIMD3<Float>(-cameraTransform.columns.2.x, -cameraTransform.columns.2.y, -cameraTransform.columns.2.z))
|
|
878
|
-
let directionToAnchor = normalize(anchorPosition - cameraPosition)
|
|
879
|
-
let angleToAnchor = acos(dot(deviceDirection, directionToAnchor))
|
|
880
|
-
|
|
881
|
-
let targetIndex = determineTargetIndex(angleToAnchor: angleToAnchor, relativeCameraTransform: relativeCameraTransform)
|
|
882
|
-
|
|
883
|
-
return DeviceTargetInfo(
|
|
884
|
-
isInFocus: angleToAnchor < Self.ANGLE_THRESHOLD,
|
|
885
|
-
targetIndex: targetIndex,
|
|
886
|
-
transform: relativeCameraTransform,
|
|
887
|
-
cameraPosition: cameraPosition,
|
|
888
|
-
deviceDirection: deviceDirection
|
|
889
|
-
)
|
|
890
494
|
}
|
|
891
495
|
|
|
892
|
-
private func
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
private func processTargetedDevice(deviceTargetInfo: DeviceTargetInfo, unlimited: Bool, callbackHandler: SafeCallbackHandler) throws {
|
|
904
|
-
DispatchQueue.main.async { [weak self] in
|
|
905
|
-
guard let self = self else { return }
|
|
906
|
-
|
|
907
|
-
self.updateCircleFocus(targetIndex: deviceTargetInfo.targetIndex)
|
|
908
|
-
let isInRange = self.checkCameraDistance(deviceTargetInfo: deviceTargetInfo)
|
|
909
|
-
if !isInRange {
|
|
910
|
-
callbackHandler.reject(.notInRange)
|
|
911
|
-
return
|
|
496
|
+
private func processAndSaveImageConcurrently(_ buf: CVPixelBuffer,
|
|
497
|
+
callback cb: SafeCallbackHandler,
|
|
498
|
+
completion: @escaping (Bool)->Void) {
|
|
499
|
+
autoreleasepool {
|
|
500
|
+
let ciImage = CIImage(cvImageBuffer: buf)
|
|
501
|
+
let context = imageProcessingContext
|
|
502
|
+
guard let resized = optimizedResizeImage(ciImage, to: Self.TARGET_IMAGE_SIZE),
|
|
503
|
+
let cg = context.createCGImage(resized, from: resized.extent) else {
|
|
504
|
+
cb.reject(.processingError); completion(false); return
|
|
912
505
|
}
|
|
913
|
-
|
|
914
|
-
guard let
|
|
915
|
-
|
|
916
|
-
return
|
|
506
|
+
let ui = UIImage(cgImage: cg).rotate(radians: .pi/2)
|
|
507
|
+
guard let url = optimizedSaveImage(ui, quality: getImageQuality()) else {
|
|
508
|
+
cb.reject(.savingError); completion(false); return
|
|
917
509
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
if let lightEstimate = frame.lightEstimate {
|
|
921
|
-
guard lightEstimate.ambientIntensity >= Self.MIN_AMBIENT_INTENSITY else {
|
|
922
|
-
callbackHandler.reject(.lightingError)
|
|
923
|
-
return
|
|
924
|
-
}
|
|
510
|
+
DispatchQueue.global(qos: .background).async {
|
|
511
|
+
self.addMetadataToImage(at: url)
|
|
925
512
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
cameraTransform: deviceTargetInfo.transform
|
|
930
|
-
) { success in
|
|
931
|
-
if !unlimited && !success {
|
|
932
|
-
callbackHandler.reject(.tooManyImages)
|
|
933
|
-
return
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
self.processAndSaveImage(frame.capturedImage, callbackHandler: callbackHandler)
|
|
513
|
+
DispatchQueue.main.async {
|
|
514
|
+
cb.resolve(url.absoluteString)
|
|
515
|
+
completion(true)
|
|
937
516
|
}
|
|
938
517
|
}
|
|
939
518
|
}
|
|
940
519
|
|
|
941
|
-
private func
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
private func checkCameraDistance(deviceTargetInfo: DeviceTargetInfo) -> Bool {
|
|
951
|
-
let distance = isCameraWithinRange(
|
|
952
|
-
cameraTransform: deviceTargetInfo.transform,
|
|
953
|
-
anchorEntity: ReplateCameraView.anchorEntity!
|
|
954
|
-
)
|
|
955
|
-
|
|
956
|
-
switch distance {
|
|
957
|
-
case 1:
|
|
958
|
-
ReplateCameraController.tooFarCallback?([])
|
|
959
|
-
ReplateCameraController.tooFarCallback = nil
|
|
960
|
-
return false
|
|
961
|
-
case -1:
|
|
962
|
-
ReplateCameraController.tooCloseCallback?([])
|
|
963
|
-
ReplateCameraController.tooCloseCallback = nil
|
|
964
|
-
return false
|
|
965
|
-
default:
|
|
966
|
-
return true
|
|
967
|
-
}
|
|
520
|
+
private func optimizedResizeImage(_ image: CIImage, to target: CGSize) -> CIImage? {
|
|
521
|
+
guard let f = CIFilter(name: "CILanczosScaleTransform") else { return nil }
|
|
522
|
+
let s = target.width / image.extent.width
|
|
523
|
+
f.setValue(image, forKey: kCIInputImageKey)
|
|
524
|
+
f.setValue(s, forKey: kCIInputScaleKey)
|
|
525
|
+
f.setValue(1.0, forKey: kCIInputAspectRatioKey)
|
|
526
|
+
return f.outputImage
|
|
968
527
|
}
|
|
969
528
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
return
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
let uiImage = UIImage(cgImage: cgImage)
|
|
980
|
-
let rotatedImage = uiImage.rotate(radians: .pi / 2)
|
|
981
|
-
|
|
982
|
-
// Save the image in the background
|
|
983
|
-
DispatchQueue.global(qos: .userInitiated).async {
|
|
984
|
-
guard let savedURL = self.saveImageAsPNG(rotatedImage) else {
|
|
985
|
-
DispatchQueue.main.async {
|
|
986
|
-
callbackHandler.reject(.processingError)
|
|
987
|
-
}
|
|
988
|
-
return
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// Call the callback on the main thread
|
|
992
|
-
DispatchQueue.main.async {
|
|
993
|
-
callbackHandler.resolve(savedURL.absoluteString)
|
|
994
|
-
}
|
|
529
|
+
private func optimizedSaveImage(_ image: UIImage, quality q: CGFloat) -> URL? {
|
|
530
|
+
guard let data = image.jpegData(compressionQuality: q) else { return nil }
|
|
531
|
+
let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
532
|
+
let url = tmp.appendingPathComponent("image_\(Date().timeIntervalSince1970).jpg")
|
|
533
|
+
do { try data.write(to: url, options: .atomic); return url }
|
|
534
|
+
catch { return nil }
|
|
995
535
|
}
|
|
996
|
-
}
|
|
997
536
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
537
|
+
private func addMetadataToImage(at url: URL) {
|
|
538
|
+
guard let src = CGImageSourceCreateWithURL(url as CFURL, nil),
|
|
539
|
+
let md = CGImageSourceCopyPropertiesAtIndex(src, 0, nil) as? [CFString:Any],
|
|
540
|
+
let dst = CGImageDestinationCreateWithURL(url as CFURL, kUTTypeJPEG, 1, nil)
|
|
541
|
+
else { return }
|
|
542
|
+
var m = md
|
|
543
|
+
m[kCGImagePropertyExifDictionary] = [
|
|
544
|
+
kCGImagePropertyExifUserComment: getOptimizedTransformJSON()
|
|
545
|
+
]
|
|
546
|
+
CGImageDestinationAddImageFromSource(dst, src, 0, m as CFDictionary)
|
|
547
|
+
CGImageDestinationFinalize(dst)
|
|
1008
548
|
}
|
|
1009
549
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
550
|
+
private func getImageQuality() -> CGFloat {
|
|
551
|
+
let now = Date().timeIntervalSince1970
|
|
552
|
+
let delta = now - lastCaptureTime
|
|
553
|
+
lastCaptureTime = now
|
|
554
|
+
if delta < 0.5 {
|
|
555
|
+
captureCount += 1
|
|
556
|
+
return max(0.7, 1.0 - (min(captureCount,5)*0.06))
|
|
557
|
+
} else {
|
|
558
|
+
captureCount = max(0, captureCount-1)
|
|
559
|
+
return 0.9
|
|
1020
560
|
}
|
|
1021
561
|
}
|
|
1022
562
|
|
|
1023
|
-
private func
|
|
1024
|
-
guard let
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
563
|
+
private func getOptimizedTransformJSON() -> String {
|
|
564
|
+
guard let frm = ReplateCameraView.arView.session.currentFrame else { return "{}" }
|
|
565
|
+
let t = frm.camera.transform
|
|
566
|
+
let obj: [String:Any] = [
|
|
567
|
+
"position": ["x":t.columns.3.x, "y":t.columns.3.y, "z":t.columns.3.z],
|
|
568
|
+
"gravityVector": ReplateCameraView.gravityVector
|
|
569
|
+
]
|
|
570
|
+
if let d = try? JSONSerialization.data(withJSONObject: obj),
|
|
571
|
+
let s = String(data: d, encoding: .utf8) {
|
|
572
|
+
return s
|
|
1033
573
|
}
|
|
574
|
+
return "{}"
|
|
1034
575
|
}
|
|
1035
576
|
|
|
1036
|
-
private func
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
return
|
|
1043
|
-
}
|
|
1044
|
-
completionCalled = true
|
|
1045
|
-
completion(result)
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
// Validate spheres initialization
|
|
1049
|
-
guard ReplateCameraView.spheresModels.count >= 144 else {
|
|
1050
|
-
print("[updateSpheres] Spheres not fully initialized. Count: \(ReplateCameraView.spheresModels.count)")
|
|
1051
|
-
safeCompletion(false)
|
|
1052
|
-
return
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
// Get anchor
|
|
1056
|
-
guard let anchorNode = ReplateCameraView.anchorEntity else {
|
|
1057
|
-
print("[updateSpheres] No anchor entity found.")
|
|
1058
|
-
safeCompletion(false)
|
|
1059
|
-
return
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Calculate camera metrics
|
|
1063
|
-
let cameraDistance = isCameraWithinRange(
|
|
1064
|
-
cameraTransform: cameraTransform,
|
|
1065
|
-
anchorEntity: anchorNode
|
|
1066
|
-
)
|
|
1067
|
-
|
|
1068
|
-
// Calculate sphere index
|
|
1069
|
-
let angleDegrees = angleBetweenAnchorXAndCamera(
|
|
1070
|
-
anchor: anchorNode,
|
|
1071
|
-
cameraTransform: cameraTransform
|
|
1072
|
-
)
|
|
1073
|
-
let sphereIndex = max(Int(round(angleDegrees / 5.0)), 0) % 72
|
|
1074
|
-
|
|
1075
|
-
DispatchQueue.main.async {
|
|
1076
|
-
self.processSphereUpdate(
|
|
1077
|
-
sphereIndex: sphereIndex,
|
|
1078
|
-
targetIndex: deviceTargetInfo.targetIndex,
|
|
1079
|
-
completion: safeCompletion
|
|
1080
|
-
)
|
|
577
|
+
private func updateCircleFocus(targetIndex: Int) {
|
|
578
|
+
if targetIndex != ReplateCameraView.circleInFocus {
|
|
579
|
+
setOpacityToCircle(circleId: ReplateCameraView.circleInFocus, opacity: 0.5)
|
|
580
|
+
setOpacityToCircle(circleId: targetIndex, opacity: 1.0)
|
|
581
|
+
ReplateCameraView.circleInFocus = targetIndex
|
|
582
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
|
|
1081
583
|
}
|
|
1082
584
|
}
|
|
1083
585
|
|
|
1084
|
-
private func
|
|
586
|
+
private func updateSpheresAfterCapture(targetIndex: Int, sphereIndex: Int, isNewAngle: Bool) {
|
|
1085
587
|
var mesh: ModelEntity?
|
|
1086
|
-
var
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
return
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
if !ReplateCameraView.upperSpheresSet[sphereIndex] {
|
|
1097
|
-
ReplateCameraView.upperSpheresSet[sphereIndex] = true
|
|
1098
|
-
ReplateCameraView.photosFromDifferentAnglesTaken += 1
|
|
1099
|
-
newAngle = true
|
|
1100
|
-
|
|
1101
|
-
guard 72 + sphereIndex < ReplateCameraView.spheresModels.count else {
|
|
1102
|
-
print("[updateSpheres] Upper spheresModels index out of range")
|
|
1103
|
-
completion(false)
|
|
1104
|
-
return
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
mesh = ReplateCameraView.spheresModels[72 + sphereIndex]
|
|
1108
|
-
|
|
1109
|
-
if ReplateCameraView.upperSpheresSet.allSatisfy({ $0 }) {
|
|
1110
|
-
callback = ReplateCameraController.completedUpperSpheresCallback
|
|
1111
|
-
ReplateCameraController.completedUpperSpheresCallback = nil
|
|
1112
|
-
}
|
|
588
|
+
var cb: RCTResponseSenderBlock?
|
|
589
|
+
if targetIndex==1 && isNewAngle {
|
|
590
|
+
ReplateCameraView.upperSpheresSet[sphereIndex]=true
|
|
591
|
+
ReplateCameraView.photosFromDifferentAnglesTaken+=1
|
|
592
|
+
mesh = ReplateCameraView.spheresModels[safe:72+sphereIndex]
|
|
593
|
+
if ReplateCameraView.upperSpheresSet.allSatisfy({$0}) {
|
|
594
|
+
cb = Self.completedUpperSpheresCallback; Self.completedUpperSpheresCallback=nil
|
|
1113
595
|
}
|
|
1114
|
-
} else if targetIndex
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
596
|
+
} else if targetIndex==0 && isNewAngle {
|
|
597
|
+
ReplateCameraView.lowerSpheresSet[sphereIndex]=true
|
|
598
|
+
ReplateCameraView.photosFromDifferentAnglesTaken+=1
|
|
599
|
+
mesh = ReplateCameraView.spheresModels[safe:sphereIndex]
|
|
600
|
+
if ReplateCameraView.lowerSpheresSet.allSatisfy({$0}) {
|
|
601
|
+
cb = Self.completedLowerSpheresCallback; Self.completedLowerSpheresCallback=nil
|
|
1119
602
|
}
|
|
1120
|
-
|
|
1121
|
-
if !ReplateCameraView.lowerSpheresSet[sphereIndex] {
|
|
1122
|
-
ReplateCameraView.lowerSpheresSet[sphereIndex] = true
|
|
1123
|
-
ReplateCameraView.photosFromDifferentAnglesTaken += 1
|
|
1124
|
-
newAngle = true
|
|
1125
|
-
|
|
1126
|
-
guard sphereIndex < ReplateCameraView.spheresModels.count else {
|
|
1127
|
-
print("[updateSpheres] Lower spheresModels index out of range")
|
|
1128
|
-
completion(false)
|
|
1129
|
-
return
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
mesh = ReplateCameraView.spheresModels[sphereIndex]
|
|
1133
|
-
|
|
1134
|
-
if ReplateCameraView.lowerSpheresSet.allSatisfy({ $0 }) {
|
|
1135
|
-
callback = ReplateCameraController.completedLowerSpheresCallback
|
|
1136
|
-
ReplateCameraController.completedLowerSpheresCallback = nil
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
if let mesh = mesh {
|
|
1142
|
-
let material = SimpleMaterial(color: .green, roughness: 1, isMetallic: false)
|
|
1143
|
-
mesh.model?.materials[0] = material
|
|
1144
|
-
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
|
|
1145
603
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// MARK: - Anchor Validation
|
|
1152
|
-
func isAnchorNodeValid(_ anchorNode: AnchorEntity) -> Bool {
|
|
1153
|
-
var isValid = false
|
|
1154
|
-
if(Thread.isMainThread){
|
|
1155
|
-
let transform = anchorNode.transform
|
|
1156
|
-
let position = transform.translation
|
|
1157
|
-
let rotation = transform.rotation
|
|
1158
|
-
let scale = transform.scale
|
|
1159
|
-
|
|
1160
|
-
isValid = !position.isNaN &&
|
|
1161
|
-
!rotation.isNaN &&
|
|
1162
|
-
scale != SIMD3<Float>(0, 0, 0) &&
|
|
1163
|
-
abs(length(rotation.vector) - 1.0) < 0.0001
|
|
1164
|
-
}else{
|
|
1165
|
-
DispatchQueue.main.sync {
|
|
1166
|
-
let transform = anchorNode.transform
|
|
1167
|
-
let position = transform.translation
|
|
1168
|
-
let rotation = transform.rotation
|
|
1169
|
-
let scale = transform.scale
|
|
1170
|
-
|
|
1171
|
-
isValid = !position.isNaN &&
|
|
1172
|
-
!rotation.isNaN &&
|
|
1173
|
-
scale != SIMD3<Float>(0, 0, 0) &&
|
|
1174
|
-
abs(length(rotation.vector) - 1.0) < 0.0001
|
|
604
|
+
if let m = mesh {
|
|
605
|
+
m.model?.materials[0] = SimpleMaterial(color:.green, roughness:1, isMetallic:false)
|
|
606
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength:.light)
|
|
1175
607
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
return isValid
|
|
608
|
+
cb?([])
|
|
1179
609
|
}
|
|
1180
610
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
cameraTransform.columns.3.z
|
|
1190
|
-
)
|
|
1191
|
-
let anchorPosition = anchorEntity.transform.translation
|
|
1192
|
-
distance = distanceBetween(cameraPosition, anchorPosition)
|
|
1193
|
-
}else{
|
|
1194
|
-
if(Thread.isMainThread){
|
|
1195
|
-
let cameraPosition = SIMD3<Float>(
|
|
1196
|
-
cameraTransform.columns.3.x,
|
|
1197
|
-
cameraTransform.columns.3.y,
|
|
1198
|
-
cameraTransform.columns.3.z
|
|
1199
|
-
)
|
|
1200
|
-
let anchorPosition = anchorEntity.transform.translation
|
|
1201
|
-
distance = distanceBetween(cameraPosition, anchorPosition)
|
|
1202
|
-
}else{
|
|
1203
|
-
DispatchQueue.main.sync {
|
|
1204
|
-
let cameraPosition = SIMD3<Float>(
|
|
1205
|
-
cameraTransform.columns.3.x,
|
|
1206
|
-
cameraTransform.columns.3.y,
|
|
1207
|
-
cameraTransform.columns.3.z
|
|
1208
|
-
)
|
|
1209
|
-
let anchorPosition = anchorEntity.transform.translation
|
|
1210
|
-
distance = distanceBetween(cameraPosition, anchorPosition)
|
|
1211
|
-
}
|
|
611
|
+
private func setOpacityToCircle(circleId: Int, opacity: Float) {
|
|
612
|
+
DispatchQueue.main.async {
|
|
613
|
+
let mat = SimpleMaterial(color: UIColor.white.withAlphaComponent(CGFloat(opacity)),
|
|
614
|
+
roughness:1, isMetallic:false)
|
|
615
|
+
for i in 0..<72 {
|
|
616
|
+
let idx = circleId==0 ? i : 72+i
|
|
617
|
+
ReplateCameraView.spheresModels[safe:idx]?.model?.materials[0]=mat
|
|
618
|
+
}
|
|
1212
619
|
}
|
|
1213
|
-
|
|
1214
|
-
}
|
|
1215
|
-
return distance <= Self.MIN_DISTANCE ? -1 :
|
|
1216
|
-
distance >= Self.MAX_DISTANCE ? 1 : 0
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
func distanceBetween(_ pos1: SIMD3<Float>, _ pos2: SIMD3<Float>) -> Float {
|
|
1220
|
-
let difference = pos1 - pos2
|
|
1221
|
-
return sqrt(dot(difference, difference))
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
// MARK: - Static Utility Methods
|
|
1225
|
-
func getTransformRelativeToAnchor(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> simd_float4x4 {
|
|
1226
|
-
var relativeTransform: simd_float4x4!
|
|
1227
|
-
if(Thread.isMainThread){
|
|
1228
|
-
let anchorTransform = anchor.transformMatrix(relativeTo: nil)
|
|
1229
|
-
relativeTransform = anchorTransform.inverse * cameraTransform
|
|
1230
|
-
}else{
|
|
1231
|
-
DispatchQueue.main.sync {
|
|
1232
|
-
let anchorTransform = anchor.transformMatrix(relativeTo: nil)
|
|
1233
|
-
relativeTransform = anchorTransform.inverse * cameraTransform
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
return relativeTransform
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
func angleBetweenAnchorXAndCamera(anchor: AnchorEntity, cameraTransform: simd_float4x4) -> Float {
|
|
1240
|
-
var angleDegrees: Float = 0
|
|
1241
|
-
if(Thread.isMainThread){
|
|
1242
|
-
let anchorTransform = anchor.transform.matrix
|
|
1243
|
-
let anchorPositionXZ = simd_float2(
|
|
1244
|
-
anchor.transform.translation.x,
|
|
1245
|
-
anchor.transform.translation.z
|
|
1246
|
-
)
|
|
1247
|
-
let relativeCameraPositionXZ = simd_float2(
|
|
1248
|
-
cameraTransform.columns.3.x,
|
|
1249
|
-
cameraTransform.columns.3.z
|
|
1250
|
-
)
|
|
1251
|
-
|
|
1252
|
-
let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
|
|
1253
|
-
let anchorXAxisXZ = simd_float2(
|
|
1254
|
-
anchorTransform.columns.0.x,
|
|
1255
|
-
anchorTransform.columns.0.z
|
|
1256
|
-
)
|
|
1257
|
-
|
|
1258
|
-
let angle = atan2(directionXZ.y, directionXZ.x) -
|
|
1259
|
-
atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
|
|
1260
|
-
|
|
1261
|
-
angleDegrees = angle * (180.0 / .pi)
|
|
1262
|
-
if angleDegrees < 0 {
|
|
1263
|
-
angleDegrees += 360
|
|
1264
|
-
}
|
|
1265
|
-
}else{
|
|
1266
|
-
DispatchQueue.main.sync {
|
|
1267
|
-
let anchorTransform = anchor.transform.matrix
|
|
1268
|
-
let anchorPositionXZ = simd_float2(
|
|
1269
|
-
anchor.transform.translation.x,
|
|
1270
|
-
anchor.transform.translation.z
|
|
1271
|
-
)
|
|
1272
|
-
let relativeCameraPositionXZ = simd_float2(
|
|
1273
|
-
cameraTransform.columns.3.x,
|
|
1274
|
-
cameraTransform.columns.3.z
|
|
1275
|
-
)
|
|
1276
|
-
|
|
1277
|
-
let directionXZ = relativeCameraPositionXZ - anchorPositionXZ
|
|
1278
|
-
let anchorXAxisXZ = simd_float2(
|
|
1279
|
-
anchorTransform.columns.0.x,
|
|
1280
|
-
anchorTransform.columns.0.z
|
|
1281
|
-
)
|
|
1282
|
-
|
|
1283
|
-
let angle = atan2(directionXZ.y, directionXZ.x) -
|
|
1284
|
-
atan2(anchorXAxisXZ.y, anchorXAxisXZ.x)
|
|
1285
|
-
|
|
1286
|
-
angleDegrees = angle * (180.0 / .pi)
|
|
1287
|
-
if angleDegrees < 0 {
|
|
1288
|
-
angleDegrees += 360
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
return angleDegrees
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
func cgImage(from ciImage: CIImage) -> CGImage? {
|
|
1296
|
-
let context = CIContext(options: nil)
|
|
1297
|
-
return context.createCGImage(ciImage, from: ciImage.extent)
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
func saveImageAsJPEG(_ image: UIImage) -> URL? {
|
|
1301
|
-
guard let imageData = image.jpegData(compressionQuality: 1),
|
|
1302
|
-
let source = CGImageSourceCreateWithData(imageData as CFData, nil) else {
|
|
1303
|
-
return nil
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
1307
|
-
let uniqueFilename = "image_\(Date().timeIntervalSince1970).jpg"
|
|
1308
|
-
let fileURL = temporaryDirectoryURL.appendingPathComponent(uniqueFilename)
|
|
1309
|
-
|
|
1310
|
-
guard let imageProperties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] else {
|
|
1311
|
-
return nil
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
var mutableMetadata = imageProperties
|
|
1315
|
-
mutableMetadata[kCGImagePropertyExifDictionary] = [
|
|
1316
|
-
kCGImagePropertyExifUserComment: getTransformJSON(session: ReplateCameraView.arView.session)
|
|
1317
|
-
]
|
|
1318
|
-
|
|
1319
|
-
guard let destination = CGImageDestinationCreateWithURL(
|
|
1320
|
-
fileURL as CFURL,
|
|
1321
|
-
kUTTypeJPEG,
|
|
1322
|
-
1,
|
|
1323
|
-
nil
|
|
1324
|
-
) else {
|
|
1325
|
-
return nil
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
CGImageDestinationAddImageFromSource(
|
|
1329
|
-
destination,
|
|
1330
|
-
source,
|
|
1331
|
-
0,
|
|
1332
|
-
mutableMetadata as CFDictionary
|
|
1333
|
-
)
|
|
1334
|
-
|
|
1335
|
-
guard CGImageDestinationFinalize(destination) else {
|
|
1336
|
-
return nil
|
|
1337
620
|
}
|
|
1338
621
|
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
622
|
+
// MARK: Anchor & Transform Helpers
|
|
623
|
+
private func getValidAnchorEntity() throws -> AnchorEntity {
|
|
624
|
+
var a: AnchorEntity?
|
|
625
|
+
if Thread.isMainThread { a = ReplateCameraView.anchorEntity }
|
|
626
|
+
else { DispatchQueue.main.sync { a = ReplateCameraView.anchorEntity } }
|
|
627
|
+
guard let anchor = a else { throw ARError.noAnchor }
|
|
628
|
+
guard isAnchorNodeValid(anchor) else { throw ARError.invalidAnchor }
|
|
629
|
+
return anchor
|
|
1347
630
|
}
|
|
1348
631
|
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
632
|
+
private func getDeviceTargetInfoFromTransform(anchorEntity: AnchorEntity,
|
|
633
|
+
cameraTransform: simd_float4x4) throws -> DeviceTargetInfo {
|
|
634
|
+
let camPos = SIMD3<Float>(cameraTransform.columns.3.x,
|
|
635
|
+
cameraTransform.columns.3.y,
|
|
636
|
+
cameraTransform.columns.3.z)
|
|
637
|
+
var anchorPos: SIMD3<Float>!
|
|
638
|
+
if Thread.isMainThread { anchorPos = anchorEntity.position(relativeTo:nil) }
|
|
639
|
+
else { DispatchQueue.main.sync { anchorPos = anchorEntity.position(relativeTo:nil) } }
|
|
640
|
+
let devDir = normalize(SIMD3(-cameraTransform.columns.2.x,
|
|
641
|
+
-cameraTransform.columns.2.y,
|
|
642
|
+
-cameraTransform.columns.2.z))
|
|
643
|
+
let toAnchor = normalize(anchorPos - camPos)
|
|
644
|
+
let angle = acos(dot(devDir, toAnchor))
|
|
645
|
+
let dist = distanceBetween(camPos, anchorPos)
|
|
646
|
+
let relT = getTransformRelativeToAnchor(anchor: anchorEntity, cameraTransform: cameraTransform)
|
|
647
|
+
let ti = determineTargetIndex(angleToAnchor: angle, relativeCameraTransform: relT)
|
|
648
|
+
return DeviceTargetInfo(isInFocus: angle<Self.ANGLE_THRESHOLD,
|
|
649
|
+
targetIndex: ti,
|
|
650
|
+
transform: relT,
|
|
651
|
+
cameraPosition: camPos,
|
|
652
|
+
deviceDirection: devDir,
|
|
653
|
+
distance: dist)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private func determineTargetIndex(angleToAnchor: Float,
|
|
657
|
+
relativeCameraTransform: simd_float4x4) -> Int {
|
|
658
|
+
guard angleToAnchor < Self.ANGLE_THRESHOLD else { return -1 }
|
|
659
|
+
let twoThirds = ReplateCameraView.spheresHeight
|
|
660
|
+
+ ReplateCameraView.distanceBetweenCircles
|
|
661
|
+
+ ReplateCameraView.distanceBetweenCircles/5
|
|
662
|
+
let camH = relativeCameraTransform.columns.3.y
|
|
663
|
+
return camH < twoThirds ? 0 : 1
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
private func isAnchorNodeValid(_ anchor: AnchorEntity) -> Bool {
|
|
667
|
+
var ok = false
|
|
668
|
+
if Thread.isMainThread {
|
|
669
|
+
let t = anchor.transform
|
|
670
|
+
ok = !t.translation.isNaN &&
|
|
671
|
+
!t.rotation.isNaN &&
|
|
672
|
+
t.scale != SIMD3<Float>(repeating:0) &&
|
|
673
|
+
abs(length(t.rotation.vector)-1.0)<0.0001
|
|
674
|
+
} else {
|
|
675
|
+
DispatchQueue.main.sync {
|
|
676
|
+
let t = anchor.transform
|
|
677
|
+
ok = !t.translation.isNaN &&
|
|
678
|
+
!t.rotation.isNaN &&
|
|
679
|
+
t.scale != SIMD3<Float>(repeating:0) &&
|
|
680
|
+
abs(length(t.rotation.vector)-1.0)<0.0001
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return ok
|
|
1357
684
|
}
|
|
1358
685
|
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
mutableMetadata[kCGImagePropertyPNGDictionary] = [
|
|
1362
|
-
kCGImagePropertyPNGComment: getTransformJSON(session: ReplateCameraView.arView.session)
|
|
1363
|
-
]
|
|
1364
|
-
|
|
1365
|
-
// Create destination for PNG file
|
|
1366
|
-
guard let destination = CGImageDestinationCreateWithURL(
|
|
1367
|
-
fileURL as CFURL,
|
|
1368
|
-
kUTTypePNG,
|
|
1369
|
-
1,
|
|
1370
|
-
nil
|
|
1371
|
-
) else {
|
|
1372
|
-
return nil
|
|
686
|
+
private func distanceBetween(_ p1: SIMD3<Float>, _ p2: SIMD3<Float>) -> Float {
|
|
687
|
+
sqrt(dot(p1-p2, p1-p2))
|
|
1373
688
|
}
|
|
1374
689
|
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
690
|
+
private func getTransformRelativeToAnchor(anchor: AnchorEntity,
|
|
691
|
+
cameraTransform: simd_float4x4) -> simd_float4x4 {
|
|
692
|
+
var rel: simd_float4x4!
|
|
693
|
+
if Thread.isMainThread {
|
|
694
|
+
rel = anchor.transformMatrix(relativeTo:nil).inverse * cameraTransform
|
|
695
|
+
} else {
|
|
696
|
+
DispatchQueue.main.sync {
|
|
697
|
+
rel = anchor.transformMatrix(relativeTo:nil).inverse * cameraTransform
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return rel
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private func angleBetweenAnchorXAndCamera(anchor: AnchorEntity,
|
|
704
|
+
cameraTransform: simd_float4x4) -> Float {
|
|
705
|
+
var deg: Float = 0
|
|
706
|
+
if Thread.isMainThread {
|
|
707
|
+
let at = anchor.transform.matrix
|
|
708
|
+
let ap = simd_float2(anchor.transform.translation.x,
|
|
709
|
+
anchor.transform.translation.z)
|
|
710
|
+
let cp = simd_float2(cameraTransform.columns.3.x,
|
|
711
|
+
cameraTransform.columns.3.z)
|
|
712
|
+
let dir = cp - ap
|
|
713
|
+
let xAxis = simd_float2(at.columns.0.x, at.columns.0.z)
|
|
714
|
+
let a = atan2(dir.y, dir.x) - atan2(xAxis.y, xAxis.x)
|
|
715
|
+
deg = a * (180.0/.pi)
|
|
716
|
+
if deg<0 { deg+=360 }
|
|
717
|
+
} else {
|
|
718
|
+
DispatchQueue.main.sync {
|
|
719
|
+
let at = anchor.transform.matrix
|
|
720
|
+
let ap = simd_float2(anchor.transform.translation.x,
|
|
721
|
+
anchor.transform.translation.z)
|
|
722
|
+
let cp = simd_float2(cameraTransform.columns.3.x,
|
|
723
|
+
cameraTransform.columns.3.z)
|
|
724
|
+
let dir = cp - ap
|
|
725
|
+
let xAxis = simd_float2(at.columns.0.x, at.columns.0.z)
|
|
726
|
+
let a = atan2(dir.y, dir.x) - atan2(xAxis.y, xAxis.x)
|
|
727
|
+
deg = a * (180.0/.pi)
|
|
728
|
+
if deg<0 { deg+=360 }
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return deg
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Legacy for compatibility
|
|
735
|
+
func saveImageAsJPEG(_ image: UIImage) -> URL? {
|
|
736
|
+
guard let data = image.jpegData(compressionQuality:1),
|
|
737
|
+
let src = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
|
|
738
|
+
let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory:true)
|
|
739
|
+
let url = tmp.appendingPathComponent("image_\(Date().timeIntervalSince1970).jpg")
|
|
740
|
+
guard let props = CGImageSourceCopyPropertiesAtIndex(src,0,nil) as? [CFString:Any],
|
|
741
|
+
let dst = CGImageDestinationCreateWithURL(url as CFURL,
|
|
742
|
+
kUTTypeJPEG,1,nil)
|
|
743
|
+
else { return nil }
|
|
744
|
+
var m = props
|
|
745
|
+
m[kCGImagePropertyExifDictionary] = [
|
|
746
|
+
kCGImagePropertyExifUserComment: getOptimizedTransformJSON()
|
|
747
|
+
]
|
|
748
|
+
CGImageDestinationAddImageFromSource(dst,src,0,m as CFDictionary)
|
|
749
|
+
guard CGImageDestinationFinalize(dst) else { return nil }
|
|
750
|
+
return url
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
func saveImageAsPNG(_ image: UIImage) -> URL? {
|
|
754
|
+
guard let data = image.pngData(),
|
|
755
|
+
let src = CGImageSourceCreateWithData(data as CFData, nil) else { return nil }
|
|
756
|
+
let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory:true)
|
|
757
|
+
let url = tmp.appendingPathComponent("image_\(Date().timeIntervalSince1970).png")
|
|
758
|
+
guard let props = CGImageSourceCopyPropertiesAtIndex(src,0,nil) as? [CFString:Any],
|
|
759
|
+
let dst = CGImageDestinationCreateWithURL(url as CFURL,
|
|
760
|
+
kUTTypePNG,1,nil)
|
|
761
|
+
else { return nil }
|
|
762
|
+
var m = props
|
|
763
|
+
m[kCGImagePropertyPNGDictionary] = [
|
|
764
|
+
kCGImagePropertyPNGComment: getOptimizedTransformJSON()
|
|
765
|
+
]
|
|
766
|
+
CGImageDestinationAddImageFromSource(dst,src,0,m as CFDictionary)
|
|
767
|
+
guard CGImageDestinationFinalize(dst) else { return nil }
|
|
768
|
+
return url
|
|
1386
769
|
}
|
|
1387
770
|
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
// Format as JSON
|
|
1425
|
-
let jsonObject: [String: Any] = [
|
|
1426
|
-
"translation": translation,
|
|
1427
|
-
"rotation": rotation,
|
|
1428
|
-
"scale": scale,
|
|
1429
|
-
"gravityVector": ReplateCameraView.gravityVector
|
|
1430
|
-
]
|
|
1431
|
-
|
|
1432
|
-
// Convert dictionary to JSON string
|
|
1433
|
-
if let jsonData = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted),
|
|
1434
|
-
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
1435
|
-
return jsonString
|
|
1436
|
-
} else {
|
|
1437
|
-
return "{}" // Return empty JSON if conversion fails
|
|
771
|
+
func getTransformJSON(session: ARSession) -> String {
|
|
772
|
+
let t = session.currentFrame?.camera.transform ?? simd_float4x4()
|
|
773
|
+
let translation = [
|
|
774
|
+
"x": t.columns.3.x,
|
|
775
|
+
"y": t.columns.3.y,
|
|
776
|
+
"z": t.columns.3.z
|
|
777
|
+
]
|
|
778
|
+
let scale = [
|
|
779
|
+
"x": simd_length(simd_float3(t.columns.0.x,
|
|
780
|
+
t.columns.0.y,
|
|
781
|
+
t.columns.0.z)),
|
|
782
|
+
"y": simd_length(simd_float3(t.columns.1.x,
|
|
783
|
+
t.columns.1.y,
|
|
784
|
+
t.columns.1.z)),
|
|
785
|
+
"z": simd_length(simd_float3(t.columns.2.x,
|
|
786
|
+
t.columns.2.y,
|
|
787
|
+
t.columns.2.z))
|
|
788
|
+
]
|
|
789
|
+
let rotMat = simd_float3x3(columns: (
|
|
790
|
+
simd_float3(t.columns.0.x/scale["x"]!,t.columns.0.y/scale["x"]!,t.columns.0.z/scale["x"]!),
|
|
791
|
+
simd_float3(t.columns.1.x/scale["y"]!,t.columns.1.y/scale["y"]!,t.columns.1.z/scale["y"]!),
|
|
792
|
+
simd_float3(t.columns.2.x/scale["z"]!,t.columns.2.y/scale["z"]!,t.columns.2.z/scale["z"]!)
|
|
793
|
+
))
|
|
794
|
+
let q = simd_quatf(rotMat)
|
|
795
|
+
let rotation = ["x":q.vector.x,"y":q.vector.y,"z":q.vector.z,"w":q.vector.w]
|
|
796
|
+
let obj: [String:Any] = [
|
|
797
|
+
"translation": translation,
|
|
798
|
+
"rotation": rotation,
|
|
799
|
+
"scale": scale,
|
|
800
|
+
"gravityVector": ReplateCameraView.gravityVector
|
|
801
|
+
]
|
|
802
|
+
if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted),
|
|
803
|
+
let s = String(data: d, encoding: .utf8) {
|
|
804
|
+
return s
|
|
805
|
+
}
|
|
806
|
+
return "{}"
|
|
1438
807
|
}
|
|
1439
|
-
}
|
|
1440
808
|
}
|
|
1441
809
|
|
|
810
|
+
// MARK: - ARView Coaching Extension
|
|
1442
811
|
extension ARView: ARCoachingOverlayViewDelegate {
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// Example callback for the delegate object
|
|
1467
|
-
public func coachingOverlayViewDidDeactivate(
|
|
1468
|
-
_ coachingOverlayView: ARCoachingOverlayView
|
|
1469
|
-
) {
|
|
1470
|
-
let callback = ReplateCameraController.completedTutorialCallback
|
|
1471
|
-
if (callback != nil) {
|
|
1472
|
-
callback!([])
|
|
1473
|
-
ReplateCameraController.completedTutorialCallback = nil
|
|
812
|
+
func addCoaching() {
|
|
813
|
+
let overlay = ARCoachingOverlayView()
|
|
814
|
+
overlay.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
815
|
+
addSubview(overlay)
|
|
816
|
+
overlay.center = convert(center, from: superview)
|
|
817
|
+
overlay.goal = .horizontalPlane
|
|
818
|
+
overlay.session = session
|
|
819
|
+
overlay.delegate = self
|
|
820
|
+
overlay.setActive(true, animated: true)
|
|
821
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .light)
|
|
822
|
+
if let cb = ReplateCameraController.openedTutorialCallback {
|
|
823
|
+
cb([]); ReplateCameraController.openedTutorialCallback = nil
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
public func coachingOverlayViewDidDeactivate(_ coachingOverlayView: ARCoachingOverlayView) {
|
|
827
|
+
if let cb = ReplateCameraController.completedTutorialCallback {
|
|
828
|
+
cb([]); ReplateCameraController.completedTutorialCallback = nil
|
|
829
|
+
}
|
|
830
|
+
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
|
|
831
|
+
ReplateCameraView.addRecognizers()
|
|
1474
832
|
}
|
|
1475
|
-
ReplateCameraView.INSTANCE.generateImpactFeedback(strength: .heavy)
|
|
1476
|
-
ReplateCameraView.addRecognizers()
|
|
1477
|
-
}
|
|
1478
833
|
}
|
|
1479
834
|
|
|
835
|
+
// MARK: - UIImage averageColor
|
|
1480
836
|
extension UIImage {
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
return nil
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
// Create a pointer to the pixel data
|
|
1502
|
-
let data: UnsafePointer<UInt8> = CFDataGetBytePtr(pixelData)
|
|
1503
|
-
|
|
1504
|
-
var totalRed: CGFloat = 0
|
|
1505
|
-
var totalGreen: CGFloat = 0
|
|
1506
|
-
var totalBlue: CGFloat = 0
|
|
1507
|
-
|
|
1508
|
-
// Loop through each pixel and calculate sum of RGB values
|
|
1509
|
-
for y in 0..<height {
|
|
1510
|
-
for x in 0..<width {
|
|
1511
|
-
let pixelInfo: Int = ((width * y) + x) * 4
|
|
1512
|
-
let red = CGFloat(data[pixelInfo]) / 255.0
|
|
1513
|
-
let green = CGFloat(data[pixelInfo + 1]) / 255.0
|
|
1514
|
-
let blue = CGFloat(data[pixelInfo + 2]) / 255.0
|
|
1515
|
-
|
|
1516
|
-
totalRed += red
|
|
1517
|
-
totalGreen += green
|
|
1518
|
-
totalBlue += blue
|
|
1519
|
-
}
|
|
837
|
+
func averageColor() -> UIColor? {
|
|
838
|
+
guard let cg = cgImage,
|
|
839
|
+
let dataProv = cg.dataProvider,
|
|
840
|
+
let data = dataProv.data else { return nil }
|
|
841
|
+
let ptr = CFDataGetBytePtr(data)
|
|
842
|
+
let w = cg.width, h = cg.height
|
|
843
|
+
var r: CGFloat=0, g: CGFloat=0, b: CGFloat=0
|
|
844
|
+
for y in 0..<h {
|
|
845
|
+
for x in 0..<w {
|
|
846
|
+
let idx = ((w*y)+x)*4
|
|
847
|
+
r += CGFloat(ptr[idx]) /255
|
|
848
|
+
g += CGFloat(ptr[idx+1]) /255
|
|
849
|
+
b += CGFloat(ptr[idx+2]) /255
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
let count = CGFloat(w*h)
|
|
853
|
+
return UIColor(red: r/count, green: g/count, blue: b/count, alpha: 1)
|
|
1520
854
|
}
|
|
1521
|
-
|
|
1522
|
-
// Calculate average RGB values
|
|
1523
|
-
let count = CGFloat(width * height)
|
|
1524
|
-
let averageRed = totalRed / count
|
|
1525
|
-
let averageGreen = totalGreen / count
|
|
1526
|
-
let averageBlue = totalBlue / count
|
|
1527
|
-
|
|
1528
|
-
// Create and return average color
|
|
1529
|
-
return UIColor(red: averageRed, green: averageGreen, blue: averageBlue, alpha: 1.0)
|
|
1530
|
-
}
|
|
1531
855
|
}
|
|
1532
856
|
|
|
857
|
+
// MARK: - UIColor RGB Components
|
|
1533
858
|
extension UIColor {
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
var alpha: CGFloat = 0
|
|
1539
|
-
|
|
1540
|
-
// Check if the color can be converted to RGB
|
|
1541
|
-
guard self.getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
|
|
1542
|
-
return nil
|
|
859
|
+
func getRGBComponents() -> (red:CGFloat, green:CGFloat, blue:CGFloat)? {
|
|
860
|
+
var r:CGFloat=0, g:CGFloat=0, b:CGFloat=0, a:CGFloat=0
|
|
861
|
+
guard getRed(&r, green:&g, blue:&b, alpha:&a) else { return nil }
|
|
862
|
+
return (r,g,b)
|
|
1543
863
|
}
|
|
1544
|
-
|
|
1545
|
-
return (red, green, blue)
|
|
1546
|
-
}
|
|
1547
864
|
}
|
|
1548
865
|
|
|
866
|
+
// MARK: - SIMD & simd_quatf Extensions
|
|
1549
867
|
public extension simd_float2 {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
868
|
+
static func -(lhs: simd_float2, rhs: simd_float2) -> simd_float2 {
|
|
869
|
+
return simd_float2(lhs.x - rhs.x, lhs.y - rhs.y)
|
|
870
|
+
}
|
|
1553
871
|
}
|
|
1554
|
-
|
|
1555
872
|
public extension SIMD3 where Scalar == Float {
|
|
1556
|
-
|
|
1557
|
-
return x.isNaN || y.isNaN || z.isNaN
|
|
1558
|
-
}
|
|
873
|
+
var isNaN: Bool { x.isNaN || y.isNaN || z.isNaN }
|
|
1559
874
|
}
|
|
1560
|
-
|
|
1561
875
|
public extension simd_quatf {
|
|
1562
|
-
|
|
1563
|
-
return vector.x.isNaN || vector.y.isNaN || vector.w.isNaN
|
|
1564
|
-
}
|
|
876
|
+
var isNaN: Bool { vector.x.isNaN || vector.y.isNaN || vector.w.isNaN }
|
|
1565
877
|
}
|