@webspatial/platform-visionos 1.0.4 → 1.1.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/package.json +2 -2
- package/web-spatial/EventEmitter.swift +56 -0
- package/web-spatial/JSBCommand.swift +348 -0
- package/web-spatial/SpatialObject.swift +108 -0
- package/web-spatial/WebMsgCommand.swift +137 -0
- package/web-spatial/WebSpatialApp.swift +111 -0
- package/web-spatial/{libs/uiKitDelegate/Window.swift → Window.swift} +1 -0
- package/web-spatial/manager/Dynamic3DManager.swift +114 -0
- package/web-spatial/manager/JSBManager.swift +148 -0
- package/web-spatial/manager/WKWebViewManager.swift +39 -0
- package/web-spatial/{libs/webView/manifest.swift → manifest.swift} +16 -5
- package/web-spatial/model/SpatialApp.swift +190 -0
- package/web-spatial/model/SpatialScene.swift +1078 -0
- package/web-spatial/model/Spatialized2DElement.swift +128 -0
- package/web-spatial/model/SpatializedDynamic3DElement.swift +34 -0
- package/web-spatial/model/SpatializedElement.swift +95 -0
- package/web-spatial/model/SpatializedStatic3DElement.swift +19 -0
- package/web-spatial/model/dynamic3d/Geometry.swift +95 -0
- package/web-spatial/model/dynamic3d/SpatialComponent.swift +72 -0
- package/web-spatial/model/dynamic3d/SpatialEntity.swift +211 -0
- package/web-spatial/model/dynamic3d/SpatialMaterial.swift +39 -0
- package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +39 -0
- package/web-spatial/model/dynamic3d/SpatialModelResource.swift +38 -0
- package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +18 -0
- package/web-spatial/protocol/ScrollAbleSpatialElementContainer.swift +1 -0
- package/web-spatial/protocol/SpatialScrollAble.swift +8 -0
- package/web-spatial/protocol/SpatializedElementContainer.swift +8 -0
- package/web-spatial/protocol/WebMsgSender.swift +5 -0
- package/web-spatial/types/Vec2.swift +12 -0
- package/web-spatial/types/Vec3.swift +7 -0
- package/web-spatial/view/SceneHandlerUIView.swift +92 -0
- package/web-spatial/{views/ui/NavView.swift → view/SpatialNavView.swift} +149 -77
- package/web-spatial/view/SpatialSceneContentView.swift +218 -0
- package/web-spatial/view/SpatialSceneView.swift +53 -0
- package/web-spatial/view/Spatialized2DElementView.swift +96 -0
- package/web-spatial/view/SpatializedDynamic3DView.swift +173 -0
- package/web-spatial/view/SpatializedElementView.swift +178 -0
- package/web-spatial/view/SpatializedStatic3DView.swift +72 -0
- package/web-spatial/{views → view/view-modifier}/MaterialWithBorderCornerModifier.swift +28 -8
- package/web-spatial/webview/SpatialWebController.swift +300 -0
- package/web-spatial/webview/SpatialWebView.swift +34 -0
- package/web-spatial/webview/SpatialWebViewModel.swift +307 -0
- package/web-spatial.xcodeproj/project.pbxproj +126 -207
- package/web-spatial/libs/EventEmitter.swift +0 -25
- package/web-spatial/libs/SpatialComponent.swift +0 -24
- package/web-spatial/libs/SpatialEntity.swift +0 -172
- package/web-spatial/libs/SpatialInputComponent.swift +0 -19
- package/web-spatial/libs/SpatialMeshResource.swift +0 -12
- package/web-spatial/libs/SpatialModel3DComponent.swift +0 -51
- package/web-spatial/libs/SpatialModelComponent.swift +0 -25
- package/web-spatial/libs/SpatialObject.swift +0 -140
- package/web-spatial/libs/SpatialPhysicallyBasedMaterial.swift +0 -19
- package/web-spatial/libs/SpatialViewComponent.swift +0 -8
- package/web-spatial/libs/SpatialWindowComponent.swift +0 -454
- package/web-spatial/libs/SpatialWindowContainer.swift +0 -153
- package/web-spatial/libs/Utils/CommandManager.swift +0 -823
- package/web-spatial/libs/Utils/PerfClock.swift +0 -43
- package/web-spatial/libs/Utils/SceneManager.swift +0 -101
- package/web-spatial/libs/Utils/WindowContainerMgr.swift +0 -122
- package/web-spatial/libs/json/JsonParser.swift +0 -45
- package/web-spatial/libs/webView/UpdateSystem.swift +0 -26
- package/web-spatial/libs/webView/backend/NativeWebView.swift +0 -350
- package/web-spatial/views/ImmersiveView.swift +0 -17
- package/web-spatial/views/OpenDismissHandlerUI.swift +0 -45
- package/web-spatial/views/PlainWindowContainerView.swift +0 -132
- package/web-spatial/views/SpatialModel3DView.swift +0 -187
- package/web-spatial/views/SpatialViewUI.swift +0 -168
- package/web-spatial/views/SpatialWebViewUI.swift +0 -179
- package/web-spatial/views/VolumetricWindowContainerView.swift +0 -30
- package/web-spatial/web_spatialApp.swift +0 -141
- /package/web-spatial/{libs/Utils → Utils}/ColorExtension.swift +0 -0
- /package/web-spatial/{libs/Utils → Utils}/Logger.swift +0 -0
- /package/web-spatial/{views → view}/LoadingView.swift +0 -0
- /package/web-spatial/{views → view/view-modifier}/HideViewModifier.swift +0 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import typealias RealityKit.Attachment
|
|
2
|
+
import typealias RealityKit.Entity
|
|
3
|
+
import typealias RealityKit.MeshResource
|
|
4
|
+
import typealias RealityKit.Model3D
|
|
5
|
+
import typealias RealityKit.ModelEntity
|
|
6
|
+
import typealias RealityKit.RealityView
|
|
7
|
+
import typealias RealityKit.SimpleMaterial
|
|
8
|
+
import SwiftUI
|
|
9
|
+
|
|
10
|
+
func getCGFloat(_ val: Double?) -> CGFloat? {
|
|
11
|
+
if let v = val {
|
|
12
|
+
return CGFloat(v)
|
|
13
|
+
} else {
|
|
14
|
+
return nil
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@main
|
|
19
|
+
struct WebSpatialApp: App {
|
|
20
|
+
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
21
|
+
|
|
22
|
+
@State var app = SpatialApp.Instance
|
|
23
|
+
|
|
24
|
+
func getDefaultSize() -> CGSize {
|
|
25
|
+
let ans = CGSize(
|
|
26
|
+
width: app
|
|
27
|
+
.getSceneOptions().defaultSize!.width,
|
|
28
|
+
height: app
|
|
29
|
+
.getSceneOptions().defaultSize!.height
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return ans
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func getDefaultSize3D() -> Size3D {
|
|
36
|
+
let ans = Size3D(
|
|
37
|
+
width: app
|
|
38
|
+
.getSceneOptions().defaultSize!.width,
|
|
39
|
+
height: app
|
|
40
|
+
.getSceneOptions().defaultSize!.height,
|
|
41
|
+
depth: app.getSceneOptions().defaultSize!.depth ?? 0
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return ans
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
var body: some Scene {
|
|
48
|
+
WindowGroup(
|
|
49
|
+
id: SpatialScene.WindowStyle.window.rawValue,
|
|
50
|
+
for: String.self
|
|
51
|
+
) { $windowData in
|
|
52
|
+
let spatialScene = SpatialApp.Instance.getScene(windowData)
|
|
53
|
+
SpatialSceneView(spatialScene: spatialScene!)
|
|
54
|
+
}
|
|
55
|
+
defaultValue: {
|
|
56
|
+
let scene = SpatialApp.Instance.createScene(
|
|
57
|
+
startURL,
|
|
58
|
+
.window,
|
|
59
|
+
.visible,
|
|
60
|
+
app.getSceneOptions()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return scene.id
|
|
64
|
+
}
|
|
65
|
+
.windowStyle(.plain)
|
|
66
|
+
.defaultSize(
|
|
67
|
+
getDefaultSize()
|
|
68
|
+
).windowResizability(
|
|
69
|
+
app.getSceneOptions().windowResizability!
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
WindowGroup(id: SpatialScene.WindowStyle.volume.rawValue, for: String.self) { $windowData in
|
|
73
|
+
let spatialScene = SpatialApp.Instance.getScene(windowData)
|
|
74
|
+
SpatialSceneView(spatialScene: spatialScene!)
|
|
75
|
+
.frame(
|
|
76
|
+
minWidth: getCGFloat(
|
|
77
|
+
app.getSceneOptions(windowData)?.resizeRange?.minWidth),
|
|
78
|
+
maxWidth: getCGFloat(
|
|
79
|
+
app.getSceneOptions(windowData)?.resizeRange?.maxWidth),
|
|
80
|
+
minHeight: getCGFloat(
|
|
81
|
+
app.getSceneOptions(windowData)?.resizeRange?.minHeight),
|
|
82
|
+
maxHeight: getCGFloat(
|
|
83
|
+
app.getSceneOptions(windowData)?.resizeRange?.maxHeight)
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
defaultValue: {
|
|
87
|
+
let scene = SpatialApp.Instance.createScene(
|
|
88
|
+
startURL,
|
|
89
|
+
.volume,
|
|
90
|
+
.visible,
|
|
91
|
+
app.getSceneOptions()
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return scene.id
|
|
95
|
+
}
|
|
96
|
+
.windowStyle(.volumetric)
|
|
97
|
+
.defaultSize(
|
|
98
|
+
getDefaultSize3D(),
|
|
99
|
+
in: .meters
|
|
100
|
+
).windowResizability(
|
|
101
|
+
app.getSceneOptions().windowResizability!
|
|
102
|
+
)
|
|
103
|
+
.defaultWorldScaling(app.getSceneOptions().worldScaling)
|
|
104
|
+
.volumeWorldAlignment(app.getSceneOptions().worldAlignment)
|
|
105
|
+
|
|
106
|
+
WindowGroup(id: "loading", for: String.self) { _ in
|
|
107
|
+
LoadingView()
|
|
108
|
+
}
|
|
109
|
+
.defaultSize(CGSize(width: 400, height: 400))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -8,6 +8,7 @@ class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
|
|
8
8
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
|
9
9
|
guard let windowScene = scene as? UIWindowScene else { return }
|
|
10
10
|
window = windowScene.keyWindow // << store !!!
|
|
11
|
+
window?.overrideUserInterfaceStyle = .light
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
// do memory cleanup after scene removed, otherwise windowContainer cannot destroy content after being dismissed
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import RealityKit
|
|
3
|
+
|
|
4
|
+
enum GeometryCreationError: LocalizedError {
|
|
5
|
+
case invalidType(String)
|
|
6
|
+
case missingFields(String, [String])
|
|
7
|
+
|
|
8
|
+
var errorDescription: String? {
|
|
9
|
+
switch self {
|
|
10
|
+
case let .invalidType(t):
|
|
11
|
+
return "invalid geometry type: \(t)"
|
|
12
|
+
case let .missingFields(type, fields):
|
|
13
|
+
return "missing required fields for \(type): " + fields.joined(separator: ", ")
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class Dynamic3DManager {
|
|
19
|
+
static func createEntity(_ props: CreateSpatialEntity) -> SpatialEntity {
|
|
20
|
+
let entity = SpatialEntity()
|
|
21
|
+
entity.name = props.name ?? ""
|
|
22
|
+
return entity
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static func createModelComponent(mesh: Geometry, mats: [SpatialMaterial]) -> SpatialModelComponent {
|
|
26
|
+
return SpatialModelComponent(mesh: mesh, mats: mats)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static func createGeometry(_ props: CreateGeometryProperties) throws -> Geometry {
|
|
30
|
+
guard let type = GeometryType(rawValue: props.type) else {
|
|
31
|
+
throw GeometryCreationError.invalidType(props.type)
|
|
32
|
+
}
|
|
33
|
+
switch type {
|
|
34
|
+
case .BoxGeometry:
|
|
35
|
+
var missing: [String] = []
|
|
36
|
+
if props.width == nil { missing.append("width") }
|
|
37
|
+
if props.height == nil { missing.append("height") }
|
|
38
|
+
if props.depth == nil { missing.append("depth") }
|
|
39
|
+
if !missing.isEmpty { throw GeometryCreationError.missingFields("BoxGeometry", missing) }
|
|
40
|
+
return BoxGeometry(width: props.width!, height: props.height!, depth: props.depth!, cornerRadius: props.cornerRadius ?? 0, splitFaces: props.splitFaces ?? false)
|
|
41
|
+
case .PlaneGeometry:
|
|
42
|
+
var missing: [String] = []
|
|
43
|
+
if props.width == nil { missing.append("width") }
|
|
44
|
+
if props.height == nil { missing.append("height") }
|
|
45
|
+
if !missing.isEmpty { throw GeometryCreationError.missingFields("PlaneGeometry", missing) }
|
|
46
|
+
return PlaneGeometry(width: props.width!, height: props.height!, cornerRadius: props.cornerRadius ?? 0)
|
|
47
|
+
case .SphereGeometry:
|
|
48
|
+
var missing: [String] = []
|
|
49
|
+
if props.radius == nil { missing.append("radius") }
|
|
50
|
+
if !missing.isEmpty { throw GeometryCreationError.missingFields("SphereGeometry", missing) }
|
|
51
|
+
return SphereGeometry(radius: props.radius!)
|
|
52
|
+
case .ConeGeometry:
|
|
53
|
+
var missing: [String] = []
|
|
54
|
+
if props.radius == nil { missing.append("radius") }
|
|
55
|
+
if props.height == nil { missing.append("height") }
|
|
56
|
+
if !missing.isEmpty { throw GeometryCreationError.missingFields("ConeGeometry", missing) }
|
|
57
|
+
return ConeGeometry(radius: props.radius!, height: props.height!)
|
|
58
|
+
case .CylinderGeometry:
|
|
59
|
+
var missing: [String] = []
|
|
60
|
+
if props.radius == nil { missing.append("radius") }
|
|
61
|
+
if props.height == nil { missing.append("height") }
|
|
62
|
+
if !missing.isEmpty { throw GeometryCreationError.missingFields("CylinderGeometry", missing) }
|
|
63
|
+
return CylinderGeometry(radius: props.radius!, height: props.height!)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Error messages are thrown from createGeometry using GeometryCreationError
|
|
68
|
+
|
|
69
|
+
static func createUnlitMaterial(_ props: CreateUnlitMaterial, _ tex: TextureResource? = nil) -> SpatialUnlitMaterial {
|
|
70
|
+
return SpatialUnlitMaterial(props.color ?? "#FFFFFF", tex, props.transparent ?? true, props.opacity ?? 1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static func loadResourceToLocal(_ urlString: String, loadComplete: @escaping (Result<URL, Error>) -> Void) {
|
|
74
|
+
guard let url = URL(string: urlString) else {
|
|
75
|
+
loadComplete(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to create URL from string: \(urlString)"])))
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
var documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
|
79
|
+
documentsUrl.appendPathComponent(url.lastPathComponent)
|
|
80
|
+
let session = URLSession(configuration: URLSessionConfiguration.default)
|
|
81
|
+
var request = URLRequest(url: url)
|
|
82
|
+
request.httpMethod = "GET"
|
|
83
|
+
print("start load")
|
|
84
|
+
let task = session.downloadTask(with: request, completionHandler: { location, response, error in
|
|
85
|
+
if let error = error {
|
|
86
|
+
loadComplete(.failure(error))
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if let httpResponse = response as? HTTPURLResponse, !(200 ... 299).contains(httpResponse.statusCode) {
|
|
90
|
+
let error = NSError(domain: "HTTP Error", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP Error \(httpResponse.statusCode)"])
|
|
91
|
+
loadComplete(.failure(error))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
guard let location = location else {
|
|
95
|
+
loadComplete(.failure(NSError(domain: "Download Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "Download location is nil"])))
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
let fileManager = FileManager.default
|
|
99
|
+
do {
|
|
100
|
+
if fileManager.fileExists(atPath: documentsUrl.path) {
|
|
101
|
+
try fileManager.removeItem(atPath: documentsUrl.path)
|
|
102
|
+
}
|
|
103
|
+
try fileManager.moveItem(at: location, to: documentsUrl)
|
|
104
|
+
print("load complete")
|
|
105
|
+
loadComplete(.success(documentsUrl))
|
|
106
|
+
} catch {
|
|
107
|
+
print("File operation error: \(error)")
|
|
108
|
+
loadComplete(.failure(error))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
})
|
|
112
|
+
task.resume()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
protocol CommandDataProtocol: Decodable {
|
|
4
|
+
static var commandType: String { get }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
protocol ReplyDataProtocol: Encodable {
|
|
8
|
+
static var dataType: String { get }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
struct JsbErrorData: Encodable {
|
|
12
|
+
var code: ReplyCode?
|
|
13
|
+
var message: String?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
enum ReplyCode: Encodable {
|
|
17
|
+
case TypeError
|
|
18
|
+
case CommandError
|
|
19
|
+
case InvalidSpatialObject
|
|
20
|
+
case InvalidMatrix
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
struct JsbError: Error, Encodable {
|
|
24
|
+
let code: ReplyCode
|
|
25
|
+
let message: String
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class JSBManager {
|
|
29
|
+
typealias ResolveHandler<T> = (Result<T?, JsbError>) -> Void
|
|
30
|
+
|
|
31
|
+
private var typeMap = [String: CommandDataProtocol.Type]()
|
|
32
|
+
private var actionWithDataMap: [String: (_ data: CommandDataProtocol, _ event: @escaping ResolveHandler<Encodable>) -> Void] = [:]
|
|
33
|
+
private var actionWithoutDataMap: [String: (@escaping ResolveHandler<Encodable>) -> Void] = [:]
|
|
34
|
+
|
|
35
|
+
private let encoder = JSONEncoder()
|
|
36
|
+
|
|
37
|
+
func register<T: CommandDataProtocol>(_ type: T.Type) {
|
|
38
|
+
typeMap[T.commandType] = type
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func register<T: CommandDataProtocol>(_ type: T.Type, _ event: @escaping (T, @escaping ResolveHandler<Encodable>) -> Void) {
|
|
42
|
+
typeMap[T.commandType] = type
|
|
43
|
+
actionWithDataMap[T.commandType] = { data, result in
|
|
44
|
+
event(data as! T, result)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func register<T: CommandDataProtocol>(_ type: T.Type, _ event: @escaping (@escaping ResolveHandler<Encodable>) -> Void) {
|
|
49
|
+
typeMap[T.commandType] = type
|
|
50
|
+
actionWithoutDataMap[T.commandType] = event
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func remove<T: CommandDataProtocol>(_ type: T.Type) {
|
|
54
|
+
typeMap.removeValue(forKey: T.commandType)
|
|
55
|
+
actionWithDataMap.removeValue(forKey: T.commandType)
|
|
56
|
+
actionWithoutDataMap.removeValue(forKey: T.commandType)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func clear() {
|
|
60
|
+
typeMap = [String: CommandDataProtocol.Type]()
|
|
61
|
+
actionWithDataMap = [:]
|
|
62
|
+
actionWithoutDataMap = [:]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func handlerMessage(_ message: String, _ replyHandler: ((Any?, String?) -> Void)? = nil) {
|
|
66
|
+
do {
|
|
67
|
+
let jsbInfo = message.components(separatedBy: "::")
|
|
68
|
+
let actionKey = jsbInfo[0]
|
|
69
|
+
let hasData = jsbInfo.count == 2 && jsbInfo[1] != ""
|
|
70
|
+
|
|
71
|
+
if hasData {
|
|
72
|
+
let data = try deserialize(cmdType: actionKey, cmdContent: jsbInfo[1])
|
|
73
|
+
if let action = actionWithDataMap[actionKey] {
|
|
74
|
+
handleAction(action: { callback in
|
|
75
|
+
action(data!, callback)
|
|
76
|
+
}, replyHandler: replyHandler)
|
|
77
|
+
} else {
|
|
78
|
+
print("Invalid JSB!!!", message)
|
|
79
|
+
replyHandler?(nil, "Invalid JSB!!! \(message)")
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
if let action = actionWithoutDataMap[actionKey] {
|
|
83
|
+
handleAction(action: action, replyHandler: replyHandler)
|
|
84
|
+
} else {
|
|
85
|
+
print("Invalid JSB!!!", message)
|
|
86
|
+
replyHandler?(nil, "Invalid JSB!!! \(message)")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private func handleAction(action: @escaping (@escaping ResolveHandler<Encodable>) -> Void,
|
|
93
|
+
replyHandler: ((Any?, String?) -> Void)?)
|
|
94
|
+
{
|
|
95
|
+
Task { @MainActor in
|
|
96
|
+
action { result in
|
|
97
|
+
switch result {
|
|
98
|
+
case let .success(data):
|
|
99
|
+
if data == nil {
|
|
100
|
+
replyHandler?("", nil)
|
|
101
|
+
} else {
|
|
102
|
+
replyHandler?(try? data?.toDictionary() ?? "", nil)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case let .failure(error):
|
|
106
|
+
let resultString = self.parseData(JsbErrorData(
|
|
107
|
+
code: error.code,
|
|
108
|
+
message: error.message
|
|
109
|
+
))
|
|
110
|
+
replyHandler?(nil, resultString)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private func deserialize(cmdType: String, cmdContent: String?) throws -> CommandDataProtocol? {
|
|
117
|
+
let decoder = JSONDecoder()
|
|
118
|
+
|
|
119
|
+
guard let type = typeof(for: cmdType) else {
|
|
120
|
+
print("unknownType")
|
|
121
|
+
return nil
|
|
122
|
+
}
|
|
123
|
+
if cmdContent == nil {
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
let concreteData = try decoder.decode(type.self, from: cmdContent!.data(using: .utf8)!)
|
|
127
|
+
return concreteData
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private func typeof(for key: String) -> CommandDataProtocol.Type? {
|
|
131
|
+
return typeMap[key]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private func parseData(_ data: Encodable) -> String? {
|
|
135
|
+
if let jsonData = try? encoder.encode(data) {
|
|
136
|
+
let jsonString = String(data: jsonData, encoding: .utf8)
|
|
137
|
+
return jsonString!
|
|
138
|
+
}
|
|
139
|
+
return nil
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
extension Encodable {
|
|
144
|
+
func toDictionary() throws -> [String: Any] {
|
|
145
|
+
let data = try JSONEncoder().encode(self)
|
|
146
|
+
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! [String: Any]
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
@preconcurrency import WebKit
|
|
3
|
+
|
|
4
|
+
class WKWebViewManager {
|
|
5
|
+
static let Instance = WKWebViewManager()
|
|
6
|
+
|
|
7
|
+
private init() {}
|
|
8
|
+
|
|
9
|
+
func create(controller: SpatialWebController, configuration: WKWebViewConfiguration? = nil, spatialId: String? = "") -> WKWebView {
|
|
10
|
+
let userContentController = WKUserContentController()
|
|
11
|
+
// TODO: get native api instead of PACKAGE_VERSION
|
|
12
|
+
let userScript = WKUserScript(source: "window.WebSpatailEnabled = true; window.WebSpatailNativeVersion = 'PACKAGE_VERSION';", injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
|
13
|
+
userContentController.addUserScript(userScript)
|
|
14
|
+
// userContentController.add(controller, name: "bridge")
|
|
15
|
+
userContentController.addScriptMessageHandler(controller, contentWorld: .page, name: "bridge")
|
|
16
|
+
let myConfig = (configuration != nil) ? configuration! : WKWebViewConfiguration()
|
|
17
|
+
myConfig.userContentController = userContentController
|
|
18
|
+
myConfig.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
19
|
+
myConfig.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
|
|
20
|
+
if myConfig.urlSchemeHandler(forURLScheme: "file") == nil {
|
|
21
|
+
myConfig.setURLSchemeHandler(controller, forURLScheme: "file")
|
|
22
|
+
}
|
|
23
|
+
controller.webview = WKWebView(frame: .zero, configuration: myConfig)
|
|
24
|
+
let configUA = myConfig.applicationNameForUserAgent ?? ""
|
|
25
|
+
// change webview ua
|
|
26
|
+
let ua = controller.webview!.value(forKey: "userAgent") as? String ?? ""
|
|
27
|
+
let webviewVersion = ua.split(separator: configUA)[0].split(separator: "AppleWebKit")[1]
|
|
28
|
+
// TODO: get native api instead of PACKAGE_VERSION
|
|
29
|
+
controller.webview!.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; wv) AppleWebKit\(webviewVersion)WebSpatial/\("PACKAGE_VERSION") SpatialID/\(spatialId!)"
|
|
30
|
+
controller.webview!.uiDelegate = controller
|
|
31
|
+
controller.webview!.allowsBackForwardNavigationGestures = true
|
|
32
|
+
controller.webview!.isInspectable = true
|
|
33
|
+
controller.webview!.allowsLinkPreview = true
|
|
34
|
+
controller.webview!.navigationDelegate = controller
|
|
35
|
+
controller.webview!.scrollView.delegate = controller
|
|
36
|
+
controller.startObserving()
|
|
37
|
+
return controller.webview!
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -5,7 +5,8 @@ var pwaManager = PWAManager()
|
|
|
5
5
|
|
|
6
6
|
struct PWAManager: Codable {
|
|
7
7
|
var isLocal: Bool = false
|
|
8
|
-
var start_url: String = "http://localhost:5173/"
|
|
8
|
+
var start_url: String = "http://localhost:5173/src/static-3d-model"
|
|
9
|
+
|
|
9
10
|
var scope: String = ""
|
|
10
11
|
var id: String = "com.webspatial.pico"
|
|
11
12
|
|
|
@@ -16,12 +17,22 @@ struct PWAManager: Codable {
|
|
|
16
17
|
var display: PWADisplayMode = .minimal
|
|
17
18
|
var display_override: [PWADisplayMode] = []
|
|
18
19
|
var protocol_handlers: [PWAProtocol] = [PWAProtocol(protocolValue: "web+spatial", url: "./?cmd=%s")]
|
|
19
|
-
var mainScene:
|
|
20
|
+
var mainScene: XSceneOptionsJSB = .init(
|
|
20
21
|
defaultSize: .init(
|
|
21
|
-
width:
|
|
22
|
-
height:
|
|
22
|
+
width: 1024,
|
|
23
|
+
height: 768,
|
|
24
|
+
depth: 0.1
|
|
25
|
+
),
|
|
26
|
+
type: nil,
|
|
27
|
+
resizability: ResizeRange(
|
|
28
|
+
minWidth: 0.5 * 1360,
|
|
29
|
+
minHeight: 0.5 * 1360,
|
|
30
|
+
maxWidth: 2 * 1360,
|
|
31
|
+
maxHeight: 2 * 1360
|
|
23
32
|
),
|
|
24
|
-
|
|
33
|
+
worldScaling: nil,
|
|
34
|
+
worldAlignment: nil,
|
|
35
|
+
baseplateVisibility: nil
|
|
25
36
|
)
|
|
26
37
|
var useMainScene: Bool = true
|
|
27
38
|
private var version: String = "PACKAGE_VERSION"
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SwiftUI
|
|
3
|
+
|
|
4
|
+
let logger = Logger()
|
|
5
|
+
|
|
6
|
+
// To load a local path, remove http:// eg. "static-web/"
|
|
7
|
+
let nativeAPIVersion = pwaManager.getVersion()
|
|
8
|
+
|
|
9
|
+
// start URL
|
|
10
|
+
let startURL = pwaManager.start_url
|
|
11
|
+
|
|
12
|
+
let DefaultPlainWindowContainerSize = CGSize(width: 1280, height: 720)
|
|
13
|
+
|
|
14
|
+
enum XLoadingMethod: String, Decodable, Encodable, Hashable {
|
|
15
|
+
case show
|
|
16
|
+
case hide
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
struct XLoadingViewData: Decodable, Hashable, Encodable {
|
|
20
|
+
let sceneID: String
|
|
21
|
+
let method: XLoadingMethod
|
|
22
|
+
let windowStyle: String?
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
struct SceneOptions {
|
|
26
|
+
var defaultSize: Size?
|
|
27
|
+
var windowResizability: WindowResizability?
|
|
28
|
+
var resizeRange: ResizeRange?
|
|
29
|
+
var worldScaling: WorldScalingBehavior
|
|
30
|
+
var worldAlignment: WorldAlignmentBehavior
|
|
31
|
+
var baseplateVisibility: Visibility
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
struct Size: Codable {
|
|
35
|
+
var width: Double
|
|
36
|
+
var height: Double
|
|
37
|
+
var depth: Double?
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
extension SceneOptions {
|
|
42
|
+
init(_ options: XSceneOptionsJSB) {
|
|
43
|
+
defaultSize = Size(
|
|
44
|
+
width: options.defaultSize?.width ?? DefaultPlainWindowContainerSize.width,
|
|
45
|
+
height: options.defaultSize?.height ?? DefaultPlainWindowContainerSize.height,
|
|
46
|
+
depth: options.defaultSize?.depth ?? 0
|
|
47
|
+
)
|
|
48
|
+
windowResizability = decodeWindowResizability(nil)
|
|
49
|
+
resizeRange = options.resizability
|
|
50
|
+
/// volume only
|
|
51
|
+
worldScaling = options.worldScaling?.toSDK ?? .automatic
|
|
52
|
+
worldAlignment = options.worldAlignment?.toSDK ?? .automatic
|
|
53
|
+
baseplateVisibility = options.baseplateVisibility?.toSDK ?? .automatic
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func decodeWindowResizability(_ windowResizability: String?) -> WindowResizability {
|
|
58
|
+
switch windowResizability {
|
|
59
|
+
case "automatic":
|
|
60
|
+
return .automatic
|
|
61
|
+
case "contentSize":
|
|
62
|
+
return .contentSize
|
|
63
|
+
case "contentMinSize":
|
|
64
|
+
return .contentMinSize
|
|
65
|
+
default:
|
|
66
|
+
return .automatic
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Observable
|
|
71
|
+
class SpatialApp {
|
|
72
|
+
private var scenes = [String: SpatialScene]()
|
|
73
|
+
|
|
74
|
+
// delegate properties to pwaManager
|
|
75
|
+
var name: String { pwaManager.name }
|
|
76
|
+
var scope: String { pwaManager.scope }
|
|
77
|
+
var displayMode: PWADisplayMode { pwaManager.display }
|
|
78
|
+
var version: String { pwaManager.getVersion() }
|
|
79
|
+
var startURL: String { pwaManager.start_url }
|
|
80
|
+
|
|
81
|
+
// used to cache scene config
|
|
82
|
+
private var sceneOptions: SceneOptions
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
static let Instance: SpatialApp = .init()
|
|
86
|
+
|
|
87
|
+
init() {
|
|
88
|
+
// init pwa manager
|
|
89
|
+
pwaManager._init()
|
|
90
|
+
|
|
91
|
+
Logger.initLogger()
|
|
92
|
+
|
|
93
|
+
sceneOptions = SceneOptions(pwaManager.mainScene);
|
|
94
|
+
|
|
95
|
+
print("plainSceneOptions",sceneOptions)
|
|
96
|
+
|
|
97
|
+
logger.debug("WebSpatial App Started -------- rootURL: " + startURL)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
func createScene(_ url: String, _ style: SpatialScene.WindowStyle, _ state: SpatialScene.SceneStateKind, _ sceneOptions: SceneOptions? = nil) -> SpatialScene {
|
|
103
|
+
var scene = SpatialScene(url, style, state, sceneOptions)
|
|
104
|
+
scenes[scene.id] = scene
|
|
105
|
+
scene
|
|
106
|
+
.on(event: SpatialObject.Events.Destroyed.rawValue, listener: onSceneDestroyed)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
return scene
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private func onSceneDestroyed(_ object: Any, _ data: Any) {
|
|
113
|
+
var spatialObject = object as! SpatialObject
|
|
114
|
+
spatialObject
|
|
115
|
+
.off(event: SpatialObject.Events.Destroyed.rawValue, listener: onSceneDestroyed)
|
|
116
|
+
|
|
117
|
+
scenes.removeValue(forKey: spatialObject.id)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func getScene(_ id: String) -> SpatialScene? {
|
|
121
|
+
return scenes[id]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
func getSceneOptions() -> SceneOptions {
|
|
126
|
+
return sceneOptions
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func getSceneOptions(_ sceneId:String) -> SceneOptions? {
|
|
130
|
+
let spatialScene = getScene(sceneId)
|
|
131
|
+
return spatialScene?.sceneConfig
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
// used form window.open logic
|
|
136
|
+
public func openWindowGroup(
|
|
137
|
+
_ targetSpatialScene: SpatialScene,
|
|
138
|
+
_ sceneData: SceneOptions
|
|
139
|
+
) {
|
|
140
|
+
if let activeScene = firstActiveScene {
|
|
141
|
+
// cache scene config
|
|
142
|
+
sceneOptions = sceneData
|
|
143
|
+
|
|
144
|
+
DispatchQueue.main.async() {
|
|
145
|
+
activeScene.openWindowData.send(targetSpatialScene.id)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public func closeWindowGroup(_ targetSpatialScene: SpatialScene) {
|
|
152
|
+
if let activeScene = firstActiveScene {
|
|
153
|
+
activeScene.closeWindowData
|
|
154
|
+
.send(targetSpatialScene.id)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// used form window.open logic with loading ui
|
|
159
|
+
public func openLoadingUI(_ targetSpatialScene: SpatialScene,_ open: Bool) {
|
|
160
|
+
let lwgdata = XLoadingViewData(
|
|
161
|
+
sceneID: targetSpatialScene.id,
|
|
162
|
+
method: open ? .show : .hide,
|
|
163
|
+
windowStyle: nil
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if let activeScene = firstActiveScene {
|
|
167
|
+
activeScene.setLoadingWindowData.send(lwgdata)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private var firstActiveScene: SpatialScene? {
|
|
172
|
+
get {
|
|
173
|
+
let activeKV = scenes.first() { kv in
|
|
174
|
+
kv.value.state == .visible
|
|
175
|
+
}
|
|
176
|
+
return (activeKV?.value)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public func focusScene(_ targetSpatialScene: SpatialScene) {
|
|
181
|
+
// only work when fully visible
|
|
182
|
+
if targetSpatialScene.state != .visible {
|
|
183
|
+
return
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if let activeScene = firstActiveScene {
|
|
187
|
+
activeScene.openWindowData.send(targetSpatialScene.id)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|