lecodes-cli 0.7.0 → 0.7.2

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 (36) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +256 -244
  3. package/package.json +4 -4
  4. package/runtime/sdk/compile/bundler.ts +4 -2
  5. package/runtime/sdk/compile/compileProject.ts +6 -1
  6. package/runtime/sdk/g2/Scene2D.ts +51 -5
  7. package/runtime/sdk/gl/Model.ts +9 -0
  8. package/runtime/sdk/gl/Scene.ts +76 -11
  9. package/runtime/sdk/inject.ts +15 -7
  10. package/runtime/sdk/plugins/camera.ts +81 -0
  11. package/runtime/sdk/plugins/geolocation.ts +123 -0
  12. package/runtime/sdk/plugins/permission.ts +7 -0
  13. package/runtime/sdk/plugins/qr.ts +61 -24
  14. package/runtime/sdk/runtime/app.ts +37 -0
  15. package/runtime/sdk/runtime/appEvents.ts +29 -0
  16. package/runtime/sdk/runtime/channel.ts +50 -0
  17. package/runtime/sdk/runtime/clipboard.ts +20 -0
  18. package/runtime/sdk/runtime/datetime.ts +2 -4
  19. package/runtime/sdk/runtime/device.ts +137 -4
  20. package/runtime/sdk/runtime/misc.ts +4 -0
  21. package/runtime/sdk/runtime/service.ts +82 -0
  22. package/runtime/sdk/runtime/touch.ts +26 -0
  23. package/runtime/sdk/ui/NativeView.ts +144 -0
  24. package/runtime/sdk/ui/UI.ts +5 -1
  25. package/runtime/sdk/ui/UIButton.ts +26 -3
  26. package/runtime/sdk/ui/UIInput.ts +2 -2
  27. package/runtime/sdk/ui/UINode.ts +33 -5
  28. package/runtime/sdk/ui/UIScreen.ts +63 -29
  29. package/runtime/sdk/ui/UIScreenHost.ts +3 -2
  30. package/runtime/sdk/ui/UIText.ts +1 -1
  31. package/runtime/sdk/ui/UIVideo.ts +48 -2
  32. package/runtime/sdk/ui/UIWidget.ts +34 -1
  33. package/runtime/sdk/ui/presentable.ts +116 -0
  34. package/runtime/sdk/ui/router.ts +73 -29
  35. package/runtime/sdk-types.json +1 -1
  36. package/runtime/sdk/runtime/camera.ts +0 -31
