@webspatial/platform-visionos 1.4.0 → 1.5.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webspatial/platform-visionos",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Used to publish WebSpatial projects to Apple Vision Pro",
5
5
  "type": "commonjs",
6
6
  "main": "package.json",
@@ -145,6 +145,26 @@ protocol SpatialObjectCommand: CommandDataProtocol {
145
145
  var id: String { get }
146
146
  }
147
147
 
148
+ struct UpdateUnlitMaterialProperties: CommandDataProtocol {
149
+ static let commandType: String = "UpdateUnlitMaterialProperties"
150
+ let id: String
151
+ let color: String?
152
+ let transparent: Bool?
153
+ let opacity: Float?
154
+ }
155
+
156
+ struct RemoveComponentFromEntity: CommandDataProtocol {
157
+ static let commandType: String = "RemoveComponentFromEntity"
158
+ let entityId: String
159
+ let componentId: String
160
+ }
161
+
162
+ struct SetMaterialsOnEntity: CommandDataProtocol {
163
+ static let commandType: String = "SetMaterialsOnEntity"
164
+ let entityId: String
165
+ let materialIds: [String]
166
+ }
167
+
148
168
  struct DestroyCommand: CommandDataProtocol {
149
169
  static let commandType: String = "Destroy"
150
170
  var id: String
@@ -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/#/geometry-verify"
9
+ var start_url: String = "http://localhost:5173/"
10
10
 
11
11
  // var start_url: String = "http://localhost:5173/#/spatial-drag-gesture"
12
12
 
@@ -321,6 +321,10 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
321
321
  spatialWebViewModel.addJSBListener(InitializeAttachmentCommand.self, onInitializeAttachment)
322
322
  spatialWebViewModel.addJSBListener(ConvertCoordinate.self, onConvertCoordinate)
323
323
 
324
+ spatialWebViewModel.addJSBListener(UpdateUnlitMaterialProperties.self, onUpdateUnlitMaterialProperties)
325
+ spatialWebViewModel.addJSBListener(RemoveComponentFromEntity.self, onRemoveComponentFromEntity)
326
+ spatialWebViewModel.addJSBListener(SetMaterialsOnEntity.self, onSetMaterialsOnEntity)
327
+
324
328
  spatialWebViewModel.addJSBListener(UpdateAttachmentEntityCommand.self, onUpdateAttachmentEntity)
325
329
 
326
330
  spatialWebViewModel.addOpenWindowListener(protocal: "webspatial", onOpenWindowHandler)
@@ -1196,6 +1200,52 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1196
1200
  resolve(.success(baseReplyData))
1197
1201
  }
1198
1202
 
1203
+ private func onUpdateUnlitMaterialProperties(command: UpdateUnlitMaterialProperties, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1204
+ guard let material = spatialObjects[command.id] as? SpatialUnlitMaterial else {
1205
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Material \(command.id) not found")))
1206
+ return
1207
+ }
1208
+ material.updateProperties(color: command.color, transparent: command.transparent, opacity: command.opacity)
1209
+ // Re-apply material to any ModelComponent or ModelEntity override that references it
1210
+ for (_, obj) in spatialObjects {
1211
+ if let comp = obj as? SpatialModelComponent, comp.usesMaterial(command.id) {
1212
+ comp.refreshMaterials()
1213
+ } else if let modelEntity = obj as? SpatialModelEntity, modelEntity.usesMaterial(command.id) {
1214
+ modelEntity.refreshMaterials()
1215
+ }
1216
+ }
1217
+ resolve(.success(baseReplyData))
1218
+ }
1219
+
1220
+ private func onRemoveComponentFromEntity(command: RemoveComponentFromEntity, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1221
+ guard let entity = spatialObjects[command.entityId] as? SpatialEntity,
1222
+ let component = spatialObjects[command.componentId] as? SpatialComponent
1223
+ else {
1224
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Remove component failed")))
1225
+ return
1226
+ }
1227
+ entity.removeComponent(component)
1228
+ resolve(.success(baseReplyData))
1229
+ }
1230
+
1231
+ private func onSetMaterialsOnEntity(command: SetMaterialsOnEntity, resolve: @escaping JSBManager.ResolveHandler<Encodable>) {
1232
+ guard let entity = spatialObjects[command.entityId] as? SpatialModelEntity else {
1233
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "ModelEntity \(command.entityId) not found")))
1234
+ return
1235
+ }
1236
+ var materials: [SpatialMaterial] = []
1237
+ for mid in command.materialIds {
1238
+ if let material = spatialObjects[mid] as? SpatialMaterial {
1239
+ materials.append(material)
1240
+ } else {
1241
+ resolve(.failure(JsbError(code: .InvalidSpatialObject, message: "Material \(mid) not found")))
1242
+ return
1243
+ }
1244
+ }
1245
+ entity.setMaterials(materials)
1246
+ resolve(.success(baseReplyData))
1247
+ }
1248
+
1199
1249
  private func addSpatialObject(_ object: any SpatialObjectProtocol) {
1200
1250
  var spatialObject = object
1201
1251
  spatialObjects[spatialObject.spatialId] = spatialObject
@@ -1227,19 +1277,12 @@ class SpatialScene: SpatialObject, ScrollAbleSpatialElementContainer, WebMsgSend
1227
1277
  return nil
1228
1278
  }
