@webspatial/core-sdk 1.5.0 → 1.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webspatial/core-sdk",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "this is the core js API for webspatial",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
package/src/JSBCommand.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  Vec3,
25
25
  AttachmentEntityOptions,
26
26
  AttachmentEntityUpdateOptions,
27
+ ModelSource,
27
28
  } from './types/types'
28
29
  import { SpatialSceneCreationOptionsInternal } from './types/internal'
29
30
  import { composeSRT } from './utils'
@@ -289,13 +290,17 @@ export class AddSpatializedElementToSpatialScene extends JSBCommand {
289
290
  export class CreateSpatializedStatic3DElementCommand extends JSBCommand {
290
291
  commandType = 'CreateSpatializedStatic3DElement'
291
292
 
292
- constructor(readonly modelURL: string) {
293
+ constructor(
294
+ readonly modelURL?: string,
295
+ readonly sources?: ModelSource[],
296
+ ) {
293
297
  super()
294
298
  this.modelURL = modelURL
299
+ this.sources = sources
295
300
  }
296
301
 
297
302
  protected getParams() {
298
- return { modelURL: this.modelURL }
303
+ return { modelURL: this.modelURL, sources: this.sources }
299
304
  }
300
305
  }
301
306
 
package/src/Spatial.ts CHANGED
@@ -29,7 +29,7 @@ export class Spatial {
29
29
  * @returns True if running in a spatial web environment, false otherwise
30
30
  */
31
31
  runInSpatialWeb() {
32
- if (navigator.userAgent.indexOf('WebSpatial/') > 0) {
32
+ if (navigator.userAgent.indexOf('WebSpatial/') >= 0) {
33
33
  return true
34
34
  }
35
35
  return false
@@ -22,6 +22,7 @@ import {
22
22
  SpatialUnlitMaterialOptions,
23
23
  SpatialEntityUserData,
24
24
  AttachmentEntityOptions,
25
+ ModelSource,
25
26
  } from './types/types'
26
27
  import { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
27
28
  import { SpatialEntity } from './reality/entity/SpatialEntity'
@@ -72,9 +73,10 @@ export class SpatialSession {
72
73
  * @returns Promise resolving to a new SpatializedStatic3DElement instance
73
74
  */
74
75
  createSpatializedStatic3DElement(
75
- modelURL: string,
76
+ modelURL?: string,
77
+ sources?: ModelSource[],
76
78
  ): Promise<SpatializedStatic3DElement> {
77
- return createSpatializedStatic3DElement(modelURL)
79
+ return createSpatializedStatic3DElement(modelURL, sources)
78
80
  }
79
81
 
80
82
  /**
@@ -111,16 +111,7 @@ export abstract class SpatializedElement extends SpatialObject {
111
111
  * Handles various spatial events like transforms, gestures, and interactions.
112
112
  * @param data The event data received from the WebSpatial system
113
113
  */
114
- protected onReceiveEvent(
115
- data:
116
- | SpatialTapMsg
117
- | SpatialDragStartMsg
118
- | SpatialDragMsg
119
- | SpatialDragEndMsg
120
- | SpatialRotateMsg
121
- | SpatialRotateEndMsg
122
- | ObjectDestroyMsg,
123
- ) {
114
+ protected onReceiveEvent(data: ReceiveEventData) {
124
115
  const { type } = data
125
116
  if (type === SpatialWebMsgType.objectdestroy) {
126
117
  this.isDestroyed = true
@@ -260,3 +251,12 @@ export abstract class SpatializedElement extends SpatialObject {
260
251
  SpatialWebEvent.removeEventReceiver(this.id)
261
252
  }
262
253
  }
254
+
255
+ export type ReceiveEventData =
256
+ | SpatialTapMsg
257
+ | SpatialDragStartMsg
258
+ | SpatialDragMsg
259
+ | SpatialDragEndMsg
260
+ | SpatialRotateMsg
261
+ | SpatialRotateEndMsg
262
+ | ObjectDestroyMsg
@@ -6,6 +6,7 @@ import {
6
6
  import { Spatialized2DElement } from './Spatialized2DElement'
7
7
  import { SpatializedStatic3DElement } from './SpatializedStatic3DElement'
8
8
  import { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
9
+ import { ModelSource } from './types/types'
9
10
 
10
11
  export async function createSpatialized2DElement(): Promise<Spatialized2DElement> {
11
12
  const result = await new createSpatialized2DElementCommand().execute()
@@ -21,16 +22,18 @@ export async function createSpatialized2DElement(): Promise<Spatialized2DElement
21
22
  }
22
23
 
23
24
  export async function createSpatializedStatic3DElement(
24
- modelURL: string,
25
+ modelURL?: string,
26
+ sources?: ModelSource[],
25
27
  ): Promise<SpatializedStatic3DElement> {
26
28
  const result = await new CreateSpatializedStatic3DElementCommand(
27
29
  modelURL,
30
+ sources,
28
31
  ).execute()
29
32
  if (!result.success) {
30
33
  throw new Error('createSpatializedStatic3DElement failed')
31
34
  } else {
32
35
  const { id } = result.data
33
- return new SpatializedStatic3DElement(id, modelURL)
36
+ return new SpatializedStatic3DElement(id, modelURL, sources)
34
37
  }
35
38
  }
36
39
 
@@ -32,10 +32,24 @@ describe('SpatializedStatic3DElement', () => {
32
32
  const el = new SpatializedStatic3DElement('s2', 'model.glb')
33
33
  const p = el.ready
34
34
 
35
- el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded })
35
+ el.onReceiveEvent({
36
+ type: SpatialWebMsgType.modelloaded,
37
+ detail: { src: 'https://example.com/model.glb' },
38
+ })
36
39
  await expect(p).resolves.toBe(true)
37
40
  })
38
41
 
42
+ it('sets currentSrc from modelloaded detail data', () => {
43
+ const el = new SpatializedStatic3DElement('s2b', 'model.glb')
44
+
45
+ el.onReceiveEvent({
46
+ type: SpatialWebMsgType.modelloaded,
47
+ detail: { src: 'https://cdn.example.com/fallback.usdz' },
48
+ })
49
+
50
+ expect(el.currentSrc).toBe('https://cdn.example.com/fallback.usdz')
51
+ })
52
+
39
53
  it('ready resolves to false on modelloadfailed event', async () => {
40
54
  const el = new SpatializedStatic3DElement('s3', 'model.glb')
41
55
  const p = el.ready
@@ -1,7 +1,16 @@
1
1
  import { UpdateSpatializedStatic3DElementProperties } from './JSBCommand'
2
- import { SpatializedElement } from './SpatializedElement'
3
- import { SpatializedStatic3DElementProperties } from './types/types'
4
- import { SpatialWebMsgType } from './WebMsgCommand'
2
+ import { ReceiveEventData, SpatializedElement } from './SpatializedElement'
3
+ import {
4
+ ModelSource,
5
+ SpatializedStatic3DElementProperties,
6
+ } from './types/types'
7
+ import {
8
+ ModelLoadSuccess,
9
+ ModelLoadFailure,
10
+ SpatialWebMsgType,
11
+ AnimationStateChangeDetail,
12
+ AnimationStateChangeMsg,
13
+ } from './WebMsgCommand'
5
14
 
6
15
  /**
7
16
  * Represents a static 3D model element in the spatial environment.
@@ -14,10 +23,12 @@ export class SpatializedStatic3DElement extends SpatializedElement {
14
23
  * Registers the element to receive spatial events.
15
24
  * @param id Unique identifier for this element
16
25
  * @param modelURL URL of the 3D model
26
+ * @param sources Optional fallback model sources
17
27
  */
18
- constructor(id: string, modelURL: string) {
28
+ constructor(id: string, modelURL?: string, sources?: ModelSource[]) {
19
29
  super(id)
20
30
  this.modelURL = modelURL
31
+ this.sources = sources
21
32
  }
22
33
 
23
34
  /**
@@ -30,7 +41,21 @@ export class SpatializedStatic3DElement extends SpatializedElement {
30
41
  * Caches the last model URL to detect changes.
31
42
  * Used to reset the ready promise when the model URL changes.
32
43
  */
33
- private modelURL: string
44
+ private modelURL?: string
45
+
46
+ /**
47
+ * Caches the last sources array to detect changes.
48
+ */
49
+ private sources?: ModelSource[]
50
+
51
+ /**
52
+ * The model URL that was successfully loaded by the native runtime.
53
+ */
54
+ private _currentSrc: string = ''
55
+
56
+ get currentSrc(): string {
57
+ return this._currentSrc
58
+ }
34
59
 
35
60
  /**
36
61
  * Creates a new promise for tracking the ready state of the model.
@@ -59,25 +84,125 @@ export class SpatializedStatic3DElement extends SpatializedElement {
59
84
  async updateProperties(
60
85
  properties: Partial<SpatializedStatic3DElementProperties>,
61
86
  ) {
87
+ let needsReadyReset = false
62
88
  if (properties.modelURL !== undefined) {
63
89
  if (this.modelURL !== properties.modelURL) {
64
90
  this.modelURL = properties.modelURL
65
- this.ready = this.createReadyPromise()
91
+ needsReadyReset = true
92
+ }
93
+ }
94
+ if (properties.sources !== undefined) {
95
+ const prevJson = JSON.stringify(this.sources)
96
+ const nextJson = JSON.stringify(properties.sources)
97
+ if (prevJson !== nextJson) {
98
+ this.sources = properties.sources
99
+ needsReadyReset = true
66
100
  }
67
101
  }
102
+ if (needsReadyReset) {
103
+ this.ready = this.createReadyPromise()
104
+ }
105
+ if (properties.autoplay !== undefined) {
106
+ this._autoplay = properties.autoplay
107
+ }
108
+ if (properties.loop !== undefined) {
109
+ this._loop = properties.loop
110
+ }
111
+ if (properties.playbackRate !== undefined) {
112
+ this._playbackRate = properties.playbackRate
113
+ }
68
114
  return new UpdateSpatializedStatic3DElementProperties(
69
115
  this,
70
116
  properties,
71
117
  ).execute()
72
118
  }
73
119
 
120
+ /**
121
+ * Total animation duration in seconds, synced from native.
122
+ */
123
+ private _duration: number = 0
124
+
125
+ /**
126
+ * Returns the total animation duration in seconds.
127
+ */
128
+ get duration(): number {
129
+ return this._duration
130
+ }
131
+
132
+ /**
133
+ * Playback speed multiplier.
134
+ */
135
+ private _playbackRate: number = 1
136
+
137
+ /**
138
+ * Returns the current playback rate.
139
+ */
140
+ get playbackRate(): number {
141
+ return this._playbackRate
142
+ }
143
+
144
+ /**
145
+ * Sets the playback rate and sends it to native.
146
+ */
147
+ set playbackRate(value: number) {
148
+ this.updateProperties({ playbackRate: value })
149
+ }
150
+
151
+ /**
152
+ * Whether the animation is currently paused.
153
+ */
154
+ private _paused: boolean = true
155
+
156
+ /**
157
+ * Returns whether the animation is currently paused.
158
+ */
159
+ get paused(): boolean {
160
+ return this._paused
161
+ }
162
+
163
+ /**
164
+ * Callback for animation state changes.
165
+ */
166
+ private _onAnimationStateChangeCallback?: (
167
+ detail: AnimationStateChangeDetail,
168
+ ) => void
169
+
170
+ /**
171
+ * Sets the callback for animation state changes.
172
+ */
173
+ set onAnimationStateChangeCallback(
174
+ callback: undefined | ((detail: AnimationStateChangeDetail) => void),
175
+ ) {
176
+ this._onAnimationStateChangeCallback = callback
177
+ }
178
+
179
+ /**
180
+ * Starts or resumes animation playback.
181
+ * @returns Promise resolving when the command is sent
182
+ */
183
+ async play(): Promise<void> {
184
+ this._paused = false
185
+ await this.updateProperties({ animationPaused: false })
186
+ }
187
+
188
+ /**
189
+ * Pauses animation playback.
190
+ * @returns Promise resolving when the command is sent
191
+ */
192
+ async pause(): Promise<void> {
193
+ this._paused = true
194
+ await this.updateProperties({ animationPaused: true })
195
+ }
196
+
74
197
  /**
75
198
  * Processes events received from the WebSpatial environment.
76
199
  * Handles model loading events in addition to base spatial events.
77
200
  * @param data The event data received from the WebSpatial system
78
201
  */
79
- override onReceiveEvent(data: { type: SpatialWebMsgType }) {
202
+ override onReceiveEvent(data: Static3DReceiveEventData) {
80
203
  if (data.type === SpatialWebMsgType.modelloaded) {
204
+ // On old runtimes (<⍺2.1) detail is not returned so fallback to modelURL
205
+ this._currentSrc = data.detail?.src ?? this.modelURL ?? ''
81
206
  // Handle successful model loading
82
207
  this._onLoadCallback?.()
83
208
  this._readyResolve?.(true)
@@ -85,12 +210,40 @@ export class SpatializedStatic3DElement extends SpatializedElement {
85
210
  // Handle model loading failure
86
211
  this._onLoadFailureCallback?.()
87
212
  this._readyResolve?.(false)
213
+ } else if (data.type === SpatialWebMsgType.animationstatechange) {
214
+ this._paused = data.detail.paused
215
+ this._duration = data.detail.duration
216
+ this._onAnimationStateChangeCallback?.(data.detail)
88
217
  } else {
89
218
  // Handle other spatial events using the base class implementation
90
- super.onReceiveEvent(data as any)
219
+ super.onReceiveEvent(data)
91
220
  }
92
221
  }
93
222
 
223
+ /**
224
+ * Whether the model should automatically play its first animation on load.
225
+ */
226
+ private _autoplay: boolean = false
227
+
228
+ /**
229
+ * Returns whether autoplay is enabled for this element.
230
+ */
231
+ get autoplay(): boolean {
232
+ return this._autoplay
233
+ }
234
+
235
+ /**
236
+ * Whether the model animation should loop continuously.
237
+ */
238
+ private _loop: boolean = false
239
+
240
+ /**
241
+ * Returns whether loop is enabled for this element.
242
+ */
243
+ get loop(): boolean {
244
+ return this._loop
245
+ }
246
+
94
247
  /**
95
248
  * Callback function for successful model loading.
96
249
  */
@@ -122,3 +275,9 @@ export class SpatializedStatic3DElement extends SpatializedElement {
122
275
  this.updateProperties({ modelTransform })
123
276
  }
124
277
  }
278
+
279
+ type Static3DReceiveEventData =
280
+ | ModelLoadSuccess
281
+ | ModelLoadFailure
282
+ | ReceiveEventData
283
+ | AnimationStateChangeMsg
@@ -19,6 +19,8 @@ export enum SpatialWebMsgType {
19
19
  spatialmagnify = 'spatialmagnify',
20
20
  spatialmagnifyend = 'spatialmagnifyend',
21
21
 
22
+ animationstatechange = 'animationstatechange',
23
+
22
24
  objectdestroy = 'objectdestroy',
23
25
  }
24
26
 
@@ -65,3 +67,23 @@ export interface SpatialMagnifyEndMsg {
65
67
  type: SpatialWebMsgType.spatialmagnifyend
66
68
  detail: SpatialMagnifyEventDetail
67
69
  }
70
+
71
+ export interface ModelLoadSuccess {
72
+ type: SpatialWebMsgType.modelloaded
73
+ // detail object is undefined in old native runtimes
74
+ detail?: { src: string }
75
+ }
76
+
77
+ export interface ModelLoadFailure {
78
+ type: SpatialWebMsgType.modelloadfailed
79
+ }
80
+
81
+ export interface AnimationStateChangeDetail {
82
+ paused: boolean
83
+ duration: number
84
+ }
85
+
86
+ export interface AnimationStateChangeMsg {
87
+ type: SpatialWebMsgType.animationstatechange
88
+ detail: AnimationStateChangeDetail
89
+ }