@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.
Files changed (40) hide show
  1. package/package.json +2 -2
  2. package/web-spatial/JSBCommand.swift +12 -0
  3. package/web-spatial/WebMsgCommand.swift +29 -2
  4. package/web-spatial/WebSpatialApp.swift +26 -8
  5. package/web-spatial/manager/Dynamic3DManager.swift +97 -37
  6. package/web-spatial/manifest.swift +2 -1
  7. package/web-spatial/model/MaterialSceneRefresh.swift +41 -0
  8. package/web-spatial/model/SpatialScene.swift +255 -45
  9. package/web-spatial/model/Spatialized2DElement.swift +2 -11
  10. package/web-spatial/model/SpatializedElement.swift +38 -0
  11. package/web-spatial/model/SpatializedStatic3DElement.swift +31 -4
  12. package/web-spatial/model/dynamic3d/EntityAnimationManager.swift +356 -0
  13. package/web-spatial/model/dynamic3d/EntityAnimationSession.swift +162 -0
  14. package/web-spatial/model/dynamic3d/SpatialMaterial.swift +26 -10
  15. package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +5 -3
  16. package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +25 -0
  17. package/web-spatial/model/element-motion/SpatializedElementAnimationCommand.swift +15 -0
  18. package/web-spatial/model/element-motion/SpatializedElementAnimationManager.swift +263 -0
  19. package/web-spatial/model/element-motion/SpatializedElementAnimationObject.swift +399 -0
  20. package/web-spatial/model/element-motion/SpatializedElementAnimationTypes.swift +45 -0
  21. package/web-spatial/model/element-motion/SpatializedElementAnimationWriteAdapter.swift +65 -0
  22. package/web-spatial/model/element-motion/SpatializedElementMotionBridgeTypes.swift +43 -0
  23. package/web-spatial/model/element-motion/SpatializedElementMotionTimelineSampler.swift +99 -0
  24. package/web-spatial/model/element-motion/SpatializedElementMotionTiming.swift +99 -0
  25. package/web-spatial/model/element-motion/SpatializedElementMotionTransformTypes.swift +22 -0
  26. package/web-spatial/protocol/SpatializedElementContainer.swift +1 -1
  27. package/web-spatial/view/SpatialNavView.swift +37 -12
  28. package/web-spatial/view/SpatialSceneContentView.swift +9 -12
  29. package/web-spatial/view/Spatialized2DElementView.swift +10 -17
  30. package/web-spatial/view/SpatializedDynamic3DView.swift +2 -6
  31. package/web-spatial/view/SpatializedElementView.swift +3 -2
  32. package/web-spatial/view/SpatializedStatic3DView.swift +104 -43
  33. package/web-spatial/view/view-modifier/OrbitModifier.swift +84 -0
  34. package/web-spatial/webview/SpatialWebController.swift +26 -18
  35. package/web-spatial/webview/SpatialWebViewModel.swift +12 -7
  36. package/web-spatial.xcodeproj/project.pbxproj +30 -17
  37. package/web-spatial.xcodeproj/xcshareddata/xcschemes/web-spatial.xcscheme +1 -1
  38. package/web-spatialTests/NavigationCleanupTests.swift +2 -1
  39. package/web-spatialTests/SpatializedElementAnimationManagerTests.swift +1466 -0
  40. 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
- @Environment(SpatializedElement.self) var spatializedElement: SpatializedElement
5
+ @Bindable var spatializedStatic3DElement: SpatializedStatic3DElement
6
6
  @Environment(SpatialScene.self) var spatialScene: SpatialScene
7
7
 
8
- @State private var asset: Model3DAsset?
9
- @State private var source: String?
10
- @State private var isLoading = false
8
+ @State private var loadState: LoadState = .idle
11
9
 
12
- private var spatializedStatic3DElement: SpatializedStatic3DElement {
13
- return spatializedElement as! SpatializedStatic3DElement
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(spatializedElement.id, ModelLoadSuccess(src: src))
16
+ spatialScene.sendWebMsg(spatializedStatic3DElement.id, ModelLoadSuccess(src: src))
18
17
  }
19
18
 
20
19
  func onLoadFailure() {
21
- spatialScene.sendWebMsg(spatializedElement.id, ModelLoadFailure())
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 = spatializedElement.depth
26
- let transform = spatializedStatic3DElement.modelTransform
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 = spatializedElement.enableGesture
35
- if !spatializedStatic3DElement.allSources.isEmpty {
37
+ let enableGesture = spatializedStatic3DElement.enableGesture
38
+ if spatializedStatic3DElement.loading == .eager {
36
39
  Group {
37
- if isLoading {
38
- ProgressView()
39
- } else if let asset, let source {
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
- } else {
54
- Text("").onAppear {
55
- self.onLoadFailure()
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
- x: scale.width,
61
- y: scale.height,
62
- z: scale.depth
63
- )
64
- .rotation3DEffect(
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) { asset?.animationPlaybackController?.speed = Float($1) }
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
- spatializedElement.id,
153
+ spatializedStatic3DElement.id,
108
154
  AnimationStateChangeEvent(
109
- detail: AnimationStateChangeDetail(paused: isPaused, duration: duration)
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
- isLoading = true
181
+ loadState = .loading
131
182
  let result = await loadSources(spatializedStatic3DElement.allSources)
132
- asset = result?.asset
133
- source = result?.url.absoluteString
134
- if spatializedStatic3DElement.autoplay {
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: false) }
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?.addObserver(self, forKeyPath: #keyPath(WKWebView.url), options: .new, context: nil)
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
- webview?.removeObserver(self, forKeyPath: #keyPath(WKWebView.url))
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
- func addStateListener(_ event: @escaping (_ type: SpatialWebViewState) -> Void) {
154
- stateChangeListeners.append(event)
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(_ event: @escaping (_ type: String) -> Void) {
167
+ func removeStateListener(_ id: UUID) {
165
168
  stateChangeListeners.removeAll(where: {
166
- $0 as AnyObject === event as AnyObject
169
+ $0.id == id
167
170
  })
168
171
  }
169
172
 
170
- func removeStateListener(_ state: SpatialWebViewState, _ event: @escaping (_ object: Any, _ data: Any) -> Void) {
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
- 2B2F1D692BEBFAAA006897EE /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; productRef = 2B2F1D682BEBFAAA006897EE /* RealityKitContent */; };
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; path = WebSpatial.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
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
- PRODUCT_NAME = WebSpatial;
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)/web-spatial.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/web-spatial";
553
- XROS_DEPLOYMENT_TARGET = 1.1;
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
- PRODUCT_NAME = WebSpatial;
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)/web-spatial.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/web-spatial";
572
- XROS_DEPLOYMENT_TARGET = 1.1;
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 = "WebSpatial.xctest"
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