@webspatial/platform-visionos 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/web-spatial/JSBCommand.swift +43 -0
- package/web-spatial/WebMsgCommand.swift +0 -15
- package/web-spatial/manager/AttachmentManager.swift +7 -4
- package/web-spatial/manager/Dynamic3DManager.swift +10 -0
- package/web-spatial/manager/WKWebViewManager.swift +4 -4
- package/web-spatial/manifest.swift +10 -5
- package/web-spatial/model/SpatialApp.swift +2 -2
- package/web-spatial/model/SpatialScene.swift +212 -35
- package/web-spatial/model/SpatializedDynamic3DElement.swift +18 -1
- package/web-spatial/model/SpatializedElement.swift +40 -0
- package/web-spatial/model/dynamic3d/SpatialComponent.swift +31 -0
- package/web-spatial/model/dynamic3d/SpatialEntity.swift +6 -0
- package/web-spatial/model/dynamic3d/SpatialMaterial.swift +25 -3
- package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +35 -0
- package/web-spatial/model/dynamic3d/SpatialRootEntity.swift +12 -0
- package/web-spatial/view/SceneHandlerUIView.swift +29 -1
- package/web-spatial/view/SpatializedDynamic3DView.swift +20 -1
- package/web-spatial/view/SpatializedElementView.swift +77 -54
- package/web-spatial/view/SpatializedStatic3DView.swift +5 -1
- package/web-spatial/webview/SpatialWebController.swift +15 -1
- package/web-spatial.xcodeproj/project.pbxproj +9 -8
package/package.json
CHANGED
|
@@ -129,6 +129,13 @@ struct ConvertFromSceneToEntity: CommandDataProtocol {
|
|
|
129
129
|
let position: Vec3
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
struct ConvertCoordinate: CommandDataProtocol {
|
|
133
|
+
static let commandType: String = "ConvertCoordinate"
|
|
134
|
+
let position: Vec3
|
|
135
|
+
let fromId: String
|
|
136
|
+
let toId: String
|
|
137
|
+
}
|
|
138
|
+
|
|
132
139
|
struct InspectCommand: CommandDataProtocol {
|
|
133
140
|
static let commandType: String = "Inspect"
|
|
134
141
|
var id: String?
|
|
@@ -138,6 +145,26 @@ protocol SpatialObjectCommand: CommandDataProtocol {
|
|
|
138
145
|
var id: String { get }
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
struct UpdateUnlitMaterialProperties: CommandDataProtocol {
|
|
149
|
+
static let commandType: String = "UpdateUnlitMaterialProperties"
|
|
150
|
+
let id: String
|
|
151
|
+
let color: String?
|
|
152
|
+
let transparent: Bool?
|
|
153
|
+
let opacity: Float?
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
struct RemoveComponentFromEntity: CommandDataProtocol {
|
|
157
|
+
static let commandType: String = "RemoveComponentFromEntity"
|
|
158
|
+
let entityId: String
|
|
159
|
+
let componentId: String
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
struct SetMaterialsOnEntity: CommandDataProtocol {
|
|
163
|
+
static let commandType: String = "SetMaterialsOnEntity"
|
|
164
|
+
let entityId: String
|
|
165
|
+
let materialIds: [String]
|
|
166
|
+
}
|
|
167
|
+
|
|
141
168
|
struct DestroyCommand: CommandDataProtocol {
|
|
142
169
|
static let commandType: String = "Destroy"
|
|
143
170
|
var id: String
|
|
@@ -166,6 +193,7 @@ protocol SpatializedElementProperties: SpatialObjectCommand {
|
|
|
166
193
|
var enableMagnifyGesture: Bool? { get }
|
|
167
194
|
var enableMagnifyEndGesture: Bool? { get }
|
|
168
195
|
var enableTapGesture: Bool? { get }
|
|
196
|
+
var rotateConstrainedToAxis: Vec3? { get }
|
|
169
197
|
}
|
|
170
198
|
|
|
171
199
|
struct UpdateSpatialized2DElementProperties: SpatializedElementProperties {
|
|
@@ -193,6 +221,8 @@ struct UpdateSpatialized2DElementProperties: SpatializedElementProperties {
|
|
|
193
221
|
var enableMagnifyEndGesture: Bool?
|
|
194
222
|
var enableTapGesture: Bool?
|
|
195
223
|
|
|
224
|
+
let rotateConstrainedToAxis: Vec3?
|
|
225
|
+
|
|
196
226
|
let scrollPageEnabled: Bool?
|
|
197
227
|
let material: BackgroundMaterial?
|
|
198
228
|
let cornerRadius: CornerRadius?
|
|
@@ -227,6 +257,8 @@ struct UpdateSpatializedStatic3DElementProperties: SpatializedElementProperties
|
|
|
227
257
|
let enableMagnifyEndGesture: Bool?
|
|
228
258
|
let enableTapGesture: Bool?
|
|
229
259
|
|
|
260
|
+
let rotateConstrainedToAxis: Vec3?
|
|
261
|
+
|
|
230
262
|
let modelURL: String?
|
|
231
263
|
let modelTransform: [Double]?
|
|
232
264
|
}
|
|
@@ -255,6 +287,8 @@ struct UpdateSpatializedDynamic3DElementProperties: SpatializedElementProperties
|
|
|
255
287
|
let enableMagnifyGesture: Bool?
|
|
256
288
|
let enableMagnifyEndGesture: Bool?
|
|
257
289
|
let enableTapGesture: Bool?
|
|
290
|
+
|
|
291
|
+
let rotateConstrainedToAxis: Vec3?
|
|
258
292
|
}
|
|
259
293
|
|
|
260
294
|
struct UpdateSpatializedElementTransform: SpatialObjectCommand {
|
|
@@ -339,6 +373,15 @@ struct GetSpatialSceneStateCommand: CommandDataProtocol {
|
|
|
339
373
|
static let commandType = "GetSpatialSceneState"
|
|
340
374
|
}
|
|
341
375
|
|
|
376
|
+
struct InitializeAttachmentCommand: CommandDataProtocol {
|
|
377
|
+
static let commandType = "InitializeAttachment"
|
|
378
|
+
let id: String
|
|
379
|
+
let parentEntityId: String
|
|
380
|
+
let position: [Float]?
|
|
381
|
+
let size: AttachmentSize?
|
|
382
|
+
let ownerViewId: String
|
|
383
|
+
}
|
|
384
|
+
|
|
342
385
|
struct UpdateAttachmentEntityCommand: CommandDataProtocol {
|
|
343
386
|
static let commandType = "UpdateAttachmentEntity"
|
|
344
387
|
let id: String
|
|
@@ -14,8 +14,6 @@ enum WebSpatialGestureType: String, Encodable {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
enum SpatialWebMsgType: String, Encodable {
|
|
17
|
-
case cubeInfo
|
|
18
|
-
case transform
|
|
19
17
|
case modelloaded
|
|
20
18
|
case modelloadfailed
|
|
21
19
|
case spatialtap
|
|
@@ -30,19 +28,6 @@ enum SpatialWebMsgType: String, Encodable {
|
|
|
30
28
|
case objectdestroy
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
/// notify Spatialized3DElement Container Cube, used for ref.current.getBoundingClientCube()
|
|
34
|
-
struct SpatiaizedContainerClientCube: Encodable {
|
|
35
|
-
let type: SpatialWebMsgType = .cubeInfo
|
|
36
|
-
let origin: Point3D
|
|
37
|
-
let size: Size3D
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// notify Spatialized3DElement Container Transform to SpatialScene, used for ref.current.convertToSpatialScene()
|
|
41
|
-
struct SpatiaizedContainerTransform: Encodable {
|
|
42
|
-
let type: SpatialWebMsgType = .transform
|
|
43
|
-
let detail: AffineTransform3D
|
|
44
|
-
}
|
|
45
|
-
|
|
46
31
|
struct WebSpatialTapGuestureEventDetail: Encodable {
|
|
47
32
|
let location3D: Point3D
|
|
48
33
|
/// Global scene location (maps to clientX/clientY/clientZ on the web side).
|
|
@@ -9,7 +9,7 @@ struct AttachmentInfo: Identifiable, Equatable {
|
|
|
9
9
|
var webViewModel: SpatialWebViewModel
|
|
10
10
|
|
|
11
11
|
static func == (lhs: AttachmentInfo, rhs: AttachmentInfo) -> Bool {
|
|
12
|
-
|
|
12
|
+
lhs.id == rhs.id
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -29,9 +29,9 @@ class AttachmentManager {
|
|
|
29
29
|
id: String,
|
|
30
30
|
parentEntityId: String,
|
|
31
31
|
position: SIMD3<Float>,
|
|
32
|
-
size: CGSize
|
|
32
|
+
size: CGSize,
|
|
33
|
+
webViewModel: SpatialWebViewModel
|
|
33
34
|
) -> AttachmentInfo {
|
|
34
|
-
let webViewModel = SpatialWebViewModel(url: nil)
|
|
35
35
|
webViewModel.setBackgroundTransparent(true)
|
|
36
36
|
// webViewModel.scrollEnabled = false
|
|
37
37
|
|
|
@@ -48,12 +48,14 @@ class AttachmentManager {
|
|
|
48
48
|
|
|
49
49
|
func update(id: String, position: SIMD3<Float>?, size: CGSize?) {
|
|
50
50
|
guard var info = attachments[id] else { return }
|
|
51
|
+
|
|
51
52
|
if let position = position {
|
|
52
53
|
info.position = position
|
|
53
54
|
}
|
|
54
55
|
if let size = size {
|
|
55
56
|
info.size = size
|
|
56
57
|
}
|
|
58
|
+
|
|
57
59
|
attachments[id] = info
|
|
58
60
|
}
|
|
59
61
|
|
|
@@ -66,12 +68,13 @@ class AttachmentManager {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
func get(id: String) -> AttachmentInfo? {
|
|
69
|
-
|
|
71
|
+
attachments[id]
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
func destroyAll() {
|
|
73
75
|
let toDestroy = Array(attachments.values)
|
|
74
76
|
attachments.removeAll()
|
|
77
|
+
|
|
75
78
|
DispatchQueue.main.async {
|
|
76
79
|
for info in toDestroy {
|
|
77
80
|
info.webViewModel.destroy()
|
|
@@ -71,6 +71,16 @@ class Dynamic3DManager {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
static func loadResourceToLocal(_ urlString: String, loadComplete: @escaping (Result<URL, Error>) -> Void) {
|
|
74
|
+
// load local file
|
|
75
|
+
if urlString.starts(with: "file://") {
|
|
76
|
+
guard let localUrl = URL(string: pwaManager.getLocalResourceURL(url: urlString)) else {
|
|
77
|
+
loadComplete(.failure(NSError(domain: "Download Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "Local file is not found"])))
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
loadComplete(.success(localUrl))
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
// load net file
|
|
74
84
|
guard let url = URL(string: urlString) else {
|
|
75
85
|
loadComplete(.failure(NSError(domain: "Invalid URL", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to create URL from string: \(urlString)"])))
|
|
76
86
|
return
|
|
@@ -8,8 +8,8 @@ class WKWebViewManager {
|
|
|
8
8
|
|
|
9
9
|
func create(controller: SpatialWebController, configuration: WKWebViewConfiguration? = nil, spatialId: String? = "") -> WKWebView {
|
|
10
10
|
let userContentController = WKUserContentController()
|
|
11
|
-
// TODO: get native api instead of
|
|
12
|
-
let userScript = WKUserScript(source: "window.WebSpatailEnabled = true; window.WebSpatailNativeVersion = '
|
|
11
|
+
// TODO: get native api instead of using the injected WS_SDK_VERSION placeholder
|
|
12
|
+
let userScript = WKUserScript(source: "window.WebSpatailEnabled = true; window.WebSpatailNativeVersion = 'WS_SDK_VERSION';", injectionTime: .atDocumentStart, forMainFrameOnly: false)
|
|
13
13
|
userContentController.addUserScript(userScript)
|
|
14
14
|
// userContentController.add(controller, name: "bridge")
|
|
15
15
|
userContentController.addScriptMessageHandler(controller, contentWorld: .page, name: "bridge")
|
|
@@ -25,8 +25,8 @@ class WKWebViewManager {
|
|
|
25
25
|
// change webview ua
|
|
26
26
|
let ua = controller.webview!.value(forKey: "userAgent") as? String ?? ""
|
|
27
27
|
let webviewVersion = ua.split(separator: configUA)[0].split(separator: "AppleWebKit")[1]
|
|
28
|
-
// TODO: get native api instead of
|
|
29
|
-
controller.webview!.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; wv) AppleWebKit\(webviewVersion)WebSpatial/\(
|
|
28
|
+
// TODO: get native api instead of relying on injected shell/sdk versions
|
|
29
|
+
controller.webview!.customUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7; wv) AppleWebKit\(webviewVersion)WSAppShell/\(pwaManager.getShellVersion()) WebSpatial/\(pwaManager.getSdkVersion()) SpatialID/\(spatialId!)"
|
|
30
30
|
controller.webview!.uiDelegate = controller
|
|
31
31
|
controller.webview!.allowsBackForwardNavigationGestures = false
|
|
32
32
|
controller.webview!.isInspectable = true
|
|
@@ -6,9 +6,9 @@ var pwaManager = PWAManager()
|
|
|
6
6
|
struct PWAManager: Codable {
|
|
7
7
|
var isLocal: Bool = false
|
|
8
8
|
|
|
9
|
-
var start_url: String = "http://localhost:5173"
|
|
9
|
+
var start_url: String = "http://localhost:5173/"
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// var start_url: String = "http://localhost:5173/#/spatial-drag-gesture"
|
|
12
12
|
|
|
13
13
|
var scope: String = ""
|
|
14
14
|
var id: String = "com.webspatial.pico"
|
|
@@ -38,7 +38,8 @@ struct PWAManager: Codable {
|
|
|
38
38
|
baseplateVisibility: nil
|
|
39
39
|
)
|
|
40
40
|
var useMainScene: Bool = true
|
|
41
|
-
private var
|
|
41
|
+
private var shellVersion: String = "WS_SHELL_VERSION"
|
|
42
|
+
private var sdkVersion: String = "WS_SDK_VERSION"
|
|
42
43
|
|
|
43
44
|
mutating func _init() {
|
|
44
45
|
let urlType = start_url.split(separator: "://").first
|
|
@@ -102,8 +103,12 @@ struct PWAManager: Codable {
|
|
|
102
103
|
return resource
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
func
|
|
106
|
-
return
|
|
106
|
+
func getShellVersion() -> String {
|
|
107
|
+
return shellVersion
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func getSdkVersion() -> String {
|
|
111
|
+
return sdkVersion
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -4,7 +4,7 @@ import SwiftUI
|
|
|
4
4
|
let logger = Logger()
|
|
5
5
|
|
|
6
6
|
/// To load a local path, remove http:// eg. "static-web/"
|
|
7
|
-
let nativeAPIVersion = pwaManager.
|
|
7
|
+
let nativeAPIVersion = pwaManager.getShellVersion()
|
|
8
8
|
|
|
9
9
|
/// start URL
|
|
10
10
|
let startURL = pwaManager.start_url
|
|
@@ -84,7 +84,7 @@ class SpatialApp {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
var version: String {
|
|
87
|
-
pwaManager.
|
|
87
|
+
pwaManager.getShellVersion()
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
var startURL: String {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Combine
|
|
2
2
|
import Foundation
|
|
3
|
+
import RealityKit
|
|
3
4
|
import simd
|
|
4
5
|
import SwiftUI
|
|
5
6
|
|
|
@@ -86,6 +87,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
86
87
|
|
|
87
88
|
var spatialWebViewModel: SpatialWebViewModel
|
|
88
89
|
|
|
90
|
+
private var meterToPtUnscaled: Double?
|
|
91
|
+
private var meterToPtScaled: Double?
|
|
92
|
+
|
|
89
93
|
init(
|
|
90
94
|
_ url: String,
|
|
91
95
|
_ windowStyle: WindowStyle,
|
|
@@ -108,6 +112,20 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
108
112
|
spatialWebViewModel.sendWebEvent(id, msg)
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
func onUpdatePhysicalMetrics(meterToPtUnscaled: Double, meterToPtScaled: Double) {
|
|
116
|
+
self.meterToPtUnscaled = meterToPtUnscaled
|
|
117
|
+
self.meterToPtScaled = meterToPtScaled
|
|
118
|
+
let js = """
|
|
119
|
+
window.__webspatialsdk__ = window.__webspatialsdk__ || {};
|
|
120
|
+
window.__webspatialsdk__.physicalMetrics = {
|
|
121
|
+
meterToPtUnscaled: \(meterToPtUnscaled),
|
|
122
|
+
meterToPtScaled: \(meterToPtScaled)
|
|
123
|
+
};
|
|
124
|
+
"""
|
|
125
|
+
spatialWebViewModel.getController().callJS(js)
|
|
126
|
+
sendWebMsg("window", "")
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
private func setupSpatialWebView() {
|
|
112
130
|
setupJSBListeners()
|
|
113
131
|
setupWebViewStateListener()
|
|
@@ -300,6 +318,12 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
300
318
|
spatialWebViewModel.addJSBListener(ConvertFromEntityToEntity.self, onConvertFromEntityToEntity)
|
|
301
319
|
spatialWebViewModel.addJSBListener(ConvertFromEntityToScene.self, onConvertFromEntityToScene)
|
|
302
320
|
spatialWebViewModel.addJSBListener(ConvertFromSceneToEntity.self, onConvertFromSceneToEntity)
|
|
321
|
+
spatialWebViewModel.addJSBListener(InitializeAttachmentCommand.self, onInitializeAttachment)
|
|
322
|
+
spatialWebViewModel.addJSBListener(ConvertCoordinate.self, onConvertCoordinate)
|
|
323
|
+
|
|
324
|
+
spatialWebViewModel.addJSBListener(UpdateUnlitMaterialProperties.self, onUpdateUnlitMaterialProperties)
|
|
325
|
+
spatialWebViewModel.addJSBListener(RemoveComponentFromEntity.self, onRemoveComponentFromEntity)
|
|
326
|
+
spatialWebViewModel.addJSBListener(SetMaterialsOnEntity.self, onSetMaterialsOnEntity)
|
|
303
327
|
|
|
304
328
|
spatialWebViewModel.addJSBListener(UpdateAttachmentEntityCommand.self, onUpdateAttachmentEntity)
|
|
305
329
|
|
|
@@ -326,8 +350,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
326
350
|
|
|
327
351
|
// write through
|
|
328
352
|
spatialWebViewModel.updateWindowKV([
|
|
329
|
-
"
|
|
330
|
-
"
|
|
353
|
+
"xrInnerDepth": depth,
|
|
354
|
+
"xrOuterDepth": depth,
|
|
331
355
|
"outerHeight": height + SpatialScene.navHeight,
|
|
332
356
|
])
|
|
333
357
|
}
|
|
@@ -349,6 +373,17 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
349
373
|
self.handleWindowClose()
|
|
350
374
|
}
|
|
351
375
|
|
|
376
|
+
spatialWebViewModel.addStateListener(.didReceive) {
|
|
377
|
+
if let meterToPtUnscaled = self.meterToPtUnscaled,
|
|
378
|
+
let meterToPtScaled = self.meterToPtScaled
|
|
379
|
+
{
|
|
380
|
+
self.onUpdatePhysicalMetrics(
|
|
381
|
+
meterToPtUnscaled: meterToPtUnscaled,
|
|
382
|
+
meterToPtScaled: meterToPtScaled
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
352
387
|
spatialWebViewModel.addStateListener(.didFailLoad) {
|
|
353
388
|
self.didFailLoad = true
|
|
354
389
|
}
|
|
@@ -394,46 +429,51 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
394
429
|
}
|
|
395
430
|
}
|
|
396
431
|
|
|
432
|
+
// Temporary storage for webview models awaiting JSB initialization
|
|
433
|
+
private var pendingAttachmentWebViewModels = [String: SpatialWebViewModel]()
|
|
434
|
+
|
|
397
435
|
private func handleCreateAttachment(_ url: URL) -> WebViewElementInfo? {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
436
|
+
// Just create a bare webview — metadata arrives via InitializeAttachment JSB
|
|
437
|
+
let id = UUID().uuidString
|
|
438
|
+
let webViewModel = SpatialWebViewModel(url: nil)
|
|
439
|
+
webViewModel.setBackgroundTransparent(true)
|
|
440
|
+
pendingAttachmentWebViewModels[id] = webViewModel
|
|
441
|
+
return WebViewElementInfo(id: id, element: webViewModel)
|
|
442
|
+
}
|
|
404
443
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
444
|
+
private func onInitializeAttachment(
|
|
445
|
+
command: InitializeAttachmentCommand,
|
|
446
|
+
resolve: @escaping JSBManager.ResolveHandler<Encodable>
|
|
447
|
+
) {
|
|
448
|
+
guard let webViewModel = pendingAttachmentWebViewModels.removeValue(forKey: command.id) else {
|
|
449
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "No pending attachment for \(command.id)")))
|
|
450
|
+
return
|
|
408
451
|
}
|
|
409
452
|
|
|
410
|
-
// Parse position (JSON array like [0,0.1,0])
|
|
411
453
|
var position = SIMD3<Float>(0, 0, 0)
|
|
412
|
-
if let
|
|
413
|
-
|
|
414
|
-
let positionArray = try? JSONDecoder().decode([Float].self, from: positionData),
|
|
415
|
-
positionArray.count >= 3
|
|
416
|
-
{
|
|
417
|
-
position = SIMD3<Float>(positionArray[0], positionArray[1], positionArray[2])
|
|
454
|
+
if let posArray = command.position, posArray.count >= 3 {
|
|
455
|
+
position = SIMD3<Float>(posArray[0], posArray[1], posArray[2])
|
|
418
456
|
}
|
|
419
457
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
let sizeObj = try? JSONDecoder().decode(AttachmentSize.self, from: sizeData)
|
|
425
|
-
{
|
|
426
|
-
size = CGSize(width: sizeObj.width, height: sizeObj.height)
|
|
427
|
-
}
|
|
458
|
+
let size = CGSize(
|
|
459
|
+
width: command.size?.width ?? 100,
|
|
460
|
+
height: command.size?.height ?? 100
|
|
461
|
+
)
|
|
428
462
|
|
|
429
|
-
let
|
|
430
|
-
|
|
431
|
-
|
|
463
|
+
let ownerId = command.ownerViewId
|
|
464
|
+
if spatialObjects[ownerId] == nil {
|
|
465
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "ownerViewId must belong to the current scene for attachment \(command.id)")))
|
|
466
|
+
return
|
|
467
|
+
}
|
|
468
|
+
attachmentManager.create(
|
|
469
|
+
id: command.id,
|
|
470
|
+
parentEntityId: command.parentEntityId,
|
|
432
471
|
position: position,
|
|
433
|
-
size: size
|
|
472
|
+
size: size,
|
|
473
|
+
webViewModel: webViewModel
|
|
434
474
|
)
|
|
435
475
|
|
|
436
|
-
|
|
476
|
+
resolve(.success(baseReplyData))
|
|
437
477
|
}
|
|
438
478
|
|
|
439
479
|
private func onPageStartLoad() {
|
|
@@ -442,7 +482,6 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
442
482
|
for spatialObject in spatialObjectArray {
|
|
443
483
|
spatialObject.destroy()
|
|
444
484
|
}
|
|
445
|
-
// destroy all attachments
|
|
446
485
|
attachmentManager.destroyAll()
|
|
447
486
|
backgroundMaterial = .None
|
|
448
487
|
}
|
|
@@ -715,6 +754,10 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
715
754
|
spatializedElement.enableRotateEndGesture = enableRotateEndGesture
|
|
716
755
|
}
|
|
717
756
|
|
|
757
|
+
if let rotateConstrainedToAxis = command.rotateConstrainedToAxis {
|
|
758
|
+
spatializedElement.rotateConstrainedToAxis = rotateConstrainedToAxis
|
|
759
|
+
}
|
|
760
|
+
|
|
718
761
|
if let enableMagnifyGesture = command.enableMagnifyGesture {
|
|
719
762
|
spatializedElement.enableMagnifyGesture = enableMagnifyGesture
|
|
720
763
|
}
|
|
@@ -1068,26 +1111,141 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
1068
1111
|
resolve(.success(ConvertReply(id: command.entityId, position: point)))
|
|
1069
1112
|
}
|
|
1070
1113
|
|
|
1114
|
+
/// Input: command.position, command.fromId, command.toId
|
|
1115
|
+
/// fromId/toId can reference either the scene (window) or an entity.
|
|
1116
|
+
/// Step 1: Convert position to window coordinates (view global, px)
|
|
1117
|
+
/// - If from is window (scene), position is already in view global (px). Go to Step 2.
|
|
1118
|
+
/// - If from is 2d frame(SpatializedElement), position is in view local (px).
|
|
1119
|
+
/// - view local → window (view global, px) using SpatializedElement.convertToScene
|
|
1120
|
+
/// - If from is an entity, position is in reality entity local (meters):
|
|
1121
|
+
/// - entity local → reality world (scene)
|
|
1122
|
+
/// - reality world → window (view global, px)
|
|
1123
|
+
/// Step 2: Convert window coordinates (view global, px) to target output
|
|
1124
|
+
/// - If to is window, output directly.
|
|
1125
|
+
/// - If to is 2d frame(SpatializedElement), output in view local (px).
|
|
1126
|
+
/// - window (view global, px) → view local using SpatializedElement.convertFromScene
|
|
1127
|
+
/// - If to is an entity, output in reality entity local (meters):
|
|
1128
|
+
/// - window (view global, px) → reality world (scene)
|
|
1129
|
+
/// - reality world → reality entity local (meters)
|
|
1130
|
+
|
|
1131
|
+
private func onConvertCoordinate(command: ConvertCoordinate, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1132
|
+
func isSceneId(_ id: String) -> Bool {
|
|
1133
|
+
return id.isEmpty
|
|
1134
|
+
}
|
|
1135
|
+
let input = SIMD3<Float>(Float(command.position.x), Float(command.position.y), Float(command.position.z))
|
|
1136
|
+
let fromEntity = spatialObjects[command.fromId] as? SpatialEntity
|
|
1137
|
+
let from2dFrame = spatialObjects[command.fromId] as? SpatializedElement
|
|
1138
|
+
let toEntity = spatialObjects[command.toId] as? SpatialEntity
|
|
1139
|
+
let to2dFrame = spatialObjects[command.toId] as? SpatializedElement
|
|
1140
|
+
|
|
1141
|
+
var globalPx: Point3D
|
|
1142
|
+
if isSceneId(command.fromId) {
|
|
1143
|
+
globalPx = Point3D(x: Double(input.x), y: Double(input.y), z: Double(input.z))
|
|
1144
|
+
} else if let fromEntity {
|
|
1145
|
+
let world = fromEntity.convert(position: input, to: nil)
|
|
1146
|
+
guard let content = findSpatializedDynamic3DElement(containingEntityId: fromEntity.spatialId)?.getViewContent() else {
|
|
1147
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "RealityView content unavailable for conversion")))
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1150
|
+
globalPx = content.convert(point: world, from: .scene, to: .global)
|
|
1151
|
+
} else if let from2dFrame {
|
|
1152
|
+
let localPoint = SIMD3<Double>(Double(input.x), Double(input.y), Double(input.z))
|
|
1153
|
+
let scenePoint = from2dFrame.convertToScene(localPoint)
|
|
1154
|
+
globalPx = Point3D(x: scenePoint.x, y: scenePoint.y, z: scenePoint.z)
|
|
1155
|
+
} else {
|
|
1156
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Invalid fromId")))
|
|
1157
|
+
return
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if isSceneId(command.toId) {
|
|
1161
|
+
let result = Vec3(x: CGFloat(globalPx.x), y: CGFloat(globalPx.y), z: CGFloat(globalPx.z))
|
|
1162
|
+
resolve(.success(result))
|
|
1163
|
+
return
|
|
1164
|
+
} else if let toEntity {
|
|
1165
|
+
guard let content = findSpatializedDynamic3DElement(containingEntityId: toEntity.spatialId)?.getViewContent() else {
|
|
1166
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "RealityView content unavailable for conversion")))
|
|
1167
|
+
return
|
|
1168
|
+
}
|
|
1169
|
+
let world = content.convert(globalPx, from: .global, to: .scene)
|
|
1170
|
+
let local = toEntity.convert(position: world, from: nil)
|
|
1171
|
+
let ret = Vec3(x: CGFloat(local.x), y: CGFloat(local.y), z: CGFloat(local.z))
|
|
1172
|
+
resolve(.success(ret))
|
|
1173
|
+
return
|
|
1174
|
+
} else if let to2dFrame {
|
|
1175
|
+
let scenePoint = SIMD3<Double>(globalPx.x, globalPx.y, globalPx.z)
|
|
1176
|
+
let localPoint = to2dFrame.convertFromScene(scenePoint)
|
|
1177
|
+
let ret = Vec3(x: CGFloat(localPoint.x), y: CGFloat(localPoint.y), z: CGFloat(localPoint.z))
|
|
1178
|
+
resolve(.success(ret))
|
|
1179
|
+
return
|
|
1180
|
+
} else {
|
|
1181
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Invalid toId")))
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1071
1186
|
private func onUpdateAttachmentEntity(command: UpdateAttachmentEntityCommand, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1072
1187
|
guard attachmentManager.get(id: command.id) != nil else {
|
|
1073
1188
|
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Attachment \(command.id) not found")))
|
|
1074
1189
|
return
|
|
1075
1190
|
}
|
|
1076
|
-
|
|
1077
1191
|
var newPosition: SIMD3<Float>? = nil
|
|
1078
1192
|
if let posArray = command.position, posArray.count >= 3 {
|
|
1079
1193
|
newPosition = SIMD3<Float>(posArray[0], posArray[1], posArray[2])
|
|
1080
1194
|
}
|
|
1081
|
-
|
|
1082
1195
|
var newSize: CGSize? = nil
|
|
1083
1196
|
if let sizeObj = command.size {
|
|
1084
1197
|
newSize = CGSize(width: sizeObj.width, height: sizeObj.height)
|
|
1085
1198
|
}
|
|
1086
|
-
|
|
1087
1199
|
attachmentManager.update(id: command.id, position: newPosition, size: newSize)
|
|
1088
1200
|
resolve(.success(baseReplyData))
|
|
1089
1201
|
}
|
|
1090
1202
|
|
|
1203
|
+
private func onUpdateUnlitMaterialProperties(command: UpdateUnlitMaterialProperties, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1204
|
+
guard let material = spatialObjects[command.id] as? SpatialUnlitMaterial else {
|
|
1205
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Material \(command.id) not found")))
|
|
1206
|
+
return
|
|
1207
|
+
}
|
|
1208
|
+
material.updateProperties(color: command.color, transparent: command.transparent, opacity: command.opacity)
|
|
1209
|
+
// Re-apply material to any ModelComponent or ModelEntity override that references it
|
|
1210
|
+
for (_, obj) in spatialObjects {
|
|
1211
|
+
if let comp = obj as? SpatialModelComponent, comp.usesMaterial(command.id) {
|
|
1212
|
+
comp.refreshMaterials()
|
|
1213
|
+
} else if let modelEntity = obj as? SpatialModelEntity, modelEntity.usesMaterial(command.id) {
|
|
1214
|
+
modelEntity.refreshMaterials()
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
resolve(.success(baseReplyData))
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private func onRemoveComponentFromEntity(command: RemoveComponentFromEntity, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1221
|
+
guard let entity = spatialObjects[command.entityId] as? SpatialEntity,
|
|
1222
|
+
let component = spatialObjects[command.componentId] as? SpatialComponent
|
|
1223
|
+
else {
|
|
1224
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Remove component failed")))
|
|
1225
|
+
return
|
|
1226
|
+
}
|
|
1227
|
+
entity.removeComponent(component)
|
|
1228
|
+
resolve(.success(baseReplyData))
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
private func onSetMaterialsOnEntity(command: SetMaterialsOnEntity, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1232
|
+
guard let entity = spatialObjects[command.entityId] as? SpatialModelEntity else {
|
|
1233
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "ModelEntity \(command.entityId) not found")))
|
|
1234
|
+
return
|
|
1235
|
+
}
|
|
1236
|
+
var materials: [SpatialMaterial] = []
|
|
1237
|
+
for mid in command.materialIds {
|
|
1238
|
+
if let material = spatialObjects[mid] as? SpatialMaterial {
|
|
1239
|
+
materials.append(material)
|
|
1240
|
+
} else {
|
|
1241
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Material \(mid) not found")))
|
|
1242
|
+
return
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
entity.setMaterials(materials)
|
|
1246
|
+
resolve(.success(baseReplyData))
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1091
1249
|
private func addSpatialObject(_ object: any SpatialObjectProtocol) {
|
|
1092
1250
|
var spatialObject = object
|
|
1093
1251
|
spatialObjects[spatialObject.spatialId] = spatialObject
|
|
@@ -1111,6 +1269,25 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
1111
1269
|
sendWebMsg(spatialObject.spatialId, SpatialObjectDestroiedEvent())
|
|
1112
1270
|
}
|
|
1113
1271
|
|
|
1272
|
+
/// Find the dynamic 3D container (SpatializedDynamic3DElement) that contains the entity by ID.
|
|
1273
|
+
/// - Parameter entityId: The entity's spatialId.
|
|
1274
|
+
/// - Returns: The container if the entity is a descendant of the container's root; otherwise nil.
|
|
1275
|
+
private func findSpatializedDynamic3DElement(containingEntityId entityId: String) -> SpatializedDynamic3DElement? {
|
|
1276
|
+
guard let entity = spatialObjects[entityId] as? SpatialEntity else {
|
|
1277
|
+
return nil
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
var current: Entity? = entity
|
|
1281
|
+
while let node = current {
|
|
1282
|
+
if let rootEntity = node as? SpatialRootEntity {
|
|
1283
|
+
return rootEntity.root
|
|
1284
|
+
}
|
|
1285
|
+
current = node.parent
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
return nil
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1114
1291
|
func findSpatialObject<T: SpatialObjectProtocol>(_ id: String) -> T? {
|
|
1115
1292
|
return spatialObjects[id] as? T
|
|
1116
1293
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import _RealityKit_SwiftUI
|
|
1
2
|
import Foundation
|
|
3
|
+
import RealityKit
|
|
2
4
|
|
|
3
5
|
@Observable
|
|
4
6
|
class SpatializedDynamic3DElement: SpatializedElement {
|
|
5
|
-
private var rootEntity =
|
|
7
|
+
private var rootEntity = SpatialRootEntity()
|
|
8
|
+
private var viewContent: RealityViewContent? = nil
|
|
9
|
+
|
|
10
|
+
override init() {
|
|
11
|
+
super.init()
|
|
12
|
+
rootEntity.root = self
|
|
13
|
+
}
|
|
6
14
|
|
|
7
15
|
func getRoot() -> SpatialEntity {
|
|
8
16
|
return rootEntity
|
|
@@ -16,6 +24,14 @@ class SpatializedDynamic3DElement: SpatializedElement {
|
|
|
16
24
|
rootEntity.removeChild(entity)
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
func getViewContent() -> RealityViewContent? {
|
|
28
|
+
return viewContent
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func setViewContent(_ content: RealityViewContent?) {
|
|
32
|
+
viewContent = content
|
|
33
|
+
}
|
|
34
|
+
|
|
19
35
|
enum CodingKeys: String, CodingKey {
|
|
20
36
|
case type, root
|
|
21
37
|
}
|
|
@@ -28,6 +44,7 @@ class SpatializedDynamic3DElement: SpatializedElement {
|
|
|
28
44
|
}
|
|
29
45
|
|
|
30
46
|
override func onDestroy() {
|
|
47
|
+
viewContent = nil
|
|
31
48
|
rootEntity.destroy()
|
|
32
49
|
super.onDestroy()
|
|
33
50
|
}
|
|
@@ -2,6 +2,9 @@ import Foundation
|
|
|
2
2
|
import RealityKit
|
|
3
3
|
import SwiftUI
|
|
4
4
|
|
|
5
|
+
/// zIndex() have some bug, so use zOrderBias to simulate zIndex effect
|
|
6
|
+
let zOrderBias = 0.001
|
|
7
|
+
|
|
5
8
|
enum SpatializedElementType: String, Codable {
|
|
6
9
|
case Spatialized2DElement
|
|
7
10
|
case SpatializedStatic3DElement
|
|
@@ -32,8 +35,45 @@ class SpatializedElement: SpatialObject {
|
|
|
32
35
|
var enableMagnifyEndGesture: Bool = false
|
|
33
36
|
var enableTapGesture: Bool = false
|
|
34
37
|
|
|
38
|
+
/// When non-nil and non-zero length, rotate gesture is constrained to this axis (world space).
|
|
39
|
+
var rotateConstrainedToAxis: Vec3?
|
|
40
|
+
|
|
35
41
|
var defaultAlignment: DepthAlignment = .back
|
|
36
42
|
|
|
43
|
+
/// Raw layout→scene transform from onGeometryChange3D proxy.
|
|
44
|
+
/// Does NOT include backOffset or zIndex offset.
|
|
45
|
+
/// Updated by SpatializedElementView whenever layout changes.
|
|
46
|
+
var proxySceneTransform: AffineTransform3D = .identity
|
|
47
|
+
|
|
48
|
+
/// Full local→scene transform accounting for --xr-back and zIndex.
|
|
49
|
+
/// Computed on-the-fly so backOffset/zIndex changes are always reflected.
|
|
50
|
+
var sceneTransform: AffineTransform3D {
|
|
51
|
+
let frameZ = (zIndex * zOrderBias) + backOffset
|
|
52
|
+
let localZ = AffineTransform3D(translation: Vector3D(x: 0, y: 0, z: frameZ))
|
|
53
|
+
return proxySceneTransform.concatenating(localZ)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Converts a point from this element's local coordinate system to scene space.
|
|
57
|
+
func convertToScene(_ localPoint: SIMD3<Double>) -> SIMD3<Double> {
|
|
58
|
+
let p = SIMD4<Double>(localPoint.x, localPoint.y, localPoint.z, 1.0)
|
|
59
|
+
let scene = sceneTransform.matrix * p
|
|
60
|
+
return SIMD3<Double>(scene.x, scene.y, scene.z)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Converts a point from scene space to this element's local coordinate system.
|
|
64
|
+
func convertFromScene(_ scenePoint: SIMD3<Double>) -> SIMD3<Double> {
|
|
65
|
+
let inv = sceneTransform.inverse!
|
|
66
|
+
let p = SIMD4<Double>(scenePoint.x, scenePoint.y, scenePoint.z, 1.0)
|
|
67
|
+
let local = inv.matrix * p
|
|
68
|
+
return SIMD3<Double>(local.x, local.y, local.z)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Converts a point from this element's local space to another element's local space.
|
|
72
|
+
func convert(_ localPoint: SIMD3<Double>, to target: SpatializedElement) -> SIMD3<Double> {
|
|
73
|
+
let scenePoint = convertToScene(localPoint)
|
|
74
|
+
return target.convertFromScene(scenePoint)
|
|
75
|
+
}
|
|
76
|
+
|
|
37
77
|
var enableGesture: Bool {
|
|
38
78
|
return enableDragStartGesture || enableDragGesture || enableDragEndGesture || enableRotateGesture || enableRotateEndGesture || enableMagnifyGesture || enableMagnifyEndGesture || enableTapGesture
|
|
39
79
|
}
|
|
@@ -43,7 +43,12 @@ class SpatialComponent: SpatialObject {
|
|
|
43
43
|
|
|
44
44
|
@Observable
|
|
45
45
|
class SpatialModelComponent: SpatialComponent {
|
|
46
|
+
private(set) var spatialMaterials: [SpatialMaterial] = []
|
|
47
|
+
private(set) var mesh: Geometry?
|
|
48
|
+
|
|
46
49
|
init(mesh: Geometry, mats: [SpatialMaterial]) {
|
|
50
|
+
spatialMaterials = mats
|
|
51
|
+
self.mesh = mesh
|
|
47
52
|
super.init(.ModelComponent)
|
|
48
53
|
var materials: [any RealityKit.Material] = []
|
|
49
54
|
for item in mats {
|
|
@@ -52,6 +57,27 @@ class SpatialModelComponent: SpatialComponent {
|
|
|
52
57
|
_resource = ModelComponent(mesh: mesh.resource!, materials: materials)
|
|
53
58
|
}
|
|
54
59
|
|
|
60
|
+
/// Rebuild the ModelComponent with current material resources (called after material properties change)
|
|
61
|
+
func refreshMaterials() {
|
|
62
|
+
guard let mesh = mesh else { return }
|
|
63
|
+
var materials: [any RealityKit.Material] = []
|
|
64
|
+
for item in spatialMaterials {
|
|
65
|
+
if let res = item.resource {
|
|
66
|
+
materials.append(res)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
_resource = ModelComponent(mesh: mesh.resource!, materials: materials)
|
|
70
|
+
if let entity = _entity {
|
|
71
|
+
entity.components.set(_resource!)
|
|
72
|
+
entity.generateCollisionShapes(recursive: true)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Check if this component uses the given material
|
|
77
|
+
func usesMaterial(_ materialId: String) -> Bool {
|
|
78
|
+
return spatialMaterials.contains { $0.id == materialId }
|
|
79
|
+
}
|
|
80
|
+
|
|
55
81
|
override func addToEntity(entity: SpatialEntity) {
|
|
56
82
|
super.addToEntity(entity: entity)
|
|
57
83
|
entity.generateCollisionShapes(recursive: true)
|
|
@@ -63,7 +89,12 @@ class SpatialModelComponent: SpatialComponent {
|
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
override func onDestroy() {
|
|
92
|
+
// TODO(P2): `mesh` is a registered `Geometry` spatial object; clearing the reference does not
|
|
93
|
+
// run `Geometry.destroy()`, so dynamic mesh rebuilds can leak mesh/registry entries until the
|
|
94
|
+
// scene ends. Call `mesh?.destroy()` (or equivalent) before nil-ing when ownership is exclusive.
|
|
66
95
|
_resource = nil
|
|
96
|
+
spatialMaterials = []
|
|
97
|
+
mesh = nil
|
|
67
98
|
}
|
|
68
99
|
}
|
|
69
100
|
|
|
@@ -138,10 +138,16 @@ class SpatialEntity: Entity, SpatialObjectProtocol {
|
|
|
138
138
|
if !components.has(InputTargetComponent.self) {
|
|
139
139
|
components.set(InputTargetComponent())
|
|
140
140
|
}
|
|
141
|
+
if !components.has(HoverEffectComponent.self) {
|
|
142
|
+
components.set(HoverEffectComponent())
|
|
143
|
+
}
|
|
141
144
|
} else {
|
|
142
145
|
if components.has(InputTargetComponent.self) {
|
|
143
146
|
components.remove(InputTargetComponent.self)
|
|
144
147
|
}
|
|
148
|
+
if components.has(HoverEffectComponent.self) {
|
|
149
|
+
components.remove(HoverEffectComponent.self)
|
|
150
|
+
}
|
|
145
151
|
}
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -22,16 +22,38 @@ class SpatialMaterial: SpatialObject {
|
|
|
22
22
|
|
|
23
23
|
@Observable
|
|
24
24
|
class SpatialUnlitMaterial: SpatialMaterial {
|
|
25
|
-
|
|
25
|
+
private(set) var currentColor: UIColor
|
|
26
|
+
private(set) var currentTexture: TextureResource?
|
|
27
|
+
private(set) var currentTransparent: Bool
|
|
28
|
+
private(set) var currentOpacity: Float
|
|
26
29
|
|
|
27
30
|
init(_ color: String, _ texture: TextureResource? = nil, _ transparent: Bool = true, _ opacity: Float = 1) {
|
|
28
|
-
|
|
31
|
+
currentColor = UIColor(Color(hex: color))
|
|
32
|
+
currentTexture = texture
|
|
33
|
+
currentTransparent = transparent
|
|
34
|
+
currentOpacity = opacity
|
|
29
35
|
super.init(.UnlitMaterial)
|
|
30
36
|
var mat = UnlitMaterial()
|
|
31
|
-
mat.color = .init(tint:
|
|
37
|
+
mat.color = .init(tint: currentColor, texture: texture != nil ? .init(texture!) : nil)
|
|
32
38
|
mat.blending = transparent ? .transparent(opacity: .init(scale: opacity)) : .opaque
|
|
33
39
|
_resource = mat
|
|
34
40
|
}
|
|
41
|
+
|
|
42
|
+
func updateProperties(color: String?, transparent: Bool?, opacity: Float?) {
|
|
43
|
+
if let color = color {
|
|
44
|
+
currentColor = UIColor(Color(hex: color))
|
|
45
|
+
}
|
|
46
|
+
if let transparent = transparent {
|
|
47
|
+
currentTransparent = transparent
|
|
48
|
+
}
|
|
49
|
+
if let opacity = opacity {
|
|
50
|
+
currentOpacity = opacity
|
|
51
|
+
}
|
|
52
|
+
var mat = UnlitMaterial()
|
|
53
|
+
mat.color = .init(tint: currentColor, texture: currentTexture != nil ? .init(currentTexture!) : nil)
|
|
54
|
+
mat.blending = currentTransparent ? .transparent(opacity: .init(scale: currentOpacity)) : .opaque
|
|
55
|
+
_resource = mat
|
|
56
|
+
}
|
|
35
57
|
}
|
|
36
58
|
|
|
37
59
|
enum SpatialMaterialType: String {
|
|
@@ -4,6 +4,9 @@ import SwiftUI
|
|
|
4
4
|
@Observable
|
|
5
5
|
class SpatialModelEntity: SpatialEntity {
|
|
6
6
|
private var modelEntity: Entity?
|
|
7
|
+
/// Retained so `UpdateUnlitMaterialProperties` can re-apply current `SpatialMaterial.resource` after native material updates.
|
|
8
|
+
private(set) var overrideSpatialMaterials: [SpatialMaterial] = []
|
|
9
|
+
|
|
7
10
|
required init(_ modelResource: SpatialModelResource, _ _name: String = "") {
|
|
8
11
|
super.init(_name)
|
|
9
12
|
modelEntity = modelResource.resource
|
|
@@ -15,12 +18,44 @@ class SpatialModelEntity: SpatialEntity {
|
|
|
15
18
|
super.init()
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
func setMaterials(_ materials: [SpatialMaterial]) {
|
|
22
|
+
overrideSpatialMaterials = materials
|
|
23
|
+
applyOverrideMaterials()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Re-apply stored override materials using each `SpatialMaterial`'s current `resource` (e.g. after unlit property updates).
|
|
27
|
+
func refreshMaterials() {
|
|
28
|
+
applyOverrideMaterials()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func usesMaterial(_ materialId: String) -> Bool {
|
|
32
|
+
overrideSpatialMaterials.contains { $0.id == materialId }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private func applyOverrideMaterials() {
|
|
36
|
+
guard let modelEntity = modelEntity else { return }
|
|
37
|
+
// TODO(P1): Clearing overrides (`setMaterials([])`) assigns an empty material list here; there is
|
|
38
|
+
// no baseline of the model asset’s authored materials to restore. Persist per-component defaults
|
|
39
|
+
// at load (or skip writing when overrides are empty) so clears return to the authored look.
|
|
40
|
+
func applyMaterials(to entity: Entity) {
|
|
41
|
+
if var modelComp = entity.components[ModelComponent.self] {
|
|
42
|
+
modelComp.materials = overrideSpatialMaterials.compactMap { $0.resource }
|
|
43
|
+
entity.components.set(modelComp)
|
|
44
|
+
}
|
|
45
|
+
for child in entity.children {
|
|
46
|
+
applyMaterials(to: child)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
applyMaterials(to: modelEntity)
|
|
50
|
+
}
|
|
51
|
+
|
|
18
52
|
override func onDestroy() {
|
|
19
53
|
super.onDestroy()
|
|
20
54
|
if let modelEntity = modelEntity {
|
|
21
55
|
removeChild(modelEntity)
|
|
22
56
|
}
|
|
23
57
|
modelEntity = nil
|
|
58
|
+
overrideSpatialMaterials = []
|
|
24
59
|
}
|
|
25
60
|
|
|
26
61
|
enum CodingKeys: String, CodingKey {
|
|
@@ -10,6 +10,9 @@ struct SceneHandlerUIView: View {
|
|
|
10
10
|
@State var spatialScene: SpatialScene
|
|
11
11
|
|
|
12
12
|
@Environment(\.scenePhase) private var scenePhase
|
|
13
|
+
@Environment(\.physicalMetrics) private var converter
|
|
14
|
+
@State private var latestScaled: Double?
|
|
15
|
+
@State private var latestUnscaled: Double?
|
|
13
16
|
|
|
14
17
|
private func setResizibility(resizingRestrictions: UIWindowScene.ResizingRestrictions) {
|
|
15
18
|
sceneDelegate.window?.windowScene?
|
|
@@ -41,10 +44,35 @@ struct SceneHandlerUIView: View {
|
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
private func updatePhysicalMetricsIfReady() {
|
|
48
|
+
if let scaled = latestScaled, let unscaled = latestUnscaled {
|
|
49
|
+
spatialScene.onUpdatePhysicalMetrics(meterToPtUnscaled: unscaled, meterToPtScaled: scaled)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
var body: some View {
|
|
54
|
+
let meterToPtScaled = converter.worldScalingCompensation(.scaled).convert(
|
|
55
|
+
1,
|
|
56
|
+
from: .meters
|
|
57
|
+
)
|
|
58
|
+
let meterToPtUnscaled = converter.worldScalingCompensation(.unscaled).convert(
|
|
59
|
+
1,
|
|
60
|
+
from: .meters
|
|
61
|
+
)
|
|
45
62
|
VStack {}
|
|
46
63
|
.onAppear {
|
|
47
|
-
|
|
64
|
+
latestScaled = meterToPtScaled
|
|
65
|
+
latestUnscaled = meterToPtUnscaled
|
|
66
|
+
updatePhysicalMetricsIfReady()
|
|
67
|
+
}
|
|
68
|
+
.onChange(of: meterToPtScaled) { _, newValue in
|
|
69
|
+
latestScaled = newValue
|
|
70
|
+
updatePhysicalMetricsIfReady()
|
|
71
|
+
}
|
|
72
|
+
.onChange(of: meterToPtUnscaled) { _, newValue in
|
|
73
|
+
latestUnscaled = newValue
|
|
74
|
+
updatePhysicalMetricsIfReady()
|
|
75
|
+
}.onAppear {
|
|
48
76
|
guard spatialScene.windowStyle == .window else {
|
|
49
77
|
return
|
|
50
78
|
}
|
|
@@ -37,7 +37,7 @@ struct SpatializedDynamic3DView: View {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
var rotate3dEvent: some Gesture {
|
|
40
|
-
|
|
40
|
+
makeRotateGesture3D().targetedToAnyEntity().onChanged { value in
|
|
41
41
|
// Always forward rotate gesture events to JS
|
|
42
42
|
if let entity = value.entity as? SpatialEntity {
|
|
43
43
|
let gestureEvent = WebSpatialRotateGuestureEvent(
|
|
@@ -62,6 +62,21 @@ struct SpatializedDynamic3DView: View {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
private func makeRotateGesture3D() -> RotateGesture3D {
|
|
66
|
+
guard let raw = spatializedElement.rotateConstrainedToAxis else {
|
|
67
|
+
return RotateGesture3D()
|
|
68
|
+
}
|
|
69
|
+
let dx = Double(raw.x)
|
|
70
|
+
let dy = Double(raw.y)
|
|
71
|
+
let dz = Double(raw.z)
|
|
72
|
+
let len = (dx * dx + dy * dy + dz * dz).squareRoot()
|
|
73
|
+
if len < 1e-9 {
|
|
74
|
+
return RotateGesture3D()
|
|
75
|
+
}
|
|
76
|
+
let axis = RotationAxis3D(x: dx / len, y: dy / len, z: dz / len)
|
|
77
|
+
return RotateGesture3D(constrainedToAxis: axis)
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
var magnifyEvent: some Gesture {
|
|
66
81
|
MagnifyGesture().targetedToAnyEntity().onChanged { value in
|
|
67
82
|
// Always forward magnify gesture events to JS
|
|
@@ -122,6 +137,7 @@ struct SpatializedDynamic3DView: View {
|
|
|
122
137
|
RealityView(make: { content, attachments in
|
|
123
138
|
let rootEntity = spatializedDynamic3DElement.getRoot()
|
|
124
139
|
content.add(rootEntity)
|
|
140
|
+
spatializedDynamic3DElement.setViewContent(content)
|
|
125
141
|
|
|
126
142
|
// Add existing attachments on initial creation
|
|
127
143
|
for (_, info) in spatialScene.attachmentManager.attachments {
|
|
@@ -168,6 +184,9 @@ struct SpatializedDynamic3DView: View {
|
|
|
168
184
|
.simultaneousGesture(rotate3dEvent)
|
|
169
185
|
.simultaneousGesture(dragEvent)
|
|
170
186
|
.simultaneousGesture(magnifyEvent)
|
|
187
|
+
.onDisappear {
|
|
188
|
+
spatializedDynamic3DElement.setViewContent(nil)
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
191
|
|
|
173
192
|
private func findSpatialEntity(_ spatialId: String) -> SpatialEntity? {
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
import CoreGraphics
|
|
2
2
|
import SwiftUI
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
let zOrderBias = 0.001
|
|
6
|
-
|
|
7
|
-
final class GestureFlags {
|
|
4
|
+
final class GestureState {
|
|
8
5
|
var isDrag = false
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
final class TransformHolder {
|
|
12
|
-
var transform: AffineTransform3D = .identity
|
|
6
|
+
var proxyTransform: AffineTransform3D = .identity
|
|
13
7
|
}
|
|
14
8
|
|
|
15
9
|
struct SpatializedElementView<Content: View>: View {
|
|
@@ -19,9 +13,7 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
19
13
|
var parentScrollOffset: Vec2
|
|
20
14
|
var content: Content
|
|
21
15
|
|
|
22
|
-
@State private var
|
|
23
|
-
@State private var transformHolder = TransformHolder()
|
|
24
|
-
@State private var currentTransform: AffineTransform3D = .identity
|
|
16
|
+
@State private var gestureState = GestureState()
|
|
25
17
|
|
|
26
18
|
init(parentScrollOffset: Vec2, @ViewBuilder content: () -> Content) {
|
|
27
19
|
self.parentScrollOffset = parentScrollOffset
|
|
@@ -34,7 +26,7 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
34
26
|
.onChanged(onDragging)
|
|
35
27
|
.onEnded(onDraggingEnded)
|
|
36
28
|
.simultaneously(with:
|
|
37
|
-
|
|
29
|
+
makeRotateGesture3D()
|
|
38
30
|
.onChanged(onRotateGesture3D)
|
|
39
31
|
.onEnded(onRotateGesture3DEnd))
|
|
40
32
|
.simultaneously(with:
|
|
@@ -46,6 +38,21 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
46
38
|
.onEnded(onTapEnded))
|
|
47
39
|
}
|
|
48
40
|
|
|
41
|
+
private func makeRotateGesture3D() -> RotateGesture3D {
|
|
42
|
+
guard let raw = spatializedElement.rotateConstrainedToAxis else {
|
|
43
|
+
return RotateGesture3D()
|
|
44
|
+
}
|
|
45
|
+
let dx = Double(raw.x)
|
|
46
|
+
let dy = Double(raw.y)
|
|
47
|
+
let dz = Double(raw.z)
|
|
48
|
+
let len = (dx * dx + dy * dy + dz * dz).squareRoot()
|
|
49
|
+
if len < 1e-9 {
|
|
50
|
+
return RotateGesture3D()
|
|
51
|
+
}
|
|
52
|
+
let axis = RotationAxis3D(x: dx / len, y: dy / len, z: dz / len)
|
|
53
|
+
return RotateGesture3D(constrainedToAxis: axis)
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
private func onRotateGesture3D(_ event: RotateGesture3D.Value) {
|
|
50
57
|
if spatializedElement.enableRotateGesture {
|
|
51
58
|
let quaternion = event.rotation.quaternion
|
|
@@ -67,16 +74,18 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
private func onDragging(_ event: DragGesture.Value) {
|
|
70
|
-
if spatializedElement.enableDragStartGesture, !
|
|
71
|
-
let
|
|
72
|
-
let
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
if spatializedElement.enableDragStartGesture, !gestureState.isDrag {
|
|
78
|
+
let frameZ = localFrameOffsetZ()
|
|
79
|
+
let startLocal = Point3D(
|
|
80
|
+
x: event.startLocation3D.x,
|
|
81
|
+
y: event.startLocation3D.y,
|
|
82
|
+
z: event.startLocation3D.z - frameZ
|
|
83
|
+
)
|
|
84
|
+
let globalPoint3D = localToScene(event.startLocation3D)
|
|
75
85
|
let gestureEvent = WebSpatialDragStartGuestureEvent(detail: .init(
|
|
76
|
-
startLocation3D:
|
|
86
|
+
startLocation3D: startLocal,
|
|
77
87
|
globalLocation3D: globalPoint3D
|
|
78
88
|
))
|
|
79
|
-
|
|
80
89
|
spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
|
|
81
90
|
}
|
|
82
91
|
|
|
@@ -88,24 +97,40 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
88
97
|
spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
gestureState.isDrag = true
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
private func onDraggingEnded(_ event: DragGesture.Value) {
|
|
95
|
-
|
|
104
|
+
gestureState.isDrag = false
|
|
96
105
|
if spatializedElement.enableDragEndGesture {
|
|
97
106
|
let gestureEvent = WebSpatialDragEndGuestureEvent()
|
|
98
107
|
spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
|
|
99
108
|
}
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
/// Z offset that defines the local coordinate system (front face = z=0).
|
|
112
|
+
/// Only backOffset and zIndex*bias; excludes translation.z (CSS translateZ is visual-only).
|
|
113
|
+
private func localFrameOffsetZ() -> Double {
|
|
114
|
+
(spatializedElement.zIndex * zOrderBias) + spatializedElement.backOffset
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Maps a point in the gesture's local coordinate system to SpatialScene.
|
|
118
|
+
private func localToScene(_ localPoint: Point3D) -> Point3D {
|
|
119
|
+
let p = SIMD4<Double>(localPoint.x, localPoint.y, localPoint.z, 1.0)
|
|
120
|
+
let scene = gestureState.proxyTransform.matrix * p
|
|
121
|
+
return Point3D(x: scene.x, y: scene.y, z: scene.z)
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
private func onTapEnded(_ event: SpatialTapGesture.Value) {
|
|
103
125
|
if spatializedElement.enableTapGesture {
|
|
104
|
-
let
|
|
105
|
-
let
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
let frameZ = localFrameOffsetZ()
|
|
127
|
+
let localPoint3D = Point3D(
|
|
128
|
+
x: event.location3D.x,
|
|
129
|
+
y: event.location3D.y,
|
|
130
|
+
z: event.location3D.z - frameZ
|
|
131
|
+
)
|
|
132
|
+
let globalPoint3D = localToScene(event.location3D)
|
|
133
|
+
spatialScene.sendWebMsg(spatializedElement.id, WebSpatialTapGuestureEvent(detail: .init(location3D: localPoint3D, globalLocation3D: globalPoint3D)))
|
|
109
134
|
}
|
|
110
135
|
}
|
|
111
136
|
|
|
@@ -130,9 +155,6 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
130
155
|
|
|
131
156
|
var body: some View {
|
|
132
157
|
let transform = spatializedElement.transform
|
|
133
|
-
let translation = transform.translation
|
|
134
|
-
let scale = transform.scale
|
|
135
|
-
let rotation = transform.rotation!
|
|
136
158
|
|
|
137
159
|
let width = spatializedElement.width
|
|
138
160
|
let height = spatializedElement.height
|
|
@@ -146,8 +168,17 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
146
168
|
let visible = spatializedElement.visible
|
|
147
169
|
let enableGesture = spatializedElement.enableGesture
|
|
148
170
|
|
|
149
|
-
let
|
|
150
|
-
let smallOffset =
|
|
171
|
+
let frameOffsetZ = localFrameOffsetZ()
|
|
172
|
+
let smallOffset = abs(frameOffsetZ) < 0.0001 ? 0.0001 : 0
|
|
173
|
+
|
|
174
|
+
// Wrap CSS transform with anchor (CSS transform-origin) since
|
|
175
|
+
// transform3DEffect does not support anchor. Preserves the original
|
|
176
|
+
// CSS transform order (e.g. rotateX(90deg) translateZ(100px)).
|
|
177
|
+
let ax = width * anchor.x
|
|
178
|
+
let ay = height * anchor.y
|
|
179
|
+
let toAnchor = AffineTransform3D(translation: Vector3D(x: -ax, y: -ay, z: 0))
|
|
180
|
+
let fromAnchor = AffineTransform3D(translation: Vector3D(x: ax, y: ay, z: 0))
|
|
181
|
+
let anchoredTransform = fromAnchor.concatenating(transform).concatenating(toAnchor)
|
|
151
182
|
|
|
152
183
|
// when spatialdiv have regular/thick/thin material and alignment is back, there'll be a bug that clipping content
|
|
153
184
|
// so when spatializedElement is spatialdiv, .center alignment will be applied
|
|
@@ -156,34 +187,26 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
156
187
|
content
|
|
157
188
|
.frame(width: width, height: height)
|
|
158
189
|
.frame(depth: depth, alignment: alignment)
|
|
159
|
-
.onGeometryChange3D(for: AffineTransform3D.self) { proxy in
|
|
160
|
-
let rect3d = proxy.frame(in: .named("SpatialScene"))
|
|
161
|
-
spatialScene.sendWebMsg(spatializedElement.id, SpatiaizedContainerClientCube(origin: rect3d.origin, size: rect3d.size))
|
|
162
|
-
let transform = proxy.transform(in: .named("SpatialScene"))!
|
|
163
|
-
transformHolder.transform = transform
|
|
164
|
-
return transform
|
|
165
|
-
} action: { _, new in
|
|
166
|
-
spatialScene.sendWebMsg(spatializedElement.id, SpatiaizedContainerTransform(detail: new))
|
|
167
|
-
}
|
|
168
190
|
.frame(depth: 0, alignment: .back)
|
|
169
191
|
// use .offset(smallVal) to workaround for glassEffect not working and small width/height spatialDiv not working
|
|
170
192
|
.offset(z: smallOffset)
|
|
171
|
-
.
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
193
|
+
// Full CSS transform matrix with anchor baked in. Preserves transform
|
|
194
|
+
// composition order; CSS translateZ participates in rotation direction.
|
|
195
|
+
.transform3DEffect(anchoredTransform)
|
|
196
|
+
// backOffset + zIndex: always along parent Z, independent of CSS transform.
|
|
197
|
+
.offset(z: frameOffsetZ)
|
|
198
|
+
// Gesture before .position(): event.location3D is in the element's local space
|
|
199
|
+
// (top-left origin), and does not include visual transforms.
|
|
200
|
+
.simultaneousGesture(enableGesture ? gesture : nil)
|
|
201
|
+
.onGeometryChange3D(for: AffineTransform3D.self) { proxy in
|
|
202
|
+
proxy.transform(in: .named("SpatialScene"))!
|
|
203
|
+
} action: { new in
|
|
204
|
+
gestureState.proxyTransform = new
|
|
205
|
+
spatializedElement.proxySceneTransform = new
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
.position(x: centerX + width / 2, y: centerY + height / 2)
|
|
184
|
-
.offset(z: spatializedElement.backOffset)
|
|
185
209
|
.opacity(opacity)
|
|
186
210
|
.hidden(!visible)
|
|
187
|
-
.simultaneousGesture(enableGesture ? gesture : nil)
|
|
188
211
|
}
|
|
189
212
|
}
|
|
@@ -28,7 +28,11 @@ struct SpatializedStatic3DView: View {
|
|
|
28
28
|
let z = translation.z
|
|
29
29
|
|
|
30
30
|
let enableGesture = spatializedElement.enableGesture
|
|
31
|
-
|
|
31
|
+
let rawModelURL = spatializedStatic3DElement.modelURL
|
|
32
|
+
let modelURL = rawModelURL.hasPrefix("file://")
|
|
33
|
+
? pwaManager.getLocalResourceURL(url: rawModelURL)
|
|
34
|
+
: rawModelURL
|
|
35
|
+
if let url = URL(string: modelURL) {
|
|
32
36
|
Model3D(url: url) { newPhase in
|
|
33
37
|
switch newPhase {
|
|
34
38
|
case .empty:
|
|
@@ -113,7 +113,21 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
113
113
|
print("urlSchemeTask")
|
|
114
114
|
let url = urlSchemeTask.request.url
|
|
115
115
|
if url!.absoluteString.starts(with: "file://") {
|
|
116
|
-
let
|
|
116
|
+
let resource: String = pwaManager.getLocalResourceURL(url: url!.absoluteString)
|
|
117
|
+
var urlRequest = urlSchemeTask.request
|
|
118
|
+
if resource != "" {
|
|
119
|
+
if let resourceUrl = URL(string: resource) {
|
|
120
|
+
urlRequest = URLRequest(url: resourceUrl)
|
|
121
|
+
} else {
|
|
122
|
+
let error = NSError(domain: "LocalResourceError", code: 404, userInfo: [NSLocalizedDescriptionKey: "Local file resource mapping failed"])
|
|
123
|
+
urlSchemeTask.didFailWithError(error)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
let error = NSError(domain: "LocalResourceError", code: 404, userInfo: [NSLocalizedDescriptionKey: "Local resource not found"])
|
|
128
|
+
urlSchemeTask.didFailWithError(error)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
117
131
|
|
|
118
132
|
let session = URLSession(configuration: URLSessionConfiguration.default)
|
|
119
133
|
let dataTask = session.dataTask(with: urlRequest) { [task = urlSchemeTask as AnyObject] data, response, _ in
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
dynamic3d/SpatialMaterial.swift,
|
|
71
71
|
dynamic3d/SpatialModelEntity.swift,
|
|
72
72
|
dynamic3d/SpatialModelResource.swift,
|
|
73
|
+
dynamic3d/SpatialRootEntity.swift,
|
|
73
74
|
dynamic3d/SpatialTextureResource.swift,
|
|
74
75
|
SpatialApp.swift,
|
|
75
76
|
Spatialized2DElement.swift,
|
|
@@ -155,22 +156,22 @@
|
|
|
155
156
|
/* End PBXFrameworksBuildPhase section */
|
|
156
157
|
|
|
157
158
|
/* Begin PBXGroup section */
|
|
158
|
-
|
|
159
|
+
2B1008A32F0B000800000003 /* web-spatialTests */ = {
|
|
159
160
|
isa = PBXGroup;
|
|
160
161
|
children = (
|
|
161
|
-
|
|
162
|
-
2B1008A32F0B000800000003 /* web-spatialTests */,
|
|
163
|
-
2B2F1D662BEBFAAA006897EE /* Packages */,
|
|
164
|
-
2B2F1D642BEBFAAA006897EE /* Products */,
|
|
162
|
+
2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */,
|
|
165
163
|
);
|
|
164
|
+
path = "web-spatialTests";
|
|
166
165
|
sourceTree = "<group>";
|
|
167
166
|
};
|
|
168
|
-
|
|
167
|
+
2B2F1D5A2BEBFAAA006897EE = {
|
|
169
168
|
isa = PBXGroup;
|
|
170
169
|
children = (
|
|
171
|
-
|
|
170
|
+
2B2F1D652BEBFAAA006897EE /* web-spatial */,
|
|
171
|
+
2B1008A32F0B000800000003 /* web-spatialTests */,
|
|
172
|
+
2B2F1D662BEBFAAA006897EE /* Packages */,
|
|
173
|
+
2B2F1D642BEBFAAA006897EE /* Products */,
|
|
172
174
|
);
|
|
173
|
-
path = "web-spatialTests";
|
|
174
175
|
sourceTree = "<group>";
|
|
175
176
|
};
|
|
176
177
|
2B2F1D642BEBFAAA006897EE /* Products */ = {
|