@webspatial/platform-visionos 1.2.1 → 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/EventEmitter.swift +11 -11
- package/web-spatial/JSBCommand.swift +38 -3
- package/web-spatial/WebMsgCommand.swift +5 -16
- package/web-spatial/WebSpatialApp.swift +10 -10
- package/web-spatial/Window.swift +2 -2
- package/web-spatial/manager/AttachmentManager.swift +84 -0
- package/web-spatial/manager/Dynamic3DManager.swift +10 -0
- package/web-spatial/manager/JSBManager.swift +1 -2
- package/web-spatial/manager/WKWebViewManager.swift +4 -4
- package/web-spatial/manifest.swift +11 -6
- package/web-spatial/model/SpatialApp.swift +60 -56
- package/web-spatial/model/SpatialScene.swift +233 -16
- package/web-spatial/model/Spatialized2DElement.swift +4 -5
- package/web-spatial/model/SpatializedDynamic3DElement.swift +12 -0
- package/web-spatial/model/SpatializedElement.swift +40 -0
- package/web-spatial/model/SpatializedStatic3DElement.swift +1 -1
- package/web-spatial/model/dynamic3d/SpatialComponent.swift +27 -27
- package/web-spatial/model/dynamic3d/SpatialEntity.swift +8 -2
- package/web-spatial/model/dynamic3d/SpatialMaterial.swift +15 -15
- package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +10 -10
- package/web-spatial/model/dynamic3d/SpatialModelResource.swift +1 -1
- package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +8 -8
- package/web-spatial/view/SceneHandlerUIView.swift +29 -1
- package/web-spatial/view/SpatialNavView.swift +52 -47
- package/web-spatial/view/SpatializedDynamic3DView.swift +88 -5
- package/web-spatial/view/SpatializedElementView.swift +85 -47
- package/web-spatial/view/SpatializedStatic3DView.swift +9 -7
- package/web-spatial/view/view-modifier/HideViewModifier.swift +2 -2
- package/web-spatial/webview/SpatialWebController.swift +42 -25
- package/web-spatial/webview/SpatialWebView.swift +5 -1
- package/web-spatial/webview/SpatialWebViewModel.swift +13 -7
- package/web-spatial.xcodeproj/project.pbxproj +13 -0
- package/web-spatialTests/NavigationCleanupTests.swift +33 -0
|
@@ -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
|
|
|
@@ -38,7 +39,9 @@ let defaultSceneConfig = SceneOptions(
|
|
|
38
39
|
class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSender {
|
|
39
40
|
var parent: (any ScrollAbleSpatialElementContainer)?
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
var attachmentManager = AttachmentManager()
|
|
43
|
+
|
|
44
|
+
/// Enum
|
|
42
45
|
enum WindowStyle: String, Codable, CaseIterable {
|
|
43
46
|
case window
|
|
44
47
|
case volume
|
|
@@ -66,15 +69,15 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
enum SceneStateKind: String {
|
|
69
|
-
|
|
72
|
+
/// default value
|
|
70
73
|
case idle
|
|
71
|
-
|
|
74
|
+
/// when SpatialScene is loading
|
|
72
75
|
case pending
|
|
73
|
-
|
|
76
|
+
/// when SpatialScen will visible after some time
|
|
74
77
|
case willVisible
|
|
75
|
-
|
|
78
|
+
/// when SpatialScen load Succesfully
|
|
76
79
|
case visible
|
|
77
|
-
|
|
80
|
+
/// when SpatialScen Failed to load
|
|
78
81
|
case fail
|
|
79
82
|
}
|
|
80
83
|
|
|
@@ -84,6 +87,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
84
87
|
|
|
85
88
|
var spatialWebViewModel: SpatialWebViewModel
|
|
86
89
|
|
|
90
|
+
private var meterToPtUnscaled: Double?
|
|
91
|
+
private var meterToPtScaled: Double?
|
|
92
|
+
|
|
87
93
|
init(
|
|
88
94
|
_ url: String,
|
|
89
95
|
_ windowStyle: WindowStyle,
|
|
@@ -101,11 +107,25 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
101
107
|
moveToState(state, sceneOptions)
|
|
102
108
|
}
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
/// used to send message to spatial root webview
|
|
105
111
|
func sendWebMsg(_ id: String, _ msg: Encodable) {
|
|
106
112
|
spatialWebViewModel.sendWebEvent(id, msg)
|
|
107
113
|
}
|
|
108
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
|
+
|
|
109
129
|
private func setupSpatialWebView() {
|
|
110
130
|
setupJSBListeners()
|
|
111
131
|
setupWebViewStateListener()
|
|
@@ -298,6 +318,10 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
298
318
|
spatialWebViewModel.addJSBListener(ConvertFromEntityToEntity.self, onConvertFromEntityToEntity)
|
|
299
319
|
spatialWebViewModel.addJSBListener(ConvertFromEntityToScene.self, onConvertFromEntityToScene)
|
|
300
320
|
spatialWebViewModel.addJSBListener(ConvertFromSceneToEntity.self, onConvertFromSceneToEntity)
|
|
321
|
+
spatialWebViewModel.addJSBListener(InitializeAttachmentCommand.self, onInitializeAttachment)
|
|
322
|
+
spatialWebViewModel.addJSBListener(ConvertCoordinate.self, onConvertCoordinate)
|
|
323
|
+
|
|
324
|
+
spatialWebViewModel.addJSBListener(UpdateAttachmentEntityCommand.self, onUpdateAttachmentEntity)
|
|
301
325
|
|
|
302
326
|
spatialWebViewModel.addOpenWindowListener(protocal: "webspatial", onOpenWindowHandler)
|
|
303
327
|
|
|
@@ -322,8 +346,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
322
346
|
|
|
323
347
|
// write through
|
|
324
348
|
spatialWebViewModel.updateWindowKV([
|
|
325
|
-
"
|
|
326
|
-
"
|
|
349
|
+
"xrInnerDepth": depth,
|
|
350
|
+
"xrOuterDepth": depth,
|
|
327
351
|
"outerHeight": height + SpatialScene.navHeight,
|
|
328
352
|
])
|
|
329
353
|
}
|
|
@@ -345,6 +369,17 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
345
369
|
self.handleWindowClose()
|
|
346
370
|
}
|
|
347
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
|
+
|
|
348
383
|
spatialWebViewModel.addStateListener(.didFailLoad) {
|
|
349
384
|
self.didFailLoad = true
|
|
350
385
|
}
|
|
@@ -380,6 +415,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
380
415
|
let host = url.host ?? ""
|
|
381
416
|
if host == "createSpatialScene" {
|
|
382
417
|
return handleWindowOpenCustom(url)
|
|
418
|
+
} else if host == "createAttachment" {
|
|
419
|
+
return handleCreateAttachment(url)
|
|
383
420
|
} else {
|
|
384
421
|
let spatialized2DElement: Spatialized2DElement = createSpatializedElement(
|
|
385
422
|
.Spatialized2DElement
|
|
@@ -388,15 +425,70 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
388
425
|
}
|
|
389
426
|
}
|
|
390
427
|
|
|
428
|
+
// Temporary storage for webview models awaiting JSB initialization
|
|
429
|
+
private var pendingAttachmentWebViewModels = [String: SpatialWebViewModel]()
|
|
430
|
+
|
|
431
|
+
private func handleCreateAttachment(_ url: URL) -> WebViewElementInfo? {
|
|
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
|
+
}
|
|
439
|
+
|
|
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
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
var position = SIMD3<Float>(0, 0, 0)
|
|
450
|
+
if let posArray = command.position, posArray.count >= 3 {
|
|
451
|
+
position = SIMD3<Float>(posArray[0], posArray[1], posArray[2])
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let size = CGSize(
|
|
455
|
+
width: command.size?.width ?? 100,
|
|
456
|
+
height: command.size?.height ?? 100
|
|
457
|
+
)
|
|
458
|
+
|
|
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,
|
|
467
|
+
position: position,
|
|
468
|
+
size: size,
|
|
469
|
+
webViewModel: webViewModel
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
resolve(.success(baseReplyData))
|
|
473
|
+
}
|
|
474
|
+
|
|
391
475
|
private func onPageStartLoad() {
|
|
392
476
|
// destroy all SpatialObject asset
|
|
393
477
|
let spatialObjectArray = spatialObjects.map { $0.value }
|
|
394
478
|
for spatialObject in spatialObjectArray {
|
|
395
479
|
spatialObject.destroy()
|
|
396
480
|
}
|
|
481
|
+
attachmentManager.destroyAll()
|
|
397
482
|
backgroundMaterial = .None
|
|
398
483
|
}
|
|
399
484
|
|
|
485
|
+
/// Some SPA navigations (history back/forward) do not trigger a full WKNavigation
|
|
486
|
+
/// lifecycle. SpatialNavView calls this before navigation actions to ensure
|
|
487
|
+
/// previously-created spatial objects are cleaned up.
|
|
488
|
+
func resetForNavigation() {
|
|
489
|
+
onPageStartLoad()
|
|
490
|
+
}
|
|
491
|
+
|
|
400
492
|
private func onGetSpatialSceneState(
|
|
401
493
|
command: GetSpatialSceneStateCommand,
|
|
402
494
|
resolve: @escaping JSBManager.ResolveHandler<Encodable>
|
|
@@ -424,6 +516,12 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
424
516
|
}
|
|
425
517
|
|
|
426
518
|
private func onDestroySpatialObjectCommand(command: DestroyCommand, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
519
|
+
// Check if it's an attachment first
|
|
520
|
+
if attachmentManager.get(id: command.id) != nil {
|
|
521
|
+
attachmentManager.remove(id: command.id)
|
|
522
|
+
resolve(.success(nil))
|
|
523
|
+
return
|
|
524
|
+
}
|
|
427
525
|
if let spatialObject: SpatialObject = findSpatialObject(command.id) {
|
|
428
526
|
spatialObject.destroy()
|
|
429
527
|
resolve(.success(nil))
|
|
@@ -652,6 +750,10 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
652
750
|
spatializedElement.enableRotateEndGesture = enableRotateEndGesture
|
|
653
751
|
}
|
|
654
752
|
|
|
753
|
+
if let rotateConstrainedToAxis = command.rotateConstrainedToAxis {
|
|
754
|
+
spatializedElement.rotateConstrainedToAxis = rotateConstrainedToAxis
|
|
755
|
+
}
|
|
756
|
+
|
|
655
757
|
if let enableMagnifyGesture = command.enableMagnifyGesture {
|
|
656
758
|
spatializedElement.enableMagnifyGesture = enableMagnifyGesture
|
|
657
759
|
}
|
|
@@ -698,21 +800,21 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
698
800
|
* Begin Implement SpatializedElementContainer Protocol
|
|
699
801
|
*/
|
|
700
802
|
|
|
701
|
-
|
|
803
|
+
/// SpatialScene can hold a collection of SpatializedElement children
|
|
702
804
|
private var children = [String: SpatializedElement]()
|
|
703
805
|
|
|
704
|
-
|
|
806
|
+
/// Called by SpatializedElement.setParent
|
|
705
807
|
func addChild(_ spatializedElement: SpatializedElement) {
|
|
706
808
|
children[spatializedElement.id] = spatializedElement
|
|
707
809
|
}
|
|
708
810
|
|
|
709
|
-
|
|
811
|
+
/// Called by SpatializedElement.setParent
|
|
710
812
|
func removeChild(_ spatializedElement: SpatializedElement) {
|
|
711
813
|
children.removeValue(forKey: spatializedElement.id)
|
|
712
814
|
}
|
|
713
815
|
|
|
714
816
|
func getChildrenOfType(_ type: SpatializedElementType) -> [String: SpatializedElement] {
|
|
715
|
-
|
|
817
|
+
return children.filter {
|
|
716
818
|
switch type {
|
|
717
819
|
case .Spatialized2DElement:
|
|
718
820
|
return $0.value is Spatialized2DElement
|
|
@@ -722,7 +824,6 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
722
824
|
return $0.value is SpatializedDynamic3DElement
|
|
723
825
|
}
|
|
724
826
|
}
|
|
725
|
-
return typedChildren
|
|
726
827
|
}
|
|
727
828
|
|
|
728
829
|
func getChildren() -> [String: SpatializedElement] {
|
|
@@ -733,7 +834,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
733
834
|
* End Implement SpatializedElementContainer Protocol
|
|
734
835
|
*/
|
|
735
836
|
|
|
736
|
-
|
|
837
|
+
/**
|
|
737
838
|
* Begin Implement SpatialScrollAble Protocol
|
|
738
839
|
*/
|
|
739
840
|
let scrollPageEnabled: Bool = true
|
|
@@ -789,7 +890,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
789
890
|
* Begin SpatialObjects management
|
|
790
891
|
*/
|
|
791
892
|
|
|
792
|
-
|
|
893
|
+
/// Resources that will be destroyed when this webpage is destoryed or if it is navigated away from
|
|
793
894
|
private var spatialObjects = [String: any SpatialObjectProtocol]()
|
|
794
895
|
|
|
795
896
|
func createSpatializedElement<T: SpatializedElement>(_ type: SpatializedElementType) -> T {
|
|
@@ -1006,6 +1107,95 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
1006
1107
|
resolve(.success(ConvertReply(id: command.entityId, position: point)))
|
|
1007
1108
|
}
|
|
1008
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
|
+
|
|
1182
|
+
private func onUpdateAttachmentEntity(command: UpdateAttachmentEntityCommand, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
|
|
1183
|
+
guard attachmentManager.get(id: command.id) != nil else {
|
|
1184
|
+
resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Attachment \(command.id) not found")))
|
|
1185
|
+
return
|
|
1186
|
+
}
|
|
1187
|
+
var newPosition: SIMD3<Float>? = nil
|
|
1188
|
+
if let posArray = command.position, posArray.count >= 3 {
|
|
1189
|
+
newPosition = SIMD3<Float>(posArray[0], posArray[1], posArray[2])
|
|
1190
|
+
}
|
|
1191
|
+
var newSize: CGSize? = nil
|
|
1192
|
+
if let sizeObj = command.size {
|
|
1193
|
+
newSize = CGSize(width: sizeObj.width, height: sizeObj.height)
|
|
1194
|
+
}
|
|
1195
|
+
attachmentManager.update(id: command.id, position: newPosition, size: newSize)
|
|
1196
|
+
resolve(.success(baseReplyData))
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1009
1199
|
private func addSpatialObject(_ object: any SpatialObjectProtocol) {
|
|
1010
1200
|
var spatialObject = object
|
|
1011
1201
|
spatialObjects[spatialObject.spatialId] = spatialObject
|
|
@@ -1029,6 +1219,32 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
1029
1219
|
sendWebMsg(spatialObject.spatialId, SpatialObjectDestroiedEvent())
|
|
1030
1220
|
}
|
|
1031
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
|
+
|
|
1032
1248
|
func findSpatialObject<T: SpatialObjectProtocol>(_ id: String) -> T? {
|
|
1033
1249
|
return spatialObjects[id] as? T
|
|
1034
1250
|
}
|
|
@@ -1042,6 +1258,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
|
|
|
1042
1258
|
for spatialObject in spatialObjectArray {
|
|
1043
1259
|
spatialObject.destroy()
|
|
1044
1260
|
}
|
|
1261
|
+
attachmentManager.destroyAll()
|
|
1045
1262
|
spatialWebViewModel.destroy()
|
|
1046
1263
|
}
|
|
1047
1264
|
|
|
@@ -57,15 +57,15 @@ class Spatialized2DElement: SpatializedElement, ScrollAbleSpatialElementContaine
|
|
|
57
57
|
defaultAlignment = .center
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
/// Spatialized2DElement can hold a collection of SpatializedElement children
|
|
61
61
|
private var children = [String: SpatializedElement]()
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
/// Called by SpatializedElement.setParent
|
|
64
64
|
func addChild(_ spatializedElement: SpatializedElement) {
|
|
65
65
|
children[spatializedElement.id] = spatializedElement
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
/// Called by SpatializedElement.setParent
|
|
69
69
|
func removeChild(_ spatializedElement: SpatializedElement) {
|
|
70
70
|
children.removeValue(forKey: spatializedElement.id)
|
|
71
71
|
}
|
|
@@ -75,7 +75,7 @@ class Spatialized2DElement: SpatializedElement, ScrollAbleSpatialElementContaine
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
func getChildrenOfType(_ type: SpatializedElementType) -> [String: SpatializedElement] {
|
|
78
|
-
|
|
78
|
+
return children.filter {
|
|
79
79
|
switch type {
|
|
80
80
|
case .Spatialized2DElement:
|
|
81
81
|
return $0.value is Spatialized2DElement
|
|
@@ -85,7 +85,6 @@ class Spatialized2DElement: SpatializedElement, ScrollAbleSpatialElementContaine
|
|
|
85
85
|
return $0.value is SpatializedDynamic3DElement
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
return typedChildren
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
func loadHtml(_ html: String) {
|
|
@@ -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
|
}
|
|
@@ -4,7 +4,7 @@ import SwiftUI
|
|
|
4
4
|
@Observable
|
|
5
5
|
class SpatializedStatic3DElement: SpatializedElement {
|
|
6
6
|
var modelURL: String = ""
|
|
7
|
-
var modelTransform: AffineTransform3D =
|
|
7
|
+
var modelTransform: AffineTransform3D = .identity
|
|
8
8
|
|
|
9
9
|
enum CodingKeys: String, CodingKey {
|
|
10
10
|
case modelURL, type
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import SwiftUI
|
|
2
1
|
import RealityKit
|
|
2
|
+
import SwiftUI
|
|
3
3
|
|
|
4
4
|
@Observable
|
|
5
5
|
class SpatialComponent: SpatialObject {
|
|
6
6
|
let type: SpatialComponentType
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
var resource:Component? {
|
|
7
|
+
|
|
8
|
+
var _resource: Component?
|
|
9
|
+
var resource: Component? {
|
|
11
10
|
_resource
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
var entity:SpatialEntity? {
|
|
12
|
+
|
|
13
|
+
var _entity: SpatialEntity?
|
|
14
|
+
var entity: SpatialEntity? {
|
|
16
15
|
_entity
|
|
17
16
|
}
|
|
18
|
-
|
|
19
|
-
init(_ _type:SpatialComponentType){
|
|
17
|
+
|
|
18
|
+
init(_ _type: SpatialComponentType) {
|
|
20
19
|
type = _type
|
|
21
20
|
super.init()
|
|
22
21
|
}
|
|
23
|
-
|
|
24
|
-
func addToEntity(entity:SpatialEntity){
|
|
22
|
+
|
|
23
|
+
func addToEntity(entity: SpatialEntity) {
|
|
25
24
|
if _entity != nil {
|
|
26
25
|
print("This component has already been added to another entity")
|
|
27
26
|
return
|
|
28
27
|
}
|
|
29
|
-
if let component = resource{
|
|
28
|
+
if let component = resource {
|
|
30
29
|
_entity = entity
|
|
31
30
|
entity.components.set(component)
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
|
-
|
|
35
|
-
func removeFromEntity(entity:SpatialEntity){
|
|
33
|
+
|
|
34
|
+
func removeFromEntity(entity: SpatialEntity) {
|
|
36
35
|
if let component = resource,
|
|
37
|
-
self.entity == entity
|
|
36
|
+
self.entity == entity
|
|
37
|
+
{
|
|
38
38
|
entity.components.remove(Swift.type(of: component))
|
|
39
39
|
_entity = nil
|
|
40
40
|
}
|
|
@@ -43,30 +43,30 @@ class SpatialComponent: SpatialObject {
|
|
|
43
43
|
|
|
44
44
|
@Observable
|
|
45
45
|
class SpatialModelComponent: SpatialComponent {
|
|
46
|
-
init(mesh:Geometry, mats:[SpatialMaterial]){
|
|
46
|
+
init(mesh: Geometry, mats: [SpatialMaterial]) {
|
|
47
47
|
super.init(.ModelComponent)
|
|
48
|
-
var materials:[any RealityKit.Material] = []
|
|
49
|
-
|
|
48
|
+
var materials: [any RealityKit.Material] = []
|
|
49
|
+
for item in mats {
|
|
50
50
|
materials.append(item.resource!)
|
|
51
51
|
}
|
|
52
52
|
_resource = ModelComponent(mesh: mesh.resource!, materials: materials)
|
|
53
53
|
}
|
|
54
|
-
|
|
55
|
-
override func addToEntity(entity:SpatialEntity){
|
|
54
|
+
|
|
55
|
+
override func addToEntity(entity: SpatialEntity) {
|
|
56
56
|
super.addToEntity(entity: entity)
|
|
57
57
|
entity.generateCollisionShapes(recursive: true)
|
|
58
58
|
}
|
|
59
|
-
|
|
60
|
-
override func removeFromEntity(entity:SpatialEntity){
|
|
59
|
+
|
|
60
|
+
override func removeFromEntity(entity: SpatialEntity) {
|
|
61
61
|
super.removeFromEntity(entity: entity)
|
|
62
62
|
entity.generateCollisionShapes(recursive: true)
|
|
63
63
|
}
|
|
64
|
-
|
|
65
|
-
override
|
|
64
|
+
|
|
65
|
+
override func onDestroy() {
|
|
66
66
|
_resource = nil
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
enum SpatialComponentType:String {
|
|
71
|
-
case ModelComponent
|
|
70
|
+
enum SpatialComponentType: String {
|
|
71
|
+
case ModelComponent
|
|
72
72
|
}
|
|
@@ -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
|
|
|
@@ -159,7 +165,7 @@ class SpatialEntity: Entity, SpatialObjectProtocol {
|
|
|
159
165
|
transform.rotation = simd_quatf(ix: Float(rotation.imag.x), iy: Float(rotation.imag.y), iz: Float(rotation.imag.z), r: Float(rotation.real))
|
|
160
166
|
}
|
|
161
167
|
|
|
162
|
-
|
|
168
|
+
/// Encodable
|
|
163
169
|
enum CodingKeys: String, CodingKey {
|
|
164
170
|
case id, name, isDestroyed, children, components
|
|
165
171
|
}
|
|
@@ -173,7 +179,7 @@ class SpatialEntity: Entity, SpatialObjectProtocol {
|
|
|
173
179
|
try container.encode(spatialComponents, forKey: .components)
|
|
174
180
|
}
|
|
175
181
|
|
|
176
|
-
|
|
182
|
+
/// Equatable
|
|
177
183
|
static func == (lhs: SpatialEntity, rhs: SpatialEntity) -> Bool {
|
|
178
184
|
return lhs.spatialId == rhs.spatialId
|
|
179
185
|
}
|