@webspatial/platform-visionos 1.2.0 → 1.3.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 (29) hide show
  1. package/package.json +1 -1
  2. package/web-spatial/EventEmitter.swift +11 -11
  3. package/web-spatial/JSBCommand.swift +15 -3
  4. package/web-spatial/WebMsgCommand.swift +7 -3
  5. package/web-spatial/WebSpatialApp.swift +10 -10
  6. package/web-spatial/Window.swift +2 -2
  7. package/web-spatial/manager/AttachmentManager.swift +81 -0
  8. package/web-spatial/manager/JSBManager.swift +1 -2
  9. package/web-spatial/manifest.swift +1 -1
  10. package/web-spatial/model/SpatialApp.swift +59 -55
  11. package/web-spatial/model/SpatialScene.swift +97 -14
  12. package/web-spatial/model/Spatialized2DElement.swift +4 -5
  13. package/web-spatial/model/SpatializedStatic3DElement.swift +1 -1
  14. package/web-spatial/model/dynamic3d/SpatialComponent.swift +27 -27
  15. package/web-spatial/model/dynamic3d/SpatialEntity.swift +2 -2
  16. package/web-spatial/model/dynamic3d/SpatialMaterial.swift +15 -15
  17. package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +10 -10
  18. package/web-spatial/model/dynamic3d/SpatialModelResource.swift +1 -1
  19. package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +8 -8
  20. package/web-spatial/view/SpatialNavView.swift +52 -47
  21. package/web-spatial/view/SpatializedDynamic3DView.swift +68 -4
  22. package/web-spatial/view/SpatializedElementView.swift +28 -13
  23. package/web-spatial/view/SpatializedStatic3DView.swift +4 -6
  24. package/web-spatial/view/view-modifier/HideViewModifier.swift +2 -2
  25. package/web-spatial/webview/SpatialWebController.swift +27 -24
  26. package/web-spatial/webview/SpatialWebView.swift +5 -1
  27. package/web-spatial/webview/SpatialWebViewModel.swift +13 -7
  28. package/web-spatial.xcodeproj/project.pbxproj +13 -0
  29. package/web-spatialTests/NavigationCleanupTests.swift +33 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webspatial/platform-visionos",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Used to publish WebSpatial projects to Apple Vision Pro",
5
5
  "type": "commonjs",
