@webspatial/core-sdk 1.2.1 → 1.4.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.2.1",
3
+ "version": "1.4.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
@@ -21,6 +21,8 @@ import {
21
21
  SpatialModelEntityCreationOptions,
22
22
  SpatialEntityEventType,
23
23
  Vec3,
24
+ AttachmentEntityOptions,
25
+ AttachmentEntityUpdateOptions,
24
26
  } from './types/types'
25
27
  import { SpatialSceneCreationOptionsInternal } from './types/internal'
26
28
  import { composeSRT } from './utils'
@@ -500,6 +502,24 @@ export class ConvertFromSceneToEntityCommand extends JSBCommand {
500
502
  commandType = 'ConvertFromSceneToEntity'
501
503
  }
502
504
 
505
+ export class ConvertCoordinateCommand extends JSBCommand {
506
+ constructor(
507
+ public position: Vec3,
508
+ public fromId: string,
509
+ public toId: string,
510
+ ) {
511
+ super()
512
+ }
513
+ protected getParams(): Record<string, any> | undefined {
514
+ return {
515
+ position: this.position,
516
+ fromId: this.fromId,
517
+ toId: this.toId,
518
+ }
519
+ }
520
+ commandType = 'ConvertCoordinate'
521
+ }
522
+
503
523
  export class CreateTextureResourceCommand extends JSBCommand {
504
524
  constructor(private url: string) {
505
525
  super()
@@ -620,6 +640,51 @@ export class createSpatialSceneCommand extends WebSpatialProtocolCommand {
620
640
  }
621
641
  }
622
642
 
643
+ export class CreateAttachmentEntityCommand extends WebSpatialProtocolCommand {
644
+ commandType = 'createAttachment'
645
+ constructor(private options: AttachmentEntityOptions) {
646
+ super()
647
+ }
648
+ protected getParams() {
649
+ return {} // No metadata — just trigger engine/webview creation
650
+ }
651
+ }
652
+
653
+ export class InitializeAttachmentCommand extends JSBCommand {
654
+ commandType = 'InitializeAttachment'
655
+ constructor(
656
+ private attachmentId: string,
657
+ private options: AttachmentEntityOptions,
658
+ ) {
659
+ super()
660
+ }
661
+ protected getParams() {
662
+ return {
663
+ id: this.attachmentId,
664
+ parentEntityId: this.options.parentEntityId,
665
+ position: this.options.position ?? [0, 0, 0],
666
+ size: this.options.size,
667
+ ownerViewId: this.options.ownerViewId,
668
+ }
669
+ }
670
+ }
671
+
672
+ export class UpdateAttachmentEntityCommand extends JSBCommand {
673
+ commandType = 'UpdateAttachmentEntity'
674
+ constructor(
675
+ private attachmentId: string,
676
+ private options: AttachmentEntityUpdateOptions,
677
+ ) {
678
+ super()
679
+ }
680
+ protected getParams() {
681
+ return {
682
+ id: this.attachmentId,
683
+ ...this.options,
684
+ }
685
+ }
686
+ }
687
+
623
688
  // TODO: Can crypto.randomUUID be used instead including in dev environments without https
624
689
  function uuid(): string {
625
690
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
package/src/Spatial.ts CHANGED
@@ -6,6 +6,8 @@ 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
+
9
11
  /**
10
12
  * Requests a spatial session object from the browser.
11
13
  * This is the primary method to initialize spatial functionality.
@@ -33,6 +35,25 @@ export class Spatial {
33
35
  return false
34
36
  }
35
37
 
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
+
36
57
  /** @deprecated
37
58
  * Checks if WebSpatial is supported in the current environment.
38
59
  * Verifies compatibility between native and client versions.
@@ -51,7 +72,7 @@ export class Spatial {
51
72
  if (window.__WebSpatialData && window.__WebSpatialData.getNativeVersion) {
52
73
  return window.__WebSpatialData.getNativeVersion()
53
74
  }
54
- return window.WebSpatailNativeVersion === 'PACKAGE_VERSION'
75
+ return window.WebSpatailNativeVersion === 'WS_SHELL_VERSION'
55
76
  ? this.getClientVersion()
56
77
  : window.WebSpatailNativeVersion
57
78
  }
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  SpatialSceneCreationOptions,
3
3
  SpatialSceneProperties,
4
+ Vec3,
4
5
  } from './types/types'
5
6
  import { SpatialSceneCreationOptionsInternal } from './types/internal'
6
7
  import {
@@ -9,6 +10,7 @@ import {
9
10
  UpdateSceneConfig,
10
11
  UpdateSpatialSceneProperties,
11
12
  } from './JSBCommand'
13
+ import { ConvertCoordinateCommand } from './JSBCommand'
12
14
 
13
15
  import { SpatializedElement } from './SpatializedElement'
14
16
  import { SpatialObject } from './SpatialObject'
@@ -34,6 +36,24 @@ export class SpatialScene extends SpatialObject {
34
36
  return instance
35
37
  }
36
38
 
39
+ async convertCoordinate(
40
+ position: Vec3,
41
+ fromId: string,
42
+ toId: string,
43
+ ): Promise<Vec3> {
44
+ try {
45
+ const ret = await new ConvertCoordinateCommand(
46
+ position,
47
+ fromId,
48
+ toId,
49
+ ).execute()
50
+ return (ret as any)?.data ?? position
51
+ } catch (error) {
52
+ console.warn('SpatialScene.convertCoordinate error:', error)
53
+ throw error
54
+ }
55
+ }
56
+
37
57
  /**
38
58
  * Updates the properties of the spatial scene.
39
59
  * This can include background settings, lighting, and other scene-wide properties.
@@ -6,6 +6,7 @@ import {
6
6
  createSpatializedDynamic3DElement,
7
7
  } from './SpatializedElementCreator'
8
8
  import { createSpatializedStatic3DElement } from './SpatializedElementCreator'
9
+ import { Attachment, createAttachmentEntity } from './reality/Attachment'
9
10
  import { SpatializedStatic3DElement } from './SpatializedStatic3DElement'
10
11
  import {
11
12
  ModelComponentOptions,
@@ -20,6 +21,7 @@ import {
20
21
  SpatialSphereGeometryOptions,
21
22
  SpatialUnlitMaterialOptions,
22
23
  SpatialEntityUserData,
24
+ AttachmentEntityOptions,
23
25
  } from './types/types'
24
26
  import { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
25
27
  import { SpatialEntity } from './reality/entity/SpatialEntity'
@@ -70,7 +72,7 @@ export class SpatialSession {
70
72
  * @returns Promise resolving to a new SpatializedStatic3DElement instance
71
73
  */
72
74
  createSpatializedStatic3DElement(
73
- modelURL: string = '',
75
+ modelURL: string,
74
76
  ): Promise<SpatializedStatic3DElement> {
75
77
  return createSpatializedStatic3DElement(modelURL)
76
78
  }
@@ -187,4 +189,16 @@ export class SpatialSession {
187
189
  ) {
188
190
  return createSpatialModelEntity(options, userData)
189
191
  }
192
+
193
+ /**
194
+ * Creates an attachment entity that renders 2D HTML content as a child
195
+ * of a 3D entity in the scene graph.
196
+ * @param options Configuration options including parent entity ID, position, and size
197
+ * @returns Promise resolving to a new Attachment instance
198
+ */
199
+ createAttachmentEntity(
200
+ options: AttachmentEntityOptions,
201
+ ): Promise<Attachment> {
202
+ return createAttachmentEntity(options)
203
+ }
190
204
  }
@@ -6,11 +6,15 @@ import {
6
6
  import { SpatialEntity } from './reality'
7
7
  import { SpatializedElement } from './SpatializedElement'
8
8
  import {
9
+ SpatialEntityEventType,
9
10
  SpatialEntityOrReality,
10
11
  SpatializedElementProperties,
11
12
  } from './types/types'
13
+
12
14
  export class SpatializedDynamic3DElement extends SpatializedElement {
13
15
  children: SpatialEntityOrReality[] = []
16
+ events: Record<string, (data: any) => void> = {}
17
+
14
18
  constructor(id: string) {
15
19
  super(id)
16
20
  }
@@ -21,6 +25,21 @@ export class SpatializedDynamic3DElement extends SpatializedElement {
21
25
  entity.parent = this
22
26
  return ans
23
27
  }
28
+
29
+ addEvent(type: SpatialEntityEventType, callback: (data: any) => void) {
30
+ this.events[type] = callback
31
+ }
32
+
33
+ removeEvent(eventName: SpatialEntityEventType) {
34
+ if (this.events[eventName]) {
35
+ delete this.events[eventName]
36
+ }
37
+ }
38
+
39
+ dispatchEvent(evt: CustomEvent) {
40
+ this.events[evt.type]?.(evt)
41
+ }
42
+
24
43
  async updateProperties(properties: Partial<SpatializedElementProperties>) {
25
44
  return new UpdateSpatializedDynamic3DElementProperties(
26
45
  this,
@@ -16,7 +16,6 @@ import {
16
16
  SpatialTapEvent,
17
17
  } from './types/types'
18
18
  import {
19
- CubeInfoMsg,
20
19
  ObjectDestroyMsg,
21
20
  SpatialDragEndMsg,
22
21
  SpatialDragMsg,
@@ -27,7 +26,6 @@ import {
27
26
  SpatialRotateMsg,
28
27
  SpatialTapMsg,
29
28
  SpatialWebMsgType,
30
- TransformMsg,
31
29
  } from './WebMsgCommand'
32
30
 
33
31
  /**
@@ -115,8 +113,6 @@ export abstract class SpatializedElement extends SpatialObject {
115
113
  */
116
114
  protected onReceiveEvent(
117
115
  data:
118
- | CubeInfoMsg
119
- | TransformMsg
120
116
  | SpatialTapMsg
121
117
  | SpatialDragStartMsg
122
118
  | SpatialDragMsg
@@ -128,31 +124,6 @@ export abstract class SpatializedElement extends SpatialObject {
128
124
  const { type } = data
129
125
  if (type === SpatialWebMsgType.objectdestroy) {
130
126
  this.isDestroyed = true
131
- } else if (type === SpatialWebMsgType.cubeInfo) {
132
- // Handle cube info updates (bounding box information)
133
- const cubeInfoMsg = data as CubeInfoMsg
134
- this._cubeInfo = new CubeInfo(cubeInfoMsg.size, cubeInfoMsg.origin)
135
- } else if (type === SpatialWebMsgType.transform) {
136
- // Handle transformation matrix updates
137
- this._transform = new DOMMatrix([
138
- data.detail.column0[0],
139
- data.detail.column0[1],
140
- data.detail.column0[2],
141
- 0,
142
- data.detail.column1[0],
143
- data.detail.column1[1],
144
- data.detail.column1[2],
145
- 0,
146
- data.detail.column2[0],
147
- data.detail.column2[1],
148
- data.detail.column2[2],
149
- 0,
150
- data.detail.column3[0],
151
- data.detail.column3[1],
152
- data.detail.column3[2],
153
- 1,
154
- ])
155
- this._transformInv = this._transform.inverse()
156
127
  } else if (type === SpatialWebMsgType.spatialtap) {
157
128
  // Handle tap gestures
158
129
  const event = createSpatialEvent(
@@ -30,7 +30,7 @@ export async function createSpatializedStatic3DElement(
30
30
  throw new Error('createSpatializedStatic3DElement failed')
31
31
  } else {
32
32
  const { id } = result.data
33
- return new SpatializedStatic3DElement(id)
33
+ return new SpatializedStatic3DElement(id, modelURL)
34
34
  }
35
35
  }
36
36
 
@@ -0,0 +1,125 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { SpatialWebMsgType } from './WebMsgCommand'
3
+ import { SpatializedStatic3DElement } from './SpatializedStatic3DElement'
4
+
5
+ // Single mock for the native bridge layer — everything else runs as-is
6
+ vi.mock('./JSBCommand', () => {
7
+ class OkCommand {
8
+ execute() {
9
+ return Promise.resolve({
10
+ success: true,
11
+ data: undefined,
12
+ errorCode: '',
13
+ errorMessage: '',
14
+ })
15
+ }
16
+ }
17
+
18
+ return { UpdateSpatializedStatic3DElementProperties: OkCommand }
19
+ })
20
+
21
+ describe('SpatializedStatic3DElement', () => {
22
+ afterEach(() => {
23
+ vi.clearAllMocks()
24
+ })
25
+
26
+ it('ready starts as a pending promise', () => {
27
+ const el = new SpatializedStatic3DElement('s1', 'model.glb')
28
+ expect(el.ready).toBeInstanceOf(Promise)
29
+ })
30
+
31
+ it('ready resolves to true on modelloaded event', async () => {
32
+ const el = new SpatializedStatic3DElement('s2', 'model.glb')
33
+ const p = el.ready
34
+
35
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded })
36
+ await expect(p).resolves.toBe(true)
37
+ })
38
+
39
+ it('ready resolves to false on modelloadfailed event', async () => {
40
+ const el = new SpatializedStatic3DElement('s3', 'model.glb')
41
+ const p = el.ready
42
+
43
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloadfailed })
44
+ await expect(p).resolves.toBe(false)
45
+ })
46
+
47
+ it('fires onLoadCallback on modelloaded', () => {
48
+ const el = new SpatializedStatic3DElement('s4', 'model.glb')
49
+ const cb = vi.fn()
50
+ el.onLoadCallback = cb
51
+
52
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded })
53
+ expect(cb).toHaveBeenCalledTimes(1)
54
+ })
55
+
56
+ it('fires onLoadFailureCallback on modelloadfailed', () => {
57
+ const el = new SpatializedStatic3DElement('s5', 'model.glb')
58
+ const cb = vi.fn()
59
+ el.onLoadFailureCallback = cb
60
+
61
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloadfailed })
62
+ expect(cb).toHaveBeenCalledTimes(1)
63
+ })
64
+
65
+ it('does not fire callbacks when they are not set', () => {
66
+ const el = new SpatializedStatic3DElement('s6', 'model.glb')
67
+ // Should not throw when no callbacks are registered
68
+ expect(() =>
69
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded }),
70
+ ).not.toThrow()
71
+ expect(() =>
72
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloadfailed }),
73
+ ).not.toThrow()
74
+ })
75
+
76
+ it('resets ready when modelURL changes', async () => {
77
+ const el = new SpatializedStatic3DElement('s7', 'a.glb')
78
+ const first = el.ready
79
+
80
+ await el.updateProperties({ modelURL: 'b.glb' })
81
+ expect(el.ready).not.toBe(first)
82
+ })
83
+
84
+ it('does not reset ready when modelURL stays the same', async () => {
85
+ const el = new SpatializedStatic3DElement('s8', 'a.glb')
86
+ const first = el.ready
87
+
88
+ await el.updateProperties({ modelURL: 'a.glb' })
89
+ expect(el.ready).toBe(first)
90
+ })
91
+
92
+ it('cancels old ready promise with false when modelURL changes', async () => {
93
+ const el = new SpatializedStatic3DElement('s9', 'a.glb')
94
+ const first = el.ready
95
+
96
+ await el.updateProperties({ modelURL: 'b.glb' })
97
+ await expect(first).resolves.toBe(false)
98
+ })
99
+
100
+ it('new ready promise works after URL change', async () => {
101
+ const el = new SpatializedStatic3DElement('s10', 'a.glb')
102
+
103
+ await el.updateProperties({ modelURL: 'b.glb' })
104
+ const second = el.ready
105
+
106
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded })
107
+ await expect(second).resolves.toBe(true)
108
+ })
109
+
110
+ it('handles multiple URL changes in sequence', async () => {
111
+ const el = new SpatializedStatic3DElement('s11', 'a.glb')
112
+ const p1 = el.ready
113
+
114
+ await el.updateProperties({ modelURL: 'b.glb' })
115
+ await expect(p1).resolves.toBe(false)
116
+
117
+ const p2 = el.ready
118
+ await el.updateProperties({ modelURL: 'c.glb' })
119
+ await expect(p2).resolves.toBe(false)
120
+
121
+ const p3 = el.ready
122
+ el.onReceiveEvent({ type: SpatialWebMsgType.modelloaded })
123
+ await expect(p3).resolves.toBe(true)
124
+ })
125
+ })
@@ -9,6 +9,17 @@ import { SpatialWebMsgType } from './WebMsgCommand'
9
9
  * and provides events for load success and failure.
