lecodes-sdk 2.2.9 → 2.2.11

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 (66) hide show
  1. package/dist/global.d.ts +42 -1
  2. package/dist/types/canvas/Canvas.d.ts +6 -6
  3. package/dist/types/gl/Camera.d.ts +5 -2
  4. package/dist/types/gl/Node.d.ts +0 -7
  5. package/dist/types/gl/Scene.d.ts +6 -24
  6. package/dist/types/inject.d.ts +5 -1
  7. package/dist/types/plugin.d.ts +27 -0
  8. package/dist/types/plugins/gen/ar/sdk/ar.d.ts +228 -0
  9. package/dist/types/plugins/gen/ar/sdk/ar.gen.d.ts +73 -0
  10. package/dist/types/plugins/gen/ar/sdk/controls.d.ts +28 -0
  11. package/dist/types/plugins/gen/audio-recorder/sdk/audio-recorder.d.ts +43 -0
  12. package/dist/types/plugins/gen/audio-recorder/sdk/audio-recorder.gen.d.ts +47 -0
  13. package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +6 -2
  14. package/dist/types/plugins/gen/speech/sdk/speech.d.ts +44 -0
  15. package/dist/types/plugins/gen/speech/sdk/speech.gen.d.ts +39 -0
  16. package/dist/types/runtime/media.d.ts +3 -2
  17. package/dist/types/runtime/permissions.d.ts +16 -0
  18. package/dist/types/version.d.ts +1 -1
  19. package/dist/types.json +1 -1
  20. package/package.json +1 -1
  21. package/prompts/ar.md +81 -48
  22. package/prompts/dist/2d-game.md +1567 -0
  23. package/prompts/dist/3d-app.md +1712 -0
  24. package/prompts/dist/ar-app.md +1585 -0
  25. package/prompts/dist/concept.md +49 -0
  26. package/prompts/dist/design.md +521 -0
  27. package/prompts/dist/ui-app.md +1175 -0
  28. package/prompts/index.md +3 -1
  29. package/src/animate/tween/Timeline.ts +175 -175
  30. package/src/bridges/device.d.ts +20 -6
  31. package/src/bridges/gl.d.ts +33 -19
  32. package/src/bridges/media.d.ts +3 -1
  33. package/src/canvas/Canvas.ts +6 -6
  34. package/src/compile/header.ts +11 -4
  35. package/src/gl/Camera.ts +5 -2
  36. package/src/gl/CameraPlace.ts +52 -52
  37. package/src/gl/Light.ts +257 -257
  38. package/src/gl/Mesh.ts +120 -120
  39. package/src/gl/Node.ts +0 -8
  40. package/src/gl/Scene.ts +14 -81
  41. package/src/gl/animation/DynamicBone.ts +482 -482
  42. package/src/gl/scenarios.ts +291 -291
  43. package/src/inject.ts +13 -1
  44. package/src/plugin.ts +45 -2
  45. package/src/plugins/gen/ar/contract.d.ts +118 -0
  46. package/src/plugins/gen/ar/sdk/ar.gen.ts +112 -0
  47. package/src/plugins/gen/ar/sdk/ar.ts +699 -0
  48. package/src/plugins/gen/ar/sdk/controls.ts +148 -0
  49. package/src/plugins/gen/audio-recorder/contract.d.ts +64 -0
  50. package/src/plugins/gen/audio-recorder/sdk/audio-recorder.gen.ts +76 -0
  51. package/src/plugins/gen/audio-recorder/sdk/audio-recorder.ts +113 -0
  52. package/src/plugins/gen/geolocation/contract.d.ts +7 -5
  53. package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +6 -2
  54. package/src/plugins/gen/speech/contract.d.ts +67 -0
  55. package/src/plugins/gen/speech/sdk/speech.gen.ts +64 -0
  56. package/src/plugins/gen/speech/sdk/speech.ts +104 -0
  57. package/src/runtime/media.ts +11 -4
  58. package/src/runtime/permissions.ts +40 -0
  59. package/src/server/auth/host.ts +1 -1
  60. package/src/ui/presentable.ts +5 -0
  61. package/src/ui/router.ts +4 -3
  62. package/src/version.ts +1 -1
  63. package/dist/types/gl/controls.d.ts +0 -19
  64. package/dist/types/plugins/permission.d.ts +0 -1
  65. package/src/gl/controls.ts +0 -95
  66. package/src/plugins/permission.ts +0 -8
