@webspatial/core-sdk 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webspatial/core-sdk",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "this is the core js API for webspatial",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
package/src/JSBCommand.ts CHANGED
@@ -21,6 +21,8 @@ import {
21
21
  SpatialModelEntityCreationOptions,
22
22
  SpatialEntityEventType,
23
23
  Vec3,
24
+ AttachmentEntityOptions,
25
+ AttachmentEntityUpdateOptions,
24
26
  } from './types/types'
25
27
  import { SpatialSceneCreationOptionsInternal } from './types/internal'
26
28
  import { composeSRT } from './utils'
@@ -620,6 +622,36 @@ export class createSpatialSceneCommand extends WebSpatialProtocolCommand {
620
622
  }
621
623
  }
622
624
 
625
+ export class CreateAttachmentEntityCommand extends WebSpatialProtocolCommand {
626
+ commandType = 'createAttachment'
627
+ constructor(private options: AttachmentEntityOptions) {
628
+ super()
629
+ }
630
+ protected getParams() {
631
+ return {
632
+ parentEntityId: this.options.parentEntityId,
633
+ position: this.options.position ?? [0, 0, 0],
634
+ size: this.options.size,
635
+ }
636
+ }
637
+ }
638
+
639
+ export class UpdateAttachmentEntityCommand extends JSBCommand {
640
+ commandType = 'UpdateAttachmentEntity'
641
+ constructor(
642
+ private attachmentId: string,
643
+ private options: AttachmentEntityUpdateOptions,
644
+ ) {
645
+ super()
646
+ }
647
+ protected getParams() {
648
+ return {
649
+ id: this.attachmentId,
650
+ ...this.options,
651
+ }
652
+ }
653
+ }
654
+
623
655
  // TODO: Can crypto.randomUUID be used instead including in dev environments without https
