@webspatial/core-sdk 1.6.1 → 1.7.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 (53) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/iife/index.d.ts +386 -236
  3. package/dist/iife/index.global.js +7 -7
  4. package/dist/iife/index.global.js.map +1 -1
  5. package/dist/index.d.ts +386 -236
  6. package/dist/index.js +1467 -1210
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/JSBCommand.ts +30 -100
  10. package/src/Spatial.ts +0 -21
  11. package/src/SpatialScene.ts +1 -5
  12. package/src/SpatialSession.ts +17 -3
  13. package/src/SpatializedDynamic3DElement.ts +0 -1
  14. package/src/SpatializedElementCreator.ts +6 -4
  15. package/src/SpatializedStatic3DElement.test.ts +99 -0
  16. package/src/SpatializedStatic3DElement.ts +99 -1
  17. package/src/WebMsgCommand.ts +10 -0
  18. package/src/coverage-boost.test.ts +38 -119
  19. package/src/index.ts +3 -1
  20. package/src/jsbcommand.coverage.test.ts +19 -48
  21. package/src/platform-adapter/CommandResultUtils.ts +2 -2
  22. package/src/platform-adapter/createPlatformSync.ts +34 -0
  23. package/src/platform-adapter/index.ts +5 -51
  24. package/src/platform-adapter/interface.ts +35 -23
  25. package/src/platform-adapter/pico-os/PicoOSPlatform.ts +84 -52
  26. package/src/platform-adapter/puppeteer/PuppeteerPlatform.ts +37 -11
  27. package/src/platform-adapter/spatialSceneQuery.ts +17 -0
  28. package/src/platform-adapter/ssr/SSRPlatform.ts +24 -15
  29. package/src/platform-adapter/vision-os/VisionOSPlatform.ts +55 -24
  30. package/src/platform-runtime.ts +13 -0
  31. package/src/reality/Attachment.ts +2 -2
  32. package/src/reality/entity/SpatialEntity.ts +0 -2
  33. package/src/reality/realityCreator.ts +15 -1
  34. package/src/reality/resource/SpatialTextureResource.ts +16 -0
  35. package/src/reality/resource/index.ts +1 -0
  36. package/src/runtime/WebSpatialRuntimeError.ts +16 -0
  37. package/src/runtime/capability-data.ts +113 -0
  38. package/src/runtime/contract-review.test.ts +44 -0
  39. package/src/runtime/index.ts +28 -0
  40. package/src/runtime/jsbAdapterPlatform.test.ts +36 -0
  41. package/src/runtime/jsbAdapterPlatform.ts +51 -0
  42. package/src/runtime/keys.ts +129 -0
  43. package/src/runtime/semver.ts +33 -0
  44. package/src/runtime/supports.test.ts +207 -0
  45. package/src/runtime/supports.ts +110 -0
  46. package/src/runtime/types.ts +11 -0
  47. package/src/runtime/userAgent.ts +64 -0
  48. package/src/scene-polyfill.manifest.test.ts +7 -5
  49. package/src/scene-polyfill.ts +8 -5
  50. package/src/spatial-host.ts +25 -0
  51. package/src/types/{global.d.ts → global.ts} +10 -5
  52. package/src/types/types.ts +9 -0
  53. package/src/platform-adapter/android/AndroidPlatform.ts +0 -133
