lecodes-sdk 1.1.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 (257) 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 +11 -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/Particles.d.ts +15 -0
  25. package/dist/types/gl/Scene.d.ts +41 -13
  26. package/dist/types/gl/Texture.d.ts +1 -1
  27. package/dist/types/gl/animation/Locomotion.d.ts +59 -3
  28. package/dist/types/gl/terrain/Terrain.d.ts +8 -0
  29. package/dist/types/inject.d.ts +11 -11
  30. package/dist/types/inject.editor.d.ts +1 -0
  31. package/dist/types/net/core.d.ts +7 -0
  32. package/dist/types/plugin.d.ts +76 -0
  33. package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
  34. package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
  35. package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
  36. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
  37. package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
  38. package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
  39. package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
  40. package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
  41. package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
  42. package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
  43. package/dist/types/runtime/app.d.ts +9 -2
  44. package/dist/types/runtime/fetch.d.ts +2 -0
  45. package/dist/types/runtime/input.d.ts +8 -1
  46. package/dist/types/runtime/media.d.ts +6 -10
  47. package/dist/types/runtime/misc.d.ts +4 -1
  48. package/dist/types/runtime/net.d.ts +3 -2
  49. package/dist/types/runtime/touch.d.ts +32 -0
  50. package/dist/types/scene/defineScene.d.ts +43 -2
  51. package/dist/types/scene/editor.d.ts +52 -0
  52. package/dist/types/scene/gizmos.d.ts +7 -4
  53. package/dist/types/ui/NativeView.d.ts +6 -4
  54. package/dist/types/ui/UI.d.ts +1 -1
  55. package/dist/types/ui/UIBottomSheet.d.ts +6 -12
  56. package/dist/types/ui/UIButton.d.ts +12 -14
  57. package/dist/types/ui/UIContainer.d.ts +0 -6
  58. package/dist/types/ui/UIImage.d.ts +1 -4
  59. package/dist/types/ui/UIInput.d.ts +8 -24
  60. package/dist/types/ui/UIModal.d.ts +0 -2
  61. package/dist/types/ui/UINode.d.ts +70 -56
  62. package/dist/types/ui/UIPager.d.ts +28 -27
  63. package/dist/types/ui/UIPopover.d.ts +0 -2
  64. package/dist/types/ui/UIScreen.d.ts +12 -17
  65. package/dist/types/ui/UIScrollable.d.ts +1 -4
  66. package/dist/types/ui/UIText.d.ts +0 -2
  67. package/dist/types/ui/UIVideo.d.ts +3 -5
  68. package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
  69. package/dist/types/ui/UIWidget.d.ts +10 -9
  70. package/dist/types/ui/colorKeys.gen.d.ts +9 -0
  71. package/dist/types/ui/presentable.d.ts +46 -32
  72. package/dist/types/ui/router.d.ts +18 -7
  73. package/dist/types/ui/styleColor.d.ts +1 -0
  74. package/dist/types/ui/transitions.d.ts +18 -0
  75. package/dist/types/ui/tree.d.ts +75 -0
  76. package/dist/types/version.d.ts +10 -0
  77. package/dist/types.json +1 -1
  78. package/package.json +12 -3
  79. package/prompts/2d.md +2 -6
  80. package/prompts/3d.md +1 -5
  81. package/prompts/README.md +142 -142
  82. package/prompts/canvas.md +9 -8
  83. package/prompts/compose.ts +1 -1
  84. package/prompts/core.md +3 -3
  85. package/prompts/design.md +1 -1
  86. package/prompts/dist/2d-game.md +77 -54
  87. package/prompts/dist/3d-app.md +75 -52
  88. package/prompts/dist/ar-app.md +74 -51
  89. package/prompts/dist/design.md +30 -8
  90. package/prompts/dist/ui-app.md +73 -46
  91. package/prompts/ui-design.md +2 -3
  92. package/prompts/ui.md +45 -37
  93. package/src/animate/tween/Animation.ts +262 -378
  94. package/src/animate/tween/animateValue.ts +103 -100
  95. package/src/animate/tween/easing.ts +10 -3
  96. package/src/animate/tween/spec.ts +505 -479
  97. package/src/audio/Sound.ts +3 -3
  98. package/src/audio/audio.ts +162 -161
  99. package/src/bridges/2d.d.ts +317 -0
  100. package/src/bridges/app.d.ts +91 -0
  101. package/src/bridges/audio.d.ts +97 -0
  102. package/src/bridges/canvas.d.ts +79 -0
  103. package/src/bridges/device.d.ts +72 -0
  104. package/src/bridges/fetch.d.ts +80 -0
  105. package/src/bridges/files.d.ts +70 -0
  106. package/src/bridges/gl.d.ts +1133 -0
  107. package/src/bridges/input.d.ts +72 -0
  108. package/src/bridges/media.d.ts +55 -0
  109. package/src/bridges/nav.d.ts +71 -0
  110. package/src/bridges/net.d.ts +52 -0
  111. package/src/bridges/service.d.ts +52 -0
  112. package/src/bridges/socket.d.ts +31 -0
  113. package/src/bridges/storage.d.ts +33 -0
  114. package/src/bridges/tree.d.ts +301 -0
  115. package/src/bridges/types.d.ts +49 -0
  116. package/src/canvas/Canvas.ts +114 -159
  117. package/src/canvas/gen/cssColor.ts +224 -0
  118. package/src/canvas/gen/recorder.ts +212 -0
  119. package/src/canvas/gen/spec.ts +201 -0
  120. package/src/chisel.ts +193 -0
  121. package/src/compile/assetMacro.ts +1 -1
  122. package/src/compile/bundler.ts +11 -2
  123. package/src/compile/compileProject.ts +43 -4
  124. package/src/compile/fontMacro.ts +3 -4
  125. package/src/compile/header.ts +26 -5
  126. package/src/compile/index.ts +3 -1
  127. package/src/compile/liteMaterial.ts +1 -1
  128. package/src/compile/sceneEditor.ts +11 -26
  129. package/src/core/color.ts +73 -30
  130. package/src/core/pins.ts +51 -0
  131. package/src/core/signals.ts +8 -1
  132. package/src/g2/CharacterController2D.ts +3 -3
  133. package/src/g2/Node2D.ts +57 -39
  134. package/src/g2/Physics2D.ts +2 -2
  135. package/src/g2/Scene2D.ts +35 -23
  136. package/src/g2/Texture2D.ts +1 -1
  137. package/src/g2/loop.ts +4 -4
  138. package/src/gl/Foliage.ts +72 -17
  139. package/src/gl/Geometry.ts +1 -2
  140. package/src/gl/Light.ts +13 -0
  141. package/src/gl/Lightmap.ts +45 -29
  142. package/src/gl/Material.ts +95 -51
  143. package/src/gl/Model.ts +172 -167
  144. package/src/gl/Node.ts +91 -24
  145. package/src/gl/Particles.ts +20 -1
  146. package/src/gl/Scene.ts +100 -44
  147. package/src/gl/Texture.ts +8 -7
  148. package/src/gl/animation/AnimationClip.ts +1 -1
  149. package/src/gl/animation/Locomotion.ts +77 -8
  150. package/src/gl/nav/NavMesh.ts +3 -4
  151. package/src/gl/physics/Physics.ts +2 -2
  152. package/src/gl/physics/physicsEvents.ts +3 -3
  153. package/src/gl/terrain/Terrain.ts +33 -5
  154. package/src/gl/touch.ts +14 -15
  155. package/src/host.d.ts +2 -3
  156. package/src/inject.editor.ts +7 -0
  157. package/src/inject.ts +233 -236
  158. package/src/net/core.ts +6 -5
  159. package/src/net/index.ts +1 -1
  160. package/src/net/replication.ts +1 -1
  161. package/src/plugin.ts +191 -0
  162. package/src/plugins/gen/camera/contract.d.ts +27 -0
  163. package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
  164. package/src/plugins/gen/camera/sdk/camera.ts +57 -0
  165. package/src/plugins/gen/geolocation/contract.d.ts +50 -0
  166. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
  167. package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
  168. package/src/plugins/gen/map/contract.d.ts +144 -0
  169. package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
  170. package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
  171. package/src/plugins/gen/push/contract.d.ts +61 -0
  172. package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
  173. package/src/plugins/gen/push/sdk/push.ts +105 -0
  174. package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
  175. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
  176. package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
  177. package/src/plugins/permission.ts +5 -4
  178. package/src/runtime/app.ts +20 -9
  179. package/src/runtime/appEvents.ts +5 -4
  180. package/src/runtime/channel.ts +18 -15
  181. package/src/runtime/clipboard.ts +4 -3
  182. package/src/runtime/datetime.ts +2 -1
  183. package/src/runtime/device.ts +17 -15
  184. package/src/runtime/fetch.ts +30 -20
  185. package/src/runtime/files.ts +16 -15
  186. package/src/runtime/input.ts +21 -8
  187. package/src/runtime/media.ts +50 -46
  188. package/src/runtime/misc.ts +7 -3
  189. package/src/runtime/net.ts +8 -7
  190. package/src/runtime/rpc.ts +1 -3
  191. package/src/runtime/service.ts +19 -14
  192. package/src/runtime/share.ts +4 -3
  193. package/src/runtime/storage.ts +6 -4
  194. package/src/runtime/touch.ts +32 -0
  195. package/src/scene/defineScene.ts +61 -365
  196. package/src/scene/editor.ts +408 -0
  197. package/src/scene/editorPlugins.ts +3 -3
  198. package/src/scene/gizmos.ts +154 -148
  199. package/src/server/db/marci/query.ts +1 -1
  200. package/src/server/host.ts +1 -1
  201. package/src/server/runtime.ts +1 -1
  202. package/src/ui/NativeView.ts +72 -25
  203. package/src/ui/UI.ts +3 -3
  204. package/src/ui/UIBottomSheet.ts +16 -17
  205. package/src/ui/UIButton.ts +54 -16
  206. package/src/ui/UIContainer.ts +0 -6
  207. package/src/ui/UIImage.ts +34 -30
  208. package/src/ui/UIInput.ts +29 -37
  209. package/src/ui/UIModal.ts +1 -3
  210. package/src/ui/UINode.ts +347 -297
  211. package/src/ui/UIPager.ts +93 -78
  212. package/src/ui/UIPopover.ts +0 -2
  213. package/src/ui/UIScreen.ts +41 -36
  214. package/src/ui/UIScrollable.ts +19 -13
  215. package/src/ui/UISpacer.ts +1 -1
  216. package/src/ui/UITabs.ts +8 -6
  217. package/src/ui/UIText.ts +7 -17
  218. package/src/ui/UIVideo.ts +30 -27
  219. package/src/ui/UIVirtualizedList.ts +58 -59
  220. package/src/ui/UIWidget.ts +38 -18
  221. package/src/ui/colorKeys.gen.ts +37 -0
  222. package/src/ui/fonts.ts +2 -2
  223. package/src/ui/presentable.ts +59 -42
  224. package/src/ui/router.ts +52 -35
  225. package/src/ui/styleColor.ts +56 -0
  226. package/src/ui/theme.ts +7 -6
  227. package/src/ui/transitions.ts +249 -0
  228. package/src/ui/tree.ts +346 -0
  229. package/src/version.ts +24 -0
  230. package/tests/helpers/engineWorld.ts +11 -0
  231. package/tests/helpers/fakeTree.ts +353 -0
  232. package/tests/helpers/hostStubs.ts +31 -0
  233. package/tests/helpers/index.ts +14 -0
  234. package/tests/helpers/memoryMarci.ts +124 -0
  235. package/tests/helpers/phases.ts +23 -0
  236. package/tests/helpers/preload.ts +18 -0
  237. package/tests/helpers/stubApp.ts +2 -0
  238. package/tests/helpers/stubDevice.ts +2 -0
  239. package/tests/helpers/stubFetch.ts +2 -0
  240. package/tests/helpers/stubInput.ts +2 -0
  241. package/dist/inject.js +0 -4730
  242. package/dist/types/core/registry.d.ts +0 -7
  243. package/dist/types/plugins/camera.d.ts +0 -25
  244. package/dist/types/plugins/push.d.ts +0 -46
  245. package/src/bridges.d.ts +0 -1760
  246. package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
  247. package/src/compile/__tests__/assetMacro.test.ts +0 -100
  248. package/src/compile/__tests__/assetName.test.ts +0 -55
  249. package/src/compile/__tests__/compile.test.ts +0 -310
  250. package/src/compile/__tests__/detectEntry.test.ts +0 -151
  251. package/src/compile/__tests__/fontMacro.test.ts +0 -199
  252. package/src/compile/__tests__/serverSplit.test.ts +0 -27
  253. package/src/core/__tests__/stateMachine.test.ts +0 -132
  254. package/src/core/registry.ts +0 -23
  255. package/src/plugins/camera.ts +0 -81
  256. package/src/plugins/push.ts +0 -132
  257. package/src/plugins/qr.ts +0 -73
