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/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 (aligned by their foot contacts, not by normalized time) and
255
- // animatorSetBlendValue picks the mix (1D linear between neighbours / 2D gradient band). Setting it
256
- // takes the layer over from whatever it shows — the previous loop and any one-shot on it go silent — with
257
- // a transition of `fade`. Empty = no loop. speed = the members' rate.
258
- animatorSetBlend(animatorId: number, layer: number, dims: 1 | 2, slots: Uint16Array, positions: Float32Array, fade: number, speed: number): void
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-gl (PsTag in creator-gl/src/particles.h; the
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
  }
@@ -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-gl's physics/world.cpp.
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
@@ -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
+ }
@@ -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, type FeetOptions, type WarpOptions } from "./animation/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 = this.warp
292
- anim.feet = this.feet === undefined ? { lock: true } : this.feet === true ? { lock: true, ik: true } : this.feet === false ? { lock: false, ik: false } : this.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.curves(n)
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?.curves(name(clip))?.speed : undefined
427
+ const measured = clip !== undefined ? this.anim?.clipInfo(name(clip))?.speed : undefined
426
428
  return (measured ?? 0) * this._mag
427
429
  }
428
430
 
@@ -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 PsTag in
7
- // creator-gl/src/particles.h (guarded by tests/particles-tags.test.ts).
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 PsTag in creator-gl/src/particles.h) -----------------
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