@@ -0,0 +1,116 @@
1
+ // The Presentable contract — the one navigation model (docs/navigation-presentable-plan.md).
2
+ // Anything that can occupy the app's main slot implements it: UIScreen, Scene (+ARScene),
3
+ // Scene2D, NativeView, UIVideo. Exactly one Presentable is visible at a time — a screen no
4
+ // longer overlays a scene, it replaces it (with a transition); UI over a scene goes through
5
+ // `UIWidget.attachTo(scene)`.
6
+ //
7
+ // This module is dependency-free on purpose: gl/ and g2/ import it, so it must not pull any UI
8
+ // element code into engine-only bundles.
9
+
10
+ /** Built-in transition catalog. `push`/`pop` are the stacked-navigation pair (slide + parallax +
11
+ * dim + edge shadow); the rest are simple two-view moves. */
12
+ export type TransitionName =
13
+ | "push" | "pop"
14
+ | "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom"
15
+ | "zoom" | "zoom-in" | "zoom-out" | "fade" | "none"
16
+
17
+ /** Start/end pose of one side of a custom transition. `x`/`y` are logical px, or a percentage of
18
+ * the destination's size (e.g. `"100%"` = one full width to the right). */
19
+ export type TransitionTransform = {
20
+ x?: number | `${number}%`
21
+ y?: number | `${number}%`
22
+ scale?: number
23
+ opacity?: number
24
+ }
25
+
26
+ /**
27
+ * A custom transition: declarative poses for the incoming/outgoing destination, played natively.
28
+ * Serializes onto each platform's existing transition machinery — there is no per-frame JS.
29
+ * Fields omitted keep the identity pose. Interactive back-swipe stays system-native (push/pop
30
+ * only); a spec applies to non-interactive transitions.
31
+ */
32
+ export interface TransitionSpec {
33
+ /** Duration in ms (default 300). */
34
+ duration?: number
35
+ /** cubic-bezier control points (default ease-out). */
36
+ easing?: [number, number, number, number]
37
+ /** The entering destination animates FROM this pose to identity. */
38
+ incoming?: { from?: TransitionTransform }
39
+ /** The leaving destination animates from identity TO this pose. */
40
+ outgoing?: { to?: TransitionTransform }
41
+ /** Scrim alpha (0..1) painted over whichever destination is underneath. */
42
+ dim?: number
43
+ /** Which side stacks on top during the animation (default "incoming"). On Android, transitions
44
+ * involving a scene always animate the screen side on top — the scene view itself never moves. */
45
+ onTop?: "incoming" | "outgoing"
46
+ }
47
+
48
+ export type Transition = TransitionName | TransitionSpec
49
+
50
+ export type PresentOptions = {
51
+ /** Transition to play while this destination replaces the current one (default "none" for a
52
+ * direct open(); the Router applies its own defaults — push/fade/pop). */
53
+ transition?: Transition
54
+ }
55
+
56
+ /**
57
+ * Anything that can be shown as the app's current destination: a `UIScreen`, a `Scene` /
58
+ * `ARScene`, a `Scene2D`, a `NativeView`, or a `UIVideo`. One Presentable is visible at a time;
59
+ * open it directly (`p.open()` — replaces the current destination, suspending an active Router
60
+ * until `Router.restore()`) or navigate with `Router.push/replace/pop`.
61
+ *
62
+ * `onOpen`/`onClose` are the presentation lifecycle (fired when the destination becomes / stops
63
+ * being the visible one — including router pushes covering it and pops revealing it).
64
+ */
65
+ export interface Presentable {
66
+ // Return type covers the implementers: UIScreen → void, Scene2D → this, ARScene → Promise.
67
+ open(options?: PresentOptions): void | this | Promise<void>
68
+ close(): void
69
+ onOpen(callback: () => void): this
70
+ onClose(callback: () => void): this
71
+ onBackPressed(callback: () => void): this
72
+ /** @internal Async work that must finish BEFORE this destination replaces the current one
73
+ * (camera permission, warm render, session launch — ARScene). The previous destination —
74
+ * typically a loading screen — stays visible while it runs; `open()` and `Router.push/replace`
75
+ * await it and skip the swap entirely if it rejects (permission denied, or superseded by
76
+ * another navigation that happened while preparing). */
77
+ _prepare?(): Promise<void>
78
+ }
79
+
80
+ // The destination visible right now. Set optimistically by every successful open()/router
81
+ // navigation, cleared by close(), and corrected by router onChange (hosts hand the wire desc
82
+ // back on every stack change, including back gestures — `_p` resolves it to the SDK instance).
83
+ let current: Presentable | null = null
84
+ /** @internal Call whenever the visible destination changes. */
85
+ export const _setCurrent = (p: Presentable | null): void => { current = p }
86
+
87
+ /** Runtime companion of the `Presentable` interface (declaration merging): the navigation
88
+ * state that isn't tied to the Router. */
89
+ export const Presentable = /*#__PURE__*/ Object.freeze({
90
+ /** The destination visible right now (a `UIScreen`, `Scene`, `Scene2D`, `NativeView` or
91
+ * `UIVideo`), or `null` before the first open. Distinct from `Router.current` — that is the
92
+ * top of the router stack, which stays meaningful while the router is suspended by a direct
93
+ * `open()`; this is what is actually on screen. `UIWidget.show()` attaches to it. */
94
+ get current(): Presentable | null { return current },
95
+ })
96
+
97
+ // Monotonic navigation epoch: every destination change (open/close/router nav) bumps it. Async
98
+ // preparers capture the epoch before their awaits and reject as "superseded" if it moved — so a
99
+ // slow AR prepare can't steal the screen from a navigation the user made in the meantime.
100
+ let navEpoch = 0
101
+ /** @internal */
102
+ export const _navEpoch = (): number => navEpoch
103
+ /** @internal Call on every action that changes the visible destination. */
104
+ export const _bumpNavEpoch = (): void => { navEpoch++ }
105
+
106
+ /** @internal Does this host implement the Presentable navigation surface? The SDK falls back to
107
+ * the legacy openScreen/openScene bridges when it doesn't (screens keep working; scene/native
108
+ * destinations in the Router then throw). */
109
+ export const _navSupported = (): boolean =>
110
+ typeof _creatorUI !== "undefined" && typeof _creatorUI.openView === "function"
111
+
112
+ /** @internal The wire descriptor for a Presentable. Screens (and anything without `_viewDesc`)
113
+ * travel as themselves; scenes/native views/video provide a stable descriptor object carrying
114
+ * `_p` (the SDK instance) so hosts can hand it back through router onChange / lifecycle. */
115
+ export const _descOf = (p: Presentable): object =>
116
+ typeof (p as any)._viewDesc === "function" ? (p as any)._viewDesc() : (p as object)
@@ -1,68 +1,112 @@
1
- import { UIScreen } from "./UIScreen";
1
+ import { _bumpNavEpoch, _descOf, _navSupported, _setCurrent, Presentable, type Transition } from "./presentable"
2
2
 
