@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
@@ -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 {
@@ -307,6 +317,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
307
317
  spatialWebViewModel.addJSBListener(CreateSpatialEntity.self, onCreateEntity)
308
318
  spatialWebViewModel.addJSBListener(CreateGeometryProperties.self, onCreateGeometry)
309
319
  spatialWebViewModel.addJSBListener(CreateUnlitMaterial.self, onCreateUnlitMaterial)
320
+ spatialWebViewModel.addJSBListener(CreateTexture.self, onCreateTexture)
321
+ spatialWebViewModel.addJSBListener(UpdateTextureProperties.self, onUpdateTextureProperties)
310
322
  spatialWebViewModel.addJSBListener(CreateModelComponent.self, onCreateModelComponent)
311
323
  spatialWebViewModel.addJSBListener(AddComponentToEntity.self, onAddComponentToEntity)
312
324
  spatialWebViewModel.addJSBListener(AddEntityToDynamic3D.self, onAddEntityToDynamic3D)
@@ -329,6 +341,10 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
329
341
 
330
342
  spatialWebViewModel.addJSBListener(UpdateAttachmentEntityCommand.self, onUpdateAttachmentEntity)
331
343
 
344
+ spatialWebViewModel.addJSBListener(AnimateTransformCommand.self, onAnimateTransform)
345
+
346
+ spatialWebViewModel.addJSBListener(CreateSpatializedElementAnimationCommand.self, onCreateSpatializedElementAnimation)
347
+ spatialWebViewModel.addJSBListener(ControlSpatializedElementAnimationCommand.self, onControlSpatializedElementAnimation)
332
348
  spatialWebViewModel.addOpenWindowListener(protocal: "webspatial", onOpenWindowHandler)
333
349
 
334
350
  spatialWebViewModel
@@ -359,6 +375,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
359
375
  }
360
376
 
361
377
  var didFailLoad = false
378
+ private var currentPageGeneration = 0
362
379
 
363
380
  private func setupWebViewStateListener() {
364
381
  spatialWebViewModel.addStateListener(.didStartLoad) {
@@ -376,6 +393,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
376
393
  }
377
394
 
378
395
  spatialWebViewModel.addStateListener(.didReceive) {
396
+ self.injectPageEpoch()
379
397
  if let meterToPtUnscaled = self.meterToPtUnscaled,
380
398
  let meterToPtScaled = self.meterToPtScaled
381
399
  {
@@ -392,27 +410,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
392
410
 
393
411
  spatialWebViewModel.addStateListener(.didFinishLoad) {
394
412
  if self.state == .pending {
395
- self.checkHookExist()
396
- }
397
- }
398
- }
399
-
400
- private func checkHookExist(_ completion: ((Bool) -> Void)? = nil) {
401
- let js = """
402
- (function() {
403
- return typeof window.xrCurrentSceneDefaults !== 'undefined';
404
- })();
405
- """
406
-
407
- spatialWebViewModel.evaluateJS(js) { result in
408
- let exists = result as? Bool ?? false
409
-
410
- if let completion = completion {
411
- completion(exists)
412
- } else {
413
- if !exists {
414
- self.moveToState(.willVisible, defaultSceneConfig)
415
- }
413
+ self.moveToState(.willVisible, self.sceneConfig ?? defaultSceneConfig)
416
414
  }
417
415
  }
418
416
  }
@@ -423,11 +421,17 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
423
421
  return handleWindowOpenCustom(url)
424
422
  } else if host == "createAttachment" {
425
423
  return handleCreateAttachment(url)
426
- } else {
424
+ } else if host == "createSpatialized2DElement" {
425
+ guard shouldAcceptSpatialRequest(url, command: host) else {
426
+ return nil
427
+ }
427
428
  let spatialized2DElement: Spatialized2DElement = createSpatializedElement(
428
429
  .Spatialized2DElement
429
430
  )
430
431
  return WebViewElementInfo(id: spatialized2DElement.id, element: spatialized2DElement.getWebViewModel())
432
+ } else {
433
+ logger.warning("Unknown webspatial open-window command: \(host)")
434
+ return nil
431
435
  }
432
436
  }
433
437
 
