lecodes-cli 0.17.2 → 0.18.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.
Files changed (62) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +2376 -755
  3. package/package.json +4 -4
  4. package/runtime/scene-harness.json +1 -1
  5. package/runtime/sdk/compile/aspectMacro.ts +52 -8
  6. package/runtime/sdk/compile/assetMacro.ts +116 -15
  7. package/runtime/sdk/compile/bundler.ts +39 -4
  8. package/runtime/sdk/compile/compileProject.ts +16 -1
  9. package/runtime/sdk/compile/header.ts +6 -1
  10. package/runtime/sdk/compile/index.ts +31 -0
  11. package/runtime/sdk/compile/liteMaterial.ts +247 -0
  12. package/runtime/sdk/compile/sceneEditor.ts +11 -1
  13. package/runtime/sdk/compile/shaderSchema.ts +202 -0
  14. package/runtime/sdk/compile/shaderTargets.ts +81 -0
  15. package/runtime/sdk/core/Aspect.ts +363 -95
  16. package/runtime/sdk/core/compWrite.ts +42 -0
  17. package/runtime/sdk/core/fields.ts +1 -1
  18. package/runtime/sdk/core/time.ts +81 -0
  19. package/runtime/sdk/g2/Camera2D.ts +8 -1
  20. package/runtime/sdk/g2/CharacterController2D.ts +253 -53
  21. package/runtime/sdk/g2/Node2D.ts +80 -10
  22. package/runtime/sdk/g2/OneWay2D.ts +66 -0
  23. package/runtime/sdk/g2/Physics2D.ts +240 -30
  24. package/runtime/sdk/g2/Scene2D.ts +33 -1
  25. package/runtime/sdk/g2/Shape2D.ts +218 -22
  26. package/runtime/sdk/g2/Trigger2D.ts +42 -12
  27. package/runtime/sdk/g2/groups2d.ts +106 -0
  28. package/runtime/sdk/g2/loop.ts +15 -4
  29. package/runtime/sdk/gl/Camera.ts +41 -0
  30. package/runtime/sdk/gl/CameraPlace.ts +52 -0
  31. package/runtime/sdk/gl/CharacterController.ts +184 -55
  32. package/runtime/sdk/gl/Gearbox.ts +212 -0
  33. package/runtime/sdk/gl/Geometry.ts +70 -9
  34. package/runtime/sdk/gl/IK.ts +193 -174
  35. package/runtime/sdk/gl/Light.ts +64 -2
  36. package/runtime/sdk/gl/Lightmap.ts +179 -0
  37. package/runtime/sdk/gl/Material.ts +36 -0
  38. package/runtime/sdk/gl/Mesh.ts +6 -23
  39. package/runtime/sdk/gl/Model.ts +23 -8
  40. package/runtime/sdk/gl/Node.ts +350 -285
  41. package/runtime/sdk/gl/Physics.ts +222 -126
  42. package/runtime/sdk/gl/Scene.ts +175 -8
  43. package/runtime/sdk/gl/Shape.ts +255 -12
  44. package/runtime/sdk/gl/Trigger.ts +1 -6
  45. package/runtime/sdk/gl/Vehicle.ts +473 -0
  46. package/runtime/sdk/gl/Wheel.ts +240 -0
  47. package/runtime/sdk/gl/{AnimationClip.ts → animation/AnimationClip.ts} +37 -7
  48. package/runtime/sdk/gl/animation/Animator.ts +87 -0
  49. package/runtime/sdk/gl/animation/Layer.ts +29 -0
  50. package/runtime/sdk/gl/animation/Loop.ts +25 -0
  51. package/runtime/sdk/gl/animation/Playback.ts +43 -0
  52. package/runtime/sdk/gl/animation/core.ts +294 -0
  53. package/runtime/sdk/gl/scenarios.ts +291 -349
  54. package/runtime/sdk/inject.ts +186 -162
  55. package/runtime/sdk/runtime/app.ts +13 -0
  56. package/runtime/sdk/runtime/input.ts +169 -6
  57. package/runtime/sdk/scene/defineScene.ts +1227 -1016
  58. package/runtime/sdk/scene/gizmos.ts +148 -0
  59. package/runtime/sdk/scene/material.ts +188 -0
  60. package/runtime/sdk-types.json +1 -1
  61. package/runtime/sdk/gl/Animator.ts +0 -642
  62. package/runtime/sdk/gl/ModelAnimation.ts +0 -95
