lecodes-sdk 0.19.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 (367) hide show
  1. package/README.md +114 -0
  2. package/dist/global.d.ts +503 -0
  3. package/dist/host.d.ts +61 -0
  4. package/dist/inject.js +4730 -0
  5. package/dist/types/animate/animate.d.ts +20 -0
  6. package/dist/types/animate/bezier.d.ts +2 -0
  7. package/dist/types/animate/easings.d.ts +21 -0
  8. package/dist/types/canvas/Canvas.d.ts +168 -0
  9. package/dist/types/core/Aspect.d.ts +171 -0
  10. package/dist/types/core/InspectorUI.d.ts +88 -0
  11. package/dist/types/core/StateMachine.d.ts +81 -0
  12. package/dist/types/core/color.d.ts +13 -0
  13. package/dist/types/core/compWrite.d.ts +11 -0
  14. package/dist/types/core/events.d.ts +11 -0
  15. package/dist/types/core/fields.d.ts +58 -0
  16. package/dist/types/core/registry.d.ts +7 -0
  17. package/dist/types/core/signals.d.ts +29 -0
  18. package/dist/types/core/time.d.ts +31 -0
  19. package/dist/types/g2/Camera2D.d.ts +19 -0
  20. package/dist/types/g2/CharacterController2D.d.ts +134 -0
  21. package/dist/types/g2/Node2D.d.ts +91 -0
  22. package/dist/types/g2/OneWay2D.d.ts +24 -0
  23. package/dist/types/g2/Physics2D.d.ts +147 -0
  24. package/dist/types/g2/Scene2D.d.ts +72 -0
  25. package/dist/types/g2/Shape2D.d.ts +58 -0
  26. package/dist/types/g2/Sprite.d.ts +51 -0
  27. package/dist/types/g2/SpriteAnimation.d.ts +50 -0
  28. package/dist/types/g2/SpriteSheet.d.ts +69 -0
  29. package/dist/types/g2/Texture2D.d.ts +16 -0
  30. package/dist/types/g2/Tilemap.d.ts +20 -0
  31. package/dist/types/g2/Tileset.d.ts +32 -0
  32. package/dist/types/g2/Trigger2D.d.ts +20 -0
  33. package/dist/types/g2/autotile.d.ts +133 -0
  34. package/dist/types/g2/cells.d.ts +14 -0
  35. package/dist/types/g2/defineScene2d.d.ts +141 -0
  36. package/dist/types/g2/groups2d.d.ts +16 -0
  37. package/dist/types/g2/loop.d.ts +4 -0
  38. package/dist/types/g2/scenarios2d.d.ts +30 -0
  39. package/dist/types/g2/touch.d.ts +2 -0
  40. package/dist/types/gl/Camera.d.ts +79 -0
  41. package/dist/types/gl/CameraPlace.d.ts +26 -0
  42. package/dist/types/gl/CharacterController.d.ts +98 -0
  43. package/dist/types/gl/Gearbox.d.ts +86 -0
  44. package/dist/types/gl/Geometry.d.ts +39 -0
  45. package/dist/types/gl/IK.d.ts +53 -0
  46. package/dist/types/gl/InstancedMesh.d.ts +46 -0
  47. package/dist/types/gl/Light.d.ts +73 -0
  48. package/dist/types/gl/Lightmap.d.ts +85 -0
  49. package/dist/types/gl/Locomotion.d.ts +279 -0
  50. package/dist/types/gl/Material.d.ts +100 -0
  51. package/dist/types/gl/Mesh.d.ts +33 -0
  52. package/dist/types/gl/Model.d.ts +53 -0
  53. package/dist/types/gl/NavAgent.d.ts +99 -0
  54. package/dist/types/gl/NavMesh.d.ts +169 -0
  55. package/dist/types/gl/Node.d.ts +99 -0
  56. package/dist/types/gl/Noise.d.ts +20 -0
  57. package/dist/types/gl/Particles.d.ts +277 -0
  58. package/dist/types/gl/Physics.d.ts +91 -0
  59. package/dist/types/gl/Plane.d.ts +19 -0
  60. package/dist/types/gl/Ragdoll.d.ts +86 -0
  61. package/dist/types/gl/Ray.d.ts +8 -0
  62. package/dist/types/gl/Scene.d.ts +221 -0
  63. package/dist/types/gl/Shape.d.ts +122 -0
  64. package/dist/types/gl/Terrain.d.ts +280 -0
  65. package/dist/types/gl/Texture.d.ts +27 -0
  66. package/dist/types/gl/Trigger.d.ts +10 -0
  67. package/dist/types/gl/Vehicle.d.ts +191 -0
  68. package/dist/types/gl/Wheel.d.ts +95 -0
  69. package/dist/types/gl/animation/AnimationClip.d.ts +60 -0
  70. package/dist/types/gl/animation/Animator.d.ts +219 -0
  71. package/dist/types/gl/animation/Layer.d.ts +31 -0
  72. package/dist/types/gl/animation/Loop.d.ts +17 -0
  73. package/dist/types/gl/animation/Playback.d.ts +36 -0
  74. package/dist/types/gl/animation/core.d.ts +367 -0
  75. package/dist/types/gl/controls.d.ts +19 -0
  76. package/dist/types/gl/physicsEvents.d.ts +1 -0
  77. package/dist/types/gl/scenarios.d.ts +98 -0
  78. package/dist/types/gl/state.d.ts +5 -0
  79. package/dist/types/gl/terrainMesh.d.ts +46 -0
  80. package/dist/types/gl/touch.d.ts +2 -0
  81. package/dist/types/inject.d.ts +123 -0
  82. package/dist/types/math/Mathf.d.ts +39 -0
  83. package/dist/types/math/mat4.d.ts +66 -0
  84. package/dist/types/math/quat.d.ts +53 -0
  85. package/dist/types/math/vec.d.ts +129 -0
  86. package/dist/types/net/codec.d.ts +25 -0
  87. package/dist/types/net/core.d.ts +106 -0
  88. package/dist/types/net/index.d.ts +80 -0
  89. package/dist/types/net/replication.d.ts +101 -0
  90. package/dist/types/plugins/camera.d.ts +25 -0
  91. package/dist/types/plugins/geolocation.d.ts +39 -0
  92. package/dist/types/plugins/oauth.d.ts +25 -0
  93. package/dist/types/plugins/permission.d.ts +1 -0
  94. package/dist/types/plugins/push.d.ts +46 -0
  95. package/dist/types/plugins/qr.d.ts +21 -0
  96. package/dist/types/plugins/service.d.ts +33 -0
  97. package/dist/types/runtime/app.d.ts +67 -0
  98. package/dist/types/runtime/appEvents.d.ts +7 -0
  99. package/dist/types/runtime/channel.d.ts +1 -0
  100. package/dist/types/runtime/clipboard.d.ts +7 -0
  101. package/dist/types/runtime/datetime.d.ts +80 -0
  102. package/dist/types/runtime/device.d.ts +126 -0
  103. package/dist/types/runtime/fetch.d.ts +31 -0
  104. package/dist/types/runtime/files.d.ts +33 -0
  105. package/dist/types/runtime/input.d.ts +121 -0
  106. package/dist/types/runtime/media.d.ts +46 -0
  107. package/dist/types/runtime/misc.d.ts +11 -0
  108. package/dist/types/runtime/net.d.ts +15 -0
  109. package/dist/types/runtime/rpc.d.ts +44 -0
  110. package/dist/types/runtime/service.d.ts +1 -0
  111. package/dist/types/runtime/share.d.ts +2 -0
  112. package/dist/types/runtime/storage.d.ts +5 -0
  113. package/dist/types/runtime/touch.d.ts +56 -0
  114. package/dist/types/scene/defineScene.d.ts +296 -0
  115. package/dist/types/scene/editorPlugins.d.ts +82 -0
  116. package/dist/types/scene/gizmos.d.ts +56 -0
  117. package/dist/types/scene/grammar.d.ts +56 -0
  118. package/dist/types/scene/level.d.ts +98 -0
  119. package/dist/types/scene/material.d.ts +76 -0
  120. package/dist/types/server/auth/appConfig.d.ts +16 -0
  121. package/dist/types/server/auth/global.d.ts +56 -0
  122. package/dist/types/server/auth/models.d.ts +164 -0
  123. package/dist/types/server/auth/types.d.ts +56 -0
  124. package/dist/types/server/channel.d.ts +28 -0
  125. package/dist/types/server/context.d.ts +27 -0
  126. package/dist/types/server/db/defineDb.d.ts +36 -0
  127. package/dist/types/server/db/fields.d.ts +146 -0
  128. package/dist/types/server/db/index.d.ts +6 -0
  129. package/dist/types/server/db/marci/query.d.ts +326 -0
  130. package/dist/types/server/db/types.d.ts +163 -0
  131. package/dist/types/server/errors.d.ts +8 -0
  132. package/dist/types/server/inject.d.ts +5 -0
  133. package/dist/types/ui/NativeView.d.ts +38 -0
  134. package/dist/types/ui/UI.d.ts +27 -0
  135. package/dist/types/ui/UIBottomSheet.d.ts +71 -0
  136. package/dist/types/ui/UIButton.d.ts +30 -0
  137. package/dist/types/ui/UIContainer.d.ts +33 -0
  138. package/dist/types/ui/UIImage.d.ts +35 -0
  139. package/dist/types/ui/UIInput.d.ts +110 -0
  140. package/dist/types/ui/UIModal.d.ts +70 -0
  141. package/dist/types/ui/UINode.d.ts +363 -0
  142. package/dist/types/ui/UIPager.d.ts +123 -0
  143. package/dist/types/ui/UIPopover.d.ts +39 -0
  144. package/dist/types/ui/UIScreen.d.ts +57 -0
  145. package/dist/types/ui/UIScrollable.d.ts +40 -0
  146. package/dist/types/ui/UISpacer.d.ts +9 -0
  147. package/dist/types/ui/UITabs.d.ts +96 -0
  148. package/dist/types/ui/UIText.d.ts +19 -0
  149. package/dist/types/ui/UIVideo.d.ts +28 -0
  150. package/dist/types/ui/UIVirtualizedList.d.ts +97 -0
  151. package/dist/types/ui/UIWidget.d.ts +65 -0
  152. package/dist/types/ui/fonts.d.ts +9 -0
  153. package/dist/types/ui/presentable.d.ts +68 -0
  154. package/dist/types/ui/router.d.ts +40 -0
  155. package/dist/types/ui/theme.d.ts +26 -0
  156. package/dist/types.json +1 -0
  157. package/package.json +46 -0
  158. package/prompts/2d.md +395 -0
  159. package/prompts/3d-scene-files.md +113 -0
  160. package/prompts/3d-scene.md +119 -0
  161. package/prompts/3d.md +303 -0
  162. package/prompts/README.md +142 -0
  163. package/prompts/ar.md +109 -0
  164. package/prompts/canvas.md +27 -0
  165. package/prompts/compose.ts +46 -0
  166. package/prompts/concept.md +49 -0
  167. package/prompts/core-design.md +75 -0
  168. package/prompts/core.md +264 -0
  169. package/prompts/design.md +218 -0
  170. package/prompts/dist/2d-game.md +1530 -0
  171. package/prompts/dist/3d-app.md +1672 -0
  172. package/prompts/dist/ar-app.md +1548 -0
  173. package/prompts/dist/concept.md +49 -0
  174. package/prompts/dist/design.md +498 -0
  175. package/prompts/dist/ui-app.md +1134 -0
  176. package/prompts/index.md +37 -0
  177. package/prompts/intake-prompt.md +33 -0
  178. package/prompts/namer-prompt.md +12 -0
  179. package/prompts/router-prompt.md +50 -0
  180. package/prompts/select.ts +111 -0
  181. package/prompts/ui-design.md +203 -0
  182. package/prompts/ui.md +803 -0
  183. package/src/animate/animate.ts +238 -0
  184. package/src/animate/bezier.ts +138 -0
  185. package/src/animate/easings.ts +126 -0
  186. package/src/bridges.d.ts +1345 -0
  187. package/src/canvas/Canvas.ts +305 -0
  188. package/src/compile/__tests__/assetIconMacro.test.ts +219 -0
  189. package/src/compile/__tests__/assetMacro.test.ts +74 -0
  190. package/src/compile/__tests__/assetName.test.ts +55 -0
  191. package/src/compile/__tests__/compile.test.ts +287 -0
  192. package/src/compile/__tests__/detectEntry.test.ts +132 -0
  193. package/src/compile/__tests__/fontMacro.test.ts +199 -0
  194. package/src/compile/aspectMacro.ts +86 -0
  195. package/src/compile/assetIconMacro.ts +384 -0
  196. package/src/compile/assetMacro.ts +146 -0
  197. package/src/compile/assetName.ts +50 -0
  198. package/src/compile/bundler.ts +287 -0
  199. package/src/compile/compileProject.ts +127 -0
  200. package/src/compile/detectEntry.ts +128 -0
  201. package/src/compile/fontMacro.ts +459 -0
  202. package/src/compile/fontRegistry.ts +78 -0
  203. package/src/compile/header.ts +67 -0
  204. package/src/compile/index.ts +100 -0
  205. package/src/compile/libraryImports.ts +52 -0
  206. package/src/compile/liteMaterial.ts +247 -0
  207. package/src/compile/sceneEditor.ts +88 -0
  208. package/src/compile/serverSplit.ts +233 -0
  209. package/src/compile/serverTypes.ts +227 -0
  210. package/src/compile/sfnt.ts +98 -0
  211. package/src/compile/shaderSchema.ts +202 -0
  212. package/src/compile/shaderTargets.ts +77 -0
  213. package/src/compile/sourcemap.ts +25 -0
  214. package/src/core/Aspect.ts +568 -0
  215. package/src/core/InspectorUI.ts +212 -0
  216. package/src/core/StateMachine.ts +308 -0
  217. package/src/core/__tests__/stateMachine.test.ts +132 -0
  218. package/src/core/color.ts +66 -0
  219. package/src/core/compWrite.ts +42 -0
  220. package/src/core/events.ts +38 -0
  221. package/src/core/fields.ts +120 -0
  222. package/src/core/registry.ts +23 -0
  223. package/src/core/signals.ts +277 -0
  224. package/src/core/time.ts +84 -0
  225. package/src/g2/Camera2D.ts +40 -0
  226. package/src/g2/CharacterController2D.ts +276 -0
  227. package/src/g2/Node2D.ts +267 -0
  228. package/src/g2/OneWay2D.ts +66 -0
  229. package/src/g2/Physics2D.ts +346 -0
  230. package/src/g2/Scene2D.ts +209 -0
  231. package/src/g2/Shape2D.ts +259 -0
  232. package/src/g2/Sprite.ts +89 -0
  233. package/src/g2/SpriteAnimation.ts +171 -0
  234. package/src/g2/SpriteSheet.ts +166 -0
  235. package/src/g2/Texture2D.ts +47 -0
  236. package/src/g2/Tilemap.ts +41 -0
  237. package/src/g2/Tileset.ts +71 -0
  238. package/src/g2/Trigger2D.ts +77 -0
  239. package/src/g2/autotile.ts +433 -0
  240. package/src/g2/cells.ts +91 -0
  241. package/src/g2/defineScene2d.ts +381 -0
  242. package/src/g2/groups2d.ts +106 -0
  243. package/src/g2/loop.ts +50 -0
  244. package/src/g2/scenarios2d.ts +69 -0
  245. package/src/g2/touch.ts +83 -0
  246. package/src/gl/Camera.ts +160 -0
  247. package/src/gl/CameraPlace.ts +52 -0
  248. package/src/gl/CharacterController.ts +238 -0
  249. package/src/gl/Gearbox.ts +212 -0
  250. package/src/gl/Geometry.ts +279 -0
  251. package/src/gl/IK.ts +193 -0
  252. package/src/gl/InstancedMesh.ts +132 -0
  253. package/src/gl/Light.ts +135 -0
  254. package/src/gl/Lightmap.ts +402 -0
  255. package/src/gl/Locomotion.ts +498 -0
  256. package/src/gl/Material.ts +279 -0
  257. package/src/gl/Mesh.ts +83 -0
  258. package/src/gl/Model.ts +124 -0
  259. package/src/gl/NavAgent.ts +337 -0
  260. package/src/gl/NavMesh.ts +397 -0
  261. package/src/gl/Node.ts +350 -0
  262. package/src/gl/Noise.ts +30 -0
  263. package/src/gl/Particles.ts +676 -0
  264. package/src/gl/Physics.ts +222 -0
  265. package/src/gl/Plane.ts +53 -0
  266. package/src/gl/Ragdoll.ts +270 -0
  267. package/src/gl/Ray.ts +16 -0
  268. package/src/gl/Scene.ts +505 -0
  269. package/src/gl/Shape.ts +409 -0
  270. package/src/gl/Terrain.ts +1092 -0
  271. package/src/gl/Texture.ts +63 -0
  272. package/src/gl/Trigger.ts +45 -0
  273. package/src/gl/Vehicle.ts +473 -0
  274. package/src/gl/Wheel.ts +240 -0
  275. package/src/gl/animation/AnimationClip.ts +204 -0
  276. package/src/gl/animation/Animator.ts +329 -0
  277. package/src/gl/animation/Layer.ts +39 -0
  278. package/src/gl/animation/Loop.ts +31 -0
  279. package/src/gl/animation/Playback.ts +52 -0
  280. package/src/gl/animation/core.ts +670 -0
  281. package/src/gl/controls.ts +95 -0
  282. package/src/gl/physicsEvents.ts +20 -0
  283. package/src/gl/scenarios.ts +291 -0
  284. package/src/gl/state.ts +6 -0
  285. package/src/gl/terrainMesh.ts +219 -0
  286. package/src/gl/touch.ts +68 -0
  287. package/src/host.d.ts +61 -0
  288. package/src/inject.ts +203 -0
  289. package/src/math/Mathf.ts +118 -0
  290. package/src/math/mat4.ts +278 -0
  291. package/src/math/quat.ts +232 -0
  292. package/src/math/vec.ts +255 -0
  293. package/src/net/codec.ts +119 -0
  294. package/src/net/core.ts +384 -0
  295. package/src/net/index.ts +181 -0
  296. package/src/net/replication.ts +622 -0
  297. package/src/plugins/camera.ts +81 -0
  298. package/src/plugins/geolocation.ts +123 -0
  299. package/src/plugins/oauth.ts +61 -0
  300. package/src/plugins/permission.ts +7 -0
  301. package/src/plugins/push.ts +132 -0
  302. package/src/plugins/qr.ts +73 -0
  303. package/src/plugins/service.ts +47 -0
  304. package/src/runtime/app.ts +101 -0
  305. package/src/runtime/appEvents.ts +54 -0
  306. package/src/runtime/channel.ts +61 -0
  307. package/src/runtime/clipboard.ts +20 -0
  308. package/src/runtime/datetime.ts +329 -0
  309. package/src/runtime/device.ts +293 -0
  310. package/src/runtime/fetch.ts +77 -0
  311. package/src/runtime/files.ts +108 -0
  312. package/src/runtime/input.ts +175 -0
  313. package/src/runtime/media.ts +111 -0
  314. package/src/runtime/misc.ts +16 -0
  315. package/src/runtime/net.ts +36 -0
  316. package/src/runtime/rpc.ts +218 -0
  317. package/src/runtime/service.ts +83 -0
  318. package/src/runtime/share.ts +9 -0
  319. package/src/runtime/storage.ts +13 -0
  320. package/src/runtime/touch.ts +76 -0
  321. package/src/scene/defineScene.ts +1396 -0
  322. package/src/scene/editorPlugins.ts +110 -0
  323. package/src/scene/gizmos.ts +148 -0
  324. package/src/scene/grammar.ts +120 -0
  325. package/src/scene/level.ts +296 -0
  326. package/src/scene/material.ts +188 -0
  327. package/src/server/auth/appConfig.ts +12 -0
  328. package/src/server/auth/global.ts +80 -0
  329. package/src/server/auth/host.ts +318 -0
  330. package/src/server/auth/models.ts +83 -0
  331. package/src/server/auth/types.ts +50 -0
  332. package/src/server/channel.ts +56 -0
  333. package/src/server/context.ts +36 -0
  334. package/src/server/db/defineDb.ts +237 -0
  335. package/src/server/db/fields.ts +132 -0
  336. package/src/server/db/httpTransport.ts +93 -0
  337. package/src/server/db/index.ts +7 -0
  338. package/src/server/db/marci/query.ts +412 -0
  339. package/src/server/db/types.ts +202 -0
  340. package/src/server/errors.ts +12 -0
  341. package/src/server/host.ts +74 -0
  342. package/src/server/inject.ts +13 -0
  343. package/src/server/runtime.ts +133 -0
  344. package/src/server/validate.ts +87 -0
  345. package/src/ui/NativeView.ts +142 -0
  346. package/src/ui/UI.ts +39 -0
  347. package/src/ui/UIBottomSheet.ts +139 -0
  348. package/src/ui/UIButton.ts +101 -0
  349. package/src/ui/UIContainer.ts +60 -0
  350. package/src/ui/UIImage.ts +83 -0
  351. package/src/ui/UIInput.ts +185 -0
  352. package/src/ui/UIModal.ts +139 -0
  353. package/src/ui/UINode.ts +830 -0
  354. package/src/ui/UIPager.ts +362 -0
  355. package/src/ui/UIPopover.ts +100 -0
  356. package/src/ui/UIScreen.ts +123 -0
  357. package/src/ui/UIScrollable.ts +87 -0
  358. package/src/ui/UISpacer.ts +14 -0
  359. package/src/ui/UITabs.ts +236 -0
  360. package/src/ui/UIText.ts +51 -0
  361. package/src/ui/UIVideo.ts +88 -0
  362. package/src/ui/UIVirtualizedList.ts +241 -0
  363. package/src/ui/UIWidget.ts +127 -0
  364. package/src/ui/fonts.ts +13 -0
  365. package/src/ui/presentable.ts +117 -0
  366. package/src/ui/router.ts +132 -0
  367. package/src/ui/theme.ts +84 -0
