@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.
- package/package.json +2 -2
- package/web-spatial/JSBCommand.swift +12 -0
- package/web-spatial/WebMsgCommand.swift +29 -2
- package/web-spatial/WebSpatialApp.swift +26 -8
- package/web-spatial/manager/Dynamic3DManager.swift +97 -37
- package/web-spatial/manifest.swift +2 -1
- package/web-spatial/model/MaterialSceneRefresh.swift +41 -0
- package/web-spatial/model/SpatialScene.swift +255 -45
- package/web-spatial/model/Spatialized2DElement.swift +2 -11
- package/web-spatial/model/SpatializedElement.swift +38 -0
- package/web-spatial/model/SpatializedStatic3DElement.swift +31 -4
- package/web-spatial/model/dynamic3d/EntityAnimationManager.swift +356 -0
- package/web-spatial/model/dynamic3d/EntityAnimationSession.swift +162 -0
- package/web-spatial/model/dynamic3d/SpatialMaterial.swift +26 -10
- package/web-spatial/model/dynamic3d/SpatialModelEntity.swift +5 -3
- package/web-spatial/model/dynamic3d/SpatialTextureResource.swift +25 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationCommand.swift +15 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationManager.swift +263 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationObject.swift +399 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationTypes.swift +45 -0
- package/web-spatial/model/element-motion/SpatializedElementAnimationWriteAdapter.swift +65 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionBridgeTypes.swift +43 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTimelineSampler.swift +99 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTiming.swift +99 -0
- package/web-spatial/model/element-motion/SpatializedElementMotionTransformTypes.swift +22 -0
- package/web-spatial/protocol/SpatializedElementContainer.swift +1 -1
- package/web-spatial/view/SpatialNavView.swift +37 -12
- package/web-spatial/view/SpatialSceneContentView.swift +9 -12
- package/web-spatial/view/Spatialized2DElementView.swift +10 -17
- package/web-spatial/view/SpatializedDynamic3DView.swift +2 -6
- package/web-spatial/view/SpatializedElementView.swift +3 -2
- package/web-spatial/view/SpatializedStatic3DView.swift +104 -43
- package/web-spatial/view/view-modifier/OrbitModifier.swift +84 -0
- package/web-spatial/webview/SpatialWebController.swift +26 -18
- package/web-spatial/webview/SpatialWebViewModel.swift +12 -7
- package/web-spatial.xcodeproj/project.pbxproj +30 -17
- package/web-spatial.xcodeproj/xcshareddata/xcschemes/web-spatial.xcscheme +1 -1
- package/web-spatialTests/NavigationCleanupTests.swift +2 -1
- package/web-spatialTests/SpatializedElementAnimationManagerTests.swift +1466 -0
- package/web-spatialTests/SpatializedElementMotionTimelineSamplerTests.swift +117 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webspatial/platform-visionos",
|
|
3
|
-
"version": "
|
|
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",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=22"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
@@ -16,6 +16,7 @@ struct CreateSpatializedStatic3DElement: CommandDataProtocol {
|
|
|
16
16
|
static let commandType: String = "CreateSpatializedStatic3DElement"
|
|
17
17
|
let modelURL: String?
|
|
18
18
|
let sources: [ModelSource]?
|
|
19
|
+
let loading: String?
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
struct CreateSpatializedDynamic3DElement: CommandDataProtocol {
|
|
@@ -52,6 +53,12 @@ struct CreateTexture: CommandDataProtocol {
|
|
|
52
53
|
let url: String
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
struct UpdateTextureProperties: CommandDataProtocol {
|
|
57
|
+
static let commandType: String = "UpdateTextureProperties"
|
|
58
|
+
let id: String
|
|
59
|
+
let url: String?
|
|
60
|
+
}
|
|
61
|
+
|
|
55
62
|
struct CreateModelAsset: CommandDataProtocol {
|
|
56
63
|
static let commandType: String = "CreateModelAsset"
|
|
57
64
|
let url: String
|
|
@@ -150,6 +157,7 @@ struct UpdateUnlitMaterialProperties: CommandDataProtocol {
|
|
|
150
157
|
static let commandType: String = "UpdateUnlitMaterialProperties"
|
|
151
158
|
let id: String
|
|
152
159
|
let color: String?
|
|
160
|
+
let textureId: String?
|
|
153
161
|
let transparent: Bool?
|
|
154
162
|
let opacity: Float?
|
|
155
163
|
}
|
|
@@ -267,6 +275,10 @@ struct UpdateSpatializedStatic3DElementProperties: SpatializedElementProperties
|
|
|
267
275
|
let loop: Bool?
|
|
268
276
|
let animationPaused: Bool?
|
|
269
277
|
let playbackRate: Double?
|
|
278
|
+
let currentTime: Double?
|
|
279
|
+
let posterURL: String?
|
|
280
|
+
let loading: String?
|
|
281
|
+
let stagemode: String?
|
|
270
282
|
}
|
|
271
283
|
|
|
272
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
|
|
|
@@ -119,6 +118,9 @@ struct ModelLoadFailure: Encodable {
|
|
|
119
118
|
struct AnimationStateChangeDetail: Encodable {
|
|
120
119
|
let paused: Bool
|
|
121
120
|
let duration: Double
|
|
121
|
+
let currentTime: Double
|
|
122
|
+
/// Unix epoch time in milliseconds
|
|
123
|
+
let timestamp: Double
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
struct AnimationStateChangeEvent: Encodable {
|
|
@@ -126,6 +128,31 @@ struct AnimationStateChangeEvent: Encodable {
|
|
|
126
128
|
let detail: AnimationStateChangeDetail
|
|
127
129
|
}
|
|
128
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
|
+
|
|
129
156
|
struct SpatialObjectDestroiedEvent: Encodable {
|
|
130
157
|
let type: SpatialWebMsgType = .objectdestroy
|
|
131
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 =
|
|
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 =
|
|
70
|
-
SpatialSceneView(spatialScene: spatialScene
|
|
87
|
+
let spatialScene = getSceneOrCreate(windowData, .volume)
|
|
88
|
+
SpatialSceneView(spatialScene: spatialScene)
|
|
71
89
|
.frame(
|
|
72
90
|
minWidth: getCGFloat(
|
|
73
|
-
|
|
91
|
+
spatialScene.sceneConfig?.resizeRange?.minWidth
|
|
74
92
|
),
|
|
75
93
|
maxWidth: getCGFloat(
|
|
76
|
-
|
|
94
|
+
spatialScene.sceneConfig?.resizeRange?.maxWidth
|
|
77
95
|
),
|
|
78
96
|
minHeight: getCGFloat(
|
|
79
|
-
|
|
97
|
+
spatialScene.sceneConfig?.resizeRange?.minHeight
|
|
80
98
|
),
|
|
81
99
|
maxHeight: getCGFloat(
|
|
82
|
-
|
|
100
|
+
spatialScene.sceneConfig?.resizeRange?.maxHeight
|
|
83
101
|
)
|
|
84
102
|
)
|
|
85
103
|
}
|
|
@@ -1,6 +1,85 @@
|
|
|
1
|
+
import CryptoKit
|
|
1
2
|
import Foundation
|
|
2
3
|
import RealityKit
|
|
3
4
|
|
|
5
|
+
/// Serializes remote URL → local file for each distinct URL string and uses a unique on-disk name
|
|
6
|
+
/// (hash + basename) so concurrent loads never `removeItem` a path another `TextureResource` read
|
|
7
|
+
/// is using (the old `Documents/lastPathComponent` scheme collided across scenes / retries).
|
|
8
|
+
private actor RemoteResourceLoadCache {
|
|
9
|
+
static let shared = RemoteResourceLoadCache()
|
|
10
|
+
|
|
11
|
+
private var inFlight: [String: Task<URL, Error>] = [:]
|
|
12
|
+
|
|
13
|
+
func localFileURL(forRemote urlString: String) async throws -> URL {
|
|
14
|
+
if let existing = inFlight[urlString] {
|
|
15
|
+
return try await existing.value
|
|
16
|
+
}
|
|
17
|
+
let task = Task {
|
|
18
|
+
try await Self.downloadInstallIfNeeded(urlString: urlString)
|
|
19
|
+
}
|
|
20
|
+
inFlight[urlString] = task
|
|
21
|
+
do {
|
|
22
|
+
let url = try await task.value
|
|
23
|
+
inFlight[urlString] = nil
|
|
24
|
+
return url
|
|
25
|
+
} catch {
|
|
26
|
+
inFlight[urlString] = nil
|
|
27
|
+
throw error
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private static func downloadInstallIfNeeded(urlString: String) async throws -> URL {
|
|
32
|
+
guard let remote = URL(string: urlString) else {
|
|
33
|
+
throw NSError(
|
|
34
|
+
domain: "Invalid URL",
|
|
35
|
+
code: 0,
|
|
36
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to create URL from string: \(urlString)"]
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
guard let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
|
40
|
+
throw NSError(
|
|
41
|
+
domain: "Download Error",
|
|
42
|
+
code: 0,
|
|
43
|
+
userInfo: [NSLocalizedDescriptionKey: "Documents directory is unavailable"]
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
let destination = cacheFileURL(documents: documents, urlString: urlString)
|
|
47
|
+
let fm = FileManager.default
|
|
48
|
+
if fm.fileExists(atPath: destination.path) {
|
|
49
|
+
return destination
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
print("start load")
|
|
53
|
+
let (tempURL, response) = try await URLSession.shared.download(from: remote)
|
|
54
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
55
|
+
throw NSError(
|
|
56
|
+
domain: "HTTP Error",
|
|
57
|
+
code: 0,
|
|
58
|
+
userInfo: [NSLocalizedDescriptionKey: "Missing HTTP response"]
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
guard (200 ... 299).contains(httpResponse.statusCode) else {
|
|
62
|
+
throw NSError(
|
|
63
|
+
domain: "HTTP Error",
|
|
64
|
+
code: httpResponse.statusCode,
|
|
65
|
+
userInfo: [NSLocalizedDescriptionKey: "HTTP Error \(httpResponse.statusCode)"]
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
try await FileCoordinator.shared.moveReplacingIfExists(from: tempURL, to: destination)
|
|
69
|
+
print("load complete")
|
|
70
|
+
return destination
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static func cacheFileURL(documents: URL, urlString: String) -> URL {
|
|
74
|
+
let digest = SHA256.hash(data: Data(urlString.utf8))
|
|
75
|
+
let hex = digest.prefix(8).reduce(into: "") { $0.append(String(format: "%02x", $1)) }
|
|
76
|
+
var baseName = URL(string: urlString)?.lastPathComponent ?? "asset"
|
|
77
|
+
if baseName.isEmpty { baseName = "asset" }
|
|
78
|
+
let safe = baseName.replacingOccurrences(of: "/", with: "_")
|
|
79
|
+
return documents.appendingPathComponent("\(hex)_\(safe)")
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
4
83
|
enum GeometryCreationError: LocalizedError {
|
|
5
84
|
case invalidType(String)
|
|
6
85
|
case missingFields(String, [String])
|
|
@@ -67,7 +146,17 @@ class Dynamic3DManager {
|
|
|
67
146
|
// Error messages are thrown from createGeometry using GeometryCreationError
|
|
68
147
|
|
|
69
148
|
static func createUnlitMaterial(_ props: CreateUnlitMaterial, _ tex: TextureResource? = nil) -> SpatialUnlitMaterial {
|
|
70
|
-
|
|
149
|
+
let textureSpatialId: String? = {
|
|
150
|
+
guard let tid = props.textureId, !tid.isEmpty else { return nil }
|
|
151
|
+
return tid
|
|
152
|
+
}()
|
|
153
|
+
return SpatialUnlitMaterial(
|
|
154
|
+
props.color ?? "#FFFFFF",
|
|
155
|
+
tex,
|
|
156
|
+
props.transparent ?? true,
|
|
157
|
+
props.opacity ?? 1,
|
|
158
|
+
textureSpatialId: textureSpatialId
|
|
159
|
+
)
|
|
71
160
|
}
|
|
72
161
|
|
|
73
162
|
static func loadResourceToLocal(_ urlString: String, loadComplete: @escaping (Result<URL, Error>) -> Void) {
|
|
@@ -80,43 +169,14 @@ class Dynamic3DManager {
|
|
|
80
169
|
loadComplete(.success(localUrl))
|
|
81
170
|
return
|
|
82
171
|
}
|
|
83
|
-
// load net file
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(url.lastPathComponent)
|
|
90
|
-
let session = URLSession(configuration: URLSessionConfiguration.default)
|
|
91
|
-
var request = URLRequest(url: url)
|
|
92
|
-
request.httpMethod = "GET"
|
|
93
|
-
print("start load")
|
|
94
|
-
let task = session.downloadTask(with: request, completionHandler: { location, response, error in
|
|
95
|
-
if let error = error {
|
|
172
|
+
// load net file — coalesced per URL + unique cache filename (see RemoteResourceLoadCache).
|
|
173
|
+
Task {
|
|
174
|
+
do {
|
|
175
|
+
let url = try await RemoteResourceLoadCache.shared.localFileURL(forRemote: urlString)
|
|
176
|
+
loadComplete(.success(url))
|
|
177
|
+
} catch {
|
|
96
178
|
loadComplete(.failure(error))
|
|
97
|
-
return
|
|
98
179
|
}
|
|
99
|
-
|
|
100
|
-
let error = NSError(domain: "HTTP Error", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: "HTTP Error \(httpResponse.statusCode)"])
|
|
101
|
-
loadComplete(.failure(error))
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
guard let location = location else {
|
|
105
|
-
loadComplete(.failure(NSError(domain: "Download Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "Download location is nil"])))
|
|
106
|
-
return
|
|
107
|
-
}
|
|
108
|
-
Task {
|
|
109
|
-
do {
|
|
110
|
-
try await FileCoordinator.shared.moveReplacingIfExists(from: location, to: documentsUrl)
|
|
111
|
-
print("load complete")
|
|
112
|
-
loadComplete(.success(documentsUrl))
|
|
113
|
-
} catch {
|
|
114
|
-
print("File operation error: \(error)")
|
|
115
|
-
loadComplete(.failure(error))
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
})
|
|
120
|
-
task.resume()
|
|
180
|
+
}
|
|
121
181
|
}
|
|
122
182
|
}
|
|
@@ -6,7 +6,8 @@ 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
|
+
var start_url: String = "http://localhost:5173/#/runtime-capabilities"
|
|
10
11
|
|
|
11
12
|
// var start_url: String = "http://localhost:5173/#/spatial-drag-gesture"
|
|
12
13
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Scene-level helpers for keeping RealityKit model components in sync after material / texture changes.
|
|
4
|
+
/// Per-material mutation lives on `SpatialUnlitMaterial` in `dynamic3d/SpatialMaterial.swift`; this type only
|
|
5
|
+
/// orchestrates lookups across `spatialObjects`.
|
|
6
|
+
enum MaterialSceneRefresh {
|
|
7
|
+
/// After `SpatialUnlitMaterial.updateProperties`, `ModelComponent` may still hold a stale material copy —
|
|
8
|
+
/// refresh every component and entity that references `materialId`.
|
|
9
|
+
static func refreshComponentsUsingMaterial(
|
|
10
|
+
_ materialId: String,
|
|
11
|
+
spatialObjects: [String: any SpatialObjectProtocol]
|
|
12
|
+
) {
|
|
13
|
+
for (_, obj) in spatialObjects {
|
|
14
|
+
if let comp = obj as? SpatialModelComponent, comp.usesMaterial(materialId) {
|
|
15
|
+
comp.refreshMaterials()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (_, obj) in spatialObjects {
|
|
19
|
+
if let entity = obj as? SpatialModelEntity, entity.usesMaterial(materialId) {
|
|
20
|
+
entity.refreshMaterials()
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// After `SpatialTextureResource` reloads from a new URL, push `texture.resource` into every bound unlit material and return their ids for `refreshComponentsUsingMaterial`.
|
|
26
|
+
static func pushReloadedTextureToBoundUnlitMaterials(
|
|
27
|
+
texture: SpatialTextureResource,
|
|
28
|
+
textureSpatialId: String,
|
|
29
|
+
spatialObjects: [String: any SpatialObjectProtocol]
|
|
30
|
+
) -> Set<String> {
|
|
31
|
+
var refreshedMaterialIds = Set<String>()
|
|
32
|
+
for (_, obj) in spatialObjects {
|
|
33
|
+
guard let material = obj as? SpatialUnlitMaterial,
|
|
34
|
+
material.textureSpatialId == textureSpatialId
|
|
35
|
+
else { continue }
|
|
36
|
+
material.updateProperties(color: nil, texture: .some(texture.resource), transparent: nil, opacity: nil)
|
|
37
|
+
refreshedMaterialIds.insert(material.spatialId)
|
|
38
|
+
}
|
|
39
|
+
return refreshedMaterialIds
|
|
40
|
+
}
|
|
41
|
+
}
|