lecodes-sdk 1.2.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.
- package/README.md +5 -2
- package/dist/editor.d.ts +12 -0
- package/dist/global.d.ts +4 -8
- package/dist/host.d.ts +2 -3
- package/dist/types/animate/tween/Animation.d.ts +0 -3
- package/dist/types/animate/tween/animateValue.d.ts +4 -2
- package/dist/types/animate/tween/easing.d.ts +8 -0
- package/dist/types/animate/tween/spec.d.ts +15 -7
- package/dist/types/audio/audio.d.ts +2 -1
- package/dist/types/canvas/Canvas.d.ts +40 -108
- package/dist/types/canvas/gen/cssColor.d.ts +17 -0
- package/dist/types/canvas/gen/recorder.d.ts +118 -0
- package/dist/types/canvas/gen/spec.d.ts +144 -0
- package/dist/types/core/color.d.ts +3 -1
- package/dist/types/core/pins.d.ts +18 -0
- package/dist/types/g2/Node2D.d.ts +5 -8
- package/dist/types/g2/Scene2D.d.ts +6 -2
- package/dist/types/gl/Foliage.d.ts +30 -7
- package/dist/types/gl/Light.d.ts +8 -0
- package/dist/types/gl/Lightmap.d.ts +13 -2
- package/dist/types/gl/Material.d.ts +17 -3
- package/dist/types/gl/Model.d.ts +6 -2
- package/dist/types/gl/Node.d.ts +3 -6
- package/dist/types/gl/Scene.d.ts +41 -13
- package/dist/types/gl/Texture.d.ts +1 -1
- package/dist/types/gl/animation/Locomotion.d.ts +8 -1
- package/dist/types/inject.d.ts +11 -11
- package/dist/types/inject.editor.d.ts +1 -0
- package/dist/types/net/core.d.ts +7 -0
- package/dist/types/plugin.d.ts +76 -0
- package/dist/types/plugins/gen/camera/sdk/camera.d.ts +24 -0
- package/dist/types/plugins/gen/camera/sdk/camera.gen.d.ts +25 -0
- package/dist/types/plugins/{geolocation.d.ts → gen/geolocation/sdk/geolocation.d.ts} +2 -20
- package/dist/types/plugins/gen/geolocation/sdk/geolocation.gen.d.ts +31 -0
- package/dist/types/plugins/{map.d.ts → gen/map/sdk/map.d.ts} +9 -50
- package/dist/types/plugins/gen/map/sdk/map.gen.d.ts +53 -0
- package/dist/types/plugins/gen/push/sdk/push.d.ts +23 -0
- package/dist/types/plugins/gen/push/sdk/push.gen.d.ts +35 -0
- package/dist/types/plugins/{qr.d.ts → gen/qr-scanner/sdk/qr-scanner.d.ts} +2 -3
- package/dist/types/plugins/gen/qr-scanner/sdk/qr-scanner.gen.d.ts +15 -0
- package/dist/types/runtime/app.d.ts +9 -2
- package/dist/types/runtime/fetch.d.ts +2 -0
- package/dist/types/runtime/input.d.ts +1 -1
- package/dist/types/runtime/media.d.ts +6 -10
- package/dist/types/runtime/misc.d.ts +4 -1
- package/dist/types/runtime/net.d.ts +3 -2
- package/dist/types/runtime/touch.d.ts +32 -0
- package/dist/types/scene/defineScene.d.ts +43 -2
- package/dist/types/scene/editor.d.ts +52 -0
- package/dist/types/scene/gizmos.d.ts +7 -4
- package/dist/types/ui/NativeView.d.ts +6 -4
- package/dist/types/ui/UI.d.ts +1 -1
- package/dist/types/ui/UIBottomSheet.d.ts +6 -12
- package/dist/types/ui/UIButton.d.ts +12 -14
- package/dist/types/ui/UIContainer.d.ts +0 -6
- package/dist/types/ui/UIImage.d.ts +1 -4
- package/dist/types/ui/UIInput.d.ts +8 -24
- package/dist/types/ui/UIModal.d.ts +0 -2
- package/dist/types/ui/UINode.d.ts +70 -56
- package/dist/types/ui/UIPager.d.ts +28 -27
- package/dist/types/ui/UIPopover.d.ts +0 -2
- package/dist/types/ui/UIScreen.d.ts +12 -17
- package/dist/types/ui/UIScrollable.d.ts +1 -4
- package/dist/types/ui/UIText.d.ts +0 -2
- package/dist/types/ui/UIVideo.d.ts +3 -5
- package/dist/types/ui/UIVirtualizedList.d.ts +14 -16
- package/dist/types/ui/UIWidget.d.ts +10 -9
- package/dist/types/ui/colorKeys.gen.d.ts +9 -0
- package/dist/types/ui/presentable.d.ts +46 -32
- package/dist/types/ui/router.d.ts +18 -7
- package/dist/types/ui/styleColor.d.ts +1 -0
- package/dist/types/ui/transitions.d.ts +18 -0
- package/dist/types/ui/tree.d.ts +75 -0
- package/dist/types/version.d.ts +10 -0
- package/dist/types.json +1 -1
- package/package.json +12 -3
- package/prompts/2d.md +2 -6
- package/prompts/3d.md +1 -5
- package/prompts/README.md +1 -1
- package/prompts/canvas.md +9 -8
- package/prompts/compose.ts +1 -1
- package/prompts/core.md +3 -3
- package/prompts/design.md +1 -1
- package/prompts/dist/2d-game.md +473 -239
- package/prompts/dist/3d-app.md +553 -205
- package/prompts/dist/ar-app.md +435 -202
- package/prompts/dist/design.md +113 -95
- package/prompts/dist/ui-app.md +386 -170
- package/prompts/ui-design.md +2 -3
- package/prompts/ui.md +45 -37
- package/src/animate/tween/Animation.ts +34 -150
- package/src/animate/tween/Timeline.ts +175 -175
- package/src/animate/tween/animateValue.ts +6 -3
- package/src/animate/tween/easing.ts +10 -3
- package/src/animate/tween/spec.ts +41 -15
- package/src/audio/Sound.ts +3 -3
- package/src/audio/audio.ts +2 -1
- package/src/bridges/2d.d.ts +317 -0
- package/src/bridges/app.d.ts +91 -0
- package/src/bridges/audio.d.ts +97 -0
- package/src/bridges/canvas.d.ts +79 -0
- package/src/bridges/device.d.ts +72 -0
- package/src/bridges/fetch.d.ts +80 -0
- package/src/bridges/files.d.ts +70 -0
- package/src/bridges/gl.d.ts +1133 -0
- package/src/bridges/input.d.ts +72 -0
- package/src/bridges/media.d.ts +55 -0
- package/src/bridges/nav.d.ts +71 -0
- package/src/bridges/net.d.ts +52 -0
- package/src/bridges/service.d.ts +52 -0
- package/src/bridges/socket.d.ts +31 -0
- package/src/bridges/storage.d.ts +33 -0
- package/src/bridges/tree.d.ts +301 -0
- package/src/bridges/types.d.ts +49 -0
- package/src/canvas/Canvas.ts +114 -159
- package/src/canvas/gen/cssColor.ts +224 -0
- package/src/canvas/gen/recorder.ts +212 -0
- package/src/canvas/gen/spec.ts +201 -0
- package/src/chisel.ts +193 -0
- package/src/compile/assetMacro.ts +1 -1
- package/src/compile/bundler.ts +11 -2
- package/src/compile/compileProject.ts +43 -4
- package/src/compile/fontMacro.ts +3 -4
- package/src/compile/header.ts +26 -5
- package/src/compile/index.ts +3 -1
- package/src/compile/liteMaterial.ts +1 -1
- package/src/compile/sceneEditor.ts +11 -26
- package/src/core/color.ts +73 -30
- package/src/core/pins.ts +51 -0
- package/src/core/signals.ts +8 -1
- package/src/g2/CharacterController2D.ts +3 -3
- package/src/g2/Node2D.ts +57 -39
- package/src/g2/Physics2D.ts +2 -2
- package/src/g2/Scene2D.ts +35 -23
- package/src/g2/Texture2D.ts +1 -1
- package/src/g2/loop.ts +4 -4
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Foliage.ts +72 -17
- package/src/gl/Geometry.ts +1 -2
- package/src/gl/Light.ts +10 -0
- package/src/gl/Lightmap.ts +45 -29
- package/src/gl/Material.ts +95 -51
- package/src/gl/Mesh.ts +120 -120
- package/src/gl/Model.ts +21 -16
- package/src/gl/Node.ts +91 -24
- package/src/gl/Particles.ts +1 -1
- package/src/gl/Scene.ts +100 -44
- package/src/gl/Texture.ts +8 -7
- package/src/gl/animation/AnimationClip.ts +1 -1
- package/src/gl/animation/DynamicBone.ts +482 -482
- package/src/gl/animation/Locomotion.ts +8 -3
- package/src/gl/nav/NavMesh.ts +3 -4
- package/src/gl/physics/Physics.ts +2 -2
- package/src/gl/physics/physicsEvents.ts +3 -3
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/terrain/Terrain.ts +4 -5
- package/src/gl/touch.ts +14 -15
- package/src/host.d.ts +2 -3
- package/src/inject.editor.ts +7 -0
- package/src/inject.ts +13 -16
- package/src/net/core.ts +6 -5
- package/src/net/index.ts +1 -1
- package/src/net/replication.ts +1 -1
- package/src/plugin.ts +191 -0
- package/src/plugins/gen/camera/contract.d.ts +27 -0
- package/src/plugins/gen/camera/sdk/camera.gen.ts +46 -0
- package/src/plugins/gen/camera/sdk/camera.ts +57 -0
- package/src/plugins/gen/geolocation/contract.d.ts +50 -0
- package/src/plugins/gen/geolocation/sdk/geolocation.gen.ts +54 -0
- package/src/plugins/{geolocation.ts → gen/geolocation/sdk/geolocation.ts} +22 -43
- package/src/plugins/gen/map/contract.d.ts +144 -0
- package/src/plugins/gen/map/sdk/map.gen.ts +88 -0
- package/src/plugins/{map.ts → gen/map/sdk/map.ts} +68 -102
- package/src/plugins/gen/push/contract.d.ts +61 -0
- package/src/plugins/gen/push/sdk/push.gen.ts +60 -0
- package/src/plugins/gen/push/sdk/push.ts +105 -0
- package/src/plugins/gen/qr-scanner/contract.d.ts +16 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.gen.ts +29 -0
- package/src/plugins/gen/qr-scanner/sdk/qr-scanner.ts +52 -0
- package/src/plugins/permission.ts +5 -4
- package/src/runtime/app.ts +20 -9
- package/src/runtime/appEvents.ts +5 -4
- package/src/runtime/channel.ts +18 -15
- package/src/runtime/clipboard.ts +4 -3
- package/src/runtime/datetime.ts +2 -1
- package/src/runtime/device.ts +17 -15
- package/src/runtime/fetch.ts +30 -20
- package/src/runtime/files.ts +16 -15
- package/src/runtime/input.ts +12 -10
- package/src/runtime/media.ts +50 -46
- package/src/runtime/misc.ts +7 -3
- package/src/runtime/net.ts +8 -7
- package/src/runtime/rpc.ts +1 -3
- package/src/runtime/service.ts +19 -14
- package/src/runtime/share.ts +4 -3
- package/src/runtime/storage.ts +6 -4
- package/src/runtime/touch.ts +32 -0
- package/src/scene/defineScene.ts +61 -365
- package/src/scene/editor.ts +408 -0
- package/src/scene/editorPlugins.ts +3 -3
- package/src/scene/gizmos.ts +15 -9
- package/src/server/db/marci/query.ts +1 -1
- package/src/server/host.ts +1 -1
- package/src/server/runtime.ts +1 -1
- package/src/ui/NativeView.ts +72 -25
- package/src/ui/UI.ts +3 -3
- package/src/ui/UIBottomSheet.ts +16 -17
- package/src/ui/UIButton.ts +54 -16
- package/src/ui/UIContainer.ts +0 -6
- package/src/ui/UIImage.ts +34 -30
- package/src/ui/UIInput.ts +29 -37
- package/src/ui/UIModal.ts +1 -3
- package/src/ui/UINode.ts +347 -297
- package/src/ui/UIPager.ts +93 -78
- package/src/ui/UIPopover.ts +0 -2
- package/src/ui/UIScreen.ts +41 -36
- package/src/ui/UIScrollable.ts +19 -13
- package/src/ui/UISpacer.ts +1 -1
- package/src/ui/UITabs.ts +8 -6
- package/src/ui/UIText.ts +7 -17
- package/src/ui/UIVideo.ts +30 -27
- package/src/ui/UIVirtualizedList.ts +58 -59
- package/src/ui/UIWidget.ts +38 -18
- package/src/ui/colorKeys.gen.ts +37 -0
- package/src/ui/fonts.ts +2 -2
- package/src/ui/presentable.ts +59 -42
- package/src/ui/router.ts +52 -35
- package/src/ui/styleColor.ts +56 -0
- package/src/ui/theme.ts +7 -6
- package/src/ui/transitions.ts +249 -0
- package/src/ui/tree.ts +346 -0
- package/src/version.ts +24 -0
- package/tests/helpers/engineWorld.ts +11 -0
- package/tests/helpers/fakeTree.ts +353 -0
- package/tests/helpers/hostStubs.ts +31 -0
- package/tests/helpers/index.ts +14 -0
- package/tests/helpers/memoryMarci.ts +124 -0
- package/tests/helpers/phases.ts +23 -0
- package/tests/helpers/preload.ts +18 -0
- package/tests/helpers/stubApp.ts +2 -0
- package/tests/helpers/stubDevice.ts +2 -0
- package/tests/helpers/stubFetch.ts +2 -0
- package/tests/helpers/stubInput.ts +2 -0
- package/dist/inject.js +0 -4629
- package/dist/types/core/registry.d.ts +0 -7
- package/dist/types/plugins/camera.d.ts +0 -25
- package/dist/types/plugins/push.d.ts +0 -46
- package/src/bridges.d.ts +0 -1769
- package/src/compile/__tests__/assetIconMacro.test.ts +0 -219
- package/src/compile/__tests__/assetMacro.test.ts +0 -100
- package/src/compile/__tests__/assetName.test.ts +0 -55
- package/src/compile/__tests__/compile.test.ts +0 -310
- package/src/compile/__tests__/detectEntry.test.ts +0 -151
- package/src/compile/__tests__/fontMacro.test.ts +0 -199
- package/src/compile/__tests__/serverSplit.test.ts +0 -27
- package/src/core/__tests__/stateMachine.test.ts +0 -132
- package/src/core/registry.ts +0 -23
- package/src/plugins/camera.ts +0 -81
- package/src/plugins/push.ts +0 -132
- package/src/plugins/qr.ts +0 -73
package/src/runtime/app.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
96
|
+
setAppBackHandler(callback ?? null)
|
|
86
97
|
},
|
|
87
98
|
addEventListener<E extends keyof AppEventMap>(event: E, callback: AppEventMap[E]): void {
|
|
88
99
|
if (event === "keyboard") {
|
package/src/runtime/appEvents.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// The shared app-event dispatcher. The host exposes ONE
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
48
|
+
if (typeof _creatorInput !== "undefined") _creatorInput.registerKeyboardEvent?.(dispatchKeyboard)
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
package/src/runtime/channel.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
// The
|
|
2
|
-
// (`
|
|
3
|
-
// (`
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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:
|
|
25
|
-
export const _channelEmit = (listeners: ChannelListeners, event: string,
|
|
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:
|
|
34
|
-
onComplete: (result?:
|
|
34
|
+
id: number, method: string, args: unknown[],
|
|
35
|
+
onComplete: (result?: unknown) => void, onError: (err: string) => void,
|
|
35
36
|
) => void
|
|
36
37
|
|
|
37
|
-
/** @internal one
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
})
|
package/src/runtime/clipboard.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
14
|
+
if (typeof _creatorDevice.clipboardRead !== "function") {
|
|
14
15
|
reject(new Error("Clipboard is not supported on this platform"))
|
|
15
16
|
return
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
_creatorDevice.clipboardRead(resolve, (err: string) => reject(new Error(err)))
|
|
18
19
|
})
|
|
19
20
|
},
|
|
20
21
|
}
|
package/src/runtime/datetime.ts
CHANGED
|
@@ -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
|
|
62
|
+
const lang = typeof _creatorDevice !== "undefined" ? _creatorDevice.language : ""
|
|
62
63
|
return lang && lang.toLowerCase().indexOf("ru") === 0 ? "ru" : "en"
|
|
63
64
|
}
|
|
64
65
|
|
package/src/runtime/device.ts
CHANGED
|
@@ -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
|
|
98
|
+
return _creatorDevice.platform
|
|
98
99
|
},
|
|
99
100
|
get language(): string {
|
|
100
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
273
|
+
ensureEmitter()
|
|
274
|
+
onTreeResize(onResize)
|
|
273
275
|
}
|
|
274
276
|
},
|
|
275
277
|
removeEventListener<E extends keyof DeviceEventMap>(channel: E, callback: DeviceEventMap[E]): void {
|
package/src/runtime/fetch.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// HTTP via the host. `fetch` is async (host streams the body), `fetchLocal` reads a preloaded asset
|
|
2
|
-
// synchronously.
|
|
2
|
+
// synchronously. Over the `_creatorFetch` bridge (sdk/src/bridges/fetch.d.ts).
|
|
3
|
+
//
|
|
4
|
+
// Ownership (owned handles, docs/plans/hosts-unification-plan.md phase 10): a fetched body lives as long as
|
|
5
|
+
// the app holds the response — `_h` is the owned handle the runtime returned with the completion,
|
|
6
|
+
// its collection frees the host buffer. There is nothing to dispose. A LOCAL asset's buffer is the
|
|
7
|
+
// host's (cached by name, shared by every `fetchLocal` of that name), so its wrapper only carries
|
|
8
|
+
// the id. The same goes for a `File` (picker / camera / canvas export): a host buffer the wrapper
|
|
9
|
+
// names but does not own.
|
|
10
|
+
|
|
3
11
|
|
|
4
12
|
export type FetchOptions = {
|
|
5
13
|
body?: any
|
|
@@ -13,47 +21,50 @@ export interface FetchResponse {
|
|
|
13
21
|
readonly status: number
|
|
14
22
|
json<T = any>(): T
|
|
15
23
|
text(): string
|
|
24
|
+
/** @deprecated A no-op: the body is freed with the response's handle (owned handles). Apps written
|
|
25
|
+
* against the pre-owned SDK still call it after `text()` / `json()`; remove the call. */
|
|
16
26
|
dispose(): void
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
class FetchResponseImpl implements FetchResponse {
|
|
20
30
|
readonly status: number
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
/** @internal The buffer handle (`_h.id` = the host buffer id). Hosts and the runtime read a buffer
|
|
32
|
+
* off any object carrying `_h` — a File / FetchResponse alike (FormData entries, `UIImage` src,
|
|
33
|
+
* `Texture.load`, `share`, `files.write`). */
|
|
34
|
+
readonly _h: Handle
|
|
35
|
+
constructor(h: Handle, status: number) {
|
|
36
|
+
this._h = h
|
|
24
37
|
this.status = status
|
|
25
38
|
}
|
|
26
39
|
json<T = any>(): T {
|
|
27
|
-
return
|
|
40
|
+
return _creatorFetch.toJson(this._h.id)
|
|
28
41
|
}
|
|
29
42
|
text(): string {
|
|
30
|
-
return
|
|
31
|
-
}
|
|
32
|
-
dispose(): void {
|
|
33
|
-
_creatorUtils.disposeFetch(this._id)
|
|
43
|
+
return _creatorFetch.toText(this._h.id)
|
|
34
44
|
}
|
|
45
|
+
/** @deprecated see the interface */
|
|
46
|
+
dispose(): void {}
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
export const fetch = (url: string, options: FetchOptions = {}): Promise<FetchResponse> =>
|
|
38
50
|
new Promise<FetchResponse>((resolve, reject) => {
|
|
39
|
-
|
|
40
|
-
resolve(new FetchResponseImpl(
|
|
51
|
+
_creatorFetch.fetch(url, options, (h, statusCode) => {
|
|
52
|
+
resolve(new FetchResponseImpl(h, statusCode))
|
|
41
53
|
}, reject)
|
|
42
54
|
})
|
|
43
55
|
|
|
44
56
|
export const fetchLocal = (path: string): FetchResponse =>
|
|
45
|
-
new FetchResponseImpl(
|
|
57
|
+
new FetchResponseImpl({ id: _creatorFetch.local(path) }, 200) // the host's buffer: not owned
|
|
46
58
|
|
|
47
59
|
/** A handle to a picked / fetched file (see openFilePicker). */
|
|
48
60
|
export class File {
|
|
49
|
-
/** @internal host buffer
|
|
50
|
-
*
|
|
51
|
-
|
|
52
|
-
readonly _id: number
|
|
61
|
+
/** @internal The host buffer (not owned — see the header). Same key `FetchResponse` carries, so
|
|
62
|
+
* both are accepted anywhere a buffer is expected. */
|
|
63
|
+
readonly _h: Handle
|
|
53
64
|
readonly name: string
|
|
54
65
|
readonly size: number
|
|
55
66
|
constructor(systemId: number, name: string, size: number) {
|
|
56
|
-
this.
|
|
67
|
+
this._h = { id: systemId }
|
|
57
68
|
this.name = name
|
|
58
69
|
this.size = size
|
|
59
70
|
}
|
|
@@ -62,9 +73,8 @@ export class File {
|
|
|
62
73
|
type FormDataValue = string | number | boolean | File | FetchResponse
|
|
63
74
|
|
|
64
75
|
export class FormData {
|
|
65
|
-
// Host-facing:
|
|
66
|
-
// (the buffer id), everything else as a text field. See
|
|
67
|
-
// creator-pkg creatorUtilsHandler.cpp, iOS Fetch.swift buildMultipartBody.
|
|
76
|
+
// Host-facing: the runtime walks `_entries` and treats any object value carrying `_h` as binary
|
|
77
|
+
// (the buffer id), everything else as a text field. See runtime/native src/host/fetch.cpp.
|
|
68
78
|
private _entries: [string, FormDataValue, filename?: string][] = []
|
|
69
79
|
|
|
70
80
|
append(name: string, value: FormDataValue, filename?: string): void {
|
package/src/runtime/files.ts
CHANGED
|
@@ -12,7 +12,9 @@ export function openFilePicker(
|
|
|
12
12
|
options: OpenFilePickerOptions & { multiple?: boolean } = {},
|
|
13
13
|
): Promise<File | File[] | null> {
|
|
14
14
|
return new Promise((resolve, reject) => {
|
|
15
|
-
|
|
15
|
+
// No picker on this host (headless, a hidden render): nothing picked, like a cancelled dialog.
|
|
16
|
+
if (typeof _creatorDevice.openFilePicker !== "function") { resolve(options.multiple ? [] : null); return }
|
|
17
|
+
_creatorDevice.openFilePicker((resp) => {
|
|
16
18
|
if (!resp || resp.length === 0) {
|
|
17
19
|
resolve(options.multiple ? [] : null)
|
|
18
20
|
return
|
|
@@ -24,7 +26,7 @@ export function openFilePicker(
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
// ---- Local filesystem -------------------------------------------------------------------------
|
|
27
|
-
// `files` is the read/write/delete half of `fetchLocal`, for platforms where the app owns a
|
|
29
|
+
// `files` (the `_creatorFiles` bridge, absent on sandboxed hosts) is the read/write/delete half of `fetchLocal`, for platforms where the app owns a
|
|
28
30
|
// filesystem — desktop today. A RELATIVE path is resolved against the app's OWN folder (for a
|
|
29
31
|
// packaged desktop app: the directory the exe sits in), never the process cwd, so a save written by
|
|
30
32
|
// a shortcut and one written from a terminal land in the same place; an ABSOLUTE path goes wherever
|
|
@@ -61,8 +63,7 @@ export type FileEntry = {
|
|
|
61
63
|
modified: number
|
|
62
64
|
}
|
|
63
65
|
|
|
64
|
-
const supported = () =>
|
|
65
|
-
typeof _creatorUtils !== "undefined" && typeof _creatorUtils.writeLocal === "function"
|
|
66
|
+
const supported = () => typeof _creatorFiles !== "undefined"
|
|
66
67
|
|
|
67
68
|
// The host is asked for the work only when it can do it; otherwise the promise rejects with the
|
|
68
69
|
// same message the native bridge uses, so a project sees one error either way.
|
|
@@ -72,10 +73,10 @@ const run = <T>(op: string, path: string, call: (resolve: (v: T) => void, reject
|
|
|
72
73
|
call(resolve, reject)
|
|
73
74
|
})
|
|
74
75
|
|
|
75
|
-
// A File / FetchResponse crosses as
|
|
76
|
-
//
|
|
76
|
+
// A File / FetchResponse crosses as its buffer handle — the same `_h` convention FormData entries
|
|
77
|
+
// and image `src` objects use, so the bytes never travel through JS.
|
|
77
78
|
const payload = (data: FileData) =>
|
|
78
|
-
(typeof data === "object" && "
|
|
79
|
+
(typeof data === "object" && "_h" in (data as any) ? { _h: (data as any)._h } : data) as any
|
|
79
80
|
|
|
80
81
|
export const files = {
|
|
81
82
|
/** Is there a filesystem here (desktop)? False on web/mobile, where every other call rejects. */
|
|
@@ -86,38 +87,38 @@ export const files = {
|
|
|
86
87
|
/** Replace the file's contents, creating the parent folders on the way. */
|
|
87
88
|
write(path: string, data: FileData): Promise<void> {
|
|
88
89
|
return run<void>("write", path, (resolve, reject) =>
|
|
89
|
-
|
|
90
|
+
_creatorFiles.write(path, payload(data), false, () => resolve(), reject))
|
|
90
91
|
},
|
|
91
92
|
|
|
92
93
|
/** Add to the end of the file, creating it (and the parent folders) when it isn't there yet. */
|
|
93
94
|
append(path: string, data: FileData): Promise<void> {
|
|
94
95
|
return run<void>("append", path, (resolve, reject) =>
|
|
95
|
-
|
|
96
|
+
_creatorFiles.write(path, payload(data), true, () => resolve(), reject))
|
|
96
97
|
},
|
|
97
98
|
|
|
98
99
|
/** The whole file as UTF-8 text — null when there is no such file. */
|
|
99
100
|
readText(path: string): Promise<string | null> {
|
|
100
101
|
return run<string | null>("readText", path, (resolve, reject) =>
|
|
101
|
-
|
|
102
|
+
_creatorFiles.readText(path, resolve, reject))
|
|
102
103
|
},
|
|
103
104
|
|
|
104
105
|
/** The whole file as bytes — null when there is no such file. */
|
|
105
106
|
readBytes(path: string): Promise<Uint8Array | null> {
|
|
106
107
|
return run<Uint8Array | null>("readBytes", path, (resolve, reject) =>
|
|
107
|
-
|
|
108
|
+
_creatorFiles.readBytes(path, resolve, reject))
|
|
108
109
|
},
|
|
109
110
|
|
|
110
111
|
/** Delete a file, or a directory (empty, or any directory with `{ recursive: true }`). Deleting
|
|
111
112
|
* something already gone is success — what's promised is that the path is free afterwards. */
|
|
112
113
|
rm(path: string, options: { recursive?: boolean } = {}): Promise<void> {
|
|
113
114
|
return run<void>("rm", path, (resolve, reject) =>
|
|
114
|
-
|
|
115
|
+
_creatorFiles.remove(path, !!options.recursive, () => resolve(), reject))
|
|
115
116
|
},
|
|
116
117
|
|
|
117
118
|
/** Create a directory and any missing parents. An existing directory is fine. */
|
|
118
119
|
mkdir(path: string): Promise<void> {
|
|
119
120
|
return run<void>("mkdir", path, (resolve, reject) =>
|
|
120
|
-
|
|
121
|
+
_creatorFiles.mkdir(path, () => resolve(), reject))
|
|
121
122
|
},
|
|
122
123
|
|
|
123
124
|
/** The entries of a directory, sorted by path — null when there is no such directory (an empty
|
|
@@ -126,13 +127,13 @@ export const files = {
|
|
|
126
127
|
* is a file, not a directory, rejects. Filter in JS — `entries.filter(e => e.name.endsWith(".json"))`. */
|
|
127
128
|
ls(path: string, options: { recursive?: boolean } = {}): Promise<FileEntry[] | null> {
|
|
128
129
|
return run<FileEntry[] | null>("ls", path, (resolve, reject) =>
|
|
129
|
-
|
|
130
|
+
_creatorFiles.list(path, !!options.recursive, resolve, reject))
|
|
130
131
|
},
|
|
131
132
|
|
|
132
133
|
/** What is at `path`: a file or a directory entry (`path` equal to `name`), or null when nothing
|
|
133
134
|
* is there — `(await files.stat(p)) !== null` is the exists check. */
|
|
134
135
|
stat(path: string): Promise<FileEntry | null> {
|
|
135
136
|
return run<FileEntry | null>("stat", path, (resolve, reject) =>
|
|
136
|
-
|
|
137
|
+
_creatorFiles.stat(path, (e) => resolve(e ? { ...e, path: e.name } : null), reject))
|
|
137
138
|
},
|
|
138
139
|
}
|
package/src/runtime/input.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// - CONTINUOUS — mouse motion/wheel, sticks, triggers: polled every frame (`Input.mouse.delta`,
|
|
8
8
|
// `Input.gamepad(0).axis('leftX')`), never events.
|
|
9
9
|
// Bindings ("jump = Space | GamepadSouth") are app code: compare `e.code`, OR a few `Input.key`s.
|
|
10
|
-
// Hosts report physical state only (`
|
|
10
|
+
// Hosts report physical state only (`_creatorInput.key / read / registerEvent`).
|
|
11
11
|
|
|
12
|
-
/** Continuous channel ids — the `
|
|
12
|
+
/** Continuous channel ids — the `_creatorInput.read` contract (sdk/src/bridges/input.d.ts). */
|
|
13
13
|
export const InputChannel = {
|
|
14
14
|
MouseX: 0, MouseY: 1, MouseDX: 2, MouseDY: 3, WheelX: 4, WheelY: 5, PointerLocked: 6,
|
|
15
15
|
GamepadCount: 7, PointerOnUI: 8,
|
|
@@ -77,10 +77,12 @@ const deliver = (kind: number, code: string, gamepad: number, repeat: number) =>
|
|
|
77
77
|
const ensureHostListener = () => {
|
|
78
78
|
if (hostListenerInstalled) return
|
|
79
79
|
hostListenerInstalled = true
|
|
80
|
-
|
|
80
|
+
input()?.registerEvent?.(deliver)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// The host's input bridge, or undefined on a host without one (every read then answers "nothing").
|
|
84
|
+
const input = () => (typeof _creatorInput !== "undefined" ? _creatorInput : undefined)
|
|
85
|
+
const read = (channel: number): number => input()?.read?.(channel) ?? 0
|
|
84
86
|
|
|
85
87
|
/** Mouse state — polled. Position is the cursor inside the viewport in logical px; `delta` is the
|
|
86
88
|
* motion during the previous frame (raw where the OS offers it) and keeps counting while locked —
|
|
@@ -99,8 +101,8 @@ export const mouse = {
|
|
|
99
101
|
* `if (!Input.mouse.onUI) look(Input.mouse.delta)` never turns a HUD drag into camera look.
|
|
100
102
|
* Always false while `locked`. A press that fell through to the scene reads false. */
|
|
101
103
|
get onUI(): boolean { return read(InputChannel.PointerOnUI) !== 0 },
|
|
102
|
-
lock(): boolean { return
|
|
103
|
-
unlock(): void {
|
|
104
|
+
lock(): boolean { return input()?.setPointerLock?.(true) ?? false },
|
|
105
|
+
unlock(): void { input()?.setPointerLock?.(false) },
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
export type GamepadAxisName = "leftX" | "leftY" | "rightX" | "rightY" | "leftTrigger" | "rightTrigger"
|
|
@@ -138,11 +140,11 @@ const gamepadAt = (index: number): GamepadState => {
|
|
|
138
140
|
const v = read(base + PAD_AXIS[name])
|
|
139
141
|
return deadzone > 0 && Math.abs(v) < deadzone ? 0 : v
|
|
140
142
|
},
|
|
141
|
-
button(code) { return
|
|
143
|
+
button(code) { return input()?.key?.(code, index) ?? false },
|
|
142
144
|
rumble(strong, weak = strong, durationMs = 200) {
|
|
143
|
-
return
|
|
145
|
+
return input()?.rumble?.(index, strong, weak, durationMs) ?? false
|
|
144
146
|
},
|
|
145
|
-
stopRumble() {
|
|
147
|
+
stopRumble() { input()?.rumble?.(index, 0, 0, 0) },
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
return g
|
|
@@ -156,7 +158,7 @@ export const Input = {
|
|
|
156
158
|
* frame-independent movement. `gamepad` scopes a Gamepad* code to one pad; omitted = any pad.
|
|
157
159
|
*/
|
|
158
160
|
key(code: string, gamepad?: number): boolean {
|
|
159
|
-
return
|
|
161
|
+
return input()?.key?.(code, gamepad ?? -1) ?? false
|
|
160
162
|
},
|
|
161
163
|
|
|
162
164
|
/** Listen for a discrete input moment. `keydown`/`keyup` cover EVERY button — keyboard, mouse,
|