@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.
Files changed (34) hide show
  1. package/package.json +2 -1
  2. package/web-spatial/EventEmitter.swift +11 -11
  3. package/web-spatial/JSBCommand.swift +38 -3
  4. package/web-spatial/WebMsgCommand.swift +5 -16
  5. package/web-spatial/WebSpatialApp.swift +10 -10
  6. package/web-spatial/Window.swift +2 -2
  7. package/web-spatial/manager/AttachmentManager.swift +84 -0
  8. package/web-spatial/manager/Dynamic3DManager.swift +10 -0
  9. package/web-spatial/manager/JSBManager.swift +1 -2
  10. package/web-spatial/manager/WKWebViewManager.swift +4 -4
  11. package/web-spatial/manifest.swift +11 -6
  12. package/web-spatial/model/SpatialApp.swift +60 -56
  13. package/web-spatial/model/SpatialScene.swift +233 -16
  14. package/web-spatial/model/Spatialized2DElement.swift +4 -5
  15. package/web-spatial/model/SpatializedDynamic3DElement.swift +12 -0
  16. package/web-spatial/model/SpatializedElement.swift +40 -0
  17. package/web-spatial/model/SpatializedStatic3DElement.swift +1 -1
  18. package/web-spatial/model/dynamic3d/SpatialComponent.swift +27 -27
  19. package/web-spatial/model/dynamic3d/SpatialEntity.swift +8 -2
  20. package/web-spatial/model/dynamic3d/SpatialMaterial.swift +15 -15
  21. package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +10 -10
  22. package/web-spatial/model/dynamic3d/SpatialModelResource.swift +1 -1
  23. package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +8 -8
  24. package/web-spatial/view/SceneHandlerUIView.swift +29 -1
  25. package/web-spatial/view/SpatialNavView.swift +52 -47
  26. package/web-spatial/view/SpatializedDynamic3DView.swift +88 -5
  27. package/web-spatial/view/SpatializedElementView.swift +85 -47
  28. package/web-spatial/view/SpatializedStatic3DView.swift +9 -7
  29. package/web-spatial/view/view-modifier/HideViewModifier.swift +2 -2
  30. package/web-spatial/webview/SpatialWebController.swift +42 -25
  31. package/web-spatial/webview/SpatialWebView.swift +5 -1
  32. package/web-spatial/webview/SpatialWebViewModel.swift +13 -7
  33. package/web-spatial.xcodeproj/project.pbxproj +13 -0
  34. 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
- // Enum
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
- // default value
72
+ /// default value
70
73
  case idle
71
- // when SpatialScene is loading
74
+ /// when SpatialScene is loading
72
75
  case pending
73
- // when SpatialScen will visible after some time
76
+ /// when SpatialScen will visible after some time
74
77
  case willVisible
75
- // when SpatialScen load Succesfully
78
+ /// when SpatialScen load Succesfully
76
79
  case visible
77
- // when SpatialScen Failed to load
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
- // used to send message to spatial root webview
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
- "innerDepth": depth,
326
- "outerDepth": depth,
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
- // SpatialScene can hold a collection of SpatializedElement children
803
+ /// SpatialScene can hold a collection of SpatializedElement children
702
804
  private var children = [String: SpatializedElement]()
703
805
 
704
- // Called by SpatializedElement.setParent
806
+ /// Called by SpatializedElement.setParent
705
807
  func addChild(_ spatializedElement: SpatializedElement) {
706
808
  children[spatializedElement.id] = spatializedElement
707
809
  }
708
810
 
709
- // Called by SpatializedElement.setParent
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
- let typedChildren = children.filter {
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
- // Resources that will be destroyed when this webpage is destoryed or if it is navigated away from
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
- // Spatialized2DElement can hold a collection of SpatializedElement children
60
+ /// Spatialized2DElement can hold a collection of SpatializedElement children
61
61
  private var children = [String: SpatializedElement]()
62
62
 
63
- // Called by SpatializedElement.setParent
63
+ /// Called by SpatializedElement.setParent
64
64
  func addChild(_ spatializedElement: SpatializedElement) {
65
65
  children[spatializedElement.id] = spatializedElement
66
66
  }
67
67
 
68
- // Called by SpatializedElement.setParent
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
- let typedChildren = children.filter {
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 = AffineTransform3D.identity
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
- internal var _resource:Component? = nil
10
- var resource:Component? {
7
+
8
+ var _resource: Component?
9
+ var resource: Component? {
11
10
  _resource
12
11
  }
13
-
14
- internal var _entity:SpatialEntity? = nil
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
- mats.forEach{ item in
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 internal func onDestroy() {
64
+
65
+ override func onDestroy() {
66
66
  _resource = nil
67
67
  }
68
68
  }
69
69
 
70
- enum SpatialComponentType:String {
71
- case ModelComponent = "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
- // Encodable
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
- // Equatable
182
+ /// Equatable
177
183
  static func == (lhs: SpatialEntity, rhs: SpatialEntity) -> Bool {
178
184
  return lhs.spatialId == rhs.spatialId
179
185
  }