@webspatial/core-sdk 1.3.0 → 1.5.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,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
+ }
@@ -40,8 +40,8 @@ export function createPlatform(): PlatformAbility {
40
40
  userAgent.includes('PicoWebApp') &&
41
41
  isVersionGreater(webSpatialVersion, [0, 0, 1])
42
42
  ) {
43
- const XRPlatform = require('./xr/XRPlatform').XRPlatform
44
- return new XRPlatform()
43
+ const PicoOSPlatform = require('./pico-os/PicoOSPlatform').PicoOSPlatform
44
+ return new PicoOSPlatform()
45
45
  } else if (userAgent.includes('Android') || userAgent.includes('Linux')) {
46
46
  const AndroidPlatform = require('./android/AndroidPlatform').AndroidPlatform
47
47
  return new AndroidPlatform()
@@ -23,9 +23,10 @@ function nextRequestId() {
23
23
  return `rId_${requestId}`
24
24
  }
25
25
 
26
- export class XRPlatform implements PlatformAbility {
26
+ // Only supports Pico OS 6
27
+ export class PicoOSPlatform implements PlatformAbility {
27
28
  async callJSB(cmd: string, msg: string): Promise<CommandResult> {
28
- // android JS Bridge interface only support sync invoking
29
+ // swan JS Bridge interface only support sync invoking
29
30
  // in order to implement promise API, register every request by requestId and remove when resolve/reject.
30
31
  return new Promise((resolve, reject) => {
31
32
  try {
@@ -54,7 +55,7 @@ export class XRPlatform implements PlatformAbility {
54
55
  }
55
56
  }
56
57
  } catch (error: unknown) {
57
- console.error(`XRPlatform cmd: ${cmd}, msg: ${msg} error: ${error}`)
58
+ console.error(`SwanPlatform cmd: ${cmd}, msg: ${msg} error: ${error}`)
58
59
  const { code, message } = error as JSBError
59
60
  resolve(CommandResultFailure(code, message))
60
61
  }
@@ -75,7 +76,6 @@ export class XRPlatform implements PlatformAbility {
75
76
  SpatialWebEvent.addEventReceiver(
76
77
  createdId,
77
78
  (result: { spatialId: string }) => {
78
- console.log('createdId', createdId, result.spatialId)
79
79
  resolve(
80
80
  CommandResultSuccess({
81
81
  windowProxy: windowProxy,
@@ -87,13 +87,11 @@ export class XRPlatform implements PlatformAbility {
87
87
  )
88
88
  windowProxy = this.openWindow(
89
89
  command,
90
- query,
90
+ 'rid=' + createdId,
91
91
  target,
92
92
  features,
93
93
  ).windowProxy
94
- windowProxy?.open(`about:blank?rid=${createdId}`, '_self')
95
94
  } catch (error: unknown) {
96
- console.error(`open window error: ${error}`)
97
95
  const { code, message } = error as JSBError
98
96
  SpatialWebEvent.removeEventReceiver(createdId)
99
97
  resolve(CommandResultFailure(code, message))
@@ -4,7 +4,7 @@ import {
4
4
  CommandResultSuccess,
5
5
  } from '../CommandResultUtils'
6
6
 
7
- // 添加window扩展接口
7
+ // add window interface for JSB call
8
8
  declare global {
9
9
  interface Window {
10
10
  __handleJSBMessage: (message: string) => any
@@ -24,7 +24,7 @@ type JSBError = {
24
24
  console.log('PuppeteerPlatform')
25
25
 
26
26
  export class PuppeteerPlatform implements PlatformAbility {
27
- // 存储iframe实例
27
+ // store iframe instance
28
28
  private iframeRegistry: Map<string, HTMLIFrameElement> = new Map()
29
29
 
30
30
  constructor() {}
@@ -32,7 +32,7 @@ export class PuppeteerPlatform implements PlatformAbility {
32
32
  callJSB(cmd: string, msg: string): Promise<CommandResult> {
33
33
  return new Promise(resolve => {
34
34
  try {
35
- // 检查__handleJSBMessage是否存在
35
+ // check __handleJSBMessage exist
36
36
  if (window.__handleJSBMessage) {
37
37
  try {
38
38
  console.log(` core-sdk Puppeteer Platform: callJSB: ${cmd}::${msg}`)
@@ -45,7 +45,7 @@ export class PuppeteerPlatform implements PlatformAbility {
45
45
  resolve(CommandResultFailure('500', 'JSB execution error'))
46
46
  }
47
47
  } else {
48
- // 如果不存在,返回默认结果
48
+ // if not exist, return default result
49
49
  resolve(CommandResultSuccess('ok'))
50
50
  }
51
51
  } catch (error: unknown) {
@@ -58,7 +58,7 @@ export class PuppeteerPlatform implements PlatformAbility {
58
58
  }
59
59
 
60
60
  /**
61
- * 同步创建Spatialized2DElementPuppeteer Runner
61
+ * Synchronously create Spatialized2DElement to Puppeteer Runner
62
62
  */
63
63
  private createSpatializedElementSync(
64
64
  spatialId: string,
@@ -68,10 +68,10 @@ export class PuppeteerPlatform implements PlatformAbility {
68
68
  console.log(
69
69
  `[Puppeteer Platform] Creating spatialized element sync with id: ${spatialId}, url: ${webspatialUrl}`,
70
70
  )
71
- // 直接调用Puppeteer Runner的方法来创建元素
71
+ // directly call Puppeteer Runner method to create element
72
72
  const win = window as any
73
73
  if (win.__handleJSBMessage) {
74
- // 使用更简洁的格式,确保JSBManager能正确使用我们传递的spatialId
74
+ // use simpler format to ensure JSBManager can correctly use our passed spatialId
75
75
  const createCommand = {
76
76
  id: spatialId,
77
77
  url: webspatialUrl,
@@ -96,9 +96,9 @@ export class PuppeteerPlatform implements PlatformAbility {
96
96
  )
97
97
  return new Promise(resolve => {
98
98
  try {
99
- // 创建完整的webspatial URL
99
+ // create complete webspatial URL
100
100
  const webspatialUrl = `webspatial://${command}${query ? `?${query}` : ''}`
101
- // 使用iframe创建新窗口
101
+ // use iframe to create new window
102
102
  const { spatialId, iframe, windowProxy } = this.createIframeWindow(
103
103
  webspatialUrl,
104
104
  target,
@@ -112,7 +112,7 @@ export class PuppeteerPlatform implements PlatformAbility {
112
112
  console.log(
113
113
  `[Puppeteer Platform] iframe created with spatialId: ${spatialId}`,
114
114
  )
115
- // 注册iframe
115
+ // store iframe instance
116
116
  this.iframeRegistry.set(spatialId, iframe)
117
117
  resolve(CommandResultSuccess({ windowProxy, id: spatialId }))
118
118
  } catch (error) {
@@ -131,7 +131,7 @@ export class PuppeteerPlatform implements PlatformAbility {
131
131
  features?: string,
132
132
  ): CommandResult {
133
133
  try {
134
- // 创建完整的webspatial URL
134
+ // create complete webspatial URL
135
135
  const webspatialUrl = `webspatial://${command}${query ? `?${query}` : ''}`
136
136
  console.log(`Calling webspatial protocol sync: ${webspatialUrl}`)
137
137
 
@@ -147,7 +147,7 @@ export class PuppeteerPlatform implements PlatformAbility {
147
147
  this.createSpatializedElementSync(spatialId, webspatialUrl)
148
148
  }
149
149
 
150
- // 注册iframe
150
+ // store iframe instance
151
151
  this.iframeRegistry.set(spatialId, iframe)
152
152
 
153
153
  return CommandResultSuccess({ windowProxy, id: spatialId })
@@ -161,27 +161,27 @@ export class PuppeteerPlatform implements PlatformAbility {
161
161
  }
162
162
 
163
163
  /**
164
- * 创建基于iframe的窗口
164
+ * Synchronously create iframe-based window
165
165
  */
166
166
  private createIframeWindow(url: string, target?: string, features?: string) {
167
- // 创建iframe元素
167
+ // create iframe element
168
168
  const iframe = document.createElement('iframe')
169
169
 
170
- // 设置iframe属性
170
+ // set iframe attributes
171
171
  iframe.style.border = 'none'
172
- iframe.style.display = 'none' // 初始隐藏
172
+ iframe.style.display = 'none'
173
173
  iframe.style.width = '100%'
174
174
  iframe.style.height = '100%'
175
175
 
176
- // 生成唯一的spatialId
176
+ // set iframe id
177
177
  const spatialId = this.generateUUID()
178
178
  iframe.spatialId = spatialId
179
179
  iframe.id = `spatial-iframe-${spatialId}`
180
180
 
181
- // 解析features参数
181
+ // parse features parameter
182
182
  const featuresObj = this.parseFeatures(features || '')
183
183
 
184
- // 根据features设置iframe样式
184
+ // set iframe styles based on features
185
185
  if (featuresObj.width) {
186
186
  iframe.style.width = featuresObj.width
187
187
  }
@@ -197,36 +197,36 @@ export class PuppeteerPlatform implements PlatformAbility {
197
197
  iframe.style.position = 'absolute'
198
198
  }
199
199
 
200
- // 添加iframeDOM
200
+ // add iframe to DOM
201
201
  document.body.appendChild(iframe)
202
202
 
203
- // 创建增强的windowProxy模拟对象
203
+ // create enhanced windowProxy object
204
204
  const windowProxy = this.createEnhancedWindowProxy(iframe, url, spatialId)
205
205
 
206
- // 设置iframesrc
206
+ // set iframe src
207
207
  iframe.src = 'about:blank'
208
208
 
209
209
  console.log(
210
210
  `PuppeteerPlatform created iframe window with spatialId: ${spatialId}, URL: ${url}`,
211
211
  )
212
212
 
213
- // 初始化iframe内容
213
+ // initialize iframe content
214
214
  this.initializeIframeContent(iframe, url, spatialId)
215
215
 
216
216
  return { spatialId, iframe, windowProxy }
217
217
  }
218
218
 
219
219
  /**
220
- * 创建增强的windowProxy对象
220
+ * create enhanced windowProxy object
221
221
  */
222
222
  private createEnhancedWindowProxy(
223
223
  iframe: HTMLIFrameElement,
224
224
  url: string,
225
225
  spatialId: string,
226
226
  ) {
227
- // 创建增强的windowProxy模拟对象
227
+ // create enhanced windowProxy object
228
228
  return {
229
- // 基本属性
229
+ // basic properties
230
230
  location: {
231
231
  href: url,
232
232
  toString: () => url,
@@ -240,25 +240,25 @@ export class PuppeteerPlatform implements PlatformAbility {
240
240
  userAgent: `Mozilla/5.0 (WebKit) SpatialId/${spatialId}`,
241
241
  },
242
242
 
243
- // 方法
243
+ // methods
244
244
  close: () => {
245
245
  console.log(`Closing iframe with spatialId: ${spatialId}`)
246
246
  iframe.remove()
247
247
  this.iframeRegistry.delete(spatialId)
248
248
  },
249
249
 
250
- // 文档访问
250
+ // document access
251
251
  document: iframe.contentDocument || ({} as Document),
252
252
  contentWindow: iframe.contentWindow || ({} as Window),
253
253
 
254
- // 添加消息通信方法
254
+ // add message communication method
255
255
  postMessage: (message: any, targetOrigin?: string) => {
256
256
  if (iframe.contentWindow) {
257
257
  iframe.contentWindow.postMessage(message, targetOrigin || '*')
258
258
  }
259
259
  },
260
260
 
261
- // 添加事件监听方法
261
+ // add event listener method
262
262
  addEventListener: (
263
263
  type: string,
264
264
  listener: EventListenerOrEventListenerObject,
@@ -277,11 +277,11 @@ export class PuppeteerPlatform implements PlatformAbility {
277
277
  }
278
278
  },
279
279
 
280
- // 执行JavaScript
280
+ // execute JavaScript
281
281
  executeScript: (code: string): any => {
282
282
  if (iframe.contentWindow) {
283
283
  try {
284
- // 使用类型断言和更安全的方式执行脚本
284
+ // use type assertion and safer way to execute script
285
285
  const win = iframe.contentWindow as any
286
286
  return win.eval(code)
287
287
  } catch (error) {
@@ -295,16 +295,16 @@ export class PuppeteerPlatform implements PlatformAbility {
295
295
  return null
296
296
  },
297
297
 
298
- // 获取iframe引用
298
+ // get iframe reference
299
299
  getIframe: () => iframe,
300
300
 
301
- // 获取spatialId
301
+ // get spatialId
302
302
  getSpatialId: () => spatialId,
303
303
  }
304
304
  }
305
305
 
306
306
  /**
307
- * 初始化iframe内容
307
+ * initialize iframe content
308
308
  */
309
309
  private initializeIframeContent(
310
310
  iframe: HTMLIFrameElement,
@@ -312,20 +312,20 @@ export class PuppeteerPlatform implements PlatformAbility {
312
312
  spatialId: string,
313
313
  ): void {
314
314
  try {
315
- // 等待iframe加载完成
315
+ // wait for iframe to load
316
316
  iframe.onload = () => {
317
317
  try {
318
- // 设置iframe内容
318
+ // set iframe content
319
319
  const iframeContent = `
320
- // 注入通信脚本
320
+ // inject communication script
321
321
  window.webSpatialId = '${spatialId}';
322
322
  window.SpatialId = '${spatialId}';
323
323
 
324
- // 重写window.open以支持webspatial协议
324
+ // override window.open to support webspatial protocol
325
325
  const originalOpen = window.open;
326
326
  window.open = function(url, target, features) {
327
327
  if (url && url.startsWith('webspatial://')) {
328
- // 通过windowProxy处理webspatial协议
328
+ // handle webspatial protocol through windowProxy
329
329
  const windowProxy = new Proxy({}, {
330
330
  get: function(target, prop) {
331
331
  if (prop === 'toString') {
@@ -339,33 +339,33 @@ export class PuppeteerPlatform implements PlatformAbility {
339
339
  return originalOpen.call(window, url, target, features);
340
340
  };
341
341
 
342
- // 设置navigator.userAgent以识别webspatial环境
342
+ // set navigator.userAgent to identify webspatial environment
343
343
  Object.defineProperty(navigator, 'userAgent', {
344
344
  value: 'WebSpatial/1.0 ' + navigator.userAgent,
345
345
  configurable: true
346
346
  });
347
347
 
348
- // 发送加载完成消息
348
+ // send loaded message
349
349
  window.parent.postMessage({
350
350
  type: 'iframe_loaded',
351
351
  spatialId: '${spatialId}',
352
352
  url: '${url}'
353
353
  }, '${window.location.origin}');
354
354
 
355
- // 设置消息处理器
355
+ // set message handler
356
356
  window.addEventListener('message', (event) => {
357
357
  if (event.origin !== window.parent.location.origin) return;
358
358
 
359
359
  const data = event.data;
360
360
  if (data && data.type === 'webspatial_command') {
361
- // 处理来自父窗口的命令
361
+ // handle command from parent window
362
362
  console.log('Received command in iframe from parent:', data.command);
363
- // 这里可以添加命令处理逻辑
363
+ // add command handling logic here
364
364
  }
365
365
  });
366
366
  `
367
367
 
368
- // 使用document.write代替eval,更安全且符合类型定义
368
+ // use document.write instead of eval for security and type compliance
369
369
  const doc = iframe.contentDocument
370
370
  if (doc) {
371
371
  doc.open()
@@ -400,7 +400,7 @@ export class PuppeteerPlatform implements PlatformAbility {
400
400
  }
401
401
 
402
402
  /**
403
- * 解析features字符串为对象
403
+ * parse features string to object
404
404
  */
405
405
  private parseFeatures(features: string): Record<string, string> {
406
406
  const result: Record<string, string> = {}
@@ -417,7 +417,7 @@ export class PuppeteerPlatform implements PlatformAbility {
417
417
  }
418
418
 
419
419
  /**
420
- * 发送消息到指定spatialId的iframe
420
+ * send message to iframe with specified spatialId
421
421
  */
422
422
  public sendMessageToIframe(spatialId: string, message: any): boolean {
423
423
  const iframe = this.iframeRegistry.get(spatialId)
@@ -429,7 +429,7 @@ export class PuppeteerPlatform implements PlatformAbility {
429
429
  }
430
430
 
431
431
  /**
432
- * 获取所有活跃的iframe
432
+ * get all active iframes
433
433
  */
434
434
  public getAllActiveIframes(): Array<{
435
435
  spatialId: string
@@ -445,10 +445,10 @@ export class PuppeteerPlatform implements PlatformAbility {
445
445
  }
446
446
 
447
447
  /**
448
- * 清理资源
448
+ * dispose all active iframes
449
449
  */
450
450
  public dispose(): void {
451
- // 关闭所有iframe
451
+ // close all iframes
452
452
  this.iframeRegistry.forEach((iframe, spatialId) => {
453
453
  console.log(`Disposing iframe with spatialId: ${spatialId}`)
454
454
  iframe.remove()
@@ -456,7 +456,7 @@ export class PuppeteerPlatform implements PlatformAbility {
456
456
  this.iframeRegistry.clear()
457
457
  }
458
458
 
459
- // 生成UUID函数
459
+ // generate UUID function
460
460
  private generateUUID(): string {
461
461
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
462
462
  /[xy]/g,
@@ -2,6 +2,7 @@ import { SpatialObject } from '../SpatialObject'
2
2
  import {
3
3
  CreateAttachmentEntityCommand,
4
4
  UpdateAttachmentEntityCommand,
5
+ InitializeAttachmentCommand,
5
6
  } from '../JSBCommand'
6
7
  import {
7
8
  AttachmentEntityOptions,
@@ -41,5 +42,6 @@ export async function createAttachmentEntity(
41
42
  throw new Error('createAttachmentEntity failed: ' + result?.errorMessage)
42
43
  }
43
44
  const { id, windowProxy } = result.data!
45
+ await new InitializeAttachmentCommand(id, options).execute()
44
46
  return new Attachment(id, windowProxy, options)
45
47
  }
@@ -13,6 +13,7 @@ import {
13
13
  import {
14
14
  AddComponentToEntityCommand,
15
15
  AddEntityToEntityCommand,
16
+ RemoveComponentFromEntityCommand,
16
17
  RemoveEntityFromParentCommand,
17
18
  UpdateEntityEventCommand,
18
19
  UpdateEntityPropertiesCommand,
@@ -23,16 +24,16 @@ import { SpatialComponent } from '../component/SpatialComponent'
23
24
  import { SpatialWebEvent } from '../../SpatialWebEvent'
24
25
  import { createSpatialEvent } from '../../SpatialWebEventCreator'
25
26
  import {
26
- CubeInfoMsg,
27
27
  ObjectDestroyMsg,
28
28
  SpatialDragEndMsg,
29
29
  SpatialDragMsg,
30
+ SpatialDragStartMsg,
31
+ SpatialMagnifyEndMsg,
30
32
  SpatialMagnifyMsg,
31
33
  SpatialRotateEndMsg,
32
34
  SpatialRotateMsg,
33
35
  SpatialTapMsg,
34
36
  SpatialWebMsgType,
35
- TransformMsg,
36
37
  } from '../../WebMsgCommand'
37
38
 
38
39
  export class SpatialEntity extends SpatialObject {
@@ -43,6 +44,31 @@ export class SpatialEntity extends SpatialObject {
43
44
  events: Record<string, (data: any) => void> = {}
44
45
  children: SpatialEntity[] = []
45
46
  parent: SpatialEntityOrReality | null = null
47
+ private _enableInput: boolean = false
48
+
49
+ get enableInput(): boolean {
50
+ return this._enableInput
51
+ }
52
+
53
+ set enableInput(value: boolean) {
54
+ // Why enabling only 'spatialtap' makes the entity interactive:
55
+ // - On the native (Swift/RealityKit) side, SpatialEntity.updateGesture(type, isEnable)
56
+ // toggles per-gesture flags. Then enableInteractive = enableTap || enableRotate || enableDrag || enableMagnify.
57
+ // - As soon as any gesture (e.g., 'spatialtap') is enabled, enableInteractive becomes true and
58
+ // InputTargetComponent is attached, making the entity targetable by targetedToAnyEntity().
59
+ // - The view layer forwards hit gestures to the web, so enabling 'spatialtap' is sufficient to
60
+ // make the entity targetable; enable additional gestures only when needed.
61
+ if (this._enableInput === value) return
62
+ this._enableInput = value
63
+ void this.updateEntityEvent('spatialtap', value).catch(err => {
64
+ console.error('enableInput updateEntityEvent failed', 'spatialtap', err)
65
+ // Roll back local flag if the native toggle fails to keep web/native states consistent.
66
+ // Otherwise, the web side would think the entity is interactive while RealityKit is not.
67
+ if (this._enableInput === value) {
68
+ this._enableInput = !value
69
+ }
70
+ })
71
+ }
46
72
  constructor(
47
73
  id: string,
48
74
  public userData?: SpatialEntityUserData,
@@ -54,6 +80,9 @@ export class SpatialEntity extends SpatialObject {
54
80
  async addComponent(component: SpatialComponent) {
55
81
  return new AddComponentToEntityCommand(this, component).execute()
56
82
  }
83
+ async removeComponent(component: SpatialComponent) {
84
+ return new RemoveComponentFromEntityCommand(this, component).execute()
85
+ }
57
86
  async setPosition(position: Vec3) {
58
87
  return this.updateTransform({ position })
59
88
  }
@@ -123,13 +152,15 @@ export class SpatialEntity extends SpatialObject {
123
152
  return new UpdateEntityEventCommand(this, eventName, isEnable).execute()
124
153
  }
125
154
  private onReceiveEvent = (
126
- data: // | CubeInfoMsg
127
- // | TransformMsg
128
- | SpatialTapMsg
129
- // | SpatialDragMsg
130
- // | SpatialDragEndMsg
131
- // | SpatialRotateMsg
132
- // | SpatialRotateEndMsg
155
+ data:
156
+ | SpatialTapMsg
157
+ | SpatialDragStartMsg
158
+ | SpatialDragMsg
159
+ | SpatialDragEndMsg
160
+ | SpatialMagnifyMsg
161
+ | SpatialMagnifyEndMsg
162
+ | SpatialRotateMsg
163
+ | SpatialRotateEndMsg
133
164
  | ObjectDestroyMsg,
134
165
  ) => {
135
166
  // console.log('SpatialEntityEvent', data)
@@ -139,27 +170,21 @@ export class SpatialEntity extends SpatialObject {
139
170
  }
140
171
  // tap
141
172
  else if (type === SpatialWebMsgType.spatialtap) {
142
- const evt = createSpatialEvent(
143
- SpatialWebMsgType.spatialtap,
144
- (data as SpatialTapMsg).detail,
145
- )
173
+ const evt = createSpatialEvent(SpatialWebMsgType.spatialtap, data.detail)
146
174
  this.dispatchEvent(evt)
147
175
  } else if (type === SpatialWebMsgType.spatialdragstart) {
148
176
  const evt = createSpatialEvent(
149
177
  SpatialWebMsgType.spatialdragstart,
150
- (data as SpatialDragMsg).detail,
178
+ data.detail,
151
179
  )
152
180
  this.dispatchEvent(evt)
153
181
  } else if (type === SpatialWebMsgType.spatialdrag) {
154
- const evt = createSpatialEvent(
155
- SpatialWebMsgType.spatialdrag,
156
- (data as SpatialDragMsg).detail,
157
- )
182
+ const evt = createSpatialEvent(SpatialWebMsgType.spatialdrag, data.detail)
158
183
  this.dispatchEvent(evt)
159
184
  } else if (type === SpatialWebMsgType.spatialdragend) {
160
185
  const evt = createSpatialEvent(
161
186
  SpatialWebMsgType.spatialdragend,
162
- (data as SpatialDragEndMsg).detail,
187
+ data.detail,
163
188
  )
164
189
  this.dispatchEvent(evt)
165
190
  }
@@ -167,13 +192,13 @@ export class SpatialEntity extends SpatialObject {
167
192
  else if (type === SpatialWebMsgType.spatialrotate) {
168
193
  const evt = createSpatialEvent(
169
194
  SpatialWebMsgType.spatialrotate,
170
- (data as SpatialRotateMsg).detail,
195
+ data.detail,
171
196
  )
172
197
  this.dispatchEvent(evt)
173
198
  } else if (type === SpatialWebMsgType.spatialrotateend) {
174
199
  const evt = createSpatialEvent(
175
200
  SpatialWebMsgType.spatialrotateend,
176
- (data as SpatialRotateEndMsg).detail,
201
+ data.detail,
177
202
  )
178
203
  this.dispatchEvent(evt)
179
204
  }
@@ -181,13 +206,13 @@ export class SpatialEntity extends SpatialObject {
181
206
  else if (type === SpatialWebMsgType.spatialmagnify) {
182
207
  const evt = createSpatialEvent(
183
208
  SpatialWebMsgType.spatialmagnify,
184
- (data as SpatialMagnifyMsg).detail,
209
+ data.detail,
185
210
  )
186
211
  this.dispatchEvent(evt)
187
212
  } else if (type === SpatialWebMsgType.spatialmagnifyend) {
188
213
  const evt = createSpatialEvent(
189
214
  SpatialWebMsgType.spatialmagnifyend,
190
- (data as SpatialMagnifyMsg).detail,
215
+ data.detail,
191
216
  )
192
217
  this.dispatchEvent(evt)
193
218
  }
@@ -200,7 +225,7 @@ export class SpatialEntity extends SpatialObject {
200
225
  }
201
226
  this.events[evt.type]?.(evt)
202
227
  if (evt.bubbles && !evt.cancelBubble) {
203
- if (this.parent && this.parent instanceof SpatialEntity) {
228
+ if (this.parent) {
204
229
  this.parent.dispatchEvent(evt)
205
230
  }
206
231
  }