lecodes-sdk 1.0.0 → 1.1.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.
Files changed (78) hide show
  1. package/dist/global.d.ts +18 -4
  2. package/dist/types/animate/tween/Animation.d.ts +69 -0
  3. package/dist/types/animate/tween/Timeline.d.ts +55 -0
  4. package/dist/types/animate/tween/animateValue.d.ts +27 -0
  5. package/dist/types/animate/tween/easing.d.ts +29 -0
  6. package/dist/types/animate/tween/spec.d.ts +178 -0
  7. package/dist/types/g2/Node2D.d.ts +16 -0
  8. package/dist/types/g2/Sprite.d.ts +11 -1
  9. package/dist/types/gl/Camera.d.ts +15 -1
  10. package/dist/types/gl/Foliage.d.ts +47 -0
  11. package/dist/types/gl/Geometry.d.ts +24 -0
  12. package/dist/types/gl/Light.d.ts +25 -7
  13. package/dist/types/gl/Lightmap.d.ts +90 -60
  14. package/dist/types/gl/Material.d.ts +28 -20
  15. package/dist/types/gl/Model.d.ts +7 -5
  16. package/dist/types/gl/Node.d.ts +18 -0
  17. package/dist/types/gl/Particles.d.ts +40 -1
  18. package/dist/types/gl/Scene.d.ts +20 -0
  19. package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
  20. package/dist/types/gl/animation/Animator.d.ts +27 -0
  21. package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
  22. package/dist/types/gl/animation/IK.d.ts +86 -30
  23. package/dist/types/gl/animation/Warp.d.ts +2 -1
  24. package/dist/types/gl/animation/core.d.ts +35 -4
  25. package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
  26. package/dist/types/gl/terrain/Terrain.d.ts +4 -2
  27. package/dist/types/inject.d.ts +8 -2
  28. package/dist/types/scene/defineScene.d.ts +44 -32
  29. package/dist/types/ui/UIButton.d.ts +3 -1
  30. package/dist/types/ui/UIInput.d.ts +5 -1
  31. package/dist/types/ui/UINode.d.ts +24 -24
  32. package/dist/types.json +1 -1
  33. package/package.json +1 -1
  34. package/prompts/core-design.md +27 -4
  35. package/prompts/core.md +35 -6
  36. package/prompts/select.ts +19 -4
  37. package/src/animate/tween/Animation.ts +378 -0
  38. package/src/animate/tween/Timeline.ts +175 -0
  39. package/src/animate/tween/animateValue.ts +100 -0
  40. package/src/animate/tween/easing.ts +172 -0
  41. package/src/animate/tween/spec.ts +479 -0
  42. package/src/bridges.d.ts +226 -65
  43. package/src/compile/__tests__/assetMacro.test.ts +26 -0
  44. package/src/compile/__tests__/detectEntry.test.ts +19 -0
  45. package/src/compile/__tests__/serverSplit.test.ts +27 -0
  46. package/src/compile/bundler.ts +34 -4
  47. package/src/compile/compileProject.ts +31 -1
  48. package/src/compile/detectEntry.ts +8 -3
  49. package/src/compile/index.ts +2 -0
  50. package/src/compile/serverSplit.ts +9 -3
  51. package/src/g2/Node2D.ts +38 -0
  52. package/src/g2/Sprite.ts +20 -1
  53. package/src/gl/Camera.ts +34 -1
  54. package/src/gl/Foliage.ts +102 -0
  55. package/src/gl/Geometry.ts +393 -348
  56. package/src/gl/Light.ts +46 -16
  57. package/src/gl/Lightmap.ts +439 -275
  58. package/src/gl/Material.ts +59 -47
  59. package/src/gl/Model.ts +167 -156
  60. package/src/gl/Node.ts +39 -0
  61. package/src/gl/Particles.ts +61 -2
  62. package/src/gl/Scene.ts +34 -1
  63. package/src/gl/animation/AnimationClip.ts +52 -0
  64. package/src/gl/animation/Animator.ts +42 -2
  65. package/src/gl/animation/DynamicBone.ts +482 -459
  66. package/src/gl/animation/IK.ts +173 -152
  67. package/src/gl/animation/Playback.ts +5 -4
  68. package/src/gl/animation/Warp.ts +5 -2
  69. package/src/gl/animation/core.ts +65 -4
  70. package/src/gl/physics/Ragdoll.ts +451 -272
  71. package/src/gl/terrain/Terrain.ts +4 -2
  72. package/src/inject.ts +12 -2
  73. package/src/scene/defineScene.ts +72 -62
  74. package/src/ui/UIButton.ts +2 -2
  75. package/src/ui/UIInput.ts +3 -3
  76. package/src/ui/UINode.ts +61 -36
  77. package/dist/types/animate/animate.d.ts +0 -20
  78. package/src/animate/animate.ts +0 -238
@@ -1,6 +1,13 @@
1
1
  import { type ColorInput } from "../core/color";
2
2
  import type { Vec3Like } from "../math/vec";
