lecodes-sdk 1.2.0 → 2.0.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.
Files changed (260) hide show
  1. package/README.md +5 -2
  2. package/dist/editor.d.ts +12 -0
  3. package/dist/global.d.ts +4 -8
  4. package/dist/host.d.ts +2 -3
  5. package/dist/types/animate/tween/Animation.d.ts +0 -3
  6. package/dist/types/animate/tween/animateValue.d.ts +4 -2
  7. package/dist/types/animate/tween/easing.d.ts +8 -0
  8. package/dist/types/animate/tween/spec.d.ts +15 -7
  9. package/dist/types/audio/audio.d.ts +2 -1
  10. package/dist/types/canvas/Canvas.d.ts +40 -108
  11. package/dist/types/canvas/gen/cssColor.d.ts +17 -0
  12. package/dist/types/canvas/gen/recorder.d.ts +118 -0
  13. package/dist/types/canvas/gen/spec.d.ts +144 -0
  14. package/dist/types/core/color.d.ts +3 -1
  15. package/dist/types/core/pins.d.ts +18 -0
  16. package/dist/types/g2/Node2D.d.ts +5 -8
  17. package/dist/types/g2/Scene2D.d.ts +6 -2
  18. package/dist/types/gl/Foliage.d.ts +30 -7
  19. package/dist/types/gl/Light.d.ts +8 -0
  20. package/dist/types/gl/Lightmap.d.ts +13 -2
  21. package/dist/types/gl/Material.d.ts +17 -3
  22. package/dist/types/gl/Model.d.ts +6 -2
  23. package/dist/types/gl/Node.d.ts +3 -6
  24. package/dist/types/gl/Scene.d.ts +41 -13
  25. package/dist/types/gl/Texture.d.ts +1 -1
  26. package/dist/types/gl/animation/Locomotion.d.ts +8 -1
  27. package/dist/types/inject.d.ts +11 -11
  28. package/dist/types/inject.editor.d.ts +1 -0
  29. package/dist/types/net/core.d.ts +7 -0
  30. package/dist/types/plugin.d.ts +76 -0
  31. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  32. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  33. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  34. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  35. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  36. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  37. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  38. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  39. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  40. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  41. package/dist/types/runtime/app.d.ts +9 -2
  42. package/dist/types/runtime/fetch.d.ts +2 -0
  43. package/dist/types/runtime/input.d.ts +1 -1
  44. package/dist/types/runtime/media.d.ts +6 -10
  45. package/dist/types/runtime/misc.d.ts +4 -1
  46. package/dist/types/runtime/net.d.ts +3 -2
  47. package/dist/types/runtime/touch.d.ts +32 -0
  48. package/dist/types/scene/defineScene.d.ts +43 -2
  49. package/dist/types/scene/editor.d.ts +52 -0
  50. package/dist/types/scene/gizmos.d.ts +7 -4
  51. package/dist/types/ui/NativeView.d.ts +6 -4
  52. package/dist/types/ui/UI.d.ts +1 -1
  53. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  54. package/dist/types/ui/UIButton.d.ts +12 -14
  55. package/dist/types/ui/UIContainer.d.ts +0 -6
  56. package/dist/types/ui/UIImage.d.ts +1 -4
  57. package/dist/types/ui/UIInput.d.ts +8 -24
  58. package/dist/types/ui/UIModal.d.ts +0 -2
  59. package/dist/types/ui/UINode.d.ts +70 -56
  60. package/dist/types/ui/UIPager.d.ts +28 -27
  61. package/dist/types/ui/UIPopover.d.ts +0 -2
  62. package/dist/types/ui/UIScreen.d.ts +12 -17
  63. package/dist/types/ui/UIScrollable.d.ts +1 -4
  64. package/dist/types/ui/UIText.d.ts +0 -2
  65. package/dist/types/ui/UIVideo.d.ts +3 -5
  66. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  67. package/dist/types/ui/UIWidget.d.ts +10 -9
  68. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  69. package/dist/types/ui/presentable.d.ts +46 -32
  70. package/dist/types/ui/router.d.ts +18 -7
  71. package/dist/types/ui/styleColor.d.ts +1 -0
  72. package/dist/types/ui/transitions.d.ts +18 -0
  73. package/dist/types/ui/tree.d.ts +75 -0
  74. package/dist/types/version.d.ts +10 -0
  75. package/dist/types.json +1 -1
  76. package/package.json +12 -3
  77. package/prompts/2d.md +2 -6
  78. package/prompts/3d.md +1 -5
  79. package/prompts/README.md +1 -1
  80. package/prompts/canvas.md +9 -8
  81. package/prompts/compose.ts +1 -1
  82. package/prompts/core.md +3 -3
  83. package/prompts/design.md +1 -1
  84. package/prompts/dist/2d-game.md +473 -239
  85. package/prompts/dist/3d-app.md +553 -205
  86. package/prompts/dist/ar-app.md +435 -202
  87. package/prompts/dist/design.md +113 -95
  88. package/prompts/dist/ui-app.md +386 -170
  89. package/prompts/ui-design.md +2 -3
  90. package/prompts/ui.md +45 -37
  91. package/src/animate/tween/Animation.ts +34 -150
  92. package/src/animate/tween/Timeline.ts +175 -175
  93. package/src/animate/tween/animateValue.ts +6 -3
  94. package/src/animate/tween/easing.ts +10 -3
  95. package/src/animate/tween/spec.ts +41 -15
  96. package/src/audio/Sound.ts +3 -3
  97. package/src/audio/audio.ts +2 -1
  98. package/src/bridges/2d.d.ts +317 -0
  99. package/src/bridges/app.d.ts +91 -0
  100. package/src/bridges/audio.d.ts +97 -0
  101. package/src/bridges/canvas.d.ts +79 -0
  102. package/src/bridges/device.d.ts +72 -0
  103. package/src/bridges/fetch.d.ts +80 -0
  104. package/src/bridges/files.d.ts +70 -0
  105. package/src/bridges/gl.d.ts +1133 -0
  106. package/src/bridges/input.d.ts +72 -0
  107. package/src/bridges/media.d.ts +55 -0
  108. package/src/bridges/nav.d.ts +71 -0
  109. package/src/bridges/net.d.ts +52 -0
  110. package/src/bridges/service.d.ts +52 -0
  111. package/src/bridges/socket.d.ts +31 -0
  112. package/src/bridges/storage.d.ts +33 -0
  113. package/src/bridges/tree.d.ts +301 -0
  114. package/src/bridges/types.d.ts +49 -0
  115. package/src/canvas/Canvas.ts +114 -159
  116. package/src/canvas/gen/cssColor.ts +224 -0
  117. package/src/canvas/gen/recorder.ts +212 -0
  118. package/src/canvas/gen/spec.ts +201 -0
  119. package/src/chisel.ts +193 -0
  120. package/src/compile/assetMacro.ts +1 -1
  121. package/src/compile/bundler.ts +11 -2
  122. package/src/compile/compileProject.ts +43 -4
  123. package/src/compile/fontMacro.ts +3 -4
  124. package/src/compile/header.ts +26 -5
  125. package/src/compile/index.ts +3 -1
  126. package/src/compile/liteMaterial.ts +1 -1
  127. package/src/compile/sceneEditor.ts +11 -26
  128. package/src/core/color.ts +73 -30
  129. package/src/core/pins.ts +51 -0
  130. package/src/core/signals.ts +8 -1
  131. package/src/g2/CharacterController2D.ts +3 -3
  132. package/src/g2/Node2D.ts +57 -39
  133. package/src/g2/Physics2D.ts +2 -2
  134. package/src/g2/Scene2D.ts +35 -23
  135. package/src/g2/Texture2D.ts +1 -1
  136. package/src/g2/loop.ts +4 -4
  137. package/src/gl/CameraPlace.ts +52 -52
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +10 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Mesh.ts +120 -120
  144. package/src/gl/Model.ts +21 -16
  145. package/src/gl/Node.ts +91 -24
  146. package/src/gl/Particles.ts +1 -1
  147. package/src/gl/Scene.ts +100 -44
  148. package/src/gl/Texture.ts +8 -7
  149. package/src/gl/animation/AnimationClip.ts +1 -1
  150. package/src/gl/animation/DynamicBone.ts +482 -482
  151. package/src/gl/animation/Locomotion.ts +8 -3
  152. package/src/gl/nav/NavMesh.ts +3 -4
  153. package/src/gl/physics/Physics.ts +2 -2
  154. package/src/gl/physics/physicsEvents.ts +3 -3
  155. package/src/gl/scenarios.ts +291 -291
  156. package/src/gl/terrain/Terrain.ts +4 -5
  157. package/src/gl/touch.ts +14 -15
  158. package/src/host.d.ts +2 -3
  159. package/src/inject.editor.ts +7 -0
  160. package/src/inject.ts +13 -16
  161. package/src/net/core.ts +6 -5
  162. package/src/net/index.ts +1 -1
  163. package/src/net/replication.ts +1 -1
  164. package/src/plugin.ts +191 -0
  165. package/src/plugins/gen/camera/contract.d.ts +27 -0
  166. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  167. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  168. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  169. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  170. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  171. package/src/plugins/gen/map/contract.d.ts +144 -0
  172. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  173. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  174. package/src/plugins/gen/push/contract.d.ts +61 -0
  175. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  176. package/src/plugins/gen/push/sdk/push.ts +105 -0
  177. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  178. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  179. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  180. package/src/plugins/permission.ts +5 -4
  181. package/src/runtime/app.ts +20 -9
  182. package/src/runtime/appEvents.ts +5 -4
  183. package/src/runtime/channel.ts +18 -15
  184. package/src/runtime/clipboard.ts +4 -3
  185. package/src/runtime/datetime.ts +2 -1
  186. package/src/runtime/device.ts +17 -15
  187. package/src/runtime/fetch.ts +30 -20
  188. package/src/runtime/files.ts +16 -15
  189. package/src/runtime/input.ts +12 -10
  190. package/src/runtime/media.ts +50 -46
  191. package/src/runtime/misc.ts +7 -3
  192. package/src/runtime/net.ts +8 -7
  193. package/src/runtime/rpc.ts +1 -3
  194. package/src/runtime/service.ts +19 -14
  195. package/src/runtime/share.ts +4 -3
  196. package/src/runtime/storage.ts +6 -4
  197. package/src/runtime/touch.ts +32 -0
  198. package/src/scene/defineScene.ts +61 -365
  199. package/src/scene/editor.ts +408 -0
  200. package/src/scene/editorPlugins.ts +3 -3
  201. package/src/scene/gizmos.ts +15 -9
  202. package/src/server/db/marci/query.ts +1 -1
  203. package/src/server/host.ts +1 -1
  204. package/src/server/runtime.ts +1 -1
  205. package/src/ui/NativeView.ts +72 -25
  206. package/src/ui/UI.ts +3 -3
  207. package/src/ui/UIBottomSheet.ts +16 -17
  208. package/src/ui/UIButton.ts +54 -16
  209. package/src/ui/UIContainer.ts +0 -6
  210. package/src/ui/UIImage.ts +34 -30
  211. package/src/ui/UIInput.ts +29 -37
  212. package/src/ui/UIModal.ts +1 -3
  213. package/src/ui/UINode.ts +347 -297
  214. package/src/ui/UIPager.ts +93 -78
  215. package/src/ui/UIPopover.ts +0 -2
  216. package/src/ui/UIScreen.ts +41 -36
  217. package/src/ui/UIScrollable.ts +19 -13
  218. package/src/ui/UISpacer.ts +1 -1
  219. package/src/ui/UITabs.ts +8 -6
  220. package/src/ui/UIText.ts +7 -17
  221. package/src/ui/UIVideo.ts +30 -27
  222. package/src/ui/UIVirtualizedList.ts +58 -59
  223. package/src/ui/UIWidget.ts +38 -18
  224. package/src/ui/colorKeys.gen.ts +37 -0
  225. package/src/ui/fonts.ts +2 -2
  226. package/src/ui/presentable.ts +59 -42
  227. package/src/ui/router.ts +52 -35
  228. package/src/ui/styleColor.ts +56 -0
  229. package/src/ui/theme.ts +7 -6
  230. package/src/ui/transitions.ts +249 -0
  231. package/src/ui/tree.ts +346 -0
  232. package/src/version.ts +24 -0
  233. package/tests/helpers/engineWorld.ts +11 -0
  234. package/tests/helpers/fakeTree.ts +353 -0
  235. package/tests/helpers/hostStubs.ts +31 -0
  236. package/tests/helpers/index.ts +14 -0
  237. package/tests/helpers/memoryMarci.ts +124 -0
  238. package/tests/helpers/phases.ts +23 -0
  239. package/tests/helpers/preload.ts +18 -0
  240. package/tests/helpers/stubApp.ts +2 -0
  241. package/tests/helpers/stubDevice.ts +2 -0
  242. package/tests/helpers/stubFetch.ts +2 -0
  243. package/tests/helpers/stubInput.ts +2 -0
  244. package/dist/inject.js +0 -4629
  245. package/dist/types/core/registry.d.ts +0 -7
  246. package/dist/types/plugins/camera.d.ts +0 -25
  247. package/dist/types/plugins/push.d.ts +0 -46
  248. package/src/bridges.d.ts +0 -1769
  249. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  250. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  251. package/src/compile/__tests__/assetName.test.ts +0 -55
  252. package/src/compile/__tests__/compile.test.ts +0 -310
  253. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  254. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  255. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  256. package/src/core/__tests__/stateMachine.test.ts +0 -132
  257. package/src/core/registry.ts +0 -23
  258. package/src/plugins/camera.ts +0 -81
  259. package/src/plugins/push.ts +0 -132
  260. package/src/plugins/qr.ts +0 -73
