@webspatial/core-sdk 1.6.0 → 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.
- package/CHANGELOG.md +29 -0
- package/dist/iife/index.d.ts +386 -236
- package/dist/iife/index.global.js +7 -7
- package/dist/iife/index.global.js.map +1 -1
- package/dist/index.d.ts +386 -236
- package/dist/index.js +1479 -1226
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/JSBCommand.ts +30 -100
- package/src/Spatial.ts +0 -21
- package/src/SpatialScene.ts +1 -5
- package/src/SpatialSession.ts +17 -3
- package/src/SpatializedDynamic3DElement.ts +0 -1
- package/src/SpatializedElementCreator.ts +6 -4
- package/src/SpatializedStatic3DElement.test.ts +99 -0
- package/src/SpatializedStatic3DElement.ts +99 -1
- package/src/WebMsgCommand.ts +10 -0
- package/src/coverage-boost.test.ts +38 -119
- package/src/index.ts +3 -1
- package/src/jsbcommand.coverage.test.ts +19 -48
- package/src/platform-adapter/CommandResultUtils.ts +2 -2
- package/src/platform-adapter/createPlatformSync.ts +34 -0
- package/src/platform-adapter/index.ts +5 -51
- package/src/platform-adapter/interface.ts +35 -23
- package/src/platform-adapter/pico-os/PicoOSPlatform.ts +84 -52
- package/src/platform-adapter/puppeteer/PuppeteerPlatform.ts +37 -11
- package/src/platform-adapter/spatialSceneQuery.ts +17 -0
- package/src/platform-adapter/ssr/SSRPlatform.ts +24 -15
- package/src/platform-adapter/vision-os/VisionOSPlatform.ts +55 -24
- package/src/platform-runtime.ts +13 -0
- package/src/reality/Attachment.ts +2 -2
- package/src/reality/entity/SpatialEntity.ts +0 -2
- package/src/reality/realityCreator.ts +15 -1
- package/src/reality/resource/SpatialTextureResource.ts +16 -0
- package/src/reality/resource/index.ts +1 -0
- package/src/runtime/WebSpatialRuntimeError.ts +16 -0
- package/src/runtime/capability-data.ts +113 -0
- package/src/runtime/contract-review.test.ts +44 -0
- package/src/runtime/index.ts +28 -0
- package/src/runtime/jsbAdapterPlatform.test.ts +36 -0
- package/src/runtime/jsbAdapterPlatform.ts +51 -0
- package/src/runtime/keys.ts +129 -0
- package/src/runtime/semver.ts +33 -0
- package/src/runtime/supports.test.ts +207 -0
- package/src/runtime/supports.ts +110 -0
- package/src/runtime/types.ts +11 -0
- package/src/runtime/userAgent.ts +64 -0
- package/src/scene-polyfill.manifest.test.ts +7 -5
- package/src/scene-polyfill.test.ts +60 -0
- package/src/scene-polyfill.ts +23 -22
- package/src/spatial-host.ts +25 -0
- package/src/types/{global.d.ts → global.ts} +10 -5
- package/src/types/types.ts +9 -0
- package/src/platform-adapter/android/AndroidPlatform.ts +0 -133
package/package.json
CHANGED
package/src/JSBCommand.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { WebSpatialProtocolResult } from './platform-adapter/interface'
|
|
1
|
+
import { getPlatform } from './platform-runtime'
|
|
3
2
|
import { SpatialComponent } from './reality/component/SpatialComponent'
|
|
4
3
|
import { SpatialEntity } from './reality/entity/SpatialEntity'
|
|
5
4
|
import { SpatialMaterial } from './reality/material/SpatialMaterial'
|
|
@@ -24,13 +23,12 @@ import {
|
|
|
24
23
|
Vec3,
|
|
25
24
|
AttachmentEntityOptions,
|
|
26
25
|
AttachmentEntityUpdateOptions,
|
|
26
|
+
ModelLoadingMode,
|
|
27
27
|
ModelSource,
|
|
28
|
+
SpatialTextureResourceOptions,
|
|
28
29
|
} from './types/types'
|
|
29
|
-
import { SpatialSceneCreationOptionsInternal } from './types/internal'
|
|
30
30
|
import { composeSRT } from './utils'
|
|
31
31
|
|
|
32
|
-
const platform = createPlatform()
|
|
33
|
-
|
|
34
32
|
abstract class JSBCommand {
|
|
35
33
|
commandType: string = ''
|
|
36
34
|
protected abstract getParams(): Record<string, any> | undefined
|
|
@@ -38,6 +36,7 @@ abstract class JSBCommand {
|
|
|
38
36
|
async execute() {
|
|
39
37
|
const param = this.getParams()
|
|
40
38
|
const msg = param ? JSON.stringify(param) : ''
|
|
39
|
+
const platform = await getPlatform()
|
|
41
40
|
return platform.callJSB(this.commandType, msg)
|
|
42
41
|
}
|
|
43
42
|
}
|
|
@@ -293,14 +292,20 @@ export class CreateSpatializedStatic3DElementCommand extends JSBCommand {
|
|
|
293
292
|
constructor(
|
|
294
293
|
readonly modelURL?: string,
|
|
295
294
|
readonly sources?: ModelSource[],
|
|
295
|
+
readonly loading: ModelLoadingMode = 'eager',
|
|
296
296
|
) {
|
|
297
297
|
super()
|
|
298
298
|
this.modelURL = modelURL
|
|
299
299
|
this.sources = sources
|
|
300
|
+
this.loading = loading
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
protected getParams() {
|
|
303
|
-
return {
|
|
304
|
+
return {
|
|
305
|
+
modelURL: this.modelURL,
|
|
306
|
+
sources: this.sources,
|
|
307
|
+
loading: this.loading,
|
|
308
|
+
}
|
|
304
309
|
}
|
|
305
310
|
}
|
|
306
311
|
|
|
@@ -558,7 +563,7 @@ export class ConvertCoordinateCommand extends JSBCommand {
|
|
|
558
563
|
commandType = 'ConvertCoordinate'
|
|
559
564
|
}
|
|
560
565
|
|
|
561
|
-
export class
|
|
566
|
+
export class CreateTextureCommand extends JSBCommand {
|
|
562
567
|
constructor(private url: string) {
|
|
563
568
|
super()
|
|
564
569
|
}
|
|
@@ -567,7 +572,24 @@ export class CreateTextureResourceCommand extends JSBCommand {
|
|
|
567
572
|
url: this.url,
|
|
568
573
|
}
|
|
569
574
|
}
|
|
570
|
-
commandType = '
|
|
575
|
+
commandType = 'CreateTexture'
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export class UpdateTexturePropertiesCommand extends SpatializedElementCommand {
|
|
579
|
+
properties: Partial<SpatialTextureResourceOptions>
|
|
580
|
+
commandType = 'UpdateTextureProperties'
|
|
581
|
+
|
|
582
|
+
constructor(
|
|
583
|
+
spatialObject: SpatialObject,
|
|
584
|
+
properties: Partial<SpatialTextureResourceOptions>,
|
|
585
|
+
) {
|
|
586
|
+
super(spatialObject)
|
|
587
|
+
this.properties = properties
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
protected getExtraParams() {
|
|
591
|
+
return this.properties
|
|
592
|
+
}
|
|
571
593
|
}
|
|
572
594
|
|
|
573
595
|
export class InspectCommand extends JSBCommand {
|
|
@@ -606,88 +628,6 @@ export class CheckWebViewCanCreateCommand extends JSBCommand {
|
|
|
606
628
|
}
|
|
607
629
|
}
|
|
608
630
|
|
|
609
|
-
/* WebSpatial Protocol Begin */
|
|
610
|
-
abstract class WebSpatialProtocolCommand extends JSBCommand {
|
|
611
|
-
target?: string
|
|
612
|
-
features?: string
|
|
613
|
-
|
|
614
|
-
async execute(): Promise<WebSpatialProtocolResult> {
|
|
615
|
-
const query = this.getQuery()
|
|
616
|
-
return platform.callWebSpatialProtocol(
|
|
617
|
-
this.commandType,
|
|
618
|
-
query,
|
|
619
|
-
this.target,
|
|
620
|
-
this.features,
|
|
621
|
-
)
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
executeSync(): WebSpatialProtocolResult {
|
|
625
|
-
const query = this.getQuery()
|
|
626
|
-
return platform.callWebSpatialProtocolSync(
|
|
627
|
-
this.commandType,
|
|
628
|
-
query,
|
|
629
|
-
this.target,
|
|
630
|
-
this.features,
|
|
631
|
-
)
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
private getQuery() {
|
|
635
|
-
let query = undefined
|
|
636
|
-
const params = this.getParams()
|
|
637
|
-
if (params) {
|
|
638
|
-
query = Object.keys(params)
|
|
639
|
-
.map(key => {
|
|
640
|
-
const value = params[key]
|
|
641
|
-
const finalValue =
|
|
642
|
-
typeof value === 'object' ? JSON.stringify(value) : value
|
|
643
|
-
return `${key}=${encodeURIComponent(finalValue)}`
|
|
644
|
-
})
|
|
645
|
-
.join('&')
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
return query
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
export class createSpatialized2DElementCommand extends WebSpatialProtocolCommand {
|
|
653
|
-
commandType = 'createSpatialized2DElement'
|
|
654
|
-
constructor() {
|
|
655
|
-
super()
|
|
656
|
-
}
|
|
657
|
-
protected getParams() {
|
|
658
|
-
return {}
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
export class createSpatialSceneCommand extends WebSpatialProtocolCommand {
|
|
663
|
-
commandType = 'createSpatialScene'
|
|
664
|
-
|
|
665
|
-
constructor(
|
|
666
|
-
private url: string,
|
|
667
|
-
private config: SpatialSceneCreationOptionsInternal | undefined,
|
|
668
|
-
public target?: string,
|
|
669
|
-
public features?: string,
|
|
670
|
-
) {
|
|
671
|
-
super()
|
|
672
|
-
}
|
|
673
|
-
protected getParams() {
|
|
674
|
-
return {
|
|
675
|
-
url: this.url,
|
|
676
|
-
config: this.config,
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
export class CreateAttachmentEntityCommand extends WebSpatialProtocolCommand {
|
|
682
|
-
commandType = 'createAttachment'
|
|
683
|
-
constructor(private options: AttachmentEntityOptions) {
|
|
684
|
-
super()
|
|
685
|
-
}
|
|
686
|
-
protected getParams() {
|
|
687
|
-
return {} // No metadata — just trigger engine/webview creation
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
631
|
export class InitializeAttachmentCommand extends JSBCommand {
|
|
692
632
|
commandType = 'InitializeAttachment'
|
|
693
633
|
constructor(
|
|
@@ -722,13 +662,3 @@ export class UpdateAttachmentEntityCommand extends JSBCommand {
|
|
|
722
662
|
}
|
|
723
663
|
}
|
|
724
664
|
}
|
|
725
|
-
|
|
726
|
-
// TODO: Can crypto.randomUUID be used instead including in dev environments without https
|
|
727
|
-
function uuid(): string {
|
|
728
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
729
|
-
const r = (Math.random() * 16) | 0
|
|
730
|
-
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
|
|
731
|
-
})
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/* WebSpatial Protocol End */
|
package/src/Spatial.ts
CHANGED
|
@@ -6,8 +6,6 @@ import { SpatialWebEvent } from './SpatialWebEvent'
|
|
|
6
6
|
* This is the main entry point for the WebSpatial SDK, providing access to spatial capabilities.
|
|
7
7
|
*/
|
|
8
8
|
export class Spatial {
|
|
9
|
-
private wsAppShellVersionFromUA: string | null | undefined
|
|
10
|
-
|
|
11
9
|
/**
|
|
12
10
|
* Requests a spatial session object from the browser.
|
|
13
11
|
* This is the primary method to initialize spatial functionality.
|
|
@@ -35,25 +33,6 @@ export class Spatial {
|
|
|
35
33
|
return false
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
getShellVersionFromUA(): string | null {
|
|
39
|
-
if (this.wsAppShellVersionFromUA !== undefined) {
|
|
40
|
-
return this.wsAppShellVersionFromUA
|
|
41
|
-
}
|
|
42
|
-
if (
|
|
43
|
-
typeof navigator === 'undefined' ||
|
|
44
|
-
typeof navigator.userAgent !== 'string'
|
|
45
|
-
) {
|
|
46
|
-
this.wsAppShellVersionFromUA = null
|
|
47
|
-
return null
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const match = navigator.userAgent.match(
|
|
51
|
-
/WSAppShell\/(\d+(?:\.\d+){2}(?:[-+][0-9A-Za-z.-]+)*)/,
|
|
52
|
-
)
|
|
53
|
-
this.wsAppShellVersionFromUA = match ? match[1] : '1.3.0'
|
|
54
|
-
return this.wsAppShellVersionFromUA
|
|
55
|
-
}
|
|
56
|
-
|
|
57
36
|
/** @deprecated
|
|
58
37
|
* Checks if WebSpatial is supported in the current environment.
|
|
59
38
|
* Verifies compatibility between native and client versions.
|
package/src/SpatialScene.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
SpatialSceneCreationOptions,
|
|
3
|
-
SpatialSceneProperties,
|
|
4
|
-
Vec3,
|
|
5
|
-
} from './types/types'
|
|
1
|
+
import { SpatialSceneProperties, Vec3 } from './types/types'
|
|
6
2
|
import { SpatialSceneCreationOptionsInternal } from './types/internal'
|
|
7
3
|
import {
|
|
8
4
|
AddSpatializedElementToSpatialScene,
|
package/src/SpatialSession.ts
CHANGED
|
@@ -14,14 +14,14 @@ import {
|
|
|
14
14
|
SpatialBoxGeometryOptions,
|
|
15
15
|
SpatialConeGeometryOptions,
|
|
16
16
|
SpatialCylinderGeometryOptions,
|
|
17
|
-
SpatialGeometryOptions,
|
|
18
17
|
SpatialModelEntityCreationOptions,
|
|
19
18
|
SpatialPlaneGeometryOptions,
|
|
20
|
-
SpatialSceneCreationOptions,
|
|
21
19
|
SpatialSphereGeometryOptions,
|
|
22
20
|
SpatialUnlitMaterialOptions,
|
|
21
|
+
SpatialTextureResourceOptions,
|
|
23
22
|
SpatialEntityUserData,
|
|
24
23
|
AttachmentEntityOptions,
|
|
24
|
+
ModelLoadingMode,
|
|
25
25
|
ModelSource,
|
|
26
26
|
} from './types/types'
|
|
27
27
|
import { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
createSpatialEntity,
|
|
33
33
|
createSpatialGeometry,
|
|
34
34
|
createSpatialModelEntity,
|
|
35
|
+
createSpatialTexture,
|
|
35
36
|
createSpatialUnlitMaterial,
|
|
36
37
|
} from './reality/realityCreator'
|
|
37
38
|
import {
|
|
@@ -70,13 +71,16 @@ export class SpatialSession {
|
|
|
70
71
|
* Creates a new static 3D element with an optional model URL.
|
|
71
72
|
* Static 3D elements represent pre-built 3D models that can be loaded from a URL.
|
|
72
73
|
* @param modelURL Optional URL to the 3D model to load
|
|
74
|
+
* @param sources Optional list of fallback model sources
|
|
75
|
+
* @param loading Whether the asset should fetch eagerly or be deferred (`'lazy'`)
|
|
73
76
|
* @returns Promise resolving to a new SpatializedStatic3DElement instance
|
|
74
77
|
*/
|
|
75
78
|
createSpatializedStatic3DElement(
|
|
76
79
|
modelURL?: string,
|
|
77
80
|
sources?: ModelSource[],
|
|
81
|
+
loading: ModelLoadingMode = 'eager',
|
|
78
82
|
): Promise<SpatializedStatic3DElement> {
|
|
79
|
-
return createSpatializedStatic3DElement(modelURL, sources)
|
|
83
|
+
return createSpatializedStatic3DElement(modelURL, sources, loading)
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
/**
|
|
@@ -169,6 +173,16 @@ export class SpatialSession {
|
|
|
169
173
|
return createSpatialUnlitMaterial(options)
|
|
170
174
|
}
|
|
171
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Creates a texture resource from the specified image URL.
|
|
178
|
+
* Texture resources can be referenced by materials and other spatial content.
|
|
179
|
+
* @param options Configuration options for the texture resource
|
|
180
|
+
* @returns Promise resolving to a new SpatialTextureResource instance
|
|
181
|
+
*/
|
|
182
|
+
createTexture(options: SpatialTextureResourceOptions) {
|
|
183
|
+
return createSpatialTexture(options)
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
/**
|
|
173
187
|
* Creates a model asset with the specified configuration.
|
|
174
188
|
* Model assets represent 3D model resources that can be used by entities.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createSpatialized2DElementCommand,
|
|
3
2
|
CreateSpatializedDynamic3DElementCommand,
|
|
4
3
|
CreateSpatializedStatic3DElementCommand,
|
|
5
4
|
} from './JSBCommand'
|
|
6
5
|
import { Spatialized2DElement } from './Spatialized2DElement'
|
|
7
6
|
import { SpatializedStatic3DElement } from './SpatializedStatic3DElement'
|
|
8
7
|
import { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
|
|
9
|
-
import {
|
|
8
|
+
import { createNativeSpatialDiv } from './spatial-host'
|
|
9
|
+
import { ModelLoadingMode, ModelSource } from './types/types'
|
|
10
10
|
|
|
11
11
|
export async function createSpatialized2DElement(): Promise<Spatialized2DElement> {
|
|
12
|
-
const result = await
|
|
12
|
+
const result = await createNativeSpatialDiv()
|
|
13
13
|
if (!result.success) {
|
|
14
14
|
throw new Error('createSpatialized2DElement failed')
|
|
15
15
|
} else {
|
|
@@ -24,16 +24,18 @@ export async function createSpatialized2DElement(): Promise<Spatialized2DElement
|
|
|
24
24
|
export async function createSpatializedStatic3DElement(
|
|
25
25
|
modelURL?: string,
|
|
26
26
|
sources?: ModelSource[],
|
|
27
|
+
loading: ModelLoadingMode = 'eager',
|
|
27
28
|
): Promise<SpatializedStatic3DElement> {
|
|
28
29
|
const result = await new CreateSpatializedStatic3DElementCommand(
|
|
29
30
|
modelURL,
|
|
30
31
|
sources,
|
|
32
|
+
loading,
|
|
31
33
|
).execute()
|
|
32
34
|
if (!result.success) {
|
|
33
35
|
throw new Error('createSpatializedStatic3DElement failed')
|
|
34
36
|
} else {
|
|
35
37
|
const { id } = result.data
|
|
36
|
-
return new SpatializedStatic3DElement(id, modelURL, sources)
|
|
38
|
+
return new SpatializedStatic3DElement(id, modelURL, sources, loading)
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -136,4 +136,103 @@ describe('SpatializedStatic3DElement', () => {
|
|
|
136
136
|
el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded })
|
|
137
137
|
await expect(p3).resolves.toBe(true)
|
|
138
138
|
})
|
|
139
|
+
|
|
140
|
+
it('currentTime defaults to 0 before any sample', () => {
|
|
141
|
+
const el = new SpatializedStatic3DElement('ct1', 'a.glb')
|
|
142
|
+
expect(el.currentTime).toBe(0)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('currentTime returns the anchor while paused', () => {
|
|
146
|
+
const el = new SpatializedStatic3DElement('ct2', 'a.glb')
|
|
147
|
+
el.onReceiveEvent({
|
|
148
|
+
type: SpatialWebMsgType.animationstatechange,
|
|
149
|
+
detail: {
|
|
150
|
+
paused: true,
|
|
151
|
+
duration: 10,
|
|
152
|
+
currentTime: 4,
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
expect(el.currentTime).toBe(4)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('currentTime extrapolates while playing using playbackRate', () => {
|
|
160
|
+
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1_000)
|
|
161
|
+
const el = new SpatializedStatic3DElement('ct3', 'a.glb')
|
|
162
|
+
el.onReceiveEvent({
|
|
163
|
+
type: SpatialWebMsgType.animationstatechange,
|
|
164
|
+
detail: {
|
|
165
|
+
paused: false,
|
|
166
|
+
duration: 10,
|
|
167
|
+
currentTime: 2,
|
|
168
|
+
timestamp: 1_000,
|
|
169
|
+
},
|
|
170
|
+
})
|
|
171
|
+
nowSpy.mockReturnValue(2_000) // +1s real time
|
|
172
|
+
// default rate 1 → +1s animation time
|
|
173
|
+
expect(el.currentTime).toBe(3)
|
|
174
|
+
nowSpy.mockRestore()
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('currentTime clamps extrapolation to duration', () => {
|
|
178
|
+
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1_000)
|
|
179
|
+
const el = new SpatializedStatic3DElement('ct4', 'a.glb')
|
|
180
|
+
el.onReceiveEvent({
|
|
181
|
+
type: SpatialWebMsgType.animationstatechange,
|
|
182
|
+
detail: {
|
|
183
|
+
paused: false,
|
|
184
|
+
duration: 5,
|
|
185
|
+
currentTime: 4,
|
|
186
|
+
timestamp: 1_000,
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
nowSpy.mockReturnValue(11_000) // +10s real time → would extrapolate to 14
|
|
190
|
+
expect(el.currentTime).toBe(5)
|
|
191
|
+
nowSpy.mockRestore()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('setting currentTime optimistically updates the anchor', async () => {
|
|
195
|
+
const el = new SpatializedStatic3DElement('ct5', 'a.glb')
|
|
196
|
+
el.onReceiveEvent({
|
|
197
|
+
type: SpatialWebMsgType.animationstatechange,
|
|
198
|
+
detail: {
|
|
199
|
+
paused: true,
|
|
200
|
+
duration: 10,
|
|
201
|
+
currentTime: 0,
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
el.currentTime = 7
|
|
206
|
+
expect(el.currentTime).toBe(7)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('setting currentTime clamps negative values to 0', async () => {
|
|
210
|
+
const el = new SpatializedStatic3DElement('ct6', 'a.glb')
|
|
211
|
+
el.onReceiveEvent({
|
|
212
|
+
type: SpatialWebMsgType.animationstatechange,
|
|
213
|
+
detail: {
|
|
214
|
+
paused: true,
|
|
215
|
+
duration: 10,
|
|
216
|
+
currentTime: 5,
|
|
217
|
+
timestamp: Date.now(),
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
el.currentTime = -3
|
|
221
|
+
expect(el.currentTime).toBe(0)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('setting currentTime clamps values above duration', async () => {
|
|
225
|
+
const el = new SpatializedStatic3DElement('ct7', 'a.glb')
|
|
226
|
+
el.onReceiveEvent({
|
|
227
|
+
type: SpatialWebMsgType.animationstatechange,
|
|
228
|
+
detail: {
|
|
229
|
+
paused: true,
|
|
230
|
+
duration: 8,
|
|
231
|
+
currentTime: 0,
|
|
232
|
+
timestamp: Date.now(),
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
el.currentTime = 100
|
|
236
|
+
expect(el.currentTime).toBe(8)
|
|
237
|
+
})
|
|
139
238
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { UpdateSpatializedStatic3DElementProperties } from './JSBCommand'
|
|
2
2
|
import { ReceiveEventData, SpatializedElement } from './SpatializedElement'
|
|
3
3
|
import {
|
|
4
|
+
ModelLoadingMode,
|
|
4
5
|
ModelSource,
|
|
5
6
|
SpatializedStatic3DElementProperties,
|
|
6
7
|
} from './types/types'
|
|
@@ -24,11 +25,18 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
24
25
|
* @param id Unique identifier for this element
|
|
25
26
|
* @param modelURL URL of the 3D model
|
|
26
27
|
* @param sources Optional fallback model sources
|
|
28
|
+
* @param loading Initial loading mode (`'eager'` by default)
|
|
27
29
|
*/
|
|
28
|
-
constructor(
|
|
30
|
+
constructor(
|
|
31
|
+
id: string,
|
|
32
|
+
modelURL?: string,
|
|
33
|
+
sources?: ModelSource[],
|
|
34
|
+
loading: ModelLoadingMode = 'eager',
|
|
35
|
+
) {
|
|
29
36
|
super(id)
|
|
30
37
|
this.modelURL = modelURL
|
|
31
38
|
this.sources = sources
|
|
39
|
+
this._loading = loading
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
@@ -42,6 +50,10 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
42
50
|
* Used to reset the ready promise when the model URL changes.
|
|
43
51
|
*/
|
|
44
52
|
private modelURL?: string
|
|
53
|
+
// @TODO: Deprecate modelURL property from Web and Swift code
|
|
54
|
+
get modelUrl(): string | undefined {
|
|
55
|
+
return this.modelURL
|
|
56
|
+
}
|
|
45
57
|
|
|
46
58
|
/**
|
|
47
59
|
* Caches the last sources array to detect changes.
|
|
@@ -109,8 +121,23 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
109
121
|
this._loop = properties.loop
|
|
110
122
|
}
|
|
111
123
|
if (properties.playbackRate !== undefined) {
|
|
124
|
+
// Re-anchor so extrapolation uses the new rate only for future elapsed
|
|
125
|
+
// time, not the window between the last sample and this change.
|
|
126
|
+
if (!this._paused) {
|
|
127
|
+
this._currentTime = this.currentTime
|
|
128
|
+
this._anchorTimestamp = Date.now()
|
|
129
|
+
}
|
|
112
130
|
this._playbackRate = properties.playbackRate
|
|
113
131
|
}
|
|
132
|
+
if (properties.currentTime !== undefined) {
|
|
133
|
+
// Optimistically update the local anchor so subsequent reads reflect
|
|
134
|
+
// the requested seek without waiting for the next native sample.
|
|
135
|
+
this._currentTime = properties.currentTime
|
|
136
|
+
this._anchorTimestamp = Date.now()
|
|
137
|
+
}
|
|
138
|
+
if (properties.loading !== undefined) {
|
|
139
|
+
this._loading = properties.loading
|
|
140
|
+
}
|
|
114
141
|
return new UpdateSpatializedStatic3DElementProperties(
|
|
115
142
|
this,
|
|
116
143
|
properties,
|
|
@@ -148,6 +175,49 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
148
175
|
this.updateProperties({ playbackRate: value })
|
|
149
176
|
}
|
|
150
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Last playback position sampled from native (seconds).
|
|
180
|
+
*/
|
|
181
|
+
private _currentTime: number = 0
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Unix epoch time (ms) corresponding to `_currentTime`. Sourced from the
|
|
185
|
+
* native `timestamp` on samples, or `Date.now()` on local seeks/transitions.
|
|
186
|
+
*/
|
|
187
|
+
private _anchorTimestamp: number = 0
|
|
188
|
+
|
|
189
|
+
private clampTime(time: number): number {
|
|
190
|
+
if (!Number.isFinite(time) || time < 0) return 0
|
|
191
|
+
// When looping the current time is modulo the duration
|
|
192
|
+
if (time > this._duration) {
|
|
193
|
+
return this.loop && this.duration > 0
|
|
194
|
+
? time % this._duration
|
|
195
|
+
: this.duration
|
|
196
|
+
}
|
|
197
|
+
return time
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Returns the current (un-scaled) playback position in seconds. While
|
|
202
|
+
* playing, the value is extrapolated from the last anchor using
|
|
203
|
+
* `playbackRate` and clamped to `[0, duration]`; while paused it returns
|
|
204
|
+
* the anchor directly.
|
|
205
|
+
*/
|
|
206
|
+
get currentTime(): number {
|
|
207
|
+
if (this._paused) return this._currentTime
|
|
208
|
+
const elapsed = (Date.now() - this._anchorTimestamp) / 1000
|
|
209
|
+
return this.clampTime(this._currentTime + elapsed * this._playbackRate)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Seeks the animation to `value` seconds (clamped to `[0, duration]`) and
|
|
214
|
+
* forwards the request to native.
|
|
215
|
+
*/
|
|
216
|
+
set currentTime(value: number) {
|
|
217
|
+
const time = Number.isNaN(value) ? 0 : clamp(value, 0, this.duration)
|
|
218
|
+
this.updateProperties({ currentTime: time })
|
|
219
|
+
}
|
|
220
|
+
|
|
151
221
|
/**
|
|
152
222
|
* Whether the animation is currently paused.
|
|
153
223
|
*/
|
|
@@ -181,6 +251,10 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
181
251
|
* @returns Promise resolving when the command is sent
|
|
182
252
|
*/
|
|
183
253
|
async play(): Promise<void> {
|
|
254
|
+
if (this._paused) {
|
|
255
|
+
// Start extrapolating from the last known position on resume.
|
|
256
|
+
this._anchorTimestamp = Date.now()
|
|
257
|
+
}
|
|
184
258
|
this._paused = false
|
|
185
259
|
await this.updateProperties({ animationPaused: false })
|
|
186
260
|
}
|
|
@@ -190,6 +264,12 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
190
264
|
* @returns Promise resolving when the command is sent
|
|
191
265
|
*/
|
|
192
266
|
async pause(): Promise<void> {
|
|
267
|
+
if (!this._paused) {
|
|
268
|
+
// Freeze the extrapolated position so reads remain stable until the
|
|
269
|
+
// next native sample arrives.
|
|
270
|
+
this._currentTime = this.currentTime
|
|
271
|
+
this._anchorTimestamp = Date.now()
|
|
272
|
+
}
|
|
193
273
|
this._paused = true
|
|
194
274
|
await this.updateProperties({ animationPaused: true })
|
|
195
275
|
}
|
|
@@ -213,6 +293,9 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
213
293
|
} else if (data.type === SpatialWebMsgType.animationstatechange) {
|
|
214
294
|
this._paused = data.detail.paused
|
|
215
295
|
this._duration = data.detail.duration
|
|
296
|
+
// In unsupported environments currentTime is invalid
|
|
297
|
+
this._currentTime = data.detail.currentTime ?? 0
|
|
298
|
+
this._anchorTimestamp = data.detail.timestamp ?? Date.now()
|
|
216
299
|
this._onAnimationStateChangeCallback?.(data.detail)
|
|
217
300
|
} else {
|
|
218
301
|
// Handle other spatial events using the base class implementation
|
|
@@ -232,6 +315,16 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
232
315
|
return this._autoplay
|
|
233
316
|
}
|
|
234
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Asset fetch policy. `'lazy'` defers fetching until the host signals the
|
|
320
|
+
* element is in view; `'eager'` fetches immediately.
|
|
321
|
+
*/
|
|
322
|
+
private _loading: ModelLoadingMode = 'eager'
|
|
323
|
+
|
|
324
|
+
get loading(): ModelLoadingMode {
|
|
325
|
+
return this._loading
|
|
326
|
+
}
|
|
327
|
+
|
|
235
328
|
/**
|
|
236
329
|
* Whether the model animation should loop continuously.
|
|
237
330
|
*/
|
|
@@ -276,6 +369,11 @@ export class SpatializedStatic3DElement extends SpatializedElement {
|
|
|
276
369
|
}
|
|
277
370
|
}
|
|
278
371
|
|
|
372
|
+
// Equivalent of proposed Math.clamp
|
|
373
|
+
function clamp(num: number, min: number, max: number) {
|
|
374
|
+
return num <= min ? min : num >= max ? max : num
|
|
375
|
+
}
|
|
376
|
+
|
|
279
377
|
type Static3DReceiveEventData =
|
|
280
378
|
| ModelLoadSuccess
|
|
281
379
|
| ModelLoadFailure
|
package/src/WebMsgCommand.ts
CHANGED
|
@@ -81,6 +81,16 @@ export interface ModelLoadFailure {
|
|
|
81
81
|
export interface AnimationStateChangeDetail {
|
|
82
82
|
paused: boolean
|
|
83
83
|
duration: number
|
|
84
|
+
/**
|
|
85
|
+
* Sampled animation playback position in seconds at `timestamp`.
|
|
86
|
+
* Optional for compatibility with older native runtimes.
|
|
87
|
+
*/
|
|
88
|
+
currentTime?: number
|
|
89
|
+
/**
|
|
90
|
+
* Unix epoch time in milliseconds at which `currentTime` was sampled.
|
|
91
|
+
* Used to extrapolate `currentTime` between samples while playing.
|
|
92
|
+
*/
|
|
93
|
+
timestamp?: number
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
export interface AnimationStateChangeMsg {
|