@webspatial/core-sdk 1.4.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.4.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
@@ -2,6 +2,7 @@ import { createPlatform } from './platform-adapter'
2
2
  import { WebSpatialProtocolResult } from './platform-adapter/interface'
3
3
  import { SpatialComponent } from './reality/component/SpatialComponent'
4
4
  import { SpatialEntity } from './reality/entity/SpatialEntity'
5
+ import { SpatialMaterial } from './reality/material/SpatialMaterial'
5
6
  import { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
6
7
  import { SpatializedElement } from './SpatializedElement'
7
8
  import { SpatialObject } from './SpatialObject'
@@ -23,6 +24,7 @@ import {
23
24
  Vec3,
24
25
  AttachmentEntityOptions,
25
26
  AttachmentEntityUpdateOptions,
27
+ ModelSource,
26
28
  } from './types/types'
27
29
  import { SpatialSceneCreationOptionsInternal } from './types/internal'
28
30
  import { composeSRT } from './utils'
@@ -288,13 +290,17 @@ export class AddSpatializedElementToSpatialScene extends JSBCommand {
288
290
  export class CreateSpatializedStatic3DElementCommand extends JSBCommand {
289
291
  commandType = 'CreateSpatializedStatic3DElement'
290
292
 
291
- constructor(readonly modelURL: string) {
293
+ constructor(
294
+ readonly modelURL?: string,
295
+ readonly sources?: ModelSource[],
296
+ ) {
292
297
  super()
293
298
  this.modelURL = modelURL
299
+ this.sources = sources
294
300
  }
295
301
 
296
302
  protected getParams() {
297
- return { modelURL: this.modelURL }
303
+ return { modelURL: this.modelURL, sources: this.sources }
298
304
  }
299
305
  }
300
306
 
@@ -386,6 +392,38 @@ export class AddComponentToEntityCommand extends JSBCommand {
386
392
  commandType = 'AddComponentToEntity'
387
393
  }
388
394
 
395
+ export class RemoveComponentFromEntityCommand extends JSBCommand {
396
+ constructor(
397
+ public entity: SpatialEntity,
398
+ public comp: SpatialComponent,
399
+ ) {
400
+ super()
401
+ }
402
+ protected getParams(): Record<string, any> | undefined {
403
+ return {
404
+ entityId: this.entity.id,
405
+ componentId: this.comp.id,
406
+ }
407
+ }
408
+ commandType = 'RemoveComponentFromEntity'
409
+ }
410
+
411
+ export class SetMaterialsOnEntityCommand extends JSBCommand {
412
+ constructor(
413
+ public entityId: string,
414
+ public materials: SpatialMaterial[],
415
+ ) {
416
+ super()
417
+ }
418
+ protected getParams(): Record<string, any> | undefined {
419
+ return {
420
+ entityId: this.entityId,
421
+ materialIds: this.materials.map(m => m.id),
422
+ }
423
+ }
424
+ commandType = 'SetMaterialsOnEntity'
425
+ }
426
+
389
427
  export class AddEntityToDynamic3DCommand extends JSBCommand {
390
428
  constructor(
391
429
  public d3dEle: SpatializedDynamic3DElement,
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
+ }
@@ -40,8 +40,8 @@ export function createPlatform(): PlatformAbility {
40
40
  userAgent.includes('PicoWebApp') &&
41
41
  isVersionGreater(webSpatialVersion, [0, 0, 1])
42
42
  ) {
43
- const XRPlatform = require('./xr/XRPlatform').XRPlatform
44
- return new XRPlatform()
43
+ const PicoOSPlatform = require('./pico-os/PicoOSPlatform').PicoOSPlatform
44
+ return new PicoOSPlatform()
45
45
  } else if (userAgent.includes('Android') || userAgent.includes('Linux')) {
46
46
  const AndroidPlatform = require('./android/AndroidPlatform').AndroidPlatform
47
47
  return new AndroidPlatform()
@@ -23,9 +23,10 @@ function nextRequestId() {
23
23
  return `rId_${requestId}`
24
24
  }
25
25
 
26
- export class XRPlatform implements PlatformAbility {
26
+ // Only supports Pico OS 6
27
+ export class PicoOSPlatform implements PlatformAbility {
27
28
  async callJSB(cmd: string, msg: string): Promise<CommandResult> {
28
- // android JS Bridge interface only support sync invoking
29
+ // swan JS Bridge interface only support sync invoking
29
30
  // in order to implement promise API, register every request by requestId and remove when resolve/reject.
30
31
  return new Promise((resolve, reject) => {
31
32
  try {
@@ -54,7 +55,7 @@ export class XRPlatform implements PlatformAbility {
54
55
  }
55
56
  }
56
57
  } catch (error: unknown) {
57
- console.error(`XRPlatform cmd: ${cmd}, msg: ${msg} error: ${error}`)
58
+ console.error(`SwanPlatform cmd: ${cmd}, msg: ${msg} error: ${error}`)
58
59
  const { code, message } = error as JSBError
59
60
  resolve(CommandResultFailure(code, message))
60
61
  }
@@ -75,7 +76,6 @@ export class XRPlatform implements PlatformAbility {
75
76
  SpatialWebEvent.addEventReceiver(
76
77
  createdId,
77
78
  (result: { spatialId: string }) => {
78
- console.log('createdId', createdId, result.spatialId)
79
79
  resolve(
80
80
  CommandResultSuccess({
81
81
  windowProxy: windowProxy,
@@ -87,13 +87,11 @@ export class XRPlatform implements PlatformAbility {
87
87
  )
88
88
  windowProxy = this.openWindow(
89
89
  command,
90
- query,
90
+ 'rid=' + createdId,
91
91
  target,
92
92
  features,
93
93
  ).windowProxy
94
- windowProxy?.open(`about:blank?rid=${createdId}`, '_self')
95
94
  } catch (error: unknown) {
96
- console.error(`open window error: ${error}`)
97
95
  const { code, message } = error as JSBError
98
96
  SpatialWebEvent.removeEventReceiver(createdId)
99
97
  resolve(CommandResultFailure(code, message))
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  AddComponentToEntityCommand,
15
15
  AddEntityToEntityCommand,
16
+ RemoveComponentFromEntityCommand,
16
17
  RemoveEntityFromParentCommand,
17
18
  UpdateEntityEventCommand,
18
19
  UpdateEntityPropertiesCommand,
@@ -79,6 +80,9 @@ export class SpatialEntity extends SpatialObject {
79
80
  async addComponent(component: SpatialComponent) {
80
81
  return new AddComponentToEntityCommand(this, component).execute()
81
82
  }
83
+ async removeComponent(component: SpatialComponent) {
84
+ return new RemoveComponentFromEntityCommand(this, component).execute()
85
+ }
82
86
  async setPosition(position: Vec3) {
83
87
  return this.updateTransform({ position })
84
88
  }
@@ -2,6 +2,8 @@ import {
2
2
  SpatialEntityUserData,
3
3
  SpatialModelEntityCreationOptions,
4
4
  } from '../../types/types'
5
+ import { SetMaterialsOnEntityCommand } from '../../JSBCommand'
6
+ import { SpatialMaterial } from '../material/SpatialMaterial'
5
7
  import { SpatialEntity } from './SpatialEntity'
6
8
 
7
9
  export class SpatialModelEntity extends SpatialEntity {
@@ -12,4 +14,8 @@ export class SpatialModelEntity extends SpatialEntity {
12
14
  ) {
13
15
  super(id, userData)
14
16
  }
17
+
18
+ async setMaterials(materials: SpatialMaterial[]) {
19
+ return new SetMaterialsOnEntityCommand(this.id, materials).execute()
20
+ }
15
21
  }