624
656
  function uuid(): string {
625
657
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
@@ -6,6 +6,7 @@ import {
6
6
  createSpatializedDynamic3DElement,
7
7
  } from './SpatializedElementCreator'
8
8
  import { createSpatializedStatic3DElement } from './SpatializedElementCreator'
9
+ import { Attachment, createAttachmentEntity } from './reality/Attachment'
9
10
  import { SpatializedStatic3DElement } from './SpatializedStatic3DElement'
10
11
  import {
11
12
  ModelComponentOptions,
@@ -20,6 +21,7 @@ import {
20
21
  SpatialSphereGeometryOptions,
21
22
  SpatialUnlitMaterialOptions,
22
23
  SpatialEntityUserData,
24
+ AttachmentEntityOptions,
23
25
  } from './types/types'
24
26
  import { SpatializedDynamic3DElement } from './SpatializedDynamic3DElement'
25
27
  import { SpatialEntity } from './reality/entity/SpatialEntity'
@@ -70,7 +72,7 @@ export class SpatialSession {
70
72
  * @returns Promise resolving to a new SpatializedStatic3DElement instance
71
73
  */
72
74
  createSpatializedStatic3DElement(
73
- modelURL: string = '',
75
+ modelURL: string,
74
76
  ): Promise<SpatializedStatic3DElement> {
75
77
  return createSpatializedStatic3DElement(modelURL)
76
78
  }
@@ -187,4 +189,16 @@ export class SpatialSession {
187
189
  ) {
188
190
  return createSpatialModelEntity(options, userData)
189
191
  }
192
+
193
+ /**
194
+ * Creates an attachment entity that renders 2D HTML content as a child
195
+ * of a 3D entity in the scene graph.
196
+ * @param options Configuration options including parent entity ID, position, and size
197
+ * @returns Promise resolving to a new Attachment instance
198
+ */
199
+ createAttachmentEntity(
200
+ options: AttachmentEntityOptions,
201
+ ): Promise<Attachment> {
202
+ return createAttachmentEntity(options)
203
+ }
190
204
  }
@@ -30,7 +30,7 @@ export async function createSpatializedStatic3DElement(
30
30
  throw new Error('createSpatializedStatic3DElement failed')
31
31
  } else {
32
32
  const { id } = result.data
33
- return new SpatializedStatic3DElement(id)
33
+ return new SpatializedStatic3DElement(id, modelURL)
34
34
  }
35
35
  }
36
36
 
@@ -9,6 +9,17 @@ import { SpatialWebMsgType } from './WebMsgCommand'
9
9
  * and provides events for load success and failure.
10
10
  */
11
11
  export class SpatializedStatic3DElement extends SpatializedElement {
12
+ /**
13
+ * Creates a new spatialized static 3D element with the specified ID and URL.
14
+ * Registers the element to receive spatial events.
15
+ * @param id Unique identifier for this element
16
+ * @param modelURL URL of the 3D model
17
+ */
18
+ constructor(id: string, modelURL: string) {
19
+ super(id)
20
+ this.modelURL = modelURL
21
+ }
22
+
12
23
  /**
13
24
  * Promise resolver for the ready state.
14
25
  * Used to resolve the ready promise when the model is loaded.
@@ -19,7 +30,7 @@ export class SpatializedStatic3DElement extends SpatializedElement {
19
30
  * Caches the last model URL to detect changes.
20
31
  * Used to reset the ready promise when the model URL changes.
21
32
  */
22
- private modelURL: string = ''
33
+ private modelURL: string
23
34
 
24
35
  /**
25
36
  * Creates a new promise for tracking the ready state of the model.
@@ -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
+ // 添加window扩展接口
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
+ // 存储iframe实例
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
+ // 检查__handleJSBMessage是否存在
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
+ // 如果不存在,返回默认结果
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
+ * 同步创建Spatialized2DElement到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
+ // 直接调用Puppeteer Runner的方法来创建元素
72
+ const win = window as any
73
+ if (win.__handleJSBMessage) {
74
+ // 使用更简洁的格式,确保JSBManager能正确使用我们传递的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
+ // 创建完整的webspatial URL
100
+ const webspatialUrl = `webspatial://${command}${query ? `?${query}` : ''}`
101
+ // 使用iframe创建新窗口
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
+ // 注册iframe
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
+ // 创建完整的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
+ // 注册iframe
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
+ * 创建基于iframe的窗口
165
+ */
166
+ private createIframeWindow(url: string, target?: string, features?: string) {
167
+ // 创建iframe元素
168
+ const iframe = document.createElement('iframe')
169
+
170
+ // 设置iframe属性
171
+ iframe.style.border = 'none'
172
+ iframe.style.display = 'none' // 初始隐藏
173
+ iframe.style.width = '100%'
174
+ iframe.style.height = '100%'
175
+
176
+ // 生成唯一的spatialId
177
+ const spatialId = this.generateUUID()
178
+ iframe.spatialId = spatialId
179
+ iframe.id = `spatial-iframe-${spatialId}`
180
+
181
+ // 解析features参数
182
+ const featuresObj = this.parseFeatures(features || '')
183
+
184
+ // 根据features设置iframe样式
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
+ // 添加iframe到DOM
201
+ document.body.appendChild(iframe)
202
+
203
+ // 创建增强的windowProxy模拟对象
204
+ const windowProxy = this.createEnhancedWindowProxy(iframe, url, spatialId)
205
+
206
+ // 设置iframe的src
207
+ iframe.src = 'about:blank'
208
+
209
+ console.log(
210
+ `PuppeteerPlatform created iframe window with spatialId: ${spatialId}, URL: ${url}`,
211
+ )
212
+
213
+ // 初始化iframe内容
214
+ this.initializeIframeContent(iframe, url, spatialId)
215
+
216
+ return { spatialId, iframe, windowProxy }
217
+ }
218
+
219
+ /**
220
+ * 创建增强的windowProxy对象
221
+ */
222
+ private createEnhancedWindowProxy(
223
+ iframe: HTMLIFrameElement,
224
+ url: string,
225
+ spatialId: string,
226
+ ) {
227
+ // 创建增强的windowProxy模拟对象
228
+ return {
229
+ // 基本属性
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
+ // 方法
244
+ close: () => {
245
+ console.log(`Closing iframe with spatialId: ${spatialId}`)
246
+ iframe.remove()
247
+ this.iframeRegistry.delete(spatialId)
248
+ },
249
+
250
+ // 文档访问
251
+ document: iframe.contentDocument || ({} as Document),
252
+ contentWindow: iframe.contentWindow || ({} as Window),
253
+
254
+ // 添加消息通信方法
255
+ postMessage: (message: any, targetOrigin?: string) => {
256
+ if (iframe.contentWindow) {
257
+ iframe.contentWindow.postMessage(message, targetOrigin || '*')
258
+ }
259
+ },
260
+
261
+ // 添加事件监听方法
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
+ // 执行JavaScript
281
+ executeScript: (code: string): any => {
282
+ if (iframe.contentWindow) {
283
+ try {
284
+ // 使用类型断言和更安全的方式执行脚本
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
+ // 获取iframe引用
299
+ getIframe: () => iframe,
300
+
301
+ // 获取spatialId
302
+ getSpatialId: () => spatialId,
303
+ }
304
+ }
305
+
306
+ /**
307
+ * 初始化iframe内容
308
+ */
309
+ private initializeIframeContent(
310
+ iframe: HTMLIFrameElement,
311
+ url: string,
312
+ spatialId: string,
313
+ ): void {
314
+ try {
315
+ // 等待iframe加载完成
316
+ iframe.onload = () => {
317
+ try {
318
+ // 设置iframe内容
319
+ const iframeContent = `
320
+ // 注入通信脚本
321
+ window.webSpatialId = '${spatialId}';
322
+ window.SpatialId = '${spatialId}';
323
+
324
+ // 重写window.open以支持webspatial协议
325
+ const originalOpen = window.open;
326
+ window.open = function(url, target, features) {
327
+ if (url && url.startsWith('webspatial://')) {
328
+ // 通过windowProxy处理webspatial协议
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
+ // 设置navigator.userAgent以识别webspatial环境
343
+ Object.defineProperty(navigator, 'userAgent', {
344
+ value: 'WebSpatial/1.0 ' + navigator.userAgent,
345
+ configurable: true
346
+ });
347
+
348
+ // 发送加载完成消息
349
+ window.parent.postMessage({
350
+ type: 'iframe_loaded',
351
+ spatialId: '${spatialId}',
352
+ url: '${url}'
353
+ }, '${window.location.origin}');
354
+
355
+ // 设置消息处理器
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
+ // 处理来自父窗口的命令
362
+ console.log('Received command in iframe from parent:', data.command);
363
+ // 这里可以添加命令处理逻辑
364
+ }
365
+ });
366
+ `
367
+
368
+ // 使用document.write代替eval,更安全且符合类型定义
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
+ * 解析features字符串为对象
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
+ * 发送消息到指定spatialId的iframe
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
+ * 获取所有活跃的iframe
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
+ * 清理资源
449
+ */
450
+ public dispose(): void {
451
+ // 关闭所有iframe
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
+ // 生成UUID函数
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
+ }
@@ -0,0 +1,45 @@
1
+ import { SpatialObject } from '../SpatialObject'
2
+ import {
3
+ CreateAttachmentEntityCommand,
4
+ UpdateAttachmentEntityCommand,
5
+ } from '../JSBCommand'
6
+ import {
7
+ AttachmentEntityOptions,
8
+ AttachmentEntityUpdateOptions,
9
+ } from '../types/types'
10
+
11
+ export class Attachment extends SpatialObject {
12
+ constructor(
13
+ id: string,
14
+ private readonly windowProxy: WindowProxy,
15
+ private options: AttachmentEntityOptions,
16
+ ) {
17
+ super(id)
18
+ }
19
+
20
+ getContainer(): HTMLElement {
21
+ return (this.windowProxy as Window).document.body
22
+ }
23
+
24
+ getWindowProxy(): WindowProxy {
25
+ return this.windowProxy
26
+ }
27
+
28
+ async update(options: AttachmentEntityUpdateOptions) {
29
+ if (this.isDestroyed) return
30
+ if (options.position) this.options.position = options.position
31
+ if (options.size) this.options.size = options.size
32
+ return new UpdateAttachmentEntityCommand(this.id, options).execute()
33
+ }
34
+ }
35
+
36
+ export async function createAttachmentEntity(
37
+ options: AttachmentEntityOptions,
38
+ ): Promise<Attachment> {
39
+ const result = await new CreateAttachmentEntityCommand(options).execute()
40
+ if (!result.success) {
41
+ throw new Error('createAttachmentEntity failed: ' + result?.errorMessage)
42
+ }
43
+ const { id, windowProxy } = result.data!
44
+ return new Attachment(id, windowProxy, options)
45
+ }
@@ -3,3 +3,4 @@ export * from './component'
3
3
  export * from './material'
4
4
  export * from './geometry'
5
5
  export * from './resource'
6
+ export * from './Attachment'
@@ -344,12 +344,14 @@ export class CubeInfo {
344
344
 
345
345
  export interface SpatialTapEventDetail {
346
346
  location3D: Point3D
347
+ globalLocation3D?: Point3D
347
348
  }
348
349
 
349
350
  export type SpatialTapEvent = CustomEvent<SpatialTapEventDetail>
350
351
 
351
352
  export interface SpatialDragStartEventDetail {
352
353
  startLocation3D: Point3D
354
+ globalLocation3D?: Point3D
353
355
  }
354
356
 
355
357
  export interface SpatialDragEventDetail {
@@ -384,3 +386,14 @@ export type SpatialMagnifyEvent = CustomEvent<SpatialMagnifyEventDetail>
384
386
  export type SpatialMagnifyEndEvent = CustomEvent<SpatialMagnifyEndEventDetail>
385
387
 
386
388
  export type SpatialEntityOrReality = SpatialEntity | SpatializedDynamic3DElement
389
+
390
+ export interface AttachmentEntityOptions {
391
+ parentEntityId: string
392
+ position?: [number, number, number]
393
+ size: { width: number; height: number }
394
+ }
395
+
396
+ export interface AttachmentEntityUpdateOptions {
397
+ position?: [number, number, number]
398
+ size?: { width: number; height: number }
399
+ }