@@ -435,6 +439,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
435
439
  private var pendingAttachmentWebViewModels = [String: SpatialWebViewModel]()
436
440
 
437
441
  private func handleCreateAttachment(_ url: URL) -> WebViewElementInfo? {
442
+ guard shouldAcceptSpatialRequest(url, command: "createAttachment") else {
443
+ return nil
444
+ }
438
445
  // Just create a bare webview — metadata arrives via InitializeAttachment JSB
439
446
  let id = UUID().uuidString
440
447
  let webViewModel = SpatialWebViewModel(url: nil)
@@ -479,6 +486,12 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
479
486
  }
480
487
 
481
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()
482
495
  // destroy all SpatialObject asset
483
496
  let spatialObjectArray = spatialObjects.map { $0.value }
484
497
  for spatialObject in spatialObjectArray {
@@ -488,6 +501,50 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
488
501
  backgroundMaterial = .None
489
502
  }
490
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
+
491
548
  /// Some SPA navigations (history back/forward) do not trigger a full WKNavigation
492
549
  /// lifecycle. SpatialNavView calls this before navigation actions to ensure
493
550
  /// previously-created spatial objects are cleaned up.
@@ -548,6 +605,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
548
605
  if let sources = command.sources {
549
606
  spatialObject.sources = sources
550
607
  }
608
+ if let loading = command.loading {
609
+ spatialObject.loading = Loading(stringValue: loading)
610
+ }
551
611
 
552
612
  resolve(.success(AddSpatializedElementReply(id: spatialObject.id)))
553
613
  }
@@ -610,7 +670,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
610
670
  let column3 = simd_double4(array[12], array[13], array[14], array[15])
611
671
  let simd_double4x4 = simd_double4x4(columns: (column0, column1, column2, column3))
612
672
  let affineTransform3D = AffineTransform3D(truncating: simd_double4x4)
613
- spatializedElement.modelTransform = affineTransform3D
673
+ if !spatializedElement.animatingMask.locksTransform {
674
+ spatializedElement.entityTransform = affineTransform3D
675
+ }
614
676
  }
615
677
 
616
678
  if let autoplay = command.autoplay {
@@ -629,6 +691,22 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
629
691
  spatializedElement.playbackRate = playbackRate
630
692
  }
631
693
 
694
+ if let currentTime = command.currentTime {
695
+ spatializedElement.pendingSeekTime = currentTime
696
+ }
697
+
698
+ if let posterURL = command.posterURL {
699
+ spatializedElement.posterURL = posterURL.isEmpty ? nil : posterURL
700
+ }
701
+
702
+ if let loading = command.loading {
703
+ spatializedElement.loading = Loading(stringValue: loading)
704
+ }
705
+
706
+ if let stagemode = command.stagemode {
707
+ spatializedElement.stagemode = StageMode(stringValue: stagemode)
708
+ }
709
+
632
710
  resolve(.success(baseReplyData))
633
711
  }
634
712
 
@@ -738,7 +816,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
738
816
  }
739
817
 
740
818
  if let opacity = command.opacity {
741
- spatializedElement.opacity = opacity
819
+ if !spatializedElement.animatingMask.locksOpacity {
820
+ spatializedElement.opacity = opacity
821
+ }
742
822
  }
743
823
 
744
824
  if let scrollWithParent = command.scrollWithParent {
@@ -804,6 +884,11 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
804
884
  return resolve(.failure(JsbError(code: .InvalidMatrix, message: "invalid UpdateSpatializedElementTransform matrix should have length 16!")))
805
885
  }
806
886
 
887
+ if spatializedElement.animatingMask.locksTransform {
888
+ resolve(.success(baseReplyData))
889
+ return
890
+ }
891
+
807
892
  let column0 = simd_double4(array[0], array[1], array[2], array[3])
808
893
  let column1 = simd_double4(array[4], array[5], array[6], array[7])
809
894
  let column2 = simd_double4(array[8], array[9], array[10], array[11])
@@ -842,17 +927,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
842
927
  children.removeValue(forKey: spatializedElement.id)
843
928
  }
844
929
 
