@webspatial/core-sdk 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 (320) hide show
  1. package/CHANGELOG.md +84 -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 -1101
  43. package/dist/index.js +21 -3393
  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 +148 -91
  241. package/src/Spatial.ts +0 -21
  242. package/src/SpatialObject.ts +10 -0
  243. package/src/SpatialScene.ts +1 -5
  244. package/src/SpatialSession.ts +17 -3
  245. package/src/Spatialized2DElement.ts +5 -0
  246. package/src/SpatializedDynamic3DElement.ts +5 -1
  247. package/src/SpatializedElement.ts +29 -0
  248. package/src/SpatializedElementCreator.ts +6 -4
  249. package/src/SpatializedStatic3DElement.test.ts +99 -0
  250. package/src/SpatializedStatic3DElement.ts +141 -4
  251. package/src/WebMsgCommand.ts +50 -0
  252. package/src/coverage-boost.test.ts +258 -316
  253. package/src/index.ts +28 -12
  254. package/src/install-polyfills.ts +38 -0
  255. package/src/jsbcommand.coverage.test.ts +42 -48
  256. package/src/motion/AnimationObject.test.ts +421 -0
  257. package/src/motion/AnimationObject.ts +208 -0
  258. package/src/motion/compute/index.ts +10 -0
  259. package/src/motion/compute/motionPropertyValues.test.ts +55 -0
  260. package/src/motion/compute/motionPropertyValues.ts +70 -0
  261. package/src/motion/compute/normalize.test.ts +99 -0
  262. package/src/motion/compute/normalize.ts +289 -0
  263. package/src/motion/compute/validate.test.ts +229 -0
  264. package/src/motion/compute/validate.ts +156 -0
  265. package/src/motion/index.ts +2 -0
  266. package/src/motion/native/serializeMotionTimeline.test.ts +61 -0
  267. package/src/motion/native/serializeMotionTimeline.ts +27 -0
  268. package/src/platform-adapter/CommandResultUtils.ts +2 -2
  269. package/src/platform-adapter/createPlatformSync.ts +36 -0
  270. package/src/platform-adapter/index.ts +5 -51
  271. package/src/platform-adapter/interface.ts +35 -23
  272. package/src/platform-adapter/pico-os/PicoOSPlatform.ts +156 -59
  273. package/src/platform-adapter/puppeteer/PuppeteerPlatform.ts +37 -11
  274. package/src/platform-adapter/spatialRequestMetadata.test.ts +175 -0
  275. package/src/platform-adapter/spatialRequestMetadata.ts +48 -0
  276. package/src/platform-adapter/spatialSceneQuery.ts +17 -0
  277. package/src/platform-adapter/vision-os/VisionOSPlatform.ts +60 -24
  278. package/src/platform-runtime.ts +13 -0
  279. package/src/reality/Attachment.ts +2 -2
  280. package/src/reality/entity/SpatialEntity.ts +127 -2
  281. package/src/reality/realityCreator.ts +15 -1
  282. package/src/reality/resource/SpatialTextureResource.ts +16 -0
  283. package/src/reality/resource/index.ts +1 -0
  284. package/src/runtime/WebSpatialRuntimeError.ts +16 -0
  285. package/src/runtime/capability-data.ts +213 -0
  286. package/src/runtime/contract-review.test.ts +44 -0
  287. package/src/runtime/index.ts +17 -0
  288. package/src/runtime/jsbAdapterPlatform.test.ts +36 -0
  289. package/src/runtime/jsbAdapterPlatform.ts +51 -0
  290. package/src/runtime/keys.ts +140 -0
  291. package/src/runtime/semver.ts +33 -0
  292. package/src/runtime/supports.test.ts +346 -0
  293. package/src/runtime/supports.ts +112 -0
  294. package/src/runtime/types.ts +11 -0
  295. package/src/runtime/useAnimation.capability.test.ts +275 -0
  296. package/src/runtime/userAgent.ts +64 -0
  297. package/src/scene-polyfill.manifest.test.ts +312 -9
  298. package/src/scene-polyfill.request-metadata.test.ts +39 -0
  299. package/src/scene-polyfill.test.ts +33 -94
  300. package/src/scene-polyfill.ts +125 -92
  301. package/src/spatial-host.ts +25 -0
  302. package/src/spatial-window-polyfill.ts +18 -2
  303. package/src/transform-roundtrip.test.ts +90 -0
  304. package/src/types/animation.ts +224 -0
  305. package/src/types/{global.d.ts → global.ts} +10 -12
  306. package/src/types/motion/index.ts +10 -0
  307. package/src/types/motion/spatializedElementMotion.ts +49 -0
  308. package/src/types/motion/spatializedMotion.ts +147 -0
  309. package/src/types/motion/spatializedPlayback.ts +17 -0
  310. package/src/types/motion/spatializedVisual.ts +24 -0
  311. package/src/types/types.ts +12 -0
  312. package/src/utils.ts +54 -0
  313. package/tsup.config.ts +61 -36
  314. package/vitest.config.ts +5 -1
  315. package/dist/iife/index.d.ts +0 -1101
  316. package/dist/iife/index.global.js +0 -75
  317. package/dist/iife/index.global.js.map +0 -1
  318. package/src/platform-adapter/android/AndroidPlatform.ts +0 -133
  319. package/src/platform-adapter/ssr/SSRPlatform.ts +0 -43
  320. /package/src/{ssr-polyfill.ts → isSSREnv.ts} +0 -0