3
- import { Node } from "./Node";
3
+ import { Node, type NodeTweenProps } from "./Node";
4
+ import { type TweenMeta } from "../animate/tween/spec";
5
+ import type { Animation } from "../animate/tween/Animation";
6
+ /** Animatable light props on top of the node transform. */
7
+ export type LightTweenProps = NodeTweenProps & {
8
+ intensity?: number | number[];
9
+ color?: ColorInput | ColorInput[];
10
+ };
4
11
  export type SunOptions = {
5
12
  /** Light direction (points where the light travels). Defaults to a typical key-light angle. */
6
13
  direction?: Vec3Like;
@@ -41,17 +48,27 @@ export type PointOptions = {
41
48
  /** Point-light shadows are a cubemap render per light; off by default. */
42
49
  castShadows?: boolean;
43
50
  /**
44
- * Whether `lecodes lightmap bake` bakes this light into the level's lightmap (docs/lightmap-plan.md).
45
- * A baked light is switched OFF at runtime once the bake applies — statics read it from the atlas,
46
- * movers from the light volume — so it costs nothing per frame; a light that must stay live (a
47
- * flicker, a lamp the player can shoot out) says `baked: false` and is kept out of the bake.
48
- * Lights created after the level loads (a muzzle flash) are never in a bake. Default true.
51
+ * Whether `lecodes lightmap bake` bakes this light into the level's lightmap. A baked lamp lights the statics
52
+ * from the atlas and the MOVERS from the level's light grid, and is held dark in real time while that grid is
53
+ * loaded (without a grid it keeps lighting the movers live); a light
54
+ * that must reach the statics live too — a flicker, a lamp the player can shoot out, a muzzle flash —
55
+ * says `baked: false`: it is kept out of the bake and put on the statics' light channel. Default true.
49
56
  */
50
57
  baked?: boolean;
58
+ /**
59
+ * THE BAKE'S SHAPE of this lamp: `[width, height]` in metres = an AREA light - a rectangle in the light's local XZ
60
+ * plane, emitting along its local -Y (down, for an unrotated node) with the same lumens: a ceiling panel. The bake
61
+ * lights from the whole rectangle (soft shadows, light from where the panel is and not from a point inside the
62
+ * fixture). Real time it is still the point light above. Ignored with `baked: false`.
63
+ */
64
+ bakeArea?: readonly [number, number];
51
65
  };
52
66
  export declare class Light extends Node {
53
67
  private _intensity;
54
- /** The most recently created sun — what `Lightmap.load` uses unless told otherwise. */
68
+ /** Tween `intensity` / `color` (a flash, a sunrise) and the transform — see {@link Node.animateTo}. */
69
+ animateTo(props: LightTweenProps & TweenMeta): Animation;
70
+ animateFrom(props: LightTweenProps & TweenMeta): Animation;
71
+ /** The most recently created sun. */
55
72
  static lastSun: Light | null;
56
73
  _shadowDistance: number;
57
74
  /** A directional sun light. */
@@ -69,6 +86,7 @@ export declare class Light extends Node {
69
86
  /** Live intensity (sun: lux, point: lumens) — animate a flash without rebuilding the light. */
70
87
  get intensity(): number;
71
88
  set intensity(value: number);
89
+ _holdDark(dark: boolean): void;
72
90
  destroy(): void;
73
91
  set color(value: ColorInput);
74
92
  get color(): ColorInput;
@@ -1,94 +1,124 @@
1
1
  import { Node } from "./Node";
2
2
  import type { Scene } from "./Scene";
3
3
  export type LightmapLoadOptions = {
4
- /** 1 = baked sun shadows at full strength, 0 = ambient occlusion only. Default 1. */
5
- sunStrength?: number;
6
- /** Multiplier on the ambient (IBL) share in the shadow math — how bright a fully shadowed texel
7
- * stays. 1 reproduces filament's own real-time shadow darkness (the shader reads the scene's sun
8
- * and IBL from filament's per-frame uniforms); raise it for lighter shadows, lower for deeper. Default 1. */
9
- ambientScale?: number;
10
- /** How much of the baked ambient occlusion applies: 1 = all of it, 0 = none. An interior lit by
11
- * its ambient probe alone can want less than the geometric truth. Default 1. */
12
- aoStrength?: number;
13
- /** Multiplier on the baked point lights' irradiance (default 1). Purely ARTISTIC: the bake is
14
- * physical (verified against the analytic 1/d^2 sum over the level's lamps - where line of sight
15
- * is clear the two agree within ~17 %), and a room lit to its real illuminance simply reads dim
16
- * against an ambient fill. This dials the lamps up without a re-bake, since their irradiance is
17
- * IN the atlas and `--lamp-gain` would cost a full one.
18
- * It scales the atlas AND the light volume together, so statics and movers stay consistent - but
19
- * note they are NOT equally forgiving: a static surface carries the bake's own cosine and
20
- * shadowing, while a mover reads the volume through a flat half-cosine, so a large boost blows
21
- * movers out long before it blows out the level. Past ~x100 expect to light the movers
22
- * separately. */
23
- lightBoost?: number;
24
- /** Multiplier on the light VOLUME's irradiance alone — the movers' own `lightBoost`. Default =
25
- * `lightBoost`, and since 2026-08-31 that default is the right one: the volume is an ambient cube
26
- * (irradiance per axis face, blended by the pixel's normal), so a mover's top takes a lamp above it
27
- * and its underside does not — the same shading the atlas gives the statics. Before that the volume
28
- * was one flat 0.5·E on every face and this knob was the workaround. Kept as a trim. */
29
- volumeBoost?: number;
30
- /** What an OCCLUDER-only static (a prop without lightmap UVs — traced into the receivers' atlas but
31
- * carrying no rect of its own) does in real time. `"baked"` (default): its shadow is in the bake, so
32
- * it stops casting and the shadow pass draws only the movers — on a level of thousands of foliage
33
- * cards that is a third of the GPU frame. `"realtime"`: it keeps casting, which is the only way its
34
- * shadow reaches OTHER non-receiver props (a rock under a tree); the honest fix for that is lightmap
35
- * UVs on the props (`lecodes assets doctor --lightmap-uv`), which makes them receivers. */
36
- occluderShadows?: "baked" | "realtime";
4
+ /** A DEBUG multiplier on the atlas' lux (default 1): the atlas is physical, so this is a knob for looking,
5
+ * not a look. */
6
+ lightScale?: number;
7
+ /** The level's own radiance of emission 1.0 for the BAKE (cd / m², `BakeConfig.emissiveNits`): an imported pack's
8
+ * emissive values are tuned for a look, not for light - this is where the level says how much its panels really give.
9
+ * The CLI's `--emissive-nits` wins over it; without either the camera's exposure decides. */
10
+ emissiveNits?: number;
11
+ /** `false` = the emissive surfaces light NOTHING in the bake (the level is lit by its lamps, the panels are decoration)
12
+ * while `emissiveNits` still says how bright their glow is drawn. Default true. */
13
+ emissiveBake?: boolean;
14
+ /** FOR THE BAKE: materials that let light THROUGH them, by their glTF name (`"name*"` = every name with that start):
15
+ * the share of a shadow ray that passes, tinted by the material's base colour x map - an awning's warm, patterned light
16
+ * on the sand. The surface itself stays opaque: baked, drawn as before, a real-time shadow caster.
17
+ * A number = the share that goes STRAIGHT through (the gaps of a weave: it draws the map's picture on the ground);
18
+ * `{ through, diffuse }` adds the share the fibres SCATTER - the cloth's underside glows like a matte panel and lights
19
+ * what is under it by the solid angle it fills, with no picture. Dense canvas: `{ through: 0.08, diffuse: 0.25 }`. */
20
+ transmit?: Record<string, LightmapTransmit>;
21
+ };
22
+ /** one `transmit` entry (LightmapLoadOptions.transmit): the straight share, or both shares */
23
+ export type LightmapTransmit = number | {
24
+ through?: number;
25
+ diffuse?: number;
37
26
  };
38
27
  export type LightmapInfo = {
39
28
  size: number;
40
29
  texel: number;
41
- /** Atlas pages the bake took (`texture` lists them in order). */
30
+ /** Atlas pages the bake took (`light` / `aux` list them in order). */
42
31
  pages: number;
43
- /** The light volume, when one was loaded: grid dims + cell size in metres. */
44
- volume?: {
45
- dims: number[];
46
- cell: number;
47
- };
48
- /** Point lights the bake carries and this load switched off (statics read them from the atlas,
49
- * movers from the volume); 0 without a `light` atlas. */
50
- lights: number;
51
32
  /** keys applied / keys in the file / registered statics without a rect */
52
33
  applied: number;
53
34
  total: number;
54
35
  missing: string[];
55
- /** Occluder-only statics whose real-time shadow was switched off (`occluderShadows: "baked"`). */
56
- occluders: number;
57
36
  };
58
37
  export type LightmapFiles = {
59
38
  data: string;
60
- /** The atlas — one page, or every page in order for a bake that took several. */
61
- texture: string | string[];
62
- /** The point lights' baked irradiance (pages like `texture`). */
63
- light?: string | string[];
39
+ /** The irradiance atlas — one page, or every page in order for a bake that took several. */
40
+ light: string | string[];
41
+ /** The aux atlas (sun / sky visibility + light direction), pages like `light`. */
42
+ aux: string | string[];
43
+ /** DEBUG: the direct-light-only pages a `--split` bake wrote (`<stem>-direct[_n].ktx2`), for the "direct" view. */
44
+ direct?: string | string[];
45
+ /** DEBUG: the bounce-only pages of a `--split` bake (`<stem>-indirect[_n].ktx2`), for the "indirect" view. */
46
+ indirect?: string | string[];
47
+ /** The reflection probes (`<stem>-probes.ktx2`), when the bake placed some (`lecodes lightmap bake --probe-spacing`). */
48
+ probes?: string;
49
+ /** THE LIGHT GRID for movers (`<stem>.lgrid`, `lecodes lightmap bake --volume`): the baked ambient light of every
50
+ * place a mover can be. Absent = movers keep the scene's IBL. */
64
51
  volume?: string;
65
52
  };
53
+ /** The lightmap material's data views (`Lightmap.debug`); "direct" / "indirect" bind a `--split` bake's pages instead. */
54
+ export type LightmapDebugMode = "off" | "irradiance" | "albedo" | "normal" | "shadingNormal" | "share" | "skyVis" | "atlas" | "direction" | "lux" | "relief" | "direct" | "indirect" | "probe" | "probeMap" | "skyPath" | "probePath";
55
+ export type LightmapDebugParams = {
56
+ /** `lux`: the false-colour ramp's log10 range (default 0..5, i.e. 1 lux .. 100 000). */
57
+ luxRange?: [number, number];
58
+ };
59
+ /** What `Lightmap.probe` answers: the baked texel under a screen point. */
60
+ export type LightmapProbe = {
61
+ key: string;
62
+ entity: number;
63
+ node: string;
64
+ material: string;
65
+ page: number;
66
+ /** The texel's column / row on its page. */
67
+ texel: [number, number];
68
+ uv1: [number, number];
69
+ world: [number, number, number];
70
+ distance: number;
71
+ };
66
72
  export declare class Lightmap {
67
73
  /** True while `lecodes lightmap bake` runs the app — skip menus and build the scene straight away. */
68
74
  static get baking(): boolean;
69
75
  private static entries;
70
- private static dynamics;
71
76
  private static ordinals;
72
77
  private static warned;
73
- private static switchedOff;
78
+ /** The bake bound last (`load`): what `probe` reads rects from. */
79
+ private static bound;
74
80
  /** Register static geometry — a Model, a Mesh, or any node whose subtree holds them. Statics are
75
- * both receivers and occluders in the bake. `key` names the entry in lightmap.bake (default:
81
+ * both receivers and occluders in the bake. `key` names the entry in the .bake (default:
76
82
  * the node's name + a running number, `container#3`); pass one when names are not stable. */
77
83
  static add(node: Node, key?: string): Node;
78
84
  private static next;
79
- /** Forget every registration (a scene rebuild); baked-off lights come back to the real-time path. */
85
+ private static movers;
86
+ private static volumeOn;
87
+ /** A MOVER takes its ambient light from the level's baked LIGHT GRID (`env.lightmap.volume`) at the place it is at,
88
+ * every frame — whatever its shader: the standard glTF one or a custom lit material. Every Model / Mesh under `node`
89
+ * is marked (a character, a weapon with its parts). A scene file does this itself for every node a Physics aspect or
90
+ * a CharacterController moves; call it for what CODE spawns — an enemy, a pickup, a projectile. `on = false` hands
91
+ * the subtree back to the scene's IBL. Safe before the level's bake has loaded. */
92
+ static track(node: Node, on?: boolean): void;
93
+ private static loadVolume;
94
+ /** Forget every registration (a scene rebuild). */
80
95
  static clear(): void;
81
96
  /** Apply a bake — or, under `lecodes lightmap bake`, run it. Resolves to null when nothing was
82
97
  * applied (no bake yet, a host without the feature, bake mode). */
98
+ private static scene;
83
99
  static load(scene: Scene, files: LightmapFiles, options?: LightmapLoadOptions): Promise<LightmapInfo | null>;
84
- /** Every live point light within 5 cm of a baked one is held dark (a moved lamp keeps lighting live
85
- * — and double, until the next bake, which is the honest state of a stale bake). */
86
- private static switchOffBaked;
87
- /** lightmap.volume → the engine's 3D textures → THE volume for every dynamic lightmap-material instance,
88
- * plus the registered dynamic Meshes. Null when the file is a placeholder or the host lacks the feature. */
89
- private static loadVolume;
90
- /** A Mesh keeps its look (colour / map / roughness / metallic) but moves to the lightmap material. */
100
+ /** The bake's `<key>#k` instances by their base key: the parts of a GLB unwrapped in groups (k = extras.lightmapGroup). */
101
+ private static groupsOf;
102
+ /** Switch every lightmapped surface to a DATA view (or back with `"off"` / 0). Level-wide, instant —
103
+ * except "direct" / "indirect", which load that page set the first time (a `--split` bake listed in the scene). */
104
+ static debug(mode: LightmapDebugMode | number, params?: LightmapDebugParams): void;
105
+ /** Bind another page set of the bound bake to every surface (the "direct" / "indirect" views). */
106
+ private static show;
107
+ private static balls;
108
+ /** DEBUG: a mirror ball at every reflection probe of the bound bake (`on`), or none (`off`) — a ball reflects the
109
+ * probes around its point unoccluded (at a probe's own position that is the probe itself), so a probe inside a
110
+ * wall, a black one or a leak across a wall shows at a glance. Metallic, roughness 0; view "probe" shows the
111
+ * probes alone. */
112
+ static debugProbes(on: boolean, radius?: number): number;
113
+ /** The view names in `lightmapDebug`'s order — for a knob that cycles them. */
114
+ static get debugModes(): readonly LightmapDebugMode[];
115
+ /** The baked texel under a screen point (logical px): its instance key, page and texel column / row,
116
+ * so the value can be looked up in the bake's debug layers (`lecodes lightmap inspect`). null when
117
+ * nothing baked is under the point or the host has no triangle pick. */
118
+ static probe(screenX: number, screenY: number): LightmapProbe | null;
119
+ /** A hex colour as the 9-char form the float4 uniform path expects ("#rrggbbaa"). */
120
+ private static hex8;
121
+ /** The lightmap material for a static Mesh, from whatever lit material it carried (colour, map, roughness, metallic). */
91
122
  private static meshMaterial;
92
- private static swapMeshMaterial;
93
123
  private static bake;
94
124
  }
@@ -119,8 +119,6 @@ export type LitMaterialOptions = MaterialColorOptions & MaterialStateOptions & {
119
119
  /** Metallic factor, 0 (dielectric) … 1 (metal). Unset = the shader's default. */
120
120
  metallic?: number;
121
121
  };
122
- /** `Material.lightmapShading` tiers — see the setter. */
123
- export type LightmapShading = "full" | "baked" | "baked-lite";
124
122
  export declare class Material {
125
123
  readonly shader: FetchResponse | "unknown";
126
124
  readonly uniforms: Record<string, UniformValue>;
@@ -153,33 +151,43 @@ export declare class Material {
153
151
  static decal(options?: DecalMaterialOptions): Material;
154
152
  /** Material that samples a VideoPlayer's texture. */
155
153
  static video(map?: Texture): Material;
156
- /** The lightmap material (docs/lightmap-plan.md): PBR base colour × a baked shadow/AO atlas on UV1.
157
- * Models take it through `Model.load(…, { lightmap: true })`; `Lightmap.load` builds one per static
158
- * Mesh. Parameters use gltfio's names (`baseColorFactor`, `baseColorMap`, `roughnessFactor`,
159
- * `metallicFactor`) plus `lightmap`, `lightmapST`, `ambientScale`, `sunStrength` (the sun / IBL terms come from
160
- * filament's per-frame uniforms inside the shader). */
154
+ /** The lightmap material (packages/creator-bake): a lit PBR material whose whole DIFFUSE light is the
155
+ * baked irradiance atlas on UV1 (`lightmapLight`, lux) — the real-time sun, lamps and diffuse IBL do not
156
+ * touch it; the specular IBL stays, occluded by the baked sky visibility. Models take it through
157
+ * `Model.load(…, { lightmap: true })`; a static Mesh gets one from here. Parameters use gltfio's names
158
+ * (`baseColorFactor`, `baseColorMap`, `roughnessFactor`, `metallicFactor`) plus `lightmapLight`, `lightmapAux`,
159
+ * `lightmapST`, `lightScale` (the level-wide value `Lightmap.load` sets) and the debug view knobs. */
161
160
  static lightmap(): Material;
162
161
  /** `Material.lightmap()`'s masked twin (`blending: masked`, same shader and parameters): what the engine
163
162
  * gives a lightmapped model's alpha-MASK materials. The cutoff comes from the glTF material. */
164
163
  static lightmapMasked(): Material;
165
164
  /** The terrain splat material (docs/terrain-plan.md §1.5): four albedo (+ normal-map) layers blended by
166
165
  * a control map on the terrain's own grid, per-layer `tiling` (metres per repeat) / `roughness` /
167
- * `normalScale` / `triplanar`, lightmap-aware (the same `lightmap` / `lightmapST` block as
168
- * `Material.lightmap`). `Terrain` builds and owns one per terrain; the uniforms are primed here so an
169
- * unset layer is white and an unbaked terrain is fully lit. */
166
+ * `normalScale` / `triplanar`. `Terrain` builds and owns one per terrain; the uniforms are primed here so an
167
+ * unset layer is white. Lit real-time; a BAKED terrain takes `Material.terrainLightmap()`. */
170
168
  static terrain(): Material;
169
+ /** `Material.terrain()` for a BAKED terrain (terrain-lightmap.mat): the same layers and uniforms, shaded like
170
+ * `Material.lightmap()` — the whole diffuse light is the baked atlas on the terrain's UV1 (one rect), which
171
+ * `Lightmap.load` binds. A scene file's `terrain:` node that is a lightmap static is built with it; without a
172
+ * bound bake it draws black, like any lightmapped static. No reflection probes (the sampler budget): it reflects the
173
+ * sky through the baked sky visibility. */
174
+ static terrainLightmap(): Material;
171
175
  private static _lmTemplate;
172
176
  private static _lmMaskedTemplate;
173
- static _lightmapShading: LightmapShading;
174
- /** Which shader lightmapped models take — a graphics-quality tier, engine-wide. `"full"` is filament's
175
- * lit path over the baked atlas (IBL specular, real-time point lights such as a muzzle flash, sun
176
- * shadows on dynamic objects). `"baked"` keeps the baked light and an approximated ambient but drops
177
- * the lit path: ~40 % cheaper per pixel on a fill-bound GPU. `"baked-lite"` is that minus the normal,
178
- * metallic/roughness and occlusion map reads (the factors stand in, the atlas keeps the baked AO;
179
- * emissive still glows): +20 % more at 1080p on the same GPU. Read when a lightmapped model LOADS, so set
180
- * it before the level (a settings menu applies it on the next level load), like `Texture.maxSize`. */
181
- static get lightmapShading(): LightmapShading;
182
- static set lightmapShading(mode: LightmapShading);
177
+ /** The FOLIAGE tier (`Model.load(…, { foliage: true })`, see `Foliage`): an unlit shader with its own cheap
178
+ * lighting plus a vertex shader that sways in the wind, bends away from benders and thins out with distance,
179
+ * and a per-copy tint. glTF parameters like `Material.lightmap()` plus `wind` / `sway` / `fade` / `benders`,
180
+ * which the engine writes itself. Not baked by the rewritten bake yet. */
181
+ static foliage(): Material;
182
+ /** `Material.foliage()`'s masked twin (`blending: masked`): what the engine gives a foliage model's alpha-MASK
183
+ * materials — the cards. */
184
+ static foliageMasked(): Material;
185
+ /** the glTF-side identity values every provider-handed material starts from */
186
+ private static _glbDefaults;
187
+ private static _lightmapDefaults;
188
+ private static _folTemplate;
189
+ static _foliageMaskedTemplate(): Material;
190
+ private static _folMaskedTemplate;
183
191
  /** Shadow-catcher material (transparent except where shadows fall). */
184
192
  static shadow(color?: ColorInput): Material;
185
193
  /** Load a custom compiled shader (.mat URL) as a material. */
@@ -77,11 +77,13 @@ export declare class Model extends Node {
77
77
  loaded: number;
78
78
  total?: number;
79
79
  }) => void;
80
- /** Baked lighting (docs/lightmap-plan.md). `true` = a STATIC: loads through the lightmap material so
80
+ /** Baked lighting (packages/creator-bake). `true` = a STATIC: loads through the lightmap material so
81
81
  * `Lightmap.load` can bind its atlas rect (the GLB needs TEXCOORD_1 — `lecodes assets doctor
82
- * --lightmap-uv`). `'dynamic'` = a mover: the same material, lit by the level's light VOLUME instead
83
- * (no UV1 needed). Omitted: `'dynamic'` while a scene file with `env.lightmap.volume` is running,
84
- * else off. `false` = the standard shader. Hosts without lightmap support ignore it. */
85
- lightmap?: boolean | "dynamic";
82
+ * --lightmap-uv`) and takes nothing from the real-time lights once the bake applies. Omitted / `false` =
83
+ * the standard shader, lit real-time (movers). Hosts without lightmap support ignore it. */
84
+ lightmap?: boolean;
85
+ /** Vegetation: load through the FOLIAGE tier (see `Foliage`) — wind, touch bending, distance fade,
86
+ * per-copy tint. Hosts without the tier fall back to the standard shader. */
87
+ foliage?: boolean;
86
88
  }): Promise<Model>;
87
89
  }
@@ -7,6 +7,17 @@ import type { Geometry } from "./Geometry";
7
7
  import { Material } from "./Material";
8
8
  import type { CompAxis, CompWriter } from "../core/compWrite";
9
9
  import type { ClickEvent, TouchStartEvent } from "../runtime/touch";
10
+ import type { Animation } from "../animate/tween/Animation";
11
+ import { type TweenMeta } from "../animate/tween/spec";
12
+ /** Animatable transform props of a 3D node: a value tweens from the current one, an ARRAY OF
13
+ * VALUES is a keyframe list (`position: [[0,0,0], [0,2,0]]`). */
14
+ export type NodeTweenProps = {
15
+ position?: Vec3Like | Vec3Like[];
16
+ scale?: number | Vec3Like | (number | Vec3Like)[];
17
+ quaternion?: QuatLike | QuatLike[];
18
+ /** Degrees, YXZ; interpolated per axis without shortest-arc, so `[0, 720, 0]` spins twice. */
19
+ eulerAngles?: Vec3Like | Vec3Like[];
20
+ };
10
21
  /** id → Node, so host callbacks (touch hits, animation events) route back to the owning object. */
11
22
  export declare const nodeRegistry: Registry<Node>;
12
23
  export type NodeEvents = {
@@ -59,6 +70,13 @@ export declare class Node extends AspectHost<NodeEvents> implements CompWriter {
59
70
  set scale(v: Vec3Like | number);
60
71
  get quaternion(): Quat;
61
72
  set quaternion(v: QuatLike);
73
+ /** Tween the transform to the given values — `node.animateTo({ position: [0, 2, 0], duration: 800,
74
+ * easing: 'inOutCubic' })`; arrays of values are keyframes. Runs on the game clock (pauses with
75
+ * the game) unless `clock: 'ui'`. Returns the {@link Animation} handle. Phase 1: plain nodes —
76
+ * a physics-owned node (a body / character) is not routed through its engine object yet. */
77
+ animateTo(props: NodeTweenProps & TweenMeta): Animation;
78
+ /** Tween FROM the given values to the node's current transform (an entrance). */
79
+ animateFrom(props: NodeTweenProps & TweenMeta): Animation;
62
80
  get eulerAngles(): Vec3;
63
81
  set eulerAngles(v: Vec3Like);
64
82
  get forward(): Vec3;
@@ -170,6 +170,15 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
170
170
  * spread evenly along the path — trail density independent of speed, no per-frame clumps.
171
171
  * Meant for `space: 'world'`. */
172
172
  rateOverDistance?: number;
173
+ /** Spread those spawn points along a CURVE through the emitter's recent path instead of the
174
+ * straight line between where it was last frame and where it is now. A straight line cuts the
175
+ * corner by however far the path bows inside one frame, which grows with the frame TIME — so a
176
+ * fast curving emitter looks faceted, and looks worse the slower the machine. Default false
177
+ * (the straight line); costs three stored positions and one curve evaluation per spawn.
178
+ *
179
+ * The curve needs a point one frame AHEAD, so each frame's spawns are placed provisionally and
180
+ * nudged into place on the next frame, once it exists. Nothing else observes them in between. */
181
+ smooth?: boolean;
173
182
  /** Draw order among blended systems at the same depth — higher draws later, i.e. on top (Unity's
174
183
  * sortingOrder). Systems are depth-sorted by their node, so the emitters of one effect tie and
175
184
  * without this the engine picks who covers whom per frame (smoke popping over a fireball).
@@ -222,6 +231,8 @@ export declare class Particles extends Node {
222
231
  set space(val: "local" | "world");
223
232
  set inheritVelocity(val: number);
224
233
  set rateOverDistance(val: number);
234
+ /** Lay a frame's spawns along a curve through the emitter's path, not the straight chord. */
235
+ set smooth(val: boolean);
225
236
  set order(val: number);
226
237
  /** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
227
238
  set renderPriority(v: number);
@@ -260,8 +271,28 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
260
271
  width?: ParticleValue;
261
272
  /** Minimum emitter movement (world units) between recorded points. Default 0.05. */
262
273
  minDistance?: number;
263
- /** Point capacity (default 128). */
274
+ /** Point capacity (default 128). A spawn that does not fit is DROPPED, so a fast emitter at a
275
+ * small `minDistance` wants headroom: a sword tip covers metres inside one `time` window. */
264
276
  maxPoints?: number;
277
+ /** Follow a CURVE through the emitter's recent path instead of the straight line between frames.
278
+ * A trail is where this shows most — the strip is the path — and it shows worse the lower the
279
+ * frame rate, since a longer frame bows further off its own chord. Default false; see
280
+ * `ParticlesOptions.smooth`. */
281
+ smooth?: boolean;
282
+ /** Which way the strip's WIDTH points.
283
+ *
284
+ * `'camera'` (default) rolls the strip about its own length to stay flat to the viewer — it can
285
+ * never disappear, which is why it is the default, but it bears no relation to the thing that drew
286
+ * it. An AXIS (`'x'`/`'y'`/`'z'`) uses that axis of the emitter's own transform, as it was when
287
+ * each point was laid: on a node riding a sword's blade, `'z'` (the blade) makes the strip the
288
+ * surface the blade actually swept, and `width` stops being a made-up number — it is the blade. */
289
+ orient?: "camera" | "x" | "y" | "z";
290
+ /** With `orient` on an axis: the floor under the strip's APPARENT width, 0..1 of its real width,
291
+ * below which it rolls back toward the camera. An honestly oriented strip goes edge-on — and
292
+ * vanishes — whenever the swing happens in the plane of view; this is the hybrid that keeps it
293
+ * readable. 0 = never roll (honest, and it will vanish), 1 = always (the same as `'camera'`).
294
+ * Default 0.4: a sweep still reads as a sweep, and a swing toward the camera still shows. */
295
+ faceCamera?: number;
265
296
  color?: ParticleColor;
266
297
  /** Opacity along the trail. Defaults to fading the tail out (`curve().to(0)`). */
267
298
  opacity?: ParticleValue;
@@ -274,6 +305,8 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
274
305
  export declare class Trail extends Node {
275
306
  private _material;
276
307
  private _rateDistance;
308
+ private _orient;
309
+ private _faceCamera;
277
310
  constructor(options?: TrailOptions);
278
311
  private _send;
279
312
  /** Trail length in seconds — how long each laid-down point lives. */
@@ -282,6 +315,12 @@ export declare class Trail extends Node {
282
315
  set opacity(val: ParticleValue);
283
316
  set color(value: ParticleColor);
284
317
  set minDistance(val: number);
318
+ /** Follow a curve through the emitter's path instead of the straight chord between frames. */
319
+ set smooth(val: boolean);
320
+ /** Which way the strip's width points — see `TrailOptions.orient`. Keeps the current `faceCamera`. */
321
+ set orient(val: "camera" | "x" | "y" | "z");
322
+ /** The hybrid's floor — see `TrailOptions.faceCamera`. */
323
+ set faceCamera(val: number);
285
324
  /** Pause/resume laying down points — existing ones still age out, so the trail fades naturally
286
325
  * after e.g. a sword swing ends. */
287
326
  set emitting(val: boolean);
@@ -174,6 +174,19 @@ export declare class Scene implements Presentable {
174
174
  setLodBias(bias: number): void;
175
175
  /** Runtime form of `bloom` / `bloomIntensity` (a graphics-settings menu). */
176
176
  setBloom(enabled: boolean, intensity?: number): void;
177
+ /**
178
+ * How bright the environment (IBL) lights the scene, in lux — `SceneOptions.environmentIntensity`
179
+ * after the fact. Live: it changes the probe's intensity, not the probe, so it costs nothing and
180
+ * can be dragged. (`setDefaultIbl`, the call that installs a probe, rebuilds the cubemap from the
181
+ * ktx every time — never drive a slider through a scene option that has to re-open.)
182
+ *
183
+ * `lecodes lightmap bake` reads the same number off the live probe, so a scene dimmed here bakes
184
+ * dimmed. Hosts without the call keep whatever the scene opened with, and the getter still
185
+ * reports what was asked for.
186
+ */
187
+ get environmentIntensity(): number;
188
+ set environmentIntensity(lux: number);
189
+ private static _warnedEnvIntensity;
177
190
  /** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
178
191
  setStencil(enabled: boolean): void;
179
192
  add(...nodes: Node[]): this;
@@ -192,6 +205,13 @@ export declare class Scene implements Presentable {
192
205
  destroy(): void;
193
206
  createOverlay(options?: SceneOptions): Scene;
194
207
  warmRender(): Promise<void>;
208
+ /** Precompile the scene's shader variants so nothing is compiled mid-game. Every material the host
209
+ * knows is queued for the sun / shadow / fog / skinning variants and the dynamic-light key — the
210
+ * first point light (a muzzle flash, an explosion) would otherwise rebuild every lit shader in view
211
+ * on that frame. Call it once the sun and fog are set, typically under a loading screen; materials
212
+ * loaded later are queued in the background as they are created. Resolves at once on a host
213
+ * without the bridge. */
214
+ precompileShaders(): Promise<void>;
195
215
  readonly cl: (() => void)[];
196
216
  _backButtonCallback?: () => void;
197
217
  private _vd?;
@@ -50,6 +50,25 @@ export declare class AnimationClip {
50
50
  * keeps the ones inside the window, re-timed. Chainable. */
51
51
  addEvent(time: number, name: string): this;
52
52
  private _pushEvents;
53
+ /** ANCHOR SPANS for a bone — what an anchored IK chain ending in `bone` rides while this clip plays
54
+ * (`IK.TwoBone` with `anchor`; the sockets come from `anim.sockets(...)`). `donor` names the socket
55
+ * set of the gun the take was AUTHORED on ('' = the live set: no delta); `spans` = `[from, to,
56
+ * socket, rotation?, pin?]` in SECONDS — the socket the hand is on through that window (`''` = free:
57
+ * it rides the joint itself), `rotation` 0..1 how much of the socket's turn it takes (default 1),
58
+ * `pin` 0..1 how much it sits ON the socket instead of keeping its own relation to it (default 0: a
59
+ * magazine is REACHED, and the take's motion is what carries the hand there).
60
+ * Gaps between spans = the chain's own socket, pinned by the chain's `anchorPin` — that is where a
61
+ * hand holding the gun is nailed to the grip, so the layers below cannot slide it. A clip WITHOUT
62
+ * anchors takes no part: the hand keeps the take's own relation to the gun. Part of the clip, like
63
+ * events; `AnimationClip.from` carries them re-timed. Chainable.
64
+ * reload.anchors('hand_l', { donor: 'tr15', spans: [[0.9, 1.4, 'mag'], [1.4, 2.0, ''], [2.0, 2.4, 'mag'], [2.4, 2.7, 'bolt']] }) */
65
+ anchors(bone: string, def: {
66
+ donor?: string;
67
+ spans?: readonly (readonly [number, number, string, number?, number?])[];
68
+ }): this;
69
+ /** Drop a bone's anchor spans. */
70
+ clearAnchors(bone: string): this;
71
+ private _pushAnchors;
53
72
  /** Load ONE clip from a GLB: the file's only/first clip, or the one named / at the given index. */
54
73
  static load(source: string | FetchResponse, clip?: string | number): Promise<AnimationClip>;
55
74
  /** Load EVERY clip of a GLB (library files), keyed by clip name. */
@@ -1,4 +1,6 @@
1
1
  import { Aspect } from "../../core/Aspect";
2
+ import { type Vec3Like } from "../../math/vec";
3
+ import { type QuatLike } from "../../math/quat";
2
4
  import type { Node } from "../Node";
3
5
  import type { AnimationClip } from "./AnimationClip";
4
6
  import { type ActiveClip, type ClipInfo, type ClipEventHandler, type LayerOptions, type LoopDef, type LoopOptions, type PlayOptions, type StopOptions } from "./core";
@@ -75,6 +77,31 @@ export declare class Animator extends Aspect<"anim", Node> {
75
77
  * turn too); on for a rig whose root carries the heading (`lecodes assets retarget --root-rotation yaw`). */
76
78
  get rootRotation(): boolean;
77
79
  set rootRotation(on: boolean);
80
+ /** The body's WORLD velocity this frame (m/s): what `anim.warp`'s stride and orientation fit the clips
81
+ * to. A `Locomotion` feeds it itself; when you drive the gait by hand set it every frame (the
82
+ * CharacterController's velocity, or your own). Reads back the last value fed. */
83
+ get motion(): readonly [number, number, number];
84
+ set motion(v: {
85
+ x: number;
86
+ y: number;
87
+ z: number;
88
+ } | readonly [number, number, number]);
89
+ private _motion;
90
+ /** SOCKETS: named frames on a bone of this rig — a weapon's grip / magazine / bolt empties, placed in
91
+ * the scene editor. A NODE socket is read by the engine every frame (a socket on a moving part
92
+ * follows it); a `{ position, quaternion }` one is fixed in the bone's space. The default set is the
93
+ * LIVE one (the weapon in the hands); `set: 'tr15'` names a DONOR set — the same sockets on the gun a
94
+ * take was authored on, which a clip's `anchors()` refer to. Chainable.
95
+ * arms.anim.sockets('ik_hand_gun', { grip_r: w.gripR, grip_l: w.gripL, mag: w.mag })
96
+ * arms.anim.sockets('ik_hand_gun', { grip_l: { position: [0.03, -0.12, -0.05] } }, { set: 'tr15' }) */
97
+ sockets(joint: Node | string, table: Record<string, Node | {
98
+ position?: Vec3Like;
99
+ quaternion?: QuatLike;
100
+ }>, options?: {
101
+ set?: string;
102
+ }): this;
103
+ /** Drop a socket (the live set, or `set`). */
104
+ removeSocket(name: string, set?: string): this;
78
105
  /** `'auto'` (default): a character small on screen or out of view is evaluated every 2nd / 4th frame
79
106
  * without its finger, toe and twist joints; `'full'`: every frame, every joint (a hero seen through a
80
107
  * scope). Independent of `Model.lod`, the mesh level. */
@@ -11,12 +11,14 @@ export type DynamicBoneFloor = "none" | "probe" | number | Vec3Like;
11
11
  * on the standard humanoid bones (hips, spine, head, limbs — the Ragdoll's layout); `'none'`; or the
12
12
  * nodes carrying the colliders to use. */
13
13
  export type DynamicBoneColliders = "auto" | "humanoid" | "none" | Node[];
14
- /** The cloth's OUTSIDE, which makes every collider one-sided: `'none'` = a collider pushes a bone to its
15
- * nearest surface (tails, ropes); `'auto'` = away from the root bone's own axis (a cloak's strips round a
16
- * spine face out from it); a vector = a fixed direction in the root bone's frame (a cape on the back:
17
- * `[0, 0, -1]`). With a side the body is under the cloth by definition: an arm that swung through the
18
- * cape pushes it away, and a bone that ended up inside a capsule leaves it on the outside — never
19
- * through it to be held against the chest from the wrong side. */
14
+ /** The cloth's SIDE of every collider marked `oneSided`: `'none'` = no side, every collider pushes a bone to
15
+ * its nearest surface (tails, ropes); `'auto'` = away from the root bone's own axis (a cloak's strips round
16
+ * a spine face out from it); a vector = a fixed direction in the root bone's frame (a cape hanging BEHIND
17
+ * the arms: the body's backward). A one-sided collider that is moving AWAY from that side — an arm swinging
18
+ * forward under the cape — puts a bone it has run into over to the side instead of carrying it: the arm
19
+ * passes through the cloth and the cape hangs behind it again, instead of being dragged round the body.
20
+ * Still, or moving toward the cloth, it pushes like any other collider, so cloth draped on an arm at rest
21
+ * stays where it is. Two-sided colliders (the body's) never read the side. */
20
22
  export type DynamicBoneSide = "none" | "auto" | Vec3Like;
21
23
  /** A capsule (or a sphere) riding a bone — what dynamic bones stay out of. Attach to a body bone:
22
24
  * `hips.aspect(DynamicBoneCollider, { radius: 0.12, to: 'Spine' })`. The capsule runs from
@@ -33,6 +35,10 @@ export declare class DynamicBoneCollider extends Aspect<"dynamicBoneCollider", N
33
35
  end?: Vec3Like;
34
36
  /** The bone the capsule reaches (its origin), instead of `end`. */
35
37
  to?: string;
38
+ /** ONE-SIDED: the cloth belongs on the chain's `side` of this capsule (see DynamicBone.side). While the capsule
39
+ * moves away from that side it puts a bone it has run into over to the side instead of carrying it — an arm
40
+ * under a cape may push the cape back, never drag it forward round the body. Needs a `side` on the DynamicBone. */
41
+ oneSided: boolean;
36
42
  static fields: FieldMeta<DynamicBoneCollider>;
37
43
  private _id;
38
44
  private _segment;
@@ -64,7 +70,8 @@ export declare class DynamicBone extends Aspect<"dynamicBone", Node> {
64
70
  mass: DynamicBoneCurve;
65
71
  /** Bones (by name) that ride the animation exactly while their subtrees still simulate: a sheet's
66
72
  * several roots under one anchor (a cloak's seven strips under the spine), so ONE chain owns them all
67
- * and `link` ties neighbouring strips. */
73
+ * and `link` ties neighbouring strips — neighbours in THIS order, so list the strips round the ring
74
+ * (front-left … back … front-right); the front stays open. */
68
75
  pinned?: string[];
69
76
  /** Metres a PINNED bone may be pushed off its animated place by a collider — a soft pin. Its particle
70
77
  * collides and takes the edge pushes like a free bone (the strip below hangs from where it IS) and is
@@ -163,7 +170,11 @@ export declare class DynamicBone extends Aspect<"dynamicBone", Node> {
163
170
  get particles(): Vec3[];
164
171
  private _collect;
165
172
  private _fill;
166
- /** Neighbour links: the bones at the same depth, in tree order, tied pairwise. */
173
+ /** Neighbour links: the bones at the same depth tied pairwise, the columns in the order of `pinned` (a cloak's
174
+ * strips listed round the ring) — columns not listed there follow in tree order. Tree order alone is the
175
+ * engine's child order, which for a GLB is the file's node order REVERSED (Filament prepends each child), so
176
+ * strips added to a rig later were tied across the body instead of to their neighbours: the ermine's cloak
177
+ * had one true neighbour pair out of eight, the rest were rods through the chest. */
167
178
  private _links;
168
179
  private _sync;
169
180
  private _pushFloor;