3
3
  type RouterOptions = {
4
4
  showDefaultBackButton?: boolean
5
5
  }
6
6
 
7
+ type NavigateOptions = {
8
+ /** Transition to play. Defaults: `"push"` for push, `"fade"` for replace, `"pop"` for pop. */
9
+ transition?: Transition
10
+ }
11
+
7
12
  type Router = {
8
- get current(): UIScreen<boolean>
9
- init<T extends boolean>(homePage: UIScreen<T>, opts?: RouterOptions): void
10
- push<T extends boolean>(screen: UIScreen<T>): void
11
- replace<T extends boolean>(screen: UIScreen<T>, options?: ReplaceOptions): void
12
- pop(to?: number): void
13
+ get current(): Presentable
14
+ init(homePage: Presentable, opts?: RouterOptions): void
15
+ /** Push a destination onto the stack — a `UIScreen`, a `Scene`, a `Scene2D`, a `NativeView`,
16
+ * or a `UIVideo`. The back gesture/button pops it. Destinations with async preparation
17
+ * (`ARScene`: camera permission + warm render) return a Promise — the current page stays
18
+ * visible until they're ready, and a rejection (permission denied / superseded) leaves the
19
+ * stack untouched. */
20
+ push(page: Presentable, opts?: NavigateOptions): void | Promise<void>
21
+ replace(page: Presentable, opts?: NavigateOptions): void | Promise<void>
22
+ pop(to?: number, opts?: NavigateOptions): void
13
23
  hide(): void
14
24
  restore(): void
15
25
 
16
- addEventListener(channel: "change", callback: (screen: UIScreen<boolean>) => void): void
17
- removeEventListener(channel: "change", callback: (screen: UIScreen<boolean>) => void): void
26
+ addEventListener(channel: "change", callback: (page: Presentable) => void): void
27
+ removeEventListener(channel: "change", callback: (page: Presentable) => void): void
18
28
  }
19
29
 