845
- func getChildrenOfType(_ type: SpatializedElementType) -> [String: SpatializedElement] {
846
- return children.filter {
847
- switch type {
848
- case .Spatialized2DElement:
849
- return $0.value is Spatialized2DElement
850
- case .SpatializedStatic3DElement:
851
- return $0.value is SpatializedStatic3DElement
852
- case .SpatializedDynamic3DElement:
853
- return $0.value is SpatializedDynamic3DElement
854
- }
855
- }
930
+ func getChildren<T: SpatializedElement>(ofType type: T.Type) -> [T] {
931
+ return children.values.compactMap { $0 as? T }
856
932
  }
857
933
 
858
934
  func getChildren() -> [String: SpatializedElement] {
@@ -964,8 +1040,28 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
964
1040
  // @fukang: add Component here
965
1041
  }
966
1042
 
1043
+ private func onCreateTexture(command: CreateTexture, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1044
+ let texture = SpatialTextureResource(command.url)
1045
+ addSpatialObject(texture)
1046
+ Task {
1047
+ do {
1048
+ try await texture.load()
1049
+ resolve(.success(AddSpatializedElementReply(id: texture.id)))
1050
+ } catch {
1051
+ texture.destroy()
1052
+ resolve(.failure(JsbError(code: .CommandError, message: "Failed to load texture from \(command.url): \(error.localizedDescription)")))
1053
+ }
1054
+ }
1055
+ }
1056
+
967
1057
  private func onCreateUnlitMaterial(command: CreateUnlitMaterial, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
968
- let material = Dynamic3DManager.createUnlitMaterial(command, nil)
1058
+ var tex: TextureResource? = nil
1059
+ if let textureId = command.textureId,
1060
+ let texObj = spatialObjects[textureId] as? SpatialTextureResource
1061
+ {
1062
+ tex = texObj.resource
1063
+ }
1064
+ let material = Dynamic3DManager.createUnlitMaterial(command, tex)
969
1065
  addSpatialObject(material)
970
1066
  resolve(.success(AddSpatializedElementReply(id: material.id)))
971
1067
  }
@@ -1225,20 +1321,124 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1225
1321
  resolve(.success(baseReplyData))
1226
1322
  }
1227
1323
 