10
10
  */
11
11
  export class SpatializedStatic3DElement extends SpatializedElement {
12
+ /**
13
+ * Creates a new spatialized static 3D element with the specified ID and URL.
14
+ * Registers the element to receive spatial events.
15
+ * @param id Unique identifier for this element
16
+ * @param modelURL URL of the 3D model
17
+ */
18
+ constructor(id: string, modelURL: string) {
19
+ super(id)
20
+ this.modelURL = modelURL
21
+ }
22
+
12
23
  /**
13
24
  * Promise resolver for the ready state.
14
25
  * Used to resolve the ready promise when the model is loaded.
@@ -19,13 +30,15 @@ export class SpatializedStatic3DElement extends SpatializedElement {
19
30
  * Caches the last model URL to detect changes.
20
31
  * Used to reset the ready promise when the model URL changes.
21
32
  */
22
- private modelURL: string = ''
33
+ private modelURL: string
23
34
 
24
35
  /**
25
36
  * Creates a new promise for tracking the ready state of the model.
26
37
  * @returns Promise that resolves when the model is loaded (true) or fails to load (false)
27
38
  */
28
39
  private createReadyPromise() {
40
+ // If there's an existing promise reject it before it's replaced
41
+ this._readyResolve?.(false)
29
42
  return new Promise<boolean>(resolve => {
30
43
  this._readyResolve = resolve
31
44
  })
@@ -1,6 +1,4 @@
1
1
  import {
2
- Vec3,
3
- Size3D,
4
2
  SpatialDragEventDetail,
5
3
  SpatialTapEventDetail,
6
4
  SpatialRotateEventDetail,
@@ -10,8 +8,6 @@ import {
10
8
  } from './types/types'
11
9
 
12
10
  export enum SpatialWebMsgType {
13
- cubeInfo = 'cubeInfo',
14
- transform = 'transform',
15
11
  modelloaded = 'modelloaded',
16
12
  modelloadfailed = 'modelloadfailed',
17
13
  spatialtap = 'spatialtap',
@@ -30,28 +26,6 @@ export interface ObjectDestroyMsg {
30
26
  type: SpatialWebMsgType.objectdestroy
31
27
  }
32
28
 
33
- export interface CubeInfoMsg {
34
- type: SpatialWebMsgType.cubeInfo
35
- origin: Vec3
36
- size: Size3D
37
- }
38
-
39
- export interface CubeInfoMsg {
40
- type: SpatialWebMsgType.cubeInfo
41
- origin: Vec3
42
- size: Size3D
43
- }
44
-
45
- export interface TransformMsg {
46
- type: SpatialWebMsgType.transform
47
- detail: {
48
- column0: [number, number, number]
49
- column1: [number, number, number]
50
- column2: [number, number, number]
51
- column3: [number, number, number]
52
- }
53
- }
54
-
55
29
  export interface SpatialTapMsg {
56
30
  type: SpatialWebMsgType.spatialtap
57
31
  detail: SpatialTapEventDetail
package/src/index.ts CHANGED
@@ -6,6 +6,8 @@ export { SpatializedElement } from './SpatializedElement'
6
6
  export { Spatialized2DElement } from './Spatialized2DElement'
7
7
  export { SpatializedStatic3DElement } from './SpatializedStatic3DElement'
8
8
  export { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
9
+ export * as PhysicalMetrics from './physicalMetrics'
10
+
9
11
  export * from './reality'
10
12
  export * from './types/types'
11
13
  export * from './types/global.d'
@@ -434,49 +434,6 @@ describe('SpatializedElement', () => {
434
434
  })
435
435
  })
436
436
 
437
- it('handles transform and cubeInfo events and updates internal state', async () => {
438
- const { SpatialWebEvent } = await import('./SpatialWebEvent')
439
- const { SpatialWebMsgType } = await import('./WebMsgCommand')
440
- const { SpatializedElement } = await import('./SpatializedElement')
441
-
442
- SpatialWebEvent.init()
443
-
444
- class TestElement extends SpatializedElement {
445
- updateProperties = vi.fn().mockResolvedValue({
446
- success: true,
447
- data: undefined,
448
- errorCode: '',
449
- errorMessage: '',
450
- })
451
- }
452
-
453
- const e = new TestElement('el2')
454
- window.__SpatialWebEvent({
455
- id: 'el2',
456
- data: {
457
- type: SpatialWebMsgType.cubeInfo,
458
- size: { width: 1, height: 2, depth: 3 },
459
- origin: { x: 4, y: 5, z: 6 },
460
- },
461
- })
462
- expect(e.cubeInfo?.front).toBe(9)
463
-
464
- window.__SpatialWebEvent({
465
- id: 'el2',
466
- data: {
467
- type: SpatialWebMsgType.transform,
468
- detail: {
469
- column0: [1, 0, 0],
470
- column1: [0, 1, 0],
471
- column2: [0, 0, 1],
472
- column3: [10, 20, 30],
473
- },
474
- },
475
- })
476
- expect(e.transform).toBeDefined()
477
- expect(e.transformInv).toBeDefined()
478
- })
479
-
480
437
  it('updates flags via gesture handler setters and updates transform via JSB', async () => {
481
438
  const { SpatialWebEvent } = await import('./SpatialWebEvent')
482
439
  const { SpatializedElement } = await import('./SpatializedElement')