@webspatial/platform-visionos 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/web-spatial/EventEmitter.swift +11 -11
- package/web-spatial/JSBCommand.swift +38 -3
- package/web-spatial/WebMsgCommand.swift +5 -16
- package/web-spatial/WebSpatialApp.swift +10 -10
- package/web-spatial/Window.swift +2 -2
- package/web-spatial/manager/AttachmentManager.swift +84 -0
- package/web-spatial/manager/Dynamic3DManager.swift +10 -0
- package/web-spatial/manager/JSBManager.swift +1 -2
- package/web-spatial/manager/WKWebViewManager.swift +4 -4
- package/web-spatial/manifest.swift +11 -6
- package/web-spatial/model/SpatialApp.swift +60 -56
- package/web-spatial/model/SpatialScene.swift +233 -16
- package/web-spatial/model/Spatialized2DElement.swift +4 -5
- package/web-spatial/model/SpatializedDynamic3DElement.swift +12 -0
- package/web-spatial/model/SpatializedElement.swift +40 -0
- package/web-spatial/model/SpatializedStatic3DElement.swift +1 -1
- package/web-spatial/model/dynamic3d/SpatialComponent.swift +27 -27
- package/web-spatial/model/dynamic3d/SpatialEntity.swift +8 -2
- package/web-spatial/model/dynamic3d/SpatialMaterial.swift +15 -15
- package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +10 -10
- package/web-spatial/model/dynamic3d/SpatialModelResource.swift +1 -1
- package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +8 -8
- package/web-spatial/view/SceneHandlerUIView.swift +29 -1
- package/web-spatial/view/SpatialNavView.swift +52 -47
- package/web-spatial/view/SpatializedDynamic3DView.swift +88 -5
- package/web-spatial/view/SpatializedElementView.swift +85 -47
- package/web-spatial/view/SpatializedStatic3DView.swift +9 -7
- package/web-spatial/view/view-modifier/HideViewModifier.swift +2 -2
- package/web-spatial/webview/SpatialWebController.swift +42 -25
- package/web-spatial/webview/SpatialWebView.swift +5 -1
- package/web-spatial/webview/SpatialWebViewModel.swift +13 -7
- package/web-spatial.xcodeproj/project.pbxproj +13 -0
- package/web-spatialTests/NavigationCleanupTests.swift +33 -0
|
@@ -1,10 +1,9 @@
|
|
|
1
|
+
import CoreGraphics
|
|
1
2
|
import SwiftUI
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
let zOrderBias = 0.001
|
|
5
|
-
|
|
6
|
-
final class GestureFlags {
|
|
4
|
+
final class GestureState {
|
|
7
5
|
var isDrag = false
|
|
6
|
+
var proxyTransform: AffineTransform3D = .identity
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
struct SpatializedElementView<Content: View>: View {
|
|
@@ -14,32 +13,44 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
14
13
|
var parentScrollOffset: Vec2
|
|
15
14
|
var content: Content
|
|
16
15
|
|
|
17
|
-
@State private var
|
|
16
|
+
@State private var gestureState = GestureState()
|
|
18
17
|
|
|
19
18
|
init(parentScrollOffset: Vec2, @ViewBuilder content: () -> Content) {
|
|
20
19
|
self.parentScrollOffset = parentScrollOffset
|
|
21
20
|
self.content = content()
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
23
|
+
/// Begin Interaction
|
|
25
24
|
var gesture: some Gesture {
|
|
26
25
|
DragGesture(minimumDistance: 10)
|
|
27
26
|
.onChanged(onDragging)
|
|
28
27
|
.onEnded(onDraggingEnded)
|
|
29
28
|
.simultaneously(with:
|
|
30
|
-
|
|
29
|
+
makeRotateGesture3D()
|
|
31
30
|
.onChanged(onRotateGesture3D)
|
|
32
|
-
.onEnded(onRotateGesture3DEnd)
|
|
33
|
-
)
|
|
31
|
+
.onEnded(onRotateGesture3DEnd))
|
|
34
32
|
.simultaneously(with:
|
|
35
33
|
MagnifyGesture()
|
|
36
34
|
.onChanged(onMagnifyGesture)
|
|
37
|
-
.onEnded(onMagnifyGestureEnd)
|
|
38
|
-
)
|
|
35
|
+
.onEnded(onMagnifyGestureEnd))
|
|
39
36
|
.simultaneously(with:
|
|
40
37
|
SpatialTapGesture(count: 1)
|
|
41
|
-
.onEnded(onTapEnded)
|
|
42
|
-
|
|
38
|
+
.onEnded(onTapEnded))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private func makeRotateGesture3D() -> RotateGesture3D {
|
|
42
|
+
guard let raw = spatializedElement.rotateConstrainedToAxis else {
|
|
43
|
+
return RotateGesture3D()
|
|
44
|
+
}
|
|
45
|
+
let dx = Double(raw.x)
|
|
46
|
+
let dy = Double(raw.y)
|
|
47
|
+
let dz = Double(raw.z)
|
|
48
|
+
let len = (dx * dx + dy * dy + dz * dz).squareRoot()
|
|
49
|
+
if len < 1e-9 {
|
|
50
|
+
return RotateGesture3D()
|
|
51
|
+
}
|
|
52
|
+
let axis = RotationAxis3D(x: dx / len, y: dy / len, z: dz / len)
|
|
53
|
+
return RotateGesture3D(constrainedToAxis: axis)
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
private func onRotateGesture3D(_ event: RotateGesture3D.Value) {
|
|
@@ -63,11 +74,18 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
private func onDragging(_ event: DragGesture.Value) {
|
|
66
|
-
if spatializedElement.enableDragStartGesture, !
|
|
77
|
+
if spatializedElement.enableDragStartGesture, !gestureState.isDrag {
|
|
78
|
+
let frameZ = localFrameOffsetZ()
|
|
79
|
+
let startLocal = Point3D(
|
|
80
|
+
x: event.startLocation3D.x,
|
|
81
|
+
y: event.startLocation3D.y,
|
|
82
|
+
z: event.startLocation3D.z - frameZ
|
|
83
|
+
)
|
|
84
|
+
let globalPoint3D = localToScene(event.startLocation3D)
|
|
67
85
|
let gestureEvent = WebSpatialDragStartGuestureEvent(detail: .init(
|
|
68
|
-
startLocation3D:
|
|
86
|
+
startLocation3D: startLocal,
|
|
87
|
+
globalLocation3D: globalPoint3D
|
|
69
88
|
))
|
|
70
|
-
|
|
71
89
|
spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
|
|
72
90
|
}
|
|
73
91
|
|
|
@@ -79,20 +97,40 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
79
97
|
spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
|
|
100
|
+
gestureState.isDrag = true
|
|
83
101
|
}
|
|
84
102
|
|
|
85
103
|
private func onDraggingEnded(_ event: DragGesture.Value) {
|
|
86
|
-
|
|
104
|
+
gestureState.isDrag = false
|
|
87
105
|
if spatializedElement.enableDragEndGesture {
|
|
88
106
|
let gestureEvent = WebSpatialDragEndGuestureEvent()
|
|
89
107
|
spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
110
|
|
|
111
|
+
/// Z offset that defines the local coordinate system (front face = z=0).
|
|
112
|
+
/// Only backOffset and zIndex*bias; excludes translation.z (CSS translateZ is visual-only).
|
|
113
|
+
private func localFrameOffsetZ() -> Double {
|
|
114
|
+
(spatializedElement.zIndex * zOrderBias) + spatializedElement.backOffset
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Maps a point in the gesture's local coordinate system to SpatialScene.
|
|
118
|
+
private func localToScene(_ localPoint: Point3D) -> Point3D {
|
|
119
|
+
let p = SIMD4<Double>(localPoint.x, localPoint.y, localPoint.z, 1.0)
|
|
120
|
+
let scene = gestureState.proxyTransform.matrix * p
|
|
121
|
+
return Point3D(x: scene.x, y: scene.y, z: scene.z)
|
|
122
|
+
}
|
|
123
|
+
|
|
93
124
|
private func onTapEnded(_ event: SpatialTapGesture.Value) {
|
|
94
125
|
if spatializedElement.enableTapGesture {
|
|
95
|
-
|
|
126
|
+
let frameZ = localFrameOffsetZ()
|
|
127
|
+
let localPoint3D = Point3D(
|
|
128
|
+
x: event.location3D.x,
|
|
129
|
+
y: event.location3D.y,
|
|
130
|
+
z: event.location3D.z - frameZ
|
|
131
|
+
)
|
|
132
|
+
let globalPoint3D = localToScene(event.location3D)
|
|
133
|
+
spatialScene.sendWebMsg(spatializedElement.id, WebSpatialTapGuestureEvent(detail: .init(location3D: localPoint3D, globalLocation3D: globalPoint3D)))
|
|
96
134
|
}
|
|
97
135
|
}
|
|
98
136
|
|
|
@@ -101,7 +139,8 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
101
139
|
let gestureEvent = WebSpatialMagnifyGuestureEvent(
|
|
102
140
|
detail: .init(
|
|
103
141
|
magnification: event.magnification
|
|
104
|
-
)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
105
144
|
spatialScene.sendWebMsg(spatializedElement.id, gestureEvent)
|
|
106
145
|
}
|
|
107
146
|
}
|
|
@@ -114,12 +153,8 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
114
153
|
|
|
115
154
|
// End Interaction
|
|
116
155
|
|
|
117
|
-
@ViewBuilder
|
|
118
156
|
var body: some View {
|
|
119
157
|
let transform = spatializedElement.transform
|
|
120
|
-
let translation = transform.translation
|
|
121
|
-
let scale = transform.scale
|
|
122
|
-
let rotation = transform.rotation!
|
|
123
158
|
|
|
124
159
|
let width = spatializedElement.width
|
|
125
160
|
let height = spatializedElement.height
|
|
@@ -133,8 +168,17 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
133
168
|
let visible = spatializedElement.visible
|
|
134
169
|
let enableGesture = spatializedElement.enableGesture
|
|
135
170
|
|
|
136
|
-
let
|
|
137
|
-
let smallOffset =
|
|
171
|
+
let frameOffsetZ = localFrameOffsetZ()
|
|
172
|
+
let smallOffset = abs(frameOffsetZ) < 0.0001 ? 0.0001 : 0
|
|
173
|
+
|
|
174
|
+
// Wrap CSS transform with anchor (CSS transform-origin) since
|
|
175
|
+
// transform3DEffect does not support anchor. Preserves the original
|
|
176
|
+
// CSS transform order (e.g. rotateX(90deg) translateZ(100px)).
|
|
177
|
+
let ax = width * anchor.x
|
|
178
|
+
let ay = height * anchor.y
|
|
179
|
+
let toAnchor = AffineTransform3D(translation: Vector3D(x: -ax, y: -ay, z: 0))
|
|
180
|
+
let fromAnchor = AffineTransform3D(translation: Vector3D(x: ax, y: ay, z: 0))
|
|
181
|
+
let anchoredTransform = fromAnchor.concatenating(transform).concatenating(toAnchor)
|
|
138
182
|
|
|
139
183
|
// when spatialdiv have regular/thick/thin material and alignment is back, there'll be a bug that clipping content
|
|
140
184
|
// so when spatializedElement is spatialdiv, .center alignment will be applied
|
|
@@ -143,32 +187,26 @@ struct SpatializedElementView<Content: View>: View {
|
|
|
143
187
|
content
|
|
144
188
|
.frame(width: width, height: height)
|
|
145
189
|
.frame(depth: depth, alignment: alignment)
|
|
146
|
-
.onGeometryChange3D(for: AffineTransform3D.self) { proxy in
|
|
147
|
-
let rect3d = proxy.frame(in: .named("SpatialScene"))
|
|
148
|
-
spatialScene.sendWebMsg(spatializedElement.id, SpatiaizedContainerClientCube(origin: rect3d.origin, size: rect3d.size))
|
|
149
|
-
return proxy.transform(in: .named("SpatialScene"))!
|
|
150
|
-
} action: { _, new in
|
|
151
|
-
spatialScene.sendWebMsg(spatializedElement.id, SpatiaizedContainerTransform(detail: new))
|
|
152
|
-
}
|
|
153
190
|
.frame(depth: 0, alignment: .back)
|
|
154
191
|
// use .offset(smallVal) to workaround for glassEffect not working and small width/height spatialDiv not working
|
|
155
192
|
.offset(z: smallOffset)
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
// Full CSS transform matrix with anchor baked in. Preserves transform
|
|
194
|
+
// composition order; CSS translateZ participates in rotation direction.
|
|
195
|
+
.transform3DEffect(anchoredTransform)
|
|
196
|
+
// backOffset + zIndex: always along parent Z, independent of CSS transform.
|
|
197
|
+
.offset(z: frameOffsetZ)
|
|
198
|
+
// Gesture before .position(): event.location3D is in the element's local space
|
|
199
|
+
// (top-left origin), and does not include visual transforms.
|
|
200
|
+
.simultaneousGesture(enableGesture ? gesture : nil)
|
|
201
|
+
.onGeometryChange3D(for: AffineTransform3D.self) { proxy in
|
|
202
|
+
proxy.transform(in: .named("SpatialScene"))!
|
|
203
|
+
} action: { new in
|
|
204
|
+
gestureState.proxyTransform = new
|
|
205
|
+
spatializedElement.proxySceneTransform = new
|
|
206
|
+
}
|
|
207
|
+
|
|
168
208
|
.position(x: centerX + width / 2, y: centerY + height / 2)
|
|
169
|
-
.offset(z: spatializedElement.backOffset)
|
|
170
209
|
.opacity(opacity)
|
|
171
210
|
.hidden(!visible)
|
|
172
|
-
.simultaneousGesture(enableGesture ? gesture : nil)
|
|
173
211
|
}
|
|
174
212
|
}
|
|
@@ -8,7 +8,7 @@ struct SpatializedStatic3DView: View {
|
|
|
8
8
|
private var spatializedStatic3DElement: SpatializedStatic3DElement {
|
|
9
9
|
return spatializedElement as! SpatializedStatic3DElement
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
func onLoadSuccess() {
|
|
13
13
|
spatialScene.sendWebMsg(spatializedElement.id, ModelLoadSuccess())
|
|
14
14
|
}
|
|
@@ -17,7 +17,6 @@ struct SpatializedStatic3DView: View {
|
|
|
17
17
|
spatialScene.sendWebMsg(spatializedElement.id, ModelLoadFailure())
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
@ViewBuilder
|
|
21
20
|
var body: some View {
|
|
22
21
|
let depth = spatializedElement.depth
|
|
23
22
|
let transform = spatializedStatic3DElement.modelTransform
|
|
@@ -27,14 +26,17 @@ struct SpatializedStatic3DView: View {
|
|
|
27
26
|
let x = translation.x
|
|
28
27
|
let y = translation.y
|
|
29
28
|
let z = translation.z
|
|
30
|
-
|
|
29
|
+
|
|
31
30
|
let enableGesture = spatializedElement.enableGesture
|
|
32
|
-
|
|
31
|
+
let rawModelURL = spatializedStatic3DElement.modelURL
|
|
32
|
+
let modelURL = rawModelURL.hasPrefix("file://")
|
|
33
|
+
? pwaManager.getLocalResourceURL(url: rawModelURL)
|
|
34
|
+
: rawModelURL
|
|
35
|
+
if let url = URL(string: modelURL) {
|
|
33
36
|
Model3D(url: url) { newPhase in
|
|
34
37
|
switch newPhase {
|
|
35
38
|
case .empty:
|
|
36
39
|
ProgressView()
|
|
37
|
-
|
|
38
40
|
case let .success(resolvedModel3D):
|
|
39
41
|
resolvedModel3D
|
|
40
42
|
.resizable(true)
|
|
@@ -42,11 +44,11 @@ struct SpatializedStatic3DView: View {
|
|
|
42
44
|
nil,
|
|
43
45
|
contentMode: .fit
|
|
44
46
|
)
|
|
45
|
-
.if(!depth.isZero){ view in view.scaledToFit3D()}
|
|
47
|
+
.if(!depth.isZero) { view in view.scaledToFit3D() }
|
|
46
48
|
.onAppear {
|
|
47
49
|
self.onLoadSuccess()
|
|
48
50
|
}
|
|
49
|
-
.if(enableGesture) { view in view.hoverEffect()}
|
|
51
|
+
.if(enableGesture) { view in view.hoverEffect() }
|
|
50
52
|
case .failure:
|
|
51
53
|
Text("").onAppear {
|
|
52
54
|
self.onLoadFailure()
|
|
@@ -2,14 +2,14 @@ import SwiftUI
|
|
|
2
2
|
|
|
3
3
|
struct HideViewModifier: ViewModifier {
|
|
4
4
|
let isHidden: Bool
|
|
5
|
-
|
|
5
|
+
func body(content: Content) -> some View {
|
|
6
6
|
content
|
|
7
7
|
.opacity(isHidden ? 0 : 1)
|
|
8
8
|
.disabled(isHidden)
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/// Extending on View to apply to all Views
|
|
13
13
|
extension View {
|
|
14
14
|
func hidden(_ isHidden: Bool) -> some View {
|
|
15
15
|
modifier(HideViewModifier(isHidden: isHidden))
|
|
@@ -9,7 +9,7 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
9
9
|
private var openWindowInvoke: ((_ data: URL) -> WebViewElementInfo?)?
|
|
10
10
|
private var webviewStateChangeInvoke: ((_ type: SpatialWebViewState) -> Void)?
|
|
11
11
|
private var scorllUpdateInvoke: ((_ type: ScrollState, _ point: CGPoint) -> Void)?
|
|
12
|
-
private var webviewTitle: String?
|
|
12
|
+
private var webviewTitle: String?
|
|
13
13
|
private var firstLoad = true
|
|
14
14
|
private var jsbManager = JSBManager()
|
|
15
15
|
|
|
@@ -62,25 +62,29 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
/// navigation request
|
|
66
|
+
/// SpatialDiv/forcestyle/normal web link protocol
|
|
67
67
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Swift.Void) {
|
|
68
68
|
let deciside = navigationInvoke?(navigationAction.request.url!)
|
|
69
69
|
if deciside == true {
|
|
70
|
-
if !firstLoad{
|
|
70
|
+
if !firstLoad {
|
|
71
71
|
webviewStateChangeInvoke?(.didUnload)
|
|
72
72
|
}
|
|
73
73
|
firstLoad = false
|
|
74
74
|
}
|
|
75
75
|
var needAllow = deciside ?? false
|
|
76
|
-
|
|
77
|
-
if !needAllow{
|
|
78
|
-
|
|
76
|
+
|
|
77
|
+
if !needAllow {
|
|
78
|
+
// webspatial:// is an internal scheme for in-app routing between main and attachment windows.
|
|
79
|
+
// Only open non-webspatial URLs externally via the system.
|
|
80
|
+
if navigationAction.request.url?.scheme != "webspatial" {
|
|
81
|
+
UIApplication.shared.open(navigationAction.request.url!, options: [:], completionHandler: nil)
|
|
82
|
+
}
|
|
79
83
|
}
|
|
80
84
|
decisionHandler(needAllow ? .allow : .cancel)
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
/// open window request
|
|
84
88
|
func webView(
|
|
85
89
|
_ webView: WKWebView,
|
|
86
90
|
createWebViewWith configuration: WKWebViewConfiguration,
|
|
@@ -95,7 +99,7 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
95
99
|
return nil
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
|
|
102
|
+
/// invoke jsb
|
|
99
103
|
func userContentController(
|
|
100
104
|
_ userContentController: WKUserContentController,
|
|
101
105
|
didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void
|
|
@@ -104,12 +108,26 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
104
108
|
jsbManager.handlerMessage(message.body as! String, replyHandler)
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
|
|
111
|
+
/// custom scheme request
|
|
108
112
|
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
|
|
109
113
|
print("urlSchemeTask")
|
|
110
114
|
let url = urlSchemeTask.request.url
|
|
111
115
|
if url!.absoluteString.starts(with: "file://") {
|
|
112
|
-
let
|
|
116
|
+
let resource: String = pwaManager.getLocalResourceURL(url: url!.absoluteString)
|
|
117
|
+
var urlRequest = urlSchemeTask.request
|
|
118
|
+
if resource != "" {
|
|
119
|
+
if let resourceUrl = URL(string: resource) {
|
|
120
|
+
urlRequest = URLRequest(url: resourceUrl)
|
|
121
|
+
} else {
|
|
122
|
+
let error = NSError(domain: "LocalResourceError", code: 404, userInfo: [NSLocalizedDescriptionKey: "Local file resource mapping failed"])
|
|
123
|
+
urlSchemeTask.didFailWithError(error)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
let error = NSError(domain: "LocalResourceError", code: 404, userInfo: [NSLocalizedDescriptionKey: "Local resource not found"])
|
|
128
|
+
urlSchemeTask.didFailWithError(error)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
113
131
|
|
|
114
132
|
let session = URLSession(configuration: URLSessionConfiguration.default)
|
|
115
133
|
let dataTask = session.dataTask(with: urlRequest) { [task = urlSchemeTask as AnyObject] data, response, _ in
|
|
@@ -221,8 +239,8 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
221
239
|
scorllUpdateInvoke = nil
|
|
222
240
|
model = nil
|
|
223
241
|
}
|
|
224
|
-
|
|
225
|
-
private var state:SpatialWebViewState?
|
|
242
|
+
|
|
243
|
+
private var state: SpatialWebViewState?
|
|
226
244
|
|
|
227
245
|
func destroyView() {
|
|
228
246
|
stopObserving()
|
|
@@ -236,15 +254,15 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
236
254
|
webviewStateChangeInvoke?(.didDestroyView)
|
|
237
255
|
}
|
|
238
256
|
}
|
|
239
|
-
|
|
257
|
+
|
|
240
258
|
private var isPageLoaded = false
|
|
241
|
-
|
|
259
|
+
|
|
242
260
|
private var jsQueue: [String] = []
|
|
243
|
-
|
|
261
|
+
|
|
244
262
|
private func enqueueJS(_ js: String) {
|
|
245
263
|
jsQueue.append(js)
|
|
246
264
|
}
|
|
247
|
-
|
|
265
|
+
|
|
248
266
|
private func flushJSQueue() {
|
|
249
267
|
guard !jsQueue.isEmpty else { return }
|
|
250
268
|
let combined = jsQueue.joined(separator: ";")
|
|
@@ -253,7 +271,7 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
253
271
|
}
|
|
254
272
|
|
|
255
273
|
func callJS(_ js: String) {
|
|
256
|
-
if webview != nil
|
|
274
|
+
if webview != nil, isPageLoaded {
|
|
257
275
|
webview!.evaluateJavaScript(js)
|
|
258
276
|
} else {
|
|
259
277
|
enqueueJS(js)
|
|
@@ -268,13 +286,13 @@ enum ScrollState {
|
|
|
268
286
|
case end
|
|
269
287
|
}
|
|
270
288
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
289
|
+
/// extend webview to support file://
|
|
290
|
+
@available(iOS 11.0, *)
|
|
291
|
+
extension WKWebView {
|
|
274
292
|
/// WKWebView, Support setting file scheme in configuration
|
|
275
293
|
public private(set) static var isEnableFileSupport = false
|
|
276
294
|
public static func enableFileScheme() {
|
|
277
|
-
|
|
295
|
+
// This method supports adapting supported files through Configuration, but cannot be cancelled (Configuration is immutable).
|
|
278
296
|
if !isEnableFileSupport {
|
|
279
297
|
switchHandlesURLScheme()
|
|
280
298
|
}
|
|
@@ -292,9 +310,8 @@ enum ScrollState {
|
|
|
292
310
|
}
|
|
293
311
|
|
|
294
312
|
/// Return true if WKWebview supports handling this protocol, but WKWebview supports HTTP by default, so return false to support using custom HTTP Handler
|
|
295
|
-
@objc private dynamic
|
|
296
|
-
static func wrapHandles(urlScheme: String) -> Bool {
|
|
313
|
+
@objc private dynamic static func wrapHandles(urlScheme: String) -> Bool {
|
|
297
314
|
if urlScheme == "file" { return false }
|
|
298
315
|
return wrapHandles(urlScheme: urlScheme)
|
|
299
316
|
}
|
|
300
|
-
|
|
317
|
+
}
|
|
@@ -8,7 +8,11 @@ struct SpatialWebView: UIViewRepresentable {
|
|
|
8
8
|
|
|
9
9
|
func makeUIView(context: Context) -> WKWebView {
|
|
10
10
|
webviewStateChangeInvoke?(.didMakeView)
|
|
11
|
-
|
|
11
|
+
let controller = model?.getController()
|
|
12
|
+
if controller?.webview == nil {
|
|
13
|
+
model?.load()
|
|
14
|
+
}
|
|
15
|
+
return controller?.webview ?? WKWebView(frame: .zero, configuration: WKWebViewConfiguration())
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
func makeCoordinator() -> SpatialWebController {
|
|
@@ -76,6 +76,12 @@ class SpatialWebViewModel {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
func getController() -> SpatialWebController {
|
|
79
|
+
// See TODO in AttachmentManager — async destroy() during SwiftUI teardown
|
|
80
|
+
// can nil the controller while the view is still being removed.
|
|
81
|
+
if controller == nil {
|
|
82
|
+
controller = SpatialWebController()
|
|
83
|
+
controller!.model = self
|
|
84
|
+
}
|
|
79
85
|
return controller!
|
|
80
86
|
}
|
|
81
87
|
|
|
@@ -103,8 +109,8 @@ class SpatialWebViewModel {
|
|
|
103
109
|
controller?.setWebViewTitle(title)
|
|
104
110
|
}
|
|
105
111
|
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
/// events
|
|
113
|
+
/// navigation event
|
|
108
114
|
func addNavigationListener(protocal: String, event: @escaping (_ data: URL) -> Bool) {
|
|
109
115
|
navigationList[protocal] = event
|
|
110
116
|
}
|
|
@@ -113,7 +119,7 @@ class SpatialWebViewModel {
|
|
|
113
119
|
navigationList = [:]
|
|
114
120
|
}
|
|
115
121
|
|
|
116
|
-
|
|
122
|
+
/// open window event
|
|
117
123
|
func addOpenWindowListener(protocal: String, _ event: @escaping (_ data: URL) -> WebViewElementInfo?) {
|
|
118
124
|
openWindowList[protocal] = event
|
|
119
125
|
}
|
|
@@ -122,7 +128,7 @@ class SpatialWebViewModel {
|
|
|
122
128
|
openWindowList = [:]
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
|
|
131
|
+
/// jsb event
|
|
126
132
|
func addJSBListener<T: CommandDataProtocol>(_ dataClass: T.Type, _ event: @escaping (T, @escaping JSBManager.ResolveHandler<Encodable>) -> Void) {
|
|
127
133
|
controller?.registeJSBHandler(dataClass, event)
|
|
128
134
|
}
|
|
@@ -143,7 +149,7 @@ class SpatialWebViewModel {
|
|
|
143
149
|
controller?.mockJSB(command)
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
|
|
152
|
+
/// webview state event
|
|
147
153
|
func addStateListener(_ event: @escaping (_ type: SpatialWebViewState) -> Void) {
|
|
148
154
|
stateChangeListeners.append(event)
|
|
149
155
|
}
|
|
@@ -176,7 +182,7 @@ class SpatialWebViewModel {
|
|
|
176
182
|
stateListeners[state] = nil
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
|
|
185
|
+
/// scroll update event
|
|
180
186
|
func addScrollUpdateListener(_ event: @escaping (_ type: ScrollState, _ point: CGPoint) -> Void) {
|
|
181
187
|
scrollUpdateListeners.append(event)
|
|
182
188
|
}
|
|
@@ -199,7 +205,7 @@ class SpatialWebViewModel {
|
|
|
199
205
|
removeAllScrollUpdateListener()
|
|
200
206
|
}
|
|
201
207
|
|
|
202
|
-
|
|
208
|
+
/// invokes
|
|
203
209
|
private func onNavigationInvoke(_ url: URL) -> Bool {
|
|
204
210
|
var matchProtocol = false
|
|
205
211
|
for key in navigationList.keys {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
2B06AEE42E4C1AE8000327E9 /* manifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B06AEC12E4C1AE8000327E9 /* manifest.swift */; };
|
|
14
14
|
2B06AEE62E4C1AE8000327E9 /* JSBCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B06AEC02E4C1AE8000327E9 /* JSBCommand.swift */; };
|
|
15
15
|
2B06AEE92E4C1AE8000327E9 /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B06AEBF2E4C1AE8000327E9 /* EventEmitter.swift */; };
|
|
16
|
+
2B1008A22F0B000800000002 /* NavigationCleanupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */; };
|
|
16
17
|
2B2F1D692BEBFAAA006897EE /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; productRef = 2B2F1D682BEBFAAA006897EE /* RealityKitContent */; };
|
|
17
18
|
2B2F1D712BEBFAAC006897EE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B2F1D702BEBFAAC006897EE /* Assets.xcassets */; };
|
|
18
19
|
2B2F1D742BEBFAAC006897EE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B2F1D732BEBFAAC006897EE /* Preview Assets.xcassets */; };
|
|
@@ -37,6 +38,7 @@
|
|
|
37
38
|
2B06AEC22E4C1AE8000327E9 /* SpatialObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpatialObject.swift; sourceTree = "<group>"; };
|
|
38
39
|
2B06AEC32E4C1AE8000327E9 /* WebSpatialApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSpatialApp.swift; sourceTree = "<group>"; };
|
|
39
40
|
2B06AEC42E4C1AE8000327E9 /* Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = "<group>"; };
|
|
41
|
+
2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCleanupTests.swift; sourceTree = "<group>"; };
|
|
40
42
|
2B2F1D632BEBFAAA006897EE /* WebSpatial.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WebSpatial.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
41
43
|
2B2F1D672BEBFAAA006897EE /* RealityKitContent */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RealityKitContent; sourceTree = "<group>"; };
|
|
42
44
|
2B2F1D702BEBFAAC006897EE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
@@ -51,6 +53,7 @@
|
|
|
51
53
|
2B06AF302E4C1AF8000327E9 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
|
52
54
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
53
55
|
membershipExceptions = (
|
|
56
|
+
AttachmentManager.swift,
|
|
54
57
|
Dynamic3DManager.swift,
|
|
55
58
|
FileCoordinator.swift,
|
|
56
59
|
JSBManager.swift,
|
|
@@ -152,10 +155,19 @@
|
|
|
152
155
|
/* End PBXFrameworksBuildPhase section */
|
|
153
156
|
|
|
154
157
|
/* Begin PBXGroup section */
|
|
158
|
+
2B1008A32F0B000800000003 /* web-spatialTests */ = {
|
|
159
|
+
isa = PBXGroup;
|
|
160
|
+
children = (
|
|
161
|
+
2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */,
|
|
162
|
+
);
|
|
163
|
+
path = "web-spatialTests";
|
|
164
|
+
sourceTree = "<group>";
|
|
165
|
+
};
|
|
155
166
|
2B2F1D5A2BEBFAAA006897EE = {
|
|
156
167
|
isa = PBXGroup;
|
|
157
168
|
children = (
|
|
158
169
|
2B2F1D652BEBFAAA006897EE /* web-spatial */,
|
|
170
|
+
2B1008A32F0B000800000003 /* web-spatialTests */,
|
|
159
171
|
2B2F1D662BEBFAAA006897EE /* Packages */,
|
|
160
172
|
2B2F1D642BEBFAAA006897EE /* Products */,
|
|
161
173
|
);
|
|
@@ -334,6 +346,7 @@
|
|
|
334
346
|
isa = PBXSourcesBuildPhase;
|
|
335
347
|
buildActionMask = 2147483647;
|
|
336
348
|
files = (
|
|
349
|
+
2B1008A22F0B000800000002 /* NavigationCleanupTests.swift in Sources */,
|
|
337
350
|
);
|
|
338
351
|
runOnlyForDeploymentPostprocessing = 0;
|
|
339
352
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
@testable import WebSpatial
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
final class NavigationCleanupTests: XCTestCase {
|
|
5
|
+
func test_resetForNavigation_destroysSceneSpatialObjectsAndAttachments() {
|
|
6
|
+
// Given: a scene with spatial objects and attachments created by the current page
|
|
7
|
+
let scene = SpatialApp.Instance.createScene(
|
|
8
|
+
"http://localhost:5173/",
|
|
9
|
+
.window,
|
|
10
|
+
.visible
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
let panel: Spatialized2DElement = scene.createSpatializedElement(.Spatialized2DElement)
|
|
14
|
+
panel.setParent(scene)
|
|
15
|
+
XCTAssertNotNil(scene.findSpatialObject(panel.id) as Spatialized2DElement?)
|
|
16
|
+
|
|
17
|
+
_ = scene.attachmentManager.create(
|
|
18
|
+
id: "test-attachment",
|
|
19
|
+
parentEntityId: "test-parent-entity",
|
|
20
|
+
position: SIMD3<Float>(0, 0, 0),
|
|
21
|
+
size: CGSize(width: 100, height: 100)
|
|
22
|
+
)
|
|
23
|
+
XCTAssertFalse(scene.attachmentManager.attachments.isEmpty)
|
|
24
|
+
|
|
25
|
+
// When: navigation happens (reload/back/forward/new URL)
|
|
26
|
+
scene.resetForNavigation()
|
|
27
|
+
|
|
28
|
+
// Then: all existing spatial objects and attachments should be cleaned up
|
|
29
|
+
XCTAssertNil(scene.findSpatialObject(panel.id) as Spatialized2DElement?)
|
|
30
|
+
XCTAssertTrue(scene.attachmentManager.attachments.isEmpty)
|
|
31
|
+
XCTAssertNil(SpatialObject.get(panel.id))
|
|
32
|
+
}
|
|
33
|
+
}
|