lecodes-cli 0.6.4 → 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/README.md +1 -0
- package/dist/index.js +920 -636
- package/package.json +9 -4
- package/runtime/scene-harness.json +1 -0
- package/runtime/sdk/compile/assetMacro.ts +4 -3
- package/runtime/sdk/compile/bundler.ts +35 -3
- package/runtime/sdk/compile/compileProject.ts +14 -2
- package/runtime/sdk/compile/libraryImports.ts +47 -0
- package/runtime/sdk/compile/sceneEditor.ts +16 -4
- package/runtime/sdk/core/Aspect.ts +37 -0
- package/runtime/sdk/core/InspectorUI.ts +212 -0
- package/runtime/sdk/core/fields.ts +15 -3
- package/runtime/sdk/g2/Scene2D.ts +51 -5
- package/runtime/sdk/gl/Model.ts +9 -0
- package/runtime/sdk/gl/Node.ts +4 -1
- package/runtime/sdk/gl/Scene.ts +76 -11
- package/runtime/sdk/gl/scenarios.ts +349 -0
- package/runtime/sdk/inject.ts +28 -8
- package/runtime/sdk/kit/UITabs.ts +105 -0
- 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/scene/defineScene.ts +651 -17
- package/runtime/sdk/scene/editorPlugins.ts +86 -0
- package/runtime/sdk/ui/NativeView.ts +144 -0
- package/runtime/sdk/ui/UI.ts +6 -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 +86 -29
- package/runtime/sdk/ui/UIScreenHost.ts +213 -0
- 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
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
// No-code scenario aspects: ready-made behaviors a scene builder attaches in the editor without
|
|
2
|
+
// writing any code — move to a target, travel a waypoint path, spin, face something, start a GLB
|
|
3
|
+
// animation. They are ordinary aspects (work from hand-written code too); in the scene editor the
|
|
4
|
+
// movement ones double as `static editor` generators that draw their paths as editor-only lines
|
|
5
|
+
// (edges meshes under `this.generated` — play mode never calls rebuild(), so the lines cost
|
|
6
|
+
// nothing at runtime).
|
|
7
|
+
//
|
|
8
|
+
// Conventions shared by the family:
|
|
9
|
+
// • targets/waypoints are scene nodes — place an Empty, then pick it in the inspector (`ref()`);
|
|
10
|
+
// • times are seconds, angles degrees; all motion runs in `update(dt)` (play mode only);
|
|
11
|
+
// • orientation is quaternion-composed — never euler-incremented (see math/quat.ts).
|
|
12
|
+
|
|
13
|
+
import { Aspect } from "../core/Aspect"
|
|
14
|
+
import type { FieldMeta } from "../core/fields"
|
|
15
|
+
import type { InspectorUI } from "../core/InspectorUI"
|
|
16
|
+
import { Vec3 } from "../math/vec"
|
|
17
|
+
import { Quat } from "../math/quat"
|
|
18
|
+
import { Node } from "./Node"
|
|
19
|
+
import { Mesh } from "./Mesh"
|
|
20
|
+
import { Model } from "./Model"
|
|
21
|
+
import { Geometry } from "./Geometry"
|
|
22
|
+
import { Material } from "./Material"
|
|
23
|
+
|
|
24
|
+
const MOVE_LINE = "#5b8ef0"
|
|
25
|
+
const LOOK_LINE = "#d9a13f"
|
|
26
|
+
const DEG2RAD = Math.PI / 180
|
|
27
|
+
|
|
28
|
+
type LoopMode = "once" | "loop" | "pingpong"
|
|
29
|
+
|
|
30
|
+
/** Normalized phase for elapsed time `t` over one trip of `duration`: once clamps at 1, loop
|
|
31
|
+
* wraps, pingpong triangles 0→1→0. */
|
|
32
|
+
const phase = (t: number, duration: number, mode: LoopMode): number => {
|
|
33
|
+
if (duration <= 0) return 1
|
|
34
|
+
const u = t / duration
|
|
35
|
+
if (mode === "once") return Math.min(u, 1)
|
|
36
|
+
if (mode === "loop") return u - Math.floor(u)
|
|
37
|
+
const v = u % 2
|
|
38
|
+
return v <= 1 ? v : 2 - v
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ease = (k: number, easing: "smooth" | "linear"): number =>
|
|
42
|
+
easing === "smooth" ? k * k * (3 - 2 * k) : k
|
|
43
|
+
|
|
44
|
+
/** A world point in `node`'s PARENT space — what `node.position` is expressed in. */
|
|
45
|
+
const toParentLocal = (node: Node, world: Vec3): Vec3 => {
|
|
46
|
+
const parent = node.parent
|
|
47
|
+
return parent ? parent.worldMatrix.invert().transformPoint(world) : world
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ---- editor line drawing (edit mode only — see rebuild() on the aspects below) -------------------
|
|
51
|
+
|
|
52
|
+
/** @internal An "edges" line mesh from flat segment endpoints ([x,y,z, x,y,z] per segment) —
|
|
53
|
+
* editor-only visuals (path lines here, node markers in defineScene). */
|
|
54
|
+
export const edgesMesh = (segments: number[], color: string): Mesh => {
|
|
55
|
+
const vertices = new Float32Array(segments)
|
|
56
|
+
const count = vertices.length / 3
|
|
57
|
+
const normals = new Float32Array(count * 3)
|
|
58
|
+
for (let i = 0; i < count; i++) normals[i * 3 + 1] = 1
|
|
59
|
+
const indices = new Uint16Array(count)
|
|
60
|
+
for (let i = 0; i < count; i++) indices[i] = i
|
|
61
|
+
const geo = new Geometry(vertices, normals, indices, new Float32Array(count * 2))
|
|
62
|
+
geo.kind = "edges"
|
|
63
|
+
const mesh = Mesh.from(geo, { material: Material.unlit({ color }), name: "__editor_line" })
|
|
64
|
+
mesh.castShadows = false
|
|
65
|
+
mesh.receiveShadows = false
|
|
66
|
+
return mesh
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pushCross = (segments: number[], p: Vec3, s: number): void => {
|
|
70
|
+
segments.push(p.x - s, p.y, p.z, p.x + s, p.y, p.z)
|
|
71
|
+
segments.push(p.x, p.y - s, p.z, p.x, p.y + s, p.z)
|
|
72
|
+
segments.push(p.x, p.y, p.z - s, p.x, p.y, p.z + s)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `this.generated` hangs UNDER the host node, so line points (world space) must be expressed in
|
|
76
|
+
* the host's local frame; the host is an editor-run dep of its own aspects, so dragging it
|
|
77
|
+
* re-runs rebuild() and the conversion stays fresh. */
|
|
78
|
+
const worldSegmentsToLocal = (node: Node, segments: number[]): number[] => {
|
|
79
|
+
const inv = node.worldMatrix.invert()
|
|
80
|
+
const out: number[] = []
|
|
81
|
+
for (let i = 0; i < segments.length; i += 3) {
|
|
82
|
+
const p = inv.transformPoint([ segments[i]!, segments[i + 1]!, segments[i + 2]! ])
|
|
83
|
+
out.push(p.x, p.y, p.z)
|
|
84
|
+
}
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** A path node's waypoints: its children in scene-FILE order (`_sceneChildOrder`, stamped by
|
|
89
|
+
* defineScene — the engine's live child order is insertion-based and may differ), editor helper
|
|
90
|
+
* nodes (`__`-prefixed) excluded. Runtime-added children append in engine order. */
|
|
91
|
+
const pathWaypoints = (path: Node): Node[] => {
|
|
92
|
+
const children = path.children.filter((c) => !(c.name ?? "").startsWith("__"))
|
|
93
|
+
const order = (path as { _sceneChildOrder?: string[] })._sceneChildOrder
|
|
94
|
+
if (!order) return children
|
|
95
|
+
const byName = new Map(children.map((c) => [ c.name, c ]))
|
|
96
|
+
const sorted: Node[] = []
|
|
97
|
+
for (const n of order) { const c = byName.get(n); if (c) sorted.push(c) }
|
|
98
|
+
const seen = new Set(sorted)
|
|
99
|
+
for (const c of children) if (!seen.has(c)) sorted.push(c)
|
|
100
|
+
return sorted
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- the aspects ---------------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
/** Travel from the node's starting position to a target node in `duration` seconds. */
|
|
106
|
+
export class MoveTo extends Aspect<"moveTo"> {
|
|
107
|
+
/** Where to travel — place an Empty node and pick it. */
|
|
108
|
+
target: Node | null = null
|
|
109
|
+
|
|
110
|
+
/** Seconds for the full trip. */
|
|
111
|
+
duration = 2
|
|
112
|
+
|
|
113
|
+
/** Seconds to wait before starting. */
|
|
114
|
+
delay = 0
|
|
115
|
+
|
|
116
|
+
/** once = stop at the target · loop = restart from the start point · pingpong = back and forth. */
|
|
117
|
+
mode: LoopMode = "once"
|
|
118
|
+
|
|
119
|
+
/** Motion curve. */
|
|
120
|
+
easing: "smooth" | "linear" = "smooth"
|
|
121
|
+
|
|
122
|
+
static readonly aspect = "moveTo"
|
|
123
|
+
static editor = { rebuild: true }
|
|
124
|
+
static fields: FieldMeta<MoveTo> = {
|
|
125
|
+
target: { editor: "node" },
|
|
126
|
+
duration: { min: 0.05, max: 120, step: 0.05 },
|
|
127
|
+
delay: { min: 0, max: 120, step: 0.05 },
|
|
128
|
+
mode: { options: [ "once", "loop", "pingpong" ] },
|
|
129
|
+
easing: { options: [ "smooth", "linear" ] },
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private _t = 0
|
|
133
|
+
private _start: Vec3 | null = null
|
|
134
|
+
|
|
135
|
+
update(dt: number): void {
|
|
136
|
+
if (!this.target) return
|
|
137
|
+
this._start ??= this.node.position
|
|
138
|
+
this._t += dt
|
|
139
|
+
const k = ease(phase(Math.max(0, this._t - this.delay), this.duration, this.mode), this.easing)
|
|
140
|
+
const end = toParentLocal(this.node, this.target.worldPosition)
|
|
141
|
+
this.node.position = new Vec3(this._start).add(end.sub(this._start).scale(k))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Editor-only path line (edit mode; play mode never calls this). */
|
|
145
|
+
rebuild(): void {
|
|
146
|
+
this.generated.clear()
|
|
147
|
+
if (!this.target) return
|
|
148
|
+
const segments: number[] = []
|
|
149
|
+
const a = this.node.worldPosition
|
|
150
|
+
const b = this.target.worldPosition
|
|
151
|
+
segments.push(a.x, a.y, a.z, b.x, b.y, b.z)
|
|
152
|
+
pushCross(segments, b, 0.09)
|
|
153
|
+
this.generated.add(edgesMesh(worldSegmentsToLocal(this.node, segments), MOVE_LINE))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Travel through the children of a path node (add an Empty per waypoint) in `duration` seconds.
|
|
158
|
+
* `loop` runs the circuit closed (last → first); `pingpong` goes back and forth along the open
|
|
159
|
+
* path; `once` stops at the last waypoint. */
|
|
160
|
+
export class FollowPath extends Aspect<"followPath"> {
|
|
161
|
+
/** A node whose CHILDREN are the waypoints, in file order. */
|
|
162
|
+
path: Node | null = null
|
|
163
|
+
|
|
164
|
+
/** Seconds for one full pass of the path. */
|
|
165
|
+
duration = 6
|
|
166
|
+
|
|
167
|
+
/** Seconds to wait before starting. */
|
|
168
|
+
delay = 0
|
|
169
|
+
|
|
170
|
+
mode: LoopMode = "loop"
|
|
171
|
+
|
|
172
|
+
/** Turn to face the direction of travel. */
|
|
173
|
+
orient = true
|
|
174
|
+
|
|
175
|
+
/** Which local axis leads when orienting. */
|
|
176
|
+
forward: "-z" | "z" | "x" | "-x" = "-z"
|
|
177
|
+
|
|
178
|
+
static readonly aspect = "followPath"
|
|
179
|
+
static editor = { rebuild: true }
|
|
180
|
+
static fields: FieldMeta<FollowPath> = {
|
|
181
|
+
path: { editor: "node" },
|
|
182
|
+
duration: { min: 0.05, max: 300, step: 0.05 },
|
|
183
|
+
delay: { min: 0, max: 120, step: 0.05 },
|
|
184
|
+
mode: { options: [ "loop", "once", "pingpong" ] },
|
|
185
|
+
forward: { options: [ "-z", "z", "x", "-x" ] },
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private _t = 0
|
|
189
|
+
|
|
190
|
+
private _points(): Vec3[] {
|
|
191
|
+
if (!this.path) return []
|
|
192
|
+
const points = pathWaypoints(this.path).map((c) => c.worldPosition)
|
|
193
|
+
if (this.mode === "loop" && points.length > 1) points.push(points[0]!.clone())
|
|
194
|
+
return points
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
update(dt: number): void {
|
|
198
|
+
const points = this._points()
|
|
199
|
+
if (points.length < 2) return
|
|
200
|
+
this._t += dt
|
|
201
|
+
const k = phase(Math.max(0, this._t - this.delay), this.duration, this.mode === "loop" ? "loop" : this.mode)
|
|
202
|
+
|
|
203
|
+
const lengths: number[] = []
|
|
204
|
+
let total = 0
|
|
205
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
206
|
+
const l = points[i + 1]!.sub(points[i]!).length()
|
|
207
|
+
lengths.push(l)
|
|
208
|
+
total += l
|
|
209
|
+
}
|
|
210
|
+
if (total <= 0) return
|
|
211
|
+
|
|
212
|
+
let dist = k * total
|
|
213
|
+
let i = 0
|
|
214
|
+
while (i < lengths.length - 1 && dist > lengths[i]!) { dist -= lengths[i]!; i++ }
|
|
215
|
+
const a = points[i]!
|
|
216
|
+
const b = points[i + 1]!
|
|
217
|
+
const seg = b.sub(a)
|
|
218
|
+
const u = lengths[i]! > 0 ? Math.min(dist / lengths[i]!, 1) : 0
|
|
219
|
+
const world = a.add(seg.scale(u))
|
|
220
|
+
this.node.position = toParentLocal(this.node, world)
|
|
221
|
+
if (this.orient && seg.lengthSq() > 1e-12) this.node.lookAt(world.add(seg), this.forward)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Editor-only waypoint polyline (edit mode; play mode never calls this). */
|
|
225
|
+
rebuild(): void {
|
|
226
|
+
this.generated.clear()
|
|
227
|
+
const points = this._points()
|
|
228
|
+
if (points.length < 2) return
|
|
229
|
+
const segments: number[] = []
|
|
230
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
231
|
+
const a = points[i]!, b = points[i + 1]!
|
|
232
|
+
segments.push(a.x, a.y, a.z, b.x, b.y, b.z)
|
|
233
|
+
}
|
|
234
|
+
for (const p of points) pushCross(segments, p, 0.07)
|
|
235
|
+
this.generated.add(edgesMesh(worldSegmentsToLocal(this.node, segments), MOVE_LINE))
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Continuous rotation about one of the node's local axes, degrees per second. */
|
|
240
|
+
export class Spin extends Aspect<"spin"> {
|
|
241
|
+
/** Degrees per second. */
|
|
242
|
+
speed = 45
|
|
243
|
+
|
|
244
|
+
/** Local axis to spin around. */
|
|
245
|
+
axis: "x" | "y" | "z" = "y"
|
|
246
|
+
|
|
247
|
+
static readonly aspect = "spin"
|
|
248
|
+
static fields: FieldMeta<Spin> = {
|
|
249
|
+
speed: { min: -720, max: 720, step: 5 },
|
|
250
|
+
axis: { options: [ "x", "y", "z" ] },
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
update(dt: number): void {
|
|
254
|
+
const axis: [number, number, number] = this.axis === "x" ? [ 1, 0, 0 ] : this.axis === "y" ? [ 0, 1, 0 ] : [ 0, 0, 1 ]
|
|
255
|
+
// compose in quaternion space about the LOCAL axis (q ⊗ dq) — never euler-increment
|
|
256
|
+
this.node.quaternion = this.node.quaternion.mul(Quat.fromAxisAngle(axis, this.speed * dt * DEG2RAD))
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Keep the node facing a target node (a turret tracking, a signpost, a camera aimed at a hero).
|
|
261
|
+
* Accessor `node.facing` — NOT `lookAt`, which would shadow the `Node.lookAt()` method. */
|
|
262
|
+
export class LookAt extends Aspect<"facing"> {
|
|
263
|
+
/** The node to face. */
|
|
264
|
+
target: Node | null = null
|
|
265
|
+
|
|
266
|
+
/** Which local axis points at the target. */
|
|
267
|
+
forward: "-z" | "z" | "x" | "-x" = "-z"
|
|
268
|
+
|
|
269
|
+
/** 0 = snap instantly; otherwise seconds of turn lag (bigger = slower, smoother). */
|
|
270
|
+
smoothing = 0
|
|
271
|
+
|
|
272
|
+
static readonly aspect = "facing"
|
|
273
|
+
static editor = { rebuild: true }
|
|
274
|
+
static fields: FieldMeta<LookAt> = {
|
|
275
|
+
target: { editor: "node" },
|
|
276
|
+
forward: { options: [ "-z", "z", "x", "-x" ] },
|
|
277
|
+
smoothing: { min: 0, max: 5, step: 0.05 },
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
update(dt: number): void {
|
|
281
|
+
if (!this.target) return
|
|
282
|
+
const point = this.target.worldPosition
|
|
283
|
+
if (this.smoothing <= 0) { this.node.lookAt(point, this.forward); return }
|
|
284
|
+
const q0 = this.node.quaternion
|
|
285
|
+
this.node.lookAt(point, this.forward)
|
|
286
|
+
const q1 = this.node.quaternion
|
|
287
|
+
this.node.quaternion = q0.slerp(q1, 1 - Math.exp(-dt / this.smoothing))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Editor-only sight line (edit mode; play mode never calls this). */
|
|
291
|
+
rebuild(): void {
|
|
292
|
+
this.generated.clear()
|
|
293
|
+
if (!this.target) return
|
|
294
|
+
const segments: number[] = []
|
|
295
|
+
const a = this.node.worldPosition
|
|
296
|
+
const b = this.target.worldPosition
|
|
297
|
+
segments.push(a.x, a.y, a.z, b.x, b.y, b.z)
|
|
298
|
+
pushCross(segments, b, 0.07)
|
|
299
|
+
this.generated.add(edgesMesh(worldSegmentsToLocal(this.node, segments), LOOK_LINE))
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Start a GLB animation clip when the scene runs. Attach to a `model:` node; the inspector card
|
|
304
|
+
* lists the model's clips live and can preview them while editing. */
|
|
305
|
+
export class PlayAnimation extends Aspect<"playAnimation", Model> {
|
|
306
|
+
/** Clip name; empty = the model's first clip. */
|
|
307
|
+
clip = ""
|
|
308
|
+
|
|
309
|
+
loop = true
|
|
310
|
+
|
|
311
|
+
/** Playback rate (1 = authored speed). */
|
|
312
|
+
speed = 1
|
|
313
|
+
|
|
314
|
+
static readonly aspect = "playAnimation"
|
|
315
|
+
static fields: FieldMeta<PlayAnimation> = {
|
|
316
|
+
speed: { min: 0.05, max: 5, step: 0.05 },
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
onAttach(): void {
|
|
320
|
+
const anim = (this.node as Model | undefined)?.anim
|
|
321
|
+
if (!anim) return
|
|
322
|
+
anim.speed = this.speed
|
|
323
|
+
anim.play(this.clip === "" ? 0 : this.clip, { loop: this.loop })
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
static inspector(ui: InspectorUI, a: PlayAnimation): void {
|
|
327
|
+
const anim = (a.node as Model | undefined)?.anim
|
|
328
|
+
const clips = anim?.clips ?? []
|
|
329
|
+
if (clips.length === 0) {
|
|
330
|
+
ui.warn("No animation clips — attach this to a GLB model node")
|
|
331
|
+
return
|
|
332
|
+
}
|
|
333
|
+
ui.select("clip", clips.map((c) => c.name))
|
|
334
|
+
ui.auto("loop", "speed")
|
|
335
|
+
if (anim) {
|
|
336
|
+
if (ui.button(anim.playing ? "Restart preview" : "Preview")) {
|
|
337
|
+
anim.speed = a.speed
|
|
338
|
+
anim.play(a.clip === "" ? 0 : a.clip, { loop: a.loop })
|
|
339
|
+
}
|
|
340
|
+
if (anim.playing && ui.button("Stop")) {
|
|
341
|
+
anim.stop()
|
|
342
|
+
anim.time = 0
|
|
343
|
+
}
|
|
344
|
+
const clip = a.clip === "" ? clips[0]?.name : a.clip
|
|
345
|
+
const duration = clips.find((c) => c.name === clip)?.duration
|
|
346
|
+
ui.info(anim.playing ? "previewing…" : duration !== undefined ? `${duration.toFixed(2)}s` : "")
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
package/runtime/sdk/inject.ts
CHANGED
|
@@ -16,6 +16,7 @@ export { Color } from "./core/color"
|
|
|
16
16
|
|
|
17
17
|
// ---- aspects (attachable node capabilities) ----
|
|
18
18
|
export { Aspect, type With } from "./core/Aspect"
|
|
19
|
+
export { InspectorUI } from "./core/InspectorUI"
|
|
19
20
|
|
|
20
21
|
// ---- reactivity (signals; UI text/style props also accept `() => value` bindings) ----
|
|
21
22
|
export { signal, computed, effect, type Signal, type Computed } from "./core/signals"
|
|
@@ -24,18 +25,19 @@ export { signal, computed, effect, type Signal, type Computed } from "./core/sig
|
|
|
24
25
|
export { Canvas, Bitmap } from "./canvas/Canvas"
|
|
25
26
|
|
|
26
27
|
// ---- platform runtime (injected globals; the host provides the lower-level _creator* bridges) ----
|
|
27
|
-
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"
|
|
28
31
|
export { date, type DateValue, type DateInput, type Unit, type Locale } from "./runtime/datetime"
|
|
29
32
|
export { fetch, fetchLocal, FormData, File } from "./runtime/fetch"
|
|
30
33
|
export { localStorage } from "./runtime/storage"
|
|
31
|
-
export { device } from "./runtime/device"
|
|
34
|
+
export { device, type HapticStyle, type MotionOptions } from "./runtime/device"
|
|
32
35
|
export { Input } from "./runtime/input"
|
|
33
36
|
export { WebSocket } from "./runtime/net"
|
|
34
37
|
export { AudioPlayer, VideoPlayer } from "./runtime/media"
|
|
35
|
-
export { CameraViewer } from "./runtime/camera"
|
|
36
38
|
export { openFilePicker } from "./runtime/files"
|
|
37
39
|
export { share } from "./runtime/share"
|
|
38
|
-
export { ClickEvent, TouchStartEvent } from "./runtime/touch"
|
|
40
|
+
export { ClickEvent, TouchStartEvent, LongPressEvent } from "./runtime/touch"
|
|
39
41
|
|
|
40
42
|
// ---- 2D engine (creator-2d) ----
|
|
41
43
|
export { Scene2D } from "./g2/Scene2D"
|
|
@@ -52,7 +54,7 @@ export { Texture2D } from "./g2/Texture2D"
|
|
|
52
54
|
|
|
53
55
|
// ---- UI (creator-ui) ----
|
|
54
56
|
export {
|
|
55
|
-
UIScreen, type UIScreenStyle,
|
|
57
|
+
UIScreen, type UIScreenStyle, type UIScrollableScreen,
|
|
56
58
|
UIRow, UIColumn, UIBox, type UIContainerStyle,
|
|
57
59
|
UIText, type UITextStyle,
|
|
58
60
|
UIButton, type UIButtonStyle,
|
|
@@ -60,30 +62,48 @@ export {
|
|
|
60
62
|
UIVideo, type UIVideoStyle,
|
|
61
63
|
UIInput, UITextArea, type UIInputStyle,
|
|
62
64
|
UIScrollable, type UIScrollableStyle,
|
|
65
|
+
UIScreenHost, type UIScreenHostStyle,
|
|
63
66
|
UIWidget, type UIWidgetStyle,
|
|
64
67
|
UISpacer, type UISpacerStyle,
|
|
65
68
|
UIVirtualizedList,
|
|
66
69
|
Router,
|
|
70
|
+
NativeView, type UINativeViewStyle,
|
|
67
71
|
registerFont,
|
|
68
72
|
__uiMap,
|
|
69
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"
|
|
70
78
|
// The base node type + the union a container accepts as children — documented as the signature of
|
|
71
79
|
// UIColumn/UIRow/append/etc., so they must be global (type-only; erased at runtime).
|
|
72
80
|
export type { UINode, UINodeChild } from "./ui/UINode"
|
|
73
81
|
|
|
82
|
+
// ---- UI kit (pure-TS components built on the core UI globals; see packages/sdk/src/kit/) ----
|
|
83
|
+
export { UITabs, type UITabItem } from "./kit/UITabs"
|
|
84
|
+
|
|
74
85
|
// ---- animate ----
|
|
75
86
|
export { animate, animateMat4, stopAnimation, pauseAnimation, resumeAnimation } from "./animate/animate"
|
|
76
87
|
export { cubicBezier } from "./animate/bezier"
|
|
77
88
|
export { easeIn, easeOut, easeInOut } from "./animate/easings"
|
|
78
89
|
|
|
79
|
-
// ---- plugins (optional, host-provided capabilities — see src/plugins/) ----
|
|
90
|
+
// ---- plugins (optional, host-provided capabilities — typed NativeView/service wrappers, see src/plugins/) ----
|
|
80
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"
|
|
81
94
|
|
|
82
95
|
// ---- scenes as data (.scene.ts files — see src/scene/defineScene.ts) ----
|
|
83
|
-
export { defineScene, use, SceneHandle } from "./scene/defineScene"
|
|
84
|
-
export type { SceneDef, SceneNodeDef, MeshDef, LightDef, MaterialDef, AspectEntry, LoadedScene } from "./scene/defineScene"
|
|
96
|
+
export { defineScene, use, ref, make, SceneHandle } from "./scene/defineScene"
|
|
97
|
+
export type { SceneDef, SceneNodeDef, MeshDef, LightDef, MaterialDef, CameraNodeDef, AspectEntry, MakeEntry, LoadedScene } from "./scene/defineScene"
|
|
85
98
|
export type { FieldMeta } from "./core/fields"
|
|
86
99
|
|
|
100
|
+
// ---- no-code scenario aspects (attachable in the scene editor — see src/gl/scenarios.ts) ----
|
|
101
|
+
export { MoveTo, FollowPath, Spin, LookAt, PlayAnimation } from "./gl/scenarios"
|
|
102
|
+
|
|
103
|
+
// ---- editor plugins (called from `*.editor.ts` files — editor bundles only; DCE'd from apps) ----
|
|
104
|
+
export { registerEditorWindow, registerEditorTool, __editorPlugins } from "./scene/editorPlugins"
|
|
105
|
+
export type { EditorApi, EditorRayHit, EditorToolHooks, EditorWindowFn } from "./scene/editorPlugins"
|
|
106
|
+
|
|
87
107
|
// ---- 3D engine (creator-gl) ----
|
|
88
108
|
export { Scene, ARScene, VRScene } from "./gl/Scene"
|
|
89
109
|
export { Node } from "./gl/Node"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// UITabs — a self-contained tab component: a swipe-disabled UIScreenHost of tab screens + a bar of
|
|
2
|
+
// tap-to-switch buttons. Pure TS over UIScreenHost, no native work. It's just an element — you place
|
|
3
|
+
// it inside your own UIScreen and own the routing; UITabs only switches the visible tab.
|
|
4
|
+
//
|
|
5
|
+
// Router.push(UIScreen([
|
|
6
|
+
// UITabs([
|
|
7
|
+
// { icon: homeSvg, label: "Home", content: HomeScreen() },
|
|
8
|
+
// { icon: userSvg, label: "Profile", content: ProfileScreen() },
|
|
9
|
+
// ]),
|
|
10
|
+
// ]))
|
|
11
|
+
//
|
|
12
|
+
// A tab's content is a full UIScreen (screens are the only page kind UIScreenHost accepts): it gets
|
|
13
|
+
// onOpen/onClose from the host's settle lifecycle, plus makeScrollable/onRefresh. All tabs stay
|
|
14
|
+
// mounted, so switching is instant and per-tab state survives.
|
|
15
|
+
|
|
16
|
+
import { UIScreenHost, type HostedScreen } from "../ui/UIScreenHost"
|
|
17
|
+
import { UIRow, UIColumn, type UIColumn as UIColumnType } from "../ui/UIContainer"
|
|
18
|
+
import { UIButton } from "../ui/UIButton"
|
|
19
|
+
import { UIText } from "../ui/UIText"
|
|
20
|
+
import { UIImage, type ImageSource } from "../ui/UIImage"
|
|
21
|
+
import type { UINodeChild } from "../ui/UINode"
|
|
22
|
+
|
|
23
|
+
export interface UITabItem {
|
|
24
|
+
/** Icon image source (SvgSource / url / File / Canvas). Optional. */
|
|
25
|
+
icon?: ImageSource
|
|
26
|
+
/** Label under the icon. Optional. */
|
|
27
|
+
label?: string
|
|
28
|
+
/** Tab content — a full UIScreen (wrap a plain element: `UIScreen([el])`). */
|
|
29
|
+
content: HostedScreen
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The tabs element (a UIColumn) plus a small control surface. Embed it like any element. */
|
|
33
|
+
export interface UITabs extends UIColumnType {
|
|
34
|
+
/** Switch the visible tab. */
|
|
35
|
+
select(index: number): this
|
|
36
|
+
onChange(callback: (index: number) => void): this
|
|
37
|
+
/** Active tab index. */
|
|
38
|
+
readonly index: number
|
|
39
|
+
/** The bar row — style it or its item buttons directly. */
|
|
40
|
+
readonly bar: UIRow
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const clamp = (v: number, min: number, max: number) => Math.max(min, Math.min(max, v))
|
|
44
|
+
|
|
45
|
+
function tabButton(item: UITabItem, i: number, onSelect: (i: number) => void): ReturnType<typeof UIButton> {
|
|
46
|
+
const parts: UINodeChild[] = []
|
|
47
|
+
if (item.icon) parts.push(UIImage({ width: 24, height: 24 }, item.icon))
|
|
48
|
+
if (item.label) parts.push(UIText({ fontSize: 11, lineHeight: 14, textAlign: "center" }, item.label))
|
|
49
|
+
|
|
50
|
+
// Chained .style() (not the legacy first-arg object) — required for the `$active` class block.
|
|
51
|
+
const btn = UIButton(parts).style({
|
|
52
|
+
flexGrow: 1,
|
|
53
|
+
flexBase: 0,
|
|
54
|
+
flexDirection: "column",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
justifyContent: "center",
|
|
57
|
+
gap: 3,
|
|
58
|
+
paddingVertical: 4,
|
|
59
|
+
opacity: 0.55,
|
|
60
|
+
$active: { opacity: 1, duration: 150 },
|
|
61
|
+
})
|
|
62
|
+
btn.onClick(() => onSelect(i))
|
|
63
|
+
return btn
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function UITabs(items: UITabItem[]): UITabs {
|
|
67
|
+
let index = 0
|
|
68
|
+
const listeners: ((index: number) => void)[] = []
|
|
69
|
+
|
|
70
|
+
const host = UIScreenHost(items.map(it => it.content))
|
|
71
|
+
.style({ swipeEnabled: false, flexGrow: 1, flexShrink: 1 })
|
|
72
|
+
|
|
73
|
+
const barItems = items.map((it, i) => tabButton(it, i, select))
|
|
74
|
+
const bar = UIRow({
|
|
75
|
+
flexShrink: 0,
|
|
76
|
+
paddingTop: 8,
|
|
77
|
+
paddingBottom: "safe-bottom",
|
|
78
|
+
borderTopWidth: 1,
|
|
79
|
+
borderTopColor: "#00000022",
|
|
80
|
+
}, barItems)
|
|
81
|
+
|
|
82
|
+
const applyActive = (i: number) => {
|
|
83
|
+
for (let k = 0; k < barItems.length; k++) barItems[k].setClass("active", k === i)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function select(i: number): UITabs {
|
|
87
|
+
i = clamp(i, 0, Math.max(0, items.length - 1))
|
|
88
|
+
if (i !== index) {
|
|
89
|
+
index = i
|
|
90
|
+
host.setPage(i, false)
|
|
91
|
+
applyActive(i)
|
|
92
|
+
for (const cb of listeners) cb(i)
|
|
93
|
+
}
|
|
94
|
+
return view
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
applyActive(0)
|
|
98
|
+
|
|
99
|
+
const view = UIColumn([ host, bar ]) as unknown as UITabs
|
|
100
|
+
;(view as any).select = select
|
|
101
|
+
;(view as any).onChange = (cb: (index: number) => void) => { listeners.push(cb); return view }
|
|
102
|
+
;(view as any).bar = bar
|
|
103
|
+
Object.defineProperty(view, "index", { get: () => index })
|
|
104
|
+
return view
|
|
105
|
+
}
|
|
@@ -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
|
+
})()
|