@webspatial/platform-visionos 1.7.0 → 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 (35) hide show
  1. package/package.json +1 -1
  2. package/web-spatial/JSBCommand.swift +1 -0
  3. package/web-spatial/WebMsgCommand.swift +26 -2
  4. package/web-spatial/WebSpatialApp.swift +26 -8
  5. package/web-spatial/manifest.swift +1 -1
  6. package/web-spatial/model/SpatialScene.swift +178 -39
  7. package/web-spatial/model/Spatialized2DElement.swift +2 -11
  8. package/web-spatial/model/SpatializedElement.swift +38 -0
  9. package/web-spatial/model/SpatializedStatic3DElement.swift +27 -8
  10. package/web-spatial/model/dynamic3d/EntityAnimationManager.swift +356 -0
  11. package/web-spatial/model/dynamic3d/EntityAnimationSession.swift +162 -0
  12. package/web-spatial/model/element-motion/SpatializedElementAnimationCommand.swift +15 -0
  13. package/web-spatial/model/element-motion/SpatializedElementAnimationManager.swift +263 -0
  14. package/web-spatial/model/element-motion/SpatializedElementAnimationObject.swift +399 -0
  15. package/web-spatial/model/element-motion/SpatializedElementAnimationTypes.swift +45 -0
  16. package/web-spatial/model/element-motion/SpatializedElementAnimationWriteAdapter.swift +65 -0
  17. package/web-spatial/model/element-motion/SpatializedElementMotionBridgeTypes.swift +43 -0
  18. package/web-spatial/model/element-motion/SpatializedElementMotionTimelineSampler.swift +99 -0
  19. package/web-spatial/model/element-motion/SpatializedElementMotionTiming.swift +99 -0
  20. package/web-spatial/model/element-motion/SpatializedElementMotionTransformTypes.swift +22 -0
  21. package/web-spatial/protocol/SpatializedElementContainer.swift +1 -1
  22. package/web-spatial/view/SpatialNavView.swift +37 -12
  23. package/web-spatial/view/SpatialSceneContentView.swift +9 -12
  24. package/web-spatial/view/Spatialized2DElementView.swift +10 -17
  25. package/web-spatial/view/SpatializedDynamic3DView.swift +2 -6
  26. package/web-spatial/view/SpatializedElementView.swift +3 -2
  27. package/web-spatial/view/SpatializedStatic3DView.swift +32 -33
  28. package/web-spatial/view/view-modifier/OrbitModifier.swift +84 -0
  29. package/web-spatial/webview/SpatialWebController.swift +26 -18
  30. package/web-spatial/webview/SpatialWebViewModel.swift +12 -7
  31. package/web-spatial.xcodeproj/project.pbxproj +29 -8
  32. package/web-spatial.xcodeproj/xcshareddata/xcschemes/web-spatial.xcscheme +1 -1
  33. package/web-spatialTests/NavigationCleanupTests.swift +2 -1
  34. package/web-spatialTests/SpatializedElementAnimationManagerTests.swift +1466 -0
  35. package/web-spatialTests/SpatializedElementMotionTimelineSamplerTests.swift +117 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webspatial/platform-visionos",
3
- "version": "1.7.0",
3
+ "version": "2.0.0",
4
4
  "description": "Used to publish WebSpatial projects to Apple Vision Pro",
5
5
  "type": "commonjs",
6
6
  "main": "package.json",
@@ -278,6 +278,7 @@ struct UpdateSpatializedStatic3DElementProperties: SpatializedElementProperties
278
278
  let currentTime: Double?
279
279
  let posterURL: String?
280
280
  let loading: String?
281
+ let stagemode: String?
281
282
  }
282
283
 
