@webspatial/core-sdk 1.2.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -1
- package/dist/iife/index.d.ts +115 -32
- package/dist/iife/index.global.js +68 -3
- package/dist/iife/index.global.js.map +1 -1
- package/dist/index.d.ts +115 -32
- package/dist/index.js +603 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/JSBCommand.ts +65 -0
- package/src/Spatial.ts +22 -1
- package/src/SpatialScene.ts +20 -0
- package/src/SpatialSession.ts +15 -1
- package/src/SpatializedDynamic3DElement.ts +19 -0
- package/src/SpatializedElement.ts +0 -29
- package/src/SpatializedElementCreator.ts +1 -1
- package/src/SpatializedStatic3DElement.test.ts +125 -0
- package/src/SpatializedStatic3DElement.ts +14 -1
- 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/index.ts +5 -1
- package/src/platform-adapter/puppeteer/PuppeteerPlatform.ts +470 -0
- package/src/reality/Attachment.ts +47 -0
- package/src/reality/entity/SpatialEntity.ts +45 -24
- package/src/reality/index.ts +1 -0
- package/src/types/global.d.ts +7 -5
- package/src/types/types.ts +29 -2
|
@@ -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
|
+
}
|