@@ -0,0 +1,148 @@
1
+ // Editor gizmos: immediate-mode line drawing for code that runs while a scene is EDITED — a
2
+ // generator aspect's `rebuild()`, a `make()` factory, an editor tool's hooks. Nothing here touches
3
+ // the scene: calls accumulate WORLD-space line segments into the buffer of the run that is
4
+ // currently executing (an ambient scope the scene loader opens around each synchronous call), and
5
+ // the scene editor pushes those buffers to the lite engine's overlay layer, which draws them over
6
+ // the picture — never outlined, never picked, never rendered by Filament, absent in play mode.
7
+ //
8
+ // rebuild() {
9
+ // Gizmos.polyline(points, { color: "#5b8ef0", closed: true })
10
+ // for (const p of points) Gizmos.cross(p, 0.07)
11
+ // }
12
+ //
13
+ // Every run starts with an EMPTY buffer — what a call draws is the whole picture (like
14
+ // `generated.clear()`, only implicit). Outside any scope (play mode, hand-attached aspects, a call
15
+ // after an `await` inside rebuild) the calls are no-ops: rebuild is synchronous by contract.
16
+ //
17
+ // ANCHORED gizmos (`{ node }` in the style): points are in that node's LOCAL frame and the engine
18
+ // follows the node live — through a gizmo drag, not only after it — with the node's scale
19
+ // stripped (an empty scaled ×10 for its children keeps a normal-sized cross). An anchored picture
20
+ // is the node's own: clicking it in the viewport selects the node, and it draws in the selection
21
+ // accent while selected. World-space calls (path lines) are never pickable.
22
+
23
+ import { Mat4, type Mat4Like } from "../math/mat4"
24
+ import type { Vec3Like } from "../math/vec"
25
+
26
+ /** The anchor of a gizmo call: any object with an engine entity id (a `Node`). */
27
+ export type GizmoAnchor = { readonly id: number }
28
+
29
+ export type GizmoStyle = {
30
+ /** CSS hex color (`#rgb` / `#rrggbb`); default a neutral light gray. */
31
+ color?: string
32
+ /** 0..1, default 1. */
33
+ alpha?: number
34
+ /** Anchor: points are in this node's LOCAL frame (scale ignored); the lines follow the node
35
+ * live, are pickable (a click selects the node) and turn the selection accent when it is
36
+ * selected. Omit for world-space lines (not pickable). */
37
+ node?: GizmoAnchor | null
38
+ }
39
+
40
+ /** One color batch of LINES (flat `[x,y,z, x,y,z]` per segment) — what a host hands the engine.
41
+ * With `entityId` the segments are in that entity's local frame (see GizmoStyle.node). */
42
+ export type GizmoBatch = { color: string, alpha: number, segments: number[], entityId?: number }
43
+
44
+ const DEFAULT_COLOR = "#cfd4dd"
45
+
46
+ /** The per-run collector (owned by the scene loader's EditorRun). Batches are keyed by style so a
47
+ * hundred same-colored segments cost one draw. */
48
+ export class GizmoBuffer {
49
+ batches: GizmoBatch[] = []
50
+ private _byStyle = new Map<string, GizmoBatch>()
51
+
52
+ clear(): void {
53
+ this.batches = []
54
+ this._byStyle.clear()
55
+ }
56
+
57
+ segments(style: GizmoStyle | undefined): number[] {
58
+ const color = style?.color ?? DEFAULT_COLOR
59
+ const alpha = Math.max(0, Math.min(1, style?.alpha ?? 1))
60
+ const entityId = style?.node?.id
61
+ const key = `${color}@${alpha}@${entityId ?? ""}`
62
+ let b = this._byStyle.get(key)
63
+ if (!b) {
64
+ b = entityId ? { color, alpha, segments: [], entityId } : { color, alpha, segments: [] }
65
+ this._byStyle.set(key, b)
66
+ this.batches.push(b)
67
+ }
68
+ return b.segments
69
+ }
70
+ }
71
+
72
+ let current: GizmoBuffer | null = null
73
+ let warnedOutOfScope = false
74
+
75
+ const xyz = (p: Vec3Like): [number, number, number] =>
76
+ Array.isArray(p) ? [ p[0] ?? 0, p[1] ?? 0, p[2] ?? 0 ] : [ (p as { x: number }).x, (p as { y: number }).y, (p as { z: number }).z ]
77
+
78
+ const target = (style: GizmoStyle | undefined): number[] | null => {
79
+ if (current) return current.segments(style)
80
+ if (!warnedOutOfScope && (globalThis as { __lecodesSceneEdit?: boolean }).__lecodesSceneEdit) {
81
+ warnedOutOfScope = true
82
+ console.warn("[Gizmos] draw call outside an editor run — gizmos must be drawn synchronously inside rebuild() / a make() factory / a tool hook")
83
+ }
84
+ return null
85
+ }
86
+
87
+ /** @internal Run `fn` with `buffer` as the ambient gizmo target (cleared first). Nested scopes
88
+ * restore the outer one. */
89
+ export const withGizmoScope = <T>(buffer: GizmoBuffer, fn: () => T): T => {
90
+ const prev = current
91
+ buffer.clear()
92
+ current = buffer
93
+ try { return fn() } finally { current = prev }
94
+ }
95
+
96
+ /**
97
+ * Editor-only line drawing, available inside generator `rebuild()`, `make()` factories and editor
98
+ * tool hooks. Points are WORLD space. Drawn by the scene editor's viewport overlay (depth-test
99
+ * off — a path through a wall is still a path); invisible everywhere else.
100
+ */
101
+ export const Gizmos = {
102
+ /** One segment from `a` to `b`. */
103
+ line(a: Vec3Like, b: Vec3Like, style?: GizmoStyle): void {
104
+ const out = target(style)
105
+ if (!out) return
106
+ out.push(...xyz(a), ...xyz(b))
107
+ },
108
+
109
+ /** Consecutive segments through `points`; `closed` joins the last point back to the first. */
110
+ polyline(points: readonly Vec3Like[], style?: GizmoStyle & { closed?: boolean }): void {
111
+ if (points.length < 2) return
112
+ const out = target(style)
113
+ if (!out) return
114
+ const n = points.length
115
+ const last = style?.closed ? n : n - 1
116
+ for (let i = 0; i < last; i++) out.push(...xyz(points[i]!), ...xyz(points[(i + 1) % n]!))
117
+ },
118
+
119
+ /** A wireframe camera frustum looking down −Z: the four edges from the origin to a rect
120
+ * `length` metres ahead sized by `fov` (vertical, degrees) × `aspect` (default 16:9), plus an
121
+ * "up" fin above the rect. Anchor it (`{ node }`) for a camera node's marker — the frustum
122
+ * then follows the node's pose; `matrix` places an unanchored one in world space. */
123
+ frustum(fov: number, style?: GizmoStyle & { aspect?: number, length?: number, matrix?: Mat4Like }): void {
124
+ const out = target(style)
125
+ if (!out) return
126
+ const aspect = style?.aspect ?? 16 / 9, length = style?.length ?? 0.6
127
+ const m = style?.matrix ? new Mat4(style.matrix) : null
128
+ const h = Math.tan((fov * Math.PI) / 360) * length, w = h * aspect, z = -length
129
+ const P = (x: number, y: number, zz: number): [number, number, number] => {
130
+ if (!m) return [ x, y, zz ]
131
+ const v = m.transformPoint([ x, y, zz ])
132
+ return [ v.x, v.y, v.z ]
133
+ }
134
+ const o = P(0, 0, 0)
135
+ const c = [ P(-w, -h, z), P(w, -h, z), P(w, h, z), P(-w, h, z) ]
136
+ for (const q of c) out.push(...o, ...q)
137
+ for (let i = 0; i < 4; i++) out.push(...c[i]!, ...c[(i + 1) % 4]!)
138
+ out.push(...P(-w * 0.4, h, z), ...P(0, h * 1.6, z), ...P(0, h * 1.6, z), ...P(w * 0.4, h, z))
139
+ },
140
+
141
+ /** A three-axis cross centred on `p` (a point marker); `size` = half extent, default 0.1. */
142
+ cross(p: Vec3Like, size = 0.1, style?: GizmoStyle): void {
143
+ const out = target(style)
144
+ if (!out) return
145
+ const [ x, y, z ] = xyz(p)
146
+ out.push(x - size, y, z, x + size, y, z, x, y - size, z, x, y + size, z, x, y, z - size, x, y, z + size)
147
+ },
148
+ }
@@ -0,0 +1,188 @@
1
+ // Materials as data — the `material:` grammar of scene files and the `defineMaterial` document
2
+ // behind `*.material.ts` assets (docs/scene-materials-plan.md).
3
+ //
4
+ // // materials/holo-glass.material.ts
5
+ // export default defineMaterial({
6
+ // shader: asset('../assets/shaders/lens.mat'),
7
+ // params: { tint: '#1d2b26', opacity: 0.22, mask: asset('../assets/weapons/reticle.png') },
8
+ // })
9
+ //
10
+ // // range.scene.ts
11
+ // import holoGlass from './materials/holo-glass.material'
12
+ // lens: { mesh: { kind: 'plane' }, material: holoGlass },
13
+ //
14
+ // One grammar serves the inline form (`material: { lit: {…} }` / `{ shader, params }` on a node),
15
+ // the asset form (the handle) and GLB-part overrides. A handle is ONE shared GPU material: every
16
+ // node using the asset renders the same instance, and editing the asset changes all of them
17
+ // (Unity's rule — two looks are two assets).
18
+ //
19
+ // Parameter values need no shader metadata at runtime: a string is a colour when it starts with
20
+ // `#`, otherwise it is a texture URL (`asset('./x.png')` compiles to one) — the same lowering
21
+ // `Material.set` already applies to colours.
22
+
23
+ import type { ColorInput } from "../core/color"
24
+ import { Material, type LitMaterialOptions, type MaterialColorOptions } from "../gl/Material"
25
+ import { Texture } from "../gl/Texture"
26
+ import type { Node } from "../gl/Node"
27
+ import { EDIT_FLAG } from "./grammar"
28
+
29
+ /** A shader parameter value as scene files write it: number, boolean, `#colour`, texture URL
30
+ * (`asset('./x.png')`), or a numeric vector. */
31
+ export type MaterialParamValue = number | boolean | string | number[]
32
+
33
+ type WithMapUrl<T> = Omit<T, "map"> & {
34
+ /** Base-colour texture — a `Texture`, a `Canvas`, or `asset('./x.png')` (a URL string). */
35
+ map?: LitMaterialOptions["map"] | string
36
+ }
37
+
38
+ export type LitMaterialDef = WithMapUrl<LitMaterialOptions>
39
+ export type UnlitMaterialDef = WithMapUrl<MaterialColorOptions>
40
+
41
+ /** A custom Filament shader (`asset('./x.mat')` — compiled by the platform / `lecodes shaders`)
42
+ * plus its parameter values. */
43
+ export type ShaderMaterialDef = {
44
+ shader: string
45
+ params?: Record<string, MaterialParamValue>
46
+ }
47
+
48
+ export type MaterialDef =
49
+ | { lit: LitMaterialDef }
50
+ | { unlit: UnlitMaterialDef }
51
+ | { shadow: ColorInput }
52
+ | ShaderMaterialDef
53
+ /** A material asset (`import m from './x.material'`) — one shared instance. */
54
+ | MaterialHandle
55
+ /** A code-level Material instance — valid at runtime; the editor shows it read-only. */
56
+ | Material
57
+
58
+ /** The data forms of MaterialDef (everything but a live instance / handle). */
59
+ export type MaterialData = Exclude<MaterialDef, MaterialHandle | Material>
60
+
61
+ const isTextureUrl = (v: unknown): v is string => typeof v === "string" && !v.startsWith("#")
62
+
63
+ /** Push one parameter into a live material. Textures load asynchronously — the returned promise
64
+ * settles once the value is applied (immediately for scalars). */
65
+ export const applyMaterialParam = (material: Material, name: string, value: MaterialParamValue | null): Promise<void> => {
66
+ if (isTextureUrl(value)) {
67
+ return Texture.load(value).then((tex) => { material.set(name, tex) }, (e) => {
68
+ console.warn(`[material] "${name}": ${e instanceof Error ? e.message : String(e)}`)
69
+ })
70
+ }
71
+ material.set(name, value as never)
72
+ return Promise.resolve()
73
+ }
74
+
75
+ /** Build a live Material from a data def. Resolves once every texture it names has loaded. */
76
+ export const createMaterialFromData = async (def: MaterialData): Promise<Material> => {
77
+ if ("shader" in def) {
78
+ const m = await Material.load(def.shader)
79
+ await Promise.all(Object.entries(def.params ?? {}).map(([ k, v ]) => applyMaterialParam(m, k, v)))
80
+ return m
81
+ }
82
+ if ("shadow" in def) return Material.shadow(def.shadow)
83
+ const lit = "lit" in def
84
+ const opts = lit ? def.lit : def.unlit
85
+ const { map, ...rest } = opts
86
+ const m = lit ? Material.lit(rest as LitMaterialOptions) : Material.unlit(rest as MaterialColorOptions)
87
+ if (isTextureUrl(map)) m.map = await Texture.load(map)
88
+ else if (map !== undefined) m.map = map as LitMaterialOptions["map"] as Texture | null
89
+ return m
90
+ }
91
+
92
+ /**
93
+ * A material asset: the default export of a `*.material.ts` file. `load()` yields ONE shared
94
+ * `Material` (cached); scene files hand the handle to `material:` and the loader resolves it.
95
+ * The editor edits the asset live through `setParam` / `reset` — every node the handle was
96
+ * applied to follows (`_users`).
97
+ */
98
+ export class MaterialHandle {
99
+ def: MaterialData
100
+ private _instance: Material | null = null
101
+ private _loading: Promise<Material> | null = null
102
+ /** Every (node, slot) this handle was applied to through a scene — the editor re-assigns them
103
+ * when the asset's shader changes. */
104
+ readonly _users = new Set<{ node: Node, slot: number }>()
105
+
106
+ constructor(def: MaterialData) {
107
+ this.def = def
108
+ }
109
+
110
+ /** The shared material instance (built on first use). */
111
+ load(): Promise<Material> {
112
+ if (this._instance) return Promise.resolve(this._instance)
113
+ this._loading ??= createMaterialFromData(this.def).then((m) => { this._instance = m; return m })
114
+ return this._loading
115
+ }
116
+
117
+ /** The live instance, if built yet. */
118
+ get material(): Material | null { return this._instance }
119
+
120
+ /** Editor: assign this material to `node`'s `slot`, remembering the user. */
121
+ async _applyTo(node: Node, slot: number): Promise<Material> {
122
+ const m = await this.load()
123
+ node.setMaterial(m, slot)
124
+ this._users.add({ node, slot })
125
+ return m
126
+ }
127
+
128
+ /** Editor: one parameter value, applied live to the shared instance (a texture loads in). */
129
+ setParam(name: string, value: MaterialParamValue | null): void {
130
+ const def = this.def
131
+ if ("shader" in def) {
132
+ def.params ??= {}
133
+ if (value === null) delete def.params[name]
134
+ else def.params[name] = value
135
+ } else if ("lit" in def || "unlit" in def) {
136
+ const opts = ("lit" in def ? def.lit : def.unlit) as Record<string, unknown>
137
+ if (value === null) delete opts[name]
138
+ else opts[name] = value
139
+ }
140
+ const m = this._instance
141
+ if (!m) return
142
+ if (!("shader" in def) && name === "color") { m.color = value as string; return }
143
+ if (!("shader" in def) && name === "map") {
144
+ if (isTextureUrl(value)) void Texture.load(value).then((t) => { m.map = t })
145
+ else m.map = null
146
+ return
147
+ }
148
+ void applyMaterialParam(m, name, value)
149
+ }
150
+
151
+ /** Editor: replace the whole definition (a shader/kind switch) — rebuilds the instance and
152
+ * re-assigns every user. */
153
+ async reset(def: MaterialData): Promise<Material> {
154
+ this.def = def
155
+ this._instance = null
156
+ this._loading = null
157
+ const m = await this.load()
158
+ for (const u of this._users) u.node.setMaterial(m, u.slot)
159
+ return m
160
+ }
161
+ }
162
+
163
+ /** Resolve any `material:` value to a live Material (a handle's shared instance, a data def's
164
+ * fresh one, a code instance as is). */
165
+ export const resolveMaterialDef = (def: MaterialDef): Promise<Material> => {
166
+ if (def instanceof Material) return Promise.resolve(def)
167
+ if (def instanceof MaterialHandle) return def.load()
168
+ return createMaterialFromData(def)
169
+ }
170
+
171
+ /** Resolve and assign a `material:` value to one slot of a node (tracking handle users). */
172
+ export const assignMaterialDef = async (node: Node, slot: number, def: MaterialDef): Promise<Material> => {
173
+ if (def instanceof MaterialHandle) return def._applyTo(node, slot)
174
+ const m = await resolveMaterialDef(def)
175
+ node.setMaterial(m, slot)
176
+ return m
177
+ }
178
+
179
+ /**
180
+ * Define a material as data — the default export of a `*.material.ts` file. Import it into scene
181
+ * files (`material: handle`) or code (`const m = await handle.load()`).
182
+ */
183
+ export const defineMaterial = (def: MaterialData): MaterialHandle => {
184
+ const handle = new MaterialHandle(def)
185
+ const g = globalThis as unknown as { [EDIT_FLAG]?: boolean, __lecodesMaterialHandles?: MaterialHandle[] }
186
+ if (g[EDIT_FLAG]) (g.__lecodesMaterialHandles ??= []).push(handle)
187
+ return handle
188
+ }