@@ -0,0 +1,105 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/push/sdk/push.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // Push — remote notifications. The host owns the OS token, the permission prompt and the
4
+ // registration with the le.codes relay; the app only ever sees the opaque project-scoped ADDRESS
5
+ // (never the raw APNs / FCM token) plus payloads.
6
+ //
7
+ // Hand-written over the generated wire because the public API is a module singleton with
8
+ // `addEventListener`, and a listener must OPEN the session: events arrive on a live one only.
9
+ //
10
+ // The app is not running when a push arrives — display is host / OS work. The JS world sees
11
+ // exactly three delivery paths:
12
+ // - event "message": arrived while this app was foregrounded (the host suppresses the banner)
13
+ // - event "tap": tapped while the world was alive (warm)
14
+ // - getLaunch(): the notification that cold-started this world (stable pull, or null)
15
+ // A payload's `url` additionally rides the app-lifecycle deep-link path (`app.launchUrl` /
16
+ // the "url" event), so apps that already route links get notification routing for free.
17
+ //
18
+ // The OS permission prompt happens inside `register()`; `getStatus()` and `getLaunch()` never
19
+ // prompt. On a host without the service every method rejects at once, BEFORE any permission path.
20
+
21
+ import { NAME, PushBase } from "./push.gen"
22
+ import type { PushEvents, PushPayload, PushRegisterOptions, PushRegistration, PushStatus } from "../contract"
23
+
24
+ export type { PushPayload, PushRegisterOptions, PushStatus }
25
+
26
+ export type PushEvent = keyof PushEvents
27
+
28
+ /**
29
+ * Remote push notifications. `Push.isSupported` reports whether this host registered the
30
+ * service — check it before offering notification features.
31
+ */
32
+ // PURE IIFE so an app that never uses push tree-shakes the whole plugin away.
33
+ export const Push: {
34
+ /** Whether this host registered a "push" service. */
35
+ readonly isSupported: boolean
36
+ /** Permission + registration state. Never prompts. */
37
+ getStatus(): Promise<PushStatus>
38
+ /** Ask for permission (first time) and register this device → the opaque address your server
39
+ * targets. Idempotent — safe to call every launch; rejects "denied" / "unavailable". */
40
+ register(options?: PushRegisterOptions): Promise<PushRegistration>
41
+ /** Remove this device's registration (the address stops receiving). */
42
+ unregister(): Promise<void>
43
+ /** The notification that cold-started this app run, or null. Stable across calls. */
44
+ getLaunch(): Promise<PushPayload | null>
45
+ /** "message" = received while the app is open; "tap" = the user tapped one while it ran. */
46
+ addEventListener(event: PushEvent, callback: (payload: PushPayload) => void): void
47
+ removeEventListener(event: PushEvent, callback: (payload: PushPayload) => void): void
48
+ } = /*#__PURE__*/ (() => {
49
+ let client: PushBase | null = null
50
+ const service = (): PushBase => client ??= new PushBase()
51
+
52
+ const assertSupported = (): void => {
53
+ // Fail before the host is touched — a permission dialog for a feature that can't exist
54
+ // would be worse than the error.
55
+ if (!PushBase.supports(NAME)) {
56
+ throw new Error('Push isn\'t available on this host — it needs a registered "push" service.')
57
+ }
58
+ }
59
+
60
+ // `on` only records listeners; events need a live session to arrive on. Poke the host with the
61
+ // prompt-free getStatus once so a listener-only app (already registered on a previous run)
62
+ // still hears warm taps.
63
+ let pipeOpened = false
64
+ const ensurePipe = (): void => {
65
+ if (pipeOpened || !PushBase.supports(NAME)) return
66
+ pipeOpened = true
67
+ service().getStatus().catch(() => { pipeOpened = false })
68
+ }
69
+
70
+ const push = {
71
+ async getStatus(): Promise<PushStatus> {
72
+ assertSupported()
73
+ return await service().getStatus()
74
+ },
75
+
76
+ async register(options?: PushRegisterOptions): Promise<PushRegistration> {
77
+ assertSupported()
78
+ return await service().register(options ?? {})
79
+ },
80
+
81
+ async unregister(): Promise<void> {
82
+ assertSupported()
83
+ await service().unregister()
84
+ },
85
+
86
+ async getLaunch(): Promise<PushPayload | null> {
87
+ assertSupported()
88
+ return (await service().getLaunch()) ?? null
89
+ },
90
+
91
+ addEventListener(event: PushEvent, callback: (payload: PushPayload) => void): void {
92
+ // Recording on an unsupported host is a silent no-op (the events just never fire),
93
+ // like app / device listeners — only the promise-returning methods reject loudly.
94
+ service().on(event, callback)
95
+ ensurePipe()
96
+ },
97
+ removeEventListener(event: PushEvent, callback: (payload: PushPayload) => void): void {
98
+ service().off(event, callback)
99
+ },
100
+ }
101
+ Object.defineProperty(push, "isSupported", {
102
+ get: (): boolean => PushBase.supports(NAME),
103
+ })
104
+ return push as any
105
+ })()
@@ -0,0 +1,16 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/qr-scanner/contract.d.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // qr-scanner — a camera view that decodes QR codes.
4
+ // A view with events only: the app opens / embeds it and listens.
5
+
6
+ import type { View } from "../../../plugin"
7
+
8
+ export interface QRScannerParams {}
9
+
10
+ export interface QRScannerEvents {
11
+ /** One per decoded camera frame: the payload, or null for a frame with no readable code (it
12
+ * repeats while the user aims). The same code may be reported more than once. */
13
+ scan: { data: string | null }
14
+ }
15
+
16
+ export interface QRScanner extends View<"qrScanner", QRScannerParams, QRScannerEvents> {}
@@ -0,0 +1,29 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/qr-scanner/sdk/qr-scanner.gen.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // GENERATED by `lecodes plugin gen` from contract.d.ts — do not edit.
4
+ // qr-scanner: the view "qrScanner", contract version 1.
5
+
6
+ import { ViewChannel } from "../../../../plugin"
7
+ import type { QRScannerEvents, QRScannerParams } from "../contract"
8
+
9
+ /** What the halves register under. */
10
+ export const NAME = "qrScanner"
11
+ /** The contract's version: the highest `@since` of its members. */
12
+ export const VERSION = 1
13
+
14
+ // ---- what the host's half has --------------------------------------------------------------------
15
+
16
+ /** A method of the contract. */
17
+ export type Method = never
18
+
19
+ /** Whether this host has the view "qrScanner" — and, given a method, a half new enough for it
20
+ * (a method added after the first version of the contract is tagged `@since`). */
21
+ export const supports = (method?: Method): boolean =>
22
+ ViewChannel.supports(NAME) // every method is of version 1
23
+
24
+ // ---- the element ----------------------------------------------------------------------------------
25
+
26
+ /** The view "qrScanner" with the wire's methods as its own. */
27
+ export class QRScannerBase extends ViewChannel<QRScannerEvents> {
28
+ constructor(params: QRScannerParams = {}) { super("QRScanner", NAME, params) }
29
+ }
@@ -0,0 +1,52 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/qr-scanner/sdk/qr-scanner.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // QRScanner — the host's camera QR scanner as a view the app opens fullscreen
4
+ // (`await scanner.open()`), pushes (`Router.push(scanner)`) or embeds as a live preview.
5
+ //
6
+ // Hand-written over the generated wire for two things: `onScan` hands over the decoded string, not
7
+ // the event's payload, and the permission is asked BEFORE the view is presented.
8
+
9
+ import { prepareCamera, type NativeView, type PresentOptions } from "../../../../plugin"
10
+ import { NAME, QRScannerBase } from "./qr-scanner.gen"
11
+
12
+ export interface QRScanner extends NativeView {
13
+ /** Fires per decoded camera frame: the decoded string, or `null` for a frame with no readable
14
+ * code (expect those repeatedly while the user aims). The same code can be reported more than
15
+ * once — `close()` or debounce once you have what you need. */
16
+ onScan(callback: (data: string | null) => void): this
17
+ /** Request the camera permission, then present the scanner. The current destination (and its
18
+ * loading state) stays visible until the scanner is ready; rejects if the camera is denied,
19
+ * the host can't scan, or another navigation superseded this one. */
20
+ open(options?: PresentOptions): Promise<void>
21
+ }
22
+
23
+ class QRScannerElement extends QRScannerBase {
24
+ onScan(callback: (data: string | null) => void): this {
25
+ return this.on("scan", payload => callback(payload?.data ?? null))
26
+ }
27
+
28
+ /** @internal prepare-then-present (see Presentable._prepare). */
29
+ _prepare(): Promise<void> { return prepareCamera(this) }
30
+
31
+ override async open(options?: PresentOptions): Promise<void> {
32
+ await this._prepare()
33
+ super.open(options)
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Create a QR scanner view. `QRScanner.isSupported` reports whether this host can scan at all —
39
+ * check it before offering the feature.
40
+ */
41
+ // PURE IIFE so an app that never scans tree-shakes the whole plugin away.
42
+ export const QRScanner: {
43
+ (): QRScanner
44
+ /** Whether this host registered a "qrScanner" view. */
45
+ readonly isSupported: boolean
46
+ } = /*#__PURE__*/ (() => {
47
+ const factory = (): QRScanner => new QRScannerElement() as unknown as QRScanner
48
+ Object.defineProperty(factory, "isSupported", {
49
+ get: (): boolean => QRScannerBase.supports(NAME),
50
+ })
51
+ return factory as any
52
+ })()
@@ -1,7 +1,8 @@
1
- // Camera permission, host-gated: hosts expose it as `_creatorUtils.requestCamera`. Hosts without
2
- // that bridge resolve immediately — their view factory prompts on its own (e.g. getUserMedia on
1
+ // Camera permission, host-gated: hosts expose it as `_creatorDevice.requestCamera`. Hosts without
2
+ // that slot resolve immediately — their view factory prompts on its own (e.g. getUserMedia on
3
3
  // the web). Shared by ARScene.prepare, QRScanner and CameraView.
4
+
4
5
  export const _requestCameraPermission = (): Promise<void> =>
5
- typeof _creatorUtils.requestCamera === "function"
6
- ? new Promise<void>((res, rej) => _creatorUtils.requestCamera!(res, rej))
6
+ typeof _creatorDevice.requestCamera === "function"
7
+ ? new Promise<void>((res, rej) => _creatorDevice.requestCamera!(res, rej))
7
8
  : Promise.resolve()
@@ -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 {