@@ -0,0 +1,104 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/speech/sdk/speech.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
+ // SpeechRecognizer — speech recognition: the microphone to text, live (dictation, voice commands).
4
+ //
5
+ // Hand-written over the generated wire because the app's object is not the wire's session: a
6
+ // `SpeechRecognizer` is reusable — every `start()` opens a fresh host session (one listening),
7
+ // `end` closes it — and its events are the SDK's `addEventListener` pattern.
8
+ //
9
+ // The permissions (speech recognition, the microphone) are asked by the host on `start()` (the
10
+ // contract's `@permission`); to ask earlier, or to act on a denial, `permissions`.
11
+
12
+ import { Emitter } from "../../../../plugin"
13
+ import { SpeechBase, NAME } from "./speech.gen"
14
+ import type { SpeechErrorCode, SpeechOptions, SpeechResult } from "../contract"
15
+
16
+ export type { SpeechErrorCode, SpeechOptions, SpeechResult }
17
+
18
+ export type SpeechRecognizerEvents = {
19
+ /** A result: partials while the user speaks (`interimResults`), then the final one. */
20
+ result: (result: SpeechResult) => void
21
+ /** The user started speaking. */
22
+ speechStart: () => void
23
+ /** The user stopped speaking: the final result follows. */
24
+ speechEnd: () => void
25
+ /** The listening is over — after the final result, after `stop()` / `abort()`, after an error. */
26
+ end: () => void
27
+ /** The listening failed; `end` follows. */
28
+ error: (code: SpeechErrorCode, message: string) => void
29
+ }
30
+
31
+ /**
32
+ * Recognizes speech from the microphone. `SpeechRecognizer.isSupported` reports whether this host
33
+ * registered the service — check it before showing a dictation button.
34
+ *
35
+ * ```ts
36
+ * const speech = new SpeechRecognizer()
37
+ * speech.addEventListener('result', r => input.value = r.transcript)
38
+ * speech.addEventListener('end', () => micButton.style({ opacity: 1 }))
39
+ * await speech.start({ language: 'en-US' })
40
+ * ```
41
+ */
42
+ export class SpeechRecognizer extends Emitter<SpeechRecognizerEvents> {
43
+ /** Whether this host registered a "speech" service. */
44
+ static get isSupported(): boolean { return SpeechBase.supports(NAME) }
45
+
46
+ private _session: SpeechBase | null = null
47
+
48
+ /** Whether a listening is under way. */
49
+ get listening(): boolean { return this._session !== null }
50
+
51
+ /** Start listening. The host asks for the permissions first; rejects `"denied"` when refused,
52
+ * `"unavailable"` when there is no recognizer for the language or on the device, and before
53
+ * touching the host when the plugin is not there or a listening is under way. */
54
+ async start(options?: SpeechOptions): Promise<void> {
55
+ if (!SpeechRecognizer.isSupported) {
56
+ throw new Error('SpeechRecognizer isn\'t available on this host — it needs a registered "speech" service.')
57
+ }
58
+ if (this._session) throw new Error("SpeechRecognizer: a listening is under way — stop() or abort() it first")
59
+ const session = new SpeechBase()
60
+ session.on("result", result => { if (result && this._session === session) this.dispatch("result", result) })
61
+ session.on("speechStart", () => { if (this._session === session) this.dispatch("speechStart") })
62
+ session.on("speechEnd", () => { if (this._session === session) this.dispatch("speechEnd") })
63
+ session.on("error", payload => { if (payload && this._session === session) this.dispatch("error", payload.code, payload.message) })
64
+ session.on("end", () => {
65
+ if (this._session !== session) return
66
+ this._drop(session)
67
+ this.dispatch("end")
68
+ })
69
+ this._session = session
70
+ try {
71
+ await session.start(options ?? {})
72
+ } catch (e) {
73
+ this._drop(session)
74
+ throw e
75
+ }
76
+ }
77
+
78
+ /** Stop listening and finish: the final result of what was heard, then `end`. */
79
+ stop(): void {
80
+ this._session?.stop().catch(() => {})
81
+ }
82
+
83
+ /** Stop and drop what was heard: `end`, no result. */
84
+ abort(): void {
85
+ this._session?.abort().catch(() => {})
86
+ }
87
+
88
+ /** The languages this host recognizes (BCP-47), sorted; empty when it cannot say (the web). */
89
+ static async supportedLanguages(): Promise<string[]> {
90
+ if (!SpeechRecognizer.isSupported) return []
91
+ const session = new SpeechBase()
92
+ try {
93
+ return await session.supportedLanguages()
94
+ } finally {
95
+ session.close()
96
+ }
97
+ }
98
+
99
+ private _drop(session: SpeechBase): void {
100
+ if (this._session !== session) return
101
+ session.close()
102
+ this._session = null
103
+ }
104
+ }
@@ -7,10 +7,14 @@
7
7
  // then; a paused player you stop referencing is released at once. There is nothing to dispose. The
