@webspatial/core-sdk 1.5.0 → 1.6.1
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/CHANGELOG.md +29 -0
- package/dist/iife/index.d.ts +164 -12
- package/dist/iife/index.global.js +11 -11
- package/dist/iife/index.global.js.map +1 -1
- package/dist/index.d.ts +164 -12
- package/dist/index.js +415 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/JSBCommand.ts +7 -2
- package/src/Spatial.ts +1 -1
- package/src/SpatialSession.ts +4 -2
- package/src/SpatializedElement.ts +10 -10
- package/src/SpatializedElementCreator.ts +5 -2
- package/src/SpatializedStatic3DElement.test.ts +15 -1
- package/src/SpatializedStatic3DElement.ts +167 -8
- package/src/WebMsgCommand.ts +22 -0
- package/src/scene-polyfill.manifest.test.ts +402 -0
- package/src/scene-polyfill.test.ts +168 -18
- package/src/scene-polyfill.ts +290 -46
- package/src/types/global.d.ts +3 -0
- package/src/types/types.ts +71 -0
- package/src/utils.ts +21 -0
package/src/scene-polyfill.ts
CHANGED
|
@@ -9,8 +9,13 @@ import {
|
|
|
9
9
|
isValidWorldScalingType,
|
|
10
10
|
isValidWorldAlignmentType,
|
|
11
11
|
isValidBaseplateVisibilityType,
|
|
12
|
+
PWAManifest,
|
|
13
|
+
XRSpatialSceneConfig,
|
|
14
|
+
XRSpatialSceneDefaults,
|
|
12
15
|
} from './types/types'
|
|
13
16
|
import { SpatialSceneCreationOptionsInternal } from './types/internal'
|
|
17
|
+
import { deepCloneJSON } from './utils'
|
|
18
|
+
import { pointToPhysical, physicalToPoint } from './physicalMetrics'
|
|
14
19
|
|
|
15
20
|
const defaultSceneConfig: SpatialSceneCreationOptions = {
|
|
16
21
|
defaultSize: {
|
|
@@ -21,17 +26,80 @@ const defaultSceneConfig: SpatialSceneCreationOptions = {
|
|
|
21
26
|
|
|
22
27
|
const defaultSceneConfigVolume: SpatialSceneCreationOptions = {
|
|
23
28
|
defaultSize: {
|
|
24
|
-
width: 0.
|
|
25
|
-
height: 0.
|
|
26
|
-
depth: 0.
|
|
29
|
+
width: '0.94m',
|
|
30
|
+
height: '0.94m',
|
|
31
|
+
depth: '0.94m',
|
|
27
32
|
},
|
|
28
33
|
}
|
|
29
34
|
|
|
35
|
+
let xr_window_defaults: SpatialSceneCreationOptions = {
|
|
36
|
+
...defaultSceneConfig,
|
|
37
|
+
}
|
|
38
|
+
let xr_volume_defaults: SpatialSceneCreationOptions = {
|
|
39
|
+
...defaultSceneConfigVolume,
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
const INTERNAL_SCHEMA_PREFIX = 'webspatial://'
|
|
31
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Deep-merge two plain object trees (no arrays, no special classes).
|
|
46
|
+
* - Creates a shallow clone of base, then recursively merges properties from over.
|
|
47
|
+
* - When both sides at a key are plain objects, merges recursively; otherwise, replaces with over.
|
|
48
|
+
* - Ignores arrays (treated as replace).
|
|
49
|
+
* Intended for small configuration objects like manifest overrides.
|
|
50
|
+
*/
|
|
51
|
+
function deepMergePlain<
|
|
52
|
+
T extends Record<string, any>,
|
|
53
|
+
U extends Record<string, any> | undefined,
|
|
54
|
+
>(base: T, over: U): T & (U extends undefined ? {} : U) {
|
|
55
|
+
if (!over) return { ...(base || {}) } as any
|
|
56
|
+
const out: any = { ...(base || {}) }
|
|
57
|
+
for (const k of Object.keys(over)) {
|
|
58
|
+
const bv = out[k]
|
|
59
|
+
const ov = (over as any)[k]
|
|
60
|
+
if (
|
|
61
|
+
ov &&
|
|
62
|
+
typeof ov === 'object' &&
|
|
63
|
+
!Array.isArray(ov) &&
|
|
64
|
+
bv &&
|
|
65
|
+
typeof bv === 'object' &&
|
|
66
|
+
!Array.isArray(bv)
|
|
67
|
+
) {
|
|
68
|
+
out[k] = deepMergePlain(bv, ov)
|
|
69
|
+
} else {
|
|
70
|
+
out[k] = ov
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalize XRSpatialSceneDefaults (manifest shape) into SpatialSceneCreationOptions (runtime shape).
|
|
78
|
+
* - Only remap default_size -> defaultSize when present.
|
|
79
|
+
* - Leaves other keys (resizability, worldScaling, etc.) unchanged.
|
|
80
|
+
* - Units are left as-is; downstream formatting is handled by formatSceneConfig.
|
|
81
|
+
*/
|
|
82
|
+
function normalizeXRDefaultsToSceneOptions(
|
|
83
|
+
src: XRSpatialSceneDefaults | Record<string, any>,
|
|
84
|
+
): SpatialSceneCreationOptions {
|
|
85
|
+
const out: any = { ...(src || {}) }
|
|
86
|
+
const ds =
|
|
87
|
+
(src as any).defaultSize !== undefined
|
|
88
|
+
? (src as any).defaultSize
|
|
89
|
+
: (src as any).default_size
|
|
90
|
+
if (ds !== undefined) {
|
|
91
|
+
out.defaultSize = ds
|
|
92
|
+
}
|
|
93
|
+
if ('default_size' in out) {
|
|
94
|
+
delete out.default_size
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
32
99
|
class SceneManager {
|
|
33
100
|
private originalOpen: any
|
|
34
101
|
private static instance: SceneManager
|
|
102
|
+
private manifestReady: Promise<void> | null = null
|
|
35
103
|
static getInstance() {
|
|
36
104
|
if (!SceneManager.instance) {
|
|
37
105
|
SceneManager.instance = new SceneManager()
|
|
@@ -40,16 +108,26 @@ class SceneManager {
|
|
|
40
108
|
}
|
|
41
109
|
|
|
42
110
|
init(window: WindowProxy) {
|
|
111
|
+
this.manifestReady = this.setupManifest()
|
|
43
112
|
this.originalOpen = window.open.bind(window)
|
|
44
113
|
;(window as any).open = this.open
|
|
45
114
|
}
|
|
46
115
|
|
|
47
|
-
|
|
116
|
+
// Stores the latest formatted config used by the platform (per scene name).
|
|
117
|
+
// This object contains normalized values and is safe for internal consumption.
|
|
118
|
+
private configMap: Record<string, SpatialSceneCreationOptionsInternal> = {}
|
|
119
|
+
// Stores the raw callback return value (per scene name) to feed into the next initScene call as `pre`.
|
|
120
|
+
// We keep this unformatted so developers receive exactly what they last returned.
|
|
121
|
+
private callbackReturnMap: Record<string, SpatialSceneCreationOptions> = {}
|
|
48
122
|
private getConfig(name?: string) {
|
|
49
123
|
if (name === undefined || !this.configMap[name]) return undefined
|
|
50
124
|
return this.configMap[name]
|
|
51
125
|
}
|
|
52
126
|
|
|
127
|
+
waitManifest(): Promise<void> {
|
|
128
|
+
return this.manifestReady ?? Promise.resolve()
|
|
129
|
+
}
|
|
130
|
+
|
|
53
131
|
// Ensure URL is absolute; only convert when a relative path is provided
|
|
54
132
|
// - Keep external and special schemes untouched (http, https, data, blob, about, file, mailto, etc.)
|
|
55
133
|
// - Handle protocol-relative URLs (//example.com/path)
|
|
@@ -73,15 +151,48 @@ class SceneManager {
|
|
|
73
151
|
}
|
|
74
152
|
}
|
|
75
153
|
|
|
154
|
+
private async setupManifest() {
|
|
155
|
+
const manifest = await this.getPWAManifest()
|
|
156
|
+
try {
|
|
157
|
+
const xr = manifest?.xr_spatial_scene
|
|
158
|
+
if (!xr || typeof xr !== 'object') return
|
|
159
|
+
const { overrides, ...topLevel } = xr as XRSpatialSceneConfig
|
|
160
|
+
// Merge top-level defaults with per-scene overrides.
|
|
161
|
+
const windowRaw = deepMergePlain(topLevel, overrides?.window_scene)
|
|
162
|
+
const volumeRaw = deepMergePlain(topLevel, overrides?.volume_scene)
|
|
163
|
+
const windowNext = normalizeXRDefaultsToSceneOptions(windowRaw)
|
|
164
|
+
|
|
165
|
+
const volumeNext = normalizeXRDefaultsToSceneOptions(volumeRaw)
|
|
166
|
+
if (windowNext && Object.keys(windowNext).length > 0) {
|
|
167
|
+
xr_window_defaults = windowNext
|
|
168
|
+
}
|
|
169
|
+
if (volumeNext && Object.keys(volumeNext).length > 0) {
|
|
170
|
+
xr_volume_defaults = volumeNext
|
|
171
|
+
}
|
|
172
|
+
} catch (error: any) {
|
|
173
|
+
console.warn(
|
|
174
|
+
'SceneManager.setupManifest failed; using built-in defaults.',
|
|
175
|
+
error?.message || error,
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
76
180
|
private open = (url?: string, target?: string, features?: string) => {
|
|
77
181
|
// bypass internal
|
|
78
182
|
if (url?.startsWith(INTERNAL_SCHEMA_PREFIX)) {
|
|
79
|
-
if (
|
|
80
|
-
|
|
183
|
+
if (
|
|
184
|
+
url.includes('createSpatialized2DElement') ||
|
|
185
|
+
url.includes('createAttachment')
|
|
186
|
+
) {
|
|
187
|
+
const token = //@ts-ignore
|
|
188
|
+
(window.webSpatial || window.__webspatialShell__)?.genToken?.()
|
|
81
189
|
if (token) {
|
|
190
|
+
const command = url.includes('createAttachment')
|
|
191
|
+
? 'createAttachment'
|
|
192
|
+
: 'createSpatialized2DElement'
|
|
82
193
|
const host = window.location.host
|
|
83
194
|
const protocol = window.location.protocol
|
|
84
|
-
const finalURL = `${protocol}//${host}/${token}/?command
|
|
195
|
+
const finalURL = `${protocol}//${host}/${token}/?command=${command}`
|
|
85
196
|
const rid = new URL(url).searchParams.get('rid')
|
|
86
197
|
const final = new URL(finalURL)
|
|
87
198
|
if (rid) final.searchParams.set('rid', rid)
|
|
@@ -100,7 +211,16 @@ class SceneManager {
|
|
|
100
211
|
return newWindow
|
|
101
212
|
}
|
|
102
213
|
|
|
103
|
-
|
|
214
|
+
let cfg = target ? this.getConfig(target) : undefined
|
|
215
|
+
|
|
216
|
+
if (cfg === undefined) {
|
|
217
|
+
// if no config, use default window config
|
|
218
|
+
const preFormatted = deepCloneJSON(getSceneDefaultConfig('window'))
|
|
219
|
+
|
|
220
|
+
const [ans] = formatSceneConfig(preFormatted, 'window')
|
|
221
|
+
cfg = { ...ans, type: 'window' }
|
|
222
|
+
}
|
|
223
|
+
|
|
104
224
|
const cmd = new createSpatialSceneCommand(url!, cfg, target, features)
|
|
105
225
|
const result = cmd.executeSync()
|
|
106
226
|
|
|
@@ -120,25 +240,137 @@ class SceneManager {
|
|
|
120
240
|
options?: { type: SpatialSceneType },
|
|
121
241
|
) {
|
|
122
242
|
const sceneType = options?.type ?? 'window'
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
|
|
243
|
+
const defaultConfigRaw = getSceneDefaultConfig(sceneType)
|
|
244
|
+
const previousOrDefault =
|
|
245
|
+
this.callbackReturnMap[name] ??
|
|
246
|
+
((): SpatialSceneCreationOptions => {
|
|
247
|
+
// Clone default config to avoid mutating shared defaults during formatting.
|
|
248
|
+
const cloned = deepCloneJSON(defaultConfigRaw)
|
|
249
|
+
return cloned
|
|
250
|
+
})()
|
|
251
|
+
const rawReturnVal = callback(previousOrDefault)
|
|
252
|
+
const sanitizedReturnVal = sanitizeSceneOptionsUnits(
|
|
253
|
+
deepCloneJSON(rawReturnVal),
|
|
254
|
+
)
|
|
255
|
+
const clonedForFormat = deepCloneJSON(sanitizedReturnVal)
|
|
256
|
+
// Merge normalized user return with scene-type defaults before final formatting.
|
|
257
|
+
// This ensures missing fields fall back to the appropriate xr_window_defaults / xr_volume_defaults.
|
|
258
|
+
const baseDefaults = deepCloneJSON(getSceneDefaultConfig(sceneType))
|
|
259
|
+
const mergedForFormat = deepMergePlain(baseDefaults, clonedForFormat)
|
|
260
|
+
|
|
261
|
+
const [formattedConfig, errors] = formatSceneConfig(
|
|
262
|
+
mergedForFormat,
|
|
263
|
+
sceneType,
|
|
264
|
+
)
|
|
265
|
+
|
|
126
266
|
if (errors.length > 0) {
|
|
127
267
|
console.warn(`initScene ${name} with errors: ${errors.join(', ')}`)
|
|
128
268
|
}
|
|
269
|
+
this.callbackReturnMap[name] = sanitizedReturnVal
|
|
129
270
|
this.configMap[name] = {
|
|
130
271
|
...formattedConfig,
|
|
131
272
|
type: sceneType,
|
|
132
273
|
}
|
|
133
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Resolve and load a PWA manifest as JSON:
|
|
277
|
+
* 1) Determine href:
|
|
278
|
+
* - Prefer explicit manifestUrl if provided;
|
|
279
|
+
* - Fallback to <link rel="manifest">, preferring the raw attribute over computed href.
|
|
280
|
+
* 2) Normalize href to absolute using ensureAbsoluteUrl (respects <base href>).
|
|
281
|
+
* 3) Handle data URLs inline:
|
|
282
|
+
* - data:...;base64,... → atob then JSON.parse
|
|
283
|
+
* - data:...,... → decodeURIComponent then JSON.parse
|
|
284
|
+
* 4) Fetch with credentials same-origin first; if that fails (e.g., CORS), attempt unauthenticated fetch.
|
|
285
|
+
* 5) Parse as JSON; if response body is text, parse the text as JSON.
|
|
286
|
+
*/
|
|
287
|
+
async getPWAManifest(manifestUrl?: string): Promise<PWAManifest | undefined> {
|
|
288
|
+
let href: string | undefined = manifestUrl
|
|
289
|
+
if (!href) {
|
|
290
|
+
const el = document.querySelector(
|
|
291
|
+
'link[rel="manifest"]',
|
|
292
|
+
) as HTMLLinkElement | null
|
|
293
|
+
href = el?.getAttribute('href') || el?.href
|
|
294
|
+
}
|
|
295
|
+
if (!href) return
|
|
296
|
+
href = this.ensureAbsoluteUrl(href)
|
|
297
|
+
if (!href) return
|
|
298
|
+
if (href.startsWith('data:')) {
|
|
299
|
+
// Inline data URL manifest: data:[<mediatype>][;base64],<data>
|
|
300
|
+
try {
|
|
301
|
+
const comma = href.indexOf(',')
|
|
302
|
+
if (comma < 0) return
|
|
303
|
+
const meta = href.slice(5, comma)
|
|
304
|
+
const data = href.slice(comma + 1)
|
|
305
|
+
const isBase64 = /;base64/i.test(meta)
|
|
306
|
+
const decoded = isBase64 ? atob(data) : decodeURIComponent(data)
|
|
307
|
+
return JSON.parse(decoded)
|
|
308
|
+
} catch {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
// Same-origin fetch with credentials first.
|
|
314
|
+
const res = await fetch(href, { credentials: 'same-origin' })
|
|
315
|
+
if (!res.ok) throw new Error(String(res.status))
|
|
316
|
+
try {
|
|
317
|
+
return await res.json()
|
|
318
|
+
} catch {
|
|
319
|
+
const t = await res.text()
|
|
320
|
+
return JSON.parse(t)
|
|
321
|
+
}
|
|
322
|
+
} catch {
|
|
323
|
+
try {
|
|
324
|
+
// Fallback: unauthenticated fetch (may help when same-origin credentials fail due to CORS).
|
|
325
|
+
const res = await fetch(href)
|
|
326
|
+
if (!res.ok) return
|
|
327
|
+
try {
|
|
328
|
+
return await res.json()
|
|
329
|
+
} catch {
|
|
330
|
+
const t = await res.text()
|
|
331
|
+
return JSON.parse(t)
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function sanitizeSceneOptionsUnits(
|
|
341
|
+
val: SpatialSceneCreationOptions,
|
|
342
|
+
): SpatialSceneCreationOptions {
|
|
343
|
+
if (val?.defaultSize) {
|
|
344
|
+
const keys = ['width', 'height', 'depth'] as const
|
|
345
|
+
for (const k of keys) {
|
|
346
|
+
if (
|
|
347
|
+
k in (val.defaultSize as any) &&
|
|
348
|
+
!isValidSceneUnit((val.defaultSize as any)[k])
|
|
349
|
+
) {
|
|
350
|
+
delete (val.defaultSize as any)[k]
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (val?.resizability) {
|
|
355
|
+
const keys = ['minWidth', 'minHeight', 'maxWidth', 'maxHeight'] as const
|
|
356
|
+
for (const k of keys) {
|
|
357
|
+
if (
|
|
358
|
+
k in (val.resizability as any) &&
|
|
359
|
+
!isValidSceneUnit((val.resizability as any)[k])
|
|
360
|
+
) {
|
|
361
|
+
delete (val.resizability as any)[k]
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return val
|
|
134
366
|
}
|
|
135
367
|
|
|
136
368
|
function pxToMeter(px: number): number {
|
|
137
|
-
return px
|
|
369
|
+
return pointToPhysical(px)
|
|
138
370
|
}
|
|
139
371
|
|
|
140
372
|
function meterToPx(meter: number): number {
|
|
141
|
-
return meter
|
|
373
|
+
return physicalToPoint(meter)
|
|
142
374
|
}
|
|
143
375
|
|
|
144
376
|
function formatToNumber(
|
|
@@ -215,12 +447,11 @@ export function formatSceneConfig(
|
|
|
215
447
|
;(config.defaultSize as any)[k] = formatToNumber(
|
|
216
448
|
(config.defaultSize as any)[k],
|
|
217
449
|
isWindow ? 'px' : 'm',
|
|
218
|
-
|
|
450
|
+
'px',
|
|
219
451
|
)
|
|
220
452
|
} else {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)[k]
|
|
453
|
+
// delete invalid unit
|
|
454
|
+
delete (config.defaultSize as any)[k]
|
|
224
455
|
errors.push(`defaultSize.${k}`)
|
|
225
456
|
}
|
|
226
457
|
}
|
|
@@ -235,10 +466,11 @@ export function formatSceneConfig(
|
|
|
235
466
|
;(config.resizability as any)[k] = formatToNumber(
|
|
236
467
|
(config.resizability as any)[k],
|
|
237
468
|
'px',
|
|
238
|
-
|
|
469
|
+
'px',
|
|
239
470
|
)
|
|
240
471
|
} else {
|
|
241
|
-
|
|
472
|
+
// delete invalid unit
|
|
473
|
+
delete (config.resizability as any)[k]
|
|
242
474
|
errors.push(`resizability.${k}`)
|
|
243
475
|
}
|
|
244
476
|
}
|
|
@@ -277,6 +509,13 @@ export function initScene(
|
|
|
277
509
|
return SceneManager.getInstance().initScene(name, callback, options)
|
|
278
510
|
}
|
|
279
511
|
|
|
512
|
+
export function __getSceneConfigSnapshotForTest(
|
|
513
|
+
name: string,
|
|
514
|
+
): SpatialSceneCreationOptionsInternal | undefined {
|
|
515
|
+
const mgr = SceneManager.getInstance() as any
|
|
516
|
+
return mgr?.configMap?.[name]
|
|
517
|
+
}
|
|
518
|
+
|
|
280
519
|
export function hijackWindowOpen(window: WindowProxy) {
|
|
281
520
|
SceneManager.getInstance().init(window)
|
|
282
521
|
}
|
|
@@ -284,23 +523,22 @@ export function hijackWindowOpen(window: WindowProxy) {
|
|
|
284
523
|
export function hijackWindowATag(openedWindow: WindowProxy) {
|
|
285
524
|
openedWindow!.document.onclick = function (e) {
|
|
286
525
|
let element = e.target as HTMLElement | null
|
|
287
|
-
let found = false
|
|
288
526
|
|
|
289
527
|
// Look for <a> element in the clicked elements parents and if found override navigation behavior if needed
|
|
290
|
-
while (
|
|
291
|
-
if (element
|
|
528
|
+
while (element) {
|
|
529
|
+
if (element.tagName == 'A') {
|
|
292
530
|
// When using libraries like react route's <Link> it sets an onclick event, when this happens we should do nothing and let that occur
|
|
293
531
|
|
|
294
532
|
// if onClick is set for the element, the raw onclick will be noop() trapped so the onclick check is no longer trustable
|
|
295
533
|
// we handle all the scenarios
|
|
296
534
|
|
|
297
|
-
if (handleATag(e)) {
|
|
535
|
+
if (handleATag(e, element as HTMLAnchorElement)) {
|
|
298
536
|
return false // prevent default action and stop event propagation
|
|
299
537
|
}
|
|
300
538
|
|
|
301
539
|
return true
|
|
302
540
|
}
|
|
303
|
-
if (element
|
|
541
|
+
if (element.parentElement) {
|
|
304
542
|
element = element.parentElement
|
|
305
543
|
} else {
|
|
306
544
|
break
|
|
@@ -309,23 +547,24 @@ export function hijackWindowATag(openedWindow: WindowProxy) {
|
|
|
309
547
|
}
|
|
310
548
|
}
|
|
311
549
|
|
|
312
|
-
function handleATag(event: MouseEvent) {
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
550
|
+
function handleATag(event: MouseEvent, link: HTMLAnchorElement) {
|
|
551
|
+
// Respect clicks that have already been cancelled (e.g. by app/router handlers).
|
|
552
|
+
if (event.defaultPrevented) return false
|
|
553
|
+
// Use the anchor found during bubbling so nested clicks like <a><img /></a> are handled correctly.
|
|
554
|
+
const target = link.target
|
|
555
|
+
const url = link.href
|
|
556
|
+
|
|
557
|
+
if (target && target !== '_self') {
|
|
558
|
+
event.preventDefault()
|
|
559
|
+
window.open(url, target)
|
|
560
|
+
return true
|
|
324
561
|
}
|
|
325
562
|
}
|
|
326
563
|
|
|
327
564
|
function getSceneDefaultConfig(sceneType: SpatialSceneType) {
|
|
328
|
-
return sceneType === 'window'
|
|
565
|
+
return sceneType === 'window'
|
|
566
|
+
? xr_window_defaults || defaultSceneConfig
|
|
567
|
+
: xr_volume_defaults || defaultSceneConfigVolume
|
|
329
568
|
}
|
|
330
569
|
|
|
331
570
|
async function injectScenePolyfill() {
|
|
@@ -348,13 +587,16 @@ async function injectScenePolyfill() {
|
|
|
348
587
|
}
|
|
349
588
|
|
|
350
589
|
onContentLoaded(async () => {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
|
|
590
|
+
await SceneManager.getInstance().waitManifest()
|
|
591
|
+
const sceneType = window.xrCurrentSceneType ?? 'window'
|
|
592
|
+
const rawDefault = getSceneDefaultConfig(sceneType)
|
|
593
|
+
// Provide a formatted 'pre' to the callback for consistent units and types.
|
|
594
|
+
const pre = deepCloneJSON(rawDefault)
|
|
595
|
+
|
|
596
|
+
let cfg = pre
|
|
355
597
|
if (typeof window.xrCurrentSceneDefaults === 'function') {
|
|
356
598
|
try {
|
|
357
|
-
cfg = await window.xrCurrentSceneDefaults?.(
|
|
599
|
+
cfg = await window.xrCurrentSceneDefaults?.(pre)
|
|
358
600
|
} catch (error) {
|
|
359
601
|
console.error(error)
|
|
360
602
|
}
|
|
@@ -366,17 +608,19 @@ async function injectScenePolyfill() {
|
|
|
366
608
|
}, 1000)
|
|
367
609
|
})
|
|
368
610
|
|
|
369
|
-
|
|
370
|
-
const
|
|
611
|
+
// Merge callback return with base defaults to ensure missing fields are filled.
|
|
612
|
+
const mergedCfg = deepMergePlain(deepCloneJSON(rawDefault), cfg)
|
|
613
|
+
const [formattedConfig, errors] = formatSceneConfig(mergedCfg, sceneType)
|
|
371
614
|
if (errors.length > 0) {
|
|
372
615
|
console.warn(
|
|
373
616
|
`window.xrCurrentSceneDefaults with errors: ${errors.join(', ')}`,
|
|
374
617
|
)
|
|
375
618
|
}
|
|
376
|
-
|
|
619
|
+
const finalCfg = {
|
|
377
620
|
...formattedConfig,
|
|
378
621
|
type: sceneType,
|
|
379
|
-
}
|
|
622
|
+
}
|
|
623
|
+
await SpatialScene.getInstance().updateSceneCreationConfig(finalCfg)
|
|
380
624
|
})
|
|
381
625
|
}
|
|
382
626
|
|
package/src/types/global.d.ts
CHANGED
package/src/types/types.ts
CHANGED
|
@@ -95,10 +95,20 @@ export interface Spatialized2DElementProperties
|
|
|
95
95
|
scrollEdgeInsetsMarginRight: number
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
export interface ModelSource {
|
|
99
|
+
src: string
|
|
100
|
+
type?: string
|
|
101
|
+
}
|
|
102
|
+
|
|
98
103
|
export interface SpatializedStatic3DElementProperties
|
|
99
104
|
extends SpatializedElementProperties {
|
|
100
105
|
modelURL: string
|
|
106
|
+
sources?: ModelSource[]
|
|
101
107
|
modelTransform?: number[]
|
|
108
|
+
autoplay?: boolean
|
|
109
|
+
loop?: boolean
|
|
110
|
+
animationPaused?: boolean
|
|
111
|
+
playbackRate?: number
|
|
102
112
|
}
|
|
103
113
|
|
|
104
114
|
export interface SpatialSceneCreationOptions {
|
|
@@ -411,3 +421,64 @@ export interface AttachmentEntityUpdateOptions {
|
|
|
411
421
|
position?: [number, number, number]
|
|
412
422
|
size?: { width: number; height: number }
|
|
413
423
|
}
|
|
424
|
+
|
|
425
|
+
// manifest
|
|
426
|
+
|
|
427
|
+
export type SceneUnitPx = `${number}px`
|
|
428
|
+
export type SceneUnitM = `${number}m`
|
|
429
|
+
export type SceneUnit = number | SceneUnitPx | SceneUnitM
|
|
430
|
+
|
|
431
|
+
export interface XRSceneSize {
|
|
432
|
+
width: SceneUnit
|
|
433
|
+
height: SceneUnit
|
|
434
|
+
depth?: SceneUnit
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export interface XRSceneResizability {
|
|
438
|
+
minWidth?: SceneUnit
|
|
439
|
+
minHeight?: SceneUnit
|
|
440
|
+
maxWidth?: SceneUnit
|
|
441
|
+
maxHeight?: SceneUnit
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export interface XRMainSceneConfig extends XRSpatialSceneDefaults {
|
|
445
|
+
type?: SpatialSceneType
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export interface XRSpatialSceneDefaults {
|
|
449
|
+
default_size?: XRSceneSize
|
|
450
|
+
resizability?: XRSceneResizability
|
|
451
|
+
worldScaling?: WorldScalingType
|
|
452
|
+
worldAlignment?: WorldAlignmentType
|
|
453
|
+
baseplateVisibility?: BaseplateVisibilityType
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface XRSpatialSceneOverrides {
|
|
457
|
+
window_scene?: XRSpatialSceneDefaults
|
|
458
|
+
volume_scene?: XRSpatialSceneDefaults
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface XRSpatialSceneConfig extends XRSpatialSceneDefaults {
|
|
462
|
+
overrides?: XRSpatialSceneOverrides
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export interface XRPrdConfig {
|
|
466
|
+
xr_main_scene?: XRMainSceneConfig
|
|
467
|
+
xr_spatial_scene?: XRSpatialSceneConfig
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export interface PWAManifest extends XRPrdConfig {
|
|
471
|
+
name?: string
|
|
472
|
+
short_name?: string
|
|
473
|
+
start_url?: string
|
|
474
|
+
display?: string
|
|
475
|
+
icons?: Array<{
|
|
476
|
+
src: string
|
|
477
|
+
sizes?: string
|
|
478
|
+
type?: string
|
|
479
|
+
purpose?: string
|
|
480
|
+
}>
|
|
481
|
+
id?: string
|
|
482
|
+
scope?: string
|
|
483
|
+
[key: string]: any
|
|
484
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -59,3 +59,24 @@ export function composeSRT(position: Vec3, rotation: Vec3, scale: Vec3) {
|
|
|
59
59
|
m = m.scale(sx, sy, sz)
|
|
60
60
|
return m
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Deep-clone a plain JSON-serializable object by value.
|
|
65
|
+
*
|
|
66
|
+
* Notes:
|
|
67
|
+
* - Only use for data composed of primitives, arrays, and plain objects.
|
|
68
|
+
* - Functions, Dates, Maps/Sets, DOM nodes, and circular structures are not supported.
|
|
69
|
+
*/
|
|
70
|
+
export function deepCloneJSON<T>(value: T): T {
|
|
71
|
+
const sc = (globalThis as any).structuredClone as
|
|
72
|
+
| ((v: any) => any)
|
|
73
|
+
| undefined
|
|
74
|
+
if (typeof sc === 'function') {
|
|
75
|
+
try {
|
|
76
|
+
return sc(value)
|
|
77
|
+
} catch {
|
|
78
|
+
// fall through to JSON method
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return JSON.parse(JSON.stringify(value))
|
|
82
|
+
}
|