@genex-ai/cli-demo 0.37.0 → 0.38.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/README.md +1 -0
- package/dist/index.js +344 -20
- package/package.json +1 -1
- package/templates/controllers/NOTICE.md +8 -5
- package/templates/controllers/assets/animation-library.glb +0 -0
- package/templates/controllers/assets/anims-manifest.json +1306 -0
- package/templates/controllers/character/animation-packs.ts +70 -0
- package/templates/controllers/character/character-animations.ts +51 -8
- package/templates/controllers/character/character-controller.ts +153 -5
- package/templates/controllers/character/keyboard-input.ts +12 -1
- package/templates/controllers/character/presets.ts +19 -1
- package/templates/controllers/character/vrm/foot-ik.ts +71 -15
- package/templates/controllers/character/vrm/vrm-retarget.ts +72 -12
- package/templates/skills/genex-threejs-character-controller/SKILL.md +55 -22
- package/templates/skills/genex-threejs-character-controller/references/animations.md +91 -46
- package/templates/skills/genex-threejs-character-controller/references/tuning-and-presets.md +3 -0
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +7 -6
- package/templates/skills/genex-threejs-skill-router/SKILL.md +1 -1
- package/templates/skills/genex-threejs-vehicle-controllers/references/enter-exit.md +3 -2
- package/templates/controllers/assets/character.glb +0 -0
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ genex texture "<prompt>" # generate a texture → prints an asset URL
|
|
|
16
16
|
genex image "<prompt>" # generate an image → prints an asset URL
|
|
17
17
|
genex video "<prompt>" # generate a video → prints an asset URL
|
|
18
18
|
genex controller <type> # install a tuned character|car|drone controller → src/controllers/
|
|
19
|
+
genex controller anims <sel…> # download extra character animation clips (by tag or name) → public/assets/anims/
|
|
19
20
|
```
|
|
20
21
|
|
|
21
22
|
> **Invoking it.** First-time setup runs via `npx @genex-ai/cli-demo@latest init`.
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,15 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
var DEFAULT_AUTH_URL = "https://demo-web.glotech.world";
|
|
12
12
|
var DEFAULT_API_URL = "https://demo-api.glotech.world";
|
|
13
13
|
var DEFAULT_COLYSEUS_URL = "wss://demo-colyseus.glotech.world";
|
|
14
|
+
var DEFAULT_ANIMS_BASE = "https://cdn.genex.technology/anims/ual1/v1/";
|
|
15
|
+
var ANIMS_BASE_ENV = "GENEX_ANIMS_BASE";
|
|
16
|
+
function getAnimsBase(override) {
|
|
17
|
+
const raw = override || process.env[ANIMS_BASE_ENV] || DEFAULT_ANIMS_BASE;
|
|
18
|
+
return raw.replace(/\/+$/, "") + "/";
|
|
19
|
+
}
|
|
20
|
+
function getAnimsCacheDir() {
|
|
21
|
+
return path.join(getGenexDir(), "cache", "anims");
|
|
22
|
+
}
|
|
14
23
|
var ENV_TOKEN_KEY = "GENEX_TOKEN";
|
|
15
24
|
var AUTH_URL_ENV = "GENEX_AUTH_URL";
|
|
16
25
|
var API_URL_ENV = "GENEX_API_URL";
|
|
@@ -1956,8 +1965,305 @@ function printHint(kind, files, log) {
|
|
|
1956
1965
|
}
|
|
1957
1966
|
|
|
1958
1967
|
// src/commands/controller.ts
|
|
1968
|
+
import fs12 from "fs/promises";
|
|
1969
|
+
import path13 from "path";
|
|
1970
|
+
|
|
1971
|
+
// src/lib/anims.ts
|
|
1959
1972
|
import fs11 from "fs/promises";
|
|
1960
1973
|
import path12 from "path";
|
|
1974
|
+
var ANIMS_DEST = path12.join("public", "assets", "anims");
|
|
1975
|
+
var HIDDEN_TAG = "reference";
|
|
1976
|
+
async function runAnims(opts) {
|
|
1977
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
1978
|
+
const root = opts.cwd ?? process.cwd();
|
|
1979
|
+
const selectors = opts.selectors ?? [];
|
|
1980
|
+
log.plain(c.bold("genex controller anims"));
|
|
1981
|
+
log.plain("");
|
|
1982
|
+
const { manifest, source } = await loadManifest(opts.animsBase);
|
|
1983
|
+
if (source === "snapshot") {
|
|
1984
|
+
log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
|
|
1985
|
+
}
|
|
1986
|
+
if (opts.list) {
|
|
1987
|
+
printCatalog(log, manifest, selectors);
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
const controllerMarker = path12.join(root, "src", "controllers", "character");
|
|
1991
|
+
if (!await exists2(controllerMarker)) {
|
|
1992
|
+
log.error(
|
|
1993
|
+
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
1994
|
+
);
|
|
1995
|
+
log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
|
|
1996
|
+
process.exitCode = 1;
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const destDir = path12.join(root, ANIMS_DEST);
|
|
2000
|
+
const gameManifestPath = path12.join(destDir, "manifest.json");
|
|
2001
|
+
if (opts.reset) {
|
|
2002
|
+
await fs11.rm(destDir, { recursive: true, force: true });
|
|
2003
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
|
|
2004
|
+
}
|
|
2005
|
+
if (selectors.length === 0) {
|
|
2006
|
+
const installed = await readGameManifest(gameManifestPath);
|
|
2007
|
+
if (installed === null || installed.clips.length === 0) {
|
|
2008
|
+
log.plain(" No animation packs installed yet.");
|
|
2009
|
+
} else {
|
|
2010
|
+
log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
|
|
2011
|
+
}
|
|
2012
|
+
log.plain("");
|
|
2013
|
+
log.plain(
|
|
2014
|
+
` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
|
|
2015
|
+
"genex controller anims --list"
|
|
2016
|
+
)}.`
|
|
2017
|
+
);
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
let resolved;
|
|
2021
|
+
try {
|
|
2022
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
2025
|
+
process.exitCode = 1;
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
const coreNames = new Set(manifest.core);
|
|
2029
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2030
|
+
let bundledSkips = 0;
|
|
2031
|
+
for (const entries of resolved.values()) {
|
|
2032
|
+
for (const entry of entries) {
|
|
2033
|
+
if (coreNames.has(entry.name)) {
|
|
2034
|
+
bundledSkips++;
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
byName.set(entry.name, entry);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2041
|
+
const cacheDir = path12.join(
|
|
2042
|
+
opts.cacheDir ?? getAnimsCacheDir(),
|
|
2043
|
+
`${manifest.library}-v${manifest.version}`
|
|
2044
|
+
);
|
|
2045
|
+
await fs11.mkdir(cacheDir, { recursive: true });
|
|
2046
|
+
await fs11.mkdir(destDir, { recursive: true });
|
|
2047
|
+
const base = getAnimsBase(opts.animsBase);
|
|
2048
|
+
let installedCount = 0;
|
|
2049
|
+
let presentCount = 0;
|
|
2050
|
+
let addedBytes = 0;
|
|
2051
|
+
const failures = [];
|
|
2052
|
+
for (const entry of wanted) {
|
|
2053
|
+
const dest = path12.join(destDir, entry.file);
|
|
2054
|
+
if (await hasSize(dest, entry.bytes)) {
|
|
2055
|
+
presentCount++;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
try {
|
|
2059
|
+
const cached = path12.join(cacheDir, entry.file);
|
|
2060
|
+
if (!await hasSize(cached, entry.bytes)) {
|
|
2061
|
+
const res = await fetch(base + entry.file);
|
|
2062
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2063
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
2064
|
+
await fs11.writeFile(cached, buf);
|
|
2065
|
+
}
|
|
2066
|
+
await fs11.copyFile(cached, dest);
|
|
2067
|
+
installedCount++;
|
|
2068
|
+
addedBytes += entry.bytes;
|
|
2069
|
+
log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
2070
|
+
} catch (err) {
|
|
2071
|
+
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
const previous = await readGameManifest(gameManifestPath);
|
|
2075
|
+
const union = new Set(previous?.clips ?? []);
|
|
2076
|
+
for (const entry of wanted) {
|
|
2077
|
+
if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
|
|
2078
|
+
}
|
|
2079
|
+
const gameManifest = {
|
|
2080
|
+
schema: 1,
|
|
2081
|
+
library: manifest.library,
|
|
2082
|
+
version: manifest.version,
|
|
2083
|
+
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
2084
|
+
};
|
|
2085
|
+
await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
2086
|
+
log.plain("");
|
|
2087
|
+
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
2088
|
+
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
2089
|
+
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
2090
|
+
log.success(
|
|
2091
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
2092
|
+
);
|
|
2093
|
+
for (const [selector, entries] of resolved) {
|
|
2094
|
+
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
2095
|
+
if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
|
|
2096
|
+
}
|
|
2097
|
+
log.info(
|
|
2098
|
+
`Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
|
|
2099
|
+
);
|
|
2100
|
+
if (failures.length > 0) {
|
|
2101
|
+
log.plain("");
|
|
2102
|
+
log.error(
|
|
2103
|
+
`${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
|
|
2104
|
+
);
|
|
2105
|
+
log.plain(
|
|
2106
|
+
" Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
|
|
2107
|
+
);
|
|
2108
|
+
process.exitCode = 1;
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
async function loadManifest(baseOverride) {
|
|
2112
|
+
const base = getAnimsBase(baseOverride);
|
|
2113
|
+
try {
|
|
2114
|
+
const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
|
|
2115
|
+
if (res.ok) {
|
|
2116
|
+
const manifest2 = await res.json();
|
|
2117
|
+
if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
|
|
2118
|
+
return { manifest: manifest2, source: "cdn" };
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
} catch {
|
|
2122
|
+
}
|
|
2123
|
+
const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
2124
|
+
const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
|
|
2125
|
+
return { manifest, source: "snapshot" };
|
|
2126
|
+
}
|
|
2127
|
+
function resolveSelectors(manifest, selectors) {
|
|
2128
|
+
const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
|
|
2129
|
+
const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
|
|
2130
|
+
const tags = /* @__PURE__ */ new Map();
|
|
2131
|
+
for (const entry of manifest.clips) {
|
|
2132
|
+
for (const tag of entry.tags) {
|
|
2133
|
+
const list = tags.get(tag) ?? [];
|
|
2134
|
+
list.push(entry);
|
|
2135
|
+
tags.set(tag, list);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
const out = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const selector of selectors) {
|
|
2140
|
+
const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
|
|
2141
|
+
if (exact) {
|
|
2142
|
+
out.set(selector, [exact]);
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
|
|
2146
|
+
if (tagHit) {
|
|
2147
|
+
out.set(selector, tagHit);
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
const candidates = [...tags.keys(), ...byName.keys()];
|
|
2151
|
+
const close = suggest(selector, candidates);
|
|
2152
|
+
throw new Error(
|
|
2153
|
+
`unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
|
|
2154
|
+
"genex controller anims --list"
|
|
2155
|
+
)} for the catalog.`
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return out;
|
|
2159
|
+
}
|
|
2160
|
+
function suggest(input, candidates) {
|
|
2161
|
+
const lower = input.toLowerCase();
|
|
2162
|
+
const scored = [];
|
|
2163
|
+
for (const candidate of candidates) {
|
|
2164
|
+
const candidateLower = candidate.toLowerCase();
|
|
2165
|
+
if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
|
|
2166
|
+
scored.push({ name: candidate, score: 0 });
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
const distance = levenshtein(lower, candidateLower, 2);
|
|
2170
|
+
if (distance <= 2) scored.push({ name: candidate, score: distance });
|
|
2171
|
+
}
|
|
2172
|
+
scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
|
2173
|
+
return scored.slice(0, 4).map((s) => s.name);
|
|
2174
|
+
}
|
|
2175
|
+
function levenshtein(a, b, max) {
|
|
2176
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
2177
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
2178
|
+
for (let i = 1; i <= a.length; i++) {
|
|
2179
|
+
const curr = [i];
|
|
2180
|
+
let rowMin = i;
|
|
2181
|
+
for (let j = 1; j <= b.length; j++) {
|
|
2182
|
+
curr[j] = Math.min(
|
|
2183
|
+
prev[j] + 1,
|
|
2184
|
+
curr[j - 1] + 1,
|
|
2185
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
2186
|
+
);
|
|
2187
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
2188
|
+
}
|
|
2189
|
+
if (rowMin > max) return max + 1;
|
|
2190
|
+
prev = curr;
|
|
2191
|
+
}
|
|
2192
|
+
return prev[b.length];
|
|
2193
|
+
}
|
|
2194
|
+
function printCatalog(log, manifest, selectors) {
|
|
2195
|
+
const coreNames = new Set(manifest.core);
|
|
2196
|
+
if (selectors.length > 0) {
|
|
2197
|
+
let resolved;
|
|
2198
|
+
try {
|
|
2199
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
2200
|
+
} catch (err) {
|
|
2201
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
2202
|
+
process.exitCode = 1;
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
for (const [selector, entries] of resolved) {
|
|
2206
|
+
log.plain(c.bold(selector));
|
|
2207
|
+
for (const entry of entries) {
|
|
2208
|
+
const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
|
|
2209
|
+
log.plain(
|
|
2210
|
+
` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const tags = /* @__PURE__ */ new Map();
|
|
2217
|
+
for (const entry of manifest.clips) {
|
|
2218
|
+
for (const tag of entry.tags) {
|
|
2219
|
+
if (tag === HIDDEN_TAG) continue;
|
|
2220
|
+
const list = tags.get(tag) ?? [];
|
|
2221
|
+
list.push(entry);
|
|
2222
|
+
tags.set(tag, list);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
log.plain(
|
|
2226
|
+
`${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
|
|
2227
|
+
);
|
|
2228
|
+
log.plain(
|
|
2229
|
+
` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
|
|
2230
|
+
"genex controller anims --list <tag>"
|
|
2231
|
+
)}`
|
|
2232
|
+
);
|
|
2233
|
+
log.plain("");
|
|
2234
|
+
for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
2235
|
+
const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
2236
|
+
const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
|
|
2237
|
+
log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
async function readGameManifest(file) {
|
|
2241
|
+
try {
|
|
2242
|
+
return JSON.parse(await fs11.readFile(file, "utf8"));
|
|
2243
|
+
} catch {
|
|
2244
|
+
return null;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
async function hasSize(file, bytes) {
|
|
2248
|
+
try {
|
|
2249
|
+
return (await fs11.stat(file)).size === bytes;
|
|
2250
|
+
} catch {
|
|
2251
|
+
return false;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
async function exists2(p) {
|
|
2255
|
+
try {
|
|
2256
|
+
await fs11.access(p);
|
|
2257
|
+
return true;
|
|
2258
|
+
} catch {
|
|
2259
|
+
return false;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
function formatMb(bytes) {
|
|
2263
|
+
return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// src/commands/controller.ts
|
|
1961
2267
|
var CONTROLLER_KINDS = ["character", "car", "drone"];
|
|
1962
2268
|
var SHARED = [
|
|
1963
2269
|
"shared/math.ts",
|
|
@@ -1976,6 +2282,7 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1976
2282
|
...SHARED,
|
|
1977
2283
|
"character/character-controller.ts",
|
|
1978
2284
|
"character/character-animations.ts",
|
|
2285
|
+
"character/animation-packs.ts",
|
|
1979
2286
|
"character/presets.ts",
|
|
1980
2287
|
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
1981
2288
|
// the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
|
|
@@ -1988,15 +2295,16 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1988
2295
|
],
|
|
1989
2296
|
// The player's VRM is written to public/assets/avatar.vrm at install time by
|
|
1990
2297
|
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
1991
|
-
// a static manifest asset. animation-library.glb (
|
|
2298
|
+
// a static manifest asset. animation-library.glb (the 12-clip core) still is;
|
|
2299
|
+
// extra clips arrive via `genex controller anims` into public/assets/anims/.
|
|
1992
2300
|
assets: ["assets/animation-library.glb"],
|
|
1993
2301
|
skill: "genex-threejs-character-controller",
|
|
1994
2302
|
sketch: [
|
|
1995
2303
|
`const physics = await PhysicsWorld.create();`,
|
|
1996
2304
|
`const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
|
|
1997
|
-
`const
|
|
2305
|
+
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
1998
2306
|
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
1999
|
-
`character.root.add(scene); const anims = new CharacterAnimations(scene,
|
|
2307
|
+
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
2000
2308
|
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
|
|
2001
2309
|
]
|
|
2002
2310
|
},
|
|
@@ -2036,45 +2344,49 @@ var CONTROLLER_FILE_SETS = {
|
|
|
2036
2344
|
]
|
|
2037
2345
|
}
|
|
2038
2346
|
};
|
|
2039
|
-
var CODE_DEST =
|
|
2040
|
-
var ASSETS_DEST =
|
|
2347
|
+
var CODE_DEST = path13.join("src", "controllers");
|
|
2348
|
+
var ASSETS_DEST = path13.join("public", "assets");
|
|
2041
2349
|
async function runController(opts) {
|
|
2042
2350
|
const log = createLogger({ quiet: opts.quiet });
|
|
2351
|
+
if (opts.kind?.trim() === "anims") {
|
|
2352
|
+
await runAnims(opts);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2043
2355
|
const kind = opts.kind?.trim();
|
|
2044
2356
|
if (!kind || !CONTROLLER_KINDS.includes(kind)) {
|
|
2045
2357
|
log.error(
|
|
2046
2358
|
`Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
|
|
2047
2359
|
"genex controller <character|car|drone> [--force]"
|
|
2048
|
-
)}`
|
|
2360
|
+
)} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
|
|
2049
2361
|
);
|
|
2050
2362
|
process.exitCode = 1;
|
|
2051
2363
|
return;
|
|
2052
2364
|
}
|
|
2053
|
-
const srcDir =
|
|
2365
|
+
const srcDir = path13.join(getTemplatesDir(), "controllers");
|
|
2054
2366
|
const root = opts.cwd ?? process.cwd();
|
|
2055
2367
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
2056
2368
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
2057
2369
|
log.plain("");
|
|
2058
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
2370
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
|
|
2059
2371
|
const plan = [
|
|
2060
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
2372
|
+
...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
|
|
2061
2373
|
...set.assets.map((rel) => ({
|
|
2062
2374
|
from: rel,
|
|
2063
|
-
rel:
|
|
2375
|
+
rel: path13.join(ASSETS_DEST, path13.basename(rel))
|
|
2064
2376
|
}))
|
|
2065
2377
|
];
|
|
2066
2378
|
let copied = 0;
|
|
2067
2379
|
let skipped = 0;
|
|
2068
2380
|
try {
|
|
2069
2381
|
for (const file of plan) {
|
|
2070
|
-
const dest =
|
|
2071
|
-
if (!opts.force && await
|
|
2382
|
+
const dest = path13.join(root, file.rel);
|
|
2383
|
+
if (!opts.force && await exists3(dest)) {
|
|
2072
2384
|
skipped++;
|
|
2073
2385
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
2074
2386
|
continue;
|
|
2075
2387
|
}
|
|
2076
|
-
await
|
|
2077
|
-
await
|
|
2388
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
2389
|
+
await fs12.copyFile(path13.join(srcDir, file.from), dest);
|
|
2078
2390
|
copied++;
|
|
2079
2391
|
log.dim(` ${file.rel}`);
|
|
2080
2392
|
}
|
|
@@ -2106,8 +2418,8 @@ async function runController(opts) {
|
|
|
2106
2418
|
}
|
|
2107
2419
|
async function installOwnerAvatar(args) {
|
|
2108
2420
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
2109
|
-
const dest =
|
|
2110
|
-
await
|
|
2421
|
+
const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
|
|
2422
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
2111
2423
|
if (token) {
|
|
2112
2424
|
try {
|
|
2113
2425
|
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -2119,7 +2431,7 @@ async function installOwnerAvatar(args) {
|
|
|
2119
2431
|
const vrmRes = await fetch(me.vrmUrl);
|
|
2120
2432
|
if (vrmRes.ok) {
|
|
2121
2433
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
2122
|
-
await
|
|
2434
|
+
await fs12.writeFile(dest, buf);
|
|
2123
2435
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
2124
2436
|
return;
|
|
2125
2437
|
}
|
|
@@ -2130,14 +2442,14 @@ async function installOwnerAvatar(args) {
|
|
|
2130
2442
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
2131
2443
|
}
|
|
2132
2444
|
}
|
|
2133
|
-
await
|
|
2445
|
+
await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
2134
2446
|
log.dim(
|
|
2135
2447
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
2136
2448
|
);
|
|
2137
2449
|
}
|
|
2138
|
-
async function
|
|
2450
|
+
async function exists3(p) {
|
|
2139
2451
|
try {
|
|
2140
|
-
await
|
|
2452
|
+
await fs12.access(p);
|
|
2141
2453
|
return true;
|
|
2142
2454
|
} catch {
|
|
2143
2455
|
return false;
|
|
@@ -2230,6 +2542,10 @@ ${c.bold("Usage")}
|
|
|
2230
2542
|
genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
|
|
2231
2543
|
genex controller <type> [--force] Install a physics controller (character|car|drone)
|
|
2232
2544
|
into src/controllers (+ assets into public/assets).
|
|
2545
|
+
genex controller anims <sel \u2026> Download extra character animation clips by tag or
|
|
2546
|
+
exact name (sword, stealth, Celebration, \u2026) into
|
|
2547
|
+
public/assets/anims/. --list shows the catalog;
|
|
2548
|
+
--reset forgets the previous selection first.
|
|
2233
2549
|
genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
|
|
2234
2550
|
Three.js systems you can clone or borrow parts
|
|
2235
2551
|
from. No query lists the whole catalog.
|
|
@@ -2390,6 +2706,12 @@ function parseArgs(argv) {
|
|
|
2390
2706
|
case "--force":
|
|
2391
2707
|
parsed.options.force = true;
|
|
2392
2708
|
break;
|
|
2709
|
+
case "--list":
|
|
2710
|
+
parsed.options.list = true;
|
|
2711
|
+
break;
|
|
2712
|
+
case "--reset":
|
|
2713
|
+
parsed.options.reset = true;
|
|
2714
|
+
break;
|
|
2393
2715
|
case "--regenerate-cover":
|
|
2394
2716
|
parsed.options.regenerateCover = true;
|
|
2395
2717
|
break;
|
|
@@ -2419,6 +2741,8 @@ function parseArgs(argv) {
|
|
|
2419
2741
|
parsed.options.name = arg;
|
|
2420
2742
|
} else if (parsed.command === "explore") {
|
|
2421
2743
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
2744
|
+
} else if (parsed.command === "controller") {
|
|
2745
|
+
(parsed.options.selectors ??= []).push(arg);
|
|
2422
2746
|
} else {
|
|
2423
2747
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
2424
2748
|
return parsed;
|
package/package.json
CHANGED
|
@@ -33,15 +33,18 @@ Every ported source file carries its own SPDX header
|
|
|
33
33
|
(`SPDX-FileCopyrightText: 2023-2026 Erdong Chen`,
|
|
34
34
|
`SPDX-License-Identifier: MIT`).
|
|
35
35
|
|
|
36
|
-
## Quaternius Universal Animation Library — CC0 1.0 Universal
|
|
36
|
+
## Quaternius Universal Animation Library [Pro] — CC0 1.0 Universal
|
|
37
37
|
|
|
38
|
-
- `assets/animation-library.glb` — the
|
|
39
|
-
**Quaternius** (quaternius.com):
|
|
40
|
-
|
|
38
|
+
- `assets/animation-library.glb` — the bundled 12-clip core, cut from the
|
|
39
|
+
**Universal Animation Library [Pro]** by **Quaternius** (quaternius.com):
|
|
40
|
+
120 humanoid animation clips on a UE-mannequin-style skeleton.
|
|
41
|
+
- `public/assets/anims/*.glb` — additional per-clip cuts from the same
|
|
42
|
+
library, installed on demand by `npx genex controller anims`.
|
|
41
43
|
|
|
42
44
|
Dedicated to the public domain under the Creative Commons CC0 1.0 Universal
|
|
43
45
|
license (SPDX: `CC0-1.0`). No attribution is required by the license; this
|
|
44
|
-
notice is provided as a courtesy
|
|
46
|
+
notice is provided as a courtesy — supporting Quaternius on Patreon is a nice
|
|
47
|
+
way to say thanks.
|
|
45
48
|
|
|
46
49
|
## Default avatar — CC0 1.0 Universal
|
|
47
50
|
|
|
Binary file
|