283
284
  struct UpdateSpatializedDynamic3DElementProperties: SpatializedElementProperties {
@@ -24,9 +24,8 @@ enum SpatialWebMsgType: String, Encodable {
24
24
  case spatialrotateend
25
25
  case spatialmagnify
26
26
  case spatialmagnifyend
27
-
28
27
  case animationstatechange
29
-
28
+ case entitytransformchange
30
29
  case objectdestroy
31
30
  }
32
31
 
@@ -129,6 +128,31 @@ struct AnimationStateChangeEvent: Encodable {
129
128
  let detail: AnimationStateChangeDetail
130
129
  }
131
130
 
131
+ /// 16-element column-major representation of the new `entityTransform`,
132
+ /// matching the wire format that JS already uses when sending the matrix to
133
+ /// native via `UpdateSpatializedStatic3DElementProperties.modelTransform`.
134
+ struct EntityTransformChangeDetail: Encodable {
135
+ let transform: [Double]
136
+ }
137
+
138
+ struct EntityTransformChangeEvent: Encodable {
139
+ let type: SpatialWebMsgType = .entitytransformchange
140
+ let detail: EntityTransformChangeDetail
141
+
142
+ /// Emits the transform to the web layer as a 16-element column-major matrix.
143
+ init(_ transform: AffineTransform3D) {
144
+ let m = transform.matrix
145
+ let c0 = m.columns.0, c1 = m.columns.1, c2 = m.columns.2, c3 = m.columns.3
146
+ let array: [Double] = [
147
+ c0.x, c0.y, c0.z, 0,
148
+ c1.x, c1.y, c1.z, 0,
149
+ c2.x, c2.y, c2.z, 0,
150
+ c3.x, c3.y, c3.z, 1,
151
+ ]
152
+ detail = EntityTransformChangeDetail(transform: array)
153
+ }
154
+ }
155
+
132
156
  struct SpatialObjectDestroiedEvent: Encodable {
133
157
  let type: SpatialWebMsgType = .objectdestroy
134
158
  }
@@ -40,13 +40,31 @@ struct WebSpatialApp: App {
40
40
  )
41
41
  }
42
42
 