8
8
  // end-of-playback report comes by id through ONE emitter per world; a playing player is pinned so
9
9
  // the report finds it.
10
+ //
11
+ // `src` is a url, or a `File` / `FetchResponse`: those play from the host's buffer store
12
+ // (`"id:<bufferId>"`, the whole bytes in memory, no streaming).
10
13
 
11
14
  import { Emitter } from "../core/events"
12
15
  import { Pins } from "../core/pins"
13
16
  import { Texture } from "../gl/Texture"
17
+ import type { File, FetchResponse } from "./fetch"
14
18
 
15
19
  type MediaEvents = {
16
20
  /** Fired at each loop boundary while `loop` is true. */
@@ -30,6 +34,9 @@ const ensureEmitter = (): void => {
30
34
  emitterInstalled = true
31
35
  m.setEmitter((id) => { players.get(id)?._finished() })
32
36
  }
37
+ /** The wire spelling of a source: a url as is, a buffer (File / FetchResponse) as "id:<bufferId>". */
38
+ const wireSrc = (src?: string | File | FetchResponse): string =>
39
+ src == null ? "" : typeof src === "string" ? src : "id:" + (src as any)._h.id
33
40
 
34
41
  abstract class MediaPlayer extends Emitter<MediaEvents> {
35
42
  /** @internal The player's owned handle (`_h.id` = the host player id; -1 = no host player). */
@@ -92,18 +99,18 @@ abstract class MediaPlayer extends Emitter<MediaEvents> {
92
99
  }
93
100
 
94
101
  export class AudioPlayer extends MediaPlayer {
95
- constructor(src?: string) {
102
+ constructor(src?: string | File | FetchResponse) {
96
103
  super()
97
- this._src = src ?? ""
104
+ this._src = wireSrc(src)
98
105
  ensureEmitter()
99
106
  this._h = media()?.createAudio(this._src) ?? { id: -1 }
100
107
  }
101
108
  }
102
109
 
103
110
  export class VideoPlayer extends MediaPlayer {
104
- constructor(src?: string) {
111
+ constructor(src?: string | File | FetchResponse) {
105
112
  super()
106
- this._src = src ?? ""
113
+ this._src = wireSrc(src)
107
114
  ensureEmitter()
108
115
  this._h = media()?.createVideo(this._src) ?? { id: -1 }
109
116
  }
@@ -0,0 +1,40 @@
1
+ // The OS permissions: what the app may ask the platform for, and when. ONE door, host-gated
2
+ // (`_creatorDevice.queryPermission / requestPermission / openSettings`): the SDK's own camera
3
+ // users (ARScene.prepare, QRScanner, CameraView) go through `_ensurePermission`, apps through
4
+ // `permissions`, a plugin's native half through the host's same service (a method tagged
5
+ // `@permission` in its contract is gated by the generated glue, never by the plugin's code).
6
+ //
7
+ // Hosts without a dialog (headless, desktop, the web for `query` where the browser has no answer)
8
+ // leave the slots out, and both answers are "granted": nothing there stands between an app and a
9
+ // feature — the feature itself says whether it exists (`isSupported`, a rejection of its own).
10
+
11
+ export type PermissionKind = "camera" | "microphone" | "location" | "speech"
12
+ /** "prompt" = never asked: `request` shows the dialog. "denied" = refused, or restricted by the
13
+ * device's policy; the OS decides whether a `request` asks again (iOS never does: `openSettings`). */
14
+ export type PermissionState = "granted" | "denied" | "prompt"
15
+
16
+ const slot = (name: "queryPermission" | "requestPermission", kind: PermissionKind): Promise<PermissionState> => {
17
+ const fn = _creatorDevice[name]
18
+ if (typeof fn !== "function") return Promise.resolve("granted")
19
+ return new Promise((resolve, reject) => fn(kind, s => resolve(s as PermissionState), err => reject(new Error(err))))
20
+ }
21
+
22
+ export const permissions = {
23
+ /** The state of `kind` without a prompt. On the web the browser may not know before a request
24
+ * (Safari before 16, Firefox for the camera): "prompt" then. */
25
+ query(kind: PermissionKind): Promise<PermissionState> { return slot("queryPermission", kind) },
26
+ /** Ask for `kind`: the dialog when it was never asked, the state after it (or the state as it
27
+ * stands when the OS will not ask again). Ask where the user expects it — the first tap on the
28
+ * record button, not the start of the call. */
29
+ request(kind: PermissionKind): Promise<PermissionState> { return slot("requestPermission", kind) },
30
+ /** Open the app's own page of the system settings: where a denied permission is turned back on
31
+ * (iOS asks exactly once). A no-op where there is no such page (web). */
32
+ openSettings(): void { _creatorDevice.openSettings?.() },
33
+ }
34
+
35
+ /** Request `kind` and reject when it was not granted — the SDK's own gate before a camera view
36
+ * opens. @internal */
37
+ export const _ensurePermission = async (kind: PermissionKind): Promise<void> => {
38
+ const state = await permissions.request(kind)
39
+ if (state !== "granted") throw new Error(`The ${kind} permission was ${state === "denied" ? "denied" : "not granted"}`)
40
+ }
@@ -40,7 +40,7 @@ export type AuthHostConfig = {
40
40
  appName?: string
41
41
  /** Sends the sign-in code (the platform mailer). Absent = codes are only echoed to the console. */
42
42
  mail?: (msg: AuthMail) => Promise<void>
43
- /** Echo every code to the server console (the draft slot: the developer is the only user). */
43
+ /** Echo every code to the server console (a developer's own backend — `lecodes dev`, a check: the developer is the only user). */
44
44
  echoCodes?: boolean
45
45
  /** Server console sink (default console). */
46
46
  log?: (level: "warn" | "info", text: string) => void
@@ -113,6 +113,11 @@ export interface Presentable {
113
113
  * await it and skip the swap entirely if it rejects (permission denied, or superseded by
114
114
  * another navigation that happened while preparing). */
115
115
  _prepare?(): Promise<void>
116
+ /** @internal What a destination that prepared waits for once it is presented — ARScene: its first
117
+ * frame drawn (the view mounted and sized, the session's frame shown). `open()` and
118
+ * `Router.push/replace` settle with it: they resolve then, or reject when another navigation came
119
+ * first. */
120
+ _presented?(): Promise<void>
116
121
  /** @internal The destination tuple (see DestTuple). */
117
122
  _dest(): DestTuple
118
123
  }
package/src/ui/router.ts CHANGED
@@ -97,9 +97,10 @@ export const Router: Router = /*#__PURE__*/ Object.freeze({
97
97
  _setCurrent(page) // optimistic — the runtime's change event confirms
98
98
  }
99
99
  // A preparing destination (ARScene) keeps the current page visible until it's ready; a
100
- // rejection (camera denied / superseded) propagates to the caller and nothing is pushed.
100
+ // rejection (camera denied / superseded) propagates to the caller and nothing is pushed. Pushed,
101
+ // it settles the push once it is on screen (`_presented`: ARScene's first frame), as its open().
101
102
  const prep = page._prepare?.()
102
- return prep ? prep.then(go) : go()
103
+ return prep ? prep.then(() => { go(); return page._presented?.() }) : go()
103
104
  },
104
105
  replace(page: Presentable, opts?: NavigateOptions): void | Promise<void> {
105
106
  const go = () => {
@@ -110,7 +111,7 @@ export const Router: Router = /*#__PURE__*/ Object.freeze({
110
111
  _setCurrent(page) // optimistic — the runtime's change event confirms
111
112
  }
112
113
  const prep = page._prepare?.()
113
- return prep ? prep.then(go) : go()
114
+ return prep ? prep.then(() => { go(); return page._presented?.() }) : go()
114
115
  },
115
116
  pop(to?: number, opts?: PopOptions): void {
116
117
  _bumpNavEpoch()
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  // same manifest). Its MAJOR is the bundle ↔ runtime contract: a bundle's `// sdk:` header line
5
5
  // and a host's embedded version must agree on the major for the host to run the bundle; minor
6
6
  // and patch never gate anything (features are detected, never versioned).
7
- export const SDK_VERSION = "2.2.9"
7
+ export const SDK_VERSION = "2.2.11"
8
8
 
9
9
  /** The major of a semver string, or null when it is not one. */
10
10
  export const sdkMajor = (version: string | null | undefined): number | null => {
@@ -1,19 +0,0 @@
1
- import type { Node } from "./Node";
2
- import type { Scene } from "./Scene";
3
- export type ControlsOptions = {
4
- /** Smallest uniform scale the pinch gesture allows (default 0.08). */
5
- minScale?: number;
6
- /** Largest uniform scale the pinch gesture allows (default 2). */
7
- maxScale?: number;
8
- /** One-finger drag moves the target across the ground plane (default true). */
9
- pan?: boolean;
10
- /** Two-finger pinch scales the target (default true). */
11
- pinch?: boolean;
12
- /** Two-finger twist rotates the target around its Y axis (default true). */
13
- twist?: boolean;
14
- };
15
- /** Returned by `addControls` — call `remove()` to detach the gesture listeners. */
16
- export type ControlsHandle = {
17
- remove(): void;
18
- };
19
- export declare const attachControls: (scene: Scene, target: Node, options?: ControlsOptions) => ControlsHandle;
@@ -1 +0,0 @@
1
- export declare const _requestCameraPermission: () => Promise<void>;
@@ -1,95 +0,0 @@
1
- // AR object-placement controls — the classic "drop a model in the room and manipulate it" gesture
2
- // recipe: ONE finger drags the target across the ground plane (camera-relative), TWO fingers pinch to
3
- // scale and twist to rotate (yaw). Lives on ARScene (see Scene.ts) because it orchestrates the scene's
4
- // camera + touch input against a target node; it's built entirely on the public API (no native calls
5
- // the user couldn't make), so it's a convenience, not a primitive.
6
-
7
- import { Vec2, Vec3 } from "../math/vec"
8
- import { Quat } from "../math/quat"
9
- import type { Node } from "./Node"
10
- import type { Scene } from "./Scene"
11
- import type { TouchStartEvent } from "../runtime/touch"
12
-
13
- export type ControlsOptions = {
14
- /** Smallest uniform scale the pinch gesture allows (default 0.08). */
15
- minScale?: number
16
- /** Largest uniform scale the pinch gesture allows (default 2). */
17
- maxScale?: number
18
- /** One-finger drag moves the target across the ground plane (default true). */
19
- pan?: boolean
20
- /** Two-finger pinch scales the target (default true). */
21
- pinch?: boolean
22
- /** Two-finger twist rotates the target around its Y axis (default true). */
23
- twist?: boolean
24
- }
25
-
26
- /** Returned by `addControls` — call `remove()` to detach the gesture listeners. */
27
- export type ControlsHandle = { remove(): void }
28
-
29
- type ActiveTouch = { pointerId: number, x: number, y: number }
30
-
31
- const clamp = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
32
-
33
- export const attachControls = (scene: Scene, target: Node, options: ControlsOptions = {}): ControlsHandle => {
34
- const minScale = options.minScale ?? 0.08
35
- const maxScale = options.maxScale ?? 2
36
- const pan = options.pan ?? true
37
- const pinch = options.pinch ?? true
38
- const twist = options.twist ?? true
39
-
40
- const active: ActiveTouch[] = []
41
-
42
- const onTouchStart = (e: TouchStartEvent<Node | null>): void => {
43
- const touch: ActiveTouch = { pointerId: e.pointerId, x: e.clientX, y: e.clientY }
44
- active.push(touch)
45
- e.track({
46
- onMove(m) {
47
- if (active.length === 2) {
48
- // --- two-finger: pinch-scale + twist-rotate about the gesture axis ---
49
- const other = active.find((t) => t !== touch)
50
- if (other) {
51
- const axis = new Vec2(other.x - touch.x, other.y - touch.y)
52
- const span = axis.length()
53
- if (span > 1e-3) {
54
- const dir = axis.scale(1 / span)
55
- const delta = new Vec2(m.deltaX, m.deltaY)
56
- if (pinch) {
57
- const along = -dir.dot(delta) // movement along the pinch axis
58
- target.scale = clamp(target.scale.x * (span + along) / span, minScale * 0.4, maxScale * 1.5)
59
- }
60
- if (twist) {
61
- const tangent = dir.perp().dot(delta) // movement across the axis
62
- target.quaternion = target.quaternion.mul(Quat.fromAxisAngle(Vec3.up, Math.atan2(tangent, span)))
63
- }
64
- }
65
- }
66
- } else if (active.length === 1 && pan) {
67
- // --- one-finger: drag across the ground plane, relative to where the camera looks ---
68
- let dir = scene.camera.forward
69
- const parent = target.parent
70
- if (parent) dir = dir.rotate(parent.quaternion.invert()) // into the parent's local frame
71
- dir = dir.withY(0)
72
- const k = Math.hypot(dir.x, dir.z)
73
- const screen = Math.max(...scene.camera.displaySize)
74
- if (k > 1e-4 && screen > 0) {
75
- const dist = target.worldPosition.distanceTo(scene.camera.worldPosition)
76
- const right = new Vec3(-dir.z, 0, dir.x).scale(m.deltaX / k / screen * dist)
77
- const fwd = dir.scale(-m.deltaY / k / screen * dist)
78
- const move = right.add(fwd)
79
- const p = target.position
80
- target.position = new Vec3(p.x + move.x, p.y, p.z + move.z) // keep height fixed
81
- }
82
- }
83
- touch.x = m.clientX
84
- touch.y = m.clientY
85
- },
86
- onEnd() {
87
- const i = active.indexOf(touch)
88
- if (i >= 0) active.splice(i, 1)
89
- },
90
- })
91
- }
92
-
93
- scene.addEventListener("touchstart", onTouchStart)
94
- return { remove() { scene.removeEventListener("touchstart", onTouchStart); active.length = 0 } }
95
- }
@@ -1,8 +0,0 @@
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
- // the web). Shared by ARScene.prepare, QRScanner and CameraView.
4
-
5
- export const _requestCameraPermission = (): Promise<void> =>
6
- typeof _creatorDevice.requestCamera === "function"
7
- ? new Promise<void>((res, rej) => _creatorDevice.requestCamera!(res, rej))
8
- : Promise.resolve()