20
- let currentScreen: UIScreen<any> | null = null
21
- let onChange = <T extends boolean>(screen: UIScreen<T>) => {
22
- currentScreen = screen;
30
+ let currentPage: Presentable | null = null
31
+ let onChange = (page: any) => {
32
+ // Hosts hand back the wire object: the screen itself, or a descriptor carrying `_p`.
33
+ currentPage = page && page._p ? page._p : page
34
+ // The router's page is the visible destination (hosts fire this on every stack change,
35
+ // including back gestures) — keep Presentable.current in sync.
36
+ _setCurrent(currentPage)
23
37
  for (let callback of (Router as any)._changeListeners) {
24
- callback(screen)
38
+ callback(currentPage)
25
39
  }
26
40
  }
27
41
 
28
- type ReplaceOptions = {
29
- transition: "slide-from-left" | "slide-from-right" | "slide-from-top" | "slide-from-bottom" |
30
- "zoom" | "zoom-out" | "zoom-in" | "fade" | "none"
42
+ // Non-screen destinations have no legacy bridge to fall back to — surface that clearly instead
43
+ // of letting an old host try to build a screen out of a scene descriptor.
44
+ const wireDesc = (page: Presentable): object => {
45
+ const desc = _descOf(page)
46
+ if (desc !== page && !_navSupported()) {
47
+ throw new Error("This host does not support scene/native-view navigation (requires _creatorUI.openView)")
48
+ }
49
+ return desc
31
50
  }
32
51
 
33
52
  export const Router: Router = /*#__PURE__*/ Object.freeze({
34
53
 
35
- init(homePage: UIScreen<any>, opts: RouterOptions = {}): void {
36
- _creatorUI.routerOpen(homePage, onChange, opts.showDefaultBackButton ?? false)
37
- currentScreen = homePage
54
+ init(homePage: Presentable, opts: RouterOptions = {}): void {
55
+ _bumpNavEpoch()
56
+ _creatorUI.routerOpen(wireDesc(homePage), onChange, opts.showDefaultBackButton ?? false)
57
+ currentPage = homePage
58
+ _setCurrent(homePage)
38
59
  },
39
- push(screen: UIScreen<any>): void {
40
- _creatorUI.routerPush(screen)
60
+ push(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
61
+ const desc = wireDesc(page) // throws early on unsupported hosts, before any async work
62
+ const go = () => {
63
+ _bumpNavEpoch()
64
+ // Legacy hosts ignore the extra transition argument.
65
+ _creatorUI.routerPush(desc, opts?.transition ?? "push")
66
+ _setCurrent(page) // optimistic — host onChange confirms (async on native hosts)
67
+ }
68
+ // A preparing destination (ARScene) keeps the current page visible until it's ready; a
69
+ // rejection (camera denied / superseded) propagates to the caller and nothing is pushed.
70
+ const prep = page._prepare?.()
71
+ return prep ? prep.then(go) : go()
41
72
  },
42
- replace(screen: UIScreen<any>, opts?: ReplaceOptions): void {
43
- _creatorUI.routerReplace(screen, opts?.transition ?? "fade")
73
+ replace(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
74
+ const desc = wireDesc(page)
75
+ const go = () => {
76
+ _bumpNavEpoch()
77
+ _creatorUI.routerReplace(desc, opts?.transition ?? "fade")
78
+ _setCurrent(page) // optimistic — host onChange confirms (async on native hosts)
79
+ }
80
+ const prep = page._prepare?.()
81
+ return prep ? prep.then(go) : go()
44
82
  },
45
- pop(to?: number): void {
46
- _creatorUI.routerPop(to ?? -1)
83
+ pop(to?: number, opts?: NavigateOptions): void {
84
+ _bumpNavEpoch()
85
+ _creatorUI.routerPop(to ?? -1, opts?.transition ?? "pop")
47
86
  },
48
87
  hide() {
88
+ _bumpNavEpoch()
49
89
  _creatorUI.routerHide()
90
+ // hide() only blanks the router's own page — a directly-opened destination stays visible.
91
+ if (Presentable.current === currentPage) _setCurrent(null)
50
92
  },
51
93
  restore() {
94
+ _bumpNavEpoch()
52
95
  _creatorUI.routerRestore()
96
+ _setCurrent(currentPage) // the stack top is visible again
53
97
  },
54
98
 
55
- get current(): UIScreen<any> {
56
- return currentScreen!
99
+ get current(): Presentable {
100
+ return currentPage!
57
101
  },
58
-
102
+
59
103
  _changeListeners: [] as any[],
60
- addEventListener(channel: "change", callback: (screen: UIScreen<boolean>) => void): void {
104
+ addEventListener(channel: "change", callback: (page: Presentable) => void): void {
61
105
  if (channel === "change") {
62
106
  this._changeListeners.push(callback)
63
107
  }
64
108
  },
65
- removeEventListener(channel: "change", callback: (screen: UIScreen<boolean>) => void): void {
109
+ removeEventListener(channel: "change", callback: (page: Presentable) => void): void {
66
110
  if (channel === "change") {
67
111
  let index = this._changeListeners.indexOf(callback)
68
112
  if (index >= 0) {