@webspatial/core-sdk 1.3.0 → 1.5.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.3.0",
3
+ "version": "1.5.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'
@@ -386,6 +387,38 @@ export class AddComponentToEntityCommand extends JSBCommand {
386
387
  commandType = 'AddComponentToEntity'
387
388
  }
388
389
 
390
+ export class RemoveComponentFromEntityCommand extends JSBCommand {
391
+ constructor(
392
+ public entity: SpatialEntity,
393
+ public comp: SpatialComponent,
394
+ ) {
395
+ super()
396
+ }
397
+ protected getParams(): Record<string, any> | undefined {
398
+ return {
399
+ entityId: this.entity.id,
400
+ componentId: this.comp.id,
401
+ }
402
+ }
403
+ commandType = 'RemoveComponentFromEntity'
404
+ }
405
+
406
+ export class SetMaterialsOnEntityCommand extends JSBCommand {
407
+ constructor(
408
+ public entityId: string,
409
+ public materials: SpatialMaterial[],
410
+ ) {
411
+ super()
412
+ }
413
+ protected getParams(): Record<string, any> | undefined {
414
+ return {
415
+ entityId: this.entityId,
416
+ materialIds: this.materials.map(m => m.id),
417
+ }
418
+ }
419
+ commandType = 'SetMaterialsOnEntity'
420
+ }
421
+
389
422
  export class AddEntityToDynamic3DCommand extends JSBCommand {
390
423
  constructor(
391
424
  public d3dEle: SpatializedDynamic3DElement,
@@ -502,6 +535,24 @@ export class ConvertFromSceneToEntityCommand extends JSBCommand {
502
535
  commandType = 'ConvertFromSceneToEntity'
503
536
  }
504
537
 
538
+ export class ConvertCoordinateCommand extends JSBCommand {
539
+ constructor(
540
+ public position: Vec3,
541
+ public fromId: string,
542
+ public toId: string,
543
+ ) {
544
+ super()
545
+ }
546
+ protected getParams(): Record<string, any> | undefined {
547
+ return {
548
+ position: this.position,
549
+ fromId: this.fromId,
550
+ toId: this.toId,
551
+ }
552
+ }
553
+ commandType = 'ConvertCoordinate'
554
+ }
555
+
505
556
  export class CreateTextureResourceCommand extends JSBCommand {
506
557
  constructor(private url: string) {
507
558
  super()
@@ -627,11 +678,26 @@ export class CreateAttachmentEntityCommand extends WebSpatialProtocolCommand {
627
678
  constructor(private options: AttachmentEntityOptions) {
628
679
  super()
629
680
  }
681
+ protected getParams() {
682
+ return {} // No metadata — just trigger engine/webview creation
683
+ }
684
+ }
685
+
686
+ export class InitializeAttachmentCommand extends JSBCommand {
687
+ commandType = 'InitializeAttachment'
688
+ constructor(
689
+ private attachmentId: string,
690
+ private options: AttachmentEntityOptions,
691
+ ) {
692
+ super()
693
+ }
630
694
  protected getParams() {
631
695
  return {
696
+ id: this.attachmentId,
632
697
  parentEntityId: this.options.parentEntityId,
633
698
  position: this.options.position ?? [0, 0, 0],
634
699
  size: this.options.size,
700
+ ownerViewId: this.options.ownerViewId,
635
701
  }
636
702
  }
637
703
  }
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,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(
@@ -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
+ })
@@ -37,6 +37,8 @@ export class SpatializedStatic3DElement extends SpatializedElement {
37
37
  * @returns Promise that resolves when the model is loaded (true) or fails to load (false)
38
38
  */
39
39
  private createReadyPromise() {
40
+ // If there's an existing promise reject it before it's replaced
41
+ this._readyResolve?.(false)
40
42
  return new Promise<boolean>(resolve => {
41
43
  this._readyResolve = resolve
42
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')
@@ -0,0 +1,110 @@
1
+ import { describe, expect, test, vi } from 'vitest'
2
+
3
+ async function loadModule() {
4
+ vi.resetModules()
5
+ return await import('./physicalMetrics')
6
+ }
7
+
8
+ describe('physicalMetrics', () => {
9
+ test('default scaled conversion', async () => {
10
+ const { pointToPhysical, physicalToPoint, getValue } = await loadModule()
11
+ const v = getValue()
12
+ expect(v.meterToPtScaled).toBe(1360)
13
+ expect(v.meterToPtUnscaled).toBe(1360)
14
+ expect(pointToPhysical(1360)).toBe(1)
15
+ expect(physicalToPoint(1)).toBe(1360)
16
+ })
17
+
18
+ test('unscaled compensation uses unscaled metrics', async () => {
19
+ const { pointToPhysical, physicalToPoint } = await loadModule()
20
+ expect(
21
+ pointToPhysical(1360, { worldScalingCompensation: 'unscaled' }),
22
+ ).toBe(1)
23
+ expect(physicalToPoint(1, { worldScalingCompensation: 'unscaled' })).toBe(
24
+ 1360,
25
+ )
26
+ })
27
+
28
+ test('updateValue applies window metrics', async () => {
29
+ const m = await loadModule()
30
+ const { SpatialWebEvent } = await import('./SpatialWebEvent')
31
+ SpatialWebEvent.init()
32
+ ;(window as any).__webspatialsdk__ = {
33
+ physicalMetrics: {
34
+ meterToPtScaled: 2000,
35
+ meterToPtUnscaled: 1500,
36
+ },
37
+ }
38
+ const unsubscribe = m.subscribe(() => {})
39
+ window.__SpatialWebEvent({ id: 'window', data: {} })
40
+ const v = m.getValue()
41
+ expect(v.meterToPtScaled).toBe(2000)
42
+ expect(v.meterToPtUnscaled).toBe(1500)
43
+ expect(m.pointToPhysical(2000)).toBe(1)
44
+ expect(
45
+ m.pointToPhysical(1500, { worldScalingCompensation: 'unscaled' }),
46
+ ).toBe(1)
47
+ unsubscribe()
48
+ ;(window as any).__webspatialsdk__ = undefined
49
+ })
50
+
51
+ test('updateValue applies partial metrics', async () => {
52
+ const m = await loadModule()
53
+ const { SpatialWebEvent } = await import('./SpatialWebEvent')
54
+ SpatialWebEvent.init()
55
+ ;(window as any).__webspatialsdk__ = {
56
+ physicalMetrics: { meterToPtScaled: 1000 },
57
+ }
58
+ const unsubscribe = m.subscribe(() => {})
59
+ window.__SpatialWebEvent({ id: 'window', data: {} })
60
+ expect(m.getValue().meterToPtScaled).toBe(1000)
61
+ expect(m.getValue().meterToPtUnscaled).toBe(1360)
62
+ expect(m.pointToPhysical(1000)).toBe(1)
63
+ expect(m.physicalToPoint(1, { worldScalingCompensation: 'unscaled' })).toBe(
64
+ 1360,
65
+ )
66
+ unsubscribe()
67
+ ;(window as any).__webspatialsdk__ = undefined
68
+ })
69
+
70
+ test('subscribe listens to event and supports unsubscribe', async () => {
71
+ const m = await loadModule()
72
+ const { SpatialWebEvent } = await import('./SpatialWebEvent')
73
+ SpatialWebEvent.init()
74
+ const cb = vi.fn()
75
+ const unsubscribe = m.subscribe(cb)
76
+ ;(window as any).__webspatialsdk__ = {
77
+ physicalMetrics: {
78
+ meterToPtScaled: 900,
79
+ meterToPtUnscaled: 800,
80
+ },
81
+ }
82
+ window.__SpatialWebEvent({ id: 'window', data: {} })
83
+ expect(cb).toHaveBeenCalledTimes(1)
84
+ expect(m.getValue().meterToPtScaled).toBe(900)
85
+ expect(m.getValue().meterToPtUnscaled).toBe(800)
86
+ ;(window as any).__webspatialsdk__ = {
87
+ physicalMetrics: {
88
+ meterToPtScaled: 700,
89
+ meterToPtUnscaled: 600,
90
+ },
91
+ }
92
+ window.__SpatialWebEvent({ id: 'window', data: {} })
93
+ expect(cb).toHaveBeenCalledTimes(2)
94
+ expect(m.getValue().meterToPtScaled).toBe(700)
95
+ expect(m.getValue().meterToPtUnscaled).toBe(600)
96
+
97
+ unsubscribe()
98
+ ;(window as any).__webspatialsdk__ = {
99
+ physicalMetrics: {
100
+ meterToPtScaled: 500,
101
+ meterToPtUnscaled: 400,
102
+ },
103
+ }
104
+ window.__SpatialWebEvent({ id: 'window', data: {} })
105
+ expect(cb).toHaveBeenCalledTimes(2)
106
+ expect(m.getValue().meterToPtScaled).toBe(500)
107
+ expect(m.getValue().meterToPtUnscaled).toBe(400)
108
+ ;(window as any).__webspatialsdk__ = undefined
109
+ })
110
+ })