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,294 @@
1
+ // The engine-facing core behind Animator / Layer / Blend / Playback (docs/animator-plan.md). NOT exported
2
+ // from the SDK: the public classes are thin views over the records here. The engine runs the whole
3
+ // per-frame loop (fades, blend spaces, one-shot fade-back, root motion); this file binds clips,
4
+ // issues play/stop/blend calls and routes the engine's slot events back to playbacks.
5
+
6
+ import { AnimationClip } from "./AnimationClip"
7
+ import { Loop } from "./Loop"
8
+ import { Layer } from "./Layer"
9
+ import { Playback } from "./Playback"
10
+ import type { Node } from "../Node"
11
+
12
+ /** A position on a blend axis (1D) or plane (2D). */
13
+ export type BlendPosition = number | readonly [number, number]
14
+
15
+ export type PlayOptions = {
16
+ /** Crossfade seconds for BOTH ends (default 0 = cut). */
17
+ fade?: number
18
+ /** Fade-in seconds (overrides `fade`). */
19
+ fadeIn?: number
20
+ /** One-shots: fade-back seconds at the END (overrides `fade`). The return fade starts this long
21
+ * BEFORE the clip ends, while it still plays — no freeze-frame on the last pose. On a layer with
22
+ * no blend to return to, giving `fadeOut` releases the layer at the end instead of holding the
23
+ * last frame. */
24
+ fadeOut?: number
25
+ /** Playback rate for this clip (default 1). */
26
+ speed?: number
27
+ /** Rewind even if the clip is already playing (default: rewind only if it isn't). */
28
+ restart?: boolean
29
+ }
30
+
31
+ export type StopOptions = { fade?: number }
32
+
33
+ export type LoopOptions = {
34
+ /** Crossfade seconds from whatever the layer shows — the previous loop, or a one-shot still playing
35
+ * (default 0 = cut). */
36
+ fade?: number
37
+ /** Playback rate of the loop's clips (default 1). */
38
+ speed?: number
39
+ }
40
+ /** What a layer loops: one clip (name / index), or a blend space `{ name: position }`. */
41
+ export type LoopDef = string | number | Record<string, BlendPosition>
42
+
43
+ export type LayerOptions = {
44
+ /** Bone name(s): the layer drives only these subtrees (`'Spine1'` = upper body). Default: the whole rig. */
45
+ mask?: string | string[]
46
+ /** Blend each clip's DELTA from its own first frame on top of the layers below (lean, breathe, recoil)
47
+ * instead of overriding them. */
48
+ additive?: boolean
49
+ /** Layer contribution 0–1 (default 1). */
50
+ weight?: number
51
+ }
52
+
53
+ export type ClipEventHandler = (clip: string, layer: Layer) => void
54
+
55
+ // ---- records -------------------------------------------------------------------------------------
56
+
57
+ export type SlotRec = { name: string, clip: AnimationClip, slot: number, layer: LayerRec, inBlend: boolean, playback?: PlaybackRec }
58
+ export type PlaybackRec = { slot: SlotRec, playing: boolean, done: Promise<boolean>, resolve: (natural: boolean) => void }
59
+ export type LayerRec = { index: number, view: Layer, slots: SlotRec[], byName: Map<string, SlotRec>, loop?: LoopRec, weight: number, mask: string, additive: boolean }
60
+ export type LoopRec = { layer: LayerRec, members: { name: string, slot: SlotRec }[], value: BlendPosition, view: Loop, dead: boolean }
61
+
62
+ const handlers = new Map<number, (slot: number, type: number) => void>()
63
+ let eventsInstalled = false
64
+ const ensureEvents = (): void => {
65
+ if (eventsInstalled) return
66
+ eventsInstalled = true
67
+ _creator.setOnAnimatorEvent((animatorId, slot, type) => handlers.get(animatorId)?.(slot, type))
68
+ }
69
+
70
+ const settled = (slot: SlotRec): PlaybackRec => {
71
+ let resolve = (_n: boolean): void => {}
72
+ const done = new Promise<boolean>((r) => { resolve = r })
73
+ resolve(false)
74
+ return { slot, playing: false, done, resolve }
75
+ }
76
+
77
+ export class Core {
78
+ id = 0
79
+ /** Added clips by name (`addClip`) — the lookup index; an added name overrides an embedded one. */
80
+ readonly table = new Map<string, AnimationClip>()
81
+ readonly layers: LayerRec[] = []
82
+ speed = 1
83
+ private _embedded?: AnimationClip[]
84
+ private readonly _added: { name: string, clip: AnimationClip }[] = []
85
+ private _list?: AnimationClip[]
86
+ private readonly _listeners = new Map<string, Set<ClipEventHandler>>()
87
+
88
+ readonly node: Node
89
+ constructor(node: Node) { this.node = node; this.layers.push(this.newLayer()) }
90
+
91
+ // ---- clips ----
92
+ embedded(): AnimationClip[] {
93
+ if (!this._embedded) {
94
+ this._embedded = AnimationClip._ofSet(_creator.getGlbClipSet(this.node.id))
95
+ const seen = new Set<string>()
96
+ for (const c of this._embedded) {
97
+ if (seen.has(c.name)) console.warn(`Animator: ${this.node.name || "the node"} has several clips named '${c.name}' — by name you get the first; reach the others by index (anim.clip(n))`)
98
+ seen.add(c.name)
99
+ }
100
+ }
101
+ return this._embedded
102
+ }
103
+ /** The ordered clip list: the GLB's clips in file order (an added clip of the same name takes the
104
+ * embedded one's place), then the remaining added clips in insertion order. Cached until addClip. */
105
+ clips(): AnimationClip[] {
106
+ if (this._list) return this._list
107
+ const out: AnimationClip[] = []
108
+ const placed = new Set<string>()
109
+ for (const c of this.embedded()) {
110
+ const override = this.table.get(c.name)
111
+ if (override && !placed.has(c.name)) { out.push(override); placed.add(c.name) }
112
+ else out.push(c)
113
+ }
114
+ for (const a of this._added) if (!placed.has(a.name)) { out.push(a.clip); placed.add(a.name) }
115
+ return this._list = out
116
+ }
117
+ addClip(name: string, clip: AnimationClip): void {
118
+ const i = this._added.findIndex((a) => a.name === name)
119
+ if (i >= 0) this._added[i] = { name, clip }
120
+ else this._added.push({ name, clip })
121
+ this.table.set(name, clip)
122
+ this._list = undefined
123
+ }
124
+ /** A clip by name (added first, then embedded), or by index into `clips()` (none = the first). */
125
+ resolveClip(ref: string | number | undefined): [string, AnimationClip] | undefined {
126
+ if (ref === undefined || typeof ref === "number") { const c = this.clips()[ref ?? 0]; return c ? [ c.name, c ] : undefined }
127
+ const c = this.table.get(ref) ?? this.embedded().find((e) => e.name === ref)
128
+ return c ? [ ref, c ] : undefined
129
+ }
130
+ private clipNames(): string { return this.clips().map((c) => c.name).join(", ") || "(empty)" }
131
+
132
+ // ---- lifecycle ----
133
+ /** Create the native animator on demand. False = nothing to animate (warned). */
134
+ ensure(): boolean {
135
+ if (this.id) return true
136
+ if (this.embedded().length === 0 && this.table.size === 0) { console.warn(`Animator: no clips on ${this.node.name || "the node"} — nothing to play`); return false }
137
+ ensureEvents()
138
+ this.id = _creator.animatorCreate(this.node.id)
139
+ if (!this.id) { console.warn("Animator: node has no transform hierarchy"); return false }
140
+ handlers.set(this.id, (slot, type) => this.onEvent(slot, type))
141
+ if (this.speed !== 1) _creator.animatorSetGlobal(this.id, this.speed, false)
142
+ for (const L of this.layers) if (L.index > 0) this.pushLayer(L)
143
+ return true
144
+ }
145
+ destroy(): void {
146
+ for (const L of this.layers) for (const s of L.slots) if (s.playback) this.settle(s.playback, false)
147
+ if (this.id) { _creator.animatorDestroy(this.id); handlers.delete(this.id); this.id = 0 }
148
+ for (const L of this.layers) { L.slots = []; L.byName.clear(); if (L.loop) { L.loop.dead = true; L.loop = undefined } }
149
+ }
150
+
151
+ // ---- layers ----
152
+ newLayer(options?: LayerOptions): LayerRec {
153
+ const rec: LayerRec = { index: this.layers.length, view: undefined as unknown as Layer, slots: [], byName: new Map(), weight: options?.weight ?? 1, mask: "", additive: options?.additive ?? false }
154
+ if (options?.mask !== undefined) rec.mask = Array.isArray(options.mask) ? options.mask.join("\n") : options.mask
155
+ rec.view = new Layer(rec, this)
156
+ return rec
157
+ }
158
+ addLayer(options: LayerOptions): Layer {
159
+ if (this.layers.length >= 255) { console.warn("Animator: too many layers"); return this.layers[this.layers.length - 1].view }
160
+ const rec = this.newLayer(options)
161
+ this.layers.push(rec)
162
+ if (this.id) this.pushLayer(rec)
163
+ return rec.view
164
+ }
165
+ pushLayer(L: LayerRec): void {
166
+ if (!this.id) return
167
+ for (const m of L.mask.split("\n")) if (m && !this.node.bone(m)) console.warn(`Animator: layer mask bone '${m}' not found under ${this.node.name || "the node"}`)
168
+ _creator.animatorSetLayer(this.id, L.index, L.weight, L.additive, L.mask)
169
+ }
170
+
171
+ // ---- slots ----
172
+ /** Bind a clip on a layer (once per name). */
173
+ slotFor(L: LayerRec, name: string, clip: AnimationClip): SlotRec | undefined {
174
+ const have = L.byName.get(name)
175
+ if (have && have.clip === clip) return have
176
+ const slot = _creator.animatorBind(this.id, clip._set, clip._index, L.index)
177
+ if (slot < 0) { console.warn(`Animator: could not bind clip '${name}'`); return undefined }
178
+ const bound = _creator.animatorBoundTracks(this.id, slot)
179
+ if (clip.trackCount > 0 && bound === 0) console.warn(`Animator: '${name}' — no track matched a node name under ${this.node.name || "the node"} (different rig?)`)
180
+ else if (bound < clip.trackCount) console.warn(`Animator: '${name}' — ${clip.trackCount - bound}/${clip.trackCount} tracks unbound (bone names not found)`)
181
+ const rec: SlotRec = { name, clip, slot, layer: L, inBlend: false }
182
+ L.slots.push(rec)
183
+ L.byName.set(name, rec)
184
+ return rec
185
+ }
186
+
187
+ // ---- play / stop ----
188
+ play(L: LayerRec, ref: string | number | undefined, o: PlayOptions): Playback {
189
+ const found = this.resolveClip(ref)
190
+ if (!found) {
191
+ console.warn(`Animator: no clip ${ref === undefined ? "to play" : JSON.stringify(ref)} on ${this.node.name || "the node"} — table: ${this.clipNames()}`)
192
+ return new Playback(settled({ name: "", clip: AnimationClip._none(), slot: -1, layer: L, inBlend: false }), this)
193
+ }
194
+ const [name, clip] = found
195
+ if (!this.ensure()) return new Playback(settled({ name, clip, slot: -1, layer: L, inBlend: false }), this)
196
+ const s = this.slotFor(L, name, clip)
197
+ if (!s) return new Playback(settled({ name, clip, slot: -1, layer: L, inBlend: false }), this)
198
+ if (s.inBlend) { console.warn(`Animator: '${name}' is part of this layer's loop — use playLoop() to change what it rests on`); return new Playback(settled(s), this) }
199
+ const fadeIn = o.fadeIn ?? o.fade ?? 0
200
+ const fadeOut = o.fadeOut ?? o.fade ?? -1
201
+ // the other one-shots on this layer are cut short (their promises resolve false)
202
+ for (const other of L.slots) if (other.playback) this.settle(other.playback, false)
203
+ let resolve = (_n: boolean): void => {}
204
+ const done = new Promise<boolean>((r) => { resolve = r })
205
+ const rec: PlaybackRec = { slot: s, playing: true, done, resolve }
206
+ s.playback = rec
207
+ _creator.animatorPlay(this.id, s.slot, false, o.speed ?? 1, fadeIn, fadeOut, o.restart ?? false)
208
+ return new Playback(rec, this)
209
+ }
210
+ /** A one-shot on the layer hasn't handed over yet. */
211
+ busy(L: LayerRec): boolean { return L.slots.some((s) => s.playback?.playing) }
212
+ settle(p: PlaybackRec, natural: boolean): void {
213
+ p.playing = false
214
+ if (p.slot.playback === p) p.slot.playback = undefined
215
+ p.resolve(natural)
216
+ }
217
+ stopSlot(s: SlotRec, fade: number): void {
218
+ if (s.playback) this.settle(s.playback, false)
219
+ if (this.id && s.slot >= 0) _creator.animatorStop(this.id, s.layer.index, s.slot, fade)
220
+ }
221
+ stopLayer(L: LayerRec, fade: number): void {
222
+ for (const s of L.slots) if (s.playback) this.settle(s.playback, false)
223
+ if (L.loop) { L.loop.dead = true; L.loop = undefined }
224
+ if (this.id) _creator.animatorStop(this.id, L.index, -1, fade)
225
+ }
226
+ stopAll(fade: number): void { for (const L of this.layers) this.stopLayer(L, fade) }
227
+
228
+ // ---- loop ----
229
+ loop(L: LayerRec, def: LoopDef, o: LoopOptions): Loop | undefined {
230
+ if (!this.ensure()) return undefined
231
+ const fade = o.fade ?? 0
232
+ if (L.loop) { L.loop.dead = true; for (const m of L.loop.members) m.slot.inBlend = false; L.loop = undefined }
233
+ for (const s of L.slots) if (s.playback) this.settle(s.playback, false) // one-shots on the layer are cut short
234
+ const space: Record<string, BlendPosition> = typeof def === "object" ? def : {}
235
+ if (typeof def !== "object") {
236
+ const found = this.resolveClip(def)
237
+ if (!found) { console.warn(`Animator: no clip ${JSON.stringify(def)} to loop — table: ${this.clipNames()}`); return undefined }
238
+ space[found[0]] = 0
239
+ }
240
+ const rec: LoopRec = { layer: L, members: [], value: 0, view: undefined as unknown as Loop, dead: false }
241
+ rec.view = new Loop(rec, this)
242
+ const slots: number[] = [], positions: number[] = []
243
+ const is2D = Object.values(space).some((p) => typeof p !== "number")
244
+ for (const [name, at] of Object.entries(space)) {
245
+ const found = this.resolveClip(name)
246
+ if (!found) { console.warn(`Animator: loop clip '${name}' is not in the clip table`); continue }
247
+ const s = this.slotFor(L, name, found[1])
248
+ if (!s) continue
249
+ if (s.playback) this.settle(s.playback, false)
250
+ s.inBlend = true
251
+ rec.members.push({ name, slot: s })
252
+ slots.push(s.slot)
253
+ if (is2D) positions.push(typeof at === "number" ? at : at[0], typeof at === "number" ? 0 : at[1])
254
+ else positions.push(at as number)
255
+ }
256
+ L.loop = rec
257
+ _creator.animatorSetBlend(this.id, L.index, is2D ? 2 : 1, new Uint16Array(slots), new Float32Array(positions), fade, o.speed ?? 1)
258
+ return rec.view
259
+ }
260
+ setLoopValue(b: LoopRec, v: BlendPosition): void {
261
+ if (b.dead) { console.warn("Animator: this loop was replaced — use the object the latest playLoop() returned"); return }
262
+ b.value = v
263
+ if (!this.id) return
264
+ const [x, y] = typeof v === "number" ? [ v, 0 ] : [ v[0], v[1] ]
265
+ _creator.animatorSetBlendValue(this.id, b.layer.index, x, y)
266
+ }
267
+ slotWeight(s: SlotRec): number { return this.id && s.slot >= 0 ? _creator.animatorGetSlotWeight(this.id, s.slot) : 0 }
268
+ slotTime(s: SlotRec): number { return this.id && s.slot >= 0 ? _creator.animatorGetSlotTime(this.id, s.slot) : 0 }
269
+ seek(s: SlotRec, time: number): void { if (this.id && s.slot >= 0) _creator.animatorSeek(this.id, s.slot, time) }
270
+
271
+ // ---- props ----
272
+ setSpeed(v: number): void { this.speed = v; if (this.id) _creator.animatorSetGlobal(this.id, v, false) }
273
+ setRootMotion(on: boolean): void { if (this.ensure()) _creator.animatorSetRootMotion(this.id, on ? "*" : "", on ? 2 : 0) }
274
+
275
+ // ---- events ----
276
+ on(event: string, cb: ClipEventHandler): void { (this._listeners.get(event) ?? this._listeners.set(event, new Set()).get(event)!).add(cb) }
277
+ off(event: string, cb: ClipEventHandler): void { this._listeners.get(event)?.delete(cb) }
278
+ /** Native slot event: 0 completed / 1 loop / 2 settled (faded to 0) / 3 hand-over / 4+i clip event i. */
279
+ private onEvent(slotIndex: number, type: number): void {
280
+ let s: SlotRec | undefined
281
+ for (const L of this.layers) { s = L.slots.find((x) => x.slot === slotIndex); if (s) break }
282
+ if (!s) return
283
+ if (type >= 4) {
284
+ const name = s.clip._events[type - 4]?.name
285
+ const cbs = name ? this._listeners.get(name) : undefined
286
+ if (cbs) for (const cb of [ ...cbs ]) cb(s.name, s.layer.view)
287
+ return
288
+ }
289
+ const p = s.playback
290
+ if (!p) return
291
+ if (type === 3) this.settle(p, true) // hand-over: the clip is over; the app's next call redirects the fade
292
+ else if (type === 2) this.settle(p, false) // faded out after a stop / replacement before handing over
293
+ }
294
+ }