@webspatial/core-sdk 1.3.0 → 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/CHANGELOG.md +41 -1
- package/dist/iife/index.d.ts +73 -31
- package/dist/iife/index.global.js +14 -14
- package/dist/iife/index.global.js.map +1 -1
- package/dist/index.d.ts +73 -31
- package/dist/index.js +182 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/JSBCommand.ts +33 -0
- package/src/Spatial.ts +22 -1
- package/src/SpatialScene.ts +20 -0
- package/src/SpatializedDynamic3DElement.ts +19 -0
- package/src/SpatializedElement.ts +0 -29
- package/src/SpatializedStatic3DElement.test.ts +125 -0
- package/src/SpatializedStatic3DElement.ts +2 -0
- package/src/WebMsgCommand.ts +0 -26
- package/src/index.ts +2 -0
- package/src/jsbcommand.coverage.test.ts +0 -43
- package/src/physicalMetrics.test.ts +110 -0
- package/src/physicalMetrics.ts +101 -0
- package/src/platform-adapter/puppeteer/PuppeteerPlatform.ts +52 -52
- package/src/reality/Attachment.ts +2 -0
- package/src/reality/entity/SpatialEntity.ts +45 -24
- package/src/types/global.d.ts +7 -5
- package/src/types/types.ts +16 -2
package/package.json
CHANGED
package/src/JSBCommand.ts
CHANGED
|
@@ -502,6 +502,24 @@ export class ConvertFromSceneToEntityCommand extends JSBCommand {
|
|
|
502
502
|
commandType = 'ConvertFromSceneToEntity'
|
|
503
503
|
}
|
|
504
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
|
+
|
|
505
523
|
export class CreateTextureResourceCommand extends JSBCommand {
|
|
506
524
|
constructor(private url: string) {
|
|
507
525
|
super()
|
|
@@ -627,11 +645,26 @@ export class CreateAttachmentEntityCommand extends WebSpatialProtocolCommand {
|
|
|
627
645
|
constructor(private options: AttachmentEntityOptions) {
|
|
628
646
|
super()
|
|
629
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
|
+
}
|
|
630
661
|
protected getParams() {
|
|
631
662
|
return {
|
|
663
|
+
id: this.attachmentId,
|
|
632
664
|
parentEntityId: this.options.parentEntityId,
|
|
633
665
|
position: this.options.position ?? [0, 0, 0],
|
|
634
666
|
size: this.options.size,
|
|
667
|
+
ownerViewId: this.options.ownerViewId,
|
|
635
668
|
}
|
|
636
669
|
}
|
|
637
670
|
}
|
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 === '
|
|
75
|
+
return window.WebSpatailNativeVersion === 'WS_SHELL_VERSION'
|
|
55
76
|
? this.getClientVersion()
|
|
56
77
|
: window.WebSpatailNativeVersion
|
|
57
78
|
}
|
package/src/SpatialScene.ts
CHANGED
|
@@ -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
|
})
|
package/src/WebMsgCommand.ts
CHANGED
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { SpatialWebEvent } from './SpatialWebEvent'
|
|
2
|
+
export type PhysicalMetricsValueShape = {
|
|
3
|
+
meterToPtUnscaled: number
|
|
4
|
+
meterToPtScaled: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type WorldScalingCompensation = 'unscaled' | 'scaled'
|
|
8
|
+
|
|
9
|
+
type ConvertOption = { worldScalingCompensation: WorldScalingCompensation }
|
|
10
|
+
|
|
11
|
+
// Fallback calibration: 1 meter ≈ 1360 pt for both scaled and unscaled modes.
|
|
12
|
+
// This baseline ensures pointToPhysical(1360) === 1 and physicalToPoint(1) === 1360
|
|
13
|
+
// until native physical metrics are injected into window.__webspatialsdk__.physicalMetrics
|
|
14
|
+
// and a 'WebSpatialPhysicalMetricsUpdate' event updates the snapshot at runtime.
|
|
15
|
+
let snapshot: PhysicalMetricsValueShape = {
|
|
16
|
+
meterToPtUnscaled: 1360,
|
|
17
|
+
meterToPtScaled: 1360,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getWorldScalingCompensation(options?: ConvertOption) {
|
|
21
|
+
return options?.worldScalingCompensation ?? 'scaled' // default to scaled
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Converts scene points (pt) to physical meters (m).
|
|
26
|
+
*
|
|
27
|
+
* @param point Points value to convert.
|
|
28
|
+
* @param options Optional conversion options to select world scaling compensation.
|
|
29
|
+
* @returns Physical length in meters.
|
|
30
|
+
*/
|
|
31
|
+
export function pointToPhysical(point: number, options?: ConvertOption) {
|
|
32
|
+
updateValue()
|
|
33
|
+
const compensation = getWorldScalingCompensation(options)
|
|
34
|
+
if (compensation === 'unscaled') {
|
|
35
|
+
return point / snapshot.meterToPtUnscaled
|
|
36
|
+
}
|
|
37
|
+
return point / snapshot.meterToPtScaled
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Converts physical meters (m) to scene points (pt).
|
|
42
|
+
*
|
|
43
|
+
* @param physical Physical length in meters to convert.
|
|
44
|
+
* @param options Optional conversion options to select world scaling compensation.
|
|
45
|
+
* @returns Points length in the scene.
|
|
46
|
+
*/
|
|
47
|
+
export function physicalToPoint(physical: number, options?: ConvertOption) {
|
|
48
|
+
updateValue()
|
|
49
|
+
const compensation = getWorldScalingCompensation(options)
|
|
50
|
+
if (compensation === 'unscaled') {
|
|
51
|
+
return physical * snapshot.meterToPtUnscaled
|
|
52
|
+
}
|
|
53
|
+
return physical * snapshot.meterToPtScaled
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function updateValue() {
|
|
57
|
+
// ssr protected
|
|
58
|
+
if (typeof window === 'undefined') return
|
|
59
|
+
const src = window.__webspatialsdk__?.physicalMetrics
|
|
60
|
+
if (!src) return
|
|
61
|
+
const next = {
|
|
62
|
+
meterToPtScaled: src.meterToPtScaled ?? snapshot.meterToPtScaled,
|
|
63
|
+
meterToPtUnscaled: src.meterToPtUnscaled ?? snapshot.meterToPtUnscaled,
|
|
64
|
+
}
|
|
65
|
+
// only update if there is a change
|
|
66
|
+
if (
|
|
67
|
+
next.meterToPtScaled !== snapshot.meterToPtScaled ||
|
|
68
|
+
next.meterToPtUnscaled !== snapshot.meterToPtUnscaled
|
|
69
|
+
) {
|
|
70
|
+
snapshot = next
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the current physical metrics used for conversions.
|
|
76
|
+
*
|
|
77
|
+
* @returns The current metrics snapshot `{ meterToPtUnscaled, meterToPtScaled }`.
|
|
78
|
+
*/
|
|
79
|
+
export function getValue(): PhysicalMetricsValueShape {
|
|
80
|
+
updateValue()
|
|
81
|
+
return snapshot
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Subscribes to physical metrics changes.
|
|
86
|
+
*
|
|
87
|
+
* @param cb Callback invoked when metrics update is detected.
|
|
88
|
+
* @returns Unsubscribe function to remove the listener.
|
|
89
|
+
*/
|
|
90
|
+
export function subscribe(cb: () => void) {
|
|
91
|
+
// ssr protected
|
|
92
|
+
if (typeof window === 'undefined') return () => {}
|
|
93
|
+
const handler = () => {
|
|
94
|
+
cb()
|
|
95
|
+
}
|
|
96
|
+
// receive metrics update from native via SpatialWebEvent, id: "window"
|
|
97
|
+
SpatialWebEvent.addEventReceiver('window', handler)
|
|
98
|
+
return () => {
|
|
99
|
+
SpatialWebEvent.removeEventReceiver('window')
|
|
100
|
+
}
|
|
101
|
+
}
|