@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,39 +1,39 @@
1
- import SwiftUI
2
1
  import RealityKit
2
+ import SwiftUI
3
3
 
4
4
  @Observable
5
5
  class SpatialMaterial: SpatialObject {
6
6
  let type: SpatialMaterialType
7
-
8
- internal var _resource:RealityKit.Material? = nil
9
- var resource:RealityKit.Material? {
7
+
8
+ var _resource: RealityKit.Material?
9
+ var resource: RealityKit.Material? {
10
10
  _resource
11
11
  }
12
-
13
- init(_ _type:SpatialMaterialType){
12
+
13
+ init(_ _type: SpatialMaterialType) {
14
14
  type = _type
15
15
  super.init()
16
16
  }
17
-
17
+
18
18
  override func onDestroy() {
19
19
  _resource = nil
20
20
  }
21
21
  }
22
22
 
23
23
  @Observable
24
- class SpatialUnlitMaterial: SpatialMaterial{
25
- let color:UIColor
26
-
27
- init(_ color:String, _ texture:TextureResource? = nil, _ transparent:Bool = true, _ opacity:Float = 1){
28
- self.color = UIColor.init(Color(hex: color))
24
+ class SpatialUnlitMaterial: SpatialMaterial {
25
+ let color: UIColor
26
+
27
+ init(_ color: String, _ texture: TextureResource? = nil, _ transparent: Bool = true, _ opacity: Float = 1) {
28
+ self.color = UIColor(Color(hex: color))
29
29
  super.init(.UnlitMaterial)
30
30
  var mat = UnlitMaterial()
31
- mat.color = .init(tint:UIColor(Color.init(hex: color)), texture: texture != nil ? .init(texture!) : nil)
31
+ mat.color = .init(tint: UIColor(Color(hex: color)), texture: texture != nil ? .init(texture!) : nil)
32
32
  mat.blending = transparent ? .transparent(opacity: .init(scale: opacity)) : .opaque
33
33
  _resource = mat
34
34
  }
35
35
  }
36
36
 
37
- enum SpatialMaterialType: String{
38
- case UnlitMaterial = "UnlitMaterial"
37
+ enum SpatialMaterialType: String {
38
+ case UnlitMaterial
39
39
  }
@@ -1,32 +1,32 @@
1
- import SwiftUI
2
1
  import RealityKit
2
+ import SwiftUI
3
3
 
4
4
  @Observable
5
- class SpatialModelEntity: SpatialEntity{
6
- private var modelEntity:Entity? = nil
7
- required init(_ modelResource:SpatialModelResource, _ _name:String = ""){
5
+ class SpatialModelEntity: SpatialEntity {
6
+ private var modelEntity: Entity?
7
+ required init(_ modelResource: SpatialModelResource, _ _name: String = "") {
8
8
  super.init(_name)
9
9
  modelEntity = modelResource.resource
10
10
  addChild(modelEntity!)
11
11
  generateCollisionShapes(recursive: true)
12
12
  }
13
-
13
+
14
14
  required init() {
15
15
  super.init()
16
16
  }
17
-
18
- override internal func onDestroy(){
17
+
18
+ override func onDestroy() {
19
19
  super.onDestroy()
20
- if let modelEntity = self.modelEntity{
20
+ if let modelEntity = modelEntity {
21
21
  removeChild(modelEntity)
22
22
  }
23
23
  modelEntity = nil
24
24
  }
25
-
25
+
26
26
  enum CodingKeys: String, CodingKey {
27
27
  case id, name, isDestroyed, children, components, model
28
28
  }
29
-
29
+
30
30
  override func encode(to encoder: any Encoder) throws {
31
31
  var container = encoder.container(keyedBy: CodingKeys.self)
32
32
  try container.encode(spatialId, forKey: .id)
@@ -3,7 +3,7 @@ import SwiftUI
3
3
 
4
4
  @Observable
5
5
  class SpatialModelResource: SpatialObject {
6
- var _resource: Entity? = nil
6
+ var _resource: Entity?
7
7
  var resource: Entity? {
8
8
  _resource
9
9
  }
@@ -1,18 +1,18 @@
1
- import SwiftUI
2
1
  import RealityKit
2
+ import SwiftUI
3
3
 
4
4
  @Observable
5
- class SpatialTextureResource:SpatialObject {
6
- internal var _resource:TextureResource? = nil
7
- var resource:TextureResource? {
5
+ class SpatialTextureResource: SpatialObject {
6
+ var _resource: TextureResource?
7
+ var resource: TextureResource? {
8
8
  _resource
9
9
  }
10
-
11
- override init(_ url:String){
10
+
11
+ override init(_ url: String) {
12
12
  super.init()
13
13
  }
14
-
15
- override internal func onDestroy() {
14
+
15
+ override func onDestroy() {
16
16
  _resource = nil
17
17
  }
18
18
  }
@@ -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
- // window scene only resize logic
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
  }
@@ -21,7 +21,6 @@ struct NavDivider: View {
21
21
  }
22
22
  }
23
23
 
24
-
25
24
  struct NavButton: View {
26
25
  var action: () -> Void
27
26
  var children: Image
@@ -55,21 +54,18 @@ struct NavButton: View {
55
54
  }
56
55
  }
57
56
 
58
-
59
57
  struct SpatialNavView: View {
60
58
  static let navHeight: CGFloat = SpatialScene.navHeight
61
59
  static let minWidth: CGFloat = 400
62
60
  var spatialScene: SpatialScene
63
- var model:SpatialWebViewModel? {
64
- get {
65
- return spatialScene.spatialWebViewModel
66
- }
61
+ var model: SpatialWebViewModel? {
62
+ return spatialScene.spatialWebViewModel
67
63
  }
64
+
68
65
  var url: String {
69
- get {
70
- return spatialScene.url
71
- }
66
+ return spatialScene.url
72
67
  }
68
+
73
69
  @State var navWidth: CGFloat = 0
74
70
  @State private var showCopyTip = false
75
71
  @State private var contentHeight: CGFloat = 60
@@ -81,36 +77,40 @@ struct SpatialNavView: View {
81
77
  @State private var showNav: Double = 0
82
78
  @State private var showUrl: Double = 0
83
79
  @Namespace var hoverNamespace
84
-
85
- func checkButtonState(){
80
+
81
+ func checkButtonState() {
86
82
  canGoBack = model!.getController().webview!.canGoBack
87
83
  canGoForward = model!.getController().webview!.canGoForward
88
84
  }
89
-
85
+
90
86
  func goBack() {
87
+ spatialScene.resetForNavigation()
91
88
  model?.getController().webview?.goBack()
92
89
  }
93
-
90
+
94
91
  func goForward() {
92
+ spatialScene.resetForNavigation()
95
93
  model?.getController().webview?.goForward()
96
94
  }
97
-
95
+
98
96
  func reload() {
97
+ spatialScene.resetForNavigation()
99
98
  model?.getController().webview?.reload()
100
99
  }
101
-
100
+
102
101
  func navigateToURL(url: URL) {
102
+ spatialScene.resetForNavigation()
103
103
  model?.load(url.absoluteString)
104
104
  }
105
-
105
+
106
106
  func getURL() -> URL? {
107
107
  return model?.getController().webview?.url
108
108
  }
109
-
109
+
110
110
  @State var canGoBack: Bool = false
111
-
111
+
112
112
  @State var canGoForward: Bool = false
113
-
113
+
114
114
  var navHoverGroup: HoverEffectGroup {
115
115
  HoverEffectGroup(hoverNamespace)
116
116
  }
@@ -126,13 +126,15 @@ struct SpatialNavView: View {
126
126
  action: { self.goBack()
127
127
  },
128
128
  children: Image("arrow_left"),
129
- clearBackGround: true)
129
+ clearBackGround: true
130
+ )
130
131
  .disabled(!(self.canGoBack))
131
132
  NavButton(
132
133
  action: { self.goForward()
133
134
  },
134
135
  children: Image("arrow_right"),
135
- clearBackGround: true)
136
+ clearBackGround: true
137
+ )
136
138
  .disabled(!(self.canGoBack))
137
139
  NavButton(action: { self.reload() }, children: Image("refresh"), clearBackGround: true)
138
140
  NavDivider()
@@ -156,12 +158,14 @@ struct SpatialNavView: View {
156
158
  NavButton(
157
159
  action: { self.goBack()
158
160
  },
159
- children: Image("arrow_left"))
161
+ children: Image("arrow_left")
162
+ )
160
163
  .disabled(!(self.canGoBack))
161
164
  NavButton(
162
165
  action: { self.goForward()
163
166
  },
164
- children: Image("arrow_right"))
167
+ children: Image("arrow_right")
168
+ )
165
169
  .disabled(!(self.canGoBack))
166
170
  NavButton(action: { self.reload() }, children: Image("refresh"))
167
171
  NavDivider()
@@ -187,14 +191,14 @@ struct SpatialNavView: View {
187
191
  self.getURL()?.absoluteString ?? ""
188
192
  )
189
193
  )
190
- .lineLimit(1)
191
- .textSelection(.enabled)
192
- .padding(12)
193
- .frame(minWidth: 200)
194
- .frame(maxWidth: 500)
195
- .frame(height: 44)
196
- .background(.black)
197
- .cornerRadius(100)
194
+ .lineLimit(1)
195
+ .textSelection(.enabled)
196
+ .padding(12)
197
+ .frame(minWidth: 200)
198
+ .frame(maxWidth: 500)
199
+ .frame(height: 44)
200
+ .background(.black)
201
+ .cornerRadius(100)
198
202
  NavButton(action: {
199
203
  UIPasteboard.general.string = self.getURL()?.absoluteString ?? ""
200
204
  showCopyTip = true
@@ -204,21 +208,22 @@ struct SpatialNavView: View {
204
208
  withAnimation(.easeInOut(duration: 0.5)) { showUrl = 0; showNav = 1 }
205
209
  }, children: Image("copy"))
206
210
  NavButton(
207
- action: {
208
- print("open browser")
209
- UIApplication.shared
210
- .open(
211
- URL(
212
- string: url.count > 0 ? url : (
213
- self.getURL()?.absoluteString ?? ""
211
+ action: {
212
+ print("open browser")
213
+ UIApplication.shared
214
+ .open(
215
+ URL(
216
+ string: url.count > 0 ? url : (
217
+ self.getURL()?.absoluteString ?? ""
218
+ )
219
+ )!,
220
+ options: [:],
221
+ completionHandler: nil
222
+ )
223
+ withAnimation(.easeInOut(duration: 0.5)) { showUrl = 0; showNav = 1 }
224
+ },
225
+ children: Image("browser")
214
226
  )
215
- )!,
216
- options: [:],
217
- completionHandler: nil
218
- )
219
- withAnimation(.easeInOut(duration: 0.5)) { showUrl = 0; showNav = 1 }
220
- },
221
- children: Image("browser"))
222
227
  NavButton(action: { withAnimation(.easeInOut(duration: 0.5)) { showUrl = 0; showNav = 1 } }, children: Image("close"))
223
228
  }
224
229
  .popover(isPresented: $showCopyTip) {
@@ -232,8 +237,8 @@ action: {
232
237
  .cornerRadius(100)
233
238
  .opacity(showUrl)
234
239
  }
235
- .onAppear(){
236
- model?.addStateListener(.didFinishLoad){
240
+ .onAppear {
241
+ model?.addStateListener(.didFinishLoad) {
237
242
  self.checkButtonState()
238
243
  }
239
244
  }
@@ -16,17 +16,28 @@ struct SpatializedDynamic3DView: View {
16
16
  SpatialTapGesture(count: 1).targetedToAnyEntity()
17
17
  .onEnded { value in
18
18
  if let entity = value.entity as? SpatialEntity {
19
- spatialScene.sendWebMsg(entity.spatialId, WebSpatialTapGuestureEvent(detail: WebSpatialTapGuestureEventDetail(location3D: value.location3D)))
19
+ // Convert local gesture coordinates into world (global) coordinates via RealityKit.
20
+ let globalLocation3D = entity.convert(position: SIMD3<Float>(Float(value.location3D.x), Float(value.location3D.y), Float(value.location3D.z)), to: nil)
21
+ let globalPoint3D = Point3D(x: Double(globalLocation3D.x), y: Double(globalLocation3D.y), z: Double(globalLocation3D.z))
22
+
23
+ spatialScene.sendWebMsg(entity.spatialId, WebSpatialTapGuestureEvent(detail: WebSpatialTapGuestureEventDetail(location3D: value.location3D, globalLocation3D: globalPoint3D)))
20
24
  } else {
21
25
  if let spatialEntity = SpatialEntity.findNearestParent(entity: value.entity) {
22
- spatialScene.sendWebMsg(spatialEntity.spatialId, WebSpatialTapGuestureEvent(detail: WebSpatialTapGuestureEventDetail(location3D: value.location3D)))
26
+ // Convert using the hit entity's coordinate space, then forward to the nearest SpatialEntity.
27
+ let globalLocation3D = value.entity.convert(
28
+ position: SIMD3<Float>(Float(value.location3D.x), Float(value.location3D.y), Float(value.location3D.z)),
29
+ to: nil
30
+ )
31
+ let globalPoint3D = Point3D(x: Double(globalLocation3D.x), y: Double(globalLocation3D.y), z: Double(globalLocation3D.z))
32
+
33
+ spatialScene.sendWebMsg(spatialEntity.spatialId, WebSpatialTapGuestureEvent(detail: WebSpatialTapGuestureEventDetail(location3D: value.location3D, globalLocation3D: globalPoint3D)))
23
34
  }
24
35
  }
25
36
  }
26
37
  }
27
38
 
28
39
  var rotate3dEvent: some Gesture {
29
- RotateGesture3D().targetedToAnyEntity().onChanged { value in
40
+ makeRotateGesture3D().targetedToAnyEntity().onChanged { value in
30
41
  // Always forward rotate gesture events to JS
31
42
  if let entity = value.entity as? SpatialEntity {
32
43
  let gestureEvent = WebSpatialRotateGuestureEvent(
@@ -51,6 +62,21 @@ struct SpatializedDynamic3DView: View {
51
62
  }
52
63
  }
53
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
+
54
80
  var magnifyEvent: some Gesture {
55
81
  MagnifyGesture().targetedToAnyEntity().onChanged { value in
56
82
  // Always forward magnify gesture events to JS
@@ -76,9 +102,16 @@ struct SpatializedDynamic3DView: View {
76
102
  // Always forward drag gesture events to JS
77
103
  if let entity = value.entity as? SpatialEntity {
78
104
  if !isDrag {
105
+ let globalStartLocation3D = value.entity.convert(
106
+ position: SIMD3<Float>(Float(value.startLocation3D.x), Float(value.startLocation3D.y), Float(value.startLocation3D.z)),
107
+ to: nil
108
+ )
109
+ let globalStartPoint3D = Point3D(x: Double(globalStartLocation3D.x), y: Double(globalStartLocation3D.y), z: Double(globalStartLocation3D.z))
110
+
79
111
  let startEvent = WebSpatialDragStartGuestureEvent(
80
112
  detail: .init(
81
- startLocation3D: value.startLocation3D
113
+ startLocation3D: value.startLocation3D,
114
+ globalLocation3D: globalStartPoint3D
82
115
  )
83
116
  )
84
117
  spatialScene.sendWebMsg(entity.spatialId, startEvent)
@@ -101,13 +134,63 @@ struct SpatializedDynamic3DView: View {
101
134
  }
102
135
 
103
136
  var body: some View {
104
- RealityView(make: { content in
137
+ RealityView(make: { content, attachments in
105
138
  let rootEntity = spatializedDynamic3DElement.getRoot()
106
139
  content.add(rootEntity)
140
+ spatializedDynamic3DElement.setViewContent(content)
141
+
142
+ // Add existing attachments on initial creation
143
+ for (_, info) in spatialScene.attachmentManager.attachments {
144
+ if let attachmentEntity = attachments.entity(for: info.id) {
145
+ attachmentEntity.position = info.position
146
+ if let parentEntity = findSpatialEntity(info.parentEntityId) {
147
+ parentEntity.addChild(attachmentEntity)
148
+ } else {
149
+ rootEntity.addChild(attachmentEntity)
150
+ }
151
+ }
152
+ }
153
+ }, update: { _, attachments in
154
+ let rootEntity = spatializedDynamic3DElement.getRoot()
155
+ // Update attachment positions and parenting
156
+ for (_, info) in spatialScene.attachmentManager.attachments {
157
+ if let attachmentEntity = attachments.entity(for: info.id) {
158
+ attachmentEntity.position = info.position
159
+ // Re-parent if not already under the correct parent
160
+ if let parentEntity = findSpatialEntity(info.parentEntityId) {
161
+ if attachmentEntity.parent != parentEntity {
162
+ parentEntity.addChild(attachmentEntity)
163
+ }
164
+ } else {
165
+ // Parent entity might have been destroyed; fall back to root.
166
+ if attachmentEntity.parent != rootEntity {
167
+ rootEntity.addChild(attachmentEntity)
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }, attachments: {
173
+ ForEach(Array(spatialScene.attachmentManager.attachments.values)) { info in
174
+ Attachment(id: info.id) {
175
+ info.webViewModel.getView()
176
+ .frame(
177
+ width: info.size.width,
178
+ height: info.size.height
179
+ )
180
+ }
181
+ }
107
182
  })
108
183
  .simultaneousGesture(spatialTapEvent)
109
184
  .simultaneousGesture(rotate3dEvent)
110
185
  .simultaneousGesture(dragEvent)
111
186
  .simultaneousGesture(magnifyEvent)
187
+ .onDisappear {
188
+ spatializedDynamic3DElement.setViewContent(nil)
189
+ }
190
+ }
191
+
192
+ private func findSpatialEntity(_ spatialId: String) -> SpatialEntity? {
193
+ // Look up the SpatialEntity from the SpatialScene's spatial object registry
194
+ return spatialScene.findSpatialObject(spatialId)
112
195
  }
113
196
  }