@webspatial/platform-visionos 0.0.1

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/Packages/RealityKitContent/.build/workspace-state.json +7 -0
  3. package/Packages/RealityKitContent/.swiftpm/xcode/xcuserdata/bytedance.xcuserdatad/xcschemes/xcschememanagement.plist +14 -0
  4. package/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json +11 -0
  5. package/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json +112 -0
  6. package/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata +17 -0
  7. package/Packages/RealityKitContent/Package.swift +27 -0
  8. package/Packages/RealityKitContent/README.md +3 -0
  9. package/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Immersive.usda +50 -0
  10. package/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda +216 -0
  11. package/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda +59 -0
  12. package/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift +4 -0
  13. package/package.json +27 -0
  14. package/web-spatial/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json +12 -0
  15. package/web-spatial/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json +6 -0
  16. package/web-spatial/Assets.xcassets/AppIcon.solidimagestack/Contents.json +17 -0
  17. package/web-spatial/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json +12 -0
  18. package/web-spatial/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json +6 -0
  19. package/web-spatial/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json +12 -0
  20. package/web-spatial/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json +6 -0
  21. package/web-spatial/Assets.xcassets/Contents.json +6 -0
  22. package/web-spatial/Info.plist +33 -0
  23. package/web-spatial/Preview Content/Preview Assets.xcassets/Contents.json +6 -0
  24. package/web-spatial/libs/EventEmitter.swift +32 -0
  25. package/web-spatial/libs/SpatialComponent.swift +31 -0
  26. package/web-spatial/libs/SpatialEntity.swift +179 -0
  27. package/web-spatial/libs/SpatialInputComponent.swift +26 -0
  28. package/web-spatial/libs/SpatialMeshResource.swift +19 -0
  29. package/web-spatial/libs/SpatialModel3DComponent.swift +51 -0
  30. package/web-spatial/libs/SpatialModelComponent.swift +32 -0
  31. package/web-spatial/libs/SpatialObject.swift +144 -0
  32. package/web-spatial/libs/SpatialPhysicallyBasedMaterial.swift +19 -0
  33. package/web-spatial/libs/SpatialViewComponent.swift +15 -0
  34. package/web-spatial/libs/SpatialWindowComponent.swift +443 -0
  35. package/web-spatial/libs/SpatialWindowContainer.swift +149 -0
  36. package/web-spatial/libs/Utils/CommandManager.swift +800 -0
  37. package/web-spatial/libs/Utils/Logger.swift +36 -0
  38. package/web-spatial/libs/Utils/SceneManager.swift +108 -0
  39. package/web-spatial/libs/Utils/WindowContainerMgr.swift +117 -0
  40. package/web-spatial/libs/json/JsonParser.swift +52 -0
  41. package/web-spatial/libs/uiKitDelegate/Window.swift +34 -0
  42. package/web-spatial/libs/webView/UpdateSystem.swift +33 -0
  43. package/web-spatial/libs/webView/backend/NativeWebView.swift +319 -0
  44. package/web-spatial/libs/webView/manifest.swift +92 -0
  45. package/web-spatial/static-web/index.html +9 -0
  46. package/web-spatial/views/HideViewModifier.swift +17 -0
  47. package/web-spatial/views/ImmersiveView.swift +24 -0
  48. package/web-spatial/views/LoadingView.swift +29 -0
  49. package/web-spatial/views/MaterialWithBorderCornerModifier.swift +82 -0
  50. package/web-spatial/views/OpenDismissHandlerUI.swift +52 -0
  51. package/web-spatial/views/PlainWindowContainerView.swift +84 -0
  52. package/web-spatial/views/SpatialModel3DView.swift +193 -0
  53. package/web-spatial/views/SpatialViewUI.swift +168 -0
  54. package/web-spatial/views/SpatialWebViewUI.swift +193 -0
  55. package/web-spatial/views/VolumetricWindowContainerView.swift +38 -0
  56. package/web-spatial/views/ui/NavView.swift +125 -0
  57. package/web-spatial/web_spatialApp.swift +158 -0
  58. package/web-spatial.xcodeproj/project.pbxproj +686 -0
  59. package/web-spatial.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  60. package/web-spatial.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  61. package/web-spatial.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +5 -0
  62. package/web-spatial.xcodeproj/project.xcworkspace/xcuserdata/bytedance.xcuserdatad/WorkspaceSettings.xcsettings +14 -0
  63. package/web-spatial.xcodeproj/xcshareddata/xcschemes/web-spatial.xcscheme +115 -0
  64. package/web-spatialTests/web_spatialTests.swift +34 -0
