@webspatial/platform-visionos 1.6.1 → 2.0.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 -2
- package/web-spatial/JSBCommand.swift +12 -0
- package/web-spatial/WebMsgCommand.swift +29 -2
- package/web-spatial/WebSpatialApp.swift +26 -8
- package/web-spatial/manager/Dynamic3DManager.swift +97 -37
- package/web-spatial/manifest.swift +2 -1
- package/web-spatial/model/MaterialSceneRefresh.swift +41 -0
- package/web-spatial/model/SpatialScene.swift +255 -45
- package/web-spatial/model/Spatialized2DElement.swift +2 -11
- package/web-spatial/model/SpatializedElement.swift +38 -0
- package/web-spatial/model/SpatializedStatic3DElement.swift +31 -4
- package/web-spatial/model/dynamic3d/EntityAnimationManager.swift +356 -0
- package/web-spatial/model/dynamic3d/EntityAnimationSession.swift +162 -0
- package/web-spatial/model/dynamic3d/SpatialMaterial.swift +26 -10
- package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +5 -3
- package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +25 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationCommand.swift +15 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationManager.swift +263 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationObject.swift +399 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationTypes.swift +45 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationWriteAdapter.swift +65 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionBridgeTypes.swift +43 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTimelineSampler.swift +99 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTiming.swift +99 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTransformTypes.swift +22 -0
- package/web-spatial/protocol/SpatializedElementContainer.swift +1 -1
- package/web-spatial/view/SpatialNavView.swift +37 -12
- package/web-spatial/view/SpatialSceneContentView.swift +9 -12
- package/web-spatial/view/Spatialized2DElementView.swift +10 -17
- package/web-spatial/view/SpatializedDynamic3DView.swift +2 -6
- package/web-spatial/view/SpatializedElementView.swift +3 -2
- package/web-spatial/view/SpatializedStatic3DView.swift +104 -43
- package/web-spatial/view/view-modifier/OrbitModifier.swift +84 -0
- package/web-spatial/webview/SpatialWebController.swift +26 -18
- package/web-spatial/webview/SpatialWebViewModel.swift +12 -7
- package/web-spatial.xcodeproj/project.pbxproj +30 -17
- package/web-spatial.xcodeproj/xcshareddata/xcschemes/web-spatial.xcscheme +1 -1
- package/web-spatialTests/NavigationCleanupTests.swift +2 -1
- package/web-spatialTests/SpatializedElementAnimationManagerTests.swift +1466 -0
- package/web-spatialTests/SpatializedElementMotionTimelineSamplerTests.swift +117 -0
|
@@ -2,28 +2,31 @@ import RealityKit
|
|
|
2
2
|
import SwiftUI
|
|
3
3
|
|
|
4
4
|
struct SpatializedStatic3DView: View {
|
|
5
|
-
@
|
|
5
|
+
@Bindable var spatializedStatic3DElement: SpatializedStatic3DElement
|
|
6
6
|
@Environment(SpatialScene.self) var spatialScene: SpatialScene
|
|
7
7
|
|
|
8
|
-
@State private var
|
|
9
|
-
@State private var source: String?
|
|
10
|
-
@State private var isLoading = false
|
|
8
|
+
@State private var loadState: LoadState = .idle
|
|
11
9
|
|
|
12
|
-
private var
|
|
13
|
-
return
|
|
10
|
+
private var asset: Model3DAsset? {
|
|
11
|
+
if case let .loaded(asset, _) = loadState { return asset }
|
|
12
|
+
return nil
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
func onLoadSuccess(src: String) {
|
|
17
|
-
spatialScene.sendWebMsg(
|
|
16
|
+
spatialScene.sendWebMsg(spatializedStatic3DElement.id, ModelLoadSuccess(src: src))
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
func onLoadFailure() {
|
|
21
|
-
spatialScene.sendWebMsg(
|
|
20
|
+
spatialScene.sendWebMsg(spatializedStatic3DElement.id, ModelLoadFailure())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private func onOrbit(_ transform: AffineTransform3D) {
|
|
24
|
+
spatialScene.sendWebMsg(spatializedStatic3DElement.id, EntityTransformChangeEvent(transform))
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
var body: some View {
|
|
25
|
-
let depth =
|
|
26
|
-
let transform = spatializedStatic3DElement.
|
|
28
|
+
let depth = spatializedStatic3DElement.depth
|
|
29
|
+
let transform = spatializedStatic3DElement.entityTransform
|
|
27
30
|
let translation = transform.translation
|
|
28
31
|
let scale = transform.scale
|
|
29
32
|
let rotation = transform.rotation!
|
|
@@ -31,38 +34,33 @@ struct SpatializedStatic3DView: View {
|
|
|
31
34
|
let y = translation.y
|
|
32
35
|
let z = translation.z
|
|
33
36
|
|
|
34
|
-
let enableGesture =
|
|
35
|
-
if
|
|
37
|
+
let enableGesture = spatializedStatic3DElement.enableGesture
|
|
38
|
+
if spatializedStatic3DElement.loading == .eager {
|
|
36
39
|
Group {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
switch loadState {
|
|
41
|
+
case .idle, .loading:
|
|
42
|
+
posterView { ProgressView() }
|
|
43
|
+
case let .loaded(asset, _):
|
|
40
44
|
Model3D(asset: asset) { resolvedModel3D in
|
|
41
45
|
resolvedModel3D
|
|
42
46
|
.resizable(true)
|
|
43
|
-
.aspectRatio(
|
|
44
|
-
nil,
|
|
45
|
-
contentMode: .fit
|
|
46
|
-
)
|
|
47
|
+
.aspectRatio(nil, contentMode: .fit)
|
|
47
48
|
.if(!depth.isZero) { view in view.scaledToFit3D() }
|
|
48
|
-
.onAppear {
|
|
49
|
-
self.onLoadSuccess(src: source)
|
|
50
|
-
}
|
|
51
49
|
.if(enableGesture) { view in view.hoverEffect() }
|
|
52
50
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
case .failed:
|
|
52
|
+
posterView {
|
|
53
|
+
// Transparent view is required so that lifecycle modifiers like .task still work
|
|
54
|
+
Color.clear
|
|
56
55
|
}
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
|
-
.scaleEffect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
rotation
|
|
58
|
+
.scaleEffect(scale)
|
|
59
|
+
.rotation3DEffect(rotation)
|
|
60
|
+
.orbit(
|
|
61
|
+
enabled: spatializedStatic3DElement.stagemode == .orbit && asset != nil,
|
|
62
|
+
entityTransform: $spatializedStatic3DElement.entityTransform,
|
|
63
|
+
onChange: onOrbit
|
|
66
64
|
)
|
|
67
65
|
.offset(x: x, y: y)
|
|
68
66
|
.offset(z: z)
|
|
@@ -81,13 +79,38 @@ struct SpatializedStatic3DView: View {
|
|
|
81
79
|
}
|
|
82
80
|
}
|
|
83
81
|
.onChange(of: spatializedStatic3DElement.animationPaused) { onPlayback(isPaused: $1) }
|
|
84
|
-
.onChange(of: spatializedStatic3DElement.playbackRate) {
|
|
82
|
+
.onChange(of: spatializedStatic3DElement.playbackRate) {
|
|
83
|
+
asset?.animationPlaybackController?.speed = spatializedStatic3DElement.animationPaused ? 0 : Float($1)
|
|
84
|
+
}
|
|
85
|
+
.onChange(of: spatializedStatic3DElement.pendingSeekTime) { _, time in onSeek(time: time) }
|
|
85
86
|
.task(id: spatializedStatic3DElement.allSources) { await loadSources() }
|
|
86
87
|
} else {
|
|
87
88
|
EmptyView()
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/// Renders the poster image while the 3D model is loading or after all
|
|
93
|
+
/// sources fail. When no poster URL is provided, or the poster itself is
|
|
94
|
+
/// still loading/failed, the supplied `fallback` view is shown instead.
|
|
95
|
+
@ViewBuilder
|
|
96
|
+
private func posterView<Fallback: View>(
|
|
97
|
+
@ViewBuilder fallback: @escaping () -> Fallback
|
|
98
|
+
) -> some View {
|
|
99
|
+
if let posterURL = spatializedStatic3DElement.posterURL,
|
|
100
|
+
let url = localOrRemoteURL(url: posterURL)
|
|
101
|
+
{
|
|
102
|
+
AsyncImage(url: url) { phase in
|
|
103
|
+
if case let .success(image) = phase {
|
|
104
|
+
image.resizable().aspectRatio(contentMode: .fit)
|
|
105
|
+
} else {
|
|
106
|
+
fallback()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
fallback()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
91
114
|
/// Plays or pauses the model animation and sends an animation state to the web code
|
|
92
115
|
private func onPlayback(isPaused: Bool) {
|
|
93
116
|
guard let asset else {
|
|
@@ -100,13 +123,41 @@ struct SpatializedStatic3DView: View {
|
|
|
100
123
|
asset.selectedAnimation = asset.availableAnimations.first
|
|
101
124
|
}
|
|
102
125
|
let controller = asset.animationPlaybackController
|
|
103
|
-
controller?.speed = Float(spatializedStatic3DElement.playbackRate)
|
|
126
|
+
controller?.speed = isPaused ? 0 : Float(spatializedStatic3DElement.playbackRate)
|
|
127
|
+
if let time = spatializedStatic3DElement.pendingSeekTime, let controller {
|
|
128
|
+
controller.time = time
|
|
129
|
+
spatializedStatic3DElement.pendingSeekTime = nil
|
|
130
|
+
}
|
|
104
131
|
isPaused ? controller?.pause() : controller?.resume()
|
|
132
|
+
sendAnimationStateChange(isPaused: isPaused)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Seeks the underlying animation controller when a non-nil `time` is
|
|
136
|
+
/// requested, then clears `pendingSeekTime` so subsequent identical
|
|
137
|
+
/// requests still trigger a fresh seek.
|
|
138
|
+
private func onSeek(time: Double?) {
|
|
139
|
+
guard let controller = asset?.animationPlaybackController, let time else { return }
|
|
140
|
+
controller.time = time
|
|
141
|
+
spatializedStatic3DElement.pendingSeekTime = nil
|
|
142
|
+
sendAnimationStateChange(isPaused: spatializedStatic3DElement.animationPaused)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Emits the current animation state to the web layer, sampling the
|
|
146
|
+
/// controller's position and the wall clock together so the web side can
|
|
147
|
+
/// extrapolate between samples.
|
|
148
|
+
private func sendAnimationStateChange(isPaused: Bool) {
|
|
149
|
+
let controller = asset?.animationPlaybackController
|
|
105
150
|
let duration = controller?.duration ?? 0
|
|
151
|
+
let currentTime = controller?.time ?? 0
|
|
106
152
|
spatialScene.sendWebMsg(
|
|
107
|
-
|
|
153
|
+
spatializedStatic3DElement.id,
|
|
108
154
|
AnimationStateChangeEvent(
|
|
109
|
-
detail: AnimationStateChangeDetail(
|
|
155
|
+
detail: AnimationStateChangeDetail(
|
|
156
|
+
paused: isPaused,
|
|
157
|
+
duration: duration,
|
|
158
|
+
currentTime: currentTime,
|
|
159
|
+
timestamp: Date().timeIntervalSince1970 * 1000
|
|
160
|
+
)
|
|
110
161
|
)
|
|
111
162
|
)
|
|
112
163
|
}
|
|
@@ -127,18 +178,21 @@ struct SpatializedStatic3DView: View {
|
|
|
127
178
|
}
|
|
128
179
|
|
|
129
180
|
private func loadSources() async {
|
|
130
|
-
|
|
181
|
+
loadState = .loading
|
|
131
182
|
let result = await loadSources(spatializedStatic3DElement.allSources)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
183
|
+
guard !Task.isCancelled else { return }
|
|
184
|
+
if let result {
|
|
185
|
+
loadState = .loaded(result.asset, result.url.absoluteString)
|
|
186
|
+
onLoadSuccess(src: result.url.absoluteString)
|
|
135
187
|
// If animationPaused didn't change then SwiftUI will not trigger onChange so manually trigger playback
|
|
136
188
|
// This happens when play is called before load and autoplay is enabled
|
|
137
|
-
if spatializedStatic3DElement.animationPaused {
|
|
189
|
+
if spatializedStatic3DElement.autoplay, spatializedStatic3DElement.animationPaused {
|
|
138
190
|
spatializedStatic3DElement.animationPaused = false
|
|
139
|
-
} else { onPlayback(isPaused:
|
|
191
|
+
} else { onPlayback(isPaused: !spatializedStatic3DElement.autoplay) }
|
|
192
|
+
} else {
|
|
193
|
+
loadState = .failed
|
|
194
|
+
onLoadFailure()
|
|
140
195
|
}
|
|
141
|
-
isLoading = false
|
|
142
196
|
}
|
|
143
197
|
|
|
144
198
|
/// Attempts to load from each source in order, returning the first success.
|
|
@@ -158,3 +212,10 @@ struct SpatializedStatic3DView: View {
|
|
|
158
212
|
private func localOrRemoteURL(url: String) -> URL? {
|
|
159
213
|
URL(string: url.hasPrefix("file://") ? pwaManager.getLocalResourceURL(url: url) : url)
|
|
160
214
|
}
|
|
215
|
+
|
|
216
|
+
private enum LoadState {
|
|
217
|
+
case idle
|
|
218
|
+
case loading
|
|
219
|
+
case loaded(Model3DAsset, String)
|
|
220
|
+
case failed
|
|
221
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
private let degreesPerPoint = 0.5
|
|
4
|
+
private let pitchLimitDegrees = 100.0
|
|
5
|
+
private let springBack = Animation.spring(response: 0.45, dampingFraction: 0.7)
|
|
6
|
+
|
|
7
|
+
/// Adds an orbital camera-style rotation gesture to a 3D model.
|
|
8
|
+
///
|
|
9
|
+
/// Both horizontal yaw (around the Y-axis) and vertical pitch (around the
|
|
10
|
+
/// X-axis, clamped to ±100° so the model can't flip past upright) are folded
|
|
11
|
+
/// directly into the bound `entityTransform` on each drag tick, so the web
|
|
12
|
+
/// layer always observes an accurate transform. On release, the pitch
|
|
13
|
+
/// elastically springs back to 0: the data is set to the unpitched final state
|
|
14
|
+
/// immediately while SwiftUI animates the existing `.rotation3DEffect` from the
|
|
15
|
+
/// old (pitched) rotation to the new (unpitched) rotation for a smooth visual.
|
|
16
|
+
struct OrbitModifier: ViewModifier {
|
|
17
|
+
let enabled: Bool
|
|
18
|
+
@Binding var entityTransform: AffineTransform3D
|
|
19
|
+
let onChange: (AffineTransform3D) -> Void
|
|
20
|
+
|
|
21
|
+
@State private var dragStartTransform: AffineTransform3D?
|
|
22
|
+
@State private var dragYawDegrees: Double = 0
|
|
23
|
+
|
|
24
|
+
func body(content: Content) -> some View {
|
|
25
|
+
content
|
|
26
|
+
.simultaneousGesture(enabled ? orbitGesture : nil)
|
|
27
|
+
.onChange(of: enabled) { _, isEnabled in
|
|
28
|
+
if !isEnabled { releaseElastic() }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private var orbitGesture: some Gesture {
|
|
33
|
+
DragGesture(minimumDistance: 0)
|
|
34
|
+
.onChanged { value in
|
|
35
|
+
let base = dragStartTransform ?? entityTransform
|
|
36
|
+
if dragStartTransform == nil { dragStartTransform = base }
|
|
37
|
+
|
|
38
|
+
dragYawDegrees = Double(value.translation.width) * degreesPerPoint
|
|
39
|
+
let pitchTarget = Double(-value.translation.height) * degreesPerPoint
|
|
40
|
+
let pitchDegrees = max(-pitchLimitDegrees, min(pitchLimitDegrees, pitchTarget))
|
|
41
|
+
|
|
42
|
+
let new = compose(base: base, yawDegrees: dragYawDegrees, pitchDegrees: pitchDegrees)
|
|
43
|
+
entityTransform = new
|
|
44
|
+
onChange(new)
|
|
45
|
+
}
|
|
46
|
+
.onEnded { _ in releaseElastic() }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private func releaseElastic() {
|
|
50
|
+
guard let base = dragStartTransform else { return }
|
|
51
|
+
let unpitched = compose(base: base, yawDegrees: dragYawDegrees, pitchDegrees: 0)
|
|
52
|
+
withAnimation(springBack) { entityTransform = unpitched }
|
|
53
|
+
onChange(unpitched)
|
|
54
|
+
dragStartTransform = nil
|
|
55
|
+
dragYawDegrees = 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private func compose(
|
|
59
|
+
base: AffineTransform3D,
|
|
60
|
+
yawDegrees: Double,
|
|
61
|
+
pitchDegrees: Double
|
|
62
|
+
) -> AffineTransform3D {
|
|
63
|
+
let translation = AffineTransform3D(translation: base.translation)
|
|
64
|
+
let yaw = AffineTransform3D(rotation: Rotation3D(angle: .degrees(yawDegrees), axis: .y))
|
|
65
|
+
let pitch = AffineTransform3D(rotation: Rotation3D(angle: .degrees(pitchDegrees), axis: .x))
|
|
66
|
+
let baseRotation = AffineTransform3D(rotation: base.rotation ?? .identity)
|
|
67
|
+
let scale = AffineTransform3D(scale: base.scale)
|
|
68
|
+
return translation
|
|
69
|
+
.concatenating(yaw)
|
|
70
|
+
.concatenating(pitch)
|
|
71
|
+
.concatenating(baseRotation)
|
|
72
|
+
.concatenating(scale)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
extension View {
|
|
77
|
+
func orbit(
|
|
78
|
+
enabled: Bool,
|
|
79
|
+
entityTransform: Binding<AffineTransform3D>,
|
|
80
|
+
onChange: @escaping (AffineTransform3D) -> Void
|
|
81
|
+
) -> some View {
|
|
82
|
+
modifier(OrbitModifier(enabled: enabled, entityTransform: entityTransform, onChange: onChange))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -5,6 +5,9 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
5
5
|
weak var model: SpatialWebViewModel?
|
|
6
6
|
var webview: WKWebView?
|
|
7
7
|
private var isObserving = false
|
|
8
|
+
private var urlObservation: NSKeyValueObservation?
|
|
9
|
+
private var canGoBackObservation: NSKeyValueObservation?
|
|
10
|
+
private var canGoForwardObservation: NSKeyValueObservation?
|
|
8
11
|
private var navigationInvoke: ((_ data: URL) -> Bool)?
|
|
9
12
|
private var openWindowInvoke: ((_ data: URL) -> WebViewElementInfo?)?
|
|
10
13
|
private var webviewStateChangeInvoke: ((_ type: SpatialWebViewState) -> Void)?
|
|
@@ -205,32 +208,37 @@ class SpatialWebController: NSObject, WKNavigationDelegate, WKScriptMessageHandl
|
|
|
205
208
|
|
|
206
209
|
func startObserving() {
|
|
207
210
|
guard !isObserving else { return }
|
|
208
|
-
webview?.
|
|
211
|
+
urlObservation = webview?.observe(\.url, options: [.new]) { [weak self] webView, _ in
|
|
212
|
+
guard let self, let url = webView.url?.absoluteString else { return }
|
|
213
|
+
Task { @MainActor in
|
|
214
|
+
self.model?.url = url
|
|
215
|
+
self.webviewStateChangeInvoke?(.didUpdateURL)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
canGoBackObservation = webview?.observe(\.canGoBack, options: [.new]) { [weak self] _, _ in
|
|
219
|
+
Task { @MainActor in
|
|
220
|
+
self?.webviewStateChangeInvoke?(.didUpdateNavigationState)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
canGoForwardObservation = webview?.observe(\.canGoForward, options: [.new]) { [weak self] _, _ in
|
|
224
|
+
Task { @MainActor in
|
|
225
|
+
self?.webviewStateChangeInvoke?(.didUpdateNavigationState)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
209
228
|
isObserving = true
|
|
210
229
|
}
|
|
211
230
|
|
|
212
231
|
func stopObserving() {
|
|
213
232
|
guard isObserving else { return }
|
|
214
|
-
|
|
233
|
+
urlObservation?.invalidate()
|
|
234
|
+
canGoBackObservation?.invalidate()
|
|
235
|
+
canGoForwardObservation?.invalidate()
|
|
236
|
+
urlObservation = nil
|
|
237
|
+
canGoBackObservation = nil
|
|
238
|
+
canGoForwardObservation = nil
|
|
215
239
|
isObserving = false
|
|
216
240
|
}
|
|
217
241
|
|
|
218
|
-
override func observeValue(
|
|
219
|
-
forKeyPath keyPath: String?,
|
|
220
|
-
of object: Any?,
|
|
221
|
-
change: [NSKeyValueChangeKey: Any]?,
|
|
222
|
-
context: UnsafeMutableRawPointer?
|
|
223
|
-
) {
|
|
224
|
-
if keyPath == #keyPath(WKWebView.url),
|
|
225
|
-
let url = (object as? WKWebView)?.url?.absoluteString
|
|
226
|
-
{
|
|
227
|
-
DispatchQueue.main.async {
|
|
228
|
-
// print("url change", url)
|
|
229
|
-
self.model?.url = url
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
242
|
func destroy() {
|
|
235
243
|
destroyView()
|
|
236
244
|
navigationInvoke = nil
|
|
@@ -10,7 +10,7 @@ class SpatialWebViewModel {
|
|
|
10
10
|
private var navigationList: [String: (_ data: URL) -> Bool] = [:]
|
|
11
11
|
private var openWindowList: [String: (_ data: URL) -> WebViewElementInfo?] = [:]
|
|
12
12
|
private var stateListeners: [SpatialWebViewState: [() -> Void]] = [:]
|
|
13
|
-
private var stateChangeListeners: [(_ type: SpatialWebViewState) -> Void] = []
|
|
13
|
+
private var stateChangeListeners: [(id: UUID, listener: (_ type: SpatialWebViewState) -> Void)] = []
|
|
14
14
|
private var scrollUpdateListeners: [(_ type: ScrollState, _ point: CGPoint) -> Void] = []
|
|
15
15
|
private var backgroundTransparent: Bool = false
|
|
16
16
|
|
|
@@ -150,8 +150,11 @@ class SpatialWebViewModel {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/// webview state event
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
@discardableResult
|
|
154
|
+
func addStateListener(_ event: @escaping (_ type: SpatialWebViewState) -> Void) -> UUID {
|
|
155
|
+
let id = UUID()
|
|
156
|
+
stateChangeListeners.append((id: id, listener: event))
|
|
157
|
+
return id
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
func addStateListener(_ state: SpatialWebViewState, _ event: @escaping () -> Void) {
|
|
@@ -161,13 +164,13 @@ class SpatialWebViewModel {
|
|
|
161
164
|
stateListeners[state]?.append(event)
|
|
162
165
|
}
|
|
163
166
|
|
|
164
|
-
func removeStateListener(_
|
|
167
|
+
func removeStateListener(_ id: UUID) {
|
|
165
168
|
stateChangeListeners.removeAll(where: {
|
|
166
|
-
$0
|
|
169
|
+
$0.id == id
|
|
167
170
|
})
|
|
168
171
|
}
|
|
169
172
|
|
|
170
|
-
func removeStateListener(_ state: SpatialWebViewState, _ event: @escaping (
|
|
173
|
+
func removeStateListener(_ state: SpatialWebViewState, _ event: @escaping () -> Void) {
|
|
171
174
|
stateListeners[state]?.removeAll(where: {
|
|
172
175
|
$0 as AnyObject === event as AnyObject
|
|
173
176
|
})
|
|
@@ -235,7 +238,7 @@ class SpatialWebViewModel {
|
|
|
235
238
|
load()
|
|
236
239
|
}
|
|
237
240
|
for onStateChange in stateChangeListeners {
|
|
238
|
-
onStateChange(state)
|
|
241
|
+
onStateChange.listener(state)
|
|
239
242
|
}
|
|
240
243
|
stateListeners[state]?.forEach { onStateChange in
|
|
241
244
|
onStateChange()
|
|
@@ -300,6 +303,8 @@ enum SpatialWebViewState: String, CaseIterable {
|
|
|
300
303
|
case didReceive
|
|
301
304
|
case didFinishLoad
|
|
302
305
|
case didFailLoad
|
|
306
|
+
case didUpdateURL
|
|
307
|
+
case didUpdateNavigationState
|
|
303
308
|
case didUnload
|
|
304
309
|
case didClose
|
|
305
310
|
case didMakeView
|
|
@@ -14,7 +14,8 @@
|
|
|
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
16
|
2B1008A22F0B000800000002 /* NavigationCleanupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */; };
|
|
17
|
-
|
|
17
|
+
2B1008A52F0B000800000005 /* SpatializedElementMotionTimelineSamplerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B1008A42F0B000800000004 /* SpatializedElementMotionTimelineSamplerTests.swift */; };
|
|
18
|
+
2B1008A72F0B000800000007 /* SpatializedElementAnimationManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B1008A62F0B000800000006 /* SpatializedElementAnimationManagerTests.swift */; };
|
|
18
19
|
2B2F1D712BEBFAAC006897EE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B2F1D702BEBFAAC006897EE /* Assets.xcassets */; };
|
|
19
20
|
2B2F1D742BEBFAAC006897EE /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2B2F1D732BEBFAAC006897EE /* Preview Assets.xcassets */; };
|
|
20
21
|
2B89E38C2E699104004079AA /* WebMsgCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B89E38B2E699104004079AA /* WebMsgCommand.swift */; };
|
|
@@ -39,12 +40,14 @@
|
|
|
39
40
|
2B06AEC32E4C1AE8000327E9 /* WebSpatialApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSpatialApp.swift; sourceTree = "<group>"; };
|
|
40
41
|
2B06AEC42E4C1AE8000327E9 /* Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = "<group>"; };
|
|
41
42
|
2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCleanupTests.swift; sourceTree = "<group>"; };
|
|
43
|
+
2B1008A42F0B000800000004 /* SpatializedElementMotionTimelineSamplerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpatializedElementMotionTimelineSamplerTests.swift; sourceTree = "<group>"; };
|
|
44
|
+
2B1008A62F0B000800000006 /* SpatializedElementAnimationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpatializedElementAnimationManagerTests.swift; sourceTree = "<group>"; };
|
|
42
45
|
2B2F1D632BEBFAAA006897EE /* WebSpatial.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WebSpatial.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
43
46
|
2B2F1D672BEBFAAA006897EE /* RealityKitContent */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RealityKitContent; sourceTree = "<group>"; };
|
|
44
47
|
2B2F1D702BEBFAAC006897EE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
|
45
48
|
2B2F1D732BEBFAAC006897EE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
|
46
49
|
2B2F1D752BEBFAAC006897EE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
|
47
|
-
2B2F1D7A2BEBFAAD006897EE /* WebSpatial.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0;
|
|
50
|
+
2B2F1D7A2BEBFAAD006897EE /* WebSpatial.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = WebSpatial.xctest; path = "web-spatialTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
48
51
|
2B89E38B2E699104004079AA /* WebMsgCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebMsgCommand.swift; sourceTree = "<group>"; };
|
|
49
52
|
2BDBF9B52C4ED9F600D269D7 /* static-web */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "static-web"; sourceTree = "<group>"; };
|
|
50
53
|
/* End PBXFileReference section */
|
|
@@ -64,6 +67,8 @@
|
|
|
64
67
|
2B06AF312E4C1AF8000327E9 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
|
65
68
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
66
69
|
membershipExceptions = (
|
|
70
|
+
dynamic3d/EntityAnimationManager.swift,
|
|
71
|
+
dynamic3d/EntityAnimationSession.swift,
|
|
67
72
|
dynamic3d/Geometry.swift,
|
|
68
73
|
dynamic3d/SpatialComponent.swift,
|
|
69
74
|
dynamic3d/SpatialEntity.swift,
|
|
@@ -72,6 +77,16 @@
|
|
|
72
77
|
dynamic3d/SpatialModelResource.swift,
|
|
73
78
|
dynamic3d/SpatialRootEntity.swift,
|
|
74
79
|
dynamic3d/SpatialTextureResource.swift,
|
|
80
|
+
"element-motion/SpatializedElementAnimationCommand.swift",
|
|
81
|
+
"element-motion/SpatializedElementAnimationManager.swift",
|
|
82
|
+
"element-motion/SpatializedElementAnimationObject.swift",
|
|
83
|
+
"element-motion/SpatializedElementAnimationTypes.swift",
|
|
84
|
+
"element-motion/SpatializedElementAnimationWriteAdapter.swift",
|
|
85
|
+
"element-motion/SpatializedElementMotionBridgeTypes.swift",
|
|
86
|
+
"element-motion/SpatializedElementMotionTimelineSampler.swift",
|
|
87
|
+
"element-motion/SpatializedElementMotionTiming.swift",
|
|
88
|
+
"element-motion/SpatializedElementMotionTransformTypes.swift",
|
|
89
|
+
MaterialSceneRefresh.swift,
|
|
75
90
|
SpatialApp.swift,
|
|
76
91
|
Spatialized2DElement.swift,
|
|
77
92
|
SpatializedDynamic3DElement.swift,
|
|
@@ -113,6 +128,7 @@
|
|
|
113
128
|
SpatialSceneView.swift,
|
|
114
129
|
"view-modifier/HideViewModifier.swift",
|
|
115
130
|
"view-modifier/MaterialWithBorderCornerModifier.swift",
|
|
131
|
+
"view-modifier/OrbitModifier.swift",
|
|
116
132
|
);
|
|
117
133
|
target = 2B2F1D622BEBFAAA006897EE /* web-spatial */;
|
|
118
134
|
};
|
|
@@ -142,7 +158,6 @@
|
|
|
142
158
|
isa = PBXFrameworksBuildPhase;
|
|
143
159
|
buildActionMask = 2147483647;
|
|
144
160
|
files = (
|
|
145
|
-
2B2F1D692BEBFAAA006897EE /* RealityKitContent in Frameworks */,
|
|
146
161
|
);
|
|
147
162
|
runOnlyForDeploymentPostprocessing = 0;
|
|
148
163
|
};
|
|
@@ -160,6 +175,8 @@
|
|
|
160
175
|
isa = PBXGroup;
|
|
161
176
|
children = (
|
|
162
177
|
2B1008A12F0B000800000001 /* NavigationCleanupTests.swift */,
|
|
178
|
+
2B1008A42F0B000800000004 /* SpatializedElementMotionTimelineSamplerTests.swift */,
|
|
179
|
+
2B1008A62F0B000800000006 /* SpatializedElementAnimationManagerTests.swift */,
|
|
163
180
|
);
|
|
164
181
|
path = "web-spatialTests";
|
|
165
182
|
sourceTree = "<group>";
|
|
@@ -244,7 +261,6 @@
|
|
|
244
261
|
);
|
|
245
262
|
name = "web-spatial";
|
|
246
263
|
packageProductDependencies = (
|
|
247
|
-
2B2F1D682BEBFAAA006897EE /* RealityKitContent */,
|
|
248
264
|
);
|
|
249
265
|
productName = "web-spatial";
|
|
250
266
|
productReference = 2B2F1D632BEBFAAA006897EE /* WebSpatial.app */;
|
|
@@ -348,6 +364,8 @@
|
|
|
348
364
|
buildActionMask = 2147483647;
|
|
349
365
|
files = (
|
|
350
366
|
2B1008A22F0B000800000002 /* NavigationCleanupTests.swift in Sources */,
|
|
367
|
+
2B1008A52F0B000800000005 /* SpatializedElementMotionTimelineSamplerTests.swift in Sources */,
|
|
368
|
+
2B1008A72F0B000800000007 /* SpatializedElementAnimationManagerTests.swift in Sources */,
|
|
351
369
|
);
|
|
352
370
|
runOnlyForDeploymentPostprocessing = 0;
|
|
353
371
|
};
|
|
@@ -544,13 +562,14 @@
|
|
|
544
562
|
GENERATE_INFOPLIST_FILE = YES;
|
|
545
563
|
MARKETING_VERSION = 1.0;
|
|
546
564
|
PRODUCT_BUNDLE_IDENTIFIER = com.webspatial.test;
|
|
547
|
-
|
|
565
|
+
PRODUCT_MODULE_NAME = WebSpatialTests;
|
|
566
|
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
548
567
|
SUPPORTED_PLATFORMS = "xros xrsimulator";
|
|
549
568
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
550
569
|
SWIFT_VERSION = 5.0;
|
|
551
570
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
|
552
|
-
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/
|
|
553
|
-
XROS_DEPLOYMENT_TARGET =
|
|
571
|
+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WebSpatial.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WebSpatial";
|
|
572
|
+
XROS_DEPLOYMENT_TARGET = 26.0;
|
|
554
573
|
};
|
|
555
574
|
name = Debug;
|
|
556
575
|
};
|
|
@@ -563,13 +582,14 @@
|
|
|
563
582
|
GENERATE_INFOPLIST_FILE = YES;
|
|
564
583
|
MARKETING_VERSION = 1.0;
|
|
565
584
|
PRODUCT_BUNDLE_IDENTIFIER = com.webspatial.test;
|
|
566
|
-
|
|
585
|
+
PRODUCT_MODULE_NAME = WebSpatialTests;
|
|
586
|
+
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
567
587
|
SUPPORTED_PLATFORMS = "xros xrsimulator";
|
|
568
588
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
|
569
589
|
SWIFT_VERSION = 5.0;
|
|
570
590
|
TARGETED_DEVICE_FAMILY = "1,2,7";
|
|
571
|
-
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/
|
|
572
|
-
XROS_DEPLOYMENT_TARGET =
|
|
591
|
+
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/WebSpatial.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/WebSpatial";
|
|
592
|
+
XROS_DEPLOYMENT_TARGET = 26.0;
|
|
573
593
|
};
|
|
574
594
|
name = Release;
|
|
575
595
|
};
|
|
@@ -604,13 +624,6 @@
|
|
|
604
624
|
defaultConfigurationName = Release;
|
|
605
625
|
};
|
|
606
626
|
/* End XCConfigurationList section */
|
|
607
|
-
|
|
608
|
-
/* Begin XCSwiftPackageProductDependency section */
|
|
609
|
-
2B2F1D682BEBFAAA006897EE /* RealityKitContent */ = {
|
|
610
|
-
isa = XCSwiftPackageProductDependency;
|
|
611
|
-
productName = RealityKitContent;
|
|
612
|
-
};
|
|
613
|
-
/* End XCSwiftPackageProductDependency section */
|
|
614
627
|
};
|
|
615
628
|
rootObject = 2B2F1D5B2BEBFAAA006897EE /* Project object */;
|
|
616
629
|
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<BuildableReference
|
|
37
37
|
BuildableIdentifier = "primary"
|
|
38
38
|
BlueprintIdentifier = "2B2F1D792BEBFAAD006897EE"
|
|
39
|
-
BuildableName = "
|
|
39
|
+
BuildableName = "web-spatialTests.xctest"
|
|
40
40
|
BlueprintName = "web-spatialTests"
|
|
41
41
|
ReferencedContainer = "container:web-spatial.xcodeproj">
|
|
42
42
|
</BuildableReference>
|
|
@@ -18,7 +18,8 @@ final class NavigationCleanupTests: XCTestCase {
|
|
|
18
18
|
id: "test-attachment",
|
|
19
19
|
parentEntityId: "test-parent-entity",
|
|
20
20
|
position: SIMD3<Float>(0, 0, 0),
|
|
21
|
-
size: CGSize(width: 100, height: 100)
|
|
21
|
+
size: CGSize(width: 100, height: 100),
|
|
22
|
+
webViewModel: SpatialWebViewModel(url: "http://localhost:5173/")
|
|
22
23
|
)
|
|
23
24
|
XCTAssertFalse(scene.attachmentManager.attachments.isEmpty)
|
|
24
25
|
|