43
+ func getSceneOrCreate(_ sceneID: String?, _ style: SpatialScene.WindowStyle) -> SpatialScene {
44
+ if let sceneID, let spatialScene = SpatialApp.Instance.getScene(sceneID) {
45
+ return spatialScene
46
+ }
47
+
48
+ logger.debug("Missing scene for WindowGroup data. Recreating scene.")
49
+ return SpatialApp.Instance.createScene(
50
+ startURL,
51
+ style,
52
+ .visible,
53
+ app.getSceneOptions()
54
+ )
55
+ }
56
+
57
+ func getSceneOrCreate(_ sceneID: String, _ style: SpatialScene.WindowStyle) -> SpatialScene {
58
+ return getSceneOrCreate(Optional(sceneID), style)
59
+ }
60
+
43
61
  var body: some Scene {
44
62
  WindowGroup(
45
63
  id: SpatialScene.WindowStyle.window.rawValue,
46
64
  for: String.self
47
65
  ) { $windowData in
48
- let spatialScene = SpatialApp.Instance.getScene(windowData)
49
- SpatialSceneView(spatialScene: spatialScene!)
66
+ let spatialScene = getSceneOrCreate(windowData, .window)
67
+ SpatialSceneView(spatialScene: spatialScene)
50
68
  }
51
69
  defaultValue: {
52
70
  let scene = SpatialApp.Instance.createScene(
@@ -66,20 +84,20 @@ struct WebSpatialApp: App {
66
84
  )
67
85
 
68
86
  WindowGroup(id: SpatialScene.WindowStyle.volume.rawValue, for: String.self) { $windowData in
69
- let spatialScene = SpatialApp.Instance.getScene(windowData)
70
- SpatialSceneView(spatialScene: spatialScene!)
87
+ let spatialScene = getSceneOrCreate(windowData, .volume)
88
+ SpatialSceneView(spatialScene: spatialScene)
71
89
  .frame(
72
90
  minWidth: getCGFloat(
73
- app.getSceneOptions(windowData)?.resizeRange?.minWidth
91
+ spatialScene.sceneConfig?.resizeRange?.minWidth
74
92
  ),
75
93
  maxWidth: getCGFloat(
76
- app.getSceneOptions(windowData)?.resizeRange?.maxWidth
94
+ spatialScene.sceneConfig?.resizeRange?.maxWidth
77
95
  ),
78
96
  minHeight: getCGFloat(
79
- app.getSceneOptions(windowData)?.resizeRange?.minHeight
97
+ spatialScene.sceneConfig?.resizeRange?.minHeight
80
98
  ),
81
99
  maxHeight: getCGFloat(
82
- app.getSceneOptions(windowData)?.resizeRange?.maxHeight
100
+ spatialScene.sceneConfig?.resizeRange?.maxHeight
83
101
  )
84
102
  )
85
103
  }
@@ -6,7 +6,7 @@ var pwaManager = PWAManager()
6
6
  struct PWAManager: Codable {
7
7
  var isLocal: Bool = false
8
8
 
9
- // var start_url: String = "http://localhost:5173/#/"
9
+ /// var start_url: String = "http://localhost:5173/#/"
10
10
  var start_url: String = "http://localhost:5173/#/runtime-capabilities"
11
11
 
12
12
  // var start_url: String = "http://localhost:5173/#/spatial-drag-gesture"
@@ -1,5 +1,6 @@
1
1
  import Combine
2
2
  import Foundation
3
+ import Observation
3
4
  import RealityKit
4
5
  import simd
5
6
  import SwiftUI
@@ -40,6 +41,15 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
40
41
  var parent: (any ScrollAbleSpatialElementContainer)?
41
42
 
42
43
  var attachmentManager = AttachmentManager()
44
+ /// NOTE: `@Observable` + `lazy` 在新版本 Swift 宏展开下会触发编译器报错(init accessor 访问 backing storage)。
45
+ /// 这里不需要让动画管理器参与 Observation,避免生成 `@ObservationTracked` 相关访问器即可。
46
+ @ObservationIgnored
47
+ lazy var animationManager: EntityAnimationManager = .init(scene: self)
48
+
49
+ @ObservationIgnored
50
+ lazy var elementAnimationManager: SpatializedElementAnimationManager = .init(sendWebMsg: { [weak self] id, msg in
51
+ self?.sendWebMsg(id, msg)
52
+ })
43
53
 
44
54
  /// Enum
45
55
  enum WindowStyle: String, Codable, CaseIterable {
@@ -331,6 +341,10 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
331
341
 
332
342
  spatialWebViewModel.addJSBListener(UpdateAttachmentEntityCommand.self, onUpdateAttachmentEntity)
333
343
 
344
+ spatialWebViewModel.addJSBListener(AnimateTransformCommand.self, onAnimateTransform)
345
+
346
+ spatialWebViewModel.addJSBListener(CreateSpatializedElementAnimationCommand.self, onCreateSpatializedElementAnimation)
347
+ spatialWebViewModel.addJSBListener(ControlSpatializedElementAnimationCommand.self, onControlSpatializedElementAnimation)
334
348
  spatialWebViewModel.addOpenWindowListener(protocal: "webspatial", onOpenWindowHandler)
335
349
 
336
350
  spatialWebViewModel
@@ -361,6 +375,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
361
375
  }
362
376
 
363
377
  var didFailLoad = false
378
+ private var currentPageGeneration = 0
364
379
 
365
380
  private func setupWebViewStateListener() {
366
381
  spatialWebViewModel.addStateListener(.didStartLoad) {
@@ -378,6 +393,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
378
393
  }
379
394
 
380
395
  spatialWebViewModel.addStateListener(.didReceive) {
396
+ self.injectPageEpoch()
381
397
  if let meterToPtUnscaled = self.meterToPtUnscaled,
382
398
  let meterToPtScaled = self.meterToPtScaled
383
399
  {
@@ -394,27 +410,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
394
410
 
395
411
  spatialWebViewModel.addStateListener(.didFinishLoad) {
396
412
  if self.state == .pending {
397
- self.checkHookExist()
398
- }
399
- }
400
- }
401
-
402
- private func checkHookExist(_ completion: ((Bool) -> Void)? = nil) {
403
- let js = """
404
- (function() {
405
- return typeof window.xrCurrentSceneDefaults !== 'undefined';
406
- })();
407
- """
408
-
409
- spatialWebViewModel.evaluateJS(js) { result in
410
- let exists = result as? Bool ?? false
411
-
412
- if let completion = completion {
413
- completion(exists)
414
- } else {
415
- if !exists {
416
- self.moveToState(.willVisible, defaultSceneConfig)
417
- }
413
+ self.moveToState(.willVisible, self.sceneConfig ?? defaultSceneConfig)
418
414
  }
419
415
  }
420
416
  }
@@ -425,11 +421,17 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
425
421
  return handleWindowOpenCustom(url)
426
422
  } else if host == "createAttachment" {
427
423
  return handleCreateAttachment(url)
428
- } else {
424
+ } else if host == "createSpatialized2DElement" {
425
+ guard shouldAcceptSpatialRequest(url, command: host) else {
426
+ return nil
427
+ }
429
428
  let spatialized2DElement: Spatialized2DElement = createSpatializedElement(
430
429
  .Spatialized2DElement
431
430
  )
432
431
  return WebViewElementInfo(id: spatialized2DElement.id, element: spatialized2DElement.getWebViewModel())
432
+ } else {
433
+ logger.warning("Unknown webspatial open-window command: \(host)")
434
+ return nil
433
435
  }
434
436
  }
435
437
 
@@ -437,6 +439,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
437
439
  private var pendingAttachmentWebViewModels = [String: SpatialWebViewModel]()
438
440
 
439
441
  private func handleCreateAttachment(_ url: URL) -> WebViewElementInfo? {
442
+ guard shouldAcceptSpatialRequest(url, command: "createAttachment") else {
443
+ return nil
444
+ }
440
445
  // Just create a bare webview — metadata arrives via InitializeAttachment JSB
441
446
  let id = UUID().uuidString
442
447
  let webViewModel = SpatialWebViewModel(url: nil)
@@ -481,6 +486,12 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
481
486
  }
482
487
 
483
488
  private func onPageStartLoad() {
489
+ currentPageGeneration += 1
490
+ injectPageEpoch()
491
+ logger.debug("SpatialScene page generation advanced to \(currentPageGeneration)")
492
+ // Clean up all animation sessions
493
+ animationManager.removeAll()
494
+ elementAnimationManager.removeAll()
484
495
  // destroy all SpatialObject asset
485
496
  let spatialObjectArray = spatialObjects.map { $0.value }
486
497
  for spatialObject in spatialObjectArray {
@@ -490,6 +501,50 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
490
501
  backgroundMaterial = .None
491
502
  }
492
503
 
504
+ private func injectPageEpoch() {
505
+ // Mirror the native page generation into JS so frontend-created
506
+ // webspatial:// requests can carry page ownership metadata.
507
+ let js = """
508
+ window.__webspatialsdk__ = window.__webspatialsdk__ || {};
509
+ window.__webspatialsdk__.pageEpoch = "\(currentPageGeneration)";
510
+ """
511
+ spatialWebViewModel.getController().callJS(js)
512
+ }
513
+
514
+ private struct SpatialRequestMetadata {
515
+ let rid: String?
516
+ let pageEpoch: String?
517
+ }
518
+
519
+ private func parseSpatialRequestMetadata(_ url: URL) -> SpatialRequestMetadata {
520
+ let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems ?? []
521
+ let rid = items.first { $0.name == "rid" }?.value
522
+ let pageEpoch = items.first { $0.name == "wsepoch" }?.value
523
+ return SpatialRequestMetadata(rid: rid, pageEpoch: pageEpoch)
524
+ }
525
+
526
+ private func shouldAcceptSpatialRequest(_ url: URL, command: String) -> Bool {
527
+ let metadata = parseSpatialRequestMetadata(url)
528
+
529
+ guard let pageEpoch = metadata.pageEpoch, !pageEpoch.isEmpty else {
530
+ // Compatibility mode for older frontend bundles that do not emit
531
+ // request epoch yet.
532
+ logger.warning("SpatialScene accepts \(command) without wsepoch in compatibility mode, rid=\(metadata.rid ?? "nil")")
533
+ return true
534
+ }
535
+
536
+ let currentEpoch = String(currentPageGeneration)
537
+ if pageEpoch != currentEpoch {
538
+ // Drop requests that were initiated by an older page generation so
539
+ // they cannot recreate stale 2D content after refresh.
540
+ logger.warning("SpatialScene drops stale \(command), rid=\(metadata.rid ?? "nil"), requestEpoch=\(pageEpoch), currentEpoch=\(currentEpoch)")
541
+ return false
542
+ }
543
+
544
+ logger.debug("SpatialScene accepts \(command), rid=\(metadata.rid ?? "nil"), epoch=\(pageEpoch)")
545
+ return true
546
+ }
547
+
493
548
  /// Some SPA navigations (history back/forward) do not trigger a full WKNavigation
494
549
  /// lifecycle. SpatialNavView calls this before navigation actions to ensure
495
550
  /// previously-created spatial objects are cleaned up.
@@ -551,7 +606,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
551
606
  spatialObject.sources = sources
552
607
  }
553
608
  if let loading = command.loading {
554
- spatialObject.loading = loading
609
+ spatialObject.loading = Loading(stringValue: loading)
555
610
  }
556
611
 
557
612
  resolve(.success(AddSpatializedElementReply(id: spatialObject.id)))
@@ -615,7 +670,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
615
670
  let column3 = simd_double4(array[12], array[13], array[14], array[15])
616
671
  let simd_double4x4 = simd_double4x4(columns: (column0, column1, column2, column3))
617
672
  let affineTransform3D = AffineTransform3D(truncating: simd_double4x4)
618
- spatializedElement.modelTransform = affineTransform3D
673
+ if !spatializedElement.animatingMask.locksTransform {
674
+ spatializedElement.entityTransform = affineTransform3D
675
+ }
619
676
  }
620
677
 
621
678
  if let autoplay = command.autoplay {
@@ -643,7 +700,11 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
643
700
  }
644
701
 
645
702
  if let loading = command.loading {
646
- spatializedElement.loading = loading
703
+ spatializedElement.loading = Loading(stringValue: loading)
704
+ }
705
+
706
+ if let stagemode = command.stagemode {
707
+ spatializedElement.stagemode = StageMode(stringValue: stagemode)
647
708
  }
648
709
 
649
710
  resolve(.success(baseReplyData))
@@ -755,7 +816,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
755
816
  }
756
817
 
757
818
  if let opacity = command.opacity {
758
- spatializedElement.opacity = opacity
819
+ if !spatializedElement.animatingMask.locksOpacity {
820
+ spatializedElement.opacity = opacity
821
+ }
759
822
  }
760
823
 
761
824
  if let scrollWithParent = command.scrollWithParent {
@@ -821,6 +884,11 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
821
884
  return resolve(.failure(JsbError(code: .InvalidMatrix, message: "invalid UpdateSpatializedElementTransform matrix should have length 16!")))
822
885
  }
823
886
 
887
+ if spatializedElement.animatingMask.locksTransform {
888
+ resolve(.success(baseReplyData))
889
+ return
890
+ }
891
+
824
892
  let column0 = simd_double4(array[0], array[1], array[2], array[3])
825
893
  let column1 = simd_double4(array[4], array[5], array[6], array[7])
826
894
  let column2 = simd_double4(array[8], array[9], array[10], array[11])
@@ -859,17 +927,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
859
927
  children.removeValue(forKey: spatializedElement.id)
860
928
  }
861
929
 
862
- func getChildrenOfType(_ type: SpatializedElementType) -> [String: SpatializedElement] {
863
- return children.filter {
864
- switch type {
865
- case .Spatialized2DElement:
866
- return $0.value is Spatialized2DElement
867
- case .SpatializedStatic3DElement:
868
- return $0.value is SpatializedStatic3DElement
869
- case .SpatializedDynamic3DElement:
870
- return $0.value is SpatializedDynamic3DElement
871
- }
872
- }
930
+ func getChildren<T: SpatializedElement>(ofType type: T.Type) -> [T] {
931
+ return children.values.compactMap { $0 as? T }
873
932
  }
874
933
 
875
934
  func getChildren() -> [String: SpatializedElement] {
@@ -1289,6 +1348,76 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1289
1348
  }
1290
1349
  }
1291
1350
 
1351
+ private func onAnimateTransform(command: AnimateTransformCommand, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1352
+ switch command.type {
1353
+ case "play":
1354
+ guard let entityId = command.entityId else {
1355
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "AnimateTransform play: entityId is required")))
1356
+ return
1357
+ }
1358
+ guard let entity: SpatialEntity = findSpatialObject(entityId) else {
1359
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "AnimateTransform play: entity \(entityId) not found")))
1360
+ return
1361
+ }
1362
+ animationManager.handlePlay(command: command, entity: entity, resolve: resolve)
1363
+
1364
+ case "pause":
1365
+ animationManager.handlePause(command: command, resolve: resolve)
1366
+
1367
+ case "resume":
1368
+ guard let session = animationManager.getSession(command.animationId),
1369
+ let entity: SpatialEntity = findSpatialObject(session.entityId)
1370
+ else {
1371
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "AnimateTransform resume: session or entity not found")))
1372
+ return
1373
+ }
1374
+ animationManager.handleResume(command: command, entity: entity, resolve: resolve)
1375
+
1376
+ case "cancel":
1377
+ guard let session = animationManager.getSession(command.animationId),
1378
+ let entity: SpatialEntity = findSpatialObject(session.entityId)
1379
+ else {
1380
+ // Session may have already been cleaned up - acknowledge silently.
1381
+ resolve(.success(nil))
1382
+ return
1383
+ }
1384
+ animationManager.handleCancel(command: command, entity: entity, resolve: resolve)
1385
+
1386
+ default:
1387
+ resolve(.failure(JsbError(code: .TypeError, message: "AnimateTransform: unknown command type '\(command.type)'")))
1388
+ }
1389
+ }
1390
+
1391
+ private func onCreateSpatializedElementAnimation(command: CreateSpatializedElementAnimationCommand, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1392
+ guard let element: SpatializedElement = findSpatialObject(command.elementId) else {
1393
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "CreateSpatializedElementAnimation play: element \(command.elementId) not found")))
1394
+ return
1395
+ }
1396
+
1397
+ do {
1398
+ let animation = try elementAnimationManager.createAnimation(command: command, target: element)
1399
+ addSpatialObject(animation)
1400
+ resolve(.success(AddSpatializedElementReply(id: animation.id)))
1401
+ } catch let SpatializedElementAnimationManagerError.invalidTarget(reason) {
1402
+ resolve(.failure(JsbError(code: .CommandError, message: reason)))
1403
+ } catch {
1404
+ resolve(.failure(JsbError(code: .CommandError, message: error.localizedDescription)))
1405
+ }
1406
+ }
1407
+
1408
+ private func onControlSpatializedElementAnimation(command: ControlSpatializedElementAnimationCommand, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1409
+ do {
1410
+ try elementAnimationManager.controlAnimation(command)
1411
+ resolve(.success(nil))
1412
+ } catch let SpatializedElementAnimationManagerError.animationNotFound(animationId) {
1413
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Animation \(animationId) not found")))
1414
+ } catch let SpatializedElementAnimationManagerError.invalidTarget(reason) {
1415
+ resolve(.failure(JsbError(code: .CommandError, message: reason)))
1416
+ } catch {
1417
+ resolve(.failure(JsbError(code: .CommandError, message: error.localizedDescription)))
1418
+ }
1419
+ }
1420
+
1292
1421
  private func onUpdateUnlitMaterialProperties(command: UpdateUnlitMaterialProperties, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1293
1422
  guard let material = spatialObjects[command.id] as? SpatialUnlitMaterial else {
1294
1423
  resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Material \(command.id) not found")))
@@ -1359,6 +1488,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1359
1488
  event: SpatialObject.Events.BeforeDestroyed.rawValue,
1360
1489
  listener: onSptatialObjectDestroyed
1361
1490
  )
1491
+ if let element = spatialObject as? SpatializedElement {
1492
+ elementAnimationManager.destroyAnimationsForElement(element.spatialId)
1493
+ }
1362
1494
  spatialObjects.removeValue(forKey: spatialObject.spatialId)
1363
1495
 
1364
1496
  // notify web side, spatialObject is destroyed
@@ -1393,6 +1525,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1393
1525
  */
1394
1526
 
1395
1527
  override func onDestroy() {
1528
+ animationManager.removeAll()
1529
+ elementAnimationManager.removeAll()
1396
1530
  let spatialObjectArray = spatialObjects.map { $0.value }
1397
1531
  for spatialObject in spatialObjectArray {
1398
1532
  spatialObject.destroy()
@@ -1402,7 +1536,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1402
1536
  }
1403
1537
 
1404
1538
  enum CodingKeys: String, CodingKey {
1405
- case children, url, backgroundMaterial, cornerRadius, scrollOffset, webviewIsOpaque, spatialObjectCount, spatialObjectRefCount, spatialObjectList
1539
+ case children, url, backgroundMaterial, cornerRadius, scrollOffset, currentPageGeneration, childrenIds, sceneSpatialObjectIds, webviewIsOpaque, spatialObjectCount, spatialObjectRefCount, spatialObjectList
1406
1540
  }
1407
1541
 
1408
1542
  override func encode(to encoder: Encoder) throws {
@@ -1413,8 +1547,13 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1413
1547
  try container.encode(cornerRadius, forKey: .cornerRadius)
1414
1548
  try container.encode(scrollOffset, forKey: .scrollOffset)
1415
1549
  try container.encode(children, forKey: .children)
1550
+ try container.encode(currentPageGeneration, forKey: .currentPageGeneration)
1416
1551
 
1417
1552
  // for debug only
1553
+ let childrenIds = children.map { $0.key }
1554
+ try container.encode(childrenIds, forKey: .childrenIds)
1555
+ let sceneSpatialObjectIds = spatialObjects.map { $0.key }
1556
+ try container.encode(sceneSpatialObjectIds, forKey: .sceneSpatialObjectIds)
1418
1557
  try container.encode(spatialWebViewModel.getController().webview?.isOpaque, forKey: .webviewIsOpaque)
1419
1558
  try container.encode(SpatialObject.objects.count, forKey: .spatialObjectCount)
1420
1559
 
@@ -1425,4 +1564,4 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1425
1564
 
1426
1565
  try container.encode(SpatialObjectWeakRefManager.weakRefObjects.count, forKey: .spatialObjectRefCount)
1427
1566
  }
1428
- }
1567
+ }
@@ -74,17 +74,8 @@ class Spatialized2DElement: SpatializedElement, ScrollAbleSpatialElementContaine
74
74
  return children