@@ -2,7 +2,9 @@
2
2
  // "pause"/"resume", never Activity/Scene vocabulary — and the current values are synchronous pulls
3
3
  // so render-loop code can read them without subscribing.
4
4
 
5
+ import { setAppBackHandler } from "../ui/tree"
5
6
  import { appEventsOn, appEventsOff, keyboardEventsOn, keyboardEventsOff } from "./appEvents"
7
+ import { SDK_VERSION } from "../version"
6
8
 
7
9
  export type AppState = "active" | "background"
8
10
 
@@ -26,15 +28,19 @@ type AppEventMap = {
26
28
  }
27
29
 
28
30
  export const app = {
31
+ /** The SDK this app was compiled with (semver, e.g. `"2.0.0"`). Its major is the bundle ↔ runtime
32
+ * contract: a host runs only bundles of its own major, and the launchers send it as `?sdk=` when
33
+ * they fetch a published bundle — the platform keeps one bundle per major. */
34
+ sdkVersion: SDK_VERSION,
29
35
  /** Current lifecycle state. `"background"` while the app is not the foreground app / the tab is
30
36
  * hidden. `"active"` on hosts that don't track it. */
31
37
  get state(): AppState {
32
- return _creatorUtils.appState?.() === "background" ? "background" : "active"
38
+ return _creatorApp.state?.() === "background" ? "background" : "active"
33
39
  },
34
40
  /** The URL the app was (most recently) opened with — the deep link — or `null` for a plain
35
41
  * launch. Warm links update it and fire the `"url"` event. */
36
42
  get launchUrl(): string | null {
37
- return _creatorUtils.getLaunchUrl?.() ?? null
43
+ return _creatorApp.launchUrl?.() ?? null
38
44
  },
39
45
  /** On-screen keyboard height in logical px currently overlapping the app viewport, `0` when
40
46
  * hidden. This is the RAW overlap, independent of the focused input's `keyboardShrink` policy —
@@ -42,7 +48,7 @@ export const app = {
42
48
  * mode (`keyboardShrink: false`), e.g. a chat composer's `pb`. `0` on hosts without an
43
49
  * on-screen keyboard. */
44
50
  get keyboardHeight(): number {
45
- return _creatorUtils.keyboardHeight?.() ?? 0
51
+ return (typeof _creatorInput !== "undefined" ? _creatorInput.keyboardHeight?.() : undefined) ?? 0
46
52
  },
47
53
  /** Lock the screen orientation — `"landscape"` for a horizontal game (the device rotates to
48
54
  * landscape right away and stays there whichever way it is held), `"portrait"` for upright
@@ -51,29 +57,29 @@ export const app = {
51
57
  * ordinary resize — layouts reflow, a scene's viewport follows. Silent no-op on hosts
52
58
  * without a rotatable screen (desktop, headless, web). */
53
59
  setOrientation(mode: OrientationLock): void {
54
- _creatorUtils.setOrientation?.(mode)
60
+ _creatorApp.setOrientation?.(mode)
55
61
  },
56
62
  /** Re-run this app's bundle in a fresh world — the platform's `location.reload()`. Pass `url`
57
63
  * to reboot with different launch arguments (the new run reads them via `app.launchUrl`),
58
64
  * `null` to reboot with a cleared one, or omit it to replay the current one. No-op on hosts
59
65
  * without world control (headless). */
60
66
  restart(url?: string | null): void {
61
- _creatorUtils.restart?.(url)
67
+ _creatorApp.restart?.(url)
62
68
  },
63
69
  /** Leave this app. Inside the LeCodes viewer that returns to the launcher (the LeCodes home
64
70
  * screen); a standalone-built app leaves to the platform instead — Android backgrounds it,
65
71
  * iOS ignores the call (the platform forbids programmatic exit). */
66
72
  quit(): void {
67
- _creatorUtils.quit?.()
73
+ _creatorApp.quit?.()
68
74
  },
69
75
  /** Global back-press fallback (Android back button/gesture — iOS has no system back). Fires
70
76
  * only when nothing else consumed the press: no open widget/modal claimed it, the current
71
- * screen has no `onBackPressed`, every pager is at its tab root and the router is at its
77
+ * screen has no `onBack`, every pager is at its tab root and the router is at its
72
78
  * stack root. The place for app-wide policy — e.g. double-press-to-quit:
73
79
  *
74
80
  * ```ts
75
81
  * let armedAt = 0
76
- * app.onBackPressed(() => {
82
+ * app.onBack(() => {
77
83
  * if (Date.now() - armedAt < 2000) { app.quit(); return }
78
84
  * armedAt = Date.now()
79
85
  * toast("Press back again to exit")
@@ -81,8 +87,13 @@ export const app = {
81
87
  * ```
82
88
  *
83
89
  * Replaces the previous handler; pass `null` to remove. */
90
+ onBack(callback: (() => void) | null): void {
91
+ setAppBackHandler(callback ?? null)
92
+ },
93
+ /** @deprecated Renamed `onBack` (2026-09-26). An alias for the projects compiled before the
94
+ * rename; removed with the release that recompiles them. */
84
95
  onBackPressed(callback: (() => void) | null): void {
85
- _creatorUI._backButtonCallback = callback ?? undefined
96
+ setAppBackHandler(callback ?? null)
86
97
  },
87
98
  addEventListener<E extends keyof AppEventMap>(event: E, callback: AppEventMap[E]): void {
88
99
  if (event === "keyboard") {
@@ -1,8 +1,9 @@
1
- // The shared app-event dispatcher. The host exposes ONE registerAppEvent slot per JS world
1
+ // The shared app-event dispatcher. The host exposes ONE `_creatorApp.registerEvent` slot per JS world
2
2
  // (docs/app-lifecycle-plan.md), so every SDK consumer (the `app` global's pause/resume/url, the
3
3
  // `device` global's online/offline) subscribes here and this module registers with the host once,
4
4
  // lazily on the first subscription — no top-level side effect, tree-shaking drops it when unused.
5
5
 
6
+
6
7
  type AppEventCallback = (data?: string) => void
7
8
 
8
9
  const listeners: Record<string, AppEventCallback[]> = {}
@@ -17,7 +18,7 @@ export const appEventsOn = (event: string, cb: AppEventCallback): void => {
17
18
  ;(listeners[event] ??= []).push(cb)
18
19
  if (!registered) {
19
20
  registered = true
20
- _creatorUtils.registerAppEvent?.(dispatch)
21
+ _creatorApp.registerEvent?.(dispatch)
21
22
  }
22
23
  }
23
24
 
@@ -28,7 +29,7 @@ export const appEventsOff = (event: string, cb: AppEventCallback): void => {
28
29
  if (i >= 0) arr.splice(i, 1)
29
30
  }
30
31
 
31
- // The keyboard channel is a separate host slot (registerKeyboardEvent) — the registerAppEvent
32
+ // The keyboard channel is a separate host slot (`_creatorInput.registerKeyboardEvent`) — the app-event
32
33
  // string channel can't carry the (height, duration) number pair. Same lazy single-slot pattern.
33
34
 
34
35
  type KeyboardCallback = (height: number, duration: number) => void
@@ -44,7 +45,7 @@ export const keyboardEventsOn = (cb: KeyboardCallback): void => {
44
45
  keyboardListeners.push(cb)
45
46
  if (!keyboardRegistered) {
46
47
  keyboardRegistered = true
47
- _creatorUtils.registerKeyboardEvent?.(dispatchKeyboard)
48
+ if (typeof _creatorInput !== "undefined") _creatorInput.registerKeyboardEvent?.(dispatchKeyboard)
48
49
  }
49
50
  }
50
51
 
@@ -1,9 +1,11 @@
1
- // The JSON wire protocol shared by the two host plugin channels — NativeView
2
- // (`_creatorUI.viewCall`, see ui/NativeView.ts) and headless services
3
- // (`_creatorUtils.serviceCall`, see runtime/service.ts). One protocol, two transports:
4
- // `args`, results, and event payloads travel as JSON strings, and an absent/empty string
5
- // decodes to `undefined`. Typed plugins (QRScanner, CameraView, Geolocation) layer methods
6
- // over this — see src/plugins/.
1
+ // The wire protocol shared by the two host plugin channels — NativeView
2
+ // (`_creatorTree.viewCall`, see ui/NativeView.ts) and headless services
3
+ // (`_creatorService.call`, see runtime/service.ts). One protocol, two transports: `args`, results
4
+ // and event payloads travel as VALUES (`value` of bridges/types.d.ts — plain data and Uint8Array;
5
+ // the runtime reads them once, the host gets its platform's own), and what a host leaves out
6
+ // arrives as `undefined`. Only a view's / a service's PARAMS are JSON text: they are part of the
7
+ // tree (a node's property, a destination). Typed plugins (QRScanner, CameraView, Geolocation)
8
+ // layer methods over this — see src/plugins/.
7
9
 
8
10
  /** @internal event-name → listeners. NativeView exposes it as `nvl` (host contract). */
9
11
  export type ChannelListeners = Record<string, ((data?: any) => void)[]>
@@ -21,20 +23,21 @@ export const _channelOff = (listeners: ChannelListeners, event: string, callback
21
23
  if (i >= 0) list.splice(i, 1)
22
24
  }
23
25
 
24
- /** @internal deliver a host event: decode the JSON payload, fan out (copy-before-iterate). */
25
- export const _channelEmit = (listeners: ChannelListeners, event: string, dataJson?: string): void => {
26
+ /** @internal deliver a host event: fan out (copy-before-iterate). */
27
+ export const _channelEmit = (listeners: ChannelListeners, event: string, data?: unknown): void => {
26
28
  const list = listeners[event]
27
29
  if (!list || list.length === 0) return
28
- const data = dataJson === undefined || dataJson === "" ? undefined : JSON.parse(dataJson)
29
30
  for (const cb of [...list]) cb(data)
30
31
  }
31
32
 
32
33
  type ChannelCallBridge = (
33
- id: number, method: string, args: string,
34
- onComplete: (result?: string) => void, onError: (err: string) => void,
34
+ id: number, method: string, args: unknown[],
35
+ onComplete: (result?: unknown) => void, onError: (err: string) => void,
35
36
  ) => void
36
37
 
37
- /** @internal one JSON call over a channel bridge.
38
+ /** @internal one call over a channel bridge. An argument that cannot cross (a cycle, a typed array
39
+ * that is not bytes) makes the bridge THROW: the promise rejects with it, the host never sees
40
+ * the call.
38
41
  *
39
42
  * Takes the HANDLER OBJECT plus the member name — never a plucked-off function reference. On
40
43
  * Apple hosts the bridge members are JSExport-ed Objective-C/Swift methods, and JavaScriptCore
@@ -55,7 +58,7 @@ export const _channelCall = (
55
58
  rej(new Error(unsupportedError))
56
59
  return
57
60
  }
58
- bridge.call(owner, id, method, JSON.stringify(args),
59
- (result?: string) => res(result === undefined || result === "" ? undefined : JSON.parse(result)),
60
- (err: string) => rej(new Error(err)))
61
+ // A host rejects with its message — as a string, or as the Error the runtime made of it (the
62
+ // Android host): the message is the same either way, never "Error: <message>".
63
+ bridge.call(owner, id, method, args, res, (err: unknown) => rej(typeof err === "string" ? new Error(err) : err))
61
64
  })
@@ -1,20 +1,21 @@
1
1
  // System clipboard (docs/app-lifecycle-plan.md). Write is fire-and-forget; read is async because
2
2
  // web gates it behind a permission prompt (native hosts complete synchronously).
3
3
 
4
+
4
5
  export const clipboard = {
5
6
  /** Put `text` on the system clipboard. Silent no-op on hosts without clipboard access. */
6
7
  write(text: string): void {
7
- _creatorUtils.clipboardWrite?.(text)
8
+ _creatorDevice.clipboardWrite?.(text)
8
9
  },
9
10
  /** Read text from the system clipboard. Rejects when the platform has no clipboard access, the
10
11
  * user denied it, or there is nothing readable. */
11
12
  read(): Promise<string> {
12
13
  return new Promise((resolve, reject) => {
13
- if (typeof _creatorUtils.clipboardRead !== "function") {
14
+ if (typeof _creatorDevice.clipboardRead !== "function") {
14
15
  reject(new Error("Clipboard is not supported on this platform"))
15
16
  return
16
17
  }
17
- _creatorUtils.clipboardRead(resolve, (err: string) => reject(new Error(err)))
18
+ _creatorDevice.clipboardRead(resolve, (err: string) => reject(new Error(err)))
18
19
  })
19
20
  },
20
21
  }
@@ -15,6 +15,7 @@
15
15
  // Even before that pass lands, each method is a class method, so chisel's method-granular DCE drops
16
16
  // whichever ones a project doesn't use.
17
17
 
18
+
18
19
  export type Locale = "en" | "ru"
19
20
  export type DateInput = Date | number | string | DateValue
20
21
  export type Unit = "ms" | "second" | "minute" | "hour" | "day" | "week" | "month" | "year"
@@ -58,7 +59,7 @@ const DATA: Record<Locale, LocaleData> = { en: EN, ru: RU }
58
59
 
59
60
  const resolveLocale = (locale?: Locale): Locale => {
60
61
  if (locale) return locale === "ru" ? "ru" : "en"
61
- const lang = typeof _creatorUtils !== "undefined" ? _creatorUtils.language : ""
62
+ const lang = typeof _creatorDevice !== "undefined" ? _creatorDevice.language : ""
62
63
  return lang && lang.toLowerCase().indexOf("ru") === 0 ? "ru" : "en"
63
64
  }
64
65
 
@@ -1,6 +1,7 @@
1
1
  // Platform info + the host resize event. The resize listener registers with the host lazily on the
2
2
  // first subscription (no top-level side effect, so tree-shaking can drop this entirely if unused).
3
3
 
4
+ import { ensureEmitter, onTreeResize } from "../ui/tree"
4
5
  import { Quat } from "../math/quat"
5
6
  import { Vec3 } from "../math/vec"
6
7
  import { appEventsOn, appEventsOff } from "./appEvents"
@@ -94,10 +95,10 @@ export const device = {
94
95
  * mouse-look instead of on-screen sticks. It is not the OS name: a Mac reports `"desktop"`,
95
96
  * like Windows and Linux do (the OS/version detail is host-side, not here). */
96
97
  get platform(): "web" | "android" | "ios" | "desktop" | string {
97
- return _creatorUtils.platform
98
+ return _creatorDevice.platform
98
99
  },
99
100
  get language(): string {
100
- return _creatorUtils.language
101
+ return _creatorDevice.language
101
102
  },
102
103
  /** Display device-pixel ratio (physical px per logical/CSS px): 1 on standard displays, 2–3 on
103
104
  * retina / iOS. Bake a Canvas at this ratio for crisp output without hardcoding, e.g.
@@ -105,17 +106,17 @@ export const device = {
105
106
  * renders at logical resolution, so on web this only helps UI canvases / is forward-looking for
106
107
  * 2D; on iOS (physical surface) it makes 2D canvases crisp. Falls back to 1 if the host omits it. */
107
108
  get pixelRatio(): number {
108
- return _creatorUtils.pixelRatio || 1
109
+ return _creatorDevice.pixelRatio || 1
109
110
  },
110
111
  /** Current display width in logical px — the same value the "resize" event delivers, but readable at
111
112
  * any time (not only inside the listener). 0 until the host has reported a size. */
112
113
  get width(): number {
113
- return _creatorUI.getDisplaySize?.()?.[0] ?? 0
114
+ return _creatorTree.getDisplaySize()[0] ?? 0
114
115
  },
115
116
  /** Current display height in logical px — the same value the "resize" event delivers, readable at any
116
117
  * time. 0 until the host has reported a size. */
117
118
  get height(): number {
118
- return _creatorUI.getDisplaySize?.()?.[1] ?? 0
119
+ return _creatorTree.getDisplaySize()[1] ?? 0
119
120
  },
120
121
  /** Opt into the precise-touch system where the platform supports it. OFF by default. When on, fast
121
122
  * strokes are sampled at the touch digitizer's full rate (iOS coalesced touches, ≈120–240 Hz) instead
@@ -123,14 +124,14 @@ export const device = {
123
124
  * points and smoother lines. Leave it off for tap/button UIs. No-op on hosts without a coalesced-input
124
125
  * concept (web already coalesces pointermove; headless). */
125
126
  setPreciseTouch(enabled: boolean): void {
126
- _creatorUtils.setPreciseTouch?.(enabled)
127
+ _creatorDevice.setPreciseTouch?.(enabled)
127
128
  },
128
129
  /** Fire a one-shot haptic of the given semantic `style` (default `"medium"`) — a physical tap on
129
130
  * supported hardware (iOS Taptic Engine, Android vibrator). Chosen by meaning, not duration, so it
130
131
  * feels native on each platform; see {@link HapticStyle}. Host-gated: a silent no-op where there's
131
132
  * no haptic hardware (iPad, older iPhones, web, headless). */
132
133
  vibrate(style: HapticStyle = "medium"): void {
133
- _creatorUtils.vibrate?.(style)
134
+ _creatorDevice.vibrate?.(style)
134
135
  },
135
136
  /** Device-orientation sensor (gyro + accelerometer, fused) for tilt/steering and magic-window /
136
137
  * 360° panoramas. Poll `attitude` / `gravity` inside setLoop; they return the freshest fused
@@ -138,7 +139,7 @@ export const device = {
138
139
  motion: {
139
140
  /** Whether this device has the motion sensors at all (no gyro → false; iPad/older, web, headless). */
140
141
  get available(): boolean {
141
- return _creatorUtils.motionAvailable?.() ?? false
142
+ return _creatorDevice.motionAvailable?.() ?? false
142
143
  },
143
144
  /** Whether updates are currently running (start succeeded and stop hasn't been called). */
144
145
  get enabled(): boolean {
@@ -148,18 +149,18 @@ export const device = {
148
149
  * Async so a web host can await its permission prompt; native resolves immediately. */
149
150
  async start(options: MotionOptions = {}): Promise<boolean> {
150
151
  motionFrame = options.frame ?? "world"
151
- motionEnabled = !!(await (_creatorUtils.motionStart?.(options.interval ?? 1 / 60) ?? false))
152
+ motionEnabled = !!(await (_creatorDevice.motionStart?.(options.interval ?? 1 / 60) ?? false))
152
153
  return motionEnabled
153
154
  },
154
155
  /** Stop sensor updates and release the sensor (battery). */
155
156
  stop(): void {
156
- _creatorUtils.motionStop?.()
157
+ _creatorDevice.motionStop?.()
157
158
  motionEnabled = false
158
159
  },
159
160
  /** Capture the current heading as "forward" — a yaw-only recenter (pitch/roll stay gravity-
160
161
  * referenced, so the horizon stays level). No-op in the "device" frame. */
161
162
  recenter(): void {
162
- const s = _creatorUtils.getMotionSample?.()
163
+ const s = _creatorDevice.motionSample?.()
163
164
  if (!s || motionFrame === "device") { motionYawOffset = 0; return }
164
165
  const f = orientedAttitude(s).rotateVec3([0, 0, -1]) // world-space camera forward
165
166
  motionYawOffset = Math.atan2(-f.x, -f.z) // heading about world up
@@ -168,7 +169,7 @@ export const device = {
168
169
  * and screen-oriented, so `camera.quaternion = device.motion.attitude` is a complete magic-window
169
170
  * / panorama camera. `Quat.identity` until the first sample arrives / when not running. */
170
171
  get attitude(): Quat {
171
- const s = _creatorUtils.getMotionSample?.()
172
+ const s = _creatorDevice.motionSample?.()
172
173
  if (!s) return Quat.identity
173
174
  if (motionFrame === "device") return new Quat(s[0], s[1], s[2], s[3])
174
175
  const q = orientedAttitude(s)
@@ -178,7 +179,7 @@ export const device = {
178
179
  * down, matching clientX/clientY), orientation-aware — a 2D game reads `gravity.x / gravity.y`.
179
180
  * `(0,0,0)` when not running. */
180
181
  get gravity(): Vec3 {
181
- const s = _creatorUtils.getMotionSample?.()
182
+ const s = _creatorDevice.motionSample?.()
182
183
  if (!s) return new Vec3(0, 0, 0)
183
184
  if (motionFrame === "device") return new Vec3(s[4], s[5], s[6])
184
185
  // Re-express the raw device-frame gravity in screen space: undo the interface roll (about the
@@ -250,7 +251,7 @@ export const device = {
250
251
  * is sure there's no network. `true` on hosts that don't track it. Change events: `"online"` /
251
252
  * `"offline"`. */
252
253
  get online(): boolean {
253
- return _creatorUtils.isOnline?.() ?? true
254
+ return _creatorApp.isOnline?.() ?? true
254
255
  },
255
256
  addEventListener<E extends keyof DeviceEventMap>(channel: E, callback: DeviceEventMap[E]): void {
256
257
  if (channel === "online" || channel === "offline") {
@@ -269,7 +270,8 @@ export const device = {
269
270
  resizeListeners.push(callback as ResizeCallback)
270
271
  if (!resizeRegistered) {
271
272
  resizeRegistered = true
272
- _creatorUI.registerResizeEvent(onResize)
273
+ ensureEmitter()
274
+ onTreeResize(onResize)
273
275
  }
274
276
  },
275
277
  removeEventListener<E extends keyof DeviceEventMap>(channel: E, callback: DeviceEventMap[E]): void {
@@ -1,5 +1,13 @@
1
1
  // HTTP via the host. `fetch` is async (host streams the body), `fetchLocal` reads a preloaded asset
2
- // synchronously. Ported from worker/src/fetch.ts over the same _creatorUtils bridge.
2
+ // synchronously. Over the `_creatorFetch` bridge (sdk/src/bridges/fetch.d.ts).
3
+ //
4
+ // Ownership (owned handles, docs/plans/hosts-unification-plan.md phase 10): a fetched body lives as long as
5
+ // the app holds the response — `_h` is the owned handle the runtime returned with the completion,
6
+ // its collection frees the host buffer. There is nothing to dispose. A LOCAL asset's buffer is the
7
+ // host's (cached by name, shared by every `fetchLocal` of that name), so its wrapper only carries
8
+ // the id. The same goes for a `File` (picker / camera / canvas export): a host buffer the wrapper
9
+ // names but does not own.
10
+
3
11
 
4
12
  export type FetchOptions = {
5
13
  body?: any
@@ -13,47 +21,50 @@ export interface FetchResponse {
13
21
  readonly status: number
14
22
  json<T = any>(): T
15
23
  text(): string
24
+ /** @deprecated A no-op: the body is freed with the response's handle (owned handles). Apps written
25
+ * against the pre-owned SDK still call it after `text()` / `json()`; remove the call. */
16
26
  dispose(): void
17
27
  }
18
28
 
19
29
  class FetchResponseImpl implements FetchResponse {
20
30
  readonly status: number
21
- private readonly _id: number
22
- constructor(id: number, status: number) {
23
- this._id = id
31
+ /** @internal The buffer handle (`_h.id` = the host buffer id). Hosts and the runtime read a buffer
32
+ * off any object carrying `_h` — a File / FetchResponse alike (FormData entries, `UIImage` src,
33
+ * `Texture.load`, `share`, `files.write`). */
34
+ readonly _h: Handle
35
+ constructor(h: Handle, status: number) {
36
+ this._h = h
24
37
  this.status = status
25
38
  }
26
39
  json<T = any>(): T {
27
- return _creatorUtils.fetchToJson(this._id)
40
+ return _creatorFetch.toJson(this._h.id)
28
41
  }
29
42
  text(): string {
30
- return _creatorUtils.fetchToText(this._id)
31
- }
32
- dispose(): void {
33
- _creatorUtils.disposeFetch(this._id)
43
+ return _creatorFetch.toText(this._h.id)
34
44
  }
45
+ /** @deprecated see the interface */
46
+ dispose(): void {}
35
47
  }
36
48
 
37
49
  export const fetch = (url: string, options: FetchOptions = {}): Promise<FetchResponse> =>
38
50
  new Promise<FetchResponse>((resolve, reject) => {
39
- _creatorUtils.fetch(url, options, (systemId, statusCode) => {
40
- resolve(new FetchResponseImpl(systemId, statusCode))
51
+ _creatorFetch.fetch(url, options, (h, statusCode) => {
52
+ resolve(new FetchResponseImpl(h, statusCode))
41
53
  }, reject)
42
54
  })
43
55
 
44
56
  export const fetchLocal = (path: string): FetchResponse =>
45
- new FetchResponseImpl(_creatorUtils.fetchLocal(path), 200)
57
+ new FetchResponseImpl({ id: _creatorFetch.local(path) }, 200) // the host's buffer: not owned
46
58
 
47
59
  /** A handle to a picked / fetched file (see openFilePicker). */
48
60
  export class File {
49
- /** @internal host buffer handle. `_`-prefixed because hosts read it off the raw object —
50
- * same key `FetchResponse` carries, so both are accepted anywhere a buffer is expected
51
- * (FormData entries, `UIImage` src, `Texture.load`, `share`). */
52
- readonly _id: number
61
+ /** @internal The host buffer (not owned — see the header). Same key `FetchResponse` carries, so
62
+ * both are accepted anywhere a buffer is expected. */
63
+ readonly _h: Handle
53
64
  readonly name: string
54
65
  readonly size: number
55
66
  constructor(systemId: number, name: string, size: number) {
56
- this._id = systemId
67
+ this._h = { id: systemId }
57
68
  this.name = name
58
69
  this.size = size
59
70
  }
@@ -62,9 +73,8 @@ export class File {
62
73
  type FormDataValue = string | number | boolean | File | FetchResponse
63
74
 
64
75
  export class FormData {
65
- // Host-facing: hosts walk `_entries` and treat any object value carrying `_id` as binary
66
- // (the buffer id), everything else as a text field. See viewer-lite utils/fetch.ts,
67
- // creator-pkg creatorUtilsHandler.cpp, iOS Fetch.swift buildMultipartBody.
76
+ // Host-facing: the runtime walks `_entries` and treats any object value carrying `_h` as binary
77
+ // (the buffer id), everything else as a text field. See runtime/native src/host/fetch.cpp.
68
78
  private _entries: [string, FormDataValue, filename?: string][] = []
69
79
 
70
80
  append(name: string, value: FormDataValue, filename?: string): void {
@@ -12,7 +12,9 @@ export function openFilePicker(
12
12
  options: OpenFilePickerOptions & { multiple?: boolean } = {},
13
13
  ): Promise<File | File[] | null> {
14
14
  return new Promise((resolve, reject) => {
15
- _creatorUtils.openFilePicker((resp) => {
15
+ // No picker on this host (headless, a hidden render): nothing picked, like a cancelled dialog.
16
+ if (typeof _creatorDevice.openFilePicker !== "function") { resolve(options.multiple ? [] : null); return }
17
+ _creatorDevice.openFilePicker((resp) => {
16
18
  if (!resp || resp.length === 0) {
17
19
  resolve(options.multiple ? [] : null)
18
20
  return
@@ -24,7 +26,7 @@ export function openFilePicker(
24
26
  }
25
27
 
26
28
  // ---- Local filesystem -------------------------------------------------------------------------
27
- // `files` is the read/write/delete half of `fetchLocal`, for platforms where the app owns a
29
+ // `files` (the `_creatorFiles` bridge, absent on sandboxed hosts) is the read/write/delete half of `fetchLocal`, for platforms where the app owns a
28
30
  // filesystem — desktop today. A RELATIVE path is resolved against the app's OWN folder (for a
29
31
  // packaged desktop app: the directory the exe sits in), never the process cwd, so a save written by
30
32
  // a shortcut and one written from a terminal land in the same place; an ABSOLUTE path goes wherever
@@ -61,8 +63,7 @@ export type FileEntry = {
61
63
  modified: number
62
64
  }
63
65
 
64
- const supported = () =>
65
- typeof _creatorUtils !== "undefined" && typeof _creatorUtils.writeLocal === "function"
66
+ const supported = () => typeof _creatorFiles !== "undefined"
66
67
 
67
68
  // The host is asked for the work only when it can do it; otherwise the promise rejects with the
68
69
  // same message the native bridge uses, so a project sees one error either way.
@@ -72,10 +73,10 @@ const run = <T>(op: string, path: string, call: (resolve: (v: T) => void, reject
72
73
  call(resolve, reject)
73
74
  })
74
75
 
75
- // A File / FetchResponse crosses as the bare host buffer handle — the same `_id` convention
76
- // FormData entries and image `src` objects use, so the bytes never travel through JS.
76
+ // A File / FetchResponse crosses as its buffer handle — the same `_h` convention FormData entries
77
+ // and image `src` objects use, so the bytes never travel through JS.
77
78
  const payload = (data: FileData) =>
78
- (typeof data === "object" && "_id" in (data as any) ? { _id: (data as any)._id } : data) as any
79
+ (typeof data === "object" && "_h" in (data as any) ? { _h: (data as any)._h } : data) as any
79
80
 
80
81
  export const files = {
81
82
  /** Is there a filesystem here (desktop)? False on web/mobile, where every other call rejects. */
@@ -86,38 +87,38 @@ export const files = {
86
87
  /** Replace the file's contents, creating the parent folders on the way. */
87
88
  write(path: string, data: FileData): Promise<void> {
88
89
  return run<void>("write", path, (resolve, reject) =>
89
- _creatorUtils.writeLocal!(path, payload(data), false, () => resolve(), reject))
90
+ _creatorFiles.write(path, payload(data), false, () => resolve(), reject))
90
91
  },
91
92
 
92
93
  /** Add to the end of the file, creating it (and the parent folders) when it isn't there yet. */
93
94
  append(path: string, data: FileData): Promise<void> {
94
95
  return run<void>("append", path, (resolve, reject) =>
95
- _creatorUtils.writeLocal!(path, payload(data), true, () => resolve(), reject))
96
+ _creatorFiles.write(path, payload(data), true, () => resolve(), reject))
96
97
  },
97
98
 
98
99
  /** The whole file as UTF-8 text — null when there is no such file. */
99
100
  readText(path: string): Promise<string | null> {
100
101
  return run<string | null>("readText", path, (resolve, reject) =>
101
- _creatorUtils.readLocalText!(path, resolve, reject))
102
+ _creatorFiles.readText(path, resolve, reject))
102
103
  },
103
104
 
104
105
  /** The whole file as bytes — null when there is no such file. */
105
106
  readBytes(path: string): Promise<Uint8Array | null> {
106
107
  return run<Uint8Array | null>("readBytes", path, (resolve, reject) =>
107
- _creatorUtils.readLocalBytes!(path, resolve, reject))
108
+ _creatorFiles.readBytes(path, resolve, reject))
108
109
  },
109
110
 
110
111
  /** Delete a file, or a directory (empty, or any directory with `{ recursive: true }`). Deleting
111
112
  * something already gone is success — what's promised is that the path is free afterwards. */
112
113
  rm(path: string, options: { recursive?: boolean } = {}): Promise<void> {
113
114
  return run<void>("rm", path, (resolve, reject) =>
114
- _creatorUtils.deleteLocal!(path, !!options.recursive, () => resolve(), reject))
115
+ _creatorFiles.remove(path, !!options.recursive, () => resolve(), reject))
115
116
  },
116
117
 
117
118
  /** Create a directory and any missing parents. An existing directory is fine. */
118
119
  mkdir(path: string): Promise<void> {
119
120
  return run<void>("mkdir", path, (resolve, reject) =>
120
- _creatorUtils.mkdirLocal!(path, () => resolve(), reject))
121
+ _creatorFiles.mkdir(path, () => resolve(), reject))
121
122
  },
122
123
 
123
124
  /** The entries of a directory, sorted by path — null when there is no such directory (an empty
@@ -126,13 +127,13 @@ export const files = {
126
127
  * is a file, not a directory, rejects. Filter in JS — `entries.filter(e => e.name.endsWith(".json"))`. */
127
128
  ls(path: string, options: { recursive?: boolean } = {}): Promise<FileEntry[] | null> {
128
129
  return run<FileEntry[] | null>("ls", path, (resolve, reject) =>
129
- _creatorUtils.listLocal!(path, !!options.recursive, resolve, reject))
130
+ _creatorFiles.list(path, !!options.recursive, resolve, reject))
130
131
  },
131
132
 
132
133
  /** What is at `path`: a file or a directory entry (`path` equal to `name`), or null when nothing
133
134
  * is there — `(await files.stat(p)) !== null` is the exists check. */
134
135
  stat(path: string): Promise<FileEntry | null> {
135
136
  return run<FileEntry | null>("stat", path, (resolve, reject) =>
136
- _creatorUtils.statLocal!(path, (e) => resolve(e ? { ...e, path: e.name } : null), reject))
137
+ _creatorFiles.stat(path, (e) => resolve(e ? { ...e, path: e.name } : null), reject))
137
138
  },
138
139
  }
@@ -7,9 +7,9 @@
7
7
  // - CONTINUOUS — mouse motion/wheel, sticks, triggers: polled every frame (`Input.mouse.delta`,
8
8
  // `Input.gamepad(0).axis('leftX')`), never events.
9
9
  // Bindings ("jump = Space | GamepadSouth") are app code: compare `e.code`, OR a few `Input.key`s.
10
- // Hosts report physical state only (`_creatorUtils.inputKey/inputRead/registerInputEvent`).
10
+ // Hosts report physical state only (`_creatorInput.key / read / registerEvent`).
11
11
 
12
- /** Continuous channel ids — the `_creatorUtils.inputRead` contract (bridges.d.ts). */
12
+ /** Continuous channel ids — the `_creatorInput.read` contract (sdk/src/bridges/input.d.ts). */
13
13
  export const InputChannel = {
14
14
  MouseX: 0, MouseY: 1, MouseDX: 2, MouseDY: 3, WheelX: 4, WheelY: 5, PointerLocked: 6,
15
15
  GamepadCount: 7, PointerOnUI: 8,
@@ -77,10 +77,12 @@ const deliver = (kind: number, code: string, gamepad: number, repeat: number) =>
77
77
  const ensureHostListener = () => {
78
78
  if (hostListenerInstalled) return
79
79
  hostListenerInstalled = true
80
- _creatorUtils.registerInputEvent?.(deliver)
80
+ input()?.registerEvent?.(deliver)
81
81
  }
82
82
 
83
- const read = (channel: number): number => _creatorUtils.inputRead?.(channel) ?? 0
83
+ // The host's input bridge, or undefined on a host without one (every read then answers "nothing").
84
+ const input = () => (typeof _creatorInput !== "undefined" ? _creatorInput : undefined)
85
+ const read = (channel: number): number => input()?.read?.(channel) ?? 0
84
86
 
85
87
  /** Mouse state — polled. Position is the cursor inside the viewport in logical px; `delta` is the
86
88
  * motion during the previous frame (raw where the OS offers it) and keeps counting while locked —
@@ -99,8 +101,8 @@ export const mouse = {
99
101
  * `if (!Input.mouse.onUI) look(Input.mouse.delta)` never turns a HUD drag into camera look.
100
102
  * Always false while `locked`. A press that fell through to the scene reads false. */
101
103
  get onUI(): boolean { return read(InputChannel.PointerOnUI) !== 0 },
102
- lock(): boolean { return _creatorUtils.inputSetPointerLock?.(true) ?? false },
103
- unlock(): void { _creatorUtils.inputSetPointerLock?.(false) },
104
+ lock(): boolean { return input()?.setPointerLock?.(true) ?? false },
105
+ unlock(): void { input()?.setPointerLock?.(false) },
104
106
  }
105
107
 
106
108
  export type GamepadAxisName = "leftX" | "leftY" | "rightX" | "rightY" | "leftTrigger" | "rightTrigger"
@@ -138,11 +140,11 @@ const gamepadAt = (index: number): GamepadState => {
138
140
  const v = read(base + PAD_AXIS[name])
139
141
  return deadzone > 0 && Math.abs(v) < deadzone ? 0 : v
140
142
  },
141
- button(code) { return _creatorUtils.inputKey(code, index) },
143
+ button(code) { return input()?.key?.(code, index) ?? false },
142
144
  rumble(strong, weak = strong, durationMs = 200) {
143
- return _creatorUtils.inputRumble?.(index, strong, weak, durationMs) ?? false
145
+ return input()?.rumble?.(index, strong, weak, durationMs) ?? false
144
146
  },
145
- stopRumble() { _creatorUtils.inputRumble?.(index, 0, 0, 0) },
147
+ stopRumble() { input()?.rumble?.(index, 0, 0, 0) },
146
148
  }
147
149
  }
148
150
  return g
@@ -156,7 +158,7 @@ export const Input = {
156
158
  * frame-independent movement. `gamepad` scopes a Gamepad* code to one pad; omitted = any pad.
157
159
  */
158
160
  key(code: string, gamepad?: number): boolean {
159
- return _creatorUtils.inputKey(code, gamepad)
161
+ return input()?.key?.(code, gamepad ?? -1) ?? false
160
162
  },
161
163
 
162
164
  /** Listen for a discrete input moment. `keydown`/`keyup` cover EVERY button — keyboard, mouse,