lecodes-sdk 0.19.2 → 0.20.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/global.d.ts +31 -0
- package/dist/host.d.ts +3 -0
- package/dist/inject.js +260 -361
- package/dist/types/gl/Locomotion.d.ts +3 -1
- package/dist/types/gl/animation/Animator.d.ts +51 -183
- package/dist/types/gl/animation/Feet.d.ts +85 -0
- package/dist/types/gl/animation/Warp.d.ts +53 -0
- package/dist/types/gl/animation/core.d.ts +46 -7
- package/dist/types/inject.d.ts +7 -2
- package/dist/types/plugins/map.d.ts +174 -0
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/dist/2d-game.md +197 -408
- package/prompts/dist/3d-app.md +166 -491
- package/prompts/dist/ar-app.md +163 -373
- package/prompts/dist/design.md +87 -83
- package/prompts/dist/ui-app.md +136 -325
- package/src/bridges.d.ts +13 -6
- package/src/compile/__tests__/compile.test.ts +12 -0
- package/src/compile/compileProject.ts +5 -0
- package/src/core/Aspect.ts +1 -1
- package/src/gl/CameraPlace.ts +52 -52
- package/src/gl/Locomotion.ts +7 -5
- package/src/gl/Particles.ts +3 -3
- package/src/gl/Ragdoll.ts +270 -270
- package/src/gl/Scene.ts +1 -1
- package/src/gl/Trigger.ts +45 -45
- package/src/gl/animation/Animator.ts +137 -329
- package/src/gl/animation/Feet.ts +134 -0
- package/src/gl/animation/Loop.ts +3 -1
- package/src/gl/animation/Warp.ts +96 -0
- package/src/gl/animation/core.ts +736 -670
- package/src/gl/scenarios.ts +291 -291
- package/src/gl/state.ts +6 -6
- package/src/host.d.ts +3 -0
- package/src/inject.ts +11 -2
- package/src/plugins/map.ts +396 -0
- package/src/scene/gizmos.ts +148 -148
package/src/bridges.d.ts
CHANGED
|
@@ -251,11 +251,13 @@ declare global {
|
|
|
251
251
|
animatorBind(animatorId: number, clipSetId: number, clipIndex: number, layer: number): number
|
|
252
252
|
animatorBoundTracks(animatorId: number, slot: number): number
|
|
253
253
|
// The layer's LOOP: member slots + positions (dims 1: x per member, 2: x,y; one member = a plain looping
|
|
254
|
-
// clip). The members share ONE cycle clock
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
254
|
+
// clip). The members share ONE cycle clock, each placed on it LINEARLY by `phases` — two floats per member,
|
|
255
|
+
// (offset, cycles): φ(t) = offset + cycles · t / duration, 0 at a left-foot-down (measured offline
|
|
256
|
+
// from the clip's foot marks). An empty array, or cycles <= 0 for a member = normalized time (0, 1) and no
|
|
257
|
+
// cycle to phase-match to — the clock never reads the clip's marks. animatorSetBlendValue picks the mix (1D linear between neighbours /
|
|
258
|
+
// 2D gradient band). Setting it takes the layer over from whatever it shows — the previous loop and any
|
|
259
|
+
// one-shot on it go silent — with a transition of `fade`. Empty = no loop. speed = the members' rate.
|
|
260
|
+
animatorSetBlend(animatorId: number, layer: number, dims: 1 | 2, slots: Uint16Array, positions: Float32Array, fade: number, speed: number, phases: Float32Array): void
|
|
259
261
|
animatorSetBlendValue(animatorId: number, layer: number, x: number, y: number): void
|
|
260
262
|
// Play a slot as the layer's one-shot, transitioned in over fadeIn (0 = cut) from whatever the layer
|
|
261
263
|
// showed (playing a blend MEMBER brings the whole blend back instead). A non-looping one-shot hands the
|
|
@@ -322,6 +324,11 @@ declare global {
|
|
|
322
324
|
// The earliest time in the slot where that hand-over costs no more than `tolerance` metres;
|
|
323
325
|
// atContact snaps to the next foot-down at or after it. -1 = never that close.
|
|
324
326
|
animatorSlotExit(animatorId: number, slot: number, target: number, tolerance: number, atContact: boolean, align: 0 | 1): number
|
|
327
|
+
// CYCLE ALIGNMENT by pose, no marks: given slot a's cycle (offA, cyclesA), the (offset, cycles) of slot b
|
|
328
|
+
// under which the two loops show the same pose at the same gait phase — b's cycle count searched over
|
|
329
|
+
// cyclesA × {1/3 … 3}, its offset on a fine grid. out = [offset, cycles, score (metres), margin (runner-up
|
|
330
|
+
// ≥ 0.2 cycle away minus the best; ~0 = ambiguous)]. Returns 1, or 0 when there is nothing to compare.
|
|
331
|
+
animatorSlotAlign(animatorId: number, a: number, b: number, offA: number, cyclesA: number, out: Float32Array): number
|
|
325
332
|
// Contact spans as (side 0 left / 1 right, from, to, atX, atY, atZ) sextuplets (seconds, model space);
|
|
326
333
|
// returns the span count, filling `out` up to its capacity.
|
|
327
334
|
animatorSlotContacts(animatorId: number, slot: number, out: Float32Array): number
|
|
@@ -667,7 +674,7 @@ declare global {
|
|
|
667
674
|
createAnchor(sceneId: number, physicalWidth: number, systemId: number): number
|
|
668
675
|
|
|
669
676
|
// Particles. All emitter/curve config crosses as ONE Float32Array of [tag, payloadLen,
|
|
670
|
-
// ...payload] records, parsed once in creator-
|
|
677
|
+
// ...payload] records, parsed once in creator-particles (CPART_TAG_* in creator-particles.h; the
|
|
671
678
|
// SDK mirror is in gl/Particles.ts, guarded by sdk/tests/particles-tags.test.ts). Unknown
|
|
672
679
|
// tags skip by length. maxParticles 0 = default (1000).
|
|
673
680
|
createParticleSystem(entityId: number, materialInstanceId: number, maxParticles: number): void
|
|
@@ -285,3 +285,15 @@ test("buildHeader / detectProjectKind signal the engine from bundled code", () =
|
|
|
285
285
|
expect(detectProjectKind("const x = 1")).toBe("ui")
|
|
286
286
|
expect(detectProjectKind("_creator.createARController()")).toBe("ar")
|
|
287
287
|
})
|
|
288
|
+
|
|
289
|
+
test("a .json project file is data — asset()/import gives the parsed value", async () => {
|
|
290
|
+
const js = await compileProject({
|
|
291
|
+
name: "Data", publicUrl: "https://le.codes", header: false,
|
|
292
|
+
entries: [
|
|
293
|
+
{ path: "/main.ts", type: "text", text: 'toast(asset("./style.json").name)\n' },
|
|
294
|
+
{ path: "/style.json", type: "text", text: '{ "name": "OSM Bright", "version": 8 }\n' },
|
|
295
|
+
],
|
|
296
|
+
})
|
|
297
|
+
expect(js).toContain("OSM Bright")
|
|
298
|
+
expect(js).not.toContain('"./style.json"') // desugared to the import, not left as a string
|
|
299
|
+
})
|
|
@@ -108,6 +108,11 @@ export const compileProject = async (opts: CompileOptions): Promise<string> => {
|
|
|
108
108
|
}
|
|
109
109
|
} else if (e.path.toLowerCase().endsWith(".svg")) {
|
|
110
110
|
files[e.path] = `export default SvgSource(\`${e.text ?? ""}\`)`
|
|
111
|
+
} else if (e.path.toLowerCase().endsWith(".json")) {
|
|
112
|
+
// A .json project file is DATA, not code: `import data from "./x.json"` — and `asset("./x.json")`,
|
|
113
|
+
// which desugars to that import — yields the parsed value. The bundler would otherwise try to
|
|
114
|
+
// parse the file as JS. JSON is an expression, so a default export is the whole translation.
|
|
115
|
+
files[e.path] = `export default ${e.text?.trim() || "null"}`
|
|
111
116
|
} else {
|
|
112
117
|
files[e.path] = e.text ?? ""
|
|
113
118
|
}
|
package/src/core/Aspect.ts
CHANGED
|
@@ -213,7 +213,7 @@ let fixedFallbackAcc = 0
|
|
|
213
213
|
// The FIXED phase: dt = Time.fixedDt (1/60) every call, once per physics substep, on every host. The
|
|
214
214
|
// engine drives it from inside its substep loop when it has the hook; otherwise (older wasm / Apple /
|
|
215
215
|
// Android builds, the setLoop path) the SDK steps its own accumulator right after the early phase -
|
|
216
|
-
// same step, same 4-substep cap, same drop-the-backlog rule as creator-
|
|
216
|
+
// same step, same 4-substep cap, same drop-the-backlog rule as creator-physics' cphysFrameBegin.
|
|
217
217
|
const fixedStep: PhaseFn = (dt) => { _phaseHooks.fixed?.(dt); fixedUpdaters.run("updateFixed", dt) }
|
|
218
218
|
const fixedFallback = (): void => {
|
|
219
219
|
const step = Time.fixedDt
|
package/src/gl/CameraPlace.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
// CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
|
|
2
|
-
// a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
|
|
3
|
-
// side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
|
|
4
|
-
// empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
|
|
5
|
-
//
|
|
6
|
-
// • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
|
|
7
|
-
// `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
|
|
8
|
-
// • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
|
|
9
|
-
// owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
|
|
10
|
-
// or anchors on it (`inst.alignTo(inst.nodes.eye)`).
|
|
11
|
-
// • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
|
|
12
|
-
// releases.
|
|
13
|
-
// • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
|
|
14
|
-
// See docs/scene-camera-place-plan.md.
|
|
15
|
-
|
|
16
|
-
import { Aspect } from "../core/Aspect"
|
|
17
|
-
import type { FieldMeta } from "../core/fields"
|
|
18
|
-
import { Gizmos } from "../scene/gizmos"
|
|
19
|
-
import type { Camera } from "./Camera"
|
|
20
|
-
import type { Node } from "./Node"
|
|
21
|
-
|
|
22
|
-
export class CameraPlace extends Aspect<"cameraPlace", Node> {
|
|
23
|
-
static readonly aspect = "cameraPlace"
|
|
24
|
-
/** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
|
|
25
|
-
fov = 60
|
|
26
|
-
/** Near clip distance. */
|
|
27
|
-
near = 0.01
|
|
28
|
-
/** Far clip distance = view range. */
|
|
29
|
-
far = 1000
|
|
30
|
-
/** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
|
|
31
|
-
* ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
|
|
32
|
-
active = false
|
|
33
|
-
/** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
|
|
34
|
-
static fields: FieldMeta<CameraPlace> = {
|
|
35
|
-
fov: { label: "FOV", min: 1, max: 179, step: 1 },
|
|
36
|
-
near: { min: 0.001, max: 10, step: 0.01 },
|
|
37
|
-
far: { min: 1, max: 100000, step: 1 },
|
|
38
|
-
active: { label: "Active camera" },
|
|
39
|
-
}
|
|
40
|
-
static editor = { rebuild: true }
|
|
41
|
-
|
|
42
|
-
/** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
|
|
43
|
-
applyTo(camera: Camera): void {
|
|
44
|
-
camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
|
|
48
|
-
* clicking it selects the node. Play mode never calls this. */
|
|
49
|
-
rebuild(): void {
|
|
50
|
-
Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
// CameraPlace: "the camera as a place". A scene has ONE camera (`scene.camera`); a CameraPlace marks
|
|
2
|
+
// a node as somewhere that camera can be — with the lens it uses there. Nothing is created engine-
|
|
3
|
+
// side (no second camera, no ABI): it is a pose + a projection record on any node: an empty, an
|
|
4
|
+
// empty mounted on a bone (`mount:`), a node a path aspect moves (a dolly shot), a model node.
|
|
5
|
+
//
|
|
6
|
+
// • In a scene FILE that runs (`open()` / `load()`), the `active: true` place drives
|
|
7
|
+
// `scene.camera` (exactly `scene.camera.follow(node)`); exactly one per file.
|
|
8
|
+
// • Inside a prefab or an `instantiate()`d subtree a place is inert DATA — the hosting scene
|
|
9
|
+
// owns its camera. The host reads it (`inst.nodes.eye.get(CameraPlace)` → world pose + fov)
|
|
10
|
+
// or anchors on it (`inst.alignTo(inst.nodes.eye)`).
|
|
11
|
+
// • From code: `scene.camera.follow(dolly.get(CameraPlace))` — a cutscene shot; `follow(null)`
|
|
12
|
+
// releases.
|
|
13
|
+
// • Edit mode: draws an anchored frustum gizmo (the editor's camera preview reads the same place).
|
|
14
|
+
// See docs/scene-camera-place-plan.md.
|
|
15
|
+
|
|
16
|
+
import { Aspect } from "../core/Aspect"
|
|
17
|
+
import type { FieldMeta } from "../core/fields"
|
|
18
|
+
import { Gizmos } from "../scene/gizmos"
|
|
19
|
+
import type { Camera } from "./Camera"
|
|
20
|
+
import type { Node } from "./Node"
|
|
21
|
+
|
|
22
|
+
export class CameraPlace extends Aspect<"cameraPlace", Node> {
|
|
23
|
+
static readonly aspect = "cameraPlace"
|
|
24
|
+
/** Vertical field of view in degrees (default 60) — smaller is a longer lens. */
|
|
25
|
+
fov = 60
|
|
26
|
+
/** Near clip distance. */
|
|
27
|
+
near = 0.01
|
|
28
|
+
/** Far clip distance = view range. */
|
|
29
|
+
far = 1000
|
|
30
|
+
/** The place that drives `scene.camera` when the file declaring it RUNS. Exactly one per file;
|
|
31
|
+
* ignored inside prefabs / instantiated subtrees (the host scene owns its camera). */
|
|
32
|
+
active = false
|
|
33
|
+
/** Aspect ratio of the frustum gizmo only (the real aspect is the viewport's). */
|
|
34
|
+
static fields: FieldMeta<CameraPlace> = {
|
|
35
|
+
fov: { label: "FOV", min: 1, max: 179, step: 1 },
|
|
36
|
+
near: { min: 0.001, max: 10, step: 0.01 },
|
|
37
|
+
far: { min: 1, max: 100000, step: 1 },
|
|
38
|
+
active: { label: "Active camera" },
|
|
39
|
+
}
|
|
40
|
+
static editor = { rebuild: true }
|
|
41
|
+
|
|
42
|
+
/** Copy this place's projection onto a camera (`camera.follow(place)` does it for you). */
|
|
43
|
+
applyTo(camera: Camera): void {
|
|
44
|
+
camera.setProjection({ fov: this.fov, near: this.near, far: this.far })
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Edit mode: the frustum marker (−Z = view), anchored on the node so it follows drags live and
|
|
48
|
+
* clicking it selects the node. Play mode never calls this. */
|
|
49
|
+
rebuild(): void {
|
|
50
|
+
Gizmos.frustum(this.fov, { node: this.node, color: this.active ? "#ffffff" : "#9aa3b2" })
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/gl/Locomotion.ts
CHANGED
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
|
|
30
30
|
import { Aspect } from "../core/Aspect"
|
|
31
31
|
import { Vec3, cx, cz, type Vec3Like } from "../math/vec"
|
|
32
|
-
import { Animator
|
|
32
|
+
import { Animator } from "./animation/Animator"
|
|
33
|
+
import type { FeetOptions } from "./animation/Feet"
|
|
34
|
+
import type { WarpOptions } from "./animation/Warp"
|
|
33
35
|
import { CharacterController } from "./CharacterController"
|
|
34
36
|
import type { Node } from "./Node"
|
|
35
37
|
|
|
@@ -288,8 +290,8 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
288
290
|
for (const c of this.set.stops ?? []) this.enter(c, KIND.STOP)
|
|
289
291
|
for (const c of this.set.turns ?? []) this.enter(c, KIND.TURN)
|
|
290
292
|
for (const c of this.set.spins ?? []) this.enter(c, KIND.SPIN)
|
|
291
|
-
if (this.warp !== undefined) anim.warp
|
|
292
|
-
anim.feet
|
|
293
|
+
if (this.warp !== undefined) anim.warp.set(this.warp)
|
|
294
|
+
anim.feet.set(this.feet === undefined ? { lock: true } : this.feet === true ? { lock: true, ik: true } : this.feet === false ? { lock: false, ik: false } : this.feet)
|
|
293
295
|
this.pushTuning()
|
|
294
296
|
}
|
|
295
297
|
|
|
@@ -303,7 +305,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
303
305
|
if (slot < 0) { console.warn(`Locomotion: no clip '${n}' on ${this.node.name || "the character"}`); return }
|
|
304
306
|
this._names.set(slot, n)
|
|
305
307
|
const o = typeof clip === "string" ? undefined : clip
|
|
306
|
-
const measured = anim.
|
|
308
|
+
const measured = anim.clipInfo(n)
|
|
307
309
|
const angle = o?.angle ?? (measured ? measured.turn * RAD2DEG : 0)
|
|
308
310
|
const gait = kind === KIND.GAIT ? -1 : o?.gait ? GAITS.indexOf(o.gait) : -1
|
|
309
311
|
_creator.locoSetEntry(this._loco, slot, kind, angle, o?.speed ?? 0, gait)
|
|
@@ -422,7 +424,7 @@ export class Locomotion extends Aspect<"loco", Node, LocomotionEvents> {
|
|
|
422
424
|
const named = t ? t[this._gait] : undefined
|
|
423
425
|
if (named !== undefined) return named * this._mag
|
|
424
426
|
const clip = (this.set.gaits ?? [])[GAITS.indexOf(this._gait)]
|
|
425
|
-
const measured = clip !== undefined ? this.anim?.
|
|
427
|
+
const measured = clip !== undefined ? this.anim?.clipInfo(name(clip))?.speed : undefined
|
|
426
428
|
return (measured ?? 0) * this._mag
|
|
427
429
|
}
|
|
428
430
|
|
package/src/gl/Particles.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
//
|
|
4
4
|
// All emitter/curve config crosses the bridge as ONE Float32Array of [tag, payloadLen, ...payload]
|
|
5
5
|
// records (_creator.setParticleSystemConfig) — the constructor batches every option into a single
|
|
6
|
-
// call, live setters send a one-record buffer. The tag values mirror
|
|
7
|
-
// creator-
|
|
6
|
+
// call, live setters send a one-record buffer. The tag values mirror CPART_TAG_* in
|
|
7
|
+
// creator-particles/include/creator-particles/creator-particles.h (guarded by tests/particles-tags.test.ts).
|
|
8
8
|
//
|
|
9
9
|
// Curves over a particle's lifetime are built with curve()/colorCurve() chains — see the builders
|
|
10
10
|
// below. A builder's whole state is the flat `_data` array (the exact record payload), so the
|
|
@@ -18,7 +18,7 @@ import { Node } from "./Node"
|
|
|
18
18
|
|
|
19
19
|
type Range<T> = T | { min: T, max: T }
|
|
20
20
|
|
|
21
|
-
// --- config record tags (keep in sync with
|
|
21
|
+
// --- config record tags (keep in sync with CPART_TAG_* in creator-particles.h) ------------------
|
|
22
22
|
|
|
23
23
|
const TAG_SHAPE = 1
|
|
24
24
|
const TAG_LIFETIME = 2
|