@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 +1 -1
- package/web-spatial/JSBCommand.swift +20 -0
- package/web-spatial/manifest.swift +1 -1
- package/web-spatial/model/SpatialScene.swift +55 -12
- package/web-spatial/model/SpatializedDynamic3DElement.swift +6 -1
- package/web-spatial/model/dynamic3d/SpatialComponent.swift +31 -0
- package/web-spatial/model/dynamic3d/SpatialMaterial.swift +25 -3
- package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +35 -0
- package/web-spatial/model/dynamic3d/SpatialRootEntity.swift +12 -0
- package/web-spatial.xcodeproj/project.pbxproj +1 -0
package/package.json
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 {
|