@@ -0,0 +1,293 @@
1
+ // Platform info + the host resize event. The resize listener registers with the host lazily on the
2
+ // first subscription (no top-level side effect, so tree-shaking can drop this entirely if unused).
3
+
4
+ import { Quat } from "../math/quat"
5
+ import { Vec3 } from "../math/vec"
6
+ import { appEventsOn, appEventsOff } from "./appEvents"
7
+
8
+ type ResizeCallback = (width: number, height: number) => void
9
+
10
+ type HdrChangeCallback = (headroom: number) => void
11
+
12
+ type DeviceEventMap = {
13
+ resize: ResizeCallback
14
+ /** Connectivity came back (best-effort, navigator.onLine semantics). */
15
+ online: () => void
16
+ /** Connectivity was lost. */
17
+ offline: () => void
18
+ /** The headroom the 3D engine renders to moved (`device.hdr.headroom`): the display ramped up to
19
+ * its peak after launch, brightness changed, the window went to another screen. */
20
+ hdrchange: HdrChangeCallback
21
+ }
22
+
23
+ const resizeListeners: ResizeCallback[] = []
24
+ let resizeRegistered = false
25
+
26
+ const onResize = (width: number, height: number): void => {
27
+ for (const cb of resizeListeners.slice()) cb(width, height)
28
+ }
29
+
30
+ // --- device.hdr state ---
31
+ // The 3D bridge is a free global that a 2D-only bundle never defines — reach it defensively. Hosts
32
+ // without the methods are SDR by definition; the look setters are remembered so the getters read
33
+ // back what the app asked for.
34
+ const gl = (): typeof _creator | undefined => (typeof _creator !== "undefined" ? _creator : undefined)
35
+ let hdrStrengthLocal = 0.35
36
+ let hdrPaperWhiteLocal = 1
37
+ // `hdrchange` is a poll: the engine quantises the headroom to half-stops, so a change is rare and
38
+ // a 4 Hz timer (only while someone listens) is cheaper than a per-frame bridge call.
39
+ const hdrListeners: HdrChangeCallback[] = []
40
+ let hdrPollTimer: ReturnType<typeof setInterval> | undefined
41
+ let hdrLastHeadroom = 1
42
+ const pollHdr = (): void => {
43
+ const h = device.hdr.headroom
44
+ if (h === hdrLastHeadroom) return
45
+ hdrLastHeadroom = h
46
+ for (const cb of hdrListeners.slice()) cb(h)
47
+ }
48
+
49
+ /** Semantic haptic styles for `device.vibrate`. Impact styles (`light`/`medium`/`heavy`/`soft`/
50
+ * `rigid`) are a physical "tap" of varying weight; notification styles (`success`/`warning`/`error`)
51
+ * cue an outcome; `selection` is a light tick for a value change. Chosen to map 1:1 onto iOS
52
+ * `UIFeedbackGenerator` and, on Android, `HapticFeedbackConstants` / `VibrationEffect` — so the same
53
+ * call feels native everywhere, rather than a duration that only web/Android can honor. */
54
+ export type HapticStyle =
55
+ | "light" | "medium" | "heavy" | "soft" | "rigid"
56
+ | "success" | "warning" | "error"
57
+ | "selection"
58
+
59
+ /** Options for `device.motion.start`. */
60
+ export interface MotionOptions {
61
+ /** Sensor update interval in seconds (default 1/60). The sensor fuses at ≥ this rate in the
62
+ * background; you poll the freshest sample each frame, so this is a floor, not a sync. */
63
+ interval?: number
64
+ /** Frame the readables are delivered in. "world" (default): engine Y-up, screen-oriented — drops
65
+ * straight into `camera.quaternion` / any node. "device": the raw sensor frame, no conversion. */
66
+ frame?: "world" | "device"
67
+ }
68
+
69
+ // --- device.motion state (pull-per-frame model) ---
70
+ let motionEnabled = false
71
+ let motionFrame: "world" | "device" = "world"
72
+ let motionYawOffset = 0 // radians about world up, captured by recenter()
73
+
74
+ // Calibration constants — VERIFY ON A PHYSICAL DEVICE (the simulator reports no motion).
75
+ // • CM_TO_WORLD: Core Motion's reference frame is Z-up; the engine world is Y-up. If a level horizon
76
+ // reads tilted ~90°, this is the term to check.
77
+ // • ORIENTATION_ROLL: roll about the view axis so camera-up tracks screen-up, indexed by the native
78
+ // interface-orientation code [portrait, landscapeLeft, landscapeRight, upsideDown]. If landscape is
79
+ // upside-down or rolled the wrong way, flip the ±π/2 signs.
80
+ // If the whole panorama turns the wrong way (look right → view goes left), conjugate `raw` below.
81
+ const CM_TO_WORLD = Quat.fromAxisAngle([1, 0, 0], -Math.PI / 2)
82
+ const ORIENTATION_ROLL = [0, Math.PI / 2, -Math.PI / 2, Math.PI]
83
+
84
+ // Device attitude → engine world frame, with the interface-orientation roll folded in (yaw NOT yet
85
+ // recentered). Shared by `attitude` (which adds the recenter) and `gravity` (which is yaw-invariant).
86
+ const orientedAttitude = (s: ArrayLike<number>): Quat => {
87
+ const raw = new Quat(s[0], s[1], s[2], s[3])
88
+ const roll = Quat.fromAxisAngle([0, 0, 1], ORIENTATION_ROLL[s[7]] ?? 0)
89
+ return CM_TO_WORLD.mul(raw).mul(roll)
90
+ }
91
+
92
+ export const device = {
93
+ /** Which INPUT MODEL the app is running under — the thing to branch on when a build needs
94
+ * mouse-look instead of on-screen sticks. It is not the OS name: a Mac reports `"desktop"`,
95
+ * like Windows and Linux do (the OS/version detail is host-side, not here). */
96
+ get platform(): "web" | "android" | "ios" | "desktop" | string {
97
+ return _creatorUtils.platform
98
+ },
99
+ get language(): string {
100
+ return _creatorUtils.language
101
+ },
102
+ /** Display device-pixel ratio (physical px per logical/CSS px): 1 on standard displays, 2–3 on
103
+ * retina / iOS. Bake a Canvas at this ratio for crisp output without hardcoding, e.g.
104
+ * `new Canvas(w, h, { pixelRatio: device.pixelRatio })`. NOTE: the web 2D/GL surface currently
105
+ * renders at logical resolution, so on web this only helps UI canvases / is forward-looking for
106
+ * 2D; on iOS (physical surface) it makes 2D canvases crisp. Falls back to 1 if the host omits it. */
107
+ get pixelRatio(): number {
108
+ return _creatorUtils.pixelRatio || 1
109
+ },
110
+ /** Current display width in logical px — the same value the "resize" event delivers, but readable at
111
+ * any time (not only inside the listener). 0 until the host has reported a size. */
112
+ get width(): number {
113
+ return _creatorUI.getDisplaySize?.()?.[0] ?? 0
114
+ },
115
+ /** Current display height in logical px — the same value the "resize" event delivers, readable at any
116
+ * time. 0 until the host has reported a size. */
117
+ get height(): number {
118
+ return _creatorUI.getDisplaySize?.()?.[1] ?? 0
119
+ },
120
+ /** Opt into the precise-touch system where the platform supports it. OFF by default. When on, fast
121
+ * strokes are sampled at the touch digitizer's full rate (iOS coalesced touches, ≈120–240 Hz) instead
122
+ * of once per display frame (~60 Hz), so a pointer-heavy app (drawing, handwriting, dragging) gets more
123
+ * points and smoother lines. Leave it off for tap/button UIs. No-op on hosts without a coalesced-input
124
+ * concept (web already coalesces pointermove; headless). */
125
+ setPreciseTouch(enabled: boolean): void {
126
+ _creatorUtils.setPreciseTouch?.(enabled)
127
+ },
128
+ /** Fire a one-shot haptic of the given semantic `style` (default `"medium"`) — a physical tap on
129
+ * supported hardware (iOS Taptic Engine, Android vibrator). Chosen by meaning, not duration, so it
130
+ * feels native on each platform; see {@link HapticStyle}. Host-gated: a silent no-op where there's
131
+ * no haptic hardware (iPad, older iPhones, web, headless). */
132
+ vibrate(style: HapticStyle = "medium"): void {
133
+ _creatorUtils.vibrate?.(style)
134
+ },
135
+ /** Device-orientation sensor (gyro + accelerometer, fused) for tilt/steering and magic-window /
136
+ * 360° panoramas. Poll `attitude` / `gravity` inside setLoop; they return the freshest fused
137
+ * sample, so the sensor rate need not match your frame rate. Host-gated: a no-op with no sensor. */
138
+ motion: {
139
+ /** Whether this device has the motion sensors at all (no gyro → false; iPad/older, web, headless). */
140
+ get available(): boolean {
141
+ return _creatorUtils.motionAvailable?.() ?? false
142
+ },
143
+ /** Whether updates are currently running (start succeeded and stop hasn't been called). */
144
+ get enabled(): boolean {
145
+ return motionEnabled
146
+ },
147
+ /** Begin sensor updates. Resolves to whether it actually started (false = no sensor / denied).
148
+ * Async so a web host can await its permission prompt; native resolves immediately. */
149
+ async start(options: MotionOptions = {}): Promise<boolean> {
150
+ motionFrame = options.frame ?? "world"
151
+ motionEnabled = !!(await (_creatorUtils.motionStart?.(options.interval ?? 1 / 60) ?? false))
152
+ return motionEnabled
153
+ },
154
+ /** Stop sensor updates and release the sensor (battery). */
155
+ stop(): void {
156
+ _creatorUtils.motionStop?.()
157
+ motionEnabled = false
158
+ },
159
+ /** Capture the current heading as "forward" — a yaw-only recenter (pitch/roll stay gravity-
160
+ * referenced, so the horizon stays level). No-op in the "device" frame. */
161
+ recenter(): void {
162
+ const s = _creatorUtils.getMotionSample?.()
163
+ if (!s || motionFrame === "device") { motionYawOffset = 0; return }
164
+ const f = orientedAttitude(s).rotateVec3([0, 0, -1]) // world-space camera forward
165
+ motionYawOffset = Math.atan2(-f.x, -f.z) // heading about world up
166
+ },
167
+ /** The device's current orientation as a `Quat`. In the "world" frame (default) it's engine Y-up
168
+ * and screen-oriented, so `camera.quaternion = device.motion.attitude` is a complete magic-window
169
+ * / panorama camera. `Quat.identity` until the first sample arrives / when not running. */
170
+ get attitude(): Quat {
171
+ const s = _creatorUtils.getMotionSample?.()
172
+ if (!s) return Quat.identity
173
+ if (motionFrame === "device") return new Quat(s[0], s[1], s[2], s[3])
174
+ const q = orientedAttitude(s)
175
+ return Quat.fromAxisAngle([0, 1, 0], -motionYawOffset).mul(q) // yaw recenter (world-frame)
176
+ },
177
+ /** Gravity direction for tilt controls. In the "world" frame it's SCREEN space (x → right, y →
178
+ * down, matching clientX/clientY), orientation-aware — a 2D game reads `gravity.x / gravity.y`.
179
+ * `(0,0,0)` when not running. */
180
+ get gravity(): Vec3 {
181
+ const s = _creatorUtils.getMotionSample?.()
182
+ if (!s) return new Vec3(0, 0, 0)
183
+ if (motionFrame === "device") return new Vec3(s[4], s[5], s[6])
184
+ // Re-express the raw device-frame gravity in screen space: undo the interface roll (about the
185
+ // screen normal), then flip Y to screen-down. Uses the unambiguous CMDeviceMotion.gravity, so
186
+ // it stays correct regardless of the attitude-quaternion convention above.
187
+ const unroll = Quat.fromAxisAngle([0, 0, 1], -(ORIENTATION_ROLL[s[7]] ?? 0))
188
+ const g = new Vec3(s[4], s[5], s[6]).rotate(unroll)
189
+ return new Vec3(g.x, -g.y, g.z)
190
+ },
191
+ },
192
+ /** The display's extended dynamic range — HDR. On a screen with headroom above SDR white (Apple's
193
+ * XDR panels; the macOS host today) the 3D surface is float and the engine renders past 1.0:
194
+ * 1.0 is the white of the UI and of a diffuse white surface, and the sun, emissives and speculars
195
+ * climb up to `headroom` times that. Everywhere else it is SDR: `available` false, `headroom` 1,
196
+ * the look setters remembered but invisible.
197
+ *
198
+ * Branch content on `available`, not on `headroom` — the live value ramps up from 1 over the
199
+ * first seconds after launch and follows the brightness keys. Particle `emissive` is the thing
200
+ * to raise: it is in post-exposure units (1 = white on screen) and everything above that clips on
201
+ * SDR, so an HDR display is the only place a fireball peaking at 8 reads as one. */
202
+ hdr: {
203
+ /** Whether the surface can show anything above SDR white at all (`maxHeadroom > 1`). */
204
+ get available(): boolean {
205
+ return device.hdr.maxHeadroom > 1
206
+ },
207
+ /** The peak the display can reach, as a multiple of SDR white (a 1600-nit XDR panel reports up
208
+ * to 16). Constant for the surface; 1 on SDR. */
209
+ get maxHeadroom(): number {
210
+ const v = gl()?.getDisplayMaxHeadroom?.() ?? 1
211
+ return v > 1 ? v : 1
212
+ },
213
+ /** The headroom the engine renders to right now — the screen's current peak as a multiple of SDR
214
+ * white, quantised to half-stops. Moves with brightness and the screen under the window
215
+ * (`hdrchange` event); never below 1. */
216
+ get headroom(): number {
217
+ const v = gl()?.getDisplayHeadroom?.() ?? 1
218
+ return v > 1 ? v : 1
219
+ },
220
+ /** How much of the picture reaches for the headroom, 0..1 (default 0.35). 0 touches only what
221
+ * SDR clipped — faithful to the SDR grade, but a mostly-mid-tone frame then looks flat next to
222
+ * the one bright spot; 1 lifts nearly everything above black toward the peak. A sunlit exterior
223
+ * takes 1, a dim interior wants less. A host may pin it (`LECODES_HDR_STRENGTH`). */
224
+ get strength(): number {
225
+ return gl()?.getHdrStrength?.() ?? hdrStrengthLocal
226
+ },
227
+ set strength(v: number) {
228
+ const s = Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0.35
229
+ hdrStrengthLocal = s
230
+ gl()?.setHdrStrength?.(s)
231
+ },
232
+ /** Where white lands, as a multiple of SDR white, 1..8 (default 1) — the "HDR brightness" of a
233
+ * console game's calibration screen, where paper white sits at ~200 nits against SDR's ~100.
234
+ * It scales the WHOLE picture: at 1 a white wall is as bright as the UI's white, at 2 it is
235
+ * twice that, and highlights still climb above it (the engine keeps `headroom / paperWhite` for
236
+ * them, clamped at the peak). `strength` decides how much of the picture reaches for the
237
+ * peak; this decides where the picture starts. 1.5–2 is the console norm; a game that wants to
238
+ * read as "HDR on" rather than "SDR with a brighter sun" wants this over `strength`. A host may
239
+ * pin it (`LECODES_HDR_PAPER_WHITE`). */
240
+ get paperWhite(): number {
241
+ return gl()?.getHdrPaperWhite?.() ?? hdrPaperWhiteLocal
242
+ },
243
+ set paperWhite(v: number) {
244
+ const p = Number.isFinite(v) ? Math.min(8, Math.max(1, v)) : 1
245
+ hdrPaperWhiteLocal = p
246
+ gl()?.setHdrPaperWhite?.(p)
247
+ },
248
+ },
249
+ /** Current connectivity — best-effort navigator.onLine semantics: `false` only when the platform
250
+ * is sure there's no network. `true` on hosts that don't track it. Change events: `"online"` /
251
+ * `"offline"`. */
252
+ get online(): boolean {
253
+ return _creatorUtils.isOnline?.() ?? true
254
+ },
255
+ addEventListener<E extends keyof DeviceEventMap>(channel: E, callback: DeviceEventMap[E]): void {
256
+ if (channel === "online" || channel === "offline") {
257
+ appEventsOn(channel, callback as () => void)
258
+ return
259
+ }
260
+ if (channel === "hdrchange") {
261
+ hdrListeners.push(callback as HdrChangeCallback)
262
+ if (hdrPollTimer === undefined) {
263
+ hdrLastHeadroom = device.hdr.headroom
264
+ hdrPollTimer = setInterval(pollHdr, 250)
265
+ }
266
+ return
267
+ }
268
+ if (channel !== "resize") return
269
+ resizeListeners.push(callback as ResizeCallback)
270
+ if (!resizeRegistered) {
271
+ resizeRegistered = true
272
+ _creatorUI.registerResizeEvent(onResize)
273
+ }
274
+ },
275
+ removeEventListener<E extends keyof DeviceEventMap>(channel: E, callback: DeviceEventMap[E]): void {
276
+ if (channel === "online" || channel === "offline") {
277
+ appEventsOff(channel, callback as () => void)
278
+ return
279
+ }
280
+ if (channel === "hdrchange") {
281
+ const i = hdrListeners.indexOf(callback as HdrChangeCallback)
282
+ if (i >= 0) hdrListeners.splice(i, 1)
283
+ if (hdrListeners.length === 0 && hdrPollTimer !== undefined) {
284
+ clearInterval(hdrPollTimer)
285
+ hdrPollTimer = undefined
286
+ }
287
+ return
288
+ }
289
+ if (channel !== "resize") return
290
+ const i = resizeListeners.indexOf(callback as ResizeCallback)
291
+ if (i >= 0) resizeListeners.splice(i, 1)
292
+ },
293
+ }
@@ -0,0 +1,77 @@
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.
3
+
4
+ export type FetchOptions = {
5
+ body?: any
6
+ method?: string
7
+ headers?: Record<string, string>
8
+ useOnce?: boolean
9
+ onProgress?: (progress: { loaded: number, total?: number }) => void
10
+ }
11
+
12
+ export interface FetchResponse {
13
+ readonly status: number
14
+ json<T = any>(): T
15
+ text(): string
16
+ dispose(): void
17
+ }
18
+
19
+ class FetchResponseImpl implements FetchResponse {
20
+ readonly status: number
21
+ private readonly _id: number
22
+ constructor(id: number, status: number) {
23
+ this._id = id
24
+ this.status = status
25
+ }
26
+ json<T = any>(): T {
27
+ return _creatorUtils.fetchToJson(this._id)
28
+ }
29
+ text(): string {
30
+ return _creatorUtils.fetchToText(this._id)
31
+ }
32
+ dispose(): void {
33
+ _creatorUtils.disposeFetch(this._id)
34
+ }
35
+ }
36
+
37
+ export const fetch = (url: string, options: FetchOptions = {}): Promise<FetchResponse> =>
38
+ new Promise<FetchResponse>((resolve, reject) => {
39
+ _creatorUtils.fetch(url, options, (systemId, statusCode) => {
40
+ resolve(new FetchResponseImpl(systemId, statusCode))
41
+ }, reject)
42
+ })
43
+
44
+ export const fetchLocal = (path: string): FetchResponse =>
45
+ new FetchResponseImpl(_creatorUtils.fetchLocal(path), 200)
46
+
47
+ /** A handle to a picked / fetched file (see openFilePicker). */
48
+ 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
53
+ readonly name: string
54
+ readonly size: number
55
+ constructor(systemId: number, name: string, size: number) {
56
+ this._id = systemId
57
+ this.name = name
58
+ this.size = size
59
+ }
60
+ }
61
+
62
+ type FormDataValue = string | number | boolean | File | FetchResponse
63
+
64
+ 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.
68
+ private _entries: [string, FormDataValue, filename?: string][] = []
69
+
70
+ append(name: string, value: FormDataValue, filename?: string): void {
71
+ this._entries.push([ name, value, filename ?? (typeof value === "object" ? (value as any).name : undefined) ])
72
+ }
73
+ delete(name: string): void {
74
+ const index = this._entries.findIndex((i) => i[0] === name)
75
+ if (index >= 0) this._entries.splice(index, 1)
76
+ }
77
+ }
@@ -0,0 +1,108 @@
1
+ // Native file picker + local file writing. openFilePicker is ported from
2
+ // worker/src/utils/openFilePicker.ts; it resolves to File handle(s) (see runtime/fetch.ts) that can
3
+ // be appended to a FormData and uploaded.
4
+
5
+ import { File, type FetchResponse } from "./fetch"
6
+
7
+ type OpenFilePickerOptions = { accept?: string }
8
+
9
+ export function openFilePicker(options?: OpenFilePickerOptions & { multiple?: false }): Promise<File | null>
10
+ export function openFilePicker(options: OpenFilePickerOptions & { multiple: true }): Promise<File[]>
11
+ export function openFilePicker(
12
+ options: OpenFilePickerOptions & { multiple?: boolean } = {},
13
+ ): Promise<File | File[] | null> {
14
+ return new Promise((resolve, reject) => {
15
+ _creatorUtils.openFilePicker((resp) => {
16
+ if (!resp || resp.length === 0) {
17
+ resolve(options.multiple ? [] : null)
18
+ return
19
+ }
20
+ if (options.multiple) resolve(resp.map((i: any) => new File(i[0], i[1], i[2])))
21
+ else resolve(new File(resp[0][0], resp[0][1], resp[0][2]))
22
+ }, reject, !!options.multiple, options.accept)
23
+ })
24
+ }
25
+
26
+ // ---- Local filesystem -------------------------------------------------------------------------
27
+ // `files` is the read/write/delete half of `fetchLocal`, for platforms where the app owns a
28
+ // filesystem — desktop today. A RELATIVE path is resolved against the app's OWN folder (for a
29
+ // packaged desktop app: the directory the exe sits in), never the process cwd, so a save written by
30
+ // a shortcut and one written from a terminal land in the same place; an ABSOLUTE path goes wherever
31
+ // it points. That folder is also one `fetchLocal` searches, so the two are a pair:
32
+ //
33
+ // await files.write("saves/slot1.json", JSON.stringify(state))
34
+ // const state = JSON.parse((await files.readText("saves/slot1.json")) ?? "{}")
35
+ //
36
+ // Every call is a promise: the host does the IO off the JS thread, so a big write never costs a
37
+ // frame. Calls stay ORDERED — two appends to one file land in call order — so a log written in a
38
+ // loop comes out in the order it was written, without awaiting each line.
39
+ //
40
+ // Writing into a folder that doesn't exist yet is not a failure (the folders are created), and
41
+ // deleting something already gone is not either — what's promised is the end state. The readers
42
+ // resolve to null for a file that isn't there; a real failure rejects with an Error carrying the
43
+ // host's reason. On a sandboxed platform (web, mobile) every call rejects — check `files.supported`
44
+ // if the same bundle runs there too.
45
+
46
+ /** Anything writable as-is: text, raw bytes, or a buffer the host already holds
47
+ * (a picked `File`, a `FetchResponse`, a `Canvas.toFile()` snapshot). */
48
+ export type FileData = string | Uint8Array | ArrayBuffer | File | FetchResponse
49
+
50
+ const supported = () =>
51
+ typeof _creatorUtils !== "undefined" && typeof _creatorUtils.writeLocal === "function"
52
+
53
+ // The host is asked for the work only when it can do it; otherwise the promise rejects with the
54
+ // same message the native bridge uses, so a project sees one error either way.
55
+ const run = <T>(op: string, path: string, call: (resolve: (v: T) => void, reject: (e: Error) => void) => void) =>
56
+ new Promise<T>((resolve, reject) => {
57
+ if (!supported()) throw new Error(`files.${op}("${path}"): this platform has no local filesystem`)
58
+ call(resolve, reject)
59
+ })
60
+
61
+ // A File / FetchResponse crosses as the bare host buffer handle — the same `_id` convention
62
+ // FormData entries and image `src` objects use, so the bytes never travel through JS.
63
+ const payload = (data: FileData) =>
64
+ (typeof data === "object" && "_id" in (data as any) ? { _id: (data as any)._id } : data) as any
65
+
66
+ export const files = {
67
+ /** Is there a filesystem here (desktop)? False on web/mobile, where every other call rejects. */
68
+ get supported(): boolean {
69
+ return supported()
70
+ },
71
+
72
+ /** Replace the file's contents, creating the parent folders on the way. */
73
+ write(path: string, data: FileData): Promise<void> {
74
+ return run<void>("write", path, (resolve, reject) =>
75
+ _creatorUtils.writeLocal!(path, payload(data), false, () => resolve(), reject))
76
+ },
77
+
78
+ /** Add to the end of the file, creating it (and the parent folders) when it isn't there yet. */
79
+ append(path: string, data: FileData): Promise<void> {
80
+ return run<void>("append", path, (resolve, reject) =>
81
+ _creatorUtils.writeLocal!(path, payload(data), true, () => resolve(), reject))
82
+ },
83
+
84
+ /** The whole file as UTF-8 text — null when there is no such file. */
85
+ readText(path: string): Promise<string | null> {
86
+ return run<string | null>("readText", path, (resolve, reject) =>
87
+ _creatorUtils.readLocalText!(path, resolve, reject))
88
+ },
89
+
90
+ /** The whole file as bytes — null when there is no such file. */
91
+ readBytes(path: string): Promise<Uint8Array | null> {
92
+ return run<Uint8Array | null>("readBytes", path, (resolve, reject) =>
93
+ _creatorUtils.readLocalBytes!(path, resolve, reject))
94
+ },
95
+
96
+ /** Delete a file, or a directory (empty, or any directory with `{ recursive: true }`). Deleting
97
+ * something already gone is success — what's promised is that the path is free afterwards. */
98
+ delete(path: string, options: { recursive?: boolean } = {}): Promise<void> {
99
+ return run<void>("delete", path, (resolve, reject) =>
100
+ _creatorUtils.deleteLocal!(path, !!options.recursive, () => resolve(), reject))
101
+ },
102
+
103
+ /** Create a directory and any missing parents. An existing directory is fine. */
104
+ mkdir(path: string): Promise<void> {
105
+ return run<void>("mkdir", path, (resolve, reject) =>
106
+ _creatorUtils.mkdirLocal!(path, () => resolve(), reject))
107
+ },
108
+ }
@@ -0,0 +1,175 @@
1
+ // Keyboard / mouse / gamepad input behind one `Input` global (docs/input-plan.md).
2
+ //
3
+ // Two kinds of signal, two shapes of API:
4
+ // - BUTTONS — anything that goes down and up: keyboard keys, mouse buttons, gamepad buttons. One
5
+ // code vocabulary ('KeyW', 'MouseLeft', 'GamepadSouth'), one held-poll (`Input.key`) and one
6
+ // event pair (`Input.on('keydown' | 'keyup')`). A mouse button IS a key.
7
+ // - CONTINUOUS — mouse motion/wheel, sticks, triggers: polled every frame (`Input.mouse.delta`,
8
+ // `Input.gamepad(0).axis('leftX')`), never events.
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`).
11
+
12
+ /** Continuous channel ids — the `_creatorUtils.inputRead` contract (bridges.d.ts). */
13
+ export const InputChannel = {
14
+ MouseX: 0, MouseY: 1, MouseDX: 2, MouseDY: 3, WheelX: 4, WheelY: 5, PointerLocked: 6,
15
+ GamepadCount: 7,
16
+ GamepadBase: 16, GamepadStride: 16,
17
+ PadConnected: 0, PadLeftX: 1, PadLeftY: 2, PadRightX: 3, PadRightY: 4, PadLeftTrigger: 5, PadRightTrigger: 6,
18
+ } as const
19
+
20
+ export type InputEventName = "keydown" | "keyup" | "gamepadconnected" | "gamepaddisconnected"
21
+
22
+ /** `keydown` / `keyup` payload. `code` is the same string `Input.key()` takes. */
23
+ export interface InputKeyEvent {
24
+ readonly type: "keydown" | "keyup"
25
+ /** Button code: KeyboardEvent.code ('KeyW', 'Space'), 'MouseLeft|Right|Middle|Back|Forward', or
26
+ * 'GamepadSouth|East|West|North|L1|R1|L2|R2|Select|Start|L3|R3|Up|Down|Left|Right'. */
27
+ readonly code: string
28
+ /** Which device: 'keyboard' | 'mouse' | 'gamepad'. */
29
+ readonly source: "keyboard" | "mouse" | "gamepad"
30
+ /** Pad index (0..3) for gamepad buttons, undefined otherwise. */
31
+ readonly gamepad?: number
32
+ /** OS auto-repeat keydown (key still held) — ignore for one-shot actions. */
33
+ readonly repeat: boolean
34
+ }
35
+
36
+ export interface InputGamepadEvent {
37
+ readonly type: "gamepadconnected" | "gamepaddisconnected"
38
+ readonly gamepad: number
39
+ }
40
+
41
+ type Listener<E> = (e: E) => void
42
+ type InputEventMap = {
43
+ keydown: InputKeyEvent
44
+ keyup: InputKeyEvent
45
+ gamepadconnected: InputGamepadEvent
46
+ gamepaddisconnected: InputGamepadEvent
47
+ }
48
+
49
+ const listeners: { [K in InputEventName]?: Listener<InputEventMap[K]>[] } = {}
50
+ let hostListenerInstalled = false
51
+
52
+ const sourceOf = (code: string): InputKeyEvent["source"] =>
53
+ code.startsWith("Mouse") ? "mouse" : code.startsWith("Gamepad") ? "gamepad" : "keyboard"
54
+
55
+ const emit = <K extends InputEventName>(name: K, e: InputEventMap[K]) => {
56
+ const list = listeners[name]
57
+ if (!list || list.length === 0) return
58
+ for (const fn of [...list]) fn(e as any)
59
+ }
60
+
61
+ // One fan-out for the host channel and the test/host `_emit` seam.
62
+ const deliver = (kind: number, code: string, gamepad: number, repeat: number) => {
63
+ if (kind === 0 || kind === 1) {
64
+ const src = sourceOf(code)
65
+ emit(kind === 0 ? "keydown" : "keyup", {
66
+ type: kind === 0 ? "keydown" : "keyup", code, source: src,
67
+ gamepad: src === "gamepad" && gamepad >= 0 ? gamepad : undefined, repeat: repeat !== 0,
68
+ })
69
+ } else if (kind === 2 || kind === 3) {
70
+ emit(kind === 2 ? "gamepadconnected" : "gamepaddisconnected",
71
+ { type: kind === 2 ? "gamepadconnected" : "gamepaddisconnected", gamepad })
72
+ }
73
+ }
74
+
75
+ // Lazily claim the host's single listener slot (never at module scope — chisel drops top-level
76
+ // side effects, and a host without the channel simply never fires).
77
+ const ensureHostListener = () => {
78
+ if (hostListenerInstalled) return
79
+ hostListenerInstalled = true
80
+ _creatorUtils.registerInputEvent?.(deliver)
81
+ }
82
+
83
+ const read = (channel: number): number => _creatorUtils.inputRead?.(channel) ?? 0
84
+
85
+ /** Mouse state — polled. Position is the cursor inside the viewport in logical px; `delta` is the
86
+ * motion during the previous frame (raw where the OS offers it) and keeps counting while locked —
87
+ * that's FPS look. Hosts without a mouse report zeros. */
88
+ export const mouse = {
89
+ get position(): { x: number, y: number } { return { x: read(InputChannel.MouseX), y: read(InputChannel.MouseY) } },
90
+ get delta(): { x: number, y: number } { return { x: read(InputChannel.MouseDX), y: read(InputChannel.MouseDY) } },
91
+ /** Wheel notches during the previous frame (+y = wheel toward you / scroll down). */
92
+ get wheel(): { x: number, y: number } { return { x: read(InputChannel.WheelX), y: read(InputChannel.WheelY) } },
93
+ /** Pointer lock: cursor hidden + confined, `delta` keeps flowing. Web grants it only from a user
94
+ * gesture (call it in a 'keydown' handler for MouseLeft); hosts drop it on focus loss and
95
+ * re-acquire on focus. Escape does NOT unlock by itself on native hosts — call `unlock()`. */
96
+ get locked(): boolean { return read(InputChannel.PointerLocked) !== 0 },
97
+ lock(): boolean { return _creatorUtils.inputSetPointerLock?.(true) ?? false },
98
+ unlock(): void { _creatorUtils.inputSetPointerLock?.(false) },
99
+ }
100
+
101
+ export type GamepadAxisName = "leftX" | "leftY" | "rightX" | "rightY" | "leftTrigger" | "rightTrigger"
102
+ const PAD_AXIS: Record<GamepadAxisName, number> = {
103
+ leftX: InputChannel.PadLeftX, leftY: InputChannel.PadLeftY, rightX: InputChannel.PadRightX,
104
+ rightY: InputChannel.PadRightY, leftTrigger: InputChannel.PadLeftTrigger, rightTrigger: InputChannel.PadRightTrigger,
105
+ }
106
+
107
+ export interface GamepadState {
108
+ readonly index: number
109
+ readonly connected: boolean
110
+ /** Stick axes −1..1 (+Y = down, like the web Gamepad API), triggers 0..1. `deadzone` (default
111
+ * 0.15) zeroes small stick drift — pass 0 for the raw value. */
112
+ axis(name: GamepadAxisName, deadzone?: number): number
113
+ /** Held? Same as `Input.key(code, index)`. */
114
+ button(code: string): boolean
115
+ }
116
+
117
+ const padCache: GamepadState[] = []
118
+ const gamepadAt = (index: number): GamepadState => {
119
+ let g = padCache[index]
120
+ if (!g) {
121
+ const base = InputChannel.GamepadBase + InputChannel.GamepadStride * index
122
+ g = padCache[index] = {
123
+ index,
124
+ get connected() { return read(base + InputChannel.PadConnected) !== 0 },
125
+ axis(name, deadzone = 0.15) {
126
+ const v = read(base + PAD_AXIS[name])
127
+ return deadzone > 0 && Math.abs(v) < deadzone ? 0 : v
128
+ },
129
+ button(code) { return _creatorUtils.inputKey(code, index) },
130
+ }
131
+ }
132
+ return g
133
+ }
134
+
135
+ export const Input = {
136
+ /**
137
+ * Is a button held? `code` is the physical key (`KeyboardEvent.code`: 'ArrowRight', 'KeyW',
138
+ * 'Space'), a mouse button ('MouseLeft', 'MouseRight', 'MouseMiddle') or a gamepad button
139
+ * ('GamepadSouth' = A/Cross, 'GamepadR2' = right trigger as a button …). Poll inside setLoop for
140
+ * frame-independent movement. `gamepad` scopes a Gamepad* code to one pad; omitted = any pad.
141
+ */
142
+ key(code: string, gamepad?: number): boolean {
143
+ return _creatorUtils.inputKey(code, gamepad)
144
+ },
145
+
146
+ /** Listen for a discrete input moment. `keydown`/`keyup` cover EVERY button — keyboard, mouse,
147
+ * gamepad — filter on `e.code`. Use these for one-shot actions (jump, shoot, charge-release);
148
+ * `Input.key` for continuous ones (walk). */
149
+ on<K extends InputEventName>(name: K, listener: Listener<InputEventMap[K]>): void {
150
+ ensureHostListener()
151
+ const list = (listeners[name] ??= []) as Listener<InputEventMap[K]>[]
152
+ if (!list.includes(listener)) list.push(listener)
153
+ },
154
+ off<K extends InputEventName>(name: K, listener: Listener<InputEventMap[K]>): void {
155
+ const list = listeners[name] as Listener<InputEventMap[K]>[] | undefined
156
+ if (!list) return
157
+ const i = list.indexOf(listener)
158
+ if (i >= 0) list.splice(i, 1)
159
+ },
160
+
161
+ mouse,
162
+
163
+ /** Gamepad `index` (0..3). Always returns an object — check `.connected`. */
164
+ gamepad(index = 0): GamepadState { return gamepadAt(index) },
165
+ /** Indices of the currently connected gamepads. */
166
+ gamepads(): number[] {
167
+ const out: number[] = []
168
+ const n = read(InputChannel.GamepadCount)
169
+ for (let i = 0; i < 4 && out.length < n; i++) if (gamepadAt(i).connected) out.push(i)
170
+ return out
171
+ },
172
+
173
+ /** @internal — hosts/tests: deliver an event exactly as the host listener would. */
174
+ _emit(kind: number, code: string, gamepad: number, repeat: number): void { deliver(kind, code, gamepad, repeat) },
175
+ }