1229
1279
 
1230
- for (_, object) in spatialObjects {
1231
- guard let dynamic3dElement = object as? SpatializedDynamic3DElement else {
1232
- continue
1233
- }
1234
-
1235
- let root = dynamic3dElement.getRoot()
1236
- var current: Entity? = entity
1237
- while let node = current {
1238
- if node === root {
1239
- return dynamic3dElement
1240
- }
1241
- current = node.parent
1280
+ var current: Entity? = entity
1281
+ while let node = current {
1282
+ if let rootEntity = node as? SpatialRootEntity {
1283
+ return rootEntity.root
1242
1284
  }
1285
+ current = node.parent
1243
1286
  }
1244
1287
 
1245
1288
  return nil
@@ -4,9 +4,14 @@ import RealityKit
4
4
 
5
5
  @Observable
6
6
  class SpatializedDynamic3DElement: SpatializedElement {
7
- private var rootEntity = SpatialEntity()
7
+ private var rootEntity = SpatialRootEntity()
8
8
  private var viewContent: RealityViewContent? = nil
9
9
 
10
+ override init() {
11
+ super.init()
12
+ rootEntity.root = self
13
+ }
14
+
10
15
  func getRoot() -> SpatialEntity {
11
16
  return rootEntity
12
17
  }
@@ -43,7 +43,12 @@ class SpatialComponent: SpatialObject {
43
43
 
44
44
  @Observable
45
45
  class SpatialModelComponent: SpatialComponent {
46
+ private(set) var spatialMaterials: [SpatialMaterial] = []
47
+ private(set) var mesh: Geometry?
48
+
46
49
  init(mesh: Geometry, mats: [SpatialMaterial]) {
50
+ spatialMaterials = mats
51
+ self.mesh = mesh
47
52
  super.init(.ModelComponent)
48
53
  var materials: [any RealityKit.Material] = []
49
54
  for item in mats {
@@ -52,6 +57,27 @@ class SpatialModelComponent: SpatialComponent {
52
57
  _resource = ModelComponent(mesh: mesh.resource!, materials: materials)
53
58
  }
54
59
 
60
+ /// Rebuild the ModelComponent with current material resources (called after material properties change)
61
+ func refreshMaterials() {
62
+ guard let mesh = mesh else { return }
63
+ var materials: [any RealityKit.Material] = []
64
+ for item in spatialMaterials {
65
+ if let res = item.resource {
66
+ materials.append(res)
67
+ }
68
+ }
69
+ _resource = ModelComponent(mesh: mesh.resource!, materials: materials)
70
+ if let entity = _entity {
71
+ entity.components.set(_resource!)
72
+ entity.generateCollisionShapes(recursive: true)
73
+ }
74
+ }
75
+
76
+ /// Check if this component uses the given material
77
+ func usesMaterial(_ materialId: String) -> Bool {
78
+ return spatialMaterials.contains { $0.id == materialId }
79
+ }
80
+
55
81
  override func addToEntity(entity: SpatialEntity) {
56
82
  super.addToEntity(entity: entity)
57
83
  entity.generateCollisionShapes(recursive: true)
@@ -63,7 +89,12 @@ class SpatialModelComponent: SpatialComponent {
63
89
  }
64
90
 
65
91
  override func onDestroy() {
92
+ // TODO(P2): `mesh` is a registered `Geometry` spatial object; clearing the reference does not
93
+ // run `Geometry.destroy()`, so dynamic mesh rebuilds can leak mesh/registry entries until the
94
+ // scene ends. Call `mesh?.destroy()` (or equivalent) before nil-ing when ownership is exclusive.
66
95
  _resource = nil
96
+ spatialMaterials = []
97
+ mesh = nil
67
98
  }
68
99
  }
69
100
 
@@ -22,16 +22,38 @@ class SpatialMaterial: SpatialObject {
22
22
 
23
23
  @Observable
24
24
  class SpatialUnlitMaterial: SpatialMaterial {
25
- let color: UIColor
25
+ private(set) var currentColor: UIColor
26
+ private(set) var currentTexture: TextureResource?
27
+ private(set) var currentTransparent: Bool
28
+ private(set) var currentOpacity: Float
26
29
 
27
30
  init(_ color: String, _ texture: TextureResource? = nil, _ transparent: Bool = true, _ opacity: Float = 1) {
28
- self.color = UIColor(Color(hex: color))
31
+ currentColor = UIColor(Color(hex: color))
32
+ currentTexture = texture
33
+ currentTransparent = transparent
34
+ currentOpacity = opacity
29
35
  super.init(.UnlitMaterial)
30
36
  var mat = UnlitMaterial()
31
- mat.color = .init(tint: UIColor(Color(hex: color)), texture: texture != nil ? .init(texture!) : nil)
37
+ mat.color = .init(tint: currentColor, texture: texture != nil ? .init(texture!) : nil)
32
38
  mat.blending = transparent ? .transparent(opacity: .init(scale: opacity)) : .opaque
33
39
  _resource = mat
34
40
  }
41
+
42
+ func updateProperties(color: String?, transparent: Bool?, opacity: Float?) {
43
+ if let color = color {
44
+ currentColor = UIColor(Color(hex: color))
45
+ }
46
+ if let transparent = transparent {
47
+ currentTransparent = transparent
48
+ }
49
+ if let opacity = opacity {
50
+ currentOpacity = opacity
51
+ }
52
+ var mat = UnlitMaterial()
53
+ mat.color = .init(tint: currentColor, texture: currentTexture != nil ? .init(currentTexture!) : nil)
54
+ mat.blending = currentTransparent ? .transparent(opacity: .init(scale: currentOpacity)) : .opaque
55
+ _resource = mat
56
+ }
35
57
  }
36
58
 
37
59
  enum SpatialMaterialType: String {
@@ -4,6 +4,9 @@ import SwiftUI
4
4
  @Observable
5
5
  class SpatialModelEntity: SpatialEntity {
6
6
  private var modelEntity: Entity?
7
+ /// Retained so `UpdateUnlitMaterialProperties` can re-apply current `SpatialMaterial.resource` after native material updates.
8
+ private(set) var overrideSpatialMaterials: [SpatialMaterial] = []
9
+
7
10
  required init(_ modelResource: SpatialModelResource, _ _name: String = "") {
8
11
  super.init(_name)
9
12
  modelEntity = modelResource.resource
@@ -15,12 +18,44 @@ class SpatialModelEntity: SpatialEntity {
15
18
  super.init()
16
19
  }
17
20
 
21
+ func setMaterials(_ materials: [SpatialMaterial]) {
22
+ overrideSpatialMaterials = materials
23
+ applyOverrideMaterials()
24
+ }
25
+
26
+ /// Re-apply stored override materials using each `SpatialMaterial`'s current `resource` (e.g. after unlit property updates).
27
+ func refreshMaterials() {
28
+ applyOverrideMaterials()
29
+ }
30
+
31
+ func usesMaterial(_ materialId: String) -> Bool {
32
+ overrideSpatialMaterials.contains { $0.id == materialId }
33
+ }
34
+
35
+ private func applyOverrideMaterials() {
36
+ guard let modelEntity = modelEntity else { return }
37
+ // TODO(P1): Clearing overrides (`setMaterials([])`) assigns an empty material list here; there is
38
+ // no baseline of the model asset’s authored materials to restore. Persist per-component defaults
39
+ // at load (or skip writing when overrides are empty) so clears return to the authored look.
40
+ func applyMaterials(to entity: Entity) {
41
+ if var modelComp = entity.components[ModelComponent.self] {
42
+ modelComp.materials = overrideSpatialMaterials.compactMap { $0.resource }
43
+ entity.components.set(modelComp)
44
+ }
45
+ for child in entity.children {
46
+ applyMaterials(to: child)
47
+ }
48
+ }
49
+ applyMaterials(to: modelEntity)
50
+ }
51
+
18
52
  override func onDestroy() {
19
53
  super.onDestroy()
20
54
  if let modelEntity = modelEntity {
21
55
  removeChild(modelEntity)
22
56
  }
23
57
  modelEntity = nil
58
+ overrideSpatialMaterials = []
24
59
  }
25
60
 
26
61
  enum CodingKeys: String, CodingKey {
@@ -0,0 +1,12 @@
1
+ import RealityKit
2
+ import SwiftUI
3
+
4
+ @Observable
5
+ class SpatialRootEntity: SpatialEntity {
6
+ weak var root: SpatializedDynamic3DElement?
7
+
8
+ convenience init(root: SpatializedDynamic3DElement) {
9
+ self.init()
10
+ self.root = root
11
+ }
12
+ }
@@ -70,6 +70,7 @@
70
70
  dynamic3d/SpatialMaterial.swift,
71
71
  dynamic3d/SpatialModelEntity.swift,
72
72
  dynamic3d/SpatialModelResource.swift,
73
+ dynamic3d/SpatialRootEntity.swift,
73
74
  dynamic3d/SpatialTextureResource.swift,
74
75
  SpatialApp.swift,
75
76
  Spatialized2DElement.swift,