@webspatial/core-sdk 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 (296) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +10 -0
  3. package/dist/JSBCommand.d.ts +398 -0
  4. package/dist/JSBCommand.js +668 -0
  5. package/dist/JSBCommand.js.map +1 -0
  6. package/dist/Spatial.d.ts +63 -0
  7. package/dist/Spatial.js +60 -0
  8. package/dist/Spatial.js.map +1 -0
  9. package/dist/SpatialObject.d.ts +19 -0
  10. package/dist/SpatialObject.js +44 -0
  11. package/dist/SpatialObject.js.map +1 -0
  12. package/dist/SpatialScene.d.ts +51 -0
  13. package/dist/SpatialScene.js +74 -0
  14. package/dist/SpatialScene.js.map +1 -0
  15. package/dist/SpatialSession.d.ts +143 -0
  16. package/dist/SpatialSession.js +173 -0
  17. package/dist/SpatialSession.js.map +1 -0
  18. package/dist/SpatialWebEvent.d.ts +8 -0
  19. package/dist/SpatialWebEvent.js +18 -0
  20. package/dist/SpatialWebEvent.js.map +1 -0
  21. package/dist/SpatialWebEventCreator.d.ts +11 -0
  22. package/dist/SpatialWebEventCreator.js +11 -0
  23. package/dist/SpatialWebEventCreator.js.map +1 -0
  24. package/dist/Spatialized2DElement.d.ts +42 -0
  25. package/dist/Spatialized2DElement.js +47 -0
  26. package/dist/Spatialized2DElement.js.map +1 -0
  27. package/dist/SpatializedDynamic3DElement.d.ts +7 -0
  28. package/dist/SpatializedDynamic3DElement.js +43 -0
  29. package/dist/SpatializedDynamic3DElement.js.map +1 -0
  30. package/dist/SpatializedElement.d.ts +7 -0
  31. package/dist/SpatializedElement.js +206 -0
  32. package/dist/SpatializedElement.js.map +1 -0
  33. package/dist/SpatializedElementCreator.d.ts +15 -0
  34. package/dist/SpatializedElementCreator.js +47 -0
  35. package/dist/SpatializedElementCreator.js.map +1 -0
  36. package/dist/SpatializedStatic3DElement.d.ts +195 -0
  37. package/dist/SpatializedStatic3DElement.js +327 -0
  38. package/dist/SpatializedStatic3DElement.js.map +1 -0
  39. package/dist/WebMsgCommand.d.ts +7 -0
  40. package/dist/WebMsgCommand.js +23 -0
  41. package/dist/WebMsgCommand.js.map +1 -0
  42. package/dist/index.d.ts +35 -1251
  43. package/dist/index.js +21 -3650
  44. package/dist/index.js.map +1 -1
  45. package/dist/install-polyfills.d.ts +2 -0
  46. package/dist/install-polyfills.js +13 -0
  47. package/dist/install-polyfills.js.map +1 -0
  48. package/dist/isSSREnv.d.ts +3 -0
  49. package/dist/isSSREnv.js +6 -0
  50. package/dist/isSSREnv.js.map +1 -0
  51. package/dist/motion/AnimationObject.d.ts +7 -0
  52. package/dist/motion/AnimationObject.js +140 -0
  53. package/dist/motion/AnimationObject.js.map +1 -0
  54. package/dist/motion/compute/index.d.ts +10 -0
  55. package/dist/motion/compute/index.js +19 -0
  56. package/dist/motion/compute/index.js.map +1 -0
  57. package/dist/motion/compute/motionPropertyValues.d.ts +19 -0
  58. package/dist/motion/compute/motionPropertyValues.js +50 -0
  59. package/dist/motion/compute/motionPropertyValues.js.map +1 -0
  60. package/dist/motion/compute/normalize.d.ts +31 -0
  61. package/dist/motion/compute/normalize.js +201 -0
  62. package/dist/motion/compute/normalize.js.map +1 -0
  63. package/dist/motion/compute/validate.d.ts +22 -0
  64. package/dist/motion/compute/validate.js +103 -0
  65. package/dist/motion/compute/validate.js.map +1 -0
  66. package/dist/motion/index.d.ts +8 -0
  67. package/dist/motion/index.js +6 -0
  68. package/dist/motion/index.js.map +1 -0
  69. package/dist/motion/native/serializeMotionTimeline.d.ts +12 -0
  70. package/dist/motion/native/serializeMotionTimeline.js +21 -0
  71. package/dist/motion/native/serializeMotionTimeline.js.map +1 -0
  72. package/dist/physicalMetrics-W5uZ5xKb.d.ts +48 -0
  73. package/dist/physicalMetrics.d.ts +1 -0
  74. package/dist/physicalMetrics.js +58 -0
  75. package/dist/physicalMetrics.js.map +1 -0
  76. package/dist/platform-adapter/CommandResultUtils.d.ts +12 -0
  77. package/dist/platform-adapter/CommandResultUtils.js +21 -0
  78. package/dist/platform-adapter/CommandResultUtils.js.map +1 -0
  79. package/dist/platform-adapter/createPlatformSync.d.ts +16 -0
  80. package/dist/platform-adapter/createPlatformSync.js +30 -0
  81. package/dist/platform-adapter/createPlatformSync.js.map +1 -0
  82. package/dist/platform-adapter/index.d.ts +12 -0
  83. package/dist/platform-adapter/index.js +10 -0
  84. package/dist/platform-adapter/index.js.map +1 -0
  85. package/dist/platform-adapter/interface.d.ts +7 -0
  86. package/dist/platform-adapter/interface.js +1 -0
  87. package/dist/platform-adapter/interface.js.map +1 -0
  88. package/dist/platform-adapter/pico-os/PicoOSPlatform.d.ts +25 -0
  89. package/dist/platform-adapter/pico-os/PicoOSPlatform.js +168 -0
  90. package/dist/platform-adapter/pico-os/PicoOSPlatform.js.map +1 -0
  91. package/dist/platform-adapter/puppeteer/PuppeteerPlatform.d.ts +66 -0
  92. package/dist/platform-adapter/puppeteer/PuppeteerPlatform.js +372 -0
  93. package/dist/platform-adapter/puppeteer/PuppeteerPlatform.js.map +1 -0
  94. package/dist/platform-adapter/spatialRequestMetadata.d.ts +6 -0
  95. package/dist/platform-adapter/spatialRequestMetadata.js +39 -0
  96. package/dist/platform-adapter/spatialRequestMetadata.js.map +1 -0
  97. package/dist/platform-adapter/spatialSceneQuery.d.ts +12 -0
  98. package/dist/platform-adapter/spatialSceneQuery.js +12 -0
  99. package/dist/platform-adapter/spatialSceneQuery.js.map +1 -0
  100. package/dist/platform-adapter/vision-os/VisionOSPlatform.d.ts +19 -0
  101. package/dist/platform-adapter/vision-os/VisionOSPlatform.js +77 -0
  102. package/dist/platform-adapter/vision-os/VisionOSPlatform.js.map +1 -0
  103. package/dist/platform-runtime.d.ts +12 -0
  104. package/dist/platform-runtime.js +14 -0
  105. package/dist/platform-runtime.js.map +1 -0
  106. package/dist/reality/Attachment.d.ts +19 -0
  107. package/dist/reality/Attachment.js +39 -0
  108. package/dist/reality/Attachment.js.map +1 -0
  109. package/dist/reality/component/ModelComponent.d.ts +14 -0
  110. package/dist/reality/component/ModelComponent.js +11 -0
  111. package/dist/reality/component/ModelComponent.js.map +1 -0
  112. package/dist/reality/component/SpatialComponent.d.ts +8 -0
  113. package/dist/reality/component/SpatialComponent.js +19 -0
  114. package/dist/reality/component/SpatialComponent.js.map +1 -0
  115. package/dist/reality/component/index.d.ts +8 -0
  116. package/dist/reality/component/index.js +3 -0
  117. package/dist/reality/component/index.js.map +1 -0
  118. package/dist/reality/entity/SpatialEntity.d.ts +7 -0
  119. package/dist/reality/entity/SpatialEntity.js +291 -0
  120. package/dist/reality/entity/SpatialEntity.js.map +1 -0
  121. package/dist/reality/entity/SpatialModelEntity.d.ts +17 -0
  122. package/dist/reality/entity/SpatialModelEntity.js +17 -0
  123. package/dist/reality/entity/SpatialModelEntity.js.map +1 -0
  124. package/dist/reality/entity/index.d.ts +8 -0
  125. package/dist/reality/entity/index.js +3 -0
  126. package/dist/reality/entity/index.js.map +1 -0
  127. package/dist/reality/geometry/SpatialBoxGeometry.d.ts +16 -0
  128. package/dist/reality/geometry/SpatialBoxGeometry.js +13 -0
  129. package/dist/reality/geometry/SpatialBoxGeometry.js.map +1 -0
  130. package/dist/reality/geometry/SpatialConeGeometry.d.ts +16 -0
  131. package/dist/reality/geometry/SpatialConeGeometry.js +13 -0
  132. package/dist/reality/geometry/SpatialConeGeometry.js.map +1 -0
  133. package/dist/reality/geometry/SpatialCylinderGeometry.d.ts +16 -0
  134. package/dist/reality/geometry/SpatialCylinderGeometry.js +13 -0
  135. package/dist/reality/geometry/SpatialCylinderGeometry.js.map +1 -0
  136. package/dist/reality/geometry/SpatialGeometry.d.ts +7 -0
  137. package/dist/reality/geometry/SpatialGeometry.js +13 -0
  138. package/dist/reality/geometry/SpatialGeometry.js.map +1 -0
  139. package/dist/reality/geometry/SpatialPlaneGeometry.d.ts +16 -0
  140. package/dist/reality/geometry/SpatialPlaneGeometry.js +13 -0
  141. package/dist/reality/geometry/SpatialPlaneGeometry.js.map +1 -0
  142. package/dist/reality/geometry/SpatialSphereGeometry.d.ts +16 -0
  143. package/dist/reality/geometry/SpatialSphereGeometry.js +13 -0
  144. package/dist/reality/geometry/SpatialSphereGeometry.js.map +1 -0
  145. package/dist/reality/geometry/index.d.ts +12 -0
  146. package/dist/reality/geometry/index.js +7 -0
  147. package/dist/reality/geometry/index.js.map +1 -0
  148. package/dist/reality/index.d.ts +18 -0
  149. package/dist/reality/index.js +7 -0
  150. package/dist/reality/index.js.map +1 -0
  151. package/dist/reality/material/SpatialMaterial.d.ts +7 -0
  152. package/dist/reality/material/SpatialMaterial.js +13 -0
  153. package/dist/reality/material/SpatialMaterial.js.map +1 -0
  154. package/dist/reality/material/SpatialUnlitMaterial.d.ts +16 -0
  155. package/dist/reality/material/SpatialUnlitMaterial.js +16 -0
  156. package/dist/reality/material/SpatialUnlitMaterial.js.map +1 -0
  157. package/dist/reality/material/index.d.ts +8 -0
  158. package/dist/reality/material/index.js +3 -0
  159. package/dist/reality/material/index.js.map +1 -0
  160. package/dist/reality/realityCreator.d.ts +22 -0
  161. package/dist/reality/realityCreator.js +89 -0
  162. package/dist/reality/realityCreator.js.map +1 -0
  163. package/dist/reality/resource/SpatialModelAsset.d.ts +15 -0
  164. package/dist/reality/resource/SpatialModelAsset.js +12 -0
  165. package/dist/reality/resource/SpatialModelAsset.js.map +1 -0
  166. package/dist/reality/resource/SpatialTextureResource.d.ts +16 -0
  167. package/dist/reality/resource/SpatialTextureResource.js +16 -0
  168. package/dist/reality/resource/SpatialTextureResource.js.map +1 -0
  169. package/dist/reality/resource/index.d.ts +9 -0
  170. package/dist/reality/resource/index.js +3 -0
  171. package/dist/reality/resource/index.js.map +1 -0
  172. package/dist/runtime/WebSpatialRuntimeError.d.ts +10 -0
  173. package/dist/runtime/WebSpatialRuntimeError.js +14 -0
  174. package/dist/runtime/WebSpatialRuntimeError.js.map +1 -0
  175. package/dist/runtime/capability-data.d.ts +12 -0
  176. package/dist/runtime/capability-data.js +134 -0
  177. package/dist/runtime/capability-data.js.map +1 -0
  178. package/dist/runtime/index.d.ts +8 -0
  179. package/dist/runtime/index.js +27 -0
  180. package/dist/runtime/index.js.map +1 -0
  181. package/dist/runtime/jsbAdapterPlatform.d.ts +9 -0
  182. package/dist/runtime/jsbAdapterPlatform.js +38 -0
  183. package/dist/runtime/jsbAdapterPlatform.js.map +1 -0
  184. package/dist/runtime/keys.d.ts +34 -0
  185. package/dist/runtime/keys.js +122 -0
  186. package/dist/runtime/keys.js.map +1 -0
  187. package/dist/runtime/semver.d.ts +8 -0
  188. package/dist/runtime/semver.js +32 -0
  189. package/dist/runtime/semver.js.map +1 -0
  190. package/dist/runtime/supports.d.ts +22 -0
  191. package/dist/runtime/supports.js +78 -0
  192. package/dist/runtime/supports.js.map +1 -0
  193. package/dist/runtime/types.d.ts +12 -0
  194. package/dist/runtime/types.js +1 -0
  195. package/dist/runtime/types.js.map +1 -0
  196. package/dist/runtime/userAgent.d.ts +20 -0
  197. package/dist/runtime/userAgent.js +41 -0
  198. package/dist/runtime/userAgent.js.map +1 -0
  199. package/dist/scene-polyfill.d.ts +18 -0
  200. package/dist/scene-polyfill.js +480 -0
  201. package/dist/scene-polyfill.js.map +1 -0
  202. package/dist/spatial-host.d.ts +13 -0
  203. package/dist/spatial-host.js +18 -0
  204. package/dist/spatial-host.js.map +1 -0
  205. package/dist/spatial-window-polyfill.d.ts +3 -0
  206. package/dist/spatial-window-polyfill.js +155 -0
  207. package/dist/spatial-window-polyfill.js.map +1 -0
  208. package/dist/spatializedElementMotion-_0XWPQCE.d.ts +1009 -0
  209. package/dist/types/animation.d.ts +7 -0
  210. package/dist/types/animation.js +1 -0
  211. package/dist/types/animation.js.map +1 -0
  212. package/dist/types/global.d.ts +38 -0
  213. package/dist/types/global.js +1 -0
  214. package/dist/types/global.js.map +1 -0
  215. package/dist/types/internal.d.ts +7 -0
  216. package/dist/types/internal.js +12 -0
  217. package/dist/types/internal.js.map +1 -0
  218. package/dist/types/motion/index.d.ts +7 -0
  219. package/dist/types/motion/index.js +3 -0
  220. package/dist/types/motion/index.js.map +1 -0
  221. package/dist/types/motion/spatializedElementMotion.d.ts +7 -0
  222. package/dist/types/motion/spatializedElementMotion.js +1 -0
  223. package/dist/types/motion/spatializedElementMotion.js.map +1 -0
  224. package/dist/types/motion/spatializedMotion.d.ts +7 -0
  225. package/dist/types/motion/spatializedMotion.js +16 -0
  226. package/dist/types/motion/spatializedMotion.js.map +1 -0
  227. package/dist/types/motion/spatializedPlayback.d.ts +11 -0
  228. package/dist/types/motion/spatializedPlayback.js +1 -0
  229. package/dist/types/motion/spatializedPlayback.js.map +1 -0
  230. package/dist/types/motion/spatializedVisual.d.ts +35 -0
  231. package/dist/types/motion/spatializedVisual.js +1 -0
  232. package/dist/types/motion/spatializedVisual.js.map +1 -0
  233. package/dist/types/types.d.ts +7 -0
  234. package/dist/types/types.js +117 -0
  235. package/dist/types/types.js.map +1 -0
  236. package/dist/utils.d.ts +44 -0
  237. package/dist/utils.js +86 -0
  238. package/dist/utils.js.map +1 -0
  239. package/package.json +20 -4
  240. package/src/JSBCommand.ts +132 -5
  241. package/src/SpatialObject.ts +10 -0
  242. package/src/Spatialized2DElement.ts +5 -0
  243. package/src/SpatializedDynamic3DElement.ts +5 -0
  244. package/src/SpatializedElement.ts +29 -0
  245. package/src/SpatializedStatic3DElement.ts +42 -3
  246. package/src/WebMsgCommand.ts +40 -0
  247. package/src/coverage-boost.test.ts +239 -216
  248. package/src/index.ts +25 -11
  249. package/src/install-polyfills.ts +38 -0
  250. package/src/jsbcommand.coverage.test.ts +23 -0
  251. package/src/motion/AnimationObject.test.ts +421 -0
  252. package/src/motion/AnimationObject.ts +208 -0
  253. package/src/motion/compute/index.ts +10 -0
  254. package/src/motion/compute/motionPropertyValues.test.ts +55 -0
  255. package/src/motion/compute/motionPropertyValues.ts +70 -0
  256. package/src/motion/compute/normalize.test.ts +99 -0
  257. package/src/motion/compute/normalize.ts +289 -0
  258. package/src/motion/compute/validate.test.ts +229 -0
  259. package/src/motion/compute/validate.ts +156 -0
  260. package/src/motion/index.ts +2 -0
  261. package/src/motion/native/serializeMotionTimeline.test.ts +61 -0
  262. package/src/motion/native/serializeMotionTimeline.ts +27 -0
  263. package/src/platform-adapter/createPlatformSync.ts +5 -3
  264. package/src/platform-adapter/pico-os/PicoOSPlatform.ts +76 -11
  265. package/src/platform-adapter/spatialRequestMetadata.test.ts +175 -0
  266. package/src/platform-adapter/spatialRequestMetadata.ts +48 -0
  267. package/src/platform-adapter/vision-os/VisionOSPlatform.ts +6 -1
  268. package/src/reality/entity/SpatialEntity.ts +127 -0
  269. package/src/runtime/capability-data.ts +104 -4
  270. package/src/runtime/index.ts +0 -11
  271. package/src/runtime/keys.ts +12 -1
  272. package/src/runtime/supports.test.ts +143 -4
  273. package/src/runtime/supports.ts +3 -1
  274. package/src/runtime/useAnimation.capability.test.ts +275 -0
  275. package/src/scene-polyfill.manifest.test.ts +306 -5
  276. package/src/scene-polyfill.request-metadata.test.ts +39 -0
  277. package/src/scene-polyfill.test.ts +33 -94
  278. package/src/scene-polyfill.ts +117 -87
  279. package/src/spatial-window-polyfill.ts +18 -2
  280. package/src/transform-roundtrip.test.ts +90 -0
  281. package/src/types/animation.ts +224 -0
  282. package/src/types/global.ts +1 -8
  283. package/src/types/motion/index.ts +10 -0
  284. package/src/types/motion/spatializedElementMotion.ts +49 -0
  285. package/src/types/motion/spatializedMotion.ts +147 -0
  286. package/src/types/motion/spatializedPlayback.ts +17 -0
  287. package/src/types/motion/spatializedVisual.ts +24 -0
  288. package/src/types/types.ts +3 -0
  289. package/src/utils.ts +54 -0
  290. package/tsup.config.ts +61 -36
  291. package/vitest.config.ts +5 -1
  292. package/dist/iife/index.d.ts +0 -1251
  293. package/dist/iife/index.global.js +0 -75
  294. package/dist/iife/index.global.js.map +0 -1
  295. package/src/platform-adapter/ssr/SSRPlatform.ts +0 -52
  296. /package/src/{ssr-polyfill.ts → isSSREnv.ts} +0 -0