@@ -0,0 +1,84 @@
1
+ //
2
+ // PlainWindowContainerView.swift
3
+ // web-spatial
4
+ //
5
+ // Created by ByteDance on 5/9/24.
6
+ //
7
+
8
+ import RealityKit
9
+ import SwiftUI
10
+
11
+ struct PlainWindowContainerView: View {
12
+ @EnvironmentObject private var sceneDelegate: SceneDelegate
13
+ @Environment(SpatialWindowContainer.self) private var windowContainerContent: SpatialWindowContainer
14
+
15
+ @State private var windowResizeInProgress = false
16
+ @State private var timer: Timer?
17
+
18
+ private func setSize(size: CGSize) {
19
+ sceneDelegate.window?.windowScene?.requestGeometryUpdate(.Vision(size: size))
20
+ }
21
+
22
+ var body: some View {
23
+ OpenDismissHandlerUI().environment(windowContainerContent).onDisappear {
24
+ windowContainerContent.destroy()
25
+ }
26
+
27
+ let rootEntity = windowContainerContent.getEntities().filter {
28
+ $0.value.getComponent(SpatialWindowComponent.self) != nil && $0.value.coordinateSpace == .ROOT
29
+ }.first?.value
30
+
31
+ GeometryReader { proxy3D in
32
+ ZStack {
33
+ if let e = rootEntity {
34
+ let _ = e.forceUpdate ? 0 : 0
35
+ let x = proxy3D.size.width / 2
36
+ let y = proxy3D.size.height / 2
37
+ let z = CGFloat(e.modelEntity.position.z)
38
+ let width = proxy3D.size.width
39
+ let height = proxy3D.size.height
40
+
41
+ if windowResizeInProgress {
42
+ VStack {}.frame(width: width, height: height).glassBackgroundEffect().padding3D(.front, -100_000)
43
+ .position(x: x, y: y)
44
+ .offset(z: z)
45
+ } else {
46
+ // Avoid showing webview until its loading completes
47
+ let wc = e.getComponent(SpatialWindowComponent.self)
48
+ let didFinishFirstLoad = wc != nil ? wc!.didFinishFirstLoad : false
49
+
50
+ SpatialWebViewUI().environment(e)
51
+ .frame(width: width, height: height).padding3D(.front, -100_000)
52
+ .rotation3DEffect(Rotation3D(simd_quatf(ix: e.modelEntity.orientation.vector.x, iy: e.modelEntity.orientation.vector.y, iz: e.modelEntity.orientation.vector.z, r: e.modelEntity.orientation.vector.w)))
53
+ .position(x: x, y: y)
54
+ .offset(z: z)
55
+ .opacity(didFinishFirstLoad ? 1.0 : 0.0)
56
+ .animation(.linear(duration: 0.2), value: didFinishFirstLoad)
57
+ }
58
+ }
59
+ }
60
+ .onReceive(windowContainerContent.setSize) { newSize in
61
+ setSize(size: newSize)
62
+ }
63
+ .onChange(of: proxy3D.size) {
64
+ // WkWebview has an issue where it doesn't resize while the swift window is resized
65
+ // Treid to call didMoveToWindow to force redraw to occur but that seemed to cause rendering artifacts so that solution was rejected
66
+ // Now we use a windowResizeInProgress state to hide the webview (by removoving from the view) and other content (using opacity).
67
+ // After resize is completed the webview is added back to the page which causes a redraw at the correct dimensions/position
68
+ if let wv = rootEntity?.getComponent(SpatialWindowComponent.self) {
69
+ windowResizeInProgress = true
70
+ if timer != nil {
71
+ timer!.invalidate()
72
+ }
73
+ // If we don't detect resolution change after x seconds we treat the resize as complete
74
+ timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in
75
+ windowResizeInProgress = false
76
+ }
77
+
78
+ // Trigger resize in the webview's body width and fire a window resize event to get the JS on the page to update state while dragging occurs
79
+ wv.evaluateJS(js: "var tempWidth_ = document.body.style.width;document.body.style.width='" + String(Float(proxy3D.size.width)) + "px'; window.dispatchEvent(new Event('resize'));")
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
@@ -0,0 +1,193 @@
1
+ //
2
+ // SpatialModel3DView.swift
3
+ // web-spatial
4
+ //
5
+ // Created by ByteDance on 1/21/25.
6
+ //
7
+ import RealityKit
8
+ import SwiftUI
9
+
10
+ class SpatialModel3DViewGestureData {
11
+ // for dragging state
12
+ var isDragging = false
13
+ }
14
+
15
+ struct SpatialModel3DView: View {
16
+ @Environment(SpatialEntity.self) var e: SpatialEntity
17
+ var parentYOffset = Float(0.0)
18
+
19
+ @State private var gestureData = SpatialModel3DViewGestureData()
20
+
21
+ var drag: some Gesture {
22
+ DragGesture()
23
+ .onChanged(onDragging)
24
+ .onEnded(onDraggingEnded)
25
+ }
26
+
27
+ var tapGesture: some Gesture {
28
+ TapGesture(count: 1)
29
+ .onEnded(onTapEnded)
30
+ }
31
+
32
+ var doubleTapGesture: some Gesture {
33
+ TapGesture(count: 2)
34
+ .onEnded(onDoubleTapEnded)
35
+ }
36
+
37
+ var longPressGesture: some Gesture {
38
+ LongPressGesture(minimumDuration: 1.0)
39
+ .onEnded(onLonePressEnded)
40
+ }
41
+
42
+ @ViewBuilder
43
+ var body: some View {
44
+ if e.coordinateSpace == .DOM {
45
+ if let childModel3DComponent = e.getComponent(SpatialModel3DComponent.self),
46
+ let url = URL(string: childModel3DComponent.modelURL)
47
+ {
48
+ let x = CGFloat(e.modelEntity.position.x)
49
+ let y = CGFloat(e.modelEntity.position.y - (childModel3DComponent.scrollWithParent ? parentYOffset : 0))
50
+ let z = CGFloat(e.modelEntity.position.z)
51
+ let width = CGFloat(childModel3DComponent.resolutionX)
52
+ let height = CGFloat(childModel3DComponent.resolutionY)
53
+ let anchor = childModel3DComponent.rotationAnchor
54
+ let opacity = childModel3DComponent.opacity
55
+ let resizable = childModel3DComponent.resizable
56
+ let aspectRatio: CGFloat? = childModel3DComponent.aspectRatio == nil ? nil : CGFloat(childModel3DComponent.aspectRatio!)
57
+ let contentMode = childModel3DComponent.contentMode
58
+
59
+ let enableTapEvent = childModel3DComponent.enableTapEvent
60
+ let enableDoubleTapEvent = childModel3DComponent.enableDoubleTapEvent
61
+ let enableDragEvent = childModel3DComponent.enableDragEvent
62
+ let enableLongPressEvent = childModel3DComponent.enableLongPressEvent
63
+
64
+ // Matrix = MTranslate X MRotate X MScale
65
+ Model3D(url: url) { newPhase in
66
+ switch newPhase {
67
+ case .empty:
68
+ ProgressView()
69
+
70
+ case let .success(resolvedModel3D):
71
+ resolvedModel3D
72
+ .resizable(resizable)
73
+ .aspectRatio(
74
+ aspectRatio,
75
+ contentMode: contentMode
76
+ )
77
+
78
+ .onAppear {
79
+ self.onLoadSuccess()
80
+ }
81
+
82
+ case let .failure(error):
83
+ // use UIView.onAppear to notify error phase.
84
+ Text("").onAppear {
85
+ self.onLoadFailure(error.localizedDescription)
86
+ }
87
+
88
+ @unknown default:
89
+ EmptyView()
90
+ }
91
+ }
92
+ .frame(width: width, height: height)
93
+ // .background(Color.blue)
94
+ .scaleEffect(
95
+ x: CGFloat(e.modelEntity.scale.x),
96
+ y: CGFloat(e.modelEntity.scale.y),
97
+ z: CGFloat(e.modelEntity.scale.z),
98
+ anchor: anchor
99
+ )
100
+ .rotation3DEffect(
101
+ Rotation3D(simd_quatf(
102
+ ix: e.modelEntity.orientation.vector.x,
103
+ iy: e.modelEntity.orientation.vector.y,
104
+ iz: e.modelEntity.orientation.vector.z,
105
+ r: e.modelEntity.orientation.vector.w
106
+ )),
107
+ anchor: anchor
108
+ )
109
+ .position(x: x, y: y)
110
+ .offset(z: z)
111
+ .frame(maxDepth: 0, alignment: .back)
112
+ .opacity(opacity)
113
+ .gesture(enableDragEvent ? drag : nil)
114
+ .gesture(enableDoubleTapEvent ?doubleTapGesture : nil)
115
+ .gesture(enableTapEvent ? tapGesture : nil)
116
+ .gesture(enableLongPressEvent ? longPressGesture : nil)
117
+ .hidden(!e.visible)
118
+ } else {
119
+ Text("").onAppear {
120
+ self.onLoadFailure("invalid URL")
121
+ }
122
+ }
123
+ } else {
124
+ EmptyView()
125
+ }
126
+ }
127
+
128
+ private func onLoadSuccess() {
129
+ if let model3DComponent = e.getComponent(SpatialModel3DComponent.self) {
130
+ let data = "{eventType: 'phase', value: 'success'}"
131
+ model3DComponent.wv?.fireComponentEvent(componentId: model3DComponent.id, data: data)
132
+ }
133
+ }
134
+
135
+ private func onLoadFailure(_ error: String) {
136
+ if let model3DComponent = e.getComponent(SpatialModel3DComponent.self) {
137
+ let data = "{eventType: 'phase', value: 'failure', error: '\(error)'} "
138
+ model3DComponent.wv?.fireComponentEvent(componentId: model3DComponent.id, data: data)
139
+ }
140
+ }
141
+
142
+ private func onDragging(dragValue: DragGesture.Value) {
143
+ var eventType = "drag"
144
+ if !gestureData.isDragging {
145
+ gestureData.isDragging = true
146
+ eventType = "dragstart"
147
+ }
148
+ fireDragEvent(eventType, dragValue)
149
+ }
150
+
151
+ private func onDraggingEnded(dragValue: DragGesture.Value) {
152
+ gestureData.isDragging = false
153
+ let eventType = "dragend"
154
+ fireDragEvent(eventType, dragValue)
155
+ }
156
+
157
+ private func onTapEnded(_: TapGesture.Value) {
158
+ print("onTapEnded")
159
+ if let model3DComponent = e.getComponent(SpatialModel3DComponent.self) {
160
+ let eventType = "tap"
161
+ let data = "{eventType: '\(eventType)' } "
162
+ model3DComponent.wv?.fireComponentEvent(componentId: model3DComponent.id, data: data)
163
+ }
164
+ }
165
+
166
+ private func onDoubleTapEnded(_: TapGesture.Value) {
167
+ print("onDoubleTapEnded")
168
+ if let model3DComponent = e.getComponent(SpatialModel3DComponent.self) {
169
+ let eventType = "doubletap"
170
+ let data = "{eventType: '\(eventType)' } "
171
+ model3DComponent.wv?.fireComponentEvent(componentId: model3DComponent.id, data: data)
172
+ }
173
+ }
174
+
175
+ private func onLonePressEnded(_: LongPressGesture.Value) {
176
+ print("onLonePressEnded")
177
+ if let model3DComponent = e.getComponent(SpatialModel3DComponent.self) {
178
+ let eventType = "longpress"
179
+ let data = "{eventType: '\(eventType)' } "
180
+ model3DComponent.wv?.fireComponentEvent(componentId: model3DComponent.id, data: data)
181
+ }
182
+ }
183
+
184
+ private func fireDragEvent(_ eventType: String, _ value: DragGesture.Value) {
185
+ if let model3DComponent = e.getComponent(SpatialModel3DComponent.self) {
186
+ let startLocation3D = value.startLocation3D
187
+ let translation3D = value.translation3D
188
+
189
+ let data = "{eventType: '\(eventType)', value: { translation3D : { x: \(translation3D.x), y: \(translation3D.y), z: \(translation3D.z) }, startLocation3D: { x: \(startLocation3D.x), y: \(startLocation3D.y), z: \(startLocation3D.z)} } } "
190
+ model3DComponent.wv?.fireComponentEvent(componentId: model3DComponent.id, data: data)
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,168 @@
1
+ //
2
+ // SpatialViewUI.swift
3
+ // web-spatial
4
+ //
5
+ // Created by ByteDance on 11/6/24.
6
+ //
7
+ import RealityKit
8
+ import SwiftUI
9
+
10
+ extension View {
11
+ @ViewBuilder
12
+ func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
13
+ if condition {
14
+ transform(self)
15
+ } else {
16
+ self
17
+ }
18
+ }
19
+ }
20
+
21
+ struct SpatialViewUI: View {
22
+ @Environment(SpatialEntity.self) var ent: SpatialEntity
23
+ @State var isRoot = false
24
+
25
+ // Entity which will contain all the content of this realityView and scale to fit frame
26
+ @State var world = Entity()
27
+ @State var portal = Entity()
28
+ @State var light = PointLight()
29
+ @State var portalModel = ModelComponent(
30
+ mesh: .generatePlane(width: 1.0, height: 1.0, cornerRadius: 0.0),
31
+ materials: [PortalMaterial()]
32
+ )
33
+ @State var worldComponent = WorldComponent()
34
+
35
+ private func toJson(val: SIMD3<Float>) -> String {
36
+ return "{x: " + String(val.x) + ",y: " + String(val.y) + ",z: " + String(val.z) + "}"
37
+ }
38
+
39
+ var dragGesture: some Gesture {
40
+ DragGesture(minimumDistance: 0).handActivationBehavior(.automatic)
41
+ .targetedToAnyEntity()
42
+ .onChanged { value in
43
+ let startPos = value.convert(value.startLocation3D, from: .local, to: .scene)
44
+ let translate = value.convert(value.location3D, from: .local, to: .scene)
45
+ let spatialEntity = value.entity.components[SpatialBridgeComponent.self]!.spatialEntity
46
+ let ic = spatialEntity.getComponent(SpatialInputComponent.self)!
47
+
48
+ if !ic.isDragging {
49
+ ic.isDragging = true
50
+ ic.trackedPosition = startPos
51
+ let delta = translate - ic.trackedPosition
52
+ ic.trackedPosition = translate
53
+
54
+ ic.wv!.fireComponentEvent(componentId: ic.id, data: "{eventType: 'dragstart', translate: " + toJson(val: delta) + "}")
55
+ } else {
56
+ let delta = translate - ic.trackedPosition
57
+ ic.trackedPosition = translate
58
+ ic.wv!.fireComponentEvent(componentId: ic.id, data: "{eventType: 'drag', translate: " + toJson(val: delta) + "}")
59
+ }
60
+ }
61
+ .onEnded { value in
62
+ let spatialEntity = value.entity.components[SpatialBridgeComponent.self]!.spatialEntity
63
+ let ic = spatialEntity.getComponent(SpatialInputComponent.self)!
64
+ ic.wv!.fireComponentEvent(componentId: ic.id, data: "{eventType: 'dragend'}")
65
+ ic.isDragging = false
66
+ }
67
+ }
68
+
69
+ var body: some View {
70
+ if let viewComponent = ent.getComponent(SpatialViewComponent.self) {
71
+ GeometryReader3D { proxy in
72
+ // Get dimensions of the frame
73
+ let proxySize3d = proxy.frame(in: .local)
74
+
75
+ RealityView { _, _ in
76
+ } update: { content, attachments in
77
+ // Scale content so it will be a 1x1x1 space and not exceed the frame
78
+ let viewSpaceDimensions = content.convert(proxySize3d, from: .local, to: content)
79
+ let newScale = min(viewSpaceDimensions.extents.x, viewSpaceDimensions.extents.y)
80
+
81
+ world.transform.scale.x = newScale
82
+ world.transform.scale.y = newScale
83
+ world.transform.scale.z = newScale
84
+ portal.transform.scale.x = newScale
85
+ portal.transform.scale.y = newScale
86
+ portal.transform.scale.z = newScale
87
+
88
+ if !isRoot {
89
+ // Pull out content so volume sits in front of the page
90
+ world.transform.translation.z = world.transform.scale.z / 2
91
+ }
92
+
93
+ for (_, entity) in ent.getEntities() {
94
+ world.addChild(entity.modelEntity)
95
+ }
96
+
97
+ // Add attachments for window entities
98
+ let entities = ent.getEntities().filter { _, entity in
99
+ entity.coordinateSpace == .APP && entity.hasComponent(SpatialWindowComponent.self)
100
+ }
101
+ for key in Array(entities.keys) {
102
+ let e = entities[key]!
103
+ let windowComponent = e.getComponent(SpatialWindowComponent.self)
104
+ if windowComponent != nil && e.coordinateSpace == .APP {
105
+ if let windowAttachment = attachments.entity(for: key) {
106
+ if e.modelEntity.children.count == 0 {
107
+ e.modelEntity.addChild(windowAttachment, preservingWorldTransform: false)
108
+
109
+ // Scale the window to fit the resolution to unit ratio as defined by setResolution API
110
+ let b = windowAttachment.attachment.bounds
111
+ let wv = e.getComponent(SpatialWindowComponent.self)!
112
+ let scaleFact = (Float(wv.resolutionX) / 1360.0) / (b.max.x - b.min.x)
113
+ windowAttachment.scale.x = scaleFact
114
+ windowAttachment.scale.y = scaleFact
115
+ windowAttachment.scale.z = scaleFact
116
+ }
117
+ }
118
+ }
119
+ }
120
+ if viewComponent.isPortal {
121
+ // Setup portal
122
+ portal.components.set(portalModel)
123
+ portal.transform.translation.z = 0.0001 // avoid z fighting
124
+ if !portal.components.has(PortalComponent.self) {
125
+ portal.components.set(PortalComponent(target: world))
126
+ }
127
+
128
+ // Setup default light
129
+ light.light.intensity = 5000
130
+ light.position.z = 2
131
+ light.position.y = 1
132
+ light.position.x = 0.5
133
+ world.addChild(light)
134
+
135
+ // Position volume behind portal instead of in front
136
+ world.transform.translation.z *= -1
137
+
138
+ // Add portal to scene
139
+ world.components.set(worldComponent)
140
+ content.add(portal)
141
+ } else {
142
+ // Remove portal elements/components
143
+ content.remove(portal)
144
+ world.components.remove(WorldComponent.self)
145
+ world.removeChild(light)
146
+ }
147
+
148
+ content.add(world)
149
+ }
150
+ attachments: {
151
+ // Create an attachment for each window component
152
+ let entities = ent.getEntities().filter { _, entity in
153
+ entity.coordinateSpace == .APP && entity.hasComponent(SpatialWindowComponent.self)
154
+ }
155
+ ForEach(Array(entities.keys), id: \.self) { key in
156
+ let entity = entities[key]!
157
+ let wv = entity.getComponent(SpatialWindowComponent.self)!
158
+ Attachment(id: key) {
159
+ SpatialWebViewUI().environment(entity).frame(width: wv.resolutionX, height: wv.resolutionY)
160
+ }
161
+ }
162
+ }.gesture(dragGesture).if(!isRoot) { view in
163
+ view.clipped()
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
@@ -0,0 +1,193 @@
1
+ //
2
+ // SpatialWebViewUI.swift
3
+ // web-spatial
4
+ //
5
+ // Created by ByteDance on 5/9/24.
6
+ //
7
+
8
+ import RealityKit
9
+ import SwiftUI
10
+
11
+ // Using scrollview has some side effects so only use it on elements we want to clip the edges of
12
+ // Seems only scrollview has this clipping property so far on visionOS otherwise we would use ZStack
13
+ struct OptionalClip<Content: View>: View {
14
+ var clipEnabled = true
15
+ let viewBuilder: () -> Content
16
+
17
+ var body: some View {
18
+ if clipEnabled {
19
+ ScrollView {
20
+ viewBuilder()
21
+ }.offset(z: CGFloat(0)).frame(maxWidth: .infinity, maxHeight: .infinity).scrollDisabled(true)
22
+ } else {
23
+ viewBuilder()
24
+ }
25
+ }
26
+ }
27
+
28
+ struct SpatialWebViewUI: View {
29
+ @Environment(SpatialEntity.self) var ent: SpatialEntity
30
+ var body: some View {
31
+ if let wv = ent.getComponent(SpatialWindowComponent.self) {
32
+ let parentYOffset = Float(wv.scrollOffset.y)
33
+
34
+ let childEntities = ent.getEntities()
35
+
36
+ // Display child entities of the webview
37
+ ZStack {
38
+ OptionalClip(clipEnabled: ent.coordinateSpace != .ROOT && wv.isScrollEnabled()) {
39
+ ZStack {
40
+ ForEach(Array(childEntities.keys), id: \.self) { key in
41
+ if let e = childEntities[key] {
42
+ let _ = e.forceUpdate ? 0 : 0
43
+ if let childWindowcomponent = e.getComponent(SpatialWindowComponent.self) {
44
+ if e.coordinateSpace == .DOM {
45
+ let view = childWindowcomponent
46
+ let x = CGFloat(e.modelEntity.position.x)
47
+ let y = CGFloat(e.modelEntity.position.y - (view.scrollWithParent ? parentYOffset : 0))
48
+ let z = CGFloat(e.modelEntity.position.z)
49
+ let width = CGFloat(view.resolutionX)
50
+ let height = CGFloat(view.resolutionY)
51
+ let anchor = view.rotationAnchor
52
+
53
+ // Matrix = MTranslate X MRotate X MScale
54
+ SpatialWebViewUI().environment(e)
55
+ .frame(width: width, height: height)
56
+ // use .offset(smallVal) to workaround for glassEffect not working and small width/height spatialDiv not working
57
+ .offset(z: 0.0001)
58
+ .scaleEffect(
59
+ x: CGFloat(e.modelEntity.scale.x),
60
+ y: CGFloat(e.modelEntity.scale.y),
61
+ z: CGFloat(e.modelEntity.scale.z),
62
+ anchor: anchor
63
+ )
64
+ .rotation3DEffect(
65
+ Rotation3D(simd_quatf(
66
+ ix: e.modelEntity.orientation.vector.x,
67
+ iy: e.modelEntity.orientation.vector.y,
68
+ iz: e.modelEntity.orientation.vector.z,
69
+ r: e.modelEntity.orientation.vector.w
70
+ )),
71
+ anchor: anchor
72
+ )
73
+
74
+ .position(x: x, y: y)
75
+ .offset(z: z)
76
+ .zIndex(e.zIndex)
77
+ .gesture(
78
+ DragGesture()
79
+ .onChanged { gesture in
80
+ let scrollEnabled = view.isScrollEnabled()
81
+ if !scrollEnabled {
82
+ // Check if there is a nearest scroll-enabled SpatialWindowComponent in the current SpatialEntity
83
+ // and scroll it if it exists
84
+ if let targetScrollWV = wv.findNearestScrollEnabledSpatialWindowComponent() {
85
+ if !view.dragStarted {
86
+ view.dragStarted = true
87
+ view.dragStart = (gesture.translation.height)
88
+ }
89
+
90
+ // TODO: this should have velocity
91
+ let delta = view.dragStart - gesture.translation.height
92
+ view.dragStart = gesture.translation.height
93
+ targetScrollWV.updateScrollOffset(delta: delta)
94
+ }
95
+ }
96
+ }
97
+ .onEnded { _ in
98
+ let scrollEnabled = view.isScrollEnabled()
99
+
100
+ if !scrollEnabled {
101
+ if let targetScrollWV = wv.findNearestScrollEnabledSpatialWindowComponent() {
102
+ view.dragStarted = false
103
+ view.dragStart = 0
104
+
105
+ targetScrollWV.stopScrolling()
106
+ }
107
+ }
108
+ }
109
+ )
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // Model3D content
116
+ ForEach(Array(childEntities.keys), id: \.self) { key in
117
+ if let e = childEntities[key] {
118
+ let _ = e.forceUpdate ? 0 : 0
119
+ SpatialModel3DView(parentYOffset: parentYOffset)
120
+ .environment(e)
121
+ }
122
+ }
123
+
124
+ // SpatialView content
125
+ ForEach(Array(childEntities.keys), id: \.self) { key in
126
+ if let e = childEntities[key] {
127
+ if e.coordinateSpace == .DOM {
128
+ if let viewComponent = e.getComponent(SpatialViewComponent.self) {
129
+ let _ = e.forceUpdate ? 0 : 0
130
+ let x = CGFloat(e.modelEntity.position.x)
131
+ let y = CGFloat(e.modelEntity.position.y - parentYOffset)
132
+ let z = CGFloat(e.modelEntity.position.z)
133
+
134
+ let width = CGFloat(viewComponent.resolutionX)
135
+ let height = CGFloat(viewComponent.resolutionY)
136
+
137
+ SpatialViewUI().environment(e).frame(width: width, height: height).scaleEffect(
138
+ x: CGFloat(e.modelEntity.scale.x),
139
+ y: CGFloat(e.modelEntity.scale.y),
140
+ z: CGFloat(e.modelEntity.scale.z)
141
+ )
142
+ .rotation3DEffect(
143
+ Rotation3D(simd_quatf(
144
+ ix: e.modelEntity.orientation.vector.x,
145
+ iy: e.modelEntity.orientation.vector.y,
146
+ iz: e.modelEntity.orientation.vector.z,
147
+ r: e.modelEntity.orientation.vector.w
148
+ ))
149
+ ).position(x: x, y: y)
150
+ .offset(z: z)
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }.frame(maxWidth: .infinity, maxHeight: .infinity).frame(maxDepth: 0, alignment: .back).offset(z: 0)
156
+ }
157
+
158
+ // Display the main webview
159
+ if wv.didFailLoad {
160
+ VStack {
161
+ Text("Failed to load webpage. Is the server running?")
162
+ .foregroundColor(.white)
163
+ Button("Reload") {
164
+ if let url = wv.getURL() {
165
+ wv.navigateToURL(url: url)
166
+ } else {
167
+ logger.warning("Unable to reload URL")
168
+ }
169
+ }
170
+ .foregroundColor(.white)
171
+ }
172
+ .frame(maxWidth: .infinity, maxHeight: .infinity).glassBackgroundEffect()
173
+
174
+ } else {
175
+ wv.getView()
176
+ .materialWithBorderCorner(
177
+ wv.backgroundMaterial,
178
+ wv.cornerRadius
179
+ )
180
+
181
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
182
+ }
183
+ }
184
+ .opacity(wv.opacity)
185
+ .hidden(!ent.visible)
186
+ .ornament(attachmentAnchor: .scene(.bottomTrailing), contentAlignment: .bottomTrailing) {
187
+ if wv.isRootWebview() {
188
+ NavView(swc: wv)
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,38 @@
1
+
2
+ //
3
+ // VolumetricWindowContainerView.swift
4
+ // web-spatial
5
+ //
6
+ // Created by ByteDance on 5/9/24.
7
+ //
8
+
9
+ import typealias RealityKit.Attachment
10
+ import typealias RealityKit.Entity
11
+ import typealias RealityKit.MeshResource
12
+ import typealias RealityKit.Model3D
13
+ import typealias RealityKit.ModelEntity
14
+ import typealias RealityKit.RealityView
15
+ import typealias RealityKit.SimpleMaterial
16
+ import SwiftUI
17
+
18
+ struct VolumetricWindowContainerView: View {
19
+ @Environment(SpatialWindowContainer.self) var windowContainerContent: SpatialWindowContainer
20
+
21
+ var body: some View {
22
+ OpenDismissHandlerUI().environment(windowContainerContent).onDisappear {
23
+ // Don't destroy immersive space content as it is reused next time its opened
24
+ if windowContainerContent.id != SpatialWindowContainer.ImmersiveID {
25
+ windowContainerContent.destroy()
26
+ }
27
+ }
28
+
29
+ let entities = windowContainerContent.getEntities().filter { _, entity in
30
+ entity.coordinateSpace == .ROOT && entity.hasComponent(SpatialViewComponent.self)
31
+ }
32
+
33
+ ForEach(Array(entities.keys), id: \.self) { key in
34
+ let entity = entities[key]!
35
+ SpatialViewUI(isRoot: true).environment(entity)
36
+ }
37
+ }
38
+ }