@webspatial/core-sdk 1.1.0 → 1.2.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 +10 -0
- package/dist/iife/index.d.ts +29 -32
- package/dist/iife/index.global.js +3 -3
- package/dist/iife/index.global.js.map +1 -1
- package/dist/index.d.ts +29 -32
- package/dist/index.js +146 -69
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/JSBCommand.ts +1 -1
- package/src/SpatialScene.ts +2 -2
- package/src/SpatialSession.ts +7 -4
- package/src/SpatializedElement.ts +13 -53
- package/src/SpatializedStatic3DElement.ts +1 -1
- package/src/WebMsgCommand.ts +8 -3
- package/src/coverage-boost.test.ts +1060 -0
- package/src/jsbcommand.coverage.test.ts +542 -0
- package/src/platform-adapter/index.ts +31 -3
- package/src/platform-adapter/vision-os/VisionOSPlatform.ts +0 -1
- package/src/platform-adapter/xr/XRPlatform.ts +133 -0
- package/src/reality/component/index.ts +1 -1
- package/src/reality/entity/SpatialEntity.ts +2 -14
- package/src/reality/entity/index.ts +1 -1
- package/src/reality/geometry/SpatialBoxGeometry.ts +4 -1
- package/src/reality/geometry/index.ts +1 -1
- package/src/reality/material/index.ts +1 -1
- package/src/reality/resource/index.ts +1 -1
- package/src/scene-polyfill.ts +0 -4
- package/src/types/internal.ts +5 -5
- package/src/types/types.ts +23 -17
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
class DOMMatrixPolyfill {
|
|
4
|
+
private tx = 0
|
|
5
|
+
private ty = 0
|
|
6
|
+
private tz = 0
|
|
7
|
+
private sx = 1
|
|
8
|
+
private sy = 1
|
|
9
|
+
private sz = 1
|
|
10
|
+
|
|
11
|
+
translate(x = 0, y = 0, z = 0) {
|
|
12
|
+
this.tx += x
|
|
13
|
+
this.ty += y
|
|
14
|
+
this.tz += z
|
|
15
|
+
return this
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
rotate() {
|
|
19
|
+
return this
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
scale(x = 1, y = 1, z = 1) {
|
|
23
|
+
this.sx *= x
|
|
24
|
+
this.sy *= y
|
|
25
|
+
this.sz *= z
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
inverse() {
|
|
30
|
+
return new DOMMatrixPolyfill()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
transformPoint(p: { x: number; y: number; z?: number }) {
|
|
34
|
+
return {
|
|
35
|
+
x: p.x * this.sx + this.tx,
|
|
36
|
+
y: p.y * this.sy + this.ty,
|
|
37
|
+
z: (p.z ?? 0) * this.sz + this.tz,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toFloat64Array() {
|
|
42
|
+
const out = new Float64Array(16)
|
|
43
|
+
out[0] = this.sx
|
|
44
|
+
out[5] = this.sy
|
|
45
|
+
out[10] = this.sz
|
|
46
|
+
out[12] = this.tx
|
|
47
|
+
out[13] = this.ty
|
|
48
|
+
out[14] = this.tz
|
|
49
|
+
out[15] = 1
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
;(globalThis as any).DOMMatrix = DOMMatrixPolyfill
|
|
55
|
+
|
|
56
|
+
const platformSpy = {
|
|
57
|
+
callJSB: vi.fn(),
|
|
58
|
+
callWebSpatialProtocol: vi.fn(),
|
|
59
|
+
callWebSpatialProtocolSync: vi.fn(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
vi.mock('./platform-adapter', () => ({
|
|
63
|
+
createPlatform: () => platformSpy,
|
|
64
|
+
}))
|
|
65
|
+
|
|
66
|
+
function ok(data: any = {}) {
|
|
67
|
+
return Promise.resolve({
|
|
68
|
+
success: true,
|
|
69
|
+
data,
|
|
70
|
+
errorCode: '',
|
|
71
|
+
errorMessage: '',
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseQuery(q?: string) {
|
|
76
|
+
const sp = new URLSearchParams(q ?? '')
|
|
77
|
+
const out: Record<string, string> = {}
|
|
78
|
+
for (const [k, v] of sp.entries()) out[k] = v
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('JSBCommand', () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
platformSpy.callJSB.mockReset()
|
|
85
|
+
platformSpy.callWebSpatialProtocol.mockReset()
|
|
86
|
+
platformSpy.callWebSpatialProtocolSync.mockReset()
|
|
87
|
+
platformSpy.callJSB.mockImplementation(() => ok({ id: 'id-1' }))
|
|
88
|
+
platformSpy.callWebSpatialProtocol.mockImplementation(() =>
|
|
89
|
+
ok({ windowProxy: {}, id: 'spatial-1' }),
|
|
90
|
+
)
|
|
91
|
+
platformSpy.callWebSpatialProtocolSync.mockImplementation(() => ({
|
|
92
|
+
success: true,
|
|
93
|
+
data: { windowProxy: {}, id: 'spatial-1' },
|
|
94
|
+
errorCode: '',
|
|
95
|
+
errorMessage: '',
|
|
96
|
+
}))
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('serializes params and calls platform.callJSB', async () => {
|
|
100
|
+
const mod = await import('./JSBCommand')
|
|
101
|
+
const { FocusScene, InspectCommand, DestroyCommand, UpdateSceneConfig } =
|
|
102
|
+
mod
|
|
103
|
+
|
|
104
|
+
await new FocusScene('scene-1').execute()
|
|
105
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
106
|
+
'FocusScene',
|
|
107
|
+
JSON.stringify({ id: 'scene-1' }),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
await new InspectCommand().execute()
|
|
111
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
112
|
+
'Inspect',
|
|
113
|
+
JSON.stringify({ id: '' }),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
await new DestroyCommand('obj-1').execute()
|
|
117
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
118
|
+
'Destroy',
|
|
119
|
+
JSON.stringify({ id: 'obj-1' }),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
await new UpdateSceneConfig({ type: 'window' } as any).execute()
|
|
123
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
124
|
+
'UpdateSceneConfig',
|
|
125
|
+
JSON.stringify({ config: { type: 'window' } }),
|
|
126
|
+
)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('builds element commands payloads', async () => {
|
|
130
|
+
const mod = await import('./JSBCommand')
|
|
131
|
+
const {
|
|
132
|
+
UpdateSpatializedElementTransform,
|
|
133
|
+
UpdateSpatialized2DElementProperties,
|
|
134
|
+
UpdateSpatializedDynamic3DElementProperties,
|
|
135
|
+
UpdateSpatializedStatic3DElementProperties,
|
|
136
|
+
AddSpatializedElementToSpatialScene,
|
|
137
|
+
AddSpatializedElementToSpatialized2DElement,
|
|
138
|
+
UpdateUnlitMaterialProperties,
|
|
139
|
+
} = mod
|
|
140
|
+
|
|
141
|
+
const obj = { id: 'so-1' } as any
|
|
142
|
+
const ele = { id: 'ele-1' } as any
|
|
143
|
+
const matrix = new DOMMatrixPolyfill().translate(1, 2, 3)
|
|
144
|
+
|
|
145
|
+
await new UpdateSpatializedElementTransform(obj, matrix as any).execute()
|
|
146
|
+
const last = platformSpy.callJSB.mock.calls.at(-1)
|
|
147
|
+
expect(last?.[0]).toBe('UpdateSpatializedElementTransform')
|
|
148
|
+
expect(JSON.parse(last?.[1])).toEqual({
|
|
149
|
+
id: 'so-1',
|
|
150
|
+
matrix: Array.from(matrix.toFloat64Array()),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await new UpdateSpatialized2DElementProperties(obj, {
|
|
154
|
+
a: 1,
|
|
155
|
+
} as any).execute()
|
|
156
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
157
|
+
'UpdateSpatialized2DElementProperties',
|
|
158
|
+
JSON.stringify({ id: 'so-1', a: 1 }),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
await new UpdateSpatializedDynamic3DElementProperties(obj, {
|
|
162
|
+
b: 2,
|
|
163
|
+
} as any).execute()
|
|
164
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
165
|
+
'UpdateSpatializedDynamic3DElementProperties',
|
|
166
|
+
JSON.stringify({ id: 'so-1', b: 2 }),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
await new UpdateSpatializedStatic3DElementProperties(obj, {
|
|
170
|
+
c: 3,
|
|
171
|
+
} as any).execute()
|
|
172
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
173
|
+
'UpdateSpatializedStatic3DElementProperties',
|
|
174
|
+
JSON.stringify({ id: 'so-1', c: 3 }),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
await new AddSpatializedElementToSpatialScene(ele).execute()
|
|
178
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
179
|
+
'AddSpatializedElementToSpatialScene',
|
|
180
|
+
JSON.stringify({ spatializedElementId: 'ele-1' }),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
await new AddSpatializedElementToSpatialized2DElement(obj, ele).execute()
|
|
184
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
185
|
+
'AddSpatializedElementToSpatialized2DElement',
|
|
186
|
+
JSON.stringify({ id: 'so-1', spatializedElementId: 'ele-1' }),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
await new UpdateUnlitMaterialProperties(obj, { color: '#fff' }).execute()
|
|
190
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
191
|
+
'UpdateUnlitMaterialProperties',
|
|
192
|
+
JSON.stringify({ id: 'so-1', color: '#fff' }),
|
|
193
|
+
)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('creates query string and calls WebSpatialProtocol', async () => {
|
|
197
|
+
const mod = await import('./JSBCommand')
|
|
198
|
+
const { createSpatialSceneCommand, createSpatialized2DElementCommand } = mod
|
|
199
|
+
|
|
200
|
+
await new createSpatialized2DElementCommand().execute()
|
|
201
|
+
expect(platformSpy.callWebSpatialProtocol).toHaveBeenCalledWith(
|
|
202
|
+
'createSpatialized2DElement',
|
|
203
|
+
'',
|
|
204
|
+
undefined,
|
|
205
|
+
undefined,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const cmd = new createSpatialSceneCommand(
|
|
209
|
+
'https://example.com/a?b=c',
|
|
210
|
+
{ type: 'window', defaultSize: { width: 1, height: 2 } } as any,
|
|
211
|
+
'_self',
|
|
212
|
+
'popup=1',
|
|
213
|
+
)
|
|
214
|
+
await cmd.execute()
|
|
215
|
+
const call = platformSpy.callWebSpatialProtocol.mock.calls.at(-1)
|
|
216
|
+
expect(call?.[0]).toBe('createSpatialScene')
|
|
217
|
+
expect(call?.[2]).toBe('_self')
|
|
218
|
+
expect(call?.[3]).toBe('popup=1')
|
|
219
|
+
const q = parseQuery(call?.[1])
|
|
220
|
+
expect(q.url).toBe('https://example.com/a?b=c')
|
|
221
|
+
expect(JSON.parse(q.config)).toEqual({
|
|
222
|
+
type: 'window',
|
|
223
|
+
defaultSize: { width: 1, height: 2 },
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
cmd.executeSync()
|
|
227
|
+
expect(platformSpy.callWebSpatialProtocolSync).toHaveBeenCalled()
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('SpatialObject', () => {
|
|
232
|
+
beforeEach(() => {
|
|
233
|
+
platformSpy.callJSB.mockReset()
|
|
234
|
+
platformSpy.callJSB.mockImplementation((cmd: string) => {
|
|
235
|
+
if (cmd === 'Inspect') return ok({ inspected: true })
|
|
236
|
+
if (cmd === 'Destroy') return ok({ destroyed: true })
|
|
237
|
+
return ok({ id: 'id-1' })
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('inspects and destroys', async () => {
|
|
242
|
+
const { SpatialObject } = await import('./SpatialObject')
|
|
243
|
+
|
|
244
|
+
class TestObj extends SpatialObject {
|
|
245
|
+
onDestroySpy = vi.fn()
|
|
246
|
+
protected override onDestroy() {
|
|
247
|
+
this.onDestroySpy()
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const o = new TestObj('obj-1')
|
|
252
|
+
await expect(o.inspect()).resolves.toEqual({ inspected: true })
|
|
253
|
+
|
|
254
|
+
await expect(o.destroy()).resolves.toEqual({ destroyed: true })
|
|
255
|
+
expect(o.isDestroyed).toBe(true)
|
|
256
|
+
expect(o.onDestroySpy).toHaveBeenCalledTimes(1)
|
|
257
|
+
|
|
258
|
+
await o.destroy()
|
|
259
|
+
expect(
|
|
260
|
+
platformSpy.callJSB.mock.calls.filter(c => c[0] === 'Destroy'),
|
|
261
|
+
).toHaveLength(1)
|
|
262
|
+
})
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
describe('realityCreator', () => {
|
|
266
|
+
beforeEach(() => {
|
|
267
|
+
platformSpy.callJSB.mockReset()
|
|
268
|
+
platformSpy.callJSB.mockImplementation((cmd: string) => {
|
|
269
|
+
if (cmd === 'CreateSpatialEntity') return ok({ id: 'ent-1' })
|
|
270
|
+
if (cmd === 'CreateGeometry') return ok({ id: 'geo-1' })
|
|
271
|
+
if (cmd === 'CreateUnlitMaterial') return ok({ id: 'mat-1' })
|
|
272
|
+
if (cmd === 'CreateModelComponent') return ok({ id: 'cmp-1' })
|
|
273
|
+
if (cmd === 'CreateSpatialModelEntity') return ok({ id: 'ment-1' })
|
|
274
|
+
if (cmd === 'CreateModelAsset') return ok({ id: 'asset-1' })
|
|
275
|
+
return ok({ id: 'id-1' })
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it('creates entities, geometry, materials, components, assets', async () => {
|
|
280
|
+
const creator = await import('./reality/realityCreator')
|
|
281
|
+
const geo = await import('./reality/geometry/SpatialBoxGeometry')
|
|
282
|
+
const mat = await import('./reality/material/SpatialUnlitMaterial')
|
|
283
|
+
const cmp = await import('./reality/component/ModelComponent')
|
|
284
|
+
const asset = await import('./reality/resource/SpatialModelAsset')
|
|
285
|
+
const ent = await import('./reality/entity/SpatialEntity')
|
|
286
|
+
const modelEnt = await import('./reality/entity/SpatialModelEntity')
|
|
287
|
+
|
|
288
|
+
await expect(
|
|
289
|
+
creator.createSpatialEntity({ name: 'n' }),
|
|
290
|
+
).resolves.toBeInstanceOf(ent.SpatialEntity)
|
|
291
|
+
|
|
292
|
+
await expect(
|
|
293
|
+
creator.createSpatialGeometry(
|
|
294
|
+
geo.SpatialBoxGeometry as any,
|
|
295
|
+
{ width: 1 } as any,
|
|
296
|
+
),
|
|
297
|
+
).resolves.toBeInstanceOf(geo.SpatialBoxGeometry)
|
|
298
|
+
|
|
299
|
+
await expect(
|
|
300
|
+
creator.createSpatialUnlitMaterial({ color: '#fff' }),
|
|
301
|
+
).resolves.toBeInstanceOf(mat.SpatialUnlitMaterial)
|
|
302
|
+
|
|
303
|
+
await expect(
|
|
304
|
+
creator.createModelComponent({
|
|
305
|
+
mesh: { id: 'geo-1' } as any,
|
|
306
|
+
materials: [{ id: 'mat-1' }] as any,
|
|
307
|
+
}),
|
|
308
|
+
).resolves.toBeInstanceOf(cmp.ModelComponent)
|
|
309
|
+
|
|
310
|
+
await expect(
|
|
311
|
+
creator.createModelAsset({ url: 'https://example.com/a.glb' }),
|
|
312
|
+
).resolves.toBeInstanceOf(asset.SpatialModelAsset)
|
|
313
|
+
|
|
314
|
+
await expect(
|
|
315
|
+
creator.createSpatialModelEntity(
|
|
316
|
+
{ modelAssetId: 'asset-1' },
|
|
317
|
+
{ name: 'u' },
|
|
318
|
+
),
|
|
319
|
+
).resolves.toBeInstanceOf(modelEnt.SpatialModelEntity)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('throws on creator failures', async () => {
|
|
323
|
+
platformSpy.callJSB.mockResolvedValueOnce({
|
|
324
|
+
success: false,
|
|
325
|
+
data: undefined,
|
|
326
|
+
errorCode: 'E',
|
|
327
|
+
errorMessage: 'bad',
|
|
328
|
+
})
|
|
329
|
+
const creator = await import('./reality/realityCreator')
|
|
330
|
+
await expect(creator.createSpatialEntity({ name: 'n' })).rejects.toThrow(
|
|
331
|
+
/createSpatialEntity failed/,
|
|
332
|
+
)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
describe('SpatialEntity', () => {
|
|
337
|
+
beforeEach(() => {
|
|
338
|
+
platformSpy.callJSB.mockReset()
|
|
339
|
+
platformSpy.callJSB.mockImplementation((cmd: string) => {
|
|
340
|
+
if (
|
|
341
|
+
cmd === 'SetParentToEntity' ||
|
|
342
|
+
cmd === 'UpdateEntityProperties' ||
|
|
343
|
+
cmd === 'UpdateEntityEvent' ||
|
|
344
|
+
cmd.startsWith('ConvertFrom')
|
|
345
|
+
) {
|
|
346
|
+
return ok({})
|
|
347
|
+
}
|
|
348
|
+
return ok({ id: 'id-1' })
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('manages parent-child relationships', async () => {
|
|
353
|
+
const { SpatialEntity } = await import('./reality/entity/SpatialEntity')
|
|
354
|
+
|
|
355
|
+
const parent = new SpatialEntity('p')
|
|
356
|
+
const child = new SpatialEntity('c')
|
|
357
|
+
await parent.addEntity(child)
|
|
358
|
+
|
|
359
|
+
expect(parent.children.map(e => e.id)).toEqual(['c'])
|
|
360
|
+
expect(child.parent).toBe(parent)
|
|
361
|
+
|
|
362
|
+
await child.removeFromParent()
|
|
363
|
+
expect(parent.children).toHaveLength(0)
|
|
364
|
+
expect(child.parent).toBe(null)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('updates transform state and calls JSB', async () => {
|
|
368
|
+
const { SpatialEntity } = await import('./reality/entity/SpatialEntity')
|
|
369
|
+
const e = new SpatialEntity('e')
|
|
370
|
+
await e.updateTransform({ position: { x: 1, y: 2, z: 3 } })
|
|
371
|
+
expect(e.position).toEqual({ x: 1, y: 2, z: 3 })
|
|
372
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
373
|
+
'UpdateEntityProperties',
|
|
374
|
+
expect.any(String),
|
|
375
|
+
)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it('dispatches and bubbles events', async () => {
|
|
379
|
+
const { SpatialEntity } = await import('./reality/entity/SpatialEntity')
|
|
380
|
+
const parent = new SpatialEntity('p')
|
|
381
|
+
const child = new SpatialEntity('c')
|
|
382
|
+
parent.children.push(child)
|
|
383
|
+
child.parent = parent
|
|
384
|
+
|
|
385
|
+
const calls: string[] = []
|
|
386
|
+
parent.events.spatialtap = () => calls.push('parent')
|
|
387
|
+
child.events.spatialtap = () => calls.push('child')
|
|
388
|
+
|
|
389
|
+
child.dispatchEvent(new CustomEvent('spatialtap', { bubbles: true }))
|
|
390
|
+
expect(calls).toEqual(['child', 'parent'])
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('handles events from SpatialWebEvent injection', async () => {
|
|
394
|
+
const { SpatialWebEvent } = await import('./SpatialWebEvent')
|
|
395
|
+
const { SpatialEntity } = await import('./reality/entity/SpatialEntity')
|
|
396
|
+
const { SpatialWebMsgType } = await import('./WebMsgCommand')
|
|
397
|
+
|
|
398
|
+
SpatialWebEvent.init()
|
|
399
|
+
const e = new SpatialEntity('e')
|
|
400
|
+
const cb = vi.fn()
|
|
401
|
+
e.events.spatialtap = cb
|
|
402
|
+
window.__SpatialWebEvent({
|
|
403
|
+
id: 'e',
|
|
404
|
+
data: {
|
|
405
|
+
type: SpatialWebMsgType.spatialtap,
|
|
406
|
+
detail: { location3D: { x: 1, y: 2, z: 3 } },
|
|
407
|
+
},
|
|
408
|
+
})
|
|
409
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('swallows updateEntityEvent failures in addEvent', async () => {
|
|
413
|
+
const { SpatialEntity } = await import('./reality/entity/SpatialEntity')
|
|
414
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
415
|
+
platformSpy.callJSB.mockImplementationOnce(() =>
|
|
416
|
+
Promise.reject(new Error('x')),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
const e = new SpatialEntity('e')
|
|
420
|
+
await e.addEvent('spatialtap' as any, () => {})
|
|
421
|
+
expect(Object.keys(e.events)).toHaveLength(0)
|
|
422
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
423
|
+
consoleSpy.mockRestore()
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
describe('SpatializedElement', () => {
|
|
428
|
+
beforeEach(() => {
|
|
429
|
+
platformSpy.callJSB.mockReset()
|
|
430
|
+
platformSpy.callJSB.mockImplementation((cmd: string) => {
|
|
431
|
+
if (cmd === 'UpdateSpatializedElementTransform') return ok({})
|
|
432
|
+
if (cmd === 'Destroy') return ok({})
|
|
433
|
+
return ok({ id: 'id-1' })
|
|
434
|
+
})
|
|
435
|
+
})
|
|
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
|
+
it('updates flags via gesture handler setters and updates transform via JSB', async () => {
|
|
481
|
+
const { SpatialWebEvent } = await import('./SpatialWebEvent')
|
|
482
|
+
const { SpatializedElement } = await import('./SpatializedElement')
|
|
483
|
+
|
|
484
|
+
SpatialWebEvent.init()
|
|
485
|
+
|
|
486
|
+
class TestElement extends SpatializedElement {
|
|
487
|
+
updateProperties = vi.fn().mockResolvedValue({
|
|
488
|
+
success: true,
|
|
489
|
+
data: undefined,
|
|
490
|
+
errorCode: '',
|
|
491
|
+
errorMessage: '',
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const e = new TestElement('el3')
|
|
496
|
+
e.onSpatialTap = () => {}
|
|
497
|
+
;(e as any).onSpatialTap = undefined
|
|
498
|
+
expect(e.updateProperties).toHaveBeenCalledWith({ enableTapGesture: true })
|
|
499
|
+
expect(e.updateProperties).toHaveBeenCalledWith({ enableTapGesture: false })
|
|
500
|
+
|
|
501
|
+
await e.updateTransform(new DOMMatrixPolyfill() as any)
|
|
502
|
+
expect(platformSpy.callJSB).toHaveBeenCalledWith(
|
|
503
|
+
'UpdateSpatializedElementTransform',
|
|
504
|
+
expect.any(String),
|
|
505
|
+
)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('removes event receiver on destroy', async () => {
|
|
509
|
+
const { SpatialWebEvent } = await import('./SpatialWebEvent')
|
|
510
|
+
const { SpatializedElement } = await import('./SpatializedElement')
|
|
511
|
+
|
|
512
|
+
SpatialWebEvent.init()
|
|
513
|
+
|
|
514
|
+
class TestElement extends SpatializedElement {
|
|
515
|
+
updateProperties = vi.fn().mockResolvedValue({
|
|
516
|
+
success: true,
|
|
517
|
+
data: undefined,
|
|
518
|
+
errorCode: '',
|
|
519
|
+
errorMessage: '',
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const e = new TestElement('el4')
|
|
524
|
+
expect(SpatialWebEvent.eventReceiver.el4).toBeDefined()
|
|
525
|
+
await e.destroy()
|
|
526
|
+
expect(SpatialWebEvent.eventReceiver.el4).toBeUndefined()
|
|
527
|
+
})
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
describe('CubeInfo', () => {
|
|
531
|
+
it('exposes derived bounds', async () => {
|
|
532
|
+
const { CubeInfo } = await import('./types/types')
|
|
533
|
+
const c = new CubeInfo(
|
|
534
|
+
{ width: 2, height: 3, depth: 4 },
|
|
535
|
+
{ x: 1, y: 2, z: 3 },
|
|
536
|
+
)
|
|
537
|
+
expect(c.left).toBe(1)
|
|
538
|
+
expect(c.right).toBe(3)
|
|
539
|
+
expect(c.bottom).toBe(5)
|
|
540
|
+
expect(c.front).toBe(7)
|
|
541
|
+
})
|
|
542
|
+
})
|
|
@@ -2,15 +2,43 @@ import { isSSREnv } from '../ssr-polyfill'
|
|
|
2
2
|
import { PlatformAbility } from './interface'
|
|
3
3
|
import { SSRPlatform } from './ssr/SSRPlatform'
|
|
4
4
|
|
|
5
|
+
function getWebSpatialVersion(ua: string): number[] | null {
|
|
6
|
+
const match = ua.match(/WebSpatial\/(\d+)\.(\d+)\.(\d+)/)
|
|
7
|
+
if (!match) {
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isVersionGreater(a: number[] | null, b: number[]): boolean {
|
|
14
|
+
if (!a) {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
for (let index = 0; index < 3; index += 1) {
|
|
18
|
+
const diff = a[index] - b[index]
|
|
19
|
+
if (diff > 0) {
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
if (diff < 0) {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
5
29
|
export function createPlatform(): PlatformAbility {
|
|
6
30
|
if (isSSREnv()) {
|
|
7
31
|
return new SSRPlatform()
|
|
8
32
|
}
|
|
9
|
-
|
|
33
|
+
const userAgent = window.navigator.userAgent
|
|
34
|
+
const webSpatialVersion = getWebSpatialVersion(userAgent)
|
|
10
35
|
if (
|
|
11
|
-
|
|
12
|
-
|
|
36
|
+
userAgent.includes('PicoWebApp') &&
|
|
37
|
+
isVersionGreater(webSpatialVersion, [0, 0, 1])
|
|
13
38
|
) {
|
|
39
|
+
const XRPlatform = require('./xr/XRPlatform').XRPlatform
|
|
40
|
+
return new XRPlatform()
|
|
41
|
+
} else if (userAgent.includes('Android') || userAgent.includes('Linux')) {
|
|
14
42
|
const AndroidPlatform = require('./android/AndroidPlatform').AndroidPlatform
|
|
15
43
|
return new AndroidPlatform()
|
|
16
44
|
} else {
|