lecodes-cli 0.11.0 → 0.13.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/dist/index.js +48869 -7399
- package/package.json +6 -5
- package/runtime/scene-harness.json +1 -1
- package/runtime/sdk/g2/Node2D.ts +3 -0
- package/runtime/sdk/g2/SpriteAnimation.ts +101 -20
- package/runtime/sdk/g2/SpriteSheet.ts +166 -0
- package/runtime/sdk/g2/Tileset.ts +71 -0
- package/runtime/sdk/g2/autotile.ts +433 -0
- package/runtime/sdk/g2/cells.ts +91 -0
- package/runtime/sdk/g2/defineScene2d.ts +381 -0
- package/runtime/sdk/g2/scenarios2d.ts +69 -0
- package/runtime/sdk/gl/Camera.ts +40 -0
- package/runtime/sdk/inject.ts +17 -0
- package/runtime/sdk/runtime/app.ts +21 -3
- package/runtime/sdk/scene/defineScene.ts +53 -83
- package/runtime/sdk/scene/editorPlugins.ts +1 -1
- package/runtime/sdk/scene/grammar.ts +85 -0
- package/runtime/sdk/ui/NativeView.ts +6 -12
- package/runtime/sdk/ui/UIBottomSheet.ts +0 -4
- package/runtime/sdk/ui/UIButton.ts +2 -21
- package/runtime/sdk/ui/UIContainer.ts +4 -58
- package/runtime/sdk/ui/UIImage.ts +2 -13
- package/runtime/sdk/ui/UIInput.ts +9 -21
- package/runtime/sdk/ui/UIModal.ts +0 -4
- package/runtime/sdk/ui/UINode.ts +37 -0
- package/runtime/sdk/ui/UIPager.ts +2 -13
- package/runtime/sdk/ui/UIScreen.ts +4 -20
- package/runtime/sdk/ui/UIScrollable.ts +2 -20
- package/runtime/sdk/ui/UISpacer.ts +2 -8
- package/runtime/sdk/ui/UIText.ts +2 -13
- package/runtime/sdk/ui/UIVideo.ts +6 -13
- package/runtime/sdk/ui/UIVirtualizedList.ts +10 -10
- package/runtime/sdk/ui/UIWidget.ts +3 -19
- package/runtime/sdk-types.json +1 -1
|
@@ -34,10 +34,15 @@
|
|
|
34
34
|
|
|
35
35
|
import { Aspect, type AspectCtor, type With } from "../core/Aspect"
|
|
36
36
|
import { describeAspect, describeFields, type AspectClassInfo } from "../core/fields"
|
|
37
|
+
import {
|
|
38
|
+
collectRefDeps, isEditMode, isNodeRef, resolveRefs, use, ref, make, EDIT_FLAG,
|
|
39
|
+
type AspectEntry, type MakeEntry as SharedMakeEntry,
|
|
40
|
+
} from "./grammar"
|
|
37
41
|
import { InspectorUI, type InspectorEvent, type InspectorWidget } from "../core/InspectorUI"
|
|
38
42
|
import type { ColorInput } from "../core/color"
|
|
39
43
|
import type { Vec3Like } from "../math/vec"
|
|
40
44
|
import { Scene, type SceneOptions } from "../gl/Scene"
|
|
45
|
+
import { CAMERA_DEFAULTS } from "../gl/Camera"
|
|
41
46
|
import { Node } from "../gl/Node"
|
|
42
47
|
import { Mesh } from "../gl/Mesh"
|
|
43
48
|
import { Model } from "../gl/Model"
|
|
@@ -63,65 +68,13 @@ export type MaterialDef =
|
|
|
63
68
|
// An imported/shared Material instance — valid at runtime; the editor shows it read-only.
|
|
64
69
|
| Material
|
|
65
70
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
ctor: AspectCtor<A>
|
|
71
|
-
props?: Partial<A>
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Reference an aspect in a scene file: `aspects: [use(Physics, { motion: 'static' })]`. Props are
|
|
75
|
-
* typechecked against the aspect's fields, exactly like `node.aspect(Ctor, props)`. */
|
|
76
|
-
export const use = <A extends Aspect<any, any>>(ctor: AspectCtor<A>, props?: Partial<A>): AspectEntry<A> =>
|
|
77
|
-
({ __use: true, ctor, props })
|
|
78
|
-
|
|
79
|
-
/** A `make(fn, args)` source entry — see {@link make}. */
|
|
80
|
-
export type MakeEntry<A extends Record<string, unknown> = Record<string, unknown>> = {
|
|
81
|
-
/** @internal grammar marker. */
|
|
82
|
-
readonly __make: true
|
|
83
|
-
fn: (args: A) => Node | Promise<Node>
|
|
84
|
-
args?: A
|
|
85
|
-
}
|
|
71
|
+
// The grammar markers (`use`/`ref`/`make`) live in ./grammar.ts, shared with defineScene2d —
|
|
72
|
+
// re-exported here so this module remains the one import site for scene-file machinery.
|
|
73
|
+
export { use, ref, make }
|
|
74
|
+
export type { AspectEntry }
|
|
86
75
|
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
* The factory runs after every scene node exists (so `ref()` args resolve, forward references
|
|
90
|
-
* included) and its returned subtree mounts under the def node — the def's transform stays
|
|
91
|
-
* editor-owned, so moving the node never re-calls the factory. Args must be literal data (same
|
|
92
|
-
* grammar as aspect props); in the editor they edit as fields, and an arg change re-CALLS the
|
|
93
|
-
* factory live — the code is already in the bundle, so no compile happens. Keep factories pure
|
|
94
|
-
* builders: same args → same subtree, no side effects outside the returned nodes.
|
|
95
|
-
*/
|
|
96
|
-
export const make = <A extends Record<string, unknown>>(
|
|
97
|
-
fn: (args: A) => Node | Promise<Node>, args?: A,
|
|
98
|
-
): MakeEntry<A> => ({ __make: true, fn, args })
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Reference another scene node by name in an aspect's props:
|
|
102
|
-
* `use(Road, { from: ref('pointA'), to: ref('pointB') })`. Resolves to the live Node when aspects
|
|
103
|
-
* attach — after EVERY node of the scene exists, so forward references work. An unknown name
|
|
104
|
-
* resolves to `null` (type your aspect field `Node | null`). Scene files only — hand-written code
|
|
105
|
-
* passes nodes directly: `node.aspect(Road, { from: nodes.pointA })`.
|
|
106
|
-
*/
|
|
107
|
-
export const ref = <T extends Node = Node>(name: string): T => ({ $ref: name } as unknown as T)
|
|
108
|
-
|
|
109
|
-
const isNodeRef = (v: unknown): v is { $ref: string } =>
|
|
110
|
-
typeof v === "object" && v !== null && typeof (v as { $ref?: unknown }).$ref === "string"
|
|
111
|
-
|
|
112
|
-
/** Swap `ref()` markers in aspect props for the live nodes (top level + one array level deep). */
|
|
113
|
-
const resolveRefs = (props: Record<string, unknown> | undefined, nodes: Record<string, Node>): Record<string, unknown> | undefined => {
|
|
114
|
-
if (!props) return props
|
|
115
|
-
let out: Record<string, unknown> | undefined
|
|
116
|
-
for (const [ k, v ] of Object.entries(props)) {
|
|
117
|
-
if (isNodeRef(v)) {
|
|
118
|
-
;(out ??= { ...props })[k] = nodes[v.$ref] ?? null
|
|
119
|
-
} else if (Array.isArray(v) && v.some(isNodeRef)) {
|
|
120
|
-
;(out ??= { ...props })[k] = v.map((el) => (isNodeRef(el) ? nodes[el.$ref] ?? null : el))
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return out ?? props
|
|
124
|
-
}
|
|
76
|
+
/** A `make(fn, args)` source entry whose factory returns a 3D {@link Node}. */
|
|
77
|
+
export type MakeEntry<A extends Record<string, unknown> = Record<string, unknown>> = SharedMakeEntry<A, Node>
|
|
125
78
|
|
|
126
79
|
/** Transform overrides for one INTERNAL node of a GLB model or a prefab instance
|
|
127
80
|
* (`overrides` on a model/prefab node, keyed by part path). */
|
|
@@ -132,8 +85,19 @@ export type ModelOverrideDef = {
|
|
|
132
85
|
visible?: boolean
|
|
133
86
|
}
|
|
134
87
|
|
|
135
|
-
/**
|
|
136
|
-
|
|
88
|
+
/** Camera projection settings, shared by the `camera:` source block and the top-level `camera:`
|
|
89
|
+
* block. All optional — an omitted key keeps the host default (60° / 0.01 / 1000). */
|
|
90
|
+
export type CameraProjectionDef = {
|
|
91
|
+
/** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
|
|
92
|
+
fov?: number
|
|
93
|
+
/** Near clip distance (default 0.01). */
|
|
94
|
+
near?: number
|
|
95
|
+
/** Far clip distance = view range (default 1000); geometry past it is culled. */
|
|
96
|
+
far?: number
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The `camera: {}` source block — projection settings for the node that drives the view. */
|
|
100
|
+
export type CameraNodeDef = CameraProjectionDef
|
|
137
101
|
|
|
138
102
|
export type SceneNodeDef = {
|
|
139
103
|
// -- source (at most one; none = plain group node) --
|
|
@@ -163,7 +127,7 @@ export type SceneNodeDef = {
|
|
|
163
127
|
eulerAngles?: Vec3Like
|
|
164
128
|
scale?: Vec3Like | number
|
|
165
129
|
visible?: boolean
|
|
166
|
-
/** Editor-only:
|
|
130
|
+
/** Editor-only: viewport manipulation won't target this node (fields still edit). No runtime effect. */
|
|
167
131
|
locked?: boolean
|
|
168
132
|
castShadows?: boolean
|
|
169
133
|
receiveShadows?: boolean
|
|
@@ -172,7 +136,7 @@ export type SceneNodeDef = {
|
|
|
172
136
|
children?: Record<string, SceneNodeDef>
|
|
173
137
|
}
|
|
174
138
|
|
|
175
|
-
export type SceneCameraDef = {
|
|
139
|
+
export type SceneCameraDef = CameraProjectionDef & {
|
|
176
140
|
position?: Vec3Like
|
|
177
141
|
/** Point the camera looks at. */
|
|
178
142
|
target?: Vec3Like
|
|
@@ -272,9 +236,6 @@ const applyModelOverrides = (root: Node, overrides: Record<string, ModelOverride
|
|
|
272
236
|
|
|
273
237
|
// ---- runtime -------------------------------------------------------------------
|
|
274
238
|
|
|
275
|
-
const EDIT_FLAG = "__lecodesSceneEdit"
|
|
276
|
-
const isEditMode = (): boolean => (globalThis as Record<string, unknown>)[EDIT_FLAG] === true
|
|
277
|
-
|
|
278
239
|
const resolveMaterial = (def: MaterialDef): Material => {
|
|
279
240
|
if (def instanceof Material) return def
|
|
280
241
|
if ("lit" in def) return Material.lit(def.lit)
|
|
@@ -309,7 +270,7 @@ const applyNode = (node: Node, name: string, def: SceneNodeDef): void => {
|
|
|
309
270
|
if (def.eulerAngles) node.eulerAngles = def.eulerAngles
|
|
310
271
|
if (def.scale !== undefined) node.scale = def.scale
|
|
311
272
|
if (def.visible !== undefined) node.visible = def.visible
|
|
312
|
-
// editor-only flag (the harness
|
|
273
|
+
// editor-only flag (the harness's manipulation layer consults it); inert at runtime
|
|
313
274
|
if (def.locked !== undefined) (node as { _sceneLocked?: boolean })._sceneLocked = def.locked
|
|
314
275
|
if (def.camera !== undefined) (node as { _sceneCamera?: boolean })._sceneCamera = true
|
|
315
276
|
// waypoint/def order for aspects that read children (FollowPath) — the engine's live child
|
|
@@ -368,16 +329,27 @@ class CameraRig extends Aspect<"__cameraRig"> {
|
|
|
368
329
|
}
|
|
369
330
|
}
|
|
370
331
|
|
|
371
|
-
/** The first camera-source def in file order (depth-first)
|
|
372
|
-
const
|
|
332
|
+
/** The first camera-source def in file order (depth-first) — its name and block — or null. */
|
|
333
|
+
const findCamera = (defs?: Record<string, SceneNodeDef>): { name: string, def: CameraNodeDef } | null => {
|
|
373
334
|
for (const [ name, nd ] of Object.entries(defs ?? {})) {
|
|
374
|
-
if (nd.camera !== undefined) return name
|
|
375
|
-
const inner =
|
|
335
|
+
if (nd.camera !== undefined) return { name, def: nd.camera }
|
|
336
|
+
const inner = findCamera(nd.children)
|
|
376
337
|
if (inner) return inner
|
|
377
338
|
}
|
|
378
339
|
return null
|
|
379
340
|
}
|
|
380
341
|
|
|
342
|
+
/** fov / near / far from a camera block onto the live camera. The build path leaves an empty block
|
|
343
|
+
* alone (a host may run with its own configured fov); `reset` — the editor's live patch — fills
|
|
344
|
+
* omitted keys with the defaults instead, so clearing a field in the inspector takes effect. */
|
|
345
|
+
const applyCameraProjection = (scene: Scene, def: CameraProjectionDef, reset = false): void => {
|
|
346
|
+
const { fov, near, far } = def
|
|
347
|
+
if (!reset && fov === undefined && near === undefined && far === undefined) return
|
|
348
|
+
scene.camera.setProjection(reset
|
|
349
|
+
? { fov: fov ?? CAMERA_DEFAULTS.fov, near: near ?? CAMERA_DEFAULTS.near, far: far ?? CAMERA_DEFAULTS.far }
|
|
350
|
+
: { fov, near, far })
|
|
351
|
+
}
|
|
352
|
+
|
|
381
353
|
// ---- editor-run aspects (generators) ------------------------------------------------------------
|
|
382
354
|
// A class with `static editor = { rebuild: true }` runs while a scene is edited: the loader
|
|
383
355
|
// constructs it (refs resolved, node + `generated` set — NEVER onAttach) and calls rebuild(); the
|
|
@@ -429,15 +401,6 @@ type EditorRun = {
|
|
|
429
401
|
deps: Set<string>
|
|
430
402
|
}
|
|
431
403
|
|
|
432
|
-
const collectRefDeps = (props: Record<string, unknown>): Set<string> => {
|
|
433
|
-
const deps = new Set<string>()
|
|
434
|
-
for (const v of Object.values(props)) {
|
|
435
|
-
if (isNodeRef(v)) deps.add(v.$ref)
|
|
436
|
-
else if (Array.isArray(v)) for (const el of v) if (isNodeRef(el)) deps.add(el.$ref)
|
|
437
|
-
}
|
|
438
|
-
return deps
|
|
439
|
-
}
|
|
440
|
-
|
|
441
404
|
const safeRebuild = (run: EditorRun): void => {
|
|
442
405
|
// a throwing generator must not take the editor session down with it
|
|
443
406
|
try { run.inst.rebuild?.() } catch (e) { console.error(`[scene] editor aspect rebuild failed on "${run.hostName}":`, e) }
|
|
@@ -626,18 +589,22 @@ const instantiate = async (def: SceneDef, editorRuns: EditorRun[]): Promise<{ sc
|
|
|
626
589
|
await buildNodes(def.nodes ?? {}, null, scene, nodes, editorRuns, new Set([ def ]))
|
|
627
590
|
|
|
628
591
|
// a camera NODE wins over the top-level `camera:` block; in play mode it keeps driving the view
|
|
629
|
-
// (CameraRig), in edit mode it only seeds the editor's starting viewpoint
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
592
|
+
// (CameraRig), in edit mode it only seeds the editor's starting viewpoint. The PROJECTION applies
|
|
593
|
+
// in both modes — it is a property of the scene, not of the viewpoint, so the editor shows the
|
|
594
|
+
// lens the running app will use.
|
|
595
|
+
const cam = findCamera(def.nodes)
|
|
596
|
+
const camNode = cam ? nodes[cam.name] : undefined
|
|
597
|
+
if (cam && camNode) {
|
|
633
598
|
scene.camera.position = camNode.worldPosition
|
|
634
599
|
scene.camera.quaternion = camNode.worldQuaternion
|
|
600
|
+
applyCameraProjection(scene, cam.def)
|
|
635
601
|
if (!isEditMode()) {
|
|
636
602
|
;(camNode as Node & { aspect(c: unknown, p?: unknown): unknown }).aspect(CameraRig, { _scene: scene })
|
|
637
603
|
}
|
|
638
604
|
} else if (def.camera) {
|
|
639
605
|
if (def.camera.position) scene.camera.position = def.camera.position
|
|
640
606
|
if (def.camera.target) scene.camera.lookAt(def.camera.target)
|
|
607
|
+
applyCameraProjection(scene, def.camera)
|
|
641
608
|
}
|
|
642
609
|
|
|
643
610
|
return { scene, nodes }
|
|
@@ -734,6 +701,9 @@ export class SceneHandle<D extends SceneDef = SceneDef> {
|
|
|
734
701
|
dispose(old) // fresh already replaced nodes[name] in build(), so it survives
|
|
735
702
|
nodes[name] = fresh
|
|
736
703
|
}
|
|
704
|
+
// the projection lives on the scene camera, not on the node — re-apply it here so an inspector
|
|
705
|
+
// fov/near/far edit lands live (a rebuilt node alone would carry none of it)
|
|
706
|
+
if (def.camera !== undefined) applyCameraProjection(scene, def.camera, true)
|
|
737
707
|
return fresh
|
|
738
708
|
}
|
|
739
709
|
|
|
@@ -57,7 +57,7 @@ export type EditorToolHooks = {
|
|
|
57
57
|
* share the generic wand icon, so any editor with two or more tools should set it. */
|
|
58
58
|
icon?: string
|
|
59
59
|
/** A viewport click while the tool is active — `hit` is the raycast result under the pointer.
|
|
60
|
-
*
|
|
60
|
+
* Viewport clicks route to the tool while it is active. */
|
|
61
61
|
onViewportClick?(hit: EditorRayHit, editor: EditorApi): void
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// The scene-file literal grammar markers, shared by `.scene.ts` (defineScene, 3D) and
|
|
2
|
+
// `.scene2d.ts` (defineScene2d, 2D): `use(Ctor, props)` aspect entries, `ref('name')` node
|
|
3
|
+
// references, `make(fn, args)` code-built sources — plus the ref-resolution helpers both loaders
|
|
4
|
+
// run. One implementation so the two formats can't drift; the engine-specific node/def types stay
|
|
5
|
+
// in their own modules (defineScene.ts / defineScene2d.ts), which re-export these for user code.
|
|
6
|
+
|
|
7
|
+
import type { Aspect, AspectCtor } from "../core/Aspect"
|
|
8
|
+
import type { Node } from "../gl/Node"
|
|
9
|
+
|
|
10
|
+
/** One aspect to attach, created by `use(Ctor, props)`. */
|
|
11
|
+
export type AspectEntry<A extends Aspect<any, any> = Aspect<any, any>> = {
|
|
12
|
+
/** @internal grammar marker. */
|
|
13
|
+
readonly __use: true
|
|
14
|
+
ctor: AspectCtor<A>
|
|
15
|
+
props?: Partial<A>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Reference an aspect in a scene file: `aspects: [use(Physics, { motion: 'static' })]`. Props are
|
|
19
|
+
* typechecked against the aspect's fields, exactly like `node.aspect(Ctor, props)`. */
|
|
20
|
+
export const use = <A extends Aspect<any, any>>(ctor: AspectCtor<A>, props?: Partial<A>): AspectEntry<A> =>
|
|
21
|
+
({ __use: true, ctor, props })
|
|
22
|
+
|
|
23
|
+
/** A `make(fn, args)` source entry — see {@link make}. `N` is the node family the factory returns
|
|
24
|
+
* (3D `Node` by default; a `.scene2d.ts` file's factories return `Node2D`). */
|
|
25
|
+
export type MakeEntry<A extends Record<string, unknown> = Record<string, unknown>, N = Node> = {
|
|
26
|
+
/** @internal grammar marker. */
|
|
27
|
+
readonly __make: true
|
|
28
|
+
fn: (args: A) => N | Promise<N>
|
|
29
|
+
args?: A
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A code-created source in a scene file: `tower: { make: make(buildTower, { floors: 5 }), … }`.
|
|
34
|
+
* The factory runs after every scene node exists (so `ref()` args resolve, forward references
|
|
35
|
+
* included) and its returned subtree mounts under the def node — the def's transform stays
|
|
36
|
+
* editor-owned, so moving the node never re-calls the factory. Args must be literal data (same
|
|
37
|
+
* grammar as aspect props); in the editor they edit as fields, and an arg change re-CALLS the
|
|
38
|
+
* factory live — the code is already in the bundle, so no compile happens. Keep factories pure
|
|
39
|
+
* builders: same args → same subtree, no side effects outside the returned nodes.
|
|
40
|
+
*/
|
|
41
|
+
export const make = <A extends Record<string, unknown>, N>(
|
|
42
|
+
fn: (args: A) => N | Promise<N>, args?: A,
|
|
43
|
+
): MakeEntry<A, N> => ({ __make: true, fn, args })
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reference another scene node by name in an aspect's props:
|
|
47
|
+
* `use(Road, { from: ref('pointA'), to: ref('pointB') })`. Resolves to the live node when aspects
|
|
48
|
+
* attach — after EVERY node of the scene exists, so forward references work. An unknown name
|
|
49
|
+
* resolves to `null` (type your aspect field `Node | null`). Scene files only — hand-written code
|
|
50
|
+
* passes nodes directly: `node.aspect(Road, { from: nodes.pointA })`.
|
|
51
|
+
*/
|
|
52
|
+
export const ref = <T = Node>(name: string): T => ({ $ref: name } as unknown as T)
|
|
53
|
+
|
|
54
|
+
export const isNodeRef = (v: unknown): v is { $ref: string } =>
|
|
55
|
+
typeof v === "object" && v !== null && typeof (v as { $ref?: unknown }).$ref === "string"
|
|
56
|
+
|
|
57
|
+
/** Swap `ref()` markers in aspect props for the live nodes (top level + one array level deep). */
|
|
58
|
+
export const resolveRefs = <N>(
|
|
59
|
+
props: Record<string, unknown> | undefined, nodes: Record<string, N>,
|
|
60
|
+
): Record<string, unknown> | undefined => {
|
|
61
|
+
if (!props) return props
|
|
62
|
+
let out: Record<string, unknown> | undefined
|
|
63
|
+
for (const [ k, v ] of Object.entries(props)) {
|
|
64
|
+
if (isNodeRef(v)) {
|
|
65
|
+
;(out ??= { ...props })[k] = nodes[v.$ref] ?? null
|
|
66
|
+
} else if (Array.isArray(v) && v.some(isNodeRef)) {
|
|
67
|
+
;(out ??= { ...props })[k] = v.map((el) => (isNodeRef(el) ? nodes[el.$ref] ?? null : el))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return out ?? props
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Names of the nodes a props record's ref() values point at (editor dep tracking). */
|
|
74
|
+
export const collectRefDeps = (props: Record<string, unknown>): Set<string> => {
|
|
75
|
+
const deps = new Set<string>()
|
|
76
|
+
for (const v of Object.values(props)) {
|
|
77
|
+
if (isNodeRef(v)) deps.add(v.$ref)
|
|
78
|
+
else if (Array.isArray(v)) for (const el of v) if (isNodeRef(el)) deps.add(el.$ref)
|
|
79
|
+
}
|
|
80
|
+
return deps
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The edit-mode flag an editor host sets before running the bundle (shared by both formats). */
|
|
84
|
+
export const EDIT_FLAG = "__lecodesSceneEdit"
|
|
85
|
+
export const isEditMode = (): boolean => (globalThis as Record<string, unknown>)[EDIT_FLAG] === true
|
|
@@ -11,29 +11,30 @@
|
|
|
11
11
|
|
|
12
12
|
import { _channelCall, _channelEmit, _channelOff, _channelOn, type ChannelListeners } from "../runtime/channel"
|
|
13
13
|
import { _bumpNavEpoch, _navSupported, _setCurrent, Presentable, type PresentOptions } from "./presentable"
|
|
14
|
-
import { Element, type
|
|
14
|
+
import { Element, type BaseStyle, type DrawableStyle, type ElementStyle, type UIElementBase } from "./UINode"
|
|
15
15
|
|
|
16
16
|
export type UINativeViewStyle = ElementStyle & DrawableStyle
|
|
17
17
|
|
|
18
18
|
/** An instance of a host-registered platform view (map, QR scanner, camera preview, …) —
|
|
19
19
|
* embeddable among a screen's children AND fullscreen-presentable (`open()`); one instance lives
|
|
20
20
|
* in one place at a time. Talk to it via the `call`/`on` channel. */
|
|
21
|
-
export interface NativeView {
|
|
21
|
+
export interface NativeView extends UIElementBase<UINativeViewStyle, DrawableStyle & BaseStyle> {
|
|
22
22
|
readonly type: "native",
|
|
23
23
|
/** The registered view kind this instance resolves to (the `registerView` name). */
|
|
24
24
|
readonly viewName: string,
|
|
25
25
|
/** Creation params, passed to the host factory (JSON-serializable). */
|
|
26
26
|
readonly params: any,
|
|
27
|
-
style: Style<this, UINativeViewStyle>,
|
|
28
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
29
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
30
27
|
|
|
31
28
|
/** Show fullscreen as the current destination (see Presentable). */
|
|
32
29
|
open(options?: PresentOptions): void,
|
|
30
|
+
/** Dismiss if this is the visible destination. */
|
|
33
31
|
close(): void,
|
|
34
32
|
|
|
33
|
+
/** Became the visible destination. */
|
|
35
34
|
onOpen(callback: () => void): this
|
|
35
|
+
/** Stopped being the visible destination. */
|
|
36
36
|
onClose(callback: () => void): this
|
|
37
|
+
/** Hardware/system back while current. */
|
|
37
38
|
onBackPressed(callback: () => void): this
|
|
38
39
|
|
|
39
40
|
/** Invoke a method on the native view (`map.call("setCenter", [lng, lat])`). Args and the
|
|
@@ -42,13 +43,6 @@ export interface NativeView {
|
|
|
42
43
|
/** Subscribe to an event the native view emits (`map.on("markerTap", cb)`). */
|
|
43
44
|
on(event: string, callback: (data?: any) => void): this
|
|
44
45
|
off(event: string, callback: (data?: any) => void): this
|
|
45
|
-
|
|
46
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
47
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
48
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
49
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
50
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
51
|
-
readonly class: Classes<this>
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
let nextViewId = 1
|
|
@@ -51,10 +51,6 @@ export interface UIBottomSheet extends UIWidget {
|
|
|
51
51
|
/** `dismissible(false)`: scrim taps, the back button and dragging below the lowest detent no
|
|
52
52
|
* longer close the sheet (a drag collapses to the lowest detent instead). Default `true`. */
|
|
53
53
|
dismissible(enabled: boolean): this,
|
|
54
|
-
|
|
55
|
-
append(...nodes: UINodeChild[]): this
|
|
56
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
57
|
-
remove(...nodes: UINodeChild[]): this
|
|
58
54
|
}
|
|
59
55
|
|
|
60
56
|
export class BottomSheetElement extends ModalElement {
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import { ClickEvent, TouchStartEvent, LongPressEvent } from "../runtime/touch"
|
|
2
2
|
import type { ClickEvent as ClickEventType, TouchStartEvent as TouchStartEventType, LongPressEvent as LongPressEventType } from "../runtime/touch"
|
|
3
|
-
import { type ElementStyle, type ContainerStyle,
|
|
3
|
+
import { type ElementStyle, type ContainerStyle, ContainerElement, buildUI, type DrawableStyle, type UIChildArg, type UIContainerBase, type UINodeChild, type ChildrenFn, type BaseStyle, type Color } from "./UINode"
|
|
4
4
|
|
|
5
5
|
export type UIButtonStyle = ElementStyle & DrawableStyle & ContainerStyle & { rippleColor?: Color | "default" }
|
|
6
6
|
|
|
7
7
|
/** A tappable container — row + centered children by default. Press feedback via the `onPressed`
|
|
8
8
|
* style block, the `$pressed` class, or `rippleColor`. */
|
|
9
|
-
export interface UIButton {
|
|
9
|
+
export interface UIButton extends UIContainerBase<UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
10
10
|
readonly type: "button",
|
|
11
11
|
|
|
12
|
-
style: Style<this, UIButtonStyle & { onPressed?: UIButtonStyle & { duration?: number } }>,
|
|
13
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>,
|
|
14
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
15
|
-
|
|
16
12
|
/** Fired on a completed tap/click. */
|
|
17
13
|
onClick(callback: (ev: ClickEventType<UIButton>) => void): this,
|
|
18
14
|
/** Fired when a touch begins; `ev.track(...)` takes over the rest of the gesture (dragging). */
|
|
@@ -23,21 +19,6 @@ export interface UIButton {
|
|
|
23
19
|
onLongPress(callback: (ev: LongPressEventType<UIButton>) => void): this,
|
|
24
20
|
/** Is a finger currently down on the button? */
|
|
25
21
|
isPressed(): boolean,
|
|
26
|
-
|
|
27
|
-
append(...nodes: UINodeChild[]): this
|
|
28
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
29
|
-
remove(...nodes: UINodeChild[]): this
|
|
30
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
31
|
-
|
|
32
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
33
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
34
|
-
|
|
35
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
36
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
37
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
38
|
-
readonly class: Classes<this>
|
|
39
|
-
|
|
40
|
-
readonly children: UINodeChild[],
|
|
41
22
|
}
|
|
42
23
|
|
|
43
24
|
|
|
@@ -1,28 +1,10 @@
|
|
|
1
|
-
import { ContainerElement, buildUI, type
|
|
1
|
+
import { ContainerElement, buildUI, type ChildrenFn, type ContainerStyle, type DrawableStyle, type ElementStyle, type UIChildArg, type UIContainerBase, type UINodeChild } from "./UINode"
|
|
2
2
|
|
|
3
3
|
export type UIContainerStyle = ElementStyle & DrawableStyle & ContainerStyle
|
|
4
4
|
|
|
5
5
|
/** A vertical flex container — children stack top to bottom. */
|
|
6
|
-
export interface UIColumn {
|
|
6
|
+
export interface UIColumn extends UIContainerBase<UIContainerStyle> {
|
|
7
7
|
readonly type: "column",
|
|
8
|
-
style: Style<this, UIContainerStyle>,
|
|
9
|
-
animateTo: AnimateStyle<this, UIContainerStyle>
|
|
10
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>
|
|
11
|
-
|
|
12
|
-
append(...nodes: UINodeChild[]): UIColumn
|
|
13
|
-
insert(index: number, ...nodes: UINodeChild[]): UIColumn
|
|
14
|
-
remove(...nodes: UINodeChild[]): UIColumn
|
|
15
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
16
|
-
|
|
17
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
18
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
19
|
-
|
|
20
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
21
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
22
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
23
|
-
readonly class: Classes<this>
|
|
24
|
-
|
|
25
|
-
readonly children: UINodeChild[]
|
|
26
8
|
}
|
|
27
9
|
const makeColumn = (style: any, children: any) => new ContainerElement("column", style, children)
|
|
28
10
|
|
|
@@ -44,26 +26,8 @@ export function UIColumn(...args: any[]): UIColumn {
|
|
|
44
26
|
}
|
|
45
27
|
|
|
46
28
|
/** A horizontal flex container — children flow left to right. */
|
|
47
|
-
export interface UIRow {
|
|
29
|
+
export interface UIRow extends UIContainerBase<UIContainerStyle> {
|
|
48
30
|
readonly type: "row",
|
|
49
|
-
style: Style<this, UIContainerStyle>,
|
|
50
|
-
animateTo: AnimateStyle<this, UIContainerStyle>,
|
|
51
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>,
|
|
52
|
-
|
|
53
|
-
append(...nodes: UINodeChild[]): UIRow
|
|
54
|
-
insert(index: number, ...nodes: UINodeChild[]): UIRow
|
|
55
|
-
remove(...nodes: UINodeChild[]): UIRow
|
|
56
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
57
|
-
|
|
58
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
59
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
60
|
-
|
|
61
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
62
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
63
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
64
|
-
readonly class: Classes<this>
|
|
65
|
-
|
|
66
|
-
readonly children: UINodeChild[],
|
|
67
31
|
}
|
|
68
32
|
const ROW_DEFAULTS = { flexDirection: "row" } as const
|
|
69
33
|
const makeRow = (style: any, children: any) => new ContainerElement("row", style, children)
|
|
@@ -78,26 +42,8 @@ export function UIRow(...args: any[]): UIRow {
|
|
|
78
42
|
}
|
|
79
43
|
|
|
80
44
|
/** A centering container — `justifyContent`/`alignItems` default to center. */
|
|
81
|
-
export interface UIBox {
|
|
45
|
+
export interface UIBox extends UIContainerBase<UIContainerStyle> {
|
|
82
46
|
readonly type: "box",
|
|
83
|
-
style: Style<this, UIContainerStyle>,
|
|
84
|
-
animateTo: AnimateStyle<this, UIContainerStyle>,
|
|
85
|
-
animateFrom: AnimateStyle<this, UIContainerStyle>,
|
|
86
|
-
|
|
87
|
-
append(...nodes: UINodeChild[]): this
|
|
88
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
89
|
-
remove(...nodes: UINodeChild[]): this
|
|
90
|
-
setContent(nodes: UINodeChild[] | ChildrenFn): this
|
|
91
|
-
|
|
92
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
93
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
94
|
-
|
|
95
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
96
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
97
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
98
|
-
readonly class: Classes<this>
|
|
99
|
-
|
|
100
|
-
readonly children: UINodeChild[],
|
|
101
47
|
}
|
|
102
48
|
const BOX_DEFAULTS = { justifyContent: "center", alignItems: "center" } as const
|
|
103
49
|
|
|
@@ -2,7 +2,7 @@ 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
4
|
import { Canvas, isCanvas } from "../canvas/Canvas"
|
|
5
|
-
import { Element, type
|
|
5
|
+
import { Element, type Color, type ElementStyle, type UIElementBase } from "./UINode"
|
|
6
6
|
|
|
7
7
|
export type UIImageStyle = ElementStyle & { borderRadius?: number, objectFit?: "cover" | "contain" | "fill", tintColor?: Color }
|
|
8
8
|
|
|
@@ -23,27 +23,16 @@ export type ResolvedImageSource = string | FetchResponse | File | SvgSource | Ca
|
|
|
23
23
|
|
|
24
24
|
/** An image element. Sources: URL / `asset(...)`, fetch response / File, an SVG source
|
|
25
25
|
* (`assetIcon`), or a live Canvas (`canvas.update()` refreshes the node in place). */
|
|
26
|
-
export interface UIImage {
|
|
26
|
+
export interface UIImage extends UIElementBase<UIImageStyle> {
|
|
27
27
|
readonly type: "image",
|
|
28
28
|
/** The image source — writable to swap the picture in place. */
|
|
29
29
|
get src(): ResolvedImageSource
|
|
30
30
|
set src(value: ImageSource)
|
|
31
|
-
style: Style<this, UIImageStyle>
|
|
32
|
-
animateTo: AnimateStyle<this, UIImageStyle>
|
|
33
|
-
animateFrom: AnimateStyle<this, UIImageStyle>
|
|
34
31
|
|
|
35
32
|
/** Render only a sub-rectangle of the source texture (texture pixels) — the atlas/spritesheet
|
|
36
33
|
* primitive: crop once, then swap the rect per frame for sprite animation. The rect's w/h
|
|
37
34
|
* become the node's intrinsic size and override `objectFit` — the cropped frame fills the box. */
|
|
38
35
|
setSourceRect(x: number, y: number, w: number, h: number): this
|
|
39
|
-
|
|
40
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
41
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
42
|
-
|
|
43
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
44
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
45
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
46
|
-
readonly class: Classes<this>
|
|
47
36
|
}
|
|
48
37
|
|
|
49
38
|
class ImageElement extends Element<"image"> {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ElementStyle, type DrawableStyle,
|
|
1
|
+
import { type ElementStyle, type DrawableStyle, Element, type TextStyle, type Color, type BaseStyle, type UIElementBase } from "./UINode";
|
|
2
2
|
|
|
3
3
|
type InputStyle = {
|
|
4
4
|
placeholder?: string,
|
|
@@ -51,7 +51,7 @@ type SubmitStyle = {
|
|
|
51
51
|
|
|
52
52
|
/** A single-line text input. Keyboard/picker kind via the `type` style, return-key behavior via
|
|
53
53
|
* `enterKey` + `onSubmit`; keyboard policy on `keyboardShrink`/`keyboardDismiss`. */
|
|
54
|
-
export interface UIInput {
|
|
54
|
+
export interface UIInput extends UIElementBase<UIInputStyle & SubmitStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
55
55
|
readonly type: "input",
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -62,51 +62,39 @@ export interface UIInput {
|
|
|
62
62
|
*/
|
|
63
63
|
value: string
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
animateTo: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
67
|
-
animateFrom: AnimateStyle<this, DrawableStyle & BaseStyle>
|
|
68
|
-
|
|
65
|
+
/** The text changed — fires on every user edit. */
|
|
69
66
|
onChange(callback: (value: string) => void): UIInput
|
|
67
|
+
/** Gained focus (keyboard opens). */
|
|
70
68
|
onFocus(callback: () => void): UIInput
|
|
69
|
+
/** Lost focus. */
|
|
71
70
|
onBlur(callback: () => void): UIInput
|
|
72
71
|
/** The keyboard's return key was pressed. `value` is the current text. See `enterKey`. */
|
|
73
72
|
onSubmit(callback: (value: string) => void): UIInput
|
|
74
|
-
onLayout(onLayout: OnLayoutCallback): this
|
|
75
|
-
getBoundingClientRect(): BoundingClientRect | null
|
|
76
73
|
|
|
77
74
|
/** Focus the input (opens the keyboard). No-op before mount. */
|
|
78
75
|
focus(): void
|
|
79
76
|
/** Release focus (dismisses the keyboard when this input held it). */
|
|
80
77
|
blur(): void
|
|
81
|
-
|
|
82
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
83
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
84
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
85
|
-
readonly class: Classes<this>
|
|
86
78
|
}
|
|
87
79
|
|
|
88
80
|
/** A multi-line text field — see the {@link UITextArea} factory for the auto-grow contract. */
|
|
89
|
-
export interface UITextArea {
|
|
81
|
+
export interface UITextArea extends UIElementBase<UIInputStyle & { onFocused?: UIInputStyle & { duration?: number } }, DrawableStyle & BaseStyle> {
|
|
90
82
|
readonly type: "textarea",
|
|
91
83
|
|
|
92
84
|
/** The field's text — same contract as {@link UIInput.value}: settable before mount to pre-fill. */
|
|
93
85
|
value: string
|
|
94
86
|
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
/** The text changed — fires on every user edit. */
|
|
97
88
|
onChange(callback: (value: string) => void): UITextArea
|
|
89
|
+
/** Gained focus (keyboard opens). */
|
|
98
90
|
onFocus(callback: () => void): UITextArea
|
|
91
|
+
/** Lost focus. */
|
|
99
92
|
onBlur(callback: () => void): UITextArea
|
|
100
93
|
|
|
101
94
|
/** Focus the text area (opens the keyboard). No-op before mount. */
|
|
102
95
|
focus(): void
|
|
103
96
|
/** Release focus (dismisses the keyboard when this text area held it). */
|
|
104
97
|
blur(): void
|
|
105
|
-
|
|
106
|
-
/** Style-class proxy — read `el.class.checked`, set `el.class.checked = true`, toggle with
|
|
107
|
-
* `!el.class.checked`, bind `el.class.done = () => sig.value`, batch/chain `el.class({ … })`.
|
|
108
|
-
* Classes cascade to descendants. See {@link Classes}. */
|
|
109
|
-
readonly class: Classes<this>
|
|
110
98
|
}
|
|
111
99
|
|
|
112
100
|
class InputElement<T extends "input" | "textarea"> extends Element<T> {
|
|
@@ -50,10 +50,6 @@ export interface UIModal extends UIWidget {
|
|
|
50
50
|
/** `dismissible(false)` keeps scrim taps and the back button from closing the modal
|
|
51
51
|
* (forced-choice dialogs). Default `true`. */
|
|
52
52
|
dismissible(enabled: boolean): this,
|
|
53
|
-
|
|
54
|
-
append(...nodes: UINodeChild[]): this
|
|
55
|
-
insert(index: number, ...nodes: UINodeChild[]): this
|
|
56
|
-
remove(...nodes: UINodeChild[]): this
|
|
57
53
|
}
|
|
58
54
|
|
|
59
55
|
export class ModalElement extends WidgetElement {
|