6
6
  "engines": {
@@ -1,38 +1,38 @@
1
1
  class EventEmitter {
2
2
  private var listeners: [String: [(_ object: Any, _ data: Any) -> Void]] = [:]
3
3
 
4
- public func on(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void) {
4
+ func on(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void) {
5
5
  if listeners[event] == nil {
6
6
  listeners[event] = []
7
7
  }
8
8
  listeners[event]?.append(listener)
9
9
  }
10
10
 
11
- public func emit(event: String, data: Any) {
11
+ func emit(event: String, data: Any) {
12
12
  listeners[event]?.forEach { listener in
13
13
  listener(self, data)
14
14
  }
15
15
  }
16
16
 
17
- public func off(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void) {
17
+ func off(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void) {
18
18
  listeners[event]?.removeAll(where: { $0 as AnyObject === listener as AnyObject })
19
19
  }
20
-
21
- public func reset(){
20
+
21
+ func reset() {
22
22
  listeners = [:]
23
23
  }
24
24
  }
25
25
 
26
- protocol EventEmitterProtocol{
27
- var listeners: [String: [(_ object: Any, _ data: Any) -> Void]] {get set}
28
-
26
+ protocol EventEmitterProtocol {
27
+ var listeners: [String: [(_ object: Any, _ data: Any) -> Void]] { get set }
28
+
29
29
  mutating func on(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void)
30
30
  func emit(event: String, data: Any)
31
31
  mutating func off(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void)
32
32
  mutating func reset()
33
33
  }
34
34
 
35
- extension EventEmitterProtocol{
35
+ extension EventEmitterProtocol {
36
36
  mutating func on(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void) {
37
37
  if listeners[event] == nil {
38
38
  listeners[event] = []
@@ -49,8 +49,8 @@ extension EventEmitterProtocol{
49
49
  mutating func off(event: String, listener: @escaping (_ object: Any, _ data: Any) -> Void) {
50
50
  listeners[event]?.removeAll(where: { $0 as AnyObject === listener as AnyObject })
51
51
  }
52
-
53
- mutating func reset(){
52
+
53
+ mutating func reset() {
54
54
  listeners = [:]
55
55
  }
56
56
  }
@@ -197,8 +197,8 @@ struct UpdateSpatialized2DElementProperties: SpatializedElementProperties {
197
197
  let material: BackgroundMaterial?
198
198
  let cornerRadius: CornerRadius?
199
199
 
200
- // this value is used by previous WebSpatial code, keep it here only for Compatibility consideration
201
- // may delete it when we think it's not needed
200
+ /// this value is used by previous WebSpatial code, keep it here only for Compatibility consideration
201
+ /// may delete it when we think it's not needed
202
202
  let scrollEdgeInsetsMarginRight: Double?
203
203
  }
204
204
 
@@ -269,7 +269,7 @@ struct AddSpatializedElementToSpatialized2DElement: SpatialObjectCommand {
269
269
  let spatializedElementId: String
270
270
  }
271
271
 
272
- // incomming JSB data
272
+ /// incomming JSB data
273
273
  struct XSceneOptionsJSB: Codable {
274
274
  let defaultSize: Size?
275
275
  let type: SpatialScene.WindowStyle?
@@ -338,3 +338,15 @@ struct FocusSceneCommand: CommandDataProtocol {
338
338
  struct GetSpatialSceneStateCommand: CommandDataProtocol {
339
339
  static let commandType = "GetSpatialSceneState"
340
340
  }
341
+
342
+ struct UpdateAttachmentEntityCommand: CommandDataProtocol {
343
+ static let commandType = "UpdateAttachmentEntity"
344
+ let id: String
345
+ let position: [Float]?
346
+ let size: AttachmentSize?
347
+ }
348
+
349
+ struct AttachmentSize: Codable {
350
+ let width: Double
351
+ let height: Double
352
+ }
@@ -30,14 +30,14 @@ enum SpatialWebMsgType: String, Encodable {
30
30
  case objectdestroy
31
31
  }
32
32
 
33
- // notify Spatialized3DElement Container Cube, used for ref.current.getBoundingClientCube()
33
+ /// notify Spatialized3DElement Container Cube, used for ref.current.getBoundingClientCube()
34
34
  struct SpatiaizedContainerClientCube: Encodable {
35
35
  let type: SpatialWebMsgType = .cubeInfo
36
36
  let origin: Point3D
37
37
  let size: Size3D
38
38
  }
39
39
 
40
- // notify Spatialized3DElement Container Transform to SpatialScene, used for ref.current.convertToSpatialScene()
40
+ /// notify Spatialized3DElement Container Transform to SpatialScene, used for ref.current.convertToSpatialScene()
41
41
  struct SpatiaizedContainerTransform: Encodable {
42
42
  let type: SpatialWebMsgType = .transform
43
43
  let detail: AffineTransform3D
@@ -45,9 +45,11 @@ struct SpatiaizedContainerTransform: Encodable {
45
45
 
46
46
  struct WebSpatialTapGuestureEventDetail: Encodable {
47
47
  let location3D: Point3D
48
+ /// Global scene location (maps to clientX/clientY/clientZ on the web side).
49
+ let globalLocation3D: Point3D?
48
50
  }
49
51
 
50
- // notify SpatializedElement/SpatialEntity tapped
52
+ /// notify SpatializedElement/SpatialEntity tapped
51
53
  struct WebSpatialTapGuestureEvent: Encodable {
52
54
  let type: SpatialWebMsgType = .spatialtap
53
55
  let detail: WebSpatialTapGuestureEventDetail
@@ -55,6 +57,8 @@ struct WebSpatialTapGuestureEvent: Encodable {
55
57
 
56
58
  struct WebSpatialDragStartGuestureEventDetail: Encodable {
57
59
  let startLocation3D: Point3D
60
+ /// Global scene location for the drag start point.
61
+ let globalLocation3D: Point3D?
58
62
  }
59
63
 
60
64
  struct WebSpatialDragStartGuestureEvent: Encodable {
@@ -22,26 +22,22 @@ struct WebSpatialApp: App {
22
22
  @State var app = SpatialApp.Instance
23
23
 
24
24
  func getDefaultSize() -> CGSize {
25
- let ans = CGSize(
25
+ return CGSize(
26
26
  width: app
27
27
  .getSceneOptions().defaultSize!.width,
28
28
  height: app
29
29
  .getSceneOptions().defaultSize!.height
30
30
  )
31
-
32
- return ans
33
31
  }
34
32
 
35
33
  func getDefaultSize3D() -> Size3D {
36
- let ans = Size3D(
34
+ return Size3D(
37
35
  width: app
38
36
  .getSceneOptions().defaultSize!.width,
39
37
  height: app
40
38
  .getSceneOptions().defaultSize!.height,
41
39
  depth: app.getSceneOptions().defaultSize!.depth ?? 0
42
40
  )
43
-
44
- return ans
45
41
  }
46
42
 
47
43
  var body: some Scene {
@@ -74,13 +70,17 @@ struct WebSpatialApp: App {
74
70
  SpatialSceneView(spatialScene: spatialScene!)
75
71
  .frame(
76
72
  minWidth: getCGFloat(
77
- app.getSceneOptions(windowData)?.resizeRange?.minWidth),
73
+ app.getSceneOptions(windowData)?.resizeRange?.minWidth
74
+ ),
78
75
  maxWidth: getCGFloat(
79
- app.getSceneOptions(windowData)?.resizeRange?.maxWidth),
76
+ app.getSceneOptions(windowData)?.resizeRange?.maxWidth
77
+ ),
80
78
  minHeight: getCGFloat(
81
- app.getSceneOptions(windowData)?.resizeRange?.minHeight),
79
+ app.getSceneOptions(windowData)?.resizeRange?.minHeight
80
+ ),
82
81
  maxHeight: getCGFloat(
83
- app.getSceneOptions(windowData)?.resizeRange?.maxHeight)
82
+ app.getSceneOptions(windowData)?.resizeRange?.maxHeight
83
+ )
84
84
  )
85
85
  }
86
86
  defaultValue: {
@@ -1,7 +1,7 @@
1
1
  import Foundation
2
2
  import SwiftUI
3
3
 
4
- // Access window (https://stackoverflow.com/questions/60359808/how-to-access-own-window-within-swiftui-view/60359809#60359809)
4
+ /// Access window (https://stackoverflow.com/questions/60359808/how-to-access-own-window-within-swiftui-view/60359809#60359809)
5
5
  class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
6
6
  var window: UIWindow? // << contract of `UIWindowSceneDelegate`
7
7
 
@@ -11,7 +11,7 @@ class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
11
11
  window?.overrideUserInterfaceStyle = .light
12
12
  }
13
13
 
14
- // do memory cleanup after scene removed, otherwise windowContainer cannot destroy content after being dismissed
14
+ /// do memory cleanup after scene removed, otherwise windowContainer cannot destroy content after being dismissed
15
15
  func sceneDidDisconnect(_ scene: UIScene) {
16
16
  window = nil
17
17
  }
@@ -0,0 +1,81 @@
1
+ import Foundation
2
+ import SwiftUI
3
+
4
+ struct AttachmentInfo: Identifiable, Equatable {
5
+ let id: String
6
+ var parentEntityId: String
7
+ var position: SIMD3<Float>
8
+ var size: CGSize
9
+ var webViewModel: SpatialWebViewModel
10
+
11
+ static func == (lhs: AttachmentInfo, rhs: AttachmentInfo) -> Bool {
12
+ return lhs.id == rhs.id
13
+ }
14
+ }
15
+
16
+ @Observable
17
+ class AttachmentManager {
18
+ var attachments: [String: AttachmentInfo] = [:]
19
+
20
+ // TODO: AttachmentManager.remove() dispatches destroy() asynchronously while
21
+ // SwiftUI tears down the outgoing SpatialWebView from the RealityView's
22
+ // ForEach. Both happen on the main queue but ordering isn't guaranteed — if
23
+ // destroy() nils the controller before SwiftUI finishes teardown,
24
+ // getController() re-creates it with only `model` set, missing the four
25
+ // callback registrations from init(url:). Refactor to give attachments a
26
+ // dedicated view path (e.g. AttachmentWebView) that doesn't depend on
27
+ // SpatialWebViewModel's lazy re-init.
28
+ func create(
29
+ id: String,
30
+ parentEntityId: String,
31
+ position: SIMD3<Float>,
32
+ size: CGSize
33
+ ) -> AttachmentInfo {
34
+ let webViewModel = SpatialWebViewModel(url: nil)
35
+ webViewModel.setBackgroundTransparent(true)
36
+ // webViewModel.scrollEnabled = false
37
+
38
+ let info = AttachmentInfo(
39
+ id: id,
40
+ parentEntityId: parentEntityId,
41
+ position: position,
42
+ size: size,
43
+ webViewModel: webViewModel
44
+ )
45
+ attachments[id] = info
46
+ return info
47
+ }
48
+
49
+ func update(id: String, position: SIMD3<Float>?, size: CGSize?) {
50
+ guard var info = attachments[id] else { return }
51
+ if let position = position {
52
+ info.position = position
53
+ }
54
+ if let size = size {
55
+ info.size = size
56
+ }
57
+ attachments[id] = info
58
+ }
59
+
60
+ func remove(id: String) {
61
+ if let info = attachments.removeValue(forKey: id) {
62
+ DispatchQueue.main.async {
63
+ info.webViewModel.destroy()
64
+ }
65
+ }
66
+ }
67
+
68
+ func get(id: String) -> AttachmentInfo? {
69
+ return attachments[id]
70
+ }
71
+
72
+ func destroyAll() {
73
+ let toDestroy = Array(attachments.values)
74
+ attachments.removeAll()
75
+ DispatchQueue.main.async {
76
+ for info in toDestroy {
77
+ info.webViewModel.destroy()
78
+ }
79
+ }
80
+ }
81
+ }
@@ -123,8 +123,7 @@ class JSBManager {
123
123
  if cmdContent == nil {
124
124
  return nil
125
125
  }
126
- let concreteData = try decoder.decode(type.self, from: cmdContent!.data(using: .utf8)!)
127
- return concreteData
126
+ return try decoder.decode(type.self, from: cmdContent!.data(using: .utf8)!)
128
127
  }
129
128
 
130
129
  private func typeof(for key: String) -> CommandDataProtocol.Type? {
@@ -66,7 +66,7 @@ struct PWAManager: Codable {
66
66
  return url.starts(with: scope)
67
67
  }
68
68
 
69
- // web+spatial://test
69
+ /// web+spatial://test
70
70
  func checkInDeeplink(url: String) -> String {
71
71
  var linkUrl: String = url
72
72
  for item in protocol_handlers {
@@ -3,10 +3,10 @@ import SwiftUI
3
3
 
4
4
  let logger = Logger()
5
5
 
6
- // To load a local path, remove http:// eg. "static-web/"
6
+ /// To load a local path, remove http:// eg. "static-web/"
7
7
  let nativeAPIVersion = pwaManager.getVersion()
8
8
 
9
- // start URL
9
+ /// start URL
10
10
  let startURL = pwaManager.start_url
11
11
 
12
12
  let DefaultPlainWindowContainerSize = CGSize(width: 1280, height: 720)
@@ -37,7 +37,6 @@ struct Size: Codable {
37
37
  var depth: Double?
38
38
  }
39
39
 
40
-
41
40
  extension SceneOptions {
42
41
  init(_ options: XSceneOptionsJSB) {
43
42
  defaultSize = Size(
@@ -47,7 +46,7 @@ extension SceneOptions {
47
46
  )
48
47
  windowResizability = decodeWindowResizability(nil)
49
48
  resizeRange = options.resizability
50
- /// volume only
49
+ // volume only
51
50
  worldScaling = options.worldScaling?.toSDK ?? .automatic
52
51
  worldAlignment = options.worldAlignment?.toSDK ?? .automatic
53
52
  baseplateVisibility = options.baseplateVisibility?.toSDK ?? .automatic
@@ -56,31 +55,44 @@ extension SceneOptions {
56
55
 
57
56
  func decodeWindowResizability(_ windowResizability: String?) -> WindowResizability {
58
57
  switch windowResizability {
59
- case "automatic":
60
- return .automatic
61
- case "contentSize":
62
- return .contentSize
63
- case "contentMinSize":
64
- return .contentMinSize
65
- default:
66
- return .automatic
58
+ case "automatic":
59
+ return .automatic
60
+ case "contentSize":
61
+ return .contentSize
62
+ case "contentMinSize":
63
+ return .contentMinSize
64
+ default:
65
+ return .automatic
67
66
  }
68
67
  }
69
68
 
70
69
  @Observable
71
70
  class SpatialApp {
72
71
  private var scenes = [String: SpatialScene]()
73
-
74
- // delegate properties to pwaManager
75
- var name: String { pwaManager.name }
76
- var scope: String { pwaManager.scope }
77
- var displayMode: PWADisplayMode { pwaManager.display }
78
- var version: String { pwaManager.getVersion() }
79
- var startURL: String { pwaManager.start_url }
80
-
81
- // used to cache scene config
82
- private var sceneOptions: SceneOptions
83
72
 
73
+ /// delegate properties to pwaManager
74
+ var name: String {
75
+ pwaManager.name
76
+ }
77
+
78
+ var scope: String {
79
+ pwaManager.scope
80
+ }
81
+
82
+ var displayMode: PWADisplayMode {
83
+ pwaManager.display
84
+ }
85
+
86
+ var version: String {
87
+ pwaManager.getVersion()
88
+ }
89
+
90
+ var startURL: String {
91
+ pwaManager.start_url
92
+ }
93
+
94
+ /// used to cache scene config
95
+ private var sceneOptions: SceneOptions
84
96
 
85
97
  static let Instance: SpatialApp = .init()
86
98
 
@@ -90,14 +102,12 @@ class SpatialApp {
90
102
 
91
103
  Logger.initLogger()
92
104
 
93
- sceneOptions = SceneOptions(pwaManager.mainScene);
94
-
95
- print("plainSceneOptions",sceneOptions)
96
-
105
+ sceneOptions = SceneOptions(pwaManager.mainScene)
106
+
107
+ print("plainSceneOptions", sceneOptions)
108
+
97
109
  logger.debug("WebSpatial App Started -------- rootURL: " + startURL)
98
110
  }
99
-
100
-
101
111
 
102
112
  func createScene(_ url: String, _ style: SpatialScene.WindowStyle, _ state: SpatialScene.SceneStateKind, _ sceneOptions: SceneOptions? = nil) -> SpatialScene {
103
113
  var scene = SpatialScene(url, style, state, sceneOptions)
@@ -105,15 +115,14 @@ class SpatialApp {
105
115
  scene
106
116
  .on(event: SpatialObject.Events.Destroyed.rawValue, listener: onSceneDestroyed)
107
117
 
108
-
109
118
  return scene
110
119
  }
111
-
120
+
112
121
  private func onSceneDestroyed(_ object: Any, _ data: Any) {
113
122
  var spatialObject = object as! SpatialObject
114
123
  spatialObject
115
124
  .off(event: SpatialObject.Events.Destroyed.rawValue, listener: onSceneDestroyed)
116
-
125
+
117
126
  scenes.removeValue(forKey: spatialObject.id)
118
127
  }
119
128
 
@@ -121,63 +130,58 @@ class SpatialApp {
121
130
  return scenes[id]
122
131
  }
123
132
 
124
-
125
133
  func getSceneOptions() -> SceneOptions {
126
134
  return sceneOptions
127
135
  }
128
-
129
- func getSceneOptions(_ sceneId:String) -> SceneOptions? {
136
+
137
+ func getSceneOptions(_ sceneId: String) -> SceneOptions? {
130
138
  let spatialScene = getScene(sceneId)
131
139
  return spatialScene?.sceneConfig
132
140
  }
133
-
134
-
135
- // used form window.open logic
136
- public func openWindowGroup(
141
+
142
+ /// used form window.open logic
143
+ func openWindowGroup(
137
144
  _ targetSpatialScene: SpatialScene,
138
145
  _ sceneData: SceneOptions
139
146
  ) {
140
147
  if let activeScene = firstActiveScene {
141
148
  // cache scene config
142
149
  sceneOptions = sceneData
143
-
144
- DispatchQueue.main.async() {
150
+
151
+ DispatchQueue.main.async {
145
152
  activeScene.openWindowData.send(targetSpatialScene.id)
146
153
  }
147
-
148
154
  }
149
155
  }
150
-
151
- public func closeWindowGroup(_ targetSpatialScene: SpatialScene) {
156
+
157
+ func closeWindowGroup(_ targetSpatialScene: SpatialScene) {
152
158
  if let activeScene = firstActiveScene {
153
159
  activeScene.closeWindowData
154
160
  .send(targetSpatialScene.id)
155
161
  }
156
162
  }
157
-
158
- // used form window.open logic with loading ui
159
- public func openLoadingUI(_ targetSpatialScene: SpatialScene,_ open: Bool) {
163
+
164
+ /// used form window.open logic with loading ui
165
+ func openLoadingUI(_ targetSpatialScene: SpatialScene, _ open: Bool) {
160
166
  let lwgdata = XLoadingViewData(
161
167
  sceneID: targetSpatialScene.id,
162
168
  method: open ? .show : .hide,
163
169
  windowStyle: nil
164
170
  )
165
-
171
+
166
172
  if let activeScene = firstActiveScene {
167
173
  activeScene.setLoadingWindowData.send(lwgdata)
168
174
  }
169
175
  }
170
-
176
+
171
177
  private var firstActiveScene: SpatialScene? {
172
- get {
173
- let activeKV = scenes.first() { kv in
174
- kv.value.state == .visible
175
- }
176
- return (activeKV?.value)
178
+ let activeKV = scenes.first { kv in
179
+ kv.value.state == .visible
177
180
  }
181
+ return (activeKV?.value)
178
182
  }
179
-
180
- public func focusScene(_ targetSpatialScene: SpatialScene) {
183
+
184
+ func focusScene(_ targetSpatialScene: SpatialScene) {
181
185
  // only work when fully visible
182
186
  if targetSpatialScene.state != .visible {
183
187
  return