@genex-ai/cli-demo 0.87.0-dev.218 → 0.90.0-dev.227

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.87.0-dev.218",
3
+ "version": "0.90.0-dev.227",
4
4
  "description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,265 @@
1
+ // anim-runtime.js — compiled-clip playback for the AG-803 scenario pages.
2
+ // Precomputes rig-corrected local quats for every clip (same precompute as
3
+ // loco.html), then layers at runtime:
4
+ // - a LOOP layer: weighted progressive-slerp blend of looping clips, one
5
+ // shared phase for the directional gaits (they're phase-aligned at compile
6
+ // time) + an independent phase per free-running loop (idle/stance/push);
7
+ // - a ONE-SHOT layer: a non-looping clip crossfaded over the loop pose with
8
+ // a smooth envelope; its hips-Y can be capped when the physics capsule owns
9
+ // the jump (otherwise capsule + clip would both rise).
10
+ // The page owns world transform: hips are driven with the clip's LOCAL values
11
+ // only (residual x/z + absolute y), and the character root carries position/yaw.
12
+ import * as THREE from "three";
13
+
14
+ export class ClipSet {
15
+ /** data = compiled json; clips = data.gaits or data.clips */
16
+ constructor(rig, data, clips) {
17
+ this.rig = rig;
18
+ this.base = { joints: data.joints, parents: data.parents,
19
+ restPositions: data.restPositions, hipsRestY: data.hipsRestY };
20
+ this.tracks = {};
21
+ for (const [name, c] of Object.entries(clips)) {
22
+ const F = c.frames;
23
+ const t = { F, fps: c.fps, dur: F / c.fps, speed: c.speed, dir: c.dir,
24
+ loop: c.loop !== false, quat: {}, hips: new Float32Array(F * 3),
25
+ contacts: c.contacts };
26
+ for (const j of rig.mapped) t.quat[j] = new Float32Array(F * 4);
27
+ for (let f = 0; f < F; f++) {
28
+ rig.retargetFrame((k, out) => out.fromArray(c.localQuat[f][k]), true,
29
+ (j, q) => q.toArray(t.quat[j], f * 4));
30
+ for (let k = 0; k < 3; k++) t.hips[f * 3 + k] = c.hipsPos[f][k];
31
+ }
32
+ this.tracks[name] = t;
33
+ }
34
+ }
35
+ }
36
+
37
+ // Upper-body mask (layered blend per bone): feathered weights up the spine so
38
+ // the locomotion's weight-shift still leaks into the lower torso while the aim
39
+ // pose owns the chest, arms and head. Hips + legs stay 0 — the gait owns them.
40
+ const MASK_FEATHER = { Spine: 0.3, Spine1: 0.55, Spine2: 0.8, Spine3: 1, Neck: 1, Head: 0.85 };
41
+ function maskFeather(name) {
42
+ if (name in MASK_FEATHER) return MASK_FEATHER[name];
43
+ return /^(Right|Left)(Shoulder|Arm|ForeArm|Hand)/.test(name) ? 1 : 0;
44
+ }
45
+
46
+ const _s0 = new THREE.Quaternion(), _s1 = new THREE.Quaternion();
47
+
48
+ function sampleQuat(t, j, ph, out) {
49
+ const f = ph * t.F;
50
+ let i0 = Math.floor(f), u = f - i0;
51
+ let i1;
52
+ if (t.loop) { i0 %= t.F; i1 = (i0 + 1) % t.F; }
53
+ else { i0 = Math.min(i0, t.F - 1); i1 = Math.min(i0 + 1, t.F - 1); }
54
+ _s0.fromArray(t.quat[j], i0 * 4);
55
+ _s1.fromArray(t.quat[j], i1 * 4);
56
+ return out.copy(_s0).slerp(_s1, u);
57
+ }
58
+
59
+ function sampleHips(t, ph, out3) {
60
+ const f = ph * t.F;
61
+ let i0 = Math.floor(f), u = f - i0;
62
+ let i1;
63
+ if (t.loop) { i0 %= t.F; i1 = (i0 + 1) % t.F; }
64
+ else { i0 = Math.min(i0, t.F - 1); i1 = Math.min(i0 + 1, t.F - 1); }
65
+ for (let k = 0; k < 3; k++) out3[k] = t.hips[i0 * 3 + k] * (1 - u) + t.hips[i1 * 3 + k] * u;
66
+ }
67
+
68
+ export function sampleContact(t, ph, foot) {
69
+ const i = Math.min(Math.floor(ph * t.F), t.F - 1);
70
+ return t.contacts[t.loop ? i % t.F : i][foot];
71
+ }
72
+
73
+ export class Animator {
74
+ constructor(clipSet) {
75
+ this.set = clipSet;
76
+ this.rig = clipSet.rig;
77
+ this.parts = []; // rebuilt each frame: [track, phase, weight]
78
+ this.phase = 0; // shared phase of the directional gaits
79
+ this.freePhase = {}; // name -> independent phase (idle/stance/…)
80
+ this.shot = null; // { t: track, time, fade, yCap, onDone }
81
+ this.mask = null; // { t: track (1-frame pose), w } — upper-body override
82
+ this.maskFeather = clipSet.base.joints.map(maskFeather);
83
+ this._acc = new THREE.Quaternion();
84
+ this._qT = new THREE.Quaternion();
85
+ this._hp = [0, 0, 0];
86
+ this._hA = [0, 0, 0];
87
+ this._shotQ = new THREE.Quaternion();
88
+ this._qV = new THREE.Quaternion();
89
+ this._shotH = [0, 0, 0];
90
+ }
91
+
92
+ /** weights: {clipName: w}. Directional gaits share this.phase (advanced by
93
+ * the weight-blended duration); every loop listed in `free` runs its own. */
94
+ setLoops(weights, dt, free = ["idle", "stance", "push"]) {
95
+ const tr = this.set.tracks;
96
+ let dur = 0, wDir = 0;
97
+ for (const [n, w] of Object.entries(weights)) {
98
+ if (w <= 1e-3 || free.includes(n)) continue;
99
+ dur += (w * tr[n].dur);
100
+ wDir += w;
101
+ }
102
+ if (wDir > 1e-4) this.phase = (this.phase + dt / (dur / wDir)) % 1;
103
+ this.parts.length = 0;
104
+ for (const [n, w] of Object.entries(weights)) {
105
+ if (w <= 1e-3) continue;
106
+ let ph;
107
+ if (free.includes(n)) {
108
+ this.freePhase[n] = ((this.freePhase[n] ?? 0) + dt / tr[n].dur) % 1;
109
+ ph = this.freePhase[n];
110
+ } else ph = this.phase;
111
+ this.parts.push([tr[n], ph, w]);
112
+ }
113
+ }
114
+
115
+ playOneShot(name, { fadeIn = 0.1, fadeOut = 0.18, yCap = null, rate = 1, onDone = null } = {}) {
116
+ const t = this.set.tracks[name];
117
+ if (!t || this.shot) return false;
118
+ this.shot = { t, time: 0, fadeIn, fadeOut, yCap, rate, onDone, name };
119
+ return true;
120
+ }
121
+
122
+ get oneShotActive() { return this.shot !== null; }
123
+
124
+ /** Upper-body mask layer: override joints above the hips with a pose track
125
+ * (typically 1 frame), feathered up the spine. weight 0..1; ≤0 clears.
126
+ * pitch01 ∈ [-1, 1] slerps toward the compiled `${name}Up` / `${name}Dn`
127
+ * aim-offset variants (aim up = positive) when they exist. */
128
+ setMask(name, weight, pitch01 = 0) {
129
+ const t = this.set.tracks[name];
130
+ this.mask = t && weight > 1e-3
131
+ ? { t, w: Math.min(weight, 1),
132
+ up: this.set.tracks[name + "Up"], dn: this.set.tracks[name + "Dn"],
133
+ k: Math.max(-1, Math.min(1, pitch01)) }
134
+ : null;
135
+ }
136
+
137
+ /** Compute + apply the frame to the rig. Call scene.updateMatrixWorld after. */
138
+ update(dt) {
139
+ const rig = this.rig;
140
+ let shotW = 0, shotPh = 0;
141
+ if (this.shot) {
142
+ const s = this.shot;
143
+ s.time += dt * s.rate;
144
+ const remain = s.t.dur - s.time;
145
+ if (remain <= 0) {
146
+ const done = s.onDone;
147
+ this.shot = null;
148
+ done?.();
149
+ } else {
150
+ shotW = Math.min(1, s.time / s.fadeIn, Math.max(0, remain / s.fadeOut));
151
+ shotW = shotW * shotW * (3 - 2 * shotW); // smoothstep
152
+ shotPh = Math.min(s.time / s.t.dur, 0.9999);
153
+ }
154
+ }
155
+
156
+ if (this.parts.length === 0 && shotW === 0 && !this.mask) {
157
+ rig.update(dt); // nothing to blend this frame — hold the last pose
158
+ return { shotW: 0 };
159
+ }
160
+
161
+ for (const j of rig.mapped) {
162
+ let accW = 0;
163
+ for (const [t, ph, wt] of this.parts) {
164
+ sampleQuat(t, j, ph, this._qT);
165
+ if (accW === 0) this._acc.copy(this._qT);
166
+ else this._acc.slerp(this._qT, wt / (accW + wt));
167
+ accW += wt;
168
+ }
169
+ // one-shot over the locomotion, then the aim mask WINS on masked
170
+ // bones: a jump drives the legs/hips/spine while the arms + chest
171
+ // KEEP AIMING (a weapon game must not drop the aim mid-jump)
172
+ if (shotW > 0 && this.shot) {
173
+ sampleQuat(this.shot.t, j, shotPh, this._shotQ);
174
+ if (accW === 0) { this._acc.copy(this._shotQ); accW = 1; }
175
+ else this._acc.slerp(this._shotQ, shotW);
176
+ }
177
+ if (this.mask) {
178
+ const mw = this.maskFeather[j] * this.mask.w;
179
+ if (mw > 1e-3) {
180
+ sampleQuat(this.mask.t, j, 0, this._qT);
181
+ const variant = this.mask.k > 0 ? this.mask.up : this.mask.dn;
182
+ if (variant && Math.abs(this.mask.k) > 1e-3) {
183
+ sampleQuat(variant, j, 0, this._qV);
184
+ this._qT.slerp(this._qV, Math.abs(this.mask.k));
185
+ }
186
+ if (accW === 0) { this._acc.copy(this._qT); accW = 1; }
187
+ else this._acc.slerp(this._qT, mw);
188
+ }
189
+ }
190
+ rig.animNode(j).quaternion.copy(this._acc);
191
+ }
192
+
193
+ const hA = this._hA;
194
+ hA[0] = hA[1] = hA[2] = 0;
195
+ let accW = 0;
196
+ for (const [t, ph, wt] of this.parts) {
197
+ sampleHips(t, ph, this._hp);
198
+ for (let k = 0; k < 3; k++) hA[k] += this._hp[k] * wt;
199
+ accW += wt;
200
+ }
201
+ for (let k = 0; k < 3; k++) hA[k] /= accW || 1;
202
+ if (shotW > 0 && this.shot) {
203
+ sampleHips(this.shot.t, shotPh, this._shotH);
204
+ let y = this._shotH[1];
205
+ if (this.shot.yCap !== null) y = Math.min(y, this.set.base.hipsRestY + this.shot.yCap);
206
+ for (let k = 0; k < 3; k++) {
207
+ const v = k === 1 ? y : this._shotH[k];
208
+ hA[k] = hA[k] * (1 - shotW) + v * shotW;
209
+ }
210
+ }
211
+ rig.setHipsFromArdy(hA[0], hA[1], hA[2]);
212
+ rig.update(dt);
213
+ return { shotW };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Directional blend weights from a character-local command (ARDY frame:
219
+ * +z forward, +x LEFT), magnitude 0..1. Weights are split between the two
220
+ * angularly-nearest AVAILABLE directions (pass `available` to restrict to the
221
+ * gaits a data set actually has — e.g. the 4-cardinal sword fallback), so a
222
+ * missing diagonal folds onto its neighbors instead of dropping the command.
223
+ */
224
+ const DIR8 = [
225
+ ["forward", 0], ["forwardLeft", Math.PI / 4], ["strafeLeft", Math.PI / 2],
226
+ ["backLeft", (3 * Math.PI) / 4], ["back", Math.PI], ["backRight", -(3 * Math.PI) / 4],
227
+ ["strafeRight", -Math.PI / 2], ["forwardRight", -Math.PI / 4],
228
+ ];
229
+ export function dir8Weights(x, z, mag, available = null) {
230
+ const w = {};
231
+ if (mag < 1e-3) return w;
232
+ const dirs = available ? DIR8.filter(([n]) => available.includes(n)) : DIR8;
233
+ if (dirs.length === 0) return w;
234
+ const a = Math.atan2(x, z);
235
+ const ds = dirs.map(([n, da]) => {
236
+ let d = a - da;
237
+ while (d > Math.PI) d -= 2 * Math.PI;
238
+ while (d < -Math.PI) d += 2 * Math.PI;
239
+ return [n, Math.abs(d)];
240
+ }).sort((p, q) => p[1] - q[1]);
241
+ const [n0, d0] = ds[0];
242
+ if (ds.length === 1 || d0 < 1e-4) { w[n0] = mag; return w; }
243
+ const [n1, d1] = ds[1];
244
+ const span = d0 + d1;
245
+ w[n0] = (d1 / span) * mag;
246
+ w[n1] = (d0 / span) * mag;
247
+ return w;
248
+ }
249
+
250
+ /**
251
+ * PROPORTIONAL AIM-MASK HAND-OFF (AG-823 §7.3 #4). The mask target follows
252
+ * the smoothed COMMAND MAGNITUDE (`mag`) instead of a binary threshold+lag:
253
+ * the chest's spine-twist leaves exactly as the bladed idle's root yaw
254
+ * arrives, so a stop never visibly twists right then settles back (the old
255
+ * binary fade did, on every stop). Airborne or pitched aim keeps the mask
256
+ * fully on (the standing idle can't carry a pitched hold).
257
+ *
258
+ * maskW = aimMaskStep(maskW, { mag, grounded, pitch, dt });
259
+ * anim.setMask(pose, maskW, pitch / 0.6);
260
+ */
261
+ export function aimMaskStep(maskW, { mag, grounded, pitch, dt, aiming = true, tau = 0.06 }) {
262
+ const airOrPitch = !grounded || Math.abs(pitch) > 0.12 ? 1 : 0;
263
+ const target = aiming ? Math.max(Math.min(mag, 1), airOrPitch) : 0;
264
+ return maskW + (target - maskW) * (1 - Math.exp(-dt / tau));
265
+ }
@@ -0,0 +1,75 @@
1
+ // ik.js — minimal analytic two-bone IK (AG-803 Step C). Pins a hand to a
2
+ // weapon grip: law-of-cosines elbow, pole-vector bend plane, applied as
3
+ // world-space quaternion deltas onto the ANIMATED nodes (rig-agnostic — works
4
+ // on the VRM normalized rig and raw Mixamo/Meshy bones alike, because a world
5
+ // delta premultiplied through the node's own parent frame is rig-independent).
6
+ // weight scales the correction (slerp from identity) so IK can fade with the
7
+ // aim mask.
8
+ import * as THREE from "three";
9
+
10
+ const _A = new THREE.Vector3(), _E = new THREE.Vector3(), _W = new THREE.Vector3();
11
+ const _n = new THREE.Vector3(), _h = new THREE.Vector3(), _bend = new THREE.Vector3();
12
+ const _e = new THREE.Vector3(), _v0 = new THREE.Vector3(), _v1 = new THREE.Vector3();
13
+ const _delta = new THREE.Quaternion(), _pw = new THREE.Quaternion();
14
+ const _w0 = new THREE.Quaternion(), _id = new THREE.Quaternion();
15
+ const _dw = new THREE.Quaternion(); // scaled delta — MUST be distinct from _delta
16
+ // (callers pass _delta in; aliasing zeroed the IK once)
17
+
18
+ /** Premultiply a WORLD-space rotation delta (scaled by w) onto a bone. */
19
+ function applyWorldDelta(bone, delta, w) {
20
+ _dw.copy(_id).slerp(delta, w);
21
+ bone.getWorldQuaternion(_w0);
22
+ _w0.premultiply(_dw); // new world orientation
23
+ bone.parent.getWorldQuaternion(_pw).invert();
24
+ bone.quaternion.copy(_pw.multiply(_w0));
25
+ bone.updateMatrixWorld(true);
26
+ }
27
+
28
+ /** Blend a bone's WORLD orientation toward worldQuat (rotates in place —
29
+ * wrist position is untouched, so it composes with the position IK). */
30
+ export function setWorldQuat(bone, worldQuat, w = 1) {
31
+ if (!bone || w <= 1e-3) return;
32
+ bone.getWorldQuaternion(_w0).slerp(worldQuat, w);
33
+ bone.parent.getWorldQuaternion(_pw).invert();
34
+ bone.quaternion.copy(_pw.multiply(_w0));
35
+ bone.updateMatrixWorld(true);
36
+ }
37
+
38
+ /**
39
+ * upper/fore/hand: THREE nodes of the arm chain (shoulder->elbow->wrist).
40
+ * target: world-space Vector3 the wrist should reach.
41
+ * hintDir: world-space direction the elbow should bend toward (pole vector).
42
+ * weight: 0..1 correction strength.
43
+ */
44
+ export function twoBoneIK(upper, fore, hand, target, hintDir, weight = 1) {
45
+ if (!upper || !fore || !hand || weight <= 1e-3) return;
46
+ upper.getWorldPosition(_A);
47
+ fore.getWorldPosition(_E);
48
+ hand.getWorldPosition(_W);
49
+ const L1 = _A.distanceTo(_E), L2 = _E.distanceTo(_W);
50
+ if (L1 < 1e-5 || L2 < 1e-5) return;
51
+
52
+ _n.copy(target).sub(_A);
53
+ const d = Math.min(Math.max(_n.length(), Math.abs(L1 - L2) + 1e-4), L1 + L2 - 1e-4);
54
+ _n.normalize();
55
+ _h.copy(hintDir).normalize();
56
+ _bend.copy(_h).addScaledVector(_n, -_h.dot(_n));
57
+ if (_bend.lengthSq() < 1e-8) _bend.set(0, -1, 0).addScaledVector(_n, _n.y); // degenerate hint
58
+ _bend.normalize();
59
+
60
+ const cosA = Math.min(1, Math.max(-1, (L1 * L1 + d * d - L2 * L2) / (2 * L1 * d)));
61
+ const sinA = Math.sqrt(1 - cosA * cosA);
62
+ _e.copy(_A).addScaledVector(_n, L1 * cosA).addScaledVector(_bend, L1 * sinA);
63
+
64
+ // 1) upper arm: current elbow dir -> desired elbow dir
65
+ _v0.copy(_E).sub(_A).normalize();
66
+ _v1.copy(_e).sub(_A).normalize();
67
+ applyWorldDelta(upper, _delta.setFromUnitVectors(_v0, _v1), weight);
68
+
69
+ // 2) forearm: fresh positions after step 1, then wrist -> target
70
+ fore.getWorldPosition(_E);
71
+ hand.getWorldPosition(_W);
72
+ _v0.copy(_W).sub(_E).normalize();
73
+ _v1.copy(target).sub(_E).normalize();
74
+ applyWorldDelta(fore, _delta.setFromUnitVectors(_v0, _v1), weight);
75
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "_": "genex motion compile tunables — the 'change the formula' surface. Pass with --config; every field overrides the built-in default (values here ARE those defaults). rampFrames/tailSkip: frames skipped at a take's head (constraint ramp) and tail (post-processing wobble). idleLoop: the idle seam-search window in frames. headGazeTargetDeg: mean head pitch is LOWERED to this (a natural aiming gaze), never raised. aimPitchDeg: the Up/Dn aim-variant magnitude, spine-distributed. aimCandidates: how many aim mask poses are plucked (one per take). hipsClampM: tall-stance clamp threshold in meters above ARDY rest. minDirDot: minimum travel-direction agreement before a gait cycle is rejected.",
3
+ "rampFrames": 50,
4
+ "tailSkip": 10,
5
+ "idleLoop": { "minFrames": 40, "maxFrames": 70, "step": 2 },
6
+ "headGazeTargetDeg": 6,
7
+ "aimPitchDeg": 40,
8
+ "aimCandidates": 3,
9
+ "hipsClampM": 0.005,
10
+ "minDirDot": 0.9
11
+ }