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.
- package/dist/global.d.ts +18 -4
- package/dist/types/animate/tween/Animation.d.ts +69 -0
- package/dist/types/animate/tween/Timeline.d.ts +55 -0
- package/dist/types/animate/tween/animateValue.d.ts +27 -0
- package/dist/types/animate/tween/easing.d.ts +29 -0
- package/dist/types/animate/tween/spec.d.ts +178 -0
- package/dist/types/g2/Node2D.d.ts +16 -0
- package/dist/types/g2/Sprite.d.ts +11 -1
- package/dist/types/gl/Camera.d.ts +15 -1
- package/dist/types/gl/Foliage.d.ts +47 -0
- package/dist/types/gl/Geometry.d.ts +24 -0
- package/dist/types/gl/Light.d.ts +25 -7
- package/dist/types/gl/Lightmap.d.ts +90 -60
- package/dist/types/gl/Material.d.ts +28 -20
- package/dist/types/gl/Model.d.ts +7 -5
- package/dist/types/gl/Node.d.ts +18 -0
- package/dist/types/gl/Particles.d.ts +40 -1
- package/dist/types/gl/Scene.d.ts +20 -0
- package/dist/types/gl/animation/AnimationClip.d.ts +19 -0
- package/dist/types/gl/animation/Animator.d.ts +27 -0
- package/dist/types/gl/animation/DynamicBone.d.ts +19 -8
- package/dist/types/gl/animation/IK.d.ts +86 -30
- package/dist/types/gl/animation/Warp.d.ts +2 -1
- package/dist/types/gl/animation/core.d.ts +35 -4
- package/dist/types/gl/physics/Ragdoll.d.ts +87 -12
- package/dist/types/gl/terrain/Terrain.d.ts +4 -2
- package/dist/types/inject.d.ts +8 -2
- package/dist/types/scene/defineScene.d.ts +44 -32
- package/dist/types/ui/UIButton.d.ts +3 -1
- package/dist/types/ui/UIInput.d.ts +5 -1
- package/dist/types/ui/UINode.d.ts +24 -24
- package/dist/types.json +1 -1
- package/package.json +1 -1
- package/prompts/core-design.md +27 -4
- package/prompts/core.md +35 -6
- package/prompts/select.ts +19 -4
- package/src/animate/tween/Animation.ts +378 -0
- package/src/animate/tween/Timeline.ts +175 -0
- package/src/animate/tween/animateValue.ts +100 -0
- package/src/animate/tween/easing.ts +172 -0
- package/src/animate/tween/spec.ts +479 -0
- package/src/bridges.d.ts +226 -65
- package/src/compile/__tests__/assetMacro.test.ts +26 -0
- package/src/compile/__tests__/detectEntry.test.ts +19 -0
- package/src/compile/__tests__/serverSplit.test.ts +27 -0
- package/src/compile/bundler.ts +34 -4
- package/src/compile/compileProject.ts +31 -1
- package/src/compile/detectEntry.ts +8 -3
- package/src/compile/index.ts +2 -0
- package/src/compile/serverSplit.ts +9 -3
- package/src/g2/Node2D.ts +38 -0
- package/src/g2/Sprite.ts +20 -1
- package/src/gl/Camera.ts +34 -1
- package/src/gl/Foliage.ts +102 -0
- package/src/gl/Geometry.ts +393 -348
- package/src/gl/Light.ts +46 -16
- package/src/gl/Lightmap.ts +439 -275
- package/src/gl/Material.ts +59 -47
- package/src/gl/Model.ts +167 -156
- package/src/gl/Node.ts +39 -0
- package/src/gl/Particles.ts +61 -2
- package/src/gl/Scene.ts +34 -1
- package/src/gl/animation/AnimationClip.ts +52 -0
- package/src/gl/animation/Animator.ts +42 -2
- package/src/gl/animation/DynamicBone.ts +482 -459
- package/src/gl/animation/IK.ts +173 -152
- package/src/gl/animation/Playback.ts +5 -4
- package/src/gl/animation/Warp.ts +5 -2
- package/src/gl/animation/core.ts +65 -4
- package/src/gl/physics/Ragdoll.ts +451 -272
- package/src/gl/terrain/Terrain.ts +4 -2
- package/src/inject.ts +12 -2
- package/src/scene/defineScene.ts +72 -62
- package/src/ui/UIButton.ts +2 -2
- package/src/ui/UIInput.ts +3 -3
- package/src/ui/UINode.ts +61 -36
- package/dist/types/animate/animate.d.ts +0 -20
- package/src/animate/animate.ts +0 -238
package/src/gl/Particles.ts
CHANGED
|
@@ -40,6 +40,10 @@ const TAG_ANGULAR_VELOCITY = 17
|
|
|
40
40
|
const TAG_GROUND = 18
|
|
41
41
|
const TAG_VELOCITY_LIFE = 19
|
|
42
42
|
const TAG_ORDER = 20
|
|
43
|
+
const TAG_SMOOTH_PATH = 21
|
|
44
|
+
const TAG_RIBBON_ORIENT = 22
|
|
45
|
+
/** `TrailOptions.orient` → the native axis code (0 = the camera-facing default). */
|
|
46
|
+
const RIBBON_AXIS: Record<"camera" | "x" | "y" | "z", number> = { camera: 0, x: 1, y: 2, z: 3 }
|
|
43
47
|
|
|
44
48
|
/** @internal — exported for the tag-sync test only. */
|
|
45
49
|
export const _particleTags = {
|
|
@@ -49,7 +53,7 @@ export const _particleTags = {
|
|
|
49
53
|
SPACE: TAG_SPACE, INHERIT_VELOCITY: TAG_INHERIT_VELOCITY, RATE_DISTANCE: TAG_RATE_DISTANCE,
|
|
50
54
|
RENDER_MODE: TAG_RENDER_MODE, MESH_GEOMETRY: TAG_MESH_GEOMETRY,
|
|
51
55
|
ANGULAR_VELOCITY: TAG_ANGULAR_VELOCITY, GROUND: TAG_GROUND, VELOCITY_LIFE: TAG_VELOCITY_LIFE,
|
|
52
|
-
ORDER: TAG_ORDER,
|
|
56
|
+
ORDER: TAG_ORDER, SMOOTH_PATH: TAG_SMOOTH_PATH, RIBBON_ORIENT: TAG_RIBBON_ORIENT,
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// --- curve builders -----------------------------------------------------------------------------
|
|
@@ -286,6 +290,15 @@ export type ParticlesOptions = ParticlesMaterialOptions & {
|
|
|
286
290
|
* spread evenly along the path — trail density independent of speed, no per-frame clumps.
|
|
287
291
|
* Meant for `space: 'world'`. */
|
|
288
292
|
rateOverDistance?: number
|
|
293
|
+
/** Spread those spawn points along a CURVE through the emitter's recent path instead of the
|
|
294
|
+
* straight line between where it was last frame and where it is now. A straight line cuts the
|
|
295
|
+
* corner by however far the path bows inside one frame, which grows with the frame TIME — so a
|
|
296
|
+
* fast curving emitter looks faceted, and looks worse the slower the machine. Default false
|
|
297
|
+
* (the straight line); costs three stored positions and one curve evaluation per spawn.
|
|
298
|
+
*
|
|
299
|
+
* The curve needs a point one frame AHEAD, so each frame's spawns are placed provisionally and
|
|
300
|
+
* nudged into place on the next frame, once it exists. Nothing else observes them in between. */
|
|
301
|
+
smooth?: boolean
|
|
289
302
|
/** Draw order among blended systems at the same depth — higher draws later, i.e. on top (Unity's
|
|
290
303
|
* sortingOrder). Systems are depth-sorted by their node, so the emitters of one effect tie and
|
|
291
304
|
* without this the engine picks who covers whom per frame (smoke popping over a fireball).
|
|
@@ -477,6 +490,7 @@ export class Particles extends Node {
|
|
|
477
490
|
if (options.space !== undefined) out.push(TAG_SPACE, 1, options.space === "world" ? 1 : 0)
|
|
478
491
|
if (options.inheritVelocity !== undefined) out.push(TAG_INHERIT_VELOCITY, 1, options.inheritVelocity)
|
|
479
492
|
if (options.rateOverDistance !== undefined) out.push(TAG_RATE_DISTANCE, 1, options.rateOverDistance)
|
|
493
|
+
if (options.smooth !== undefined) out.push(TAG_SMOOTH_PATH, 1, options.smooth ? 1 : 0)
|
|
480
494
|
if (options.order !== undefined) out.push(TAG_ORDER, 1, options.order)
|
|
481
495
|
if (options.rate !== undefined) out.push(TAG_RATE, 1, options.rate)
|
|
482
496
|
if (options.shape !== undefined) pushShape(out, options.shape)
|
|
@@ -530,6 +544,8 @@ export class Particles extends Node {
|
|
|
530
544
|
set space(val: "local" | "world") { this._send([ TAG_SPACE, 1, val === "world" ? 1 : 0 ]) }
|
|
531
545
|
set inheritVelocity(val: number) { this._send([ TAG_INHERIT_VELOCITY, 1, val ]) }
|
|
532
546
|
set rateOverDistance(val: number) { this._send([ TAG_RATE_DISTANCE, 1, val ]) }
|
|
547
|
+
/** Lay a frame's spawns along a curve through the emitter's path, not the straight chord. */
|
|
548
|
+
set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
|
|
533
549
|
set order(val: number) { this._send([ TAG_ORDER, 1, val ]) }
|
|
534
550
|
/** Coarse draw order, 0 … 7 — see `Mesh.renderPriority`. Write-only. */
|
|
535
551
|
set renderPriority(v: number) {
|
|
@@ -620,8 +636,28 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
|
|
|
620
636
|
width?: ParticleValue
|
|
621
637
|
/** Minimum emitter movement (world units) between recorded points. Default 0.05. */
|
|
622
638
|
minDistance?: number
|
|
623
|
-
/** Point capacity (default 128).
|
|
639
|
+
/** Point capacity (default 128). A spawn that does not fit is DROPPED, so a fast emitter at a
|
|
640
|
+
* small `minDistance` wants headroom: a sword tip covers metres inside one `time` window. */
|
|
624
641
|
maxPoints?: number
|
|
642
|
+
/** Follow a CURVE through the emitter's recent path instead of the straight line between frames.
|
|
643
|
+
* A trail is where this shows most — the strip is the path — and it shows worse the lower the
|
|
644
|
+
* frame rate, since a longer frame bows further off its own chord. Default false; see
|
|
645
|
+
* `ParticlesOptions.smooth`. */
|
|
646
|
+
smooth?: boolean
|
|
647
|
+
/** Which way the strip's WIDTH points.
|
|
648
|
+
*
|
|
649
|
+
* `'camera'` (default) rolls the strip about its own length to stay flat to the viewer — it can
|
|
650
|
+
* never disappear, which is why it is the default, but it bears no relation to the thing that drew
|
|
651
|
+
* it. An AXIS (`'x'`/`'y'`/`'z'`) uses that axis of the emitter's own transform, as it was when
|
|
652
|
+
* each point was laid: on a node riding a sword's blade, `'z'` (the blade) makes the strip the
|
|
653
|
+
* surface the blade actually swept, and `width` stops being a made-up number — it is the blade. */
|
|
654
|
+
orient?: "camera" | "x" | "y" | "z"
|
|
655
|
+
/** With `orient` on an axis: the floor under the strip's APPARENT width, 0..1 of its real width,
|
|
656
|
+
* below which it rolls back toward the camera. An honestly oriented strip goes edge-on — and
|
|
657
|
+
* vanishes — whenever the swing happens in the plane of view; this is the hybrid that keeps it
|
|
658
|
+
* readable. 0 = never roll (honest, and it will vanish), 1 = always (the same as `'camera'`).
|
|
659
|
+
* Default 0.4: a sweep still reads as a sweep, and a swing toward the camera still shows. */
|
|
660
|
+
faceCamera?: number
|
|
625
661
|
color?: ParticleColor
|
|
626
662
|
/** Opacity along the trail. Defaults to fading the tail out (`curve().to(0)`). */
|
|
627
663
|
opacity?: ParticleValue
|
|
@@ -635,6 +671,8 @@ export type TrailOptions = Omit<ParticlesMaterialOptions, "render" | "stretch">
|
|
|
635
671
|
export class Trail extends Node {
|
|
636
672
|
private _material: Material
|
|
637
673
|
private _rateDistance: number
|
|
674
|
+
private _orient = 0
|
|
675
|
+
private _faceCamera = 0.4
|
|
638
676
|
|
|
639
677
|
constructor(options: TrailOptions = {}) {
|
|
640
678
|
super()
|
|
@@ -650,6 +688,12 @@ export class Trail extends Node {
|
|
|
650
688
|
TAG_RATE_DISTANCE, 1, this._rateDistance,
|
|
651
689
|
TAG_LIFETIME, 2, time, time,
|
|
652
690
|
]
|
|
691
|
+
if (options.smooth) out.push(TAG_SMOOTH_PATH, 1, 1)
|
|
692
|
+
if (options.orient !== undefined || options.faceCamera !== undefined) {
|
|
693
|
+
this._orient = RIBBON_AXIS[options.orient ?? "camera"]
|
|
694
|
+
if (options.faceCamera !== undefined) this._faceCamera = options.faceCamera
|
|
695
|
+
out.push(TAG_RIBBON_ORIENT, 2, this._orient, this._faceCamera)
|
|
696
|
+
}
|
|
653
697
|
pushParam(out, 0, options.width ?? 0.1)
|
|
654
698
|
pushParam(out, 2, options.opacity ?? curve().to(0))
|
|
655
699
|
if (options.color !== undefined) pushColor(out, options.color)
|
|
@@ -686,6 +730,21 @@ export class Trail extends Node {
|
|
|
686
730
|
this._send([ TAG_RATE_DISTANCE, 1, this._rateDistance ])
|
|
687
731
|
}
|
|
688
732
|
|
|
733
|
+
/** Follow a curve through the emitter's path instead of the straight chord between frames. */
|
|
734
|
+
set smooth(val: boolean) { this._send([ TAG_SMOOTH_PATH, 1, val ? 1 : 0 ]) }
|
|
735
|
+
|
|
736
|
+
/** Which way the strip's width points — see `TrailOptions.orient`. Keeps the current `faceCamera`. */
|
|
737
|
+
set orient(val: "camera" | "x" | "y" | "z") {
|
|
738
|
+
this._orient = RIBBON_AXIS[val]
|
|
739
|
+
this._send([ TAG_RIBBON_ORIENT, 2, this._orient, this._faceCamera ])
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** The hybrid's floor — see `TrailOptions.faceCamera`. */
|
|
743
|
+
set faceCamera(val: number) {
|
|
744
|
+
this._faceCamera = val
|
|
745
|
+
this._send([ TAG_RIBBON_ORIENT, 2, this._orient, val ])
|
|
746
|
+
}
|
|
747
|
+
|
|
689
748
|
/** Pause/resume laying down points — existing ones still age out, so the trail fades naturally
|
|
690
749
|
* after e.g. a sword swing ends. */
|
|
691
750
|
set emitting(val: boolean) { this._send([ TAG_RATE_DISTANCE, 1, val ? this._rateDistance : 0 ]) }
|
package/src/gl/Scene.ts
CHANGED
|
@@ -177,7 +177,8 @@ const installRenderSyncedFrames = (): void => {
|
|
|
177
177
|
export class Scene implements Presentable {
|
|
178
178
|
/** @internal native scene handle. */
|
|
179
179
|
readonly _id: number
|
|
180
|
-
/** @internal
|
|
180
|
+
/** @internal what `environmentIntensity` currently asks for — the accessor's backing field, so it
|
|
181
|
+
* answers on hosts whose engine has no live setter too. */
|
|
181
182
|
_environmentIntensity = 20000
|
|
182
183
|
readonly camera: Camera
|
|
183
184
|
/** The listener + global 3D audio knobs (docs/audio-plan.md). */
|
|
@@ -309,6 +310,26 @@ export class Scene implements Presentable {
|
|
|
309
310
|
setBloom(enabled: boolean, intensity = 0.2): void {
|
|
310
311
|
_creator.setBloomOptions(this._id, enabled, intensity, 1)
|
|
311
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* How bright the environment (IBL) lights the scene, in lux — `SceneOptions.environmentIntensity`
|
|
315
|
+
* after the fact. Live: it changes the probe's intensity, not the probe, so it costs nothing and
|
|
316
|
+
* can be dragged. (`setDefaultIbl`, the call that installs a probe, rebuilds the cubemap from the
|
|
317
|
+
* ktx every time — never drive a slider through a scene option that has to re-open.)
|
|
318
|
+
*
|
|
319
|
+
* `lecodes lightmap bake` reads the same number off the live probe, so a scene dimmed here bakes
|
|
320
|
+
* dimmed. Hosts without the call keep whatever the scene opened with, and the getter still
|
|
321
|
+
* reports what was asked for.
|
|
322
|
+
*/
|
|
323
|
+
get environmentIntensity(): number { return this._environmentIntensity }
|
|
324
|
+
set environmentIntensity(lux: number) {
|
|
325
|
+
this._environmentIntensity = lux
|
|
326
|
+
if (_creator.setEnvironmentIntensity) _creator.setEnvironmentIntensity(this._id, lux)
|
|
327
|
+
else if (!Scene._warnedEnvIntensity) {
|
|
328
|
+
Scene._warnedEnvIntensity = true
|
|
329
|
+
console.warn("[scene] this host has no setEnvironmentIntensity — environmentIntensity stays as the scene opened")
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
private static _warnedEnvIntensity = false
|
|
312
333
|
/** The scene's stencil buffer on / off (see `SceneOptions.stencil`). */
|
|
313
334
|
setStencil(enabled: boolean): void {
|
|
314
335
|
if (_creator.setSceneStencil) _creator.setSceneStencil(this._id, enabled)
|
|
@@ -367,6 +388,18 @@ export class Scene implements Presentable {
|
|
|
367
388
|
return new Promise<void>((res) => _creator.warmRender(this._id, res))
|
|
368
389
|
}
|
|
369
390
|
|
|
391
|
+
/** Precompile the scene's shader variants so nothing is compiled mid-game. Every material the host
|
|
392
|
+
* knows is queued for the sun / shadow / fog / skinning variants and the dynamic-light key — the
|
|
393
|
+
* first point light (a muzzle flash, an explosion) would otherwise rebuild every lit shader in view
|
|
394
|
+
* on that frame. Call it once the sun and fog are set, typically under a loading screen; materials
|
|
395
|
+
* loaded later are queued in the background as they are created. Resolves at once on a host
|
|
396
|
+
* without the bridge. */
|
|
397
|
+
precompileShaders(): Promise<void> {
|
|
398
|
+
const fn = _creator.precompileShaders
|
|
399
|
+
if (!fn) return Promise.resolve()
|
|
400
|
+
return new Promise<void>((res) => fn.call(_creator, this._id, res))
|
|
401
|
+
}
|
|
402
|
+
|
|
370
403
|
// ---- Presentable (docs/navigation-presentable-plan.md) ----
|
|
371
404
|
// A scene is a destination like a screen: opening it replaces whatever is visible (suspending
|
|
372
405
|
// an active Router until Router.restore()), and it can be pushed onto the Router stack. The
|
|
@@ -82,6 +82,8 @@ export class AnimationClip {
|
|
|
82
82
|
readonly _index: number
|
|
83
83
|
/** @internal events: seconds + name, sorted (the engine gets them normalized via setClipEvents). */
|
|
84
84
|
_events: { t: number, name: string }[] = []
|
|
85
|
+
/** @internal anchor spans per bone, seconds (the engine gets them normalized via setClipAnchors). */
|
|
86
|
+
_anchors: { bone: string, donor: string, spans: { from: number, to: number, socket: string, rotation: number, pin: number }[] }[] = []
|
|
85
87
|
|
|
86
88
|
private constructor(set: number, index: number, info: ClipInfo) {
|
|
87
89
|
this._set = set
|
|
@@ -119,6 +121,48 @@ export class AnimationClip {
|
|
|
119
121
|
_creator.setClipEvents(this._set, this._index, new Float32Array(this._events.map((e) => (d > 0 ? e.t / d : 0))))
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/** ANCHOR SPANS for a bone — what an anchored IK chain ending in `bone` rides while this clip plays
|
|
125
|
+
* (`IK.TwoBone` with `anchor`; the sockets come from `anim.sockets(...)`). `donor` names the socket
|
|
126
|
+
* set of the gun the take was AUTHORED on ('' = the live set: no delta); `spans` = `[from, to,
|
|
127
|
+
* socket, rotation?, pin?]` in SECONDS — the socket the hand is on through that window (`''` = free:
|
|
128
|
+
* it rides the joint itself), `rotation` 0..1 how much of the socket's turn it takes (default 1),
|
|
129
|
+
* `pin` 0..1 how much it sits ON the socket instead of keeping its own relation to it (default 0: a
|
|
130
|
+
* magazine is REACHED, and the take's motion is what carries the hand there).
|
|
131
|
+
* Gaps between spans = the chain's own socket, pinned by the chain's `anchorPin` — that is where a
|
|
132
|
+
* hand holding the gun is nailed to the grip, so the layers below cannot slide it. A clip WITHOUT
|
|
133
|
+
* anchors takes no part: the hand keeps the take's own relation to the gun. Part of the clip, like
|
|
134
|
+
* events; `AnimationClip.from` carries them re-timed. Chainable.
|
|
135
|
+
* 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']] }) */
|
|
136
|
+
anchors(bone: string, def: { donor?: string, spans?: readonly (readonly [number, number, string, number?, number?])[] }): this {
|
|
137
|
+
const spans = (def.spans ?? []).map(([ from, to, socket, rotation, pin ]) => ({
|
|
138
|
+
from: Math.min(Math.max(0, from), this.duration), to: Math.min(Math.max(0, to), this.duration), socket,
|
|
139
|
+
rotation: rotation ?? 1, pin: pin ?? 0,
|
|
140
|
+
})).filter((s) => s.to > s.from).sort((a, b) => a.from - b.from)
|
|
141
|
+
const rec = { bone, donor: def.donor ?? "", spans }
|
|
142
|
+
const i = this._anchors.findIndex((a) => a.bone === bone)
|
|
143
|
+
if (i >= 0) this._anchors[i] = rec
|
|
144
|
+
else this._anchors.push(rec)
|
|
145
|
+
this._pushAnchors(rec)
|
|
146
|
+
return this
|
|
147
|
+
}
|
|
148
|
+
/** Drop a bone's anchor spans. */
|
|
149
|
+
clearAnchors(bone: string): this {
|
|
150
|
+
this._anchors = this._anchors.filter((a) => a.bone !== bone)
|
|
151
|
+
_creator.setClipAnchors?.(this._set, this._index, bone, "", "", new Float32Array(0))
|
|
152
|
+
return this
|
|
153
|
+
}
|
|
154
|
+
private _pushAnchors(a: { bone: string, donor: string, spans: { from: number, to: number, socket: string, rotation: number, pin: number }[] }): void {
|
|
155
|
+
const d = this.duration
|
|
156
|
+
const spans = new Float32Array(a.spans.length * 4)
|
|
157
|
+
a.spans.forEach((s, i) => {
|
|
158
|
+
spans[i * 4] = d > 0 ? s.from / d : 0
|
|
159
|
+
spans[i * 4 + 1] = d > 0 ? s.to / d : 0
|
|
160
|
+
spans[i * 4 + 2] = s.rotation
|
|
161
|
+
spans[i * 4 + 3] = s.pin
|
|
162
|
+
})
|
|
163
|
+
_creator.setClipAnchors?.(this._set, this._index, a.bone, a.donor, a.spans.map((s) => s.socket).join("\n"), spans)
|
|
164
|
+
}
|
|
165
|
+
|
|
122
166
|
/** Load ONE clip from a GLB: the file's only/first clip, or the one named / at the given index. */
|
|
123
167
|
static load(source: string | FetchResponse, clip?: string | number): Promise<AnimationClip> {
|
|
124
168
|
return AnimationClip._loadSet(source).then(([setId, infos]) => {
|
|
@@ -200,6 +244,14 @@ export class AnimationClip {
|
|
|
200
244
|
out._events = windowed
|
|
201
245
|
? clip._events.filter((ev) => ev.t >= st - 1e-6 && ev.t <= e + 1e-6).map((ev) => ({ t: Math.min(Math.max(0, ev.t - st), e - st), name: ev.name }))
|
|
202
246
|
: clip._events.map((ev) => ({ ...ev }))
|
|
247
|
+
// the anchor spans too: clipped to the window and re-timed (a mirrored clip keeps its bone names —
|
|
248
|
+
// mirror the sockets yourself if a hand's anchors must swap sides)
|
|
249
|
+
for (const a of clip._anchors) {
|
|
250
|
+
const spans = windowed
|
|
251
|
+
? a.spans.map((s) => ({ ...s, from: Math.max(0, s.from - st), to: Math.min(e - st, s.to - st) })).filter((s) => s.to > s.from)
|
|
252
|
+
: a.spans.map((s) => ({ ...s }))
|
|
253
|
+
if (spans.length > 0 || a.donor) out.anchors(a.bone, { donor: a.donor, spans: spans.map((s) => [ s.from, s.to, s.socket, s.rotation ] as const) })
|
|
254
|
+
}
|
|
203
255
|
return out
|
|
204
256
|
}
|
|
205
257
|
|
|
@@ -17,9 +17,12 @@
|
|
|
17
17
|
// default. A clip may be replaced at any time; a one-shot on a layer with no loop holds its last frame.
|
|
18
18
|
//
|
|
19
19
|
// The surface, top to bottom: clips → playing → root motion → the feet and the warp (`anim.feet`,
|
|
20
|
-
// `anim.warp` — locomotion's post-processing of the pose) →
|
|
20
|
+
// `anim.warp` — locomotion's post-processing of the pose) → sockets (what IK chains ride) → level of
|
|
21
|
+
// detail → engine-facing.
|
|
21
22
|
|
|
22
23
|
import { Aspect } from "../../core/Aspect"
|
|
24
|
+
import { Vec3, type Vec3Like } from "../../math/vec"
|
|
25
|
+
import { Quat, type QuatLike } from "../../math/quat"
|
|
23
26
|
import type { Node } from "../Node"
|
|
24
27
|
import type { AnimationClip } from "./AnimationClip"
|
|
25
28
|
import { Core, type ActiveClip, type ClipInfo, type ClipEventHandler, type LayerOptions, type LoopDef, type LoopOptions, type PlayOptions, type StopOptions } from "./core"
|
|
@@ -122,6 +125,41 @@ export class Animator extends Aspect<"anim", Node> {
|
|
|
122
125
|
* turn too); on for a rig whose root carries the heading (`lecodes assets retarget --root-rotation yaw`). */
|
|
123
126
|
get rootRotation(): boolean { return this._rootRotation }
|
|
124
127
|
set rootRotation(on: boolean) { this._rootRotation = on; if (this._rootMotion) this._c.setRootMotion(true, on) }
|
|
128
|
+
/** The body's WORLD velocity this frame (m/s): what `anim.warp`'s stride and orientation fit the clips
|
|
129
|
+
* to. A `Locomotion` feeds it itself; when you drive the gait by hand set it every frame (the
|
|
130
|
+
* CharacterController's velocity, or your own). Reads back the last value fed. */
|
|
131
|
+
get motion(): readonly [number, number, number] { return this._motion }
|
|
132
|
+
set motion(v: { x: number, y: number, z: number } | readonly [number, number, number]) {
|
|
133
|
+
const [ x, y, z ] = Array.isArray(v) ? v as readonly [number, number, number] : [ (v as { x: number }).x, (v as { y: number }).y, (v as { z: number }).z ]
|
|
134
|
+
this._motion = [ x, y, z ]
|
|
135
|
+
this._c.setMotion(x, y, z)
|
|
136
|
+
}
|
|
137
|
+
private _motion: readonly [number, number, number] = [ 0, 0, 0 ]
|
|
138
|
+
|
|
139
|
+
// ---- sockets (what anchored IK chains ride — IK.ts) ---------------------------------------------
|
|
140
|
+
/** SOCKETS: named frames on a bone of this rig — a weapon's grip / magazine / bolt empties, placed in
|
|
141
|
+
* the scene editor. A NODE socket is read by the engine every frame (a socket on a moving part
|
|
142
|
+
* follows it); a `{ position, quaternion }` one is fixed in the bone's space. The default set is the
|
|
143
|
+
* LIVE one (the weapon in the hands); `set: 'tr15'` names a DONOR set — the same sockets on the gun a
|
|
144
|
+
* take was authored on, which a clip's `anchors()` refer to. Chainable.
|
|
145
|
+
* arms.anim.sockets('ik_hand_gun', { grip_r: w.gripR, grip_l: w.gripL, mag: w.mag })
|
|
146
|
+
* arms.anim.sockets('ik_hand_gun', { grip_l: { position: [0.03, -0.12, -0.05] } }, { set: 'tr15' }) */
|
|
147
|
+
sockets(joint: Node | string, table: Record<string, Node | { position?: Vec3Like, quaternion?: QuatLike }>, options: { set?: string } = {}): this {
|
|
148
|
+
const bone = typeof joint === "string" ? this.node.bone(joint) : joint
|
|
149
|
+
if (!bone) { console.warn(`Animator.sockets: no bone '${String(joint)}' under ${this.node.name || "the node"}`); return this }
|
|
150
|
+
const set = options.set ?? ""
|
|
151
|
+
for (const [ name, at ] of Object.entries(table)) {
|
|
152
|
+
if (typeof (at as Node).id === "number") this._c.setSocket(set, name, bone.id, (at as Node).id, null)
|
|
153
|
+
else {
|
|
154
|
+
const o = at as { position?: Vec3Like, quaternion?: QuatLike }
|
|
155
|
+
const p = new Vec3(o.position ?? [ 0, 0, 0 ]), q = o.quaternion ? new Quat(o.quaternion) : Quat.identity
|
|
156
|
+
this._c.setSocket(set, name, bone.id, 0, new Float32Array([ p.x, p.y, p.z, q.x, q.y, q.z, q.w ]))
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return this
|
|
160
|
+
}
|
|
161
|
+
/** Drop a socket (the live set, or `set`). */
|
|
162
|
+
removeSocket(name: string, set = ""): this { this._c.removeSocket(set, name); return this }
|
|
125
163
|
|
|
126
164
|
// ---- level of detail --------------------------------------------------------------------------
|
|
127
165
|
/** `'auto'` (default): a character small on screen or out of view is evaluated every 2nd / 4th frame
|
|
@@ -130,9 +168,11 @@ export class Animator extends Aspect<"anim", Node> {
|
|
|
130
168
|
get lod(): "auto" | "full" { return this._lod }
|
|
131
169
|
set lod(v: "auto" | "full") { this._lod = v; _pushLod(this.node) }
|
|
132
170
|
|
|
133
|
-
// ---- engine-facing (a Locomotion, a bench)
|
|
171
|
+
// ---- engine-facing (a Locomotion, a bench, the IK aspects) ----------------------------------
|
|
134
172
|
/** The native animator's id — 0 until something is played or bound. */
|
|
135
173
|
get _id(): number { return this._c.id }
|
|
174
|
+
/** @internal the engine-facing core (IK.ts) */
|
|
175
|
+
get _core(): Core { return this._c }
|
|
136
176
|
/** Bind a clip to the base layer without playing it; its native slot (-1 = no such clip). */
|
|
137
177
|
_slot(clip: string | number): number { return this._c.slotIndex(clip) }
|
|
138
178
|
}
|