lecodes-cli 0.7.0 → 0.7.1
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.
- package/dist/index.js +3 -3
- package/package.json +3 -3
- package/runtime/sdk/g2/Scene2D.ts +51 -5
- package/runtime/sdk/gl/Model.ts +9 -0
- package/runtime/sdk/gl/Scene.ts +76 -11
- package/runtime/sdk/inject.ts +14 -6
- package/runtime/sdk/plugins/camera.ts +81 -0
- package/runtime/sdk/plugins/geolocation.ts +123 -0
- package/runtime/sdk/plugins/permission.ts +7 -0
- package/runtime/sdk/plugins/qr.ts +61 -24
- package/runtime/sdk/runtime/app.ts +37 -0
- package/runtime/sdk/runtime/appEvents.ts +29 -0
- package/runtime/sdk/runtime/channel.ts +50 -0
- package/runtime/sdk/runtime/clipboard.ts +20 -0
- package/runtime/sdk/runtime/datetime.ts +2 -4
- package/runtime/sdk/runtime/device.ts +137 -4
- package/runtime/sdk/runtime/misc.ts +4 -0
- package/runtime/sdk/runtime/service.ts +82 -0
- package/runtime/sdk/runtime/touch.ts +26 -0
- package/runtime/sdk/ui/NativeView.ts +144 -0
- package/runtime/sdk/ui/UI.ts +5 -1
- package/runtime/sdk/ui/UIButton.ts +26 -3
- package/runtime/sdk/ui/UIInput.ts +2 -2
- package/runtime/sdk/ui/UINode.ts +24 -4
- package/runtime/sdk/ui/UIScreen.ts +63 -29
- package/runtime/sdk/ui/UIScreenHost.ts +3 -2
- package/runtime/sdk/ui/UIText.ts +1 -1
- package/runtime/sdk/ui/UIVideo.ts +48 -2
- package/runtime/sdk/ui/UIWidget.ts +34 -1
- package/runtime/sdk/ui/presentable.ts +116 -0
- package/runtime/sdk/ui/router.ts +73 -29
- package/runtime/sdk-types.json +1 -1
- package/runtime/sdk/runtime/camera.ts +0 -31
package/dist/index.js
CHANGED
|
@@ -1457,7 +1457,7 @@ var require_main = __commonJS((exports, module) => {
|
|
|
1457
1457
|
// package.json
|
|
1458
1458
|
var package_default = {
|
|
1459
1459
|
name: "lecodes-cli",
|
|
1460
|
-
version: "0.7.
|
|
1460
|
+
version: "0.7.1",
|
|
1461
1461
|
description: "Clone, edit and push LeCodes projects from your machine.",
|
|
1462
1462
|
type: "module",
|
|
1463
1463
|
license: "MIT",
|
|
@@ -1487,11 +1487,11 @@ var package_default = {
|
|
|
1487
1487
|
prepublishOnly: "bun run sync-peers && bun run build"
|
|
1488
1488
|
},
|
|
1489
1489
|
dependencies: {
|
|
1490
|
-
"@letary/chisel": "^0.4.
|
|
1490
|
+
"@letary/chisel": "^0.4.1"
|
|
1491
1491
|
},
|
|
1492
1492
|
peerDependencies: {
|
|
1493
1493
|
"lecodes-design": "^0.3.1",
|
|
1494
|
-
"lecodes-renderer": "^0.6.
|
|
1494
|
+
"lecodes-renderer": "^0.6.1",
|
|
1495
1495
|
"lecodes-3d-editor": "^0.1.0"
|
|
1496
1496
|
},
|
|
1497
1497
|
peerDependenciesMeta: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lecodes-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Clone, edit and push LeCodes projects from your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"prepublishOnly": "bun run sync-peers && bun run build"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@letary/chisel": "^0.4.
|
|
33
|
+
"@letary/chisel": "^0.4.1"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"lecodes-design": "^0.3.1",
|
|
37
|
-
"lecodes-renderer": "^0.6.
|
|
37
|
+
"lecodes-renderer": "^0.6.1",
|
|
38
38
|
"lecodes-3d-editor": "^0.1.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { Color, type ColorInput } from "../core/color"
|
|
5
5
|
import { _installAspectFrames } from "../core/Aspect"
|
|
6
|
+
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "../ui/presentable"
|
|
6
7
|
import { cx, cy, type Vec2Like } from "../math/vec"
|
|
7
8
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
8
9
|
import { Camera2D } from "./Camera2D"
|
|
@@ -48,7 +49,7 @@ class LayerHandle {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
export class Scene2D {
|
|
52
|
+
export class Scene2D implements Presentable {
|
|
52
53
|
/** Native scene handle. */
|
|
53
54
|
readonly id: number
|
|
54
55
|
readonly camera: Camera2D
|
|
@@ -111,17 +112,62 @@ export class Scene2D {
|
|
|
111
112
|
return this
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
116
|
+
|
|
117
|
+
/** @internal presentation lifecycle — fired by the host on present/dismiss (new navigation),
|
|
118
|
+
* or synchronously by open()/close() on legacy hosts. */
|
|
119
|
+
readonly ol: (() => void)[] = []
|
|
120
|
+
readonly cl: (() => void)[] = []
|
|
121
|
+
_backButtonCallback?: () => void
|
|
122
|
+
private _vd?: object
|
|
123
|
+
|
|
124
|
+
/** Fires when the scene becomes the visible destination (incl. a pop revealing it). */
|
|
125
|
+
onOpen(callback: () => void): this {
|
|
126
|
+
this.ol.push(callback)
|
|
127
|
+
return this
|
|
128
|
+
}
|
|
129
|
+
/** Fires when the scene stops being visible (closed, replaced, or covered by a push). */
|
|
130
|
+
onClose(callback: () => void): this {
|
|
131
|
+
this.cl.push(callback)
|
|
132
|
+
return this
|
|
133
|
+
}
|
|
134
|
+
onBackPressed(callback: () => void): this {
|
|
135
|
+
this._backButtonCallback = callback
|
|
136
|
+
return this
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @internal stable wire descriptor; hosts hand it back via router onChange (`_p` = this). */
|
|
140
|
+
_viewDesc(): object {
|
|
141
|
+
return this._vd ??= { type: "scene2d", sceneId: this.id, _p: this }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Make this the active scene — shows it as the current destination (replaces a screen /
|
|
145
|
+
* another scene; only the active scene renders). */
|
|
146
|
+
open(options?: PresentOptions): this {
|
|
147
|
+
_bumpNavEpoch()
|
|
116
148
|
// Reassert this scene's texture filter (the sampler is global — see Scene2DOptions.filter).
|
|
117
149
|
if (this._filter !== undefined) _creator2d.setDefaultFilter(this._filter === 'linear')
|
|
118
|
-
|
|
150
|
+
if (_navSupported()) {
|
|
151
|
+
// The host activates the engine (openScene) as part of presenting the destination.
|
|
152
|
+
_creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
|
|
153
|
+
} else {
|
|
154
|
+
_creator2d.openScene(this.id)
|
|
155
|
+
for (const cb of this.ol) cb()
|
|
156
|
+
}
|
|
119
157
|
Scene2D._active = this
|
|
158
|
+
_setCurrent(this)
|
|
120
159
|
return this
|
|
121
160
|
}
|
|
122
161
|
close(): this {
|
|
123
|
-
|
|
162
|
+
_bumpNavEpoch()
|
|
163
|
+
if (_navSupported()) {
|
|
164
|
+
_creatorUI.closeView!()
|
|
165
|
+
} else {
|
|
166
|
+
_creator2d.closeScene()
|
|
167
|
+
for (const cb of this.cl) cb()
|
|
168
|
+
}
|
|
124
169
|
if (Scene2D._active === this) Scene2D._active = null
|
|
170
|
+
if (Presentable.current === this) _setCurrent(null)
|
|
125
171
|
return this
|
|
126
172
|
}
|
|
127
173
|
destroy(): void {
|
package/runtime/sdk/gl/Model.ts
CHANGED
|
@@ -17,6 +17,15 @@ export class Model extends Node {
|
|
|
17
17
|
this.aspect(ModelAnimation)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Duplicate this model — a deep copy of the GLB (meshes, skeleton, animation clips), attached to
|
|
21
|
+
* the same parent and scene and sharing this model's current transform. The clone has its own
|
|
22
|
+
* independent animation state (reach it via clone.anim). Mirrors Model.load's culling default. */
|
|
23
|
+
clone(): Model {
|
|
24
|
+
const id = _creator.cloneEntity(this.id)
|
|
25
|
+
_creator.setGlbCulling(id, false)
|
|
26
|
+
return new Model(id)
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
/** Load a GLB model. Returns its root as a Model; play its baked clips via model.anim. */
|
|
21
30
|
static load(
|
|
22
31
|
source: string | FetchResponse,
|
package/runtime/sdk/gl/Scene.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { Color, type ColorInput } from "../core/color"
|
|
9
9
|
import { _installAspectFrames } from "../core/Aspect"
|
|
10
|
+
import { _bumpNavEpoch, _navEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "../ui/presentable"
|
|
10
11
|
import type { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
11
12
|
import type { FetchResponse } from "../runtime/fetch"
|
|
13
|
+
import { _requestCameraPermission } from "../plugins/permission"
|
|
12
14
|
import { Camera } from "./Camera"
|
|
13
15
|
import { attachControls, type ControlsHandle, type ControlsOptions } from "./controls"
|
|
14
16
|
import { Material } from "./Material"
|
|
@@ -46,7 +48,7 @@ const installRenderSyncedFrames = (): void => {
|
|
|
46
48
|
})
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
export class Scene {
|
|
51
|
+
export class Scene implements Presentable {
|
|
50
52
|
/** @internal native scene handle. */
|
|
51
53
|
readonly _id: number
|
|
52
54
|
readonly camera: Camera
|
|
@@ -115,16 +117,64 @@ export class Scene {
|
|
|
115
117
|
return new Promise<void>((res) => _creator.warmRender(this._id, res))
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
121
|
+
// A scene is a destination like a screen: opening it replaces whatever is visible (suspending
|
|
122
|
+
// an active Router until Router.restore()), and it can be pushed onto the Router stack. The
|
|
123
|
+
// scene graph survives while covered — only the view detaches and rendering pauses.
|
|
124
|
+
|
|
125
|
+
/** @internal presentation lifecycle — fired by the host on present/dismiss (new navigation),
|
|
126
|
+
* or synchronously by open()/close() on legacy hosts. */
|
|
127
|
+
readonly ol: (() => void)[] = []
|
|
128
|
+
readonly cl: (() => void)[] = []
|
|
129
|
+
_backButtonCallback?: () => void
|
|
130
|
+
private _vd?: object
|
|
131
|
+
|
|
132
|
+
/** Fires when the scene becomes the visible destination (incl. a pop revealing it). */
|
|
133
|
+
onOpen(callback: () => void): this {
|
|
134
|
+
this.ol.push(callback)
|
|
135
|
+
return this
|
|
136
|
+
}
|
|
137
|
+
/** Fires when the scene stops being visible (closed, replaced, or covered by a push). */
|
|
138
|
+
onClose(callback: () => void): this {
|
|
139
|
+
this.cl.push(callback)
|
|
140
|
+
return this
|
|
141
|
+
}
|
|
142
|
+
onBackPressed(callback: () => void): this {
|
|
143
|
+
this._backButtonCallback = callback
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @internal stable wire descriptor; hosts hand it back via router onChange (`_p` = this). */
|
|
148
|
+
_viewDesc(): object {
|
|
149
|
+
return this._vd ??= { type: "scene3d", sceneId: this._id, _p: this }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Make this the active scene — shows it as the current destination (replaces a screen /
|
|
153
|
+
* another scene; only the active scene renders). */
|
|
154
|
+
open(options?: PresentOptions): void | Promise<void> {
|
|
155
|
+
_bumpNavEpoch()
|
|
156
|
+
if (_navSupported()) {
|
|
157
|
+
// The host activates the engine (openScene) as part of presenting the destination.
|
|
158
|
+
_creatorUI.openView!(this._viewDesc(), options?.transition ?? "none")
|
|
159
|
+
} else {
|
|
160
|
+
_creator.openScene(this._id)
|
|
161
|
+
for (const cb of this.ol) cb()
|
|
162
|
+
}
|
|
121
163
|
Scene._active = this
|
|
122
164
|
glState.activeScene = this
|
|
165
|
+
_setCurrent(this)
|
|
123
166
|
}
|
|
124
167
|
close(): void {
|
|
125
|
-
|
|
168
|
+
_bumpNavEpoch()
|
|
169
|
+
if (_navSupported()) {
|
|
170
|
+
_creatorUI.closeView!()
|
|
171
|
+
} else {
|
|
172
|
+
_creator.closeScene()
|
|
173
|
+
for (const cb of this.cl) cb()
|
|
174
|
+
}
|
|
126
175
|
if (Scene._active === this) Scene._active = null
|
|
127
176
|
if (glState.activeScene === this) glState.activeScene = null
|
|
177
|
+
if (Presentable.current === this) _setCurrent(null)
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
addEventListener(channel: "click", callback: (ev: ClickEvent<Node | null>) => void): void
|
|
@@ -188,11 +238,26 @@ export class ARScene<T extends ARMode = "default"> extends Scene {
|
|
|
188
238
|
return attachControls(this, target, options)
|
|
189
239
|
}
|
|
190
240
|
|
|
191
|
-
override async open(): Promise<void> {
|
|
192
|
-
await
|
|
193
|
-
|
|
241
|
+
override async open(options?: PresentOptions): Promise<void> {
|
|
242
|
+
await this._prepare()
|
|
243
|
+
super.open(options)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** @internal Presentable prepare hook: camera permission + warm render + AR session, all while
|
|
247
|
+
* the previous destination (typically a loading screen) stays visible — the swap to this scene
|
|
248
|
+
* happens only after it resolves. Warm render is headless on every platform (offscreen
|
|
249
|
+
* swapchain), so no GL view is created here — the host mounts the scene view at present time.
|
|
250
|
+
* Rejects on camera denial / AR launch failure, or if another navigation happened while
|
|
251
|
+
* preparing (superseded) — in every rejection case the current destination stays visible. */
|
|
252
|
+
async _prepare(): Promise<void> {
|
|
253
|
+
const epoch = _navEpoch()
|
|
254
|
+
await _requestCameraPermission()
|
|
255
|
+
if (this.useWarmRender) await this.warmRender()
|
|
194
256
|
await new Promise<void>((res, rej) => _creator.launchAR(this._id, res, rej))
|
|
195
|
-
|
|
257
|
+
if (_navEpoch() !== epoch) {
|
|
258
|
+
_creator.stopAR()
|
|
259
|
+
throw new Error("ARScene.open() superseded: another destination was opened while preparing")
|
|
260
|
+
}
|
|
196
261
|
}
|
|
197
262
|
|
|
198
263
|
override close(): void {
|
|
@@ -212,9 +277,9 @@ export class ARScene<T extends ARMode = "default"> extends Scene {
|
|
|
212
277
|
* `open()` rejects on hosts without a VR runtime ("VR is not supported on this device").
|
|
213
278
|
*/
|
|
214
279
|
export class VRScene extends Scene {
|
|
215
|
-
override async open(): Promise<void> {
|
|
280
|
+
override async open(options?: PresentOptions): Promise<void> {
|
|
216
281
|
await new Promise<void>((res, rej) => _creator.launchVR(this._id, res, rej))
|
|
217
|
-
super.open()
|
|
282
|
+
super.open(options)
|
|
218
283
|
}
|
|
219
284
|
|
|
220
285
|
override close(): void {
|
package/runtime/sdk/inject.ts
CHANGED
|
@@ -25,18 +25,19 @@ export { signal, computed, effect, type Signal, type Computed } from "./core/sig
|
|
|
25
25
|
export { Canvas, Bitmap } from "./canvas/Canvas"
|
|
26
26
|
|
|
27
27
|
// ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
|
|
28
|
-
export { toast, SvgSource } from "./runtime/misc"
|
|
28
|
+
export { toast, openURL, SvgSource } from "./runtime/misc"
|
|
29
|
+
export { app, type AppState } from "./runtime/app"
|
|
30
|
+
export { clipboard } from "./runtime/clipboard"
|
|
29
31
|
export { date, type DateValue, type DateInput, type Unit, type Locale } from "./runtime/datetime"
|
|
30
32
|
export { fetch, fetchLocal, FormData, File } from "./runtime/fetch"
|
|
31
33
|
export { localStorage } from "./runtime/storage"
|
|
32
|
-
export { device } from "./runtime/device"
|
|
34
|
+
export { device, type HapticStyle, type MotionOptions } from "./runtime/device"
|
|
33
35
|
export { Input } from "./runtime/input"
|
|
34
36
|
export { WebSocket } from "./runtime/net"
|
|
35
37
|
export { AudioPlayer, VideoPlayer } from "./runtime/media"
|
|
36
|
-
export { CameraViewer } from "./runtime/camera"
|
|
37
38
|
export { openFilePicker } from "./runtime/files"
|
|
38
39
|
export { share } from "./runtime/share"
|
|
39
|
-
export { ClickEvent, TouchStartEvent } from "./runtime/touch"
|
|
40
|
+
export { ClickEvent, TouchStartEvent, LongPressEvent } from "./runtime/touch"
|
|
40
41
|
|
|
41
42
|
// ---- 2D engine (creator-2d) ----
|
|
42
43
|
export { Scene2D } from "./g2/Scene2D"
|
|
@@ -53,7 +54,7 @@ export { Texture2D } from "./g2/Texture2D"
|
|
|
53
54
|
|
|
54
55
|
// ---- UI (creator-ui) ----
|
|
55
56
|
export {
|
|
56
|
-
UIScreen, type UIScreenStyle,
|
|
57
|
+
UIScreen, type UIScreenStyle, type UIScrollableScreen,
|
|
57
58
|
UIRow, UIColumn, UIBox, type UIContainerStyle,
|
|
58
59
|
UIText, type UITextStyle,
|
|
59
60
|
UIButton, type UIButtonStyle,
|
|
@@ -66,9 +67,14 @@ export {
|
|
|
66
67
|
UISpacer, type UISpacerStyle,
|
|
67
68
|
UIVirtualizedList,
|
|
68
69
|
Router,
|
|
70
|
+
NativeView, type UINativeViewStyle,
|
|
69
71
|
registerFont,
|
|
70
72
|
__uiMap,
|
|
71
73
|
} from "./ui/UI"
|
|
74
|
+
// The Presentable navigation contract (docs/navigation-presentable-plan.md). `Presentable` is
|
|
75
|
+
// interface + runtime companion (declaration merging): `Presentable.current` = what's on screen.
|
|
76
|
+
export { Presentable } from "./ui/UI"
|
|
77
|
+
export type { PresentOptions, Transition, TransitionName, TransitionSpec, TransitionTransform } from "./ui/UI"
|
|
72
78
|
// The base node type + the union a container accepts as children — documented as the signature of
|
|
73
79
|
// UIColumn/UIRow/append/etc., so they must be global (type-only; erased at runtime).
|
|
74
80
|
export type { UINode, UINodeChild } from "./ui/UINode"
|
|
@@ -81,8 +87,10 @@ export { animate, animateMat4, stopAnimation, pauseAnimation, resumeAnimation }
|
|
|
81
87
|
export { cubicBezier } from "./animate/bezier"
|
|
82
88
|
export { easeIn, easeOut, easeInOut } from "./animate/easings"
|
|
83
89
|
|
|
84
|
-
// ---- plugins (optional, host-provided capabilities — see src/plugins/) ----
|
|
90
|
+
// ---- plugins (optional, host-provided capabilities — typed NativeView/service wrappers, see src/plugins/) ----
|
|
85
91
|
export { QRScanner } from "./plugins/qr"
|
|
92
|
+
export { CameraView, type CameraFacing } from "./plugins/camera"
|
|
93
|
+
export { Geolocation, type GeoPosition, type GeoOptions, type GeoWatch } from "./plugins/geolocation"
|
|
86
94
|
|
|
87
95
|
// ---- scenes as data (.scene.ts files — see src/scene/defineScene.ts) ----
|
|
88
96
|
export { defineScene, use, ref, make, SceneHandle } from "./scene/defineScene"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// CameraView — the device camera (live preview + still capture) as a Presentable. A typed wrapper
|
|
2
|
+
// over the `NativeView("camera")` capability (docs/navigation-presentable-plan.md): open it
|
|
3
|
+
// fullscreen (`await camera.open()`), push it (`Router.push(camera)`), or embed it among a
|
|
4
|
+
// screen's children as a live preview node. `takePhoto()` returns a `File` (a host buffer
|
|
5
|
+
// handle) — use it as a texture (`Texture2D.load(file)`), upload it (`FormData`), or `share` it.
|
|
6
|
+
//
|
|
7
|
+
// Host-OPTIONAL: it exists where the host registers a "camera" view (Android; web can via
|
|
8
|
+
// `_creatorUI._registerView`) — gate on `CameraView.isSupported`. Opening prepares first (camera
|
|
9
|
+
// permission) while the current destination stays visible — the same contract as ARScene/QRScanner.
|
|
10
|
+
//
|
|
11
|
+
// The pre-Presentable `_creatorUtils.camera*` bridges are gone from the ABI (2026-07-15): hosts
|
|
12
|
+
// keep their implementations only to serve previously compiled bundles.
|
|
13
|
+
|
|
14
|
+
import { File } from "../runtime/fetch"
|
|
15
|
+
import { _navEpoch, type PresentOptions } from "../ui/presentable"
|
|
16
|
+
import { NativeView, NativeViewElement } from "../ui/NativeView"
|
|
17
|
+
import { _requestCameraPermission } from "./permission"
|
|
18
|
+
|
|
19
|
+
export type CameraFacing = "front" | "back"
|
|
20
|
+
|
|
21
|
+
export interface CameraView extends NativeView {
|
|
22
|
+
/** Request camera permission, then present the preview. The current destination stays visible
|
|
23
|
+
* until the camera is ready; rejects if permission is denied, the host has no camera, or
|
|
24
|
+
* another navigation superseded this one. */
|
|
25
|
+
open(options?: PresentOptions): Promise<void>
|
|
26
|
+
/** Capture a still frame as an image `File` (host-encoded). */
|
|
27
|
+
takePhoto(): Promise<File>
|
|
28
|
+
/** Switch between the front and back camera while the preview is live. */
|
|
29
|
+
setFacingMode(facingMode: CameraFacing): Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class CameraViewElement extends NativeViewElement {
|
|
33
|
+
constructor(options: { facingMode?: CameraFacing } = {}) {
|
|
34
|
+
super("camera", { facingMode: options.facingMode ?? "back" })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** @internal prepare-then-present: permission before the swap (see Presentable._prepare). */
|
|
38
|
+
async _prepare(): Promise<void> {
|
|
39
|
+
const epoch = _navEpoch()
|
|
40
|
+
await _requestCameraPermission()
|
|
41
|
+
if (_navEpoch() !== epoch) {
|
|
42
|
+
throw new Error("CameraView.open() superseded: another destination was opened while preparing")
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
override async open(options?: PresentOptions): Promise<void> {
|
|
47
|
+
// Fail before prompting for the camera — see QRScanner.
|
|
48
|
+
if (!NativeView.isSupported("camera")) {
|
|
49
|
+
throw new Error("CameraView isn't available on this host — it needs a registered \"camera\" view.")
|
|
50
|
+
}
|
|
51
|
+
await this._prepare()
|
|
52
|
+
super.open(options)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async takePhoto(): Promise<File> {
|
|
56
|
+
const photo = await this.call("takePhoto")
|
|
57
|
+
return new File(photo.systemId, photo.name, photo.size)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async setFacingMode(facingMode: CameraFacing): Promise<void> {
|
|
61
|
+
await this.call("setFacingMode", facingMode)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a camera view (default: the back camera). `CameraView.isSupported` reports whether this
|
|
67
|
+
* host has a camera capability — check it before offering the feature.
|
|
68
|
+
*/
|
|
69
|
+
// PURE IIFE so an app that never uses the camera tree-shakes the whole plugin away (see NativeView).
|
|
70
|
+
export const CameraView: {
|
|
71
|
+
(options?: { facingMode?: CameraFacing }): CameraView
|
|
72
|
+
/** Whether this host registered a "camera" view. */
|
|
73
|
+
readonly isSupported: boolean
|
|
74
|
+
} = /*#__PURE__*/ (() => {
|
|
75
|
+
const factory = (options?: { facingMode?: CameraFacing }): CameraView =>
|
|
76
|
+
new CameraViewElement(options) as unknown as CameraView
|
|
77
|
+
Object.defineProperty(factory, "isSupported", {
|
|
78
|
+
get: (): boolean => NativeView.isSupported("camera"),
|
|
79
|
+
})
|
|
80
|
+
return factory as any
|
|
81
|
+
})()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Geolocation — the device position as a typed service plugin (docs/service-channel-plan.md).
|
|
2
|
+
// A wrapper over the host-registered "geolocation" service, exactly like QRScanner wraps the
|
|
3
|
+
// "qrScanner" NativeView — except services have no UI surface, so there is nothing to present:
|
|
4
|
+
// just `getCurrent()` and `watch()`.
|
|
5
|
+
//
|
|
6
|
+
// Host-OPTIONAL, like every plugin: it exists where the host registered a "geolocation" service
|
|
7
|
+
// (web hosts do out of the box; Android via MainActivity) — gate on `Geolocation.isSupported`.
|
|
8
|
+
// The OS permission prompt happens on the first call that needs it (host-side); denial is a
|
|
9
|
+
// rejection. Both methods reject immediately on unsupported hosts, BEFORE any permission path.
|
|
10
|
+
//
|
|
11
|
+
// Service contract ("geolocation"): calls `getCurrent(options) → GeoPosition`,
|
|
12
|
+
// `startWatch(options)` (resolves once watching), `stopWatch()`; event `position` → GeoPosition.
|
|
13
|
+
|
|
14
|
+
import { ServiceClient } from "../runtime/service"
|
|
15
|
+
|
|
16
|
+
export interface GeoPosition {
|
|
17
|
+
latitude: number
|
|
18
|
+
longitude: number
|
|
19
|
+
/** Horizontal accuracy radius, meters. */
|
|
20
|
+
accuracy: number
|
|
21
|
+
/** Meters above sea level, or null when the provider doesn't know. */
|
|
22
|
+
altitude: number | null
|
|
23
|
+
/** Direction of travel in degrees clockwise from north, or null when stationary/unknown. */
|
|
24
|
+
heading: number | null
|
|
25
|
+
/** Ground speed in m/s, or null when unknown. */
|
|
26
|
+
speed: number | null
|
|
27
|
+
/** When this fix was taken, ms epoch. */
|
|
28
|
+
timestamp: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GeoOptions {
|
|
32
|
+
/** Prefer the precise (GPS) provider — slower first fix, more battery. Default false. */
|
|
33
|
+
highAccuracy?: boolean
|
|
34
|
+
/** getCurrent only: reject with "timeout" if no fix arrives in this many ms. */
|
|
35
|
+
timeout?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A live `Geolocation.watch()` subscription — `stop()` it when the screen goes away. */
|
|
39
|
+
export interface GeoWatch {
|
|
40
|
+
stop(): void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The device position. `Geolocation.isSupported` reports whether this host registered the
|
|
45
|
+
* service — check it before offering location features.
|
|
46
|
+
*/
|
|
47
|
+
// PURE IIFE so an app that never uses location tree-shakes the whole plugin away (see NativeView).
|
|
48
|
+
export const Geolocation: {
|
|
49
|
+
/** Whether this host registered a "geolocation" service. */
|
|
50
|
+
readonly isSupported: boolean
|
|
51
|
+
/** One position fix. Prompts for permission on first use; rejects on denial ("denied"), no
|
|
52
|
+
* provider ("unavailable"), or `options.timeout` elapsing ("timeout"). */
|
|
53
|
+
getCurrent(options?: GeoOptions): Promise<GeoPosition>
|
|
54
|
+
/** Continuous updates. Resolves once watching (permission granted + provider started) — so a
|
|
55
|
+
* denial is a rejection, never a silently dead callback. One host watch serves every
|
|
56
|
+
* subscriber; the options of the watch that starts it win. Always `stop()` when done. */
|
|
57
|
+
watch(callback: (position: GeoPosition) => void, options?: GeoOptions): Promise<GeoWatch>
|
|
58
|
+
} = /*#__PURE__*/ (() => {
|
|
59
|
+
let client: ServiceClient | null = null
|
|
60
|
+
const service = (): ServiceClient => client ??= new ServiceClient("geolocation")
|
|
61
|
+
|
|
62
|
+
// Watch state: one host watch fans out to N subscribers (SDK-side), the last stop() releases it.
|
|
63
|
+
const watchers: ((position: GeoPosition) => void)[] = []
|
|
64
|
+
let positionHooked = false
|
|
65
|
+
let startPromise: Promise<void> | null = null
|
|
66
|
+
|
|
67
|
+
const assertSupported = (): void => {
|
|
68
|
+
// Fail before the host is touched — a permission dialog for a feature that can't exist
|
|
69
|
+
// would be worse than the error (same ordering rule as QRScanner.open).
|
|
70
|
+
if (!ServiceClient.isSupported("geolocation")) {
|
|
71
|
+
throw new Error('Geolocation isn\'t available on this host — it needs a registered "geolocation" service.')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const removeWatcher = (callback: (position: GeoPosition) => void): void => {
|
|
76
|
+
const i = watchers.indexOf(callback)
|
|
77
|
+
if (i >= 0) watchers.splice(i, 1)
|
|
78
|
+
if (watchers.length === 0 && startPromise !== null) {
|
|
79
|
+
startPromise = null
|
|
80
|
+
service().call("stopWatch").catch(() => {})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const geolocation = {
|
|
85
|
+
async getCurrent(options?: GeoOptions): Promise<GeoPosition> {
|
|
86
|
+
assertSupported()
|
|
87
|
+
return await service().call("getCurrent", options ?? {})
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
async watch(callback: (position: GeoPosition) => void, options?: GeoOptions): Promise<GeoWatch> {
|
|
91
|
+
assertSupported()
|
|
92
|
+
if (!positionHooked) {
|
|
93
|
+
positionHooked = true
|
|
94
|
+
service().on("position", (position?: GeoPosition) => {
|
|
95
|
+
if (position) for (const cb of [...watchers]) cb(position)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
watchers.push(callback)
|
|
99
|
+
startPromise ??= service().call("startWatch", options ?? {})
|
|
100
|
+
try {
|
|
101
|
+
await startPromise
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// The watch never started — unwind without the stopWatch a live watch would need.
|
|
104
|
+
const i = watchers.indexOf(callback)
|
|
105
|
+
if (i >= 0) watchers.splice(i, 1)
|
|
106
|
+
if (watchers.length === 0) startPromise = null
|
|
107
|
+
throw e
|
|
108
|
+
}
|
|
109
|
+
let stopped = false
|
|
110
|
+
return {
|
|
111
|
+
stop(): void {
|
|
112
|
+
if (stopped) return
|
|
113
|
+
stopped = true
|
|
114
|
+
removeWatcher(callback)
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
Object.defineProperty(geolocation, "isSupported", {
|
|
120
|
+
get: (): boolean => ServiceClient.isSupported("geolocation"),
|
|
121
|
+
})
|
|
122
|
+
return geolocation as any
|
|
123
|
+
})()
|
|
@@ -0,0 +1,7 @@
|
|
|
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
|
|
3
|
+
// the web). Shared by ARScene.prepare, QRScanner and CameraView.
|
|
4
|
+
export const _requestCameraPermission = (): Promise<void> =>
|
|
5
|
+
typeof _creatorUtils.requestCamera === "function"
|
|
6
|
+
? new Promise<void>((res, rej) => _creatorUtils.requestCamera!(res, rej))
|
|
7
|
+
: Promise.resolve()
|
|
@@ -1,36 +1,73 @@
|
|
|
1
|
-
// QRScanner —
|
|
2
|
-
// (
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// QRScanner — the host's camera QR scanner as a Presentable. A typed wrapper over the
|
|
2
|
+
// `NativeView("qrScanner")` capability (docs/navigation-presentable-plan.md): open it fullscreen
|
|
3
|
+
// (`await scanner.open()`), push it (`Router.push(scanner)`), or embed it among a screen's
|
|
4
|
+
// children as a live preview node — everything a NativeView can do, plus a typed `onScan`.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Host-OPTIONAL, like every plugin: it exists where the host registers a "qrScanner" view
|
|
7
|
+
// (Android; web can via `_creatorUI._registerView`) — gate on `QRScanner.isSupported`. Opening
|
|
8
|
+
// prepares first (camera permission) while the current destination stays visible — the same
|
|
9
|
+
// prepare-then-present contract as ARScene.
|
|
10
|
+
//
|
|
11
|
+
// The pre-Presentable `_creatorUtils.openScanner/closeScanner` bridges are gone from the ABI
|
|
12
|
+
// (2026-07-15): hosts keep their implementations only to serve previously compiled bundles.
|
|
13
|
+
|
|
14
|
+
import { _navEpoch, type PresentOptions } from "../ui/presentable"
|
|
15
|
+
import { NativeView, NativeViewElement } from "../ui/NativeView"
|
|
16
|
+
import { _requestCameraPermission } from "./permission"
|
|
9
17
|
|
|
10
|
-
export
|
|
11
|
-
|
|
18
|
+
export interface QRScanner extends NativeView {
|
|
19
|
+
/** Fires per decoded camera frame: the decoded string, or `null` for a frame with no readable
|
|
20
|
+
* code (expect those repeatedly while the user aims). The same code can be reported more than
|
|
21
|
+
* once — `close()` or debounce once you have what you need. */
|
|
22
|
+
onScan(callback: (data: string | null) => void): this
|
|
23
|
+
/** Request camera permission, then present the scanner. The current destination (and its
|
|
24
|
+
* loading state) stays visible until the scanner is ready; rejects if the camera is denied,
|
|
25
|
+
* the host can't scan, or another navigation superseded this one. */
|
|
26
|
+
open(options?: PresentOptions): Promise<void>
|
|
27
|
+
}
|
|
12
28
|
|
|
13
|
-
|
|
14
|
-
constructor(
|
|
15
|
-
|
|
29
|
+
class QRScannerElement extends NativeViewElement {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("qrScanner", {})
|
|
16
32
|
}
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return typeof _creatorUtils.openScanner === "function"
|
|
34
|
+
onScan(callback: (data: string | null) => void): this {
|
|
35
|
+
return this.on("scan", (payload?: { data?: string | null }) => callback(payload?.data ?? null))
|
|
21
36
|
}
|
|
22
37
|
|
|
23
|
-
/**
|
|
24
|
-
async
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
/** @internal prepare-then-present: permission before the swap (see Presentable._prepare). */
|
|
39
|
+
async _prepare(): Promise<void> {
|
|
40
|
+
const epoch = _navEpoch()
|
|
41
|
+
await _requestCameraPermission()
|
|
42
|
+
if (_navEpoch() !== epoch) {
|
|
43
|
+
throw new Error("QRScanner.open() superseded: another destination was opened while preparing")
|
|
27
44
|
}
|
|
28
|
-
await new Promise<void>((res, rej) => _creator.requestCamera(res, rej))
|
|
29
|
-
await new Promise<void>((res, rej) => _creatorUtils.openScanner(res, rej, this._onData))
|
|
30
45
|
}
|
|
31
46
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
47
|
+
override async open(options?: PresentOptions): Promise<void> {
|
|
48
|
+
// Fail before prompting for the camera — a permission dialog for a scanner that can't exist
|
|
49
|
+
// would be worse than the error.
|
|
50
|
+
if (!NativeView.isSupported("qrScanner")) {
|
|
51
|
+
throw new Error("QRScanner isn't available on this host — it needs a registered \"qrScanner\" view.")
|
|
52
|
+
}
|
|
53
|
+
await this._prepare()
|
|
54
|
+
super.open(options)
|
|
35
55
|
}
|
|
36
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a QR scanner view. `QRScanner.isSupported` reports whether this host can scan at all —
|
|
60
|
+
* check it before offering the feature.
|
|
61
|
+
*/
|
|
62
|
+
// PURE IIFE so an app that never scans tree-shakes the whole plugin away (see NativeView).
|
|
63
|
+
export const QRScanner: {
|
|
64
|
+
(): QRScanner
|
|
65
|
+
/** Whether this host registered a "qrScanner" view. */
|
|
66
|
+
readonly isSupported: boolean
|
|
67
|
+
} = /*#__PURE__*/ (() => {
|
|
68
|
+
const factory = (): QRScanner => new QRScannerElement() as unknown as QRScanner
|
|
69
|
+
Object.defineProperty(factory, "isSupported", {
|
|
70
|
+
get: (): boolean => NativeView.isSupported("qrScanner"),
|
|
71
|
+
})
|
|
72
|
+
return factory as any
|
|
73
|
+
})()
|