@webspatial/platform-visionos 1.3.0 → 1.4.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 +23 -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 +169 -35
- package/web-spatial/model/SpatializedDynamic3DElement.swift +12 -0
- package/web-spatial/model/SpatializedElement.swift +40 -0
- package/web-spatial/model/dynamic3d/SpatialEntity.swift +6 -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 +8 -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?
|
|
@@ -166,6 +173,7 @@ protocol SpatializedElementProperties: SpatialObjectCommand {
|
|
|
166
173
|
var enableMagnifyGesture: Bool? { get }
|
|
167
174
|
var enableMagnifyEndGesture: Bool? { get }
|
|
168
175
|
var enableTapGesture: Bool? { get }
|
|
176
|
+
var rotateConstrainedToAxis: Vec3? { get }
|
|
169
177
|
}
|
|
170
178
|
|
|
171
179
|
struct UpdateSpatialized2DElementProperties: SpatializedElementProperties {
|
|
@@ -193,6 +201,8 @@ struct UpdateSpatialized2DElementProperties: SpatializedElementProperties {
|
|
|
193
201
|
var enableMagnifyEndGesture: Bool?
|
|
194
202
|
var enableTapGesture: Bool?
|
|
195
203
|
|
|
204
|
+
let rotateConstrainedToAxis: Vec3?
|
|
205
|
+
|
|
196
206
|
let scrollPageEnabled: Bool?
|
|
197
207
|
let material: BackgroundMaterial?
|
|
198
208
|
let cornerRadius: CornerRadius?
|
|
@@ -227,6 +237,8 @@ struct UpdateSpatializedStatic3DElementProperties: SpatializedElementProperties
|
|
|
227
237
|
let enableMagnifyEndGesture: Bool?
|
|
228
238
|
let enableTapGesture: Bool?
|
|
229
239
|
|
|
240
|
+
let rotateConstrainedToAxis: Vec3?
|
|
241
|
+
|
|
230
242
|
let modelURL: String?
|
|
231
243
|
let modelTransform: [Double]?
|
|
232
244
|
}
|
|
@@ -255,6 +267,8 @@ struct UpdateSpatializedDynamic3DElementProperties: SpatializedElementProperties
|
|
|
255
267
|
let enableMagnifyGesture: Bool?
|
|
256
268
|
let enableMagnifyEndGesture: Bool?
|
|
257
269
|
let enableTapGesture: Bool?
|
|
270
|
+
|
|
271
|
+
let rotateConstrainedToAxis: Vec3?
|
|
258
272
|
}
|
|
259
273
|
|
|
260
274
|
struct UpdateSpatializedElementTransform: SpatialObjectCommand {
|
|
@@ -339,6 +353,15 @@ struct GetSpatialSceneStateCommand: CommandDataProtocol {
|
|
|
339
353
|
static let commandType = "GetSpatialSceneState"
|
|
340
354
|
}
|
|
341
355
|
|
|
356
|
+
struct InitializeAttachmentCommand: CommandDataProtocol {
|
|
357
|
+
static let commandType = "InitializeAttachment"
|
|
358
|
+
let id: String
|
|
359
|
+
let parentEntityId: String
|
|
360
|
+
let position: [Float]?
|
|
361
|
+
let size: AttachmentSize?
|
|
362
|
+
let ownerViewId: String
|
|
363
|
+
}
|
|
364
|
+
|
|
342
365
|
struct UpdateAttachmentEntityCommand: CommandDataProtocol {
|
|
343
366
|
static let commandType = "UpdateAttachmentEntity"
|
|
344
367
|
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/#/geometry-verify"
|
|
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,8 @@ 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)
|
|
303
323
|
|
|
304
324
|
spatialWebViewModel.addJSBListener(UpdateAttachmentEntityCommand.self, onUpdateAttachmentEntity)
|
|
305
325
|
|
|
@@ -326,8 +346,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
326
346
|
|
|
327
347
|
// write through
|
|
328
348
|
spatialWebViewModel.updateWindowKV([
|
|
329
|
-
"
|
|
330
|
-
"
|
|
349
|
+
"xrInnerDepth": depth,
|
|
350
|
+
"xrOuterDepth": depth,
|
|
331
351
|
"outerHeight": height + SpatialScene.navHeight,
|
|
332
352
|
])
|
|
333
353
|
}
|
|
@@ -349,6 +369,17 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
349
369
|
self.handleWindowClose()
|
|
350
370
|
}
|
|
351
371
|
|
|
372
|
+
spatialWebViewModel.addStateListener(.didReceive) {
|
|
373
|
+
if let meterToPtUnscaled = self.meterToPtUnscaled,
|
|
374
|
+
let meterToPtScaled = self.meterToPtScaled
|
|
375
|
+
{
|
|
376
|
+
self.onUpdatePhysicalMetrics(
|
|
377
|
+
meterToPtUnscaled: meterToPtUnscaled,
|
|
378
|
+
meterToPtScaled: meterToPtScaled
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
352
383
|
spatialWebViewModel.addStateListener(.didFailLoad) {
|
|
353
384
|
self.didFailLoad = true
|
|
354
385
|
}
|
|
@@ -394,46 +425,51 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
394
425
|
}
|
|
395
426
|
}
|
|
396
427
|
|
|
428
|
+
// Temporary storage for webview models awaiting JSB initialization
|
|
429
|
+
private var pendingAttachmentWebViewModels = [String: SpatialWebViewModel]()
|
|
430
|
+
|
|
397
431
|
private func handleCreateAttachment(_ url: URL) -> WebViewElementInfo? {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
432
|
+
// Just create a bare webview — metadata arrives via InitializeAttachment JSB
|
|
433
|
+
let id = UUID().uuidString
|
|
434
|
+
let webViewModel = SpatialWebViewModel(url: nil)
|
|
435
|
+
webViewModel.setBackgroundTransparent(true)
|
|
436
|
+
pendingAttachmentWebViewModels[id] = webViewModel
|
|
437
|
+
return WebViewElementInfo(id: id, element: webViewModel)
|
|
438
|
+
}
|
|
404
439
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
440
|
+
private func onInitializeAttachment(
|
|
441
|
+
command: InitializeAttachmentCommand,
|
|
442
|
+
resolve: @escaping JSBManager.ResolveHandler<Encodable>
|
|
443
|
+
) {
|
|
444
|
+
guard let webViewModel = pendingAttachmentWebViewModels.removeValue(forKey: command.id) else {
|
|
445
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "No pending attachment for \(command.id)")))
|
|
446
|
+
return
|
|
408
447
|
}
|
|
409
448
|
|
|
410
|
-
// Parse position (JSON array like [0,0.1,0])
|
|
411
449
|
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])
|
|
450
|
+
if let posArray = command.position, posArray.count >= 3 {
|
|
451
|
+
position = SIMD3<Float>(posArray[0], posArray[1], posArray[2])
|
|
418
452
|
}
|
|
419
453
|
|
|
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
|
-
}
|
|
454
|
+
let size = CGSize(
|
|
455
|
+
width: command.size?.width ?? 100,
|
|
456
|
+
height: command.size?.height ?? 100
|
|
457
|
+
)
|
|
428
458
|
|
|
429
|
-
let
|
|
430
|
-
|
|
431
|
-
|
|
459
|
+
let ownerId = command.ownerViewId
|
|
460
|
+
if spatialObjects[ownerId] == nil {
|
|
461
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "ownerViewId must belong to the current scene for attachment \(command.id)")))
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
attachmentManager.create(
|
|
465
|
+
id: command.id,
|
|
466
|
+
parentEntityId: command.parentEntityId,
|
|
432
467
|
position: position,
|
|
433
|
-
size: size
|
|
468
|
+
size: size,
|
|
469
|
+
webViewModel: webViewModel
|
|
434
470
|
)
|
|
435
471
|
|
|
436
|
-
|
|
472
|
+
resolve(.success(baseReplyData))
|
|
437
473
|
}
|
|
438
474
|
|
|
439
475
|
private func onPageStartLoad() {
|
|
@@ -442,7 +478,6 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
442
478
|
for spatialObject in spatialObjectArray {
|
|
443
479
|
spatialObject.destroy()
|
|
444
480
|
}
|
|
445
|
-
// destroy all attachments
|
|
446
481
|
attachmentManager.destroyAll()
|
|
447
482
|
backgroundMaterial = .None
|
|
448
483
|
}
|
|
@@ -715,6 +750,10 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
715
750
|
spatializedElement.enableRotateEndGesture = enableRotateEndGesture
|
|
716
751
|
}
|
|
717
752
|
|
|
753
|
+
if let rotateConstrainedToAxis = command.rotateConstrainedToAxis {
|
|
754
|
+
spatializedElement.rotateConstrainedToAxis = rotateConstrainedToAxis
|
|
755
|
+
}
|
|
756
|
+
|
|
718
757
|
if let enableMagnifyGesture = command.enableMagnifyGesture {
|
|
719
758
|
spatializedElement.enableMagnifyGesture = enableMagnifyGesture
|
|
720
759
|
}
|
|
@@ -1068,22 +1107,91 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
1068
1107
|
resolve(.success(ConvertReply(id: command.entityId, position: point)))
|
|
1069
1108
|
}
|
|
1070
1109
|
|
|
1110
|
+
/// Input: command.position, command.fromId, command.toId
|
|
1111
|
+
/// fromId/toId can reference either the scene (window) or an entity.
|
|
1112
|
+
/// Step 1: Convert position to window coordinates (view global, px)
|
|
1113
|
+
/// - If from is window (scene), position is already in view global (px). Go to Step 2.
|
|
1114
|
+
/// - If from is 2d frame(SpatializedElement), position is in view local (px).
|
|
1115
|
+
/// - view local → window (view global, px) using SpatializedElement.convertToScene
|
|
1116
|
+
/// - If from is an entity, position is in reality entity local (meters):
|
|
1117
|
+
/// - entity local → reality world (scene)
|
|
1118
|
+
/// - reality world → window (view global, px)
|
|
1119
|
+
/// Step 2: Convert window coordinates (view global, px) to target output
|
|
1120
|
+
/// - If to is window, output directly.
|
|
1121
|
+
/// - If to is 2d frame(SpatializedElement), output in view local (px).
|
|
1122
|
+
/// - window (view global, px) → view local using SpatializedElement.convertFromScene
|
|
1123
|
+
/// - If to is an entity, output in reality entity local (meters):
|
|
1124
|
+
/// - window (view global, px) → reality world (scene)
|
|
1125
|
+
/// - reality world → reality entity local (meters)
|
|
1126
|
+
|
|
1127
|
+
private func onConvertCoordinate(command: ConvertCoordinate, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1128
|
+
func isSceneId(_ id: String) -> Bool {
|
|
1129
|
+
return id.isEmpty
|
|
1130
|
+
}
|
|
1131
|
+
let input = SIMD3<Float>(Float(command.position.x), Float(command.position.y), Float(command.position.z))
|
|
1132
|
+
let fromEntity = spatialObjects[command.fromId] as? SpatialEntity
|
|
1133
|
+
let from2dFrame = spatialObjects[command.fromId] as? SpatializedElement
|
|
1134
|
+
let toEntity = spatialObjects[command.toId] as? SpatialEntity
|
|
1135
|
+
let to2dFrame = spatialObjects[command.toId] as? SpatializedElement
|
|
1136
|
+
|
|
1137
|
+
var globalPx: Point3D
|
|
1138
|
+
if isSceneId(command.fromId) {
|
|
1139
|
+
globalPx = Point3D(x: Double(input.x), y: Double(input.y), z: Double(input.z))
|
|
1140
|
+
} else if let fromEntity {
|
|
1141
|
+
let world = fromEntity.convert(position: input, to: nil)
|
|
1142
|
+
guard let content = findSpatializedDynamic3DElement(containingEntityId: fromEntity.spatialId)?.getViewContent() else {
|
|
1143
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "RealityView content unavailable for conversion")))
|
|
1144
|
+
return
|
|
1145
|
+
}
|
|
1146
|
+
globalPx = content.convert(point: world, from: .scene, to: .global)
|
|
1147
|
+
} else if let from2dFrame {
|
|
1148
|
+
let localPoint = SIMD3<Double>(Double(input.x), Double(input.y), Double(input.z))
|
|
1149
|
+
let scenePoint = from2dFrame.convertToScene(localPoint)
|
|
1150
|
+
globalPx = Point3D(x: scenePoint.x, y: scenePoint.y, z: scenePoint.z)
|
|
1151
|
+
} else {
|
|
1152
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Invalid fromId")))
|
|
1153
|
+
return
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if isSceneId(command.toId) {
|
|
1157
|
+
let result = Vec3(x: CGFloat(globalPx.x), y: CGFloat(globalPx.y), z: CGFloat(globalPx.z))
|
|
1158
|
+
resolve(.success(result))
|
|
1159
|
+
return
|
|
1160
|
+
} else if let toEntity {
|
|
1161
|
+
guard let content = findSpatializedDynamic3DElement(containingEntityId: toEntity.spatialId)?.getViewContent() else {
|
|
1162
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "RealityView content unavailable for conversion")))
|
|
1163
|
+
return
|
|
1164
|
+
}
|
|
1165
|
+
let world = content.convert(globalPx, from: .global, to: .scene)
|
|
1166
|
+
let local = toEntity.convert(position: world, from: nil)
|
|
1167
|
+
let ret = Vec3(x: CGFloat(local.x), y: CGFloat(local.y), z: CGFloat(local.z))
|
|
1168
|
+
resolve(.success(ret))
|
|
1169
|
+
return
|
|
1170
|
+
} else if let to2dFrame {
|
|
1171
|
+
let scenePoint = SIMD3<Double>(globalPx.x, globalPx.y, globalPx.z)
|
|
1172
|
+
let localPoint = to2dFrame.convertFromScene(scenePoint)
|
|
1173
|
+
let ret = Vec3(x: CGFloat(localPoint.x), y: CGFloat(localPoint.y), z: CGFloat(localPoint.z))
|
|
1174
|
+
resolve(.success(ret))
|
|
1175
|
+
return
|
|
1176
|
+
} else {
|
|
1177
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Invalid toId")))
|
|
1178
|
+
return
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1071
1182
|
private func onUpdateAttachmentEntity(command: UpdateAttachmentEntityCommand, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1072
1183
|
guard attachmentManager.get(id: command.id) != nil else {
|
|
1073
1184
|
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Attachment \(command.id) not found")))
|
|
1074
1185
|
return
|
|
1075
1186
|
}
|
|
1076
|
-
|
|
1077
1187
|
var newPosition: SIMD3<Float>? = nil
|
|
1078
1188
|
if let posArray = command.position, posArray.count >= 3 {
|
|
1079
1189
|
newPosition = SIMD3<Float>(posArray[0], posArray[1], posArray[2])
|
|
1080
1190
|
}
|
|
1081
|
-
|
|
1082
1191
|
var newSize: CGSize? = nil
|
|
1083
1192
|
if let sizeObj = command.size {
|
|
1084
1193
|
newSize = CGSize(width: sizeObj.width, height: sizeObj.height)
|
|
1085
1194
|
}
|
|
1086
|
-
|
|
1087
1195
|
attachmentManager.update(id: command.id, position: newPosition, size: newSize)
|
|
1088
1196
|
resolve(.success(baseReplyData))
|
|
1089
1197
|
}
|
|
@@ -1111,6 +1219,32 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
1111
1219
|
sendWebMsg(spatialObject.spatialId, SpatialObjectDestroiedEvent())
|
|
1112
1220
|
}
|
|
1113
1221
|
|
|
1222
|
+
/// Find the dynamic 3D container (SpatializedDynamic3DElement) that contains the entity by ID.
|
|
1223
|
+
/// - Parameter entityId: The entity's spatialId.
|
|
1224
|
+
/// - Returns: The container if the entity is a descendant of the container's root; otherwise nil.
|
|
1225
|
+
private func findSpatializedDynamic3DElement(containingEntityId entityId: String) -> SpatializedDynamic3DElement? {
|
|
1226
|
+
guard let entity = spatialObjects[entityId] as? SpatialEntity else {
|
|
1227
|
+
return nil
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
for (_, object) in spatialObjects {
|
|
1231
|
+
guard let dynamic3dElement = object as? SpatializedDynamic3DElement else {
|
|
1232
|
+
continue
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
let root = dynamic3dElement.getRoot()
|
|
1236
|
+
var current: Entity? = entity
|
|
1237
|
+
while let node = current {
|
|
1238
|
+
if node === root {
|
|
1239
|
+
return dynamic3dElement
|
|
1240
|
+
}
|
|
1241
|
+
current = node.parent
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
return nil
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1114
1248
|
func findSpatialObject<T: SpatialObjectProtocol>(_ id: String) -> T? {
|
|
1115
1249
|
return spatialObjects[id] as? T
|
|
1116
1250
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import _RealityKit_SwiftUI
|
|
1
2
|
import Foundation
|
|
3
|
+
import RealityKit
|
|
2
4
|
|
|
3
5
|
@Observable
|
|
4
6
|
class SpatializedDynamic3DElement: SpatializedElement {
|
|
5
7
|
private var rootEntity = SpatialEntity()
|
|
8
|
+
private var viewContent: RealityViewContent? = nil
|
|
6
9
|
|
|
7
10
|
func getRoot() -> SpatialEntity {
|
|
8
11
|
return rootEntity
|
|
@@ -16,6 +19,14 @@ class SpatializedDynamic3DElement: SpatializedElement {
|
|
|
16
19
|
rootEntity.removeChild(entity)
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
func getViewContent() -> RealityViewContent? {
|
|
23
|
+
return viewContent
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
func setViewContent(_ content: RealityViewContent?) {
|
|
27
|
+
viewContent = content
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
enum CodingKeys: String, CodingKey {
|
|
20
31
|
case type, root
|
|
21
32
|
}
|
|
@@ -28,6 +39,7 @@ class SpatializedDynamic3DElement: SpatializedElement {
|
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
override func onDestroy() {
|
|
42
|
+
viewContent = nil
|
|
31
43
|
rootEntity.destroy()
|
|
32
44
|
super.onDestroy()
|
|
33
45
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
|
|
@@ -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
|
|
@@ -155,22 +155,22 @@
|
|
|
155
155
|
/* End PBXFrameworksBuildPhase section */
|
|
156
156
|
|
|
157
157
|
/* Begin PBXGroup section */
|
|
158
|
-
|
|
158
|
+
2B1008A32F0B000800000003 /* web-spatialTests */ = {
|
|
159
159
|
isa = PBXGroup;
|
|
160
160
|
children = (
|
|
161
|
-
|
|
162
|
-
2B1008A32F0B000800000003 /* web-spatialTests */,
|
|
163
|
-
2B2F1D662BEBFAAA006897EE /* Packages */,
|
|
164
|
-
2B2F1D642BEBFAAA006897EE /* Products */,
|
|
161
|
+
2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */,
|
|
165
162
|
);
|
|
163
|
+
path = "web-spatialTests";
|
|
166
164
|
sourceTree = "<group>";
|
|
167
165
|
};
|
|
168
|
-
|
|
166
|
+
2B2F1D5A2BEBFAAA006897EE = {
|
|
169
167
|
isa = PBXGroup;
|
|
170
168
|
children = (
|
|
171
|
-
|
|
169
|
+
2B2F1D652BEBFAAA006897EE /* web-spatial */,
|
|
170
|
+
2B1008A32F0B000800000003 /* web-spatialTests */,
|
|
171
|
+
2B2F1D662BEBFAAA006897EE /* Packages */,
|
|
172
|
+
2B2F1D642BEBFAAA006897EE /* Products */,
|
|
172
173
|
);
|
|
173
|
-
path = "web-spatialTests";
|
|
174
174
|
sourceTree = "<group>";
|
|
175
175
|
};
|
|
176
176
|
2B2F1D642BEBFAAA006897EE /* Products */ = {
|