@@ -0,0 +1,110 @@
1
+ import { CAPABILITY_TABLE, type CapabilityVersionRow } from './capability-data'
2
+ import { compareSemver, parseSemverOrNull } from './semver'
3
+ import type { WebSpatialRuntimeSnapshot } from './types'
4
+ import { computeRuntimeFromUserAgent } from './userAgent'
5
+ import {
6
+ type CapabilityKey,
7
+ isKnownSubToken,
8
+ isKnownTopLevel,
9
+ normalizeCapabilityName,
10
+ } from './keys'
11
+
12
+ /**
13
+ * Unsubstituted shell version placeholder from visionOS manifest / Xcode template
14
+ * (`manifest.swift`). When still present in `WSAppShell/<version>`, treat as debug
15
+ * build: {@link supports} returns true for every valid capability query.
16
+ */
17
+ export const VISIONOS_DEBUG_SHELL_VERSION_PLACEHOLDER = 'WS_SHELL_VERSION'
18
+
19
+ let runtimeCache: WebSpatialRuntimeSnapshot | undefined
20
+
21
+ /** Test helper: clear cached UA/runtime snapshot between Vitest cases. */
22
+ export function resetRuntimeCacheForTests(): void {
23
+ runtimeCache = undefined
24
+ }
25
+
26
+ /**
27
+ * Internal runtime snapshot (not part of the public `@webspatial/react-sdk` surface).
28
+ */
29
+ export function getRuntime(): WebSpatialRuntimeSnapshot {
30
+ if (runtimeCache !== undefined) return runtimeCache
31
+ if (typeof navigator === 'undefined') {
32
+ runtimeCache = { type: null, shellVersion: null }
33
+ return runtimeCache
34
+ }
35
+ runtimeCache = computeRuntimeFromUserAgent(navigator.userAgent)
36
+ return runtimeCache
37
+ }
38
+
39
+ function selectRow(
40
+ type: 'visionos' | 'picoos',
41
+ shellVersion: string,
42
+ ): CapabilityVersionRow | null {
43
+ const norm = parseSemverOrNull(shellVersion)
44
+ if (!norm) return null
45
+ const rows = CAPABILITY_TABLE[type]
46
+ if (!rows.length) return null
47
+ const sorted = [...rows].sort((a, b) => compareSemver(a.version, b.version))
48
+ const minV = sorted[0].version
49
+ if (compareSemver(norm, minV) < 0) return null
50
+
51
+ let chosen: CapabilityVersionRow | null = null
52
+ for (const row of sorted) {
53
+ if (compareSemver(row.version, norm) <= 0) {
54
+ chosen = row
55
+ } else {
56
+ break
57
+ }
58
+ }
59
+ return chosen
60
+ }
61
+
62
+ /**
63
+ * Public capability probe (`WebSpatialRuntime.supports` re-exports this from React SDK).
64
+ */
65
+ export function supports(
66
+ name: CapabilityKey,
67
+ tokens?: readonly string[],
68
+ ): boolean
69
+ export function supports(name: string, tokens?: readonly string[]): boolean
70
+ export function supports(name: string, tokens?: readonly string[]): boolean {
71
+ if (typeof name !== 'string') return false
72
+ const canonical = normalizeCapabilityName(name)
73
+ if (!isKnownTopLevel(canonical)) return false
74
+
75
+ const tokList =
76
+ tokens === undefined ? [] : Array.isArray(tokens) ? [...tokens] : []
77
+ if (tokList.some(t => typeof t !== 'string')) return false
78
+
79
+ for (const t of tokList) {
80
+ if (!isKnownSubToken(canonical, t)) return false
81
+ }
82
+
83
+ const rt = getRuntime()
84
+ if (rt.type === 'puppeteer') {
85
+ return true
86
+ }
87
+ if (rt.type === null) return false
88
+ if (rt.shellVersion === null) return false
89
+
90
+ if (
91
+ rt.type === 'visionos' &&
92
+ rt.shellVersion === VISIONOS_DEBUG_SHELL_VERSION_PLACEHOLDER
93
+ ) {
94
+ return true
95
+ }
96
+
97
+ const parsedShell = parseSemverOrNull(rt.shellVersion)
98
+ if (!parsedShell) return false
99
+
100
+ if (rt.type !== 'visionos' && rt.type !== 'picoos') return false
101
+
102
+ const row = selectRow(rt.type, parsedShell)
103
+ if (!row) return false
104
+
105
+ if (tokList.length === 0) {
106
+ return row.flags[canonical] === true
107
+ }
108
+ if (row.flags[canonical] !== true) return false
109
+ return tokList.every(t => row.flags[`${canonical}:${t}`] === true)
110
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Internal runtime classification (see OpenSpec `review.md`).
3
+ * `puppeteer`: automation / test harness UA — not a product matrix row; see `supports()`.
4
+ */
5
+ export type WebSpatialRuntimeType = 'visionos' | 'picoos' | 'puppeteer' | null
6
+
7
+ /** Snapshot returned by internal `getRuntime()` (not a public app API). */
8
+ export type WebSpatialRuntimeSnapshot = {
9
+ type: WebSpatialRuntimeType
10
+ shellVersion: string | null
11
+ }
@@ -0,0 +1,64 @@
1
+ import type { WebSpatialRuntimeSnapshot } from './types'
2
+
3
+ /**
4
+ * Parse shell token from UA:
5
+ * - packaged/hybrid runtimes use `WSAppShell/<version>`
6
+ * - browser-mode runtime on Pico OS uses `PicoWebApp/<version>`
7
+ *
8
+ * Keep `WSAppShell` precedence when both appear in one UA.
9
+ */
10
+ export function parseShellToken(ua: string): {
11
+ version: string | null
12
+ source: 'wsapp' | 'picoapp' | null
13
+ } {
14
+ const ws = /\bWSAppShell\/([\w.-]+)/i.exec(ua)
15
+ if (ws?.[1]) return { version: ws[1], source: 'wsapp' }
16
+ const pico = /\bPicoWebApp\/([\w.-]+)/i.exec(ua)
17
+ if (pico?.[1]) return { version: pico[1], source: 'picoapp' }
18
+ return { version: null, source: null }
19
+ }
20
+
21
+ /** Pico runtime: require explicit Pico tokens (do not treat generic `like Quest` VR UAs as picoos). */
22
+ function inferPicoOs(ua: string): boolean {
23
+ return /\bPicoWebApp\//i.test(ua) || /\bPicoBrowser\b/i.test(ua)
24
+ }
25
+
26
+ /** visionOS-class WebView UAs include a Mac OS X platform token; `WSAppShell` alone is not enough. */
27
+ function inferVisionOsFromUa(ua: string): boolean {
28
+ return /Mac OS X/i.test(ua)
29
+ }
30
+
31
+ /**
32
+ * Resolve internal runtime snapshot from `navigator.userAgent`.
33
+ * SSR / no navigator → `{ type: null, shellVersion: null }` (no throw).
34
+ */
35
+ export function computeRuntimeFromUserAgent(
36
+ userAgent: string | undefined,
37
+ ): WebSpatialRuntimeSnapshot {
38
+ if (userAgent === undefined || userAgent === '') {
39
+ return { type: null, shellVersion: null }
40
+ }
41
+ // Must run before the shell-token gate: Puppeteer UAs may omit WSAppShell/PicoWebApp.
42
+ if (userAgent.includes('Puppeteer')) {
43
+ const { version } = parseShellToken(userAgent)
44
+ return { type: 'puppeteer', shellVersion: version }
45
+ }
46
+
47
+ const { version, source } = parseShellToken(userAgent)
48
+ if (!version) {
49
+ return { type: null, shellVersion: null }
50
+ }
51
+
52
+ if (source === 'picoapp' || inferPicoOs(userAgent)) {
53
+ return { type: 'picoos', shellVersion: version }
54
+ }
55
+
56
+ if (source === 'wsapp') {
57
+ if (inferVisionOsFromUa(userAgent)) {
58
+ return { type: 'visionos', shellVersion: version }
59
+ }
60
+ return { type: null, shellVersion: version }
61
+ }
62
+
63
+ return { type: null, shellVersion: version }
64
+ }
@@ -1,12 +1,14 @@
1
1
  import { describe, it, expect, vi } from 'vitest'
2
2
 
3
+ vi.mock('./spatial-host', () => ({
4
+ openSpatialSceneSync: vi.fn().mockReturnValue({
5
+ success: true,
6
+ data: { id: 'scene-1', windowProxy: {} },
7
+ }),
8
+ }))
9
+
3
10
  vi.mock('./JSBCommand', () => {
4
11
  return {
5
- createSpatialSceneCommand: vi.fn().mockImplementation(() => ({
6
- executeSync: vi.fn().mockReturnValue({
7
- data: { id: 'scene-1', windowProxy: {} },
8
- }),
9
- })),
10
12
  FocusScene: vi.fn().mockImplementation(() => ({
11
13
  execute: vi.fn().mockResolvedValue(undefined),
12
14
  })),
@@ -1,4 +1,5 @@
1
- import { createSpatialSceneCommand, FocusScene } from './JSBCommand'
1
+ import { FocusScene } from './JSBCommand'
2
+ import { openSpatialSceneSync } from './spatial-host'
2
3
  import { SpatialScene } from './SpatialScene'
3
4
  import {
4
5
  SpatialSceneCreationOptions,
@@ -221,8 +222,12 @@ class SceneManager {
221
222
  cfg = { ...ans, type: 'window' }
222
223
  }
223
224
 
224
- const cmd = new createSpatialSceneCommand(url!, cfg, target, features)
225
- const result = cmd.executeSync()
225
+ const result = openSpatialSceneSync(
226
+ url!,
227
+ cfg as SpatialSceneCreationOptionsInternal,
228
+ target,
229
+ features,
230
+ )
226
231
 
227
232
  const id = result.data?.id
228
233
 
@@ -429,8 +434,6 @@ export function formatSceneConfig(
429
434
  // defaultSize should format into px if window
430
435
  // defaultSize should format into m if volume
431
436
 
432
- const defaultSceneConfig = getSceneDefaultConfig(sceneType)
433
-
434
437
  const errors: string[] = []
435
438
 
436
439
  const isWindow = sceneType === 'window'
@@ -0,0 +1,25 @@
1
+ import type { WebSpatialProtocolResult } from './platform-adapter/interface'
2
+ import { getPlatform, getPlatformSync } from './platform-runtime'
3
+ import type { SpatialSceneCreationOptionsInternal } from './types/internal'
4
+ import type { AttachmentEntityOptions } from './types/types'
5
+
6
+ export async function createNativeSpatialDiv(): Promise<WebSpatialProtocolResult> {
7
+ const platform = await getPlatform()
8
+ return platform.createNativeSpatialDiv()
9
+ }
10
+
11
+ export async function createNativeAttachment(
12
+ options: AttachmentEntityOptions,
13
+ ): Promise<WebSpatialProtocolResult> {
14
+ const platform = await getPlatform()
15
+ return platform.createNativeAttachment(options)
16
+ }
17
+
18
+ export function openSpatialSceneSync(
19
+ url: string,
20
+ config: SpatialSceneCreationOptionsInternal | undefined,
21
+ target?: string,
22
+ features?: string,
23
+ ): WebSpatialProtocolResult {
24
+ return getPlatformSync().openSpatialSceneSync(url, config, target, features)
25
+ }
@@ -1,7 +1,8 @@
1
1
  import type { PhysicalMetricsValueShape } from '../physicalMetrics'
2
+ import type { SpatialSceneCreationOptions, SpatialSceneType } from './types'
2
3
 
3
4
  declare global {
4
- declare const __WEBSPATIAL_CORE_SDK_VERSION__: string
5
+ const __WEBSPATIAL_CORE_SDK_VERSION__: string
5
6
 
6
7
  interface Window {
7
8
  xrCurrentSceneType: SpatialSceneType
@@ -41,13 +42,17 @@ declare global {
41
42
  physicalMetrics?: PhysicalMetricsValueShape
42
43
  }
43
44
 
44
- xrInnerDepth: number
45
- xrOuterDepth: number
45
+ /** Present when `supports('xrInnerDepth')` is true; otherwise `undefined`. */
46
+ xrInnerDepth?: number
47
+ /** Present when `supports('xrOuterDepth')` is true; otherwise `undefined`. */
48
+ xrOuterDepth?: number
46
49
  }
47
50
 
48
51
  interface HTMLElement {
49
- xrOffsetBack: number
50
- xrClientDepth: number
52
+ /** Present when `supports('xrOffsetBack')` is true for element readbacks. */
53
+ xrOffsetBack?: number
54
+ /** Present when `supports('xrClientDepth')` is true for element readbacks. */
55
+ xrClientDepth?: number
51
56
  }
52
57
  }
53
58
 
@@ -100,6 +100,8 @@ export interface ModelSource {
100
100
  type?: string
101
101
  }
102
102
 
103
+ export type ModelLoadingMode = 'eager' | 'lazy'
104
+
103
105
  export interface SpatializedStatic3DElementProperties
104
106
  extends SpatializedElementProperties {
105
107
  modelURL: string
@@ -109,6 +111,9 @@ export interface SpatializedStatic3DElementProperties
109
111
  loop?: boolean
110
112
  animationPaused?: boolean
111
113
  playbackRate?: number
114
+ currentTime?: number
115
+ posterURL?: string
116
+ loading?: ModelLoadingMode
112
117
  }
113
118
 
114
119
  export interface SpatialSceneCreationOptions {
@@ -262,6 +267,10 @@ export interface SpatialUnlitMaterialOptions {
262
267
  opacity?: number
263
268
  }
264
269
 
270
+ export interface SpatialTextureResourceOptions {
271
+ url: string
272
+ }
273
+
265
274
  export interface ModelComponentOptions {
266
275
  mesh: SpatialGeometry
267
276
  materials: SpatialMaterial[]
@@ -1,133 +0,0 @@
1
- import { PlatformAbility, CommandResult } from '../interface'
2
- import {
3
- CommandResultFailure,
4
- CommandResultSuccess,
5
- } from '../CommandResultUtils'
6
- import { CheckWebViewCanCreateCommand } from '../../JSBCommand'
7
- import { SpatialWebEvent } from '../../SpatialWebEvent'
8
-
9
- interface JSBResponse {
10
- success: boolean
11
- data: any
12
- }
13
- type JSBError = {
14
- code: string
15
- message: string
16
- }
17
-
18
- let creatingElementCount = 0
19
-
20
- let requestId = 0
21
-
22
- const MAX_ID = 100000
23
-
24
- function nextRequestId() {
25
- requestId = (requestId + 1) % MAX_ID
26
- return `rId_${requestId}`
27
- }
28
-
29
- export class AndroidPlatform implements PlatformAbility {
30
- async callJSB(cmd: string, msg: string): Promise<CommandResult> {
31
- // android JS Bridge interface only support sync invoking
32
- // in order to implement promise API, register every request by requestId and remove when resolve/reject.
33
- return new Promise((resolve, reject) => {
34
- try {
35
- const rId = nextRequestId()
36
-
37
- SpatialWebEvent.addEventReceiver(rId, (result: JSBResponse) => {
38
- SpatialWebEvent.removeEventReceiver(rId)
39
- if (result.success) {
40
- resolve(CommandResultSuccess(result.data))
41
- } else {
42
- const { code, message } = result.data as JSBError
43
- resolve(CommandResultFailure(code, message))
44
- }
45
- })
46
-
47
- const ans = window.webspatialBridge.postMessage(rId, cmd, msg)
48
- if (ans !== '') {
49
- SpatialWebEvent.removeEventReceiver(rId)
50
- // sync call
51
- const result = JSON.parse(ans) as JSBResponse
52
- if (result.success) {
53
- resolve(CommandResultSuccess(result.data))
54
- } else {
55
- const { code, message } = result.data as JSBError
56
- resolve(CommandResultFailure(code, message))
57
- }
58
- }
59
- } catch (error: unknown) {
60
- console.error(
61
- `AndroidPlatform cmd: ${cmd}, msg: ${msg} error: ${error}`,
62
- )
63
- const { code, message } = error as JSBError
64
- resolve(CommandResultFailure(code, message))
65
- }
66
- })
67
- }
68
-
69
- async callWebSpatialProtocol(
70
- command: string,
71
- query?: string,
72
- target?: string,
73
- features?: string,
74
- ): Promise<CommandResult> {
75
- // Waiting for request to create spatial div
76
- await new Promise(resolve => setTimeout(resolve, 16 * creatingElementCount))
77
- // Count the current total number of created spatial div queues
78
- creatingElementCount++
79
- // Create a spatial div through JSB polling request
80
- let canCreate = await new CheckWebViewCanCreateCommand().execute()
81
- while (!canCreate.data.can) {
82
- await new Promise(resolve => setTimeout(resolve, 16))
83
- canCreate = await new CheckWebViewCanCreateCommand().execute()
84
- }
85
- // Request successful, call window.open
86
- const { windowProxy } = this.openWindow(command, query, target, features)
87
- // Polling waiting for windowProxy to convert into a real window object
88
- while (!windowProxy?.open) {
89
- await new Promise(resolve => setTimeout(resolve, 16))
90
- }
91
- // Make the page renderable through window.open
92
- windowProxy?.open('about:blank', '_self')
93
- // Polling to check if SpatialId injection is successful
94
- while (!windowProxy?.__SpatialId) {
95
- await new Promise(resolve => setTimeout(resolve, 16))
96
- }
97
- let spatialId = windowProxy?.__SpatialId
98
- creatingElementCount--
99
- return Promise.resolve(
100
- CommandResultSuccess({ windowProxy: windowProxy, id: spatialId }),
101
- )
102
- }
103
-
104
- callWebSpatialProtocolSync(
105
- command: string,
106
- query?: string,
107
- target?: string,
108
- features?: string,
109
- ): CommandResult {
110
- const { spatialId: id = '', windowProxy } = this.openWindow(
111
- command,
112
- query,
113
- target,
114
- features,
115
- )
116
-
117
- return CommandResultSuccess({ windowProxy, id })
118
- }
119
-
120
- private openWindow(
121
- command: string,
122
- query?: string,
123
- target?: string,
124
- features?: string,
125
- ) {
126
- const windowProxy = window.open(
127
- `webspatial://${command}?${query || ''}`,
128
- target,
129
- features,
130
- )
131
- return { spatialId: '', windowProxy }
132
- }
133
- }