@@ -0,0 +1,229 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+ import {
3
+ validateNormalizedMotionConfig,
4
+ validateSpatializedMotionConfig,
5
+ } from './validate'
6
+
7
+ describe('validateNormalizedMotionConfig', () => {
8
+ test('accepts a canonical track-based motion config', () => {
9
+ expect(() =>
10
+ validateNormalizedMotionConfig({
11
+ duration: 1,
12
+ tracks: [
13
+ {
14
+ property: 'transform.translate.x',
15
+ keyframes: [
16
+ { at: 0, value: 0 },
17
+ { at: 1, value: 10 },
18
+ ],
19
+ },
20
+ ],
21
+ }),
22
+ ).not.toThrow()
23
+ })
24
+
25
+ test('accepts opacity tracks', () => {
26
+ expect(() =>
27
+ validateNormalizedMotionConfig({
28
+ duration: 1,
29
+ tracks: [
30
+ {
31
+ property: 'opacity',
32
+ keyframes: [
33
+ { at: 0, value: 0 },
34
+ { at: 1, value: 1 },
35
+ ],
36
+ },
37
+ ],
38
+ }),
39
+ ).not.toThrow()
40
+ })
41
+ })
42
+
43
+ describe('validateSpatializedMotionConfig', () => {
44
+ test('accepts complete top-level segment authoring', () => {
45
+ expect(() =>
46
+ validateSpatializedMotionConfig({
47
+ from: { opacity: 0 },
48
+ to: { opacity: 1 },
49
+ }),
50
+ ).not.toThrow()
51
+ })
52
+
53
+ test.each([{ to: { opacity: 1 } }, { from: { opacity: 0 } }])(
54
+ 'rejects incomplete top-level segment authoring',
55
+ config => {
56
+ expect(() => validateSpatializedMotionConfig(config as never)).toThrow(
57
+ /top-level from and to are both required/,
58
+ )
59
+ },
60
+ )
61
+
62
+ test('accepts a timeline without top-level boundaries', () => {
63
+ expect(() =>
64
+ validateSpatializedMotionConfig({
65
+ duration: 1,
66
+ timeline: {
67
+ from: { opacity: 0 },
68
+ '50%': { opacity: 0.5 },
69
+ to: { opacity: 1 },
70
+ },
71
+ }),
72
+ ).not.toThrow()
73
+ })
74
+
75
+ test('accepts decimal spellings of percentage boundaries', () => {
76
+ expect(() =>
77
+ validateSpatializedMotionConfig({
78
+ duration: 1,
79
+ timeline: {
80
+ '0.0%': { opacity: 0 },
81
+ '100.0%': { opacity: 1 },
82
+ },
83
+ }),
84
+ ).not.toThrow()
85
+ })
86
+
87
+ test('ignores top-level boundaries when timeline is present', () => {
88
+ expect(() =>
89
+ validateSpatializedMotionConfig({
90
+ duration: 1,
91
+ from: { opacity: Number.NaN },
92
+ to: { opacity: Number.NaN },
93
+ timeline: {
94
+ '0%': { opacity: 0 },
95
+ '100%': { opacity: 1 },
96
+ },
97
+ }),
98
+ ).not.toThrow()
99
+ })
100
+
101
+ test('rejects a timeline missing the start frame', () => {
102
+ expect(() =>
103
+ validateSpatializedMotionConfig({
104
+ duration: 1,
105
+ timeline: { '50%': { opacity: 0.5 }, to: { opacity: 1 } },
106
+ }),
107
+ ).toThrow(/must define a start frame/)
108
+ })
109
+
110
+ test('rejects a timeline missing the end frame', () => {
111
+ expect(() =>
112
+ validateSpatializedMotionConfig({
113
+ duration: 1,
114
+ timeline: { from: { opacity: 0 }, '50%': { opacity: 0.5 } },
115
+ }),
116
+ ).toThrow(/must define an end frame/)
117
+ })
118
+
119
+ test('warns when top-level from/to coexist with a timeline', () => {
120
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
121
+ validateSpatializedMotionConfig({
122
+ duration: 1,
123
+ from: { opacity: 0.25 },
124
+ to: { opacity: 0.75 },
125
+ timeline: {
126
+ '0%': { opacity: 0 },
127
+ '100%': { opacity: 1 },
128
+ },
129
+ })
130
+ expect(warnSpy).toHaveBeenCalledWith(
131
+ expect.stringContaining('top-level from/to are ignored'),
132
+ )
133
+ warnSpy.mockRestore()
134
+ })
135
+
136
+ test('rejects a property with only one keyframe', () => {
137
+ // Timeline has overall start/end frames (opacity at 0% and 100%), but the
138
+ // rotate.z property appears only once at 50%, which is fewer than two keyframes.
139
+ expect(() =>
140
+ validateSpatializedMotionConfig({
141
+ duration: 1,
142
+ timeline: {
143
+ '0%': { opacity: 0 },
144
+ '50%': { transform: { rotate: { z: 45 } } },
145
+ '100%': { opacity: 1 },
146
+ },
147
+ }),
148
+ ).toThrow(/track "transform.rotate.z" needs at least 2 keyframes/)
149
+ })
150
+
151
+ test.each([
152
+ {
153
+ duration: 1,
154
+ timeline: {
155
+ from: { opacity: 0 },
156
+ '0%': { opacity: 0 },
157
+ to: { opacity: 1 },
158
+ },
159
+ expected: /duplicate start boundary.*opacity/,
160
+ },
161
+ {
162
+ duration: 1,
163
+ timeline: {
164
+ from: { opacity: 0 },
165
+ to: { opacity: 1 },
166
+ '100%': { opacity: 1 },
167
+ },
168
+ expected: /duplicate end boundary.*opacity/,
169
+ },
170
+ ])('rejects duplicate property boundaries', config => {
171
+ expect(() => validateSpatializedMotionConfig(config as never)).toThrow(
172
+ config.expected,
173
+ )
174
+ })
175
+
176
+ test.each([
177
+ ['0%', '00%'],
178
+ ['0%', '0.0%'],
179
+ ['50%', '050.0%'],
180
+ ])(
181
+ 'rejects equivalent percentage keys %s and %s for the same property',
182
+ (firstKey, secondKey) => {
183
+ expect(() =>
184
+ validateSpatializedMotionConfig({
185
+ duration: 1,
186
+ timeline: {
187
+ '0%': { opacity: 0 },
188
+ [firstKey]: { opacity: 0.25 },
189
+ [secondKey]: { opacity: 0.25 },
190
+ '100%': { opacity: 1 },
191
+ },
192
+ }),
193
+ ).toThrow(/keyframe times must be strictly increasing.*opacity/)
194
+ },
195
+ )
196
+
197
+ test('accepts equivalent percentage keys for different properties', () => {
198
+ expect(() =>
199
+ validateSpatializedMotionConfig({
200
+ duration: 1,
201
+ timeline: {
202
+ '0%': { opacity: 0 },
203
+ '0.0%': { transform: { translate: { x: 0 } } },
204
+ '100%': {
205
+ opacity: 1,
206
+ transform: { translate: { x: 1 } },
207
+ },
208
+ },
209
+ }),
210
+ ).not.toThrow()
211
+ })
212
+
213
+ test('rejects public tracks authoring', () => {
214
+ expect(() =>
215
+ validateSpatializedMotionConfig({
216
+ duration: 1,
217
+ tracks: [
218
+ {
219
+ property: 'opacity',
220
+ keyframes: [
221
+ { at: 0, value: 0 },
222
+ { at: 1, value: 1 },
223
+ ],
224
+ },
225
+ ],
226
+ } as never),
227
+ ).toThrow(/tracks authoring is internal/)
228
+ })
229
+ })
@@ -0,0 +1,156 @@
1
+ import { SPATIALIZED_MOTION_PROPERTIES } from '../../types/motion/spatializedMotion'
2
+ import type {
3
+ NormalizedSpatializedMotionConfig,
4
+ SpatializedMotionProperty,
5
+ SpatializedMotionConfig,
6
+ SpatializedMotionTrack,
7
+ } from '../../types/motion/spatializedMotion'
8
+ import { normalizeMotionConfig } from './normalize'
9
+ const ALLOWED_PROPERTIES = new Set<SpatializedMotionProperty>(
10
+ SPATIALIZED_MOTION_PROPERTIES,
11
+ )
12
+
13
+ const FORBIDDEN_PREFIXES = [
14
+ 'width',
15
+ 'height',
16
+ 'back',
17
+ 'backOffset',
18
+ 'depth',
19
+ ] as const
20
+
21
+ /**
22
+ * Validates any supported motion config shape before playback or serialization.
23
+ *
24
+ * @param config Motion config in any supported input shape.
25
+ */
26
+ export function validateSpatializedMotionConfig(
27
+ config: SpatializedMotionConfig,
28
+ ): void {
29
+ if (config.tracks !== undefined) {
30
+ throw new Error('[SpatializedMotion] tracks authoring is internal')
31
+ }
32
+ const normalized = normalizeMotionConfig(config)
33
+ validateNormalizedMotionConfig(normalized)
34
+ }
35
+
36
+ /**
37
+ * Validates the canonical track-based motion config form.
38
+ *
39
+ * @param config Canonical motion config to validate.
40
+ */
41
+ export function validateNormalizedMotionConfig(
42
+ config: NormalizedSpatializedMotionConfig,
43
+ ): void {
44
+ const { duration, tracks } = config
45
+
46
+ if (
47
+ typeof duration !== 'number' ||
48
+ !Number.isFinite(duration) ||
49
+ duration <= 0
50
+ ) {
51
+ throw new Error('[SpatializedMotion] duration must be a finite number > 0')
52
+ }
53
+
54
+ if (!Array.isArray(tracks) || tracks.length === 0) {
55
+ throw new Error('[SpatializedMotion] tracks must be a non-empty array')
56
+ }
57
+
58
+ const seen = new Set<string>()
59
+ for (const track of tracks) {
60
+ validateTrack(track, duration, seen)
61
+ }
62
+
63
+ if (config.delay !== undefined) {
64
+ const d = config.delay
65
+ if (typeof d !== 'number' || !Number.isFinite(d) || d < 0) {
66
+ throw new Error('[SpatializedMotion] delay must be >= 0 and finite')
67
+ }
68
+ }
69
+
70
+ if (config.playbackRate !== undefined) {
71
+ const r = config.playbackRate
72
+ if (typeof r !== 'number' || !Number.isFinite(r) || r <= 0) {
73
+ throw new Error('[SpatializedMotion] playbackRate must be > 0 and finite')
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Validates a single track against property and keyframe invariants.
80
+ *
81
+ * @param track Motion track to validate.
82
+ * @param duration Parent motion duration.
83
+ * @param seen Track-property set used to detect duplicates.
84
+ */
85
+ function validateTrack(
86
+ track: SpatializedMotionTrack,
87
+ duration: number,
88
+ seen: Set<string>,
89
+ ): void {
90
+ const prop = track.property as string
91
+ for (const forbidden of FORBIDDEN_PREFIXES) {
92
+ if (prop === forbidden || prop.startsWith(`${forbidden}.`)) {
93
+ throw new Error(
94
+ `[SpatializedMotion] property "${prop}" is not animatable`,
95
+ )
96
+ }
97
+ }
98
+
99
+ if (!ALLOWED_PROPERTIES.has(track.property)) {
100
+ throw new Error(`[SpatializedMotion] unknown property "${prop}"`)
101
+ }
102
+
103
+ if (seen.has(prop)) {
104
+ throw new Error(`[SpatializedMotion] duplicate track for "${prop}"`)
105
+ }
106
+ seen.add(prop)
107
+
108
+ const kf = track.keyframes
109
+ if (!Array.isArray(kf) || kf.length < 2) {
110
+ throw new Error(
111
+ `[SpatializedMotion] track "${prop}" needs at least 2 keyframes`,
112
+ )
113
+ }
114
+
115
+ let prevAt = -Infinity
116
+ for (const frame of kf) {
117
+ if (typeof frame.at !== 'number' || !Number.isFinite(frame.at)) {
118
+ throw new Error(
119
+ `[SpatializedMotion] keyframe.at must be finite for "${prop}"`,
120
+ )
121
+ }
122
+ if (frame.at < 0 || frame.at > duration) {
123
+ throw new Error(
124
+ `[SpatializedMotion] keyframe.at must be within [0, duration] for "${prop}"`,
125
+ )
126
+ }
127
+ if (frame.at <= prevAt) {
128
+ throw new Error(
129
+ `[SpatializedMotion] keyframe times must be strictly increasing for "${prop}"`,
130
+ )
131
+ }
132
+ prevAt = frame.at
133
+ validateScalar(frame.value, track.property)
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Validates a numeric sample for a specific motion property.
139
+ *
140
+ * @param value Numeric sample value to validate.
141
+ * @param property Motion property the value belongs to.
142
+ */
143
+ function validateScalar(
144
+ value: number,
145
+ property: SpatializedMotionProperty,
146
+ ): void {
147
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
148
+ throw new Error(`[SpatializedMotion] invalid value for ${property}`)
149
+ }
150
+ if (property === 'opacity' && (value < 0 || value > 1)) {
151
+ throw new Error('[SpatializedMotion] opacity must be in [0, 1]')
152
+ }
153
+ if (property.startsWith('transform.scale.') && value < 0) {
154
+ throw new Error('[SpatializedMotion] scale must be >= 0')
155
+ }
156
+ }
@@ -0,0 +1,2 @@
1
+ export * from './AnimationObject'
2
+ export { validateSpatializedMotionConfig } from './compute/validate'
@@ -0,0 +1,61 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { motionConfigToNativeTimeline } from './serializeMotionTimeline'
3
+
4
+ describe('motionConfigToNativeTimeline', () => {
5
+ test('builds timeline payload for multi-track', () => {
6
+ const tl = motionConfigToNativeTimeline({
7
+ duration: 5,
8
+ tracks: [
9
+ {
10
+ property: 'transform.translate.x',
11
+ keyframes: [
12
+ { at: 0, value: 0 },
13
+ { at: 5, value: 100 },
14
+ ],
15
+ },
16
+ {
17
+ property: 'opacity',
18
+ timingFunction: 'easeOut',
19
+ keyframes: [
20
+ { at: 3, value: 0 },
21
+ { at: 5, value: 1 },
22
+ ],
23
+ },
24
+ ],
25
+ })
26
+ expect(tl.duration).toBe(5)
27
+ expect(tl.tracks).toHaveLength(2)
28
+ expect(tl.tracks[1].timingFunction).toBe('easeOut')
29
+ })
30
+
31
+ test('serializes keyframe and config timingFunction propagation', () => {
32
+ const tl = motionConfigToNativeTimeline({
33
+ duration: 4,
34
+ timingFunction: 'easeInOut',
35
+ tracks: [
36
+ {
37
+ property: 'transform.translate.x',
38
+ keyframes: [
39
+ { at: 0, value: 0, timingFunction: 'easeOut' },
40
+ { at: 2, value: 100 },
41
+ { at: 4, value: 200 },
42
+ ],
43
+ },
44
+ {
45
+ property: 'opacity',
46
+ keyframes: [
47
+ { at: 0, value: 0 },
48
+ { at: 4, value: 1 },
49
+ ],
50
+ },
51
+ ],
52
+ })
53
+
54
+ expect(tl.tracks[0].keyframes[0]).toMatchObject({
55
+ at: 0,
56
+ value: 0,
57
+ timingFunction: 'easeOut',
58
+ })
59
+ expect(tl.tracks[1].timingFunction).toBe('easeInOut')
60
+ })
61
+ })
@@ -0,0 +1,27 @@
1
+ import type {
2
+ NormalizedSpatializedMotionConfig,
3
+ SpatializedMotionTimeline,
4
+ } from '../../types/motion/spatializedMotion'
5
+
6
+ /** Full timeline payload for native. */
7
+ export function motionConfigToNativeTimeline(
8
+ config: NormalizedSpatializedMotionConfig,
9
+ ): SpatializedMotionTimeline {
10
+ return {
11
+ duration: config.duration,
12
+ delay: config.delay,
13
+ playbackRate: config.playbackRate,
14
+ loop: config.loop,
15
+ tracks: config.tracks.map(track => ({
16
+ property: track.property,
17
+ keyframes: track.keyframes.map(k => ({
18
+ at: k.at,
19
+ value: k.value,
20
+ ...(k.timingFunction !== undefined
21
+ ? { timingFunction: k.timingFunction }
22
+ : {}),
23
+ })),
24
+ timingFunction: track.timingFunction ?? config.timingFunction ?? 'linear',
25
+ })),
26
+ }
27
+ }
@@ -1,11 +1,13 @@
1
1
  import { resolveJsbAdapterPlatform } from '../runtime/jsbAdapterPlatform'
2
- import { isSSREnv } from '../ssr-polyfill'
2
+ import { isSSREnv } from '../isSSREnv'
3
3
  import { PlatformAbility } from './interface'
4
4
  import { PicoOSPlatform } from './pico-os/PicoOSPlatform'
5
5
  import { PuppeteerPlatform } from './puppeteer/PuppeteerPlatform'
6
- import { SSRPlatform } from './ssr/SSRPlatform'
7
6
  import { VisionOSPlatform } from './vision-os/VisionOSPlatform'
8
7
 
8
+ const SSR_PLATFORM_ERROR =
9
+ 'WebSpatial platform APIs (JSB / openSpatialScene) cannot run during SSR or without window. Use @webspatial/react-sdk default entry with facades, or CSR-gate spatial UI on the client.'
10
+
9
11
  function assertNever(_: never): never {
10
12
  throw new Error('Unhandled jsb adapter platform kind')
11
13
  }
@@ -17,7 +19,7 @@ function assertNever(_: never): never {
17
19
  */
18
20
  export function createPlatformSync(): PlatformAbility {
19
21
  if (isSSREnv()) {
20
- return new SSRPlatform()
22
+ throw new Error(SSR_PLATFORM_ERROR)
21
23
  }
22
24
  const userAgent = window.navigator.userAgent
23
25
  const kind = resolveJsbAdapterPlatform(userAgent)
@@ -11,6 +11,11 @@ import { buildSpatialSceneQuery } from '../spatialSceneQuery'
11
11
  import { SpatialWebEvent } from '../../SpatialWebEvent'
12
12
  import type { SpatialSceneCreationOptionsInternal } from '../../types/internal'
13
13
  import type { AttachmentEntityOptions } from '../../types/types'
14
+ import {
15
+ buildSpatialRequestQuery,
16
+ createSpatialRequestId,
17
+ DEFAULT_SPATIAL_REQUEST_TIMEOUT_MS,
18
+ } from '../spatialRequestMetadata'
14
19
 
15
20
  interface JSBResponse {
16
21
  success: boolean
@@ -95,26 +100,86 @@ export class PicoOSPlatform implements PlatformAbility {
95
100
  command: string,
96
101
  ): Promise<WebSpatialProtocolResult> {
97
102
  return new Promise(resolve => {
98
- const createdId = nextRequestId()
103
+ const createdId = createSpatialRequestId()
104
+ let settled = false
105
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
106
+ let cleanupDocumentReadyListener: (() => void) | undefined
107
+
108
+ // Native creation may respond synchronously, asynchronously, or never.
109
+ // Funnel every path through one settle() helper so receiver cleanup is
110
+ // exactly-once.
111
+ const settle = (result: WebSpatialProtocolResult) => {
112
+ if (settled) return
113
+ settled = true
114
+ if (timeoutId !== undefined) {
115
+ clearTimeout(timeoutId)
116
+ }
117
+ cleanupDocumentReadyListener?.()
118
+ cleanupDocumentReadyListener = undefined
119
+ SpatialWebEvent.removeEventReceiver(createdId)
120
+ resolve(result)
121
+ }
122
+
99
123
  try {
100
124
  let windowProxy: WindowProxy | null = null
125
+ const settleWhenDocumentComplete = (spatialId: string) => {
126
+ const successResult = CommandResultSuccess({
127
+ windowProxy: windowProxy,
128
+ id: spatialId,
129
+ })
130
+ const childDocument = windowProxy?.document
131
+
132
+ if (!childDocument) {
133
+ settle(
134
+ CommandResultFailure(
135
+ 'E_SPATIAL_DOCUMENT_UNAVAILABLE',
136
+ `${command} document is unavailable`,
137
+ ),
138
+ )
139
+ return
140
+ }
141
+
142
+ if (childDocument.readyState === 'complete') {
143
+ settle(successResult)
144
+ return
145
+ }
146
+
147
+ const onReadyStateChange = () => {
148
+ if (childDocument.readyState !== 'complete') return
149
+ settle(successResult)
150
+ }
151
+
152
+ cleanupDocumentReadyListener = () => {
153
+ childDocument.removeEventListener(
154
+ 'readystatechange',
155
+ onReadyStateChange,
156
+ )
157
+ }
158
+ childDocument.addEventListener('readystatechange', onReadyStateChange)
159
+ }
160
+
161
+ timeoutId = setTimeout(() => {
162
+ settle(
163
+ CommandResultFailure(
164
+ 'E_SPATIAL_REQUEST_TIMEOUT',
165
+ `${command} request timed out`,
166
+ ),
167
+ )
168
+ }, DEFAULT_SPATIAL_REQUEST_TIMEOUT_MS)
169
+
101
170
  SpatialWebEvent.addEventReceiver(
102
171
  createdId,
103
172
  (result: { spatialId: string }) => {
104
- resolve(
105
- CommandResultSuccess({
106
- windowProxy: windowProxy,
107
- id: result.spatialId,
108
- }),
109
- )
110
- SpatialWebEvent.removeEventReceiver(createdId)
173
+ settleWhenDocumentComplete(result.spatialId)
111
174
  },
112
175
  )
113
- windowProxy = this.openWindow(command, 'rid=' + createdId).windowProxy
176
+ windowProxy = this.openWindow(
177
+ command,
178
+ buildSpatialRequestQuery(createdId),
179
+ ).windowProxy
114
180
  } catch (error: unknown) {
115
181
  const { code, message } = this.parseJSBError(error)
116
- SpatialWebEvent.removeEventReceiver(createdId)
117
- resolve(CommandResultFailure(code, message))
182
+ settle(CommandResultFailure(code, message))
118
183
  }
119
184
  })
120
185
  }