Skip to content

Commit 8905f67

Browse files
authored
Merge pull request #255 from olexale/version-1.2.2
Version 1.3.0
2 parents 8868800 + c85cebd commit 8905f67

7 files changed

Lines changed: 196 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.3.0
4+
5+
* BREAKING CHANGE! Make network image loading asynchronous
6+
* Implement batch processing for image detection when exceeding 100 images (by @vlad0209)
7+
38
## 1.2.1
49

510
* Enhance ARKit initialization by adding onInitialized callback (by @vlad0209)

ios/Classes/FlutterArkitView+Initialization.swift

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,22 @@ extension FlutterArkitView {
2626
? (detectionImages, "detectionImages")
2727
: (trackingImages, "trackingImages")
2828

29-
if allImages.count > 100 {
30-
let imageBatches = stride(from: 0, to: allImages.count, by: 100).map {
31-
Array(allImages[$0..<min($0 + 100, allImages.count)])
29+
let imageNames = allImages.compactMap { $0["name"] as? String }
30+
prefetchImagesIfNeeded(imageNames) { [weak self] in
31+
guard let self = self else { return }
32+
if allImages.count > 100 {
33+
let imageBatches = stride(from: 0, to: allImages.count, by: 100).map {
34+
Array(allImages[$0..<min($0 + 100, allImages.count)])
35+
}
36+
self.runImageDetectionBatches(
37+
baseArguments: arguments,
38+
imageKey: key,
39+
imageBatches: imageBatches,
40+
sendInitialized: true
41+
)
42+
} else {
43+
self.runConfiguration(arguments, sendInitialized: true)
3244
}
33-
runImageDetectionBatches(
34-
baseArguments: arguments,
35-
imageKey: key,
36-
imageBatches: imageBatches,
37-
sendInitialized: true
38-
)
39-
} else {
40-
runConfiguration(arguments, sendInitialized: true)
4145
}
4246
}
4347

ios/Classes/GeometryBuilders/GeometryBuilder.swift

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ARKit
2+
import Foundation
23

34
func createGeometry(_ arguments: [String: Any]?, withDevice device: MTLDevice?) -> SCNGeometry? {
45
if let arguments = arguments {
@@ -68,19 +69,19 @@ private func parseMaterial(_ dict: [String: Any]) -> SCNMaterial {
6869
material.blendMode = SCNBlendMode(rawValue: dict["blendMode"] as! Int)!
6970
material.isDoubleSided = dict["doubleSided"] as! Bool
7071

71-
material.diffuse.contents = parsePropertyContents(dict["diffuse"])
72-
material.ambient.contents = parsePropertyContents(dict["ambient"])
73-
material.specular.contents = parsePropertyContents(dict["specular"])
74-
material.emission.contents = parsePropertyContents(dict["emission"])
75-
material.transparent.contents = parsePropertyContents(dict["transparent"])
76-
material.reflective.contents = parsePropertyContents(dict["reflective"])
77-
material.multiply.contents = parsePropertyContents(dict["multiply"])
78-
material.normal.contents = parsePropertyContents(dict["normal"])
79-
material.displacement.contents = parsePropertyContents(dict["displacement"])
80-
material.ambientOcclusion.contents = parsePropertyContents(dict["ambientOcclusion"])
81-
material.selfIllumination.contents = parsePropertyContents(dict["selfIllumination"])
82-
material.metalness.contents = parsePropertyContents(dict["metalness"])
83-
material.roughness.contents = parsePropertyContents(dict["roughness"])
72+
assignMaterialProperty(material.diffuse, from: dict["diffuse"])
73+
assignMaterialProperty(material.ambient, from: dict["ambient"])
74+
assignMaterialProperty(material.specular, from: dict["specular"])
75+
assignMaterialProperty(material.emission, from: dict["emission"])
76+
assignMaterialProperty(material.transparent, from: dict["transparent"])
77+
assignMaterialProperty(material.reflective, from: dict["reflective"])
78+
assignMaterialProperty(material.multiply, from: dict["multiply"])
79+
assignMaterialProperty(material.normal, from: dict["normal"])
80+
assignMaterialProperty(material.displacement, from: dict["displacement"])
81+
assignMaterialProperty(material.ambientOcclusion, from: dict["ambientOcclusion"])
82+
assignMaterialProperty(material.selfIllumination, from: dict["selfIllumination"])
83+
assignMaterialProperty(material.metalness, from: dict["metalness"])
84+
assignMaterialProperty(material.roughness, from: dict["roughness"])
8485

8586
return material
8687
}
@@ -171,3 +172,30 @@ private func parsePropertyContents(_ dict: Any?) -> Any? {
171172
}
172173
return nil
173174
}
175+
176+
private func assignMaterialProperty(_ property: SCNMaterialProperty, from value: Any?) {
177+
guard let dict = value as? [String: Any] else {
178+
property.contents = nil
179+
return
180+
}
181+
182+
if let imageName = dict["image"] as? String {
183+
if let image = getImageByName(imageName) {
184+
property.contents = image
185+
return
186+
}
187+
if let url = URL(string: imageName),
188+
url.scheme == "http" || url.scheme == "https"
189+
{
190+
fetchNetworkImageIfNeeded(url) { image in
191+
guard let image = image else { return }
192+
DispatchQueue.main.async {
193+
property.contents = image
194+
}
195+
}
196+
}
197+
return
198+
}
199+
200+
property.contents = parsePropertyContents(dict)
201+
}

ios/Classes/Utils/ARHitResultsHelper.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ func getARHitResultsArray(_ sceneView: ARSCNView, atLocation location: CGPoint)
77
}
88

99
private func getARHitResults(_ sceneView: ARSCNView, atLocation location: CGPoint) -> [ARHitTestResult] {
10-
var types = ARHitTestResult.ResultType(
10+
let types = ARHitTestResult.ResultType(
1111
[.featurePoint, .estimatedHorizontalPlane, .existingPlane, .existingPlaneUsingExtent, .estimatedVerticalPlane, .existingPlaneUsingGeometry])
1212
let results = sceneView.hitTest(location, types: types)
1313
return results
Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,128 @@
11
import Foundation
22

3+
private let networkSession: URLSession = {
4+
let sessionConfig = URLSessionConfiguration.default
5+
sessionConfig.timeoutIntervalForRequest = 15
6+
sessionConfig.timeoutIntervalForResource = 20
7+
return URLSession(configuration: sessionConfig)
8+
}()
9+
10+
private let imageCache = NSCache<NSString, UIImage>()
11+
private let imageFetchQueue = DispatchQueue(label: "arkit.imageFetchQueue")
12+
private var pendingImageRequests: [String: [(UIImage?) -> Void]] = [:]
13+
14+
func prefetchImagesIfNeeded(_ names: [String], completion: @escaping () -> Void) {
15+
let urls = names.compactMap { URL(string: $0) }.filter {
16+
$0.scheme == "http" || $0.scheme == "https"
17+
}
18+
19+
guard !urls.isEmpty else {
20+
completion()
21+
return
22+
}
23+
24+
let group = DispatchGroup()
25+
for url in urls {
26+
if imageCache.object(forKey: url.absoluteString as NSString) != nil {
27+
continue
28+
}
29+
group.enter()
30+
fetchNetworkImageIfNeeded(url) { _ in
31+
group.leave()
32+
}
33+
}
34+
35+
group.notify(queue: .main) {
36+
completion()
37+
}
38+
}
39+
40+
func fetchNetworkImageIfNeeded(_ url: URL, completion: @escaping (UIImage?) -> Void) {
41+
if let cached = imageCache.object(forKey: url.absoluteString as NSString) {
42+
completion(cached)
43+
return
44+
}
45+
let cacheKey = url.absoluteString
46+
var shouldStartRequest = false
47+
imageFetchQueue.sync {
48+
if pendingImageRequests[cacheKey] != nil {
49+
pendingImageRequests[cacheKey]?.append(completion)
50+
} else {
51+
pendingImageRequests[cacheKey] = [completion]
52+
shouldStartRequest = true
53+
}
54+
}
55+
if !shouldStartRequest {
56+
return
57+
}
58+
59+
var request = URLRequest(url: url)
60+
request.cachePolicy = .useProtocolCachePolicy
61+
request.setValue("Mozilla/5.0", forHTTPHeaderField: "User-Agent")
62+
63+
let task = networkSession.dataTask(with: request) { data, response, error in
64+
if let http = response as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
65+
let httpURLString = http.url?.absoluteString ?? ""
66+
debugPrint("getImageByName: network non-2xx status \(http.statusCode) url \(httpURLString)")
67+
}
68+
69+
if let error = error {
70+
let nsError = error as NSError
71+
debugPrint(
72+
"getImageByName: network load failed for \(url.absoluteString) " +
73+
"error: \(nsError.localizedDescription) " +
74+
"domain: \(nsError.domain) code: \(nsError.code) " +
75+
"userInfo: \(nsError.userInfo)"
76+
)
77+
let completions = imageFetchQueue.sync { pendingImageRequests.removeValue(forKey: cacheKey) } ?? []
78+
completions.forEach { $0(nil) }
79+
return
80+
}
81+
82+
guard let data = data else {
83+
debugPrint("getImageByName: network returned no data for \(url.absoluteString)")
84+
let completions = imageFetchQueue.sync { pendingImageRequests.removeValue(forKey: cacheKey) } ?? []
85+
completions.forEach { $0(nil) }
86+
return
87+
}
88+
89+
let img = UIImage(data: data)
90+
if img == nil {
91+
debugPrint("getImageByName: network data not decodable as image for \(url.absoluteString) (bytes: \(data.count))")
92+
} else {
93+
imageCache.setObject(img!, forKey: url.absoluteString as NSString)
94+
}
95+
let completions = imageFetchQueue.sync { pendingImageRequests.removeValue(forKey: cacheKey) } ?? []
96+
completions.forEach { $0(img) }
97+
}
98+
task.resume()
99+
}
100+
3101
func getImageByName(_ name: String) -> UIImage? {
4102
if let img = UIImage(named: name) {
5103
return img
6104
}
7105
if let path = Bundle.main.path(forResource: SwiftArkitPlugin.registrar!.lookupKey(forAsset: name), ofType: nil) {
8-
return UIImage(named: path)
106+
let img = UIImage(named: path)
107+
if img == nil {
108+
debugPrint("getImageByName: failed to load asset image at path \(path) for \(name)")
109+
}
110+
return img
9111
}
10112
if let url = URL(string: name) {
11-
do {
12-
let data = try Data(contentsOf: url)
13-
return UIImage(data: data)
14-
} catch {}
113+
if let cached = imageCache.object(forKey: url.absoluteString as NSString) {
114+
return cached
115+
}
116+
debugPrint("getImageByName: network image not prefetched for \(name)")
117+
return nil
15118
}
16119
if let base64 = Data(base64Encoded: name, options: .ignoreUnknownCharacters) {
17-
return UIImage(data: base64)
120+
let img = UIImage(data: base64)
121+
if img == nil {
122+
debugPrint("getImageByName: base64 data not decodable as image (bytes: \(base64.count))")
123+
}
124+
return img
18125
}
126+
debugPrint("getImageByName: failed to resolve image for \(name)")
19127
return nil
20128
}

ios/Classes/Utils/ReferenceImagesParser.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,24 @@ func parseReferenceImagesSet(_ images: [[String: Any]]) -> Set<ARReferenceImage>
77
}
88

99
func parseReferenceImage(_ dict: [String: Any]) -> ARReferenceImage? {
10-
if let physicalWidth = dict["physicalWidth"] as? Double,
11-
let name = dict["name"] as? String,
12-
let image = getImageByName(name),
13-
let cgImage = image.cgImage
14-
{
15-
let referenceImage = ARReferenceImage(cgImage, orientation: CGImagePropertyOrientation.up, physicalWidth: CGFloat(physicalWidth))
16-
referenceImage.name = name
17-
return referenceImage
10+
guard let physicalWidth = dict["physicalWidth"] as? Double else {
11+
debugPrint("ARKitReferenceImage: missing or invalid physicalWidth: \(dict)")
12+
return nil
1813
}
19-
return nil
14+
guard let name = dict["name"] as? String else {
15+
debugPrint("ARKitReferenceImage: missing or invalid name: \(dict)")
16+
return nil
17+
}
18+
guard let image = getImageByName(name) else {
19+
debugPrint("ARKitReferenceImage: failed to load image for name: \(name)")
20+
return nil
21+
}
22+
guard let cgImage = image.cgImage else {
23+
debugPrint("ARKitReferenceImage: loaded image has no CGImage for name: \(name)")
24+
return nil
25+
}
26+
27+
let referenceImage = ARReferenceImage(cgImage, orientation: CGImagePropertyOrientation.up, physicalWidth: CGFloat(physicalWidth))
28+
referenceImage.name = name
29+
return referenceImage
2030
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: arkit_plugin
22
description: Flutter Plugin for ARKit - Apple's augmented reality (AR) development platform for iOS mobile devices.
3-
version: 1.2.1
3+
version: 1.3.0
44
repository: https://github.com/olexale/arkit_flutter_plugin
55
issue_tracker: https://github.com/olexale/arkit_flutter_plugin/issues
66
topics:

0 commit comments

Comments
 (0)