75
75
  }
76
76
 
77
- func getChildrenOfType(_ type: SpatializedElementType) -> [String: SpatializedElement] {
78
- return children.filter {
79
- switch type {
80
- case .Spatialized2DElement:
81
- return $0.value is Spatialized2DElement
82
- case .SpatializedStatic3DElement:
83
- return $0.value is SpatializedStatic3DElement
84
- case .SpatializedDynamic3DElement:
85
- return $0.value is SpatializedDynamic3DElement
86
- }
87
- }
77
+ func getChildren<T: SpatializedElement>(ofType type: T.Type) -> [T] {
78
+ return children.values.compactMap { $0 as? T }
88
79
  }
89
80
 
90
81
  func loadHtml(_ html: String) {
@@ -5,6 +5,43 @@ import SwiftUI
5
5
  /// zIndex() have some bug, so use zOrderBias to simulate zIndex effect
6
6
  let zOrderBias = 0.001
7
7
 
8
+ struct SpatializedElementAnimatingMask {
9
+ var transformAnimationId: String?
10
+ var opacityAnimationId: String?
11
+ }
12
+
13
+ extension SpatializedElementAnimatingMask {
14
+ mutating func acquire(transform animationId: String?) {
15
+ transformAnimationId = animationId
16
+ }
17
+
18
+ mutating func acquire(opacity animationId: String?) {
19
+ opacityAnimationId = animationId
20
+ }
21
+
22
+ mutating func release(animationId: String) {
23
+ if transformAnimationId == animationId {
24
+ transformAnimationId = nil
25
+ }
26
+ if opacityAnimationId == animationId {
27
+ opacityAnimationId = nil
28
+ }
29
+ }
30
+
31
+ mutating func clear() {
32
+ transformAnimationId = nil
33
+ opacityAnimationId = nil
34
+ }
35
+
36
+ var locksTransform: Bool {
37
+ transformAnimationId != nil
38
+ }
39
+
40
+ var locksOpacity: Bool {
41
+ opacityAnimationId != nil
42
+ }
43
+ }
44
+
8
45
  enum SpatializedElementType: String, Codable {
9
46
  case Spatialized2DElement
10
47
  case SpatializedStatic3DElement
@@ -25,6 +62,7 @@ class SpatializedElement: SpatialObject {
25
62
  var visible = true
26
63
  var scrollWithParent = true
27
64
  var zIndex: Double = 0
65
+ var animatingMask = SpatializedElementAnimatingMask()
28
66
 
29
67
  var enableDragStartGesture: Bool = false
30
68
  var enableDragGesture: Bool = false
@@ -6,11 +6,29 @@ struct ModelSource: Codable, Equatable {
6
6
  let type: String?
7
7
  }
8
8
 
9
+ enum Loading: String {
10
+ case eager
11
+ case lazy
12
+
13
+ init(stringValue value: String) {
14
+ self = Loading(rawValue: value) ?? .eager
15
+ }
16
+ }
17
+
18
+ enum StageMode: String {
19
+ case none
20
+ case orbit
21
+
22
+ init(stringValue value: String) {
23
+ self = StageMode(rawValue: value) ?? .none
24
+ }
25
+ }
26
+
9
27
  @Observable
10
28
  class SpatializedStatic3DElement: SpatializedElement {
11
29
  var modelURL: String?
12
30
  var sources: [ModelSource] = []
13
- var modelTransform: AffineTransform3D = .identity
31
+ var entityTransform: AffineTransform3D = .identity
14
32
  var autoplay: Bool = false
15
33
  var loop: Bool = false
16
34
  var animationPaused: Bool = true
@@ -19,14 +37,15 @@ class SpatializedStatic3DElement: SpatializedElement {
19
37
  /// `SpatializedStatic3DView`, which clears it back to `nil`.
20
38
  var pendingSeekTime: Double?
21
39
  var posterURL: String?
22
- /// `"eager"` (default) fetches the model immediately; `"lazy"` defers
23
- /// fetching until the web layer flips it back to `"eager"`.
24
- var loading: String = "eager"
40
+ var loading: Loading = .eager
41
+ var stagemode: StageMode = .none
25
42
  var allSources: [ModelSource] {
26
- guard loading == "eager" else { return [] }
27
- return if let modelURL {
28
- [ModelSource(src: modelURL, type: nil)] + sources
29
- } else { sources }
43
+ if let modelURL { [ModelSource(src: modelURL, type: nil)] + sources }
44
+ else { sources }
45
+ }
46
+
47
+ override var enableGesture: Bool {
48
+ stagemode == .orbit || super.enableGesture
30
49
  }
31
50
 
32
51
  enum CodingKeys: String, CodingKey {