lecodes-cli 0.2.0 → 0.5.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 +3 -0
- package/dist/index.js +883 -507
- package/package.json +9 -4
- package/runtime/sdk/canvas/Canvas.ts +270 -0
- package/runtime/sdk/compile/aspectMacro.ts +42 -0
- package/runtime/sdk/compile/assetMacro.ts +44 -0
- package/runtime/sdk/compile/bundler.ts +173 -0
- package/runtime/sdk/compile/compileProject.ts +84 -0
- package/runtime/sdk/compile/header.ts +46 -0
- package/runtime/sdk/compile/index.ts +12 -0
- package/runtime/sdk/compile/sourcemap.ts +25 -0
- package/runtime/sdk/core/Aspect.ts +82 -26
- package/runtime/sdk/core/signals.ts +277 -0
- package/runtime/sdk/g2/CharacterController2D.ts +5 -4
- package/runtime/sdk/g2/Node2D.ts +1 -1
- package/runtime/sdk/g2/Physics2D.ts +22 -12
- package/runtime/sdk/g2/Scene2D.ts +30 -0
- package/runtime/sdk/g2/{Shape.ts → Shape2D.ts} +10 -10
- package/runtime/sdk/g2/Sprite.ts +16 -6
- package/runtime/sdk/g2/Texture2D.ts +12 -2
- package/runtime/sdk/g2/{Trigger.ts → Trigger2D.ts} +9 -9
- package/runtime/sdk/gl/CharacterController.ts +101 -0
- package/runtime/sdk/gl/Material.ts +10 -3
- package/runtime/sdk/gl/Node.ts +11 -2
- package/runtime/sdk/gl/Physics.ts +65 -41
- package/runtime/sdk/gl/Scene.ts +18 -0
- package/runtime/sdk/gl/Shape.ts +98 -0
- package/runtime/sdk/gl/Texture.ts +13 -2
- package/runtime/sdk/gl/Trigger.ts +46 -0
- package/runtime/sdk/gl/physicsEvents.ts +20 -0
- package/runtime/sdk/inject.ts +14 -3
- package/runtime/sdk/runtime/camera.ts +31 -0
- package/runtime/sdk/runtime/device.ts +26 -0
- package/runtime/sdk/runtime/share.ts +9 -0
- package/runtime/sdk/ui/UI.ts +5 -1
- package/runtime/sdk/ui/UIButton.ts +11 -5
- package/runtime/sdk/ui/UIContainer.ts +28 -13
- package/runtime/sdk/ui/UIImage.ts +31 -11
- package/runtime/sdk/ui/UIInput.ts +10 -0
- package/runtime/sdk/ui/UINode.ts +206 -8
- package/runtime/sdk/ui/UIScreen.ts +17 -7
- package/runtime/sdk/ui/UIScrollable.ts +10 -5
- package/runtime/sdk/ui/UISpacer.ts +4 -0
- package/runtime/sdk/ui/UIText.ts +21 -5
- package/runtime/sdk/ui/UIVideo.ts +5 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +5 -0
- package/runtime/sdk/ui/UIWidget.ts +10 -5
- package/runtime/sdk/gl/Collider.ts +0 -29
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// A collision shape, as an aspect on a 3D Node. PURE GEOMETRY: attaching a Shape does not by itself
|
|
2
|
+
// make the node rigid or a sensor — pair it with a Physics (rigid body) or a Trigger (sensor), both
|
|
3
|
+
// of which require a Shape. The Shape also makes the node pointer-pickable: on its own it creates a
|
|
4
|
+
// lightweight "pick-only" JoltPhysics body (collides with nothing, exists only for the raycast), so
|
|
5
|
+
// any node with a Shape can receive click/touchstart. Physics/Trigger REPLACE that body with the real
|
|
6
|
+
// one. Set `raycast = false` to opt a shape out of pointer picking.
|
|
7
|
+
//
|
|
8
|
+
// wall.aspect(Shape, { box: [1, 2, 1] }).aspect(Physics, { motion: 'static' })
|
|
9
|
+
// coin.aspect(Shape, { sphere: 0.5 }).aspect(Trigger)
|
|
10
|
+
// crate.aspect(Shape, {}).aspect(Physics) // auto box from the mesh AABB × world scale
|
|
11
|
+
//
|
|
12
|
+
// Explicit dims are world-space half-extents (not scaled by the node). Use {} ("auto") to track the
|
|
13
|
+
// mesh × world scale. Note: a Shape-only pick body is a STATIC snapshot at attach time — for a moving
|
|
14
|
+
// pickable object add a Physics body (which follows the sim), rather than relying on the pick body.
|
|
15
|
+
|
|
16
|
+
import { Aspect } from "../core/Aspect"
|
|
17
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
18
|
+
import type { Node } from "./Node"
|
|
19
|
+
|
|
20
|
+
export class Shape extends Aspect<"shape", Node> {
|
|
21
|
+
static readonly aspect = "shape"
|
|
22
|
+
|
|
23
|
+
/** Box half-extents [hx, hy, hz] (world units). */
|
|
24
|
+
box?: Vec3Like
|
|
25
|
+
/** Sphere radius. */
|
|
26
|
+
sphere?: number
|
|
27
|
+
/** Cylinder aligned on Y: half-height + radius. */
|
|
28
|
+
cylinder?: { halfHeight: number, radius: number }
|
|
29
|
+
/** Capsule aligned on Y: half-height of the cylinder section + radius. */
|
|
30
|
+
capsule?: { halfHeight: number, radius: number }
|
|
31
|
+
/** Whether pointer rays can hit this shape (click / touchstart). Default true. */
|
|
32
|
+
raycast = true
|
|
33
|
+
|
|
34
|
+
private _shapeId = 0
|
|
35
|
+
private _bodyId = 0
|
|
36
|
+
|
|
37
|
+
/** @internal native shape id (0 if the build has no physics). */
|
|
38
|
+
get shapeId(): number { return this._shapeId }
|
|
39
|
+
/** @internal current native body id (a pick-only body until Physics/Trigger claim it). */
|
|
40
|
+
get bodyId(): number { return this._bodyId }
|
|
41
|
+
|
|
42
|
+
onAttach(): void {
|
|
43
|
+
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
44
|
+
this._shapeId = this._build()
|
|
45
|
+
if (this._shapeId) this._createPickBody()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
onDetach(): void {
|
|
49
|
+
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
50
|
+
if (this._shapeId) { _creator.physicsDestroyShape(this._shapeId); this._shapeId = 0 }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @internal Physics/Trigger take over: drop the pick body and hand back the shape id. */
|
|
54
|
+
_claim(): number {
|
|
55
|
+
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
56
|
+
return this._shapeId
|
|
57
|
+
}
|
|
58
|
+
/** @internal Physics/Trigger report the body they created, so Shape owns its lifetime + pickability. */
|
|
59
|
+
_ownBody(bodyId: number): void {
|
|
60
|
+
this._bodyId = bodyId
|
|
61
|
+
if (!this.raycast && bodyId) _creator.physicsSetPickable(bodyId, false)
|
|
62
|
+
}
|
|
63
|
+
/** @internal restore a pick-only body when Physics/Trigger detach but the Shape remains. */
|
|
64
|
+
_recreatePickBody(): void {
|
|
65
|
+
if (!this._bodyId && this._shapeId) this._createPickBody()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private _createPickBody(): void {
|
|
69
|
+
this._bodyId = _creator.physicsCreateBody(this.node.id, this._shapeId, 0, 0, false, /*pickOnly*/ true)
|
|
70
|
+
if (!this.raycast && this._bodyId) _creator.physicsSetPickable(this._bodyId, false)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private _build(): number {
|
|
74
|
+
if (this.sphere !== undefined) return _creator.physicsBuildSphere(this.sphere)
|
|
75
|
+
if (this.cylinder) return _creator.physicsBuildCylinder(this.cylinder.halfHeight, this.cylinder.radius)
|
|
76
|
+
if (this.capsule) return _creator.physicsBuildCapsule(this.capsule.halfHeight, this.capsule.radius)
|
|
77
|
+
if (this.box) return _creator.physicsBuildBox(cx(this.box), cy(this.box), cz(this.box))
|
|
78
|
+
|
|
79
|
+
// auto: a box from the mesh AABB × world scale (so the physical box matches the rendered size).
|
|
80
|
+
const s = this.node.worldScale
|
|
81
|
+
let hx = 0.5, hy = 0.5, hz = 0.5
|
|
82
|
+
const geom = this.node.geometry
|
|
83
|
+
if (geom) {
|
|
84
|
+
const v = geom.vertices
|
|
85
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity, maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
|
|
86
|
+
for (let i = 0; i + 2 < v.length; i += 3) {
|
|
87
|
+
if (v[i] < minX) minX = v[i]
|
|
88
|
+
if (v[i] > maxX) maxX = v[i]
|
|
89
|
+
if (v[i + 1] < minY) minY = v[i + 1]
|
|
90
|
+
if (v[i + 1] > maxY) maxY = v[i + 1]
|
|
91
|
+
if (v[i + 2] < minZ) minZ = v[i + 2]
|
|
92
|
+
if (v[i + 2] > maxZ) maxZ = v[i + 2]
|
|
93
|
+
}
|
|
94
|
+
hx = (maxX - minX) * 0.5; hy = (maxY - minY) * 0.5; hz = (maxZ - minZ) * 0.5
|
|
95
|
+
}
|
|
96
|
+
return _creator.physicsBuildBox(hx * s.x, hy * s.y, hz * s.z)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A 3D GPU texture, decoded natively by creator-gl. Load from a URL or an already-fetched response.
|
|
2
2
|
|
|
3
|
-
import { fetch, type FetchResponse } from "../runtime/fetch"
|
|
3
|
+
import { fetch, type FetchResponse, type File } from "../runtime/fetch"
|
|
4
4
|
|
|
5
5
|
export class Texture {
|
|
6
6
|
/** @internal native texture handle. */
|
|
@@ -19,7 +19,18 @@ export class Texture {
|
|
|
19
19
|
this._id = id
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Wrap a Canvas's baked surface as a 3D texture (RGBA8, already rasterized → synchronous, no decode).
|
|
23
|
+
// The id is cached on the canvas (opaque _tex3dId), so backing several materials shares one texture and
|
|
24
|
+
// canvas.update() re-uploads it. Structural type avoids a Canvas import (keeps 3D free of 2D-canvas code).
|
|
25
|
+
static fromCanvas(canvas: { _bakeSurface(): number, width: number, height: number, pixelRatio: number, _tex3dId: number }): Texture {
|
|
26
|
+
if (!canvas._tex3dId) {
|
|
27
|
+
const surfaceId = canvas._bakeSurface()
|
|
28
|
+
canvas._tex3dId = _creator.createTextureFromCanvas(surfaceId)
|
|
29
|
+
}
|
|
30
|
+
return new Texture(Math.round(canvas.width * canvas.pixelRatio), Math.round(canvas.height * canvas.pixelRatio), canvas._tex3dId)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static load(source: string | FetchResponse | File): Promise<Texture> {
|
|
23
34
|
if (typeof source === "string") {
|
|
24
35
|
return fetch(source, { useOnce: true }).then((resp) => {
|
|
25
36
|
if (resp.status >= 400) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// A trigger zone, as an aspect on a 3D Node. Requires a Shape (its geometry); creates a static SENSOR
|
|
2
|
+
// body from it, so a physics body overlapping the zone fires the node's 'enter' / 'exit' events
|
|
3
|
+
// instead of colliding. A trigger does not block movement, and is still pointer-pickable.
|
|
4
|
+
//
|
|
5
|
+
// const goal = new Mesh(box(), material)
|
|
6
|
+
// .aspect(Shape, { box: [1, 2, 1] })
|
|
7
|
+
// .aspect(Trigger)
|
|
8
|
+
// goal.addEventListener('enter', other => win(other))
|
|
9
|
+
// goal.addEventListener('exit', other => …)
|
|
10
|
+
|
|
11
|
+
import { Aspect } from "../core/Aspect"
|
|
12
|
+
import { cx, cy, cz, type Vec3Like } from "../math/vec"
|
|
13
|
+
import { Shape } from "./Shape"
|
|
14
|
+
import { ensurePhysicsEvents } from "./physicsEvents"
|
|
15
|
+
import type { Node } from "./Node"
|
|
16
|
+
|
|
17
|
+
export class Trigger extends Aspect<"trigger", Node> {
|
|
18
|
+
static readonly aspect = "trigger"
|
|
19
|
+
|
|
20
|
+
private _bodyId = 0
|
|
21
|
+
/** Native body id (0 if no physics support). */
|
|
22
|
+
get id(): number { return this._bodyId }
|
|
23
|
+
|
|
24
|
+
onAttach(): void {
|
|
25
|
+
if (!_creator.physicsHasSupport || !_creator.physicsHasSupport()) return
|
|
26
|
+
const shape = this.node.get(Shape)
|
|
27
|
+
if (!shape) {
|
|
28
|
+
throw new Error("Trigger requires a Shape aspect — add it first: node.aspect(Shape, {…}).aspect(Trigger)")
|
|
29
|
+
}
|
|
30
|
+
const shapeId = shape._claim()
|
|
31
|
+
this._bodyId = _creator.physicsCreateBody(this.node.id, shapeId, 0 /* static */, 0, true /* sensor */, false)
|
|
32
|
+
shape._ownBody(this._bodyId)
|
|
33
|
+
ensurePhysicsEvents() // routes overlap events → the node's 'enter' / 'exit'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onDetach(): void {
|
|
37
|
+
if (this._bodyId) { _creator.physicsRemoveBody(this._bodyId); this._bodyId = 0 }
|
|
38
|
+
this.node.get(Shape)?._recreatePickBody()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Reposition the trigger zone to a world point. */
|
|
42
|
+
moveTo(p: Vec3Like): this {
|
|
43
|
+
if (this._bodyId) _creator.physicsSetBodyPosition(this._bodyId, cx(p), cy(p), cz(p))
|
|
44
|
+
return this
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Routes native physics contact/sensor events to the owning nodes' 'enter'/'exit' events. Each event
|
|
2
|
+
// is delivered to BOTH nodes with the other as the argument. type: 0 = enter (contact/overlap began),
|
|
3
|
+
// 1 = exit (ended). Registered lazily on the first trigger/physics body or 'enter'/'exit' listener.
|
|
4
|
+
|
|
5
|
+
import { nodeRegistry } from "./Node"
|
|
6
|
+
|
|
7
|
+
let registered = false
|
|
8
|
+
export const ensurePhysicsEvents = (): void => {
|
|
9
|
+
if (registered) return
|
|
10
|
+
if (!_creator.setOnPhysicsEvent) return // physics-less build / bridge without the hook
|
|
11
|
+
registered = true
|
|
12
|
+
_creator.setOnPhysicsEvent((a, b, type) => {
|
|
13
|
+
const na = nodeRegistry.get(a)
|
|
14
|
+
const nb = nodeRegistry.get(b)
|
|
15
|
+
if (!na || !nb) return
|
|
16
|
+
const channel = type === 1 ? "exit" : "enter"
|
|
17
|
+
na._emitCollision(channel, nb)
|
|
18
|
+
nb._emitCollision(channel, na)
|
|
19
|
+
})
|
|
20
|
+
}
|
package/runtime/sdk/inject.ts
CHANGED
|
@@ -17,6 +17,12 @@ export { Color } from "./core/color"
|
|
|
17
17
|
// ---- aspects (attachable node capabilities) ----
|
|
18
18
|
export { Aspect, type With } from "./core/Aspect"
|
|
19
19
|
|
|
20
|
+
// ---- reactivity (signals; UI text/style props also accept `() => value` bindings) ----
|
|
21
|
+
export { signal, computed, effect, type Signal, type Computed } from "./core/signals"
|
|
22
|
+
|
|
23
|
+
// ---- canvas (core 2D drawing that bakes to a texture; shared by 2D/3D/UI) ----
|
|
24
|
+
export { Canvas, Bitmap } from "./canvas/Canvas"
|
|
25
|
+
|
|
20
26
|
// ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
|
|
21
27
|
export { toast, SvgSource } from "./runtime/misc"
|
|
22
28
|
export { fetch, fetchLocal, FormData, File } from "./runtime/fetch"
|
|
@@ -25,7 +31,9 @@ export { device } from "./runtime/device"
|
|
|
25
31
|
export { Input } from "./runtime/input"
|
|
26
32
|
export { WebSocket } from "./runtime/net"
|
|
27
33
|
export { AudioPlayer, VideoPlayer } from "./runtime/media"
|
|
34
|
+
export { CameraViewer } from "./runtime/camera"
|
|
28
35
|
export { openFilePicker } from "./runtime/files"
|
|
36
|
+
export { share } from "./runtime/share"
|
|
29
37
|
export { ClickEvent, TouchStartEvent } from "./runtime/touch"
|
|
30
38
|
|
|
31
39
|
// ---- 2D engine (creator-2d) ----
|
|
@@ -34,9 +42,9 @@ export { Node2D } from "./g2/Node2D"
|
|
|
34
42
|
export { Camera2D } from "./g2/Camera2D"
|
|
35
43
|
export { Sprite } from "./g2/Sprite"
|
|
36
44
|
export { SpriteAnimation } from "./g2/SpriteAnimation"
|
|
37
|
-
export {
|
|
45
|
+
export { Shape2D } from "./g2/Shape2D"
|
|
38
46
|
export { Physics2D } from "./g2/Physics2D"
|
|
39
|
-
export {
|
|
47
|
+
export { Trigger2D } from "./g2/Trigger2D"
|
|
40
48
|
export { CharacterController2D } from "./g2/CharacterController2D"
|
|
41
49
|
export { Tilemap } from "./g2/Tilemap"
|
|
42
50
|
export { Texture2D } from "./g2/Texture2D"
|
|
@@ -56,6 +64,7 @@ export {
|
|
|
56
64
|
UIVirtualizedList,
|
|
57
65
|
Router,
|
|
58
66
|
registerFont,
|
|
67
|
+
__uiMap,
|
|
59
68
|
} from "./ui/UI"
|
|
60
69
|
|
|
61
70
|
// ---- animate ----
|
|
@@ -71,8 +80,10 @@ export { Scene, ARScene } from "./gl/Scene"
|
|
|
71
80
|
export { Node } from "./gl/Node"
|
|
72
81
|
export { Mesh } from "./gl/Mesh"
|
|
73
82
|
export { Model } from "./gl/Model"
|
|
83
|
+
export { Shape } from "./gl/Shape"
|
|
74
84
|
export { Physics } from "./gl/Physics"
|
|
75
|
-
export {
|
|
85
|
+
export { Trigger } from "./gl/Trigger"
|
|
86
|
+
export { CharacterController } from "./gl/CharacterController"
|
|
76
87
|
export { ModelAnimation } from "./gl/ModelAnimation"
|
|
77
88
|
export { Light } from "./gl/Light"
|
|
78
89
|
export { Material } from "./gl/Material"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Device camera → a still-photo File. Ported from worker/src/utils/CameraViewer.ts. Permission is
|
|
2
|
+
// requested through the engine bridge's `requestCamera` (same as QRScanner); the live preview and the
|
|
3
|
+
// capture go through `_creatorUtils.camera*`. `takePhoto()` returns a File (a host buffer handle), so
|
|
4
|
+
// the shot can be used as a texture (`Texture2D.load(file)`), uploaded via FormData, or shared.
|
|
5
|
+
|
|
6
|
+
import { File } from "./fetch"
|
|
7
|
+
|
|
8
|
+
export class CameraViewer {
|
|
9
|
+
/** Ask for camera permission and start the live preview (default: the back camera). */
|
|
10
|
+
static async open(facingMode: "front" | "back" = "back"): Promise<void> {
|
|
11
|
+
await new Promise<void>((res, rej) => _creator.requestCamera(res, rej))
|
|
12
|
+
await new Promise<void>((res, rej) => _creatorUtils.cameraOpen(facingMode, res, rej))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Switch between the front and back camera while the preview is open. */
|
|
16
|
+
static setFacingMode(facingMode: "front" | "back"): void {
|
|
17
|
+
_creatorUtils.cameraSetFacingMode(facingMode)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Capture a still frame as an image File (host-encoded). */
|
|
21
|
+
static takePhoto(): Promise<File> {
|
|
22
|
+
return new Promise<File>((res, rej) => {
|
|
23
|
+
_creatorUtils.cameraTakePhoto((bufferId, name, size) => res(new File(bufferId, name, size)), rej)
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Stop the preview and release the camera. */
|
|
28
|
+
static close(): void {
|
|
29
|
+
_creatorUtils.cameraClose()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -17,6 +17,32 @@ export const device = {
|
|
|
17
17
|
get language(): string {
|
|
18
18
|
return _creatorUtils.language
|
|
19
19
|
},
|
|
20
|
+
/** Display device-pixel ratio (physical px per logical/CSS px): 1 on standard displays, 2–3 on
|
|
21
|
+
* retina / iOS. Bake a Canvas at this ratio for crisp output without hardcoding, e.g.
|
|
22
|
+
* `new Canvas(w, h, { pixelRatio: device.pixelRatio })`. NOTE: the web 2D/GL surface currently
|
|
23
|
+
* renders at logical resolution, so on web this only helps UI canvases / is forward-looking for
|
|
24
|
+
* 2D; on iOS (physical surface) it makes 2D canvases crisp. Falls back to 1 if the host omits it. */
|
|
25
|
+
get pixelRatio(): number {
|
|
26
|
+
return _creatorUtils.pixelRatio || 1
|
|
27
|
+
},
|
|
28
|
+
/** Current display width in logical px — the same value the "resize" event delivers, but readable at
|
|
29
|
+
* any time (not only inside the listener). 0 until the host has reported a size. */
|
|
30
|
+
get width(): number {
|
|
31
|
+
return _creatorUI.getDisplaySize?.()?.[0] ?? 0
|
|
32
|
+
},
|
|
33
|
+
/** Current display height in logical px — the same value the "resize" event delivers, readable at any
|
|
34
|
+
* time. 0 until the host has reported a size. */
|
|
35
|
+
get height(): number {
|
|
36
|
+
return _creatorUI.getDisplaySize?.()?.[1] ?? 0
|
|
37
|
+
},
|
|
38
|
+
/** Opt into the precise-touch system where the platform supports it. OFF by default. When on, fast
|
|
39
|
+
* strokes are sampled at the touch digitizer's full rate (iOS coalesced touches, ≈120–240 Hz) instead
|
|
40
|
+
* of once per display frame (~60 Hz), so a pointer-heavy app (drawing, handwriting, dragging) gets more
|
|
41
|
+
* points and smoother lines. Leave it off for tap/button UIs. No-op on hosts without a coalesced-input
|
|
42
|
+
* concept (web already coalesces pointermove; headless). */
|
|
43
|
+
setPreciseTouch(enabled: boolean): void {
|
|
44
|
+
_creatorUtils.setPreciseTouch?.(enabled)
|
|
45
|
+
},
|
|
20
46
|
addEventListener(channel: "resize", callback: ResizeCallback): void {
|
|
21
47
|
if (channel !== "resize") return
|
|
22
48
|
resizeListeners.push(callback)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Share a media File (image/video) via the OS share sheet. Wraps `_creatorUtils.shareMedia`, which
|
|
2
|
+
// takes the host buffer id behind the File. On iOS the sheet also lets the user save to Files/Photos;
|
|
3
|
+
// on web it saves (downloads) the file. `text` is an optional caption/message.
|
|
4
|
+
|
|
5
|
+
import type { File, FetchResponse } from "./fetch"
|
|
6
|
+
|
|
7
|
+
export function share(media: File | FetchResponse, text?: string): void {
|
|
8
|
+
_creatorUtils.shareMedia((media as any).id ?? (media as any)._id, text)
|
|
9
|
+
}
|
package/runtime/sdk/ui/UI.ts
CHANGED
|
@@ -16,4 +16,8 @@ export { UIScrollable, type UIScrollableStyle } from './UIScrollable'
|
|
|
16
16
|
|
|
17
17
|
export { UIVirtualizedList } from './UIVirtualizedList'
|
|
18
18
|
|
|
19
|
-
export { Router } from './router'
|
|
19
|
+
export { Router } from './router'
|
|
20
|
+
|
|
21
|
+
// Internal: memoized map for reactive children — the chisel `reactiveUi` pass rewrites
|
|
22
|
+
// `list.map(fn)` inside children bindings to `__uiMap(list, fn, slot)` calls.
|
|
23
|
+
export { __uiMap } from './UINode'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ClickEvent, TouchStartEvent } from "../runtime/touch"
|
|
2
2
|
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType } from "../runtime/touch"
|
|
3
|
-
import { type ElementStyle, type ContainerStyle, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type OnLayoutCallback, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
|
|
3
|
+
import { type ElementStyle, type ContainerStyle, type Style, ContainerElement, type DrawableStyle, type UINodeChild, type ChildrenFn, type OnLayoutCallback, type BaseStyle, type AnimateStyle, type Color } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
6
6
|
|
|
@@ -18,9 +18,15 @@ export interface UIButton {
|
|
|
18
18
|
append(...nodes: UINodeChild[]): this
|
|
19
19
|
insert(index: number, ...nodes: UINodeChild[]): this
|
|
20
20
|
remove(...nodes: UINodeChild[]): this
|
|
21
|
-
setContent(nodes: UINodeChild[]): this
|
|
21
|
+
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
22
22
|
|
|
23
23
|
onLayout(onLayout: OnLayoutCallback): this
|
|
24
|
+
|
|
25
|
+
setClass(name: string, enabled: boolean): this
|
|
26
|
+
toggleClass(name: string): this
|
|
27
|
+
hasClass(name: string): boolean
|
|
28
|
+
bindClass(name: string, fn: () => boolean): this
|
|
29
|
+
|
|
24
30
|
readonly children: UINodeChild[],
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -66,14 +72,14 @@ class ButtonElement extends ContainerElement<"button"> {
|
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
export function UIButton(): UIButton;
|
|
69
|
-
export function UIButton(children: UINodeChild[]): UIButton;
|
|
75
|
+
export function UIButton(children: UINodeChild[] | ChildrenFn): UIButton;
|
|
70
76
|
export function UIButton(style: UIButtonStyle): UIButton;
|
|
71
77
|
export function UIButton(style: UIButtonStyle, children: UINodeChild[]): UIButton;
|
|
72
|
-
export function UIButton(...args: [] | [UINodeChild[] | UIButtonStyle] | [UIButtonStyle, children: UINodeChild[]]): UIButton {
|
|
78
|
+
export function UIButton(...args: [] | [UINodeChild[] | ChildrenFn | UIButtonStyle] | [UIButtonStyle, children: UINodeChild[] | ChildrenFn]): UIButton {
|
|
73
79
|
if (args.length === 0) {
|
|
74
80
|
return new ButtonElement("button", {}, [])
|
|
75
81
|
} else if (args.length === 1) {
|
|
76
|
-
if (Array.isArray(args[0])) {
|
|
82
|
+
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
77
83
|
return new ButtonElement("button", {}, args[0])
|
|
78
84
|
} else {
|
|
79
85
|
return new ButtonElement("button", args[0], [])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContainerElement, type AnimateStyle, type AppearStyle, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
|
|
1
|
+
import { ContainerElement, type AnimateStyle, type AppearStyle, type ChildrenFn, type ContainerStyle, type DisappearStyle, type DrawableStyle, type ElementStyle, type OnLayoutCallback, type Style, type StyleFn, type UINode, type UINodeChild } from "./UINode"
|
|
2
2
|
|
|
3
3
|
export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle
|
|
4
4
|
|
|
@@ -11,21 +11,26 @@ export interface UIColumn {
|
|
|
11
11
|
append(...nodes: UINodeChild[]): UIColumn
|
|
12
12
|
insert(index: number, ...nodes: UINodeChild[]): UIColumn
|
|
13
13
|
remove(...nodes: UINodeChild[]): UIColumn
|
|
14
|
-
setContent(nodes: UINodeChild[]): this
|
|
14
|
+
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
15
15
|
|
|
16
16
|
onLayout(onLayout: OnLayoutCallback): this
|
|
17
17
|
|
|
18
|
+
setClass(name: string, enabled: boolean): this
|
|
19
|
+
toggleClass(name: string): this
|
|
20
|
+
hasClass(name: string): boolean
|
|
21
|
+
bindClass(name: string, fn: () => boolean): this
|
|
22
|
+
|
|
18
23
|
readonly children: UINodeChild[]
|
|
19
24
|
}
|
|
20
25
|
export function UIColumn(): UIColumn
|
|
21
|
-
export function UIColumn(children: UINodeChild[]): UIColumn
|
|
26
|
+
export function UIColumn(children: UINodeChild[] | ChildrenFn): UIColumn
|
|
22
27
|
export function UIColumn(style: UIContainerStyle): UIColumn
|
|
23
28
|
export function UIColumn(style: UIContainerStyle, children: UINodeChild[]): UIColumn
|
|
24
|
-
export function UIColumn(...args: [] | [UINodeChild[] | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[]]): UIColumn {
|
|
29
|
+
export function UIColumn(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIColumn {
|
|
25
30
|
if (args.length === 0) {
|
|
26
31
|
return new ContainerElement("column", {}, [])
|
|
27
32
|
} else if (args.length === 1) {
|
|
28
|
-
if (Array.isArray(args[0])) {
|
|
33
|
+
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
29
34
|
return new ContainerElement("column", {}, args[0])
|
|
30
35
|
} else {
|
|
31
36
|
return new ContainerElement("column", args[0], [])
|
|
@@ -44,20 +49,25 @@ export interface UIRow {
|
|
|
44
49
|
append(...nodes: UINodeChild[]): UIRow
|
|
45
50
|
insert(index: number, ...nodes: UINodeChild[]): UIRow
|
|
46
51
|
remove(...nodes: UINodeChild[]): UIRow
|
|
47
|
-
setContent(nodes: UINodeChild[]): this
|
|
52
|
+
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
48
53
|
|
|
49
54
|
onLayout(onLayout: OnLayoutCallback): this
|
|
50
55
|
|
|
56
|
+
setClass(name: string, enabled: boolean): this
|
|
57
|
+
toggleClass(name: string): this
|
|
58
|
+
hasClass(name: string): boolean
|
|
59
|
+
bindClass(name: string, fn: () => boolean): this
|
|
60
|
+
|
|
51
61
|
readonly children: UINodeChild[],
|
|
52
62
|
}
|
|
53
|
-
export function UIRow(children: UINodeChild[]): UIRow
|
|
63
|
+
export function UIRow(children: UINodeChild[] | ChildrenFn): UIRow
|
|
54
64
|
export function UIRow(style: UIContainerStyle): UIRow
|
|
55
65
|
export function UIRow(style: UIContainerStyle, children: UINodeChild[]): UIRow
|
|
56
|
-
export function UIRow(...args: [] | [UINodeChild[] | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[]]): UIRow {
|
|
66
|
+
export function UIRow(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIRow {
|
|
57
67
|
if (args.length === 0) {
|
|
58
68
|
return new ContainerElement("row", {}, [])
|
|
59
69
|
} else if (args.length === 1) {
|
|
60
|
-
if (Array.isArray(args[0])) {
|
|
70
|
+
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
61
71
|
return new ContainerElement("row", {}, args[0])
|
|
62
72
|
} else {
|
|
63
73
|
return new ContainerElement("row", args[0], [])
|
|
@@ -76,20 +86,25 @@ export interface UIBox {
|
|
|
76
86
|
append(...nodes: UINodeChild[]): this
|
|
77
87
|
insert(index: number, ...nodes: UINodeChild[]): this
|
|
78
88
|
remove(...nodes: UINodeChild[]): this
|
|
79
|
-
setContent(nodes: UINodeChild[]): this
|
|
89
|
+
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
80
90
|
|
|
81
91
|
onLayout(onLayout: OnLayoutCallback): this
|
|
82
92
|
|
|
93
|
+
setClass(name: string, enabled: boolean): this
|
|
94
|
+
toggleClass(name: string): this
|
|
95
|
+
hasClass(name: string): boolean
|
|
96
|
+
bindClass(name: string, fn: () => boolean): this
|
|
97
|
+
|
|
83
98
|
readonly children: UINodeChild[],
|
|
84
99
|
}
|
|
85
|
-
export function UIBox(children: UINodeChild[]): UIBox
|
|
100
|
+
export function UIBox(children: UINodeChild[] | ChildrenFn): UIBox
|
|
86
101
|
export function UIBox(style: UIContainerStyle): UIBox
|
|
87
102
|
export function UIBox(style: UIContainerStyle, children: UINodeChild[]): UIBox
|
|
88
|
-
export function UIBox(...args: [] | [UINodeChild[] | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[]]): UIBox {
|
|
103
|
+
export function UIBox(...args: [] | [UINodeChild[] | ChildrenFn | UIContainerStyle] | [UIContainerStyle, children: UINodeChild[] | ChildrenFn]): UIBox {
|
|
89
104
|
if (args.length === 0) {
|
|
90
105
|
return new ContainerElement("box", {}, [])
|
|
91
106
|
} else if (args.length === 1) {
|
|
92
|
-
if (Array.isArray(args[0])) {
|
|
107
|
+
if (Array.isArray(args[0]) || typeof args[0] === "function") {
|
|
93
108
|
return new ContainerElement("box", {}, args[0])
|
|
94
109
|
} else {
|
|
95
110
|
return new ContainerElement("box", args[0], [])
|
|
@@ -1,13 +1,28 @@
|
|
|
1
1
|
import type { FetchResponse } from "../runtime/fetch"
|
|
2
2
|
import type { SvgSourceValue as SvgSource } from "../runtime/misc"
|
|
3
3
|
import type { File } from "../runtime/fetch"
|
|
4
|
+
import { Canvas, isCanvas } from "../canvas/Canvas"
|
|
4
5
|
import { Element, type AnimateStyle, type Color, type ElementStyle, type OnLayoutCallback, type Style } from "./UINode"
|
|
5
6
|
|
|
6
7
|
export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "cover" | "contain" | "fill", tintColor?: Color }
|
|
7
8
|
|
|
9
|
+
/** A baked Canvas surface as an image source. The UI backend resolves it to a UI texture — see
|
|
10
|
+
* docs/canvas-contract.md ("Pending"). */
|
|
11
|
+
export type CanvasImageSource = { canvasSurface: number }
|
|
12
|
+
|
|
13
|
+
/** Accepted image sources. A Canvas is baked to a surface marker on assign. */
|
|
14
|
+
export type ImageSource = string | FetchResponse | File | SvgSource | Canvas
|
|
15
|
+
|
|
16
|
+
const toSrc = (src: ImageSource): string | FetchResponse | File | SvgSource | CanvasImageSource =>
|
|
17
|
+
isCanvas(src) ? { canvasSurface: src._bakeSurface() } : src
|
|
18
|
+
|
|
19
|
+
/** The stored/resolved image source (a Canvas is resolved to a surface marker). */
|
|
20
|
+
export type ResolvedImageSource = string | FetchResponse | File | SvgSource | CanvasImageSource
|
|
21
|
+
|
|
8
22
|
export interface UIImage {
|
|
9
23
|
readonly type: "image",
|
|
10
|
-
src:
|
|
24
|
+
get src(): ResolvedImageSource
|
|
25
|
+
set src(value: ImageSource)
|
|
11
26
|
style: Style<this, UIImageStyle>
|
|
12
27
|
animateTo: AnimateStyle<this, UIImageStyle>
|
|
13
28
|
animateFrom: AnimateStyle<this, UIImageStyle>
|
|
@@ -20,26 +35,31 @@ export interface UIImage {
|
|
|
20
35
|
setSourceRect(x: number, y: number, w: number, h: number): this
|
|
21
36
|
|
|
22
37
|
onLayout(onLayout: OnLayoutCallback): this
|
|
38
|
+
|
|
39
|
+
setClass(name: string, enabled: boolean): this
|
|
40
|
+
toggleClass(name: string): this
|
|
41
|
+
hasClass(name: string): boolean
|
|
42
|
+
bindClass(name: string, fn: () => boolean): this
|
|
23
43
|
}
|
|
24
44
|
|
|
25
45
|
class ImageElement extends Element<"image"> {
|
|
26
|
-
private _src:
|
|
46
|
+
private _src: ResolvedImageSource
|
|
27
47
|
// Current source sub-rect; undefined until the first setSourceRect. Kept on the node (not
|
|
28
48
|
// just sent as a command) so a crop set while detached (_id == 0) is reapplied at mount —
|
|
29
49
|
// the viewer reads it when it creates the element.
|
|
30
50
|
private _sourceRect?: [number, number, number, number]
|
|
31
|
-
constructor(style: any, src:
|
|
51
|
+
constructor(style: any, src: ImageSource) {
|
|
32
52
|
super("image", style)
|
|
33
|
-
this._src = src
|
|
53
|
+
this._src = toSrc(src)
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
get src() {
|
|
56
|
+
get src(): ResolvedImageSource {
|
|
37
57
|
return this._src
|
|
38
58
|
}
|
|
39
|
-
set src(value) {
|
|
40
|
-
this._src = value
|
|
59
|
+
set src(value: ImageSource) {
|
|
60
|
+
this._src = toSrc(value)
|
|
41
61
|
if (this._id !== 0) {
|
|
42
|
-
_creatorUI.updateNode(this._id, "src",
|
|
62
|
+
_creatorUI.updateNode(this._id, "src", this._src)
|
|
43
63
|
}
|
|
44
64
|
}
|
|
45
65
|
|
|
@@ -57,9 +77,9 @@ class ImageElement extends Element<"image"> {
|
|
|
57
77
|
}
|
|
58
78
|
}
|
|
59
79
|
|
|
60
|
-
export function UIImage(src:
|
|
61
|
-
export function UIImage(style: UIImageStyle, src:
|
|
62
|
-
export function UIImage(...args: [
|
|
80
|
+
export function UIImage(src: ImageSource): UIImage
|
|
81
|
+
export function UIImage(style: UIImageStyle, src: ImageSource): UIImage
|
|
82
|
+
export function UIImage(...args: [ImageSource] | [UIImageStyle, ImageSource]): UIImage {
|
|
63
83
|
if (args.length === 1) {
|
|
64
84
|
return new ImageElement({}, args[0])
|
|
65
85
|
} else {
|
|
@@ -16,6 +16,11 @@ export interface UIInput {
|
|
|
16
16
|
onFocus(callback: () => void): UIInput
|
|
17
17
|
onBlur(callback: () => void): UIInput
|
|
18
18
|
onLayout(onLayout: OnLayoutCallback): this
|
|
19
|
+
|
|
20
|
+
setClass(name: string, enabled: boolean): this
|
|
21
|
+
toggleClass(name: string): this
|
|
22
|
+
hasClass(name: string): boolean
|
|
23
|
+
bindClass(name: string, fn: () => boolean): this
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export interface UITextArea {
|
|
@@ -33,6 +38,11 @@ export interface UITextArea {
|
|
|
33
38
|
onChange(callback: (value: string) => void): UITextArea
|
|
34
39
|
onFocus(callback: () => void): UITextArea
|
|
35
40
|
onBlur(callback: () => void): UITextArea
|
|
41
|
+
|
|
42
|
+
setClass(name: string, enabled: boolean): this
|
|
43
|
+
toggleClass(name: string): this
|
|
44
|
+
hasClass(name: string): boolean
|
|
45
|
+
bindClass(name: string, fn: () => boolean): this
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
class InputElement<T extends "input" | "textarea"> extends Element<T> {
|