@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.
@@ -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
+ }
@@ -32,7 +32,11 @@ export function createPlatform(): PlatformAbility {
32
32
  }
33
33
  const userAgent = window.navigator.userAgent
34
34
  const webSpatialVersion = getWebSpatialVersion(userAgent)
35
- if (
35
+ if (window.navigator.userAgent.includes('Puppeteer')) {
36
+ const PuppeteerPlatform =
37
+ require('./puppeteer/PuppeteerPlatform').PuppeteerPlatform
38
+ return new PuppeteerPlatform()
39
+ } else if (
36
40
  userAgent.includes('PicoWebApp') &&
37
41
  isVersionGreater(webSpatialVersion, [0, 0, 1])
38
42
  ) {
@@ -0,0 +1,470 @@
1
+ import { PlatformAbility, CommandResult } from '../interface'
2
+ import {
3
+ CommandResultFailure,
4
+ CommandResultSuccess,
5
+ } from '../CommandResultUtils'
6
+
7
+ // add window interface for JSB call
8
+ declare global {
9
+ interface Window {
10
+ __handleJSBMessage: (message: string) => any
11
+ SpatialId?: string
12
+ }
13
+
14
+ interface HTMLIFrameElement {
15
+ spatialId?: string
16
+ webSpatialId?: string
17
+ }
18
+ }
19
+
20
+ type JSBError = {
21
+ message: string
22
+ }
23
+
24
+ console.log('PuppeteerPlatform')
25
+
26
+ export class PuppeteerPlatform implements PlatformAbility {
27
+ // store iframe instance
28
+ private iframeRegistry: Map<string, HTMLIFrameElement> = new Map()
29
+
30
+ constructor() {}
31
+
32
+ callJSB(cmd: string, msg: string): Promise<CommandResult> {
33
+ return new Promise(resolve => {
34
+ try {
35
+ // check __handleJSBMessage exist
36
+ if (window.__handleJSBMessage) {
37
+ try {
38
+ console.log(` core-sdk Puppeteer Platform: callJSB: ${cmd}::${msg}`)
39
+ const result = window.__handleJSBMessage(`${cmd}::${msg}`)
40
+ console.log(
41
+ ` core-sdk Puppeteer Platform callJSB result: ${result}`,
42
+ )
43
+ resolve(CommandResultSuccess(result))
44
+ } catch (err) {
45
+ resolve(CommandResultFailure('500', 'JSB execution error'))
46
+ }
47
+ } else {
48
+ // if not exist, return default result
49
+ resolve(CommandResultSuccess('ok'))
50
+ }
51
+ } catch (error: unknown) {
52
+ console.error(
53
+ `PuppeteerPlatform cmd Error: ${cmd}, msg: ${msg} error: ${error}`,
54
+ )
55
+ resolve(CommandResultFailure('500', 'Internal error'))
56
+ }
57
+ })
58
+ }
59
+
60
+ /**
61
+ * Synchronously create Spatialized2DElement to Puppeteer Runner
62
+ */
63
+ private createSpatializedElementSync(
64
+ spatialId: string,
65
+ webspatialUrl: string,
66
+ ): void {
67
+ try {
68
+ console.log(
69
+ `[Puppeteer Platform] Creating spatialized element sync with id: ${spatialId}, url: ${webspatialUrl}`,
70
+ )
71
+ // directly call Puppeteer Runner method to create element
72
+ const win = window as any
73
+ if (win.__handleJSBMessage) {
74
+ // use simpler format to ensure JSBManager can correctly use our passed spatialId
75
+ const createCommand = {
76
+ id: spatialId,
77
+ url: webspatialUrl,
78
+ }
79
+ win.__handleJSBMessage(
80
+ `CreateSpatialized2DElement::${JSON.stringify(createCommand)}`,
81
+ )
82
+ }
83
+ } catch (error) {
84
+ console.error('Error creating spatialized element sync:', error)
85
+ }
86
+ }
87
+
88
+ callWebSpatialProtocol(
89
+ command: string,
90
+ query?: string,
91
+ target?: string,
92
+ features?: string,
93
+ ): Promise<CommandResult> {
94
+ console.log(
95
+ `PuppeteerPlatform: Calling webspatial protocol: webspatial://${command}${query ? `?${query}` : ''}`,
96
+ )
97
+ return new Promise(resolve => {
98
+ try {
99
+ // create complete webspatial URL
100
+ const webspatialUrl = `webspatial://${command}${query ? `?${query}` : ''}`
101
+ // use iframe to create new window
102
+ const { spatialId, iframe, windowProxy } = this.createIframeWindow(
103
+ webspatialUrl,
104
+ target,
105
+ features,
106
+ )
107
+
108
+ // 对于createSpatialized2DElement命令,同步创建元素
109
+ if (command === 'createSpatialized2DElement') {
110
+ this.createSpatializedElementSync(spatialId, webspatialUrl)
111
+ }
112
+ console.log(
113
+ `[Puppeteer Platform] iframe created with spatialId: ${spatialId}`,
114
+ )
115
+ // store iframe instance
116
+ this.iframeRegistry.set(spatialId, iframe)
117
+ resolve(CommandResultSuccess({ windowProxy, id: spatialId }))
118
+ } catch (error) {
119
+ console.error('Error calling webspatial protocol:', error)
120
+ resolve(
121
+ CommandResultFailure('500', 'Failed to call webspatial protocol'),
122
+ )
123
+ }
124
+ })
125
+ }
126
+
127
+ callWebSpatialProtocolSync(
128
+ command: string,
129
+ query?: string,
130
+ target?: string,
131
+ features?: string,
132
+ ): CommandResult {
133
+ try {
134
+ // create complete webspatial URL
135
+ const webspatialUrl = `webspatial://${command}${query ? `?${query}` : ''}`
136
+ console.log(`Calling webspatial protocol sync: ${webspatialUrl}`)
137
+
138
+ // 使用iframe创建新窗口
139
+ const { spatialId, iframe, windowProxy } = this.createIframeWindow(
140
+ webspatialUrl,
141
+ target,
142
+ features,
143
+ )
144
+
145
+ // 对于createSpatialized2DElement命令,同步创建元素
146
+ if (command === 'createSpatialized2DElement') {
147
+ this.createSpatializedElementSync(spatialId, webspatialUrl)
148
+ }
149
+
150
+ // store iframe instance
151
+ this.iframeRegistry.set(spatialId, iframe)
152
+
153
+ return CommandResultSuccess({ windowProxy, id: spatialId })
154
+ } catch (error) {
155
+ console.error('Error calling webspatial protocol sync:', error)
156
+ return CommandResultFailure(
157
+ '500',
158
+ 'Failed to call webspatial protocol sync',
159
+ )
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Synchronously create iframe-based window
165
+ */
166
+ private createIframeWindow(url: string, target?: string, features?: string) {
167
+ // create iframe element
168
+ const iframe = document.createElement('iframe')
169
+
170
+ // set iframe attributes
171
+ iframe.style.border = 'none'
172
+ iframe.style.display = 'none'
173
+ iframe.style.width = '100%'
174
+ iframe.style.height = '100%'
175
+
176
+ // set iframe id
177
+ const spatialId = this.generateUUID()
178
+ iframe.spatialId = spatialId
179
+ iframe.id = `spatial-iframe-${spatialId}`
180
+
181
+ // parse features parameter
182
+ const featuresObj = this.parseFeatures(features || '')
183
+
184
+ // set iframe styles based on features
185
+ if (featuresObj.width) {
186
+ iframe.style.width = featuresObj.width
187
+ }
188
+ if (featuresObj.height) {
189
+ iframe.style.height = featuresObj.height
190
+ }
191
+ if (featuresObj.left) {
192
+ iframe.style.left = featuresObj.left
193
+ iframe.style.position = 'absolute'
194
+ }
195
+ if (featuresObj.top) {
196
+ iframe.style.top = featuresObj.top
197
+ iframe.style.position = 'absolute'
198
+ }
199
+
200
+ // add iframe to DOM
201
+ document.body.appendChild(iframe)
202
+
203
+ // create enhanced windowProxy object
204
+ const windowProxy = this.createEnhancedWindowProxy(iframe, url, spatialId)
205
+
206
+ // set iframe src
207
+ iframe.src = 'about:blank'
208
+
209
+ console.log(
210
+ `PuppeteerPlatform created iframe window with spatialId: ${spatialId}, URL: ${url}`,
211
+ )
212
+
213
+ // initialize iframe content
214
+ this.initializeIframeContent(iframe, url, spatialId)
215
+
216
+ return { spatialId, iframe, windowProxy }
217
+ }
218
+
219
+ /**
220
+ * create enhanced windowProxy object
221
+ */
222
+ private createEnhancedWindowProxy(
223
+ iframe: HTMLIFrameElement,
224
+ url: string,
225
+ spatialId: string,
226
+ ) {
227
+ // create enhanced windowProxy object
228
+ return {
229
+ // basic properties
230
+ location: {
231
+ href: url,
232
+ toString: () => url,
233
+ reload: () => {
234
+ if (iframe.contentWindow) {
235
+ iframe.contentWindow.location.reload()
236
+ }
237
+ },
238
+ },
239
+ navigator: {
240
+ userAgent: `Mozilla/5.0 (WebKit) SpatialId/${spatialId}`,
241
+ },
242
+
243
+ // methods
244
+ close: () => {
245
+ console.log(`Closing iframe with spatialId: ${spatialId}`)
246
+ iframe.remove()
247
+ this.iframeRegistry.delete(spatialId)
248
+ },
249
+
250
+ // document access
251
+ document: iframe.contentDocument || ({} as Document),
252
+ contentWindow: iframe.contentWindow || ({} as Window),
253
+
254
+ // add message communication method
255
+ postMessage: (message: any, targetOrigin?: string) => {
256
+ if (iframe.contentWindow) {
257
+ iframe.contentWindow.postMessage(message, targetOrigin || '*')
258
+ }
259
+ },
260
+
261
+ // add event listener method
262
+ addEventListener: (
263
+ type: string,
264
+ listener: EventListenerOrEventListenerObject,
265
+ ) => {
266
+ if (iframe.contentWindow) {
267
+ iframe.contentWindow.addEventListener(type, listener)
268
+ }
269
+ },
270
+
271
+ removeEventListener: (
272
+ type: string,
273
+ listener: EventListenerOrEventListenerObject,
274
+ ) => {
275
+ if (iframe.contentWindow) {
276
+ iframe.contentWindow.removeEventListener(type, listener)
277
+ }
278
+ },
279
+
280
+ // execute JavaScript
281
+ executeScript: (code: string): any => {
282
+ if (iframe.contentWindow) {
283
+ try {
284
+ // use type assertion and safer way to execute script
285
+ const win = iframe.contentWindow as any
286
+ return win.eval(code)
287
+ } catch (error) {
288
+ console.error(
289
+ `Error executing script in iframe ${spatialId}:`,
290
+ error,
291
+ )
292
+ return null
293
+ }
294
+ }
295
+ return null
296
+ },
297
+
298
+ // get iframe reference
299
+ getIframe: () => iframe,
300
+
301
+ // get spatialId
302
+ getSpatialId: () => spatialId,
303
+ }
304
+ }
305
+
306
+ /**
307
+ * initialize iframe content
308
+ */
309
+ private initializeIframeContent(
310
+ iframe: HTMLIFrameElement,
311
+ url: string,
312
+ spatialId: string,
313
+ ): void {
314
+ try {
315
+ // wait for iframe to load
316
+ iframe.onload = () => {
317
+ try {
318
+ // set iframe content
319
+ const iframeContent = `
320
+ // inject communication script
321
+ window.webSpatialId = '${spatialId}';
322
+ window.SpatialId = '${spatialId}';
323
+
324
+ // override window.open to support webspatial protocol
325
+ const originalOpen = window.open;
326
+ window.open = function(url, target, features) {
327
+ if (url && url.startsWith('webspatial://')) {
328
+ // handle webspatial protocol through windowProxy
329
+ const windowProxy = new Proxy({}, {
330
+ get: function(target, prop) {
331
+ if (prop === 'toString') {
332
+ return function() { return url; };
333
+ }
334
+ return undefined;
335
+ }
336
+ });
337
+ return windowProxy;
338
+ }
339
+ return originalOpen.call(window, url, target, features);
340
+ };
341
+
342
+ // set navigator.userAgent to identify webspatial environment
343
+ Object.defineProperty(navigator, 'userAgent', {
344
+ value: 'WebSpatial/1.0 ' + navigator.userAgent,
345
+ configurable: true
346
+ });
347
+
348
+ // send loaded message
349
+ window.parent.postMessage({
350
+ type: 'iframe_loaded',
351
+ spatialId: '${spatialId}',
352
+ url: '${url}'
353
+ }, '${window.location.origin}');
354
+
355
+ // set message handler
356
+ window.addEventListener('message', (event) => {
357
+ if (event.origin !== window.parent.location.origin) return;
358
+
359
+ const data = event.data;
360
+ if (data && data.type === 'webspatial_command') {
361
+ // handle command from parent window
362
+ console.log('Received command in iframe from parent:', data.command);
363
+ // add command handling logic here
364
+ }
365
+ });
366
+ `
367
+
368
+ // use document.write instead of eval for security and type compliance
369
+ const doc = iframe.contentDocument
370
+ if (doc) {
371
+ doc.open()
372
+ doc.write(`
373
+ <!DOCTYPE html>
374
+ <html>
375
+ <head>
376
+ <title>Spatial Iframe - ${spatialId}</title>
377
+ <meta charset="UTF-8">
378
+ <style>
379
+ body {
380
+ margin: 0;
381
+ padding: 0;
382
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
383
+ }
384
+ </style>
385
+ </head>
386
+ <body>
387
+ <script>${iframeContent}</script>
388
+ </body>
389
+ </html>
390
+ `)
391
+ doc.close()
392
+ }
393
+ } catch (error) {
394
+ console.error('Error initializing iframe content:', error)
395
+ }
396
+ }
397
+ } catch (error) {
398
+ console.error('Error setting up iframe:', error)
399
+ }
400
+ }
401
+
402
+ /**
403
+ * parse features string to object
404
+ */
405
+ private parseFeatures(features: string): Record<string, string> {
406
+ const result: Record<string, string> = {}
407
+ const pairs = features.split(',')
408
+
409
+ pairs.forEach(pair => {
410
+ const [key, value] = pair.split('=').map(s => s.trim())
411
+ if (key && value) {
412
+ result[key] = value
413
+ }
414
+ })
415
+
416
+ return result
417
+ }
418
+
419
+ /**
420
+ * send message to iframe with specified spatialId
421
+ */
422
+ public sendMessageToIframe(spatialId: string, message: any): boolean {
423
+ const iframe = this.iframeRegistry.get(spatialId)
424
+ if (iframe && iframe.contentWindow) {
425
+ iframe.contentWindow.postMessage(message, window.location.origin)
426
+ return true
427
+ }
428
+ return false
429
+ }
430
+
431
+ /**
432
+ * get all active iframes
433
+ */
434
+ public getAllActiveIframes(): Array<{
435
+ spatialId: string
436
+ iframe: HTMLIFrameElement
437
+ }> {
438
+ const result: Array<{ spatialId: string; iframe: HTMLIFrameElement }> = []
439
+
440
+ this.iframeRegistry.forEach((iframe, spatialId) => {
441
+ result.push({ spatialId, iframe })
442
+ })
443
+
444
+ return result
445
+ }
446
+
447
+ /**
448
+ * dispose all active iframes
449
+ */
450
+ public dispose(): void {
451
+ // close all iframes
452
+ this.iframeRegistry.forEach((iframe, spatialId) => {
453
+ console.log(`Disposing iframe with spatialId: ${spatialId}`)
454
+ iframe.remove()
455
+ })
456
+ this.iframeRegistry.clear()
457
+ }
458
+
459
+ // generate UUID function
460
+ private generateUUID(): string {
461
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
462
+ /[xy]/g,
463
+ function (c) {
464
+ const r = (Math.random() * 16) | 0
465
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
466
+ return v.toString(16).toUpperCase()
467
+ },
468
+ )
469
+ }
470
+ }