@@ -0,0 +1,289 @@
1
+ import type { SpatializedVisualValues } from '../../types/motion/spatializedVisual'
2
+ import type { TimingFunction } from '../../types/animation'
3
+ import type {
4
+ NormalizedSpatializedMotionConfig,
5
+ SpatializedMotionConfig,
6
+ SpatializedMotionProperty,
7
+ SpatializedMotionSegmentConfig,
8
+ SpatializedMotionTrack,
9
+ SpatializedMotionTimelineConfig,
10
+ } from '../../types/motion/spatializedMotion'
11
+
12
+ /**
13
+ * Flattens visual values into motion-property/value pairs.
14
+ *
15
+ * @param values Visual values to inspect.
16
+ * @param out Collector for discovered motion property samples.
17
+ */
18
+ function collectScalars(
19
+ values: SpatializedVisualValues,
20
+ out: Array<{ property: SpatializedMotionProperty; value: number }>,
21
+ ): void {
22
+ if (values.opacity !== undefined) {
23
+ out.push({ property: 'opacity', value: values.opacity })
24
+ }
25
+ const tr = values.transform
26
+ if (!tr) return
27
+ for (const group of ['translate', 'rotate', 'scale'] as const) {
28
+ const part = tr[group]
29
+ if (!part) continue
30
+ for (const axis of ['x', 'y', 'z'] as const) {
31
+ const v = part[axis]
32
+ if (v !== undefined) {
33
+ out.push({
34
+ property: `transform.${group}.${axis}` as SpatializedMotionProperty,
35
+ value: v,
36
+ })
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Converts `0%`-style timeline keys into [0, 1] ratios.
44
+ *
45
+ * @param key Percentage key from the timeline config.
46
+ * @returns The normalized ratio represented by the key.
47
+ */
48
+ function parsePercentageKey(key: string): number {
49
+ if (!/^\d+(\.\d+)?%$/.test(key)) {
50
+ throw new Error(`[SpatializedMotion] invalid timeline key "${key}"`)
51
+ }
52
+ const ratio = Number(key.slice(0, -1))
53
+ if (!Number.isFinite(ratio) || ratio < 0 || ratio > 100) {
54
+ throw new Error(`[SpatializedMotion] invalid timeline key "${key}"`)
55
+ }
56
+ return ratio / 100
57
+ }
58
+
59
+ /**
60
+ * Desugars shorthand from/to motion config into canonical tracks.
61
+ *
62
+ * @param simple Shorthand segment config.
63
+ * @returns Canonical track-based motion config.
64
+ */
65
+ export function segmentConfigToMotionConfig(
66
+ simple: SpatializedMotionSegmentConfig,
67
+ ): NormalizedSpatializedMotionConfig {
68
+ if (!simple.from || !simple.to) {
69
+ throw new Error(
70
+ '[SpatializedMotion] top-level from and to are both required',
71
+ )
72
+ }
73
+ const duration = simple.duration ?? 0.3
74
+ const fromScalars: Array<{
75
+ property: SpatializedMotionProperty
76
+ value: number
77
+ }> = []
78
+ const toScalars: Array<{
79
+ property: SpatializedMotionProperty
80
+ value: number
81
+ }> = []
82
+
83
+ collectScalars(simple.from, fromScalars)
84
+ collectScalars(simple.to, toScalars)
85
+
86
+ const trackMap = new Map<SpatializedMotionProperty, SpatializedMotionTrack>()
87
+ const properties = new Set([
88
+ ...fromScalars.map(entry => entry.property),
89
+ ...toScalars.map(entry => entry.property),
90
+ ])
91
+
92
+ for (const property of properties) {
93
+ const fromEntry = fromScalars.find(f => f.property === property)
94
+ const toEntry = toScalars.find(f => f.property === property)
95
+ if (!fromEntry) {
96
+ throw new Error(
97
+ `[SpatializedMotion] missing start boundary for "${property}"`,
98
+ )
99
+ }
100
+ if (!toEntry) {
101
+ throw new Error(
102
+ `[SpatializedMotion] missing end boundary for "${property}"`,
103
+ )
104
+ }
105
+ trackMap.set(property, {
106
+ property,
107
+ timingFunction: simple.timingFunction,
108
+ keyframes: [
109
+ { at: 0, value: fromEntry.value },
110
+ { at: duration, value: toEntry.value },
111
+ ],
112
+ })
113
+ }
114
+
115
+ const tracks = [...trackMap.values()]
116
+ if (tracks.length === 0) {
117
+ throw new Error(
118
+ '[SpatializedMotion.simple] to must include at least one animatable field',
119
+ )
120
+ }
121
+
122
+ return {
123
+ duration,
124
+ tracks,
125
+ timingFunction: simple.timingFunction,
126
+ delay: simple.delay,
127
+ autoStart: simple.autoStart,
128
+ loop: simple.loop,
129
+ playbackRate: simple.playbackRate,
130
+ onStart: simple.onStart,
131
+ onComplete: simple.onComplete,
132
+ onStop: simple.onStop,
133
+ onReset: simple.onReset,
134
+ onError: simple.onError,
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Desugars percentage-key timeline config into canonical tracks.
140
+ *
141
+ * @param config Timeline config using percentage keys.
142
+ * @returns Canonical track-based motion config.
143
+ */
144
+ export function desugarTimelineConfig(
145
+ config: SpatializedMotionTimelineConfig,
146
+ ): NormalizedSpatializedMotionConfig {
147
+ if (Array.isArray(config.timeline) || !config.timeline) {
148
+ throw new Error('[SpatializedMotion] timeline must be an object')
149
+ }
150
+
151
+ const entries = Object.entries(config.timeline)
152
+ let hasPercentageKey = false
153
+ const frames = entries
154
+ .map(([key, values]) => {
155
+ if (!values || typeof values !== 'object' || Array.isArray(values)) {
156
+ throw new Error(`[SpatializedMotion] invalid timeline frame "${key}"`)
157
+ }
158
+ if (key === 'from') return { key, atRatio: 0, values }
159
+ if (key === 'to') return { key, atRatio: 1, values }
160
+ hasPercentageKey = true
161
+ return { key, atRatio: parsePercentageKey(key), values }
162
+ })
163
+ .sort((a, b) => a.atRatio - b.atRatio)
164
+
165
+ // Dev warn: top-level from/to are ignored once a timeline is present.
166
+ if (config.from !== undefined || config.to !== undefined) {
167
+ console.warn(
168
+ '[SpatializedMotion] top-level from/to are ignored because timeline is present',
169
+ )
170
+ }
171
+
172
+ // Start/end boundary frames are required; this change does not fill missing
173
+ // boundaries from underlying element style.
174
+ const hasStartFrame = frames.some(frame => frame.atRatio === 0)
175
+ const hasEndFrame = frames.some(frame => frame.atRatio === 1)
176
+ if (!hasStartFrame) {
177
+ throw new Error(
178
+ '[SpatializedMotion] timeline must define a start frame (from or 0%)',
179
+ )
180
+ }
181
+ if (!hasEndFrame) {
182
+ throw new Error(
183
+ '[SpatializedMotion] timeline must define an end frame (to or 100%)',
184
+ )
185
+ }
186
+
187
+ if (hasPercentageKey && config.duration === undefined) {
188
+ throw new Error(
189
+ '[SpatializedMotion] duration is required when timeline uses percentage keys',
190
+ )
191
+ }
192
+ const duration = config.duration ?? 0.3
193
+
194
+ const trackMap = new Map<
195
+ SpatializedMotionProperty,
196
+ Array<{
197
+ at: number
198
+ value: number
199
+ timingFunction?: TimingFunction
200
+ }>
201
+ >()
202
+ const fromProperties = new Set<SpatializedMotionProperty>()
203
+ const zeroProperties = new Set<SpatializedMotionProperty>()
204
+ const toProperties = new Set<SpatializedMotionProperty>()
205
+ const hundredProperties = new Set<SpatializedMotionProperty>()
206
+
207
+ for (const frame of frames) {
208
+ const at = frame.atRatio * duration
209
+ const scalars: Array<{
210
+ property: SpatializedMotionProperty
211
+ value: number
212
+ }> = []
213
+ collectScalars(frame.values, scalars)
214
+ for (const scalar of scalars) {
215
+ if (frame.key === 'from') fromProperties.add(scalar.property)
216
+ if (frame.key !== 'from' && frame.atRatio === 0) {
217
+ zeroProperties.add(scalar.property)
218
+ }
219
+ if (frame.key === 'to') toProperties.add(scalar.property)
220
+ if (frame.key !== 'to' && frame.atRatio === 1) {
221
+ hundredProperties.add(scalar.property)
222
+ }
223
+ const keyframes = trackMap.get(scalar.property) ?? []
224
+ const keyframe = {
225
+ at,
226
+ value: scalar.value,
227
+ ...(frame.values.timingFunction !== undefined
228
+ ? { timingFunction: frame.values.timingFunction }
229
+ : {}),
230
+ }
231
+ keyframes.push(keyframe)
232
+ trackMap.set(scalar.property, keyframes)
233
+ }
234
+ }
235
+
236
+ const tracks = [...trackMap.entries()].map(([property, keyframes]) => {
237
+ const startCount =
238
+ Number(fromProperties.has(property)) +
239
+ Number(zeroProperties.has(property))
240
+ const endCount =
241
+ Number(toProperties.has(property)) +
242
+ Number(hundredProperties.has(property))
243
+ if (startCount > 1) {
244
+ throw new Error(
245
+ `[SpatializedMotion] duplicate start boundary for "${property}"`,
246
+ )
247
+ }
248
+ if (endCount > 1) {
249
+ throw new Error(
250
+ `[SpatializedMotion] duplicate end boundary for "${property}"`,
251
+ )
252
+ }
253
+ return {
254
+ property,
255
+ timingFunction: config.timingFunction,
256
+ keyframes,
257
+ }
258
+ })
259
+
260
+ return {
261
+ duration,
262
+ tracks,
263
+ timingFunction: config.timingFunction,
264
+ delay: config.delay,
265
+ autoStart: config.autoStart,
266
+ loop: config.loop,
267
+ playbackRate: config.playbackRate,
268
+ onStart: config.onStart,
269
+ onComplete: config.onComplete,
270
+ onStop: config.onStop,
271
+ onReset: config.onReset,
272
+ onError: config.onError,
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Normalizes any supported motion config shape into canonical tracks.
278
+ *
279
+ * @param config Motion config in shorthand, timeline, or canonical form.
280
+ * @returns Canonical track-based motion config.
281
+ */
282
+ export function normalizeMotionConfig(
283
+ config: SpatializedMotionConfig,
284
+ ): NormalizedSpatializedMotionConfig {
285
+ if (config.timeline !== undefined) {
286
+ return desugarTimelineConfig(config as SpatializedMotionTimelineConfig)
287
+ }
288
+ return segmentConfigToMotionConfig(config as SpatializedMotionSegmentConfig)
289
+ }
@@ -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'