1324
+ private func onUpdateTextureProperties(command: UpdateTextureProperties, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1325
+ guard let texture = spatialObjects[command.id] as? SpatialTextureResource else {
1326
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Texture \(command.id) not found")))
1327
+ return
1328
+ }
1329
+ guard let newURL = command.url else {
1330
+ resolve(.success(baseReplyData))
1331
+ return
1332
+ }
1333
+ Task {
1334
+ do {
1335
+ try await texture.updateURL(newURL)
1336
+ let refreshedMaterialIds = MaterialSceneRefresh.pushReloadedTextureToBoundUnlitMaterials(
1337
+ texture: texture,
1338
+ textureSpatialId: command.id,
1339
+ spatialObjects: spatialObjects
1340
+ )
1341
+ for mid in refreshedMaterialIds {
1342
+ MaterialSceneRefresh.refreshComponentsUsingMaterial(mid, spatialObjects: spatialObjects)
1343
+ }
1344
+ resolve(.success(baseReplyData))
1345
+ } catch {
1346
+ resolve(.failure(JsbError(code: .CommandError, message: error.localizedDescription)))
1347
+ }
1348
+ }
1349
+ }
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
+
1228
1421
  private func onUpdateUnlitMaterialProperties(command: UpdateUnlitMaterialProperties, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1229
1422
  guard let material = spatialObjects[command.id] as? SpatialUnlitMaterial else {
1230
1423
  resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Material \(command.id) not found")))
1231
1424
  return
1232
1425
  }
1233
- material.updateProperties(color: command.color, transparent: command.transparent, opacity: command.opacity)
1234
- // Re-apply material to any ModelComponent or ModelEntity override that references it
1235
- for (_, obj) in spatialObjects {
1236
- if let comp = obj as? SpatialModelComponent, comp.usesMaterial(command.id) {
1237
- comp.refreshMaterials()
1238
- } else if let modelEntity = obj as? SpatialModelEntity, modelEntity.usesMaterial(command.id) {
1239
- modelEntity.refreshMaterials()
1426
+ var texture: TextureResource?? = nil
1427
+ if let textureId = command.textureId {
1428
+ if textureId.isEmpty {
1429
+ texture = .some(nil)
1430
+ } else if let texObj = spatialObjects[textureId] as? SpatialTextureResource {
1431
+ texture = .some(texObj.resource)
1432
+ } else {
1433
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Texture \(textureId) not found")))
1434
+ return
1240
1435
  }
1241
1436
  }
1437
+ material.updateProperties(color: command.color, texture: texture, transparent: command.transparent, opacity: command.opacity)
1438
+ if let tid = command.textureId {
1439
+ material.textureSpatialId = tid.isEmpty ? nil : tid
1440
+ }
1441
+ MaterialSceneRefresh.refreshComponentsUsingMaterial(command.id, spatialObjects: spatialObjects)
1242
1442
  resolve(.success(baseReplyData))
1243
1443
  }
1244
1444
 
@@ -1288,6 +1488,9 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1288
1488
  event: SpatialObject.Events.BeforeDestroyed.rawValue,
1289
1489
  listener: onSptatialObjectDestroyed
1290
1490
  )
1491
+ if let element = spatialObject as? SpatializedElement {
1492
+ elementAnimationManager.destroyAnimationsForElement(element.spatialId)
1493
+ }
1291
1494
  spatialObjects.removeValue(forKey: spatialObject.spatialId)
1292
1495
 
1293
1496
  // notify web side, spatialObject is destroyed
@@ -1322,6 +1525,8 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1322
1525
  */
1323
1526
 
1324
1527
  override func onDestroy() {
1528
+ animationManager.removeAll()
1529
+ elementAnimationManager.removeAll()
1325
1530
  let spatialObjectArray = spatialObjects.map { $0.value }
1326
1531
  for spatialObject in spatialObjectArray {
1327
1532
  spatialObject.destroy()
@@ -1331,7 +1536,7 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1331
1536
  }
1332
1537
 
1333
1538
  enum CodingKeys: String, CodingKey {
1334
- case children, url, backgroundMaterial, cornerRadius, scrollOffset, webviewIsOpaque, spatialObjectCount, spatialObjectRefCount, spatialObjectList
1539
+ case children, url, backgroundMaterial, cornerRadius, scrollOffset, currentPageGeneration, childrenIds, sceneSpatialObjectIds, webviewIsOpaque, spatialObjectCount, spatialObjectRefCount, spatialObjectList
1335
1540
  }
1336
1541
 
1337
1542
  override func encode(to encoder: Encoder) throws {
@@ -1342,8 +1547,13 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1342
1547
  try container.encode(cornerRadius, forKey: .cornerRadius)
1343
1548
  try container.encode(scrollOffset, forKey: .scrollOffset)
1344
1549
  try container.encode(children, forKey: .children)
1550
+ try container.encode(currentPageGeneration, forKey: .currentPageGeneration)
1345
1551
 
1346
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)
1347
1557
  try container.encode(spatialWebViewModel.getController().webview?.isOpaque, forKey: .webviewIsOpaque)
1348
1558
  try container.encode(SpatialObject.objects.count, forKey: .spatialObjectCount)
1349
1559
 
@@ -1354,4 +1564,4 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1354
1564
 
1355
1565
  try container.encode(SpatialObjectWeakRefManager.weakRefObjects.count, forKey: .spatialObjectRefCount)
1356
1566
  }
1357
- }
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,19 +6,46 @@ 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
17
35
  var playbackRate: Double = 1.0
36
+ /// Requested seek position in seconds. Setting it triggers a seek in
37
+ /// `SpatializedStatic3DView`, which clears it back to `nil`.
38
+ var pendingSeekTime: Double?
39
+ var posterURL: String?
40
+ var loading: Loading = .eager
41
+ var stagemode: StageMode = .none
18
42
  var allSources: [ModelSource] {
19
- return if let modelURL {
20
- [ModelSource(src: modelURL, type: nil)] + sources
21
- } 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
22
49
  }
23
50
 
24
51
  enum CodingKeys: String, CodingKey {