@genex-ai/cli-demo 0.36.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 +11 -5
- package/dist/index.js +382 -28
- 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-ai-image/SKILL.md +181 -0
- package/templates/skills/genex-ai-video/SKILL.md +148 -0
- package/templates/skills/genex-getting-started/SKILL.md +4 -1
- 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 +9 -8
- 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
|
@@ -13,7 +13,10 @@ genex model "<prompt>" # generate a 3D model → prints an asset URL
|
|
|
13
13
|
genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
|
|
14
14
|
genex sfx "<prompt>" # generate a sound fx → prints an asset URL
|
|
15
15
|
genex texture "<prompt>" # generate a texture → prints an asset URL
|
|
16
|
+
genex image "<prompt>" # generate an image → prints an asset URL
|
|
17
|
+
genex video "<prompt>" # generate a video → prints an asset URL
|
|
16
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/
|
|
17
20
|
```
|
|
18
21
|
|
|
19
22
|
> **Invoking it.** First-time setup runs via `npx @genex-ai/cli-demo@latest init`.
|
|
@@ -72,8 +75,8 @@ Defaults: API `https://demo-api.glotech.world`, auth site
|
|
|
72
75
|
|
|
73
76
|
## Generating assets
|
|
74
77
|
|
|
75
|
-
`genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a
|
|
76
|
-
game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
|
|
78
|
+
`genex model | skybox | sfx | texture | image | video "<prompt>"` turn a prompt into a
|
|
79
|
+
real, game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
|
|
77
80
|
stream), then prints its public URL — the game loads it directly, nothing is
|
|
78
81
|
downloaded or committed:
|
|
79
82
|
|
|
@@ -83,6 +86,8 @@ downloaded or committed:
|
|
|
83
86
|
| `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
|
|
84
87
|
| `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
|
|
85
88
|
| `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
|
|
89
|
+
| `genex image "<prompt>" [--transparent] [--aspect <ratio>]` | fal.ai | `…/generations/<id>/image-main` |
|
|
90
|
+
| `genex video "<prompt>" [--duration <s>] [--loop]` | fal.ai | `…/generations/<id>/video-mp4` |
|
|
86
91
|
|
|
87
92
|
Each URL is a permanent `https://assets.genex.technology/...` address served straight
|
|
88
93
|
from R2 (with CORS, so three.js loads it cross-origin without tainting). It resolves
|
|
@@ -92,9 +97,10 @@ Three.js loader code.
|
|
|
92
97
|
|
|
93
98
|
Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
|
|
94
99
|
provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
|
|
95
|
-
`ELEVENLABS_API_KEY`, `GEMINI_API_KEY`); when a key
|
|
96
|
-
a built-in **mock** provider that returns sample
|
|
97
|
-
`--no-wait` enqueues without downloading
|
|
100
|
+
`ELEVENLABS_API_KEY`, `GEMINI_API_KEY`, and `FAL_KEY` for image + video); when a key
|
|
101
|
+
is unset that kind falls back to a built-in **mock** provider that returns sample
|
|
102
|
+
assets, so the flow runs keyless. `--no-wait` enqueues without downloading — useful
|
|
103
|
+
for video, which is minutes-class end-to-end.
|
|
98
104
|
|
|
99
105
|
## Install / run
|
|
100
106
|
|
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";
|
|
@@ -1790,6 +1799,10 @@ async function runGenerate(kind, opts) {
|
|
|
1790
1799
|
const options = {};
|
|
1791
1800
|
if (kind === "texture" && opts.terrain) options.terrain = true;
|
|
1792
1801
|
if (kind === "sfx" && opts.duration) options.durationSeconds = opts.duration;
|
|
1802
|
+
if (kind === "image" && opts.transparent) options.transparent = true;
|
|
1803
|
+
if (kind === "image" && opts.aspect) options.aspect = opts.aspect;
|
|
1804
|
+
if (kind === "video" && opts.duration) options.durationSeconds = opts.duration;
|
|
1805
|
+
if (kind === "video" && opts.loop) options.loop = true;
|
|
1793
1806
|
log.plain(c.bold(`genex ${kind}`));
|
|
1794
1807
|
log.dim(` ${prompt}`);
|
|
1795
1808
|
log.plain("");
|
|
@@ -1826,9 +1839,10 @@ async function runGenerate(kind, opts) {
|
|
|
1826
1839
|
}
|
|
1827
1840
|
log.step("Generating\u2026 (this can take up to a minute)");
|
|
1828
1841
|
const onProgress = (p) => log.dim(` ${p}%`);
|
|
1829
|
-
const
|
|
1842
|
+
const timeoutMs = waitTimeoutFor(kind);
|
|
1843
|
+
const deadline = Date.now() + timeoutMs;
|
|
1830
1844
|
const streamed = await waitViaSSE(apiUrl, token, id, onProgress, deadline);
|
|
1831
|
-
const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress) : streamed;
|
|
1845
|
+
const view = streamed === "unsupported" ? await poll(apiUrl, token, id, onProgress, timeoutMs) : streamed;
|
|
1832
1846
|
if (!view) {
|
|
1833
1847
|
log.error("Timed out waiting for the generation.");
|
|
1834
1848
|
process.exitCode = 1;
|
|
@@ -1855,6 +1869,11 @@ async function runGenerate(kind, opts) {
|
|
|
1855
1869
|
printHint(kind, files, log);
|
|
1856
1870
|
}
|
|
1857
1871
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1872
|
+
var KIND_WAIT_TIMEOUT_MS = {
|
|
1873
|
+
video: 15 * 60 * 1e3
|
|
1874
|
+
// 15 min
|
|
1875
|
+
};
|
|
1876
|
+
var waitTimeoutFor = (kind) => KIND_WAIT_TIMEOUT_MS[kind] ?? WAIT_TIMEOUT_MS;
|
|
1858
1877
|
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
1859
1878
|
async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
1860
1879
|
let connectFailures = 0;
|
|
@@ -1907,8 +1926,8 @@ async function waitViaSSE(apiUrl, token, id, onProgress, deadline) {
|
|
|
1907
1926
|
}
|
|
1908
1927
|
return null;
|
|
1909
1928
|
}
|
|
1910
|
-
async function poll(apiUrl, token, id, onProgress) {
|
|
1911
|
-
const deadline = Date.now() +
|
|
1929
|
+
async function poll(apiUrl, token, id, onProgress, timeoutMs) {
|
|
1930
|
+
const deadline = Date.now() + timeoutMs;
|
|
1912
1931
|
let last = -1;
|
|
1913
1932
|
while (Date.now() < deadline) {
|
|
1914
1933
|
try {
|
|
@@ -1937,15 +1956,314 @@ function printHint(kind, files, log) {
|
|
|
1937
1956
|
model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
|
|
1938
1957
|
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
|
|
1939
1958
|
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
|
|
1940
|
-
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role
|
|
1959
|
+
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`,
|
|
1960
|
+
image: `Load with TextureLoader (set colorSpace = SRGBColorSpace) onto any mesh/plane/sprite \u2014 see genex-ai-image. url = "${url}"`,
|
|
1961
|
+
video: `Wire an HTMLVideoElement (crossOrigin="anonymous", muted, loop, playsInline) into a THREE.VideoTexture \u2014 see genex-ai-video. url = "${url}"`
|
|
1941
1962
|
};
|
|
1942
1963
|
log.dim(` ${hint[kind]}`);
|
|
1943
1964
|
log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
|
|
1944
1965
|
}
|
|
1945
1966
|
|
|
1946
1967
|
// src/commands/controller.ts
|
|
1968
|
+
import fs12 from "fs/promises";
|
|
1969
|
+
import path13 from "path";
|
|
1970
|
+
|
|
1971
|
+
// src/lib/anims.ts
|
|
1947
1972
|
import fs11 from "fs/promises";
|
|
1948
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
|
|
1949
2267
|
var CONTROLLER_KINDS = ["character", "car", "drone"];
|
|
1950
2268
|
var SHARED = [
|
|
1951
2269
|
"shared/math.ts",
|
|
@@ -1964,6 +2282,7 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1964
2282
|
...SHARED,
|
|
1965
2283
|
"character/character-controller.ts",
|
|
1966
2284
|
"character/character-animations.ts",
|
|
2285
|
+
"character/animation-packs.ts",
|
|
1967
2286
|
"character/presets.ts",
|
|
1968
2287
|
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
1969
2288
|
// the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
|
|
@@ -1976,15 +2295,16 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1976
2295
|
],
|
|
1977
2296
|
// The player's VRM is written to public/assets/avatar.vrm at install time by
|
|
1978
2297
|
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
1979
|
-
// 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/.
|
|
1980
2300
|
assets: ["assets/animation-library.glb"],
|
|
1981
2301
|
skill: "genex-threejs-character-controller",
|
|
1982
2302
|
sketch: [
|
|
1983
2303
|
`const physics = await PhysicsWorld.create();`,
|
|
1984
2304
|
`const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
|
|
1985
|
-
`const
|
|
2305
|
+
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
1986
2306
|
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
1987
|
-
`character.root.add(scene); const anims = new CharacterAnimations(scene,
|
|
2307
|
+
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
1988
2308
|
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
|
|
1989
2309
|
]
|
|
1990
2310
|
},
|
|
@@ -2024,45 +2344,49 @@ var CONTROLLER_FILE_SETS = {
|
|
|
2024
2344
|
]
|
|
2025
2345
|
}
|
|
2026
2346
|
};
|
|
2027
|
-
var CODE_DEST =
|
|
2028
|
-
var ASSETS_DEST =
|
|
2347
|
+
var CODE_DEST = path13.join("src", "controllers");
|
|
2348
|
+
var ASSETS_DEST = path13.join("public", "assets");
|
|
2029
2349
|
async function runController(opts) {
|
|
2030
2350
|
const log = createLogger({ quiet: opts.quiet });
|
|
2351
|
+
if (opts.kind?.trim() === "anims") {
|
|
2352
|
+
await runAnims(opts);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2031
2355
|
const kind = opts.kind?.trim();
|
|
2032
2356
|
if (!kind || !CONTROLLER_KINDS.includes(kind)) {
|
|
2033
2357
|
log.error(
|
|
2034
2358
|
`Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
|
|
2035
2359
|
"genex controller <character|car|drone> [--force]"
|
|
2036
|
-
)}`
|
|
2360
|
+
)} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
|
|
2037
2361
|
);
|
|
2038
2362
|
process.exitCode = 1;
|
|
2039
2363
|
return;
|
|
2040
2364
|
}
|
|
2041
|
-
const srcDir =
|
|
2365
|
+
const srcDir = path13.join(getTemplatesDir(), "controllers");
|
|
2042
2366
|
const root = opts.cwd ?? process.cwd();
|
|
2043
2367
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
2044
2368
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
2045
2369
|
log.plain("");
|
|
2046
|
-
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)}`);
|
|
2047
2371
|
const plan = [
|
|
2048
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
2372
|
+
...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
|
|
2049
2373
|
...set.assets.map((rel) => ({
|
|
2050
2374
|
from: rel,
|
|
2051
|
-
rel:
|
|
2375
|
+
rel: path13.join(ASSETS_DEST, path13.basename(rel))
|
|
2052
2376
|
}))
|
|
2053
2377
|
];
|
|
2054
2378
|
let copied = 0;
|
|
2055
2379
|
let skipped = 0;
|
|
2056
2380
|
try {
|
|
2057
2381
|
for (const file of plan) {
|
|
2058
|
-
const dest =
|
|
2059
|
-
if (!opts.force && await
|
|
2382
|
+
const dest = path13.join(root, file.rel);
|
|
2383
|
+
if (!opts.force && await exists3(dest)) {
|
|
2060
2384
|
skipped++;
|
|
2061
2385
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
2062
2386
|
continue;
|
|
2063
2387
|
}
|
|
2064
|
-
await
|
|
2065
|
-
await
|
|
2388
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
2389
|
+
await fs12.copyFile(path13.join(srcDir, file.from), dest);
|
|
2066
2390
|
copied++;
|
|
2067
2391
|
log.dim(` ${file.rel}`);
|
|
2068
2392
|
}
|
|
@@ -2094,8 +2418,8 @@ async function runController(opts) {
|
|
|
2094
2418
|
}
|
|
2095
2419
|
async function installOwnerAvatar(args) {
|
|
2096
2420
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
2097
|
-
const dest =
|
|
2098
|
-
await
|
|
2421
|
+
const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
|
|
2422
|
+
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
2099
2423
|
if (token) {
|
|
2100
2424
|
try {
|
|
2101
2425
|
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -2107,7 +2431,7 @@ async function installOwnerAvatar(args) {
|
|
|
2107
2431
|
const vrmRes = await fetch(me.vrmUrl);
|
|
2108
2432
|
if (vrmRes.ok) {
|
|
2109
2433
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
2110
|
-
await
|
|
2434
|
+
await fs12.writeFile(dest, buf);
|
|
2111
2435
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
2112
2436
|
return;
|
|
2113
2437
|
}
|
|
@@ -2118,14 +2442,14 @@ async function installOwnerAvatar(args) {
|
|
|
2118
2442
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
2119
2443
|
}
|
|
2120
2444
|
}
|
|
2121
|
-
await
|
|
2445
|
+
await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
2122
2446
|
log.dim(
|
|
2123
2447
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
2124
2448
|
);
|
|
2125
2449
|
}
|
|
2126
|
-
async function
|
|
2450
|
+
async function exists3(p) {
|
|
2127
2451
|
try {
|
|
2128
|
-
await
|
|
2452
|
+
await fs12.access(p);
|
|
2129
2453
|
return true;
|
|
2130
2454
|
} catch {
|
|
2131
2455
|
return false;
|
|
@@ -2198,7 +2522,7 @@ function rank(items, query) {
|
|
|
2198
2522
|
}
|
|
2199
2523
|
|
|
2200
2524
|
// src/index.ts
|
|
2201
|
-
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
|
|
2525
|
+
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "image", "video"]);
|
|
2202
2526
|
var HELP = `${c.bold("genex")} \u2014 set up your ~/.claude workspace, authorize, and publish 3D games.
|
|
2203
2527
|
|
|
2204
2528
|
${c.bold("Usage")}
|
|
@@ -2214,15 +2538,24 @@ ${c.bold("Usage")}
|
|
|
2214
2538
|
genex skybox "<prompt>" [options] Generate a skybox (equirect) into public/assets/skybox.
|
|
2215
2539
|
genex sfx "<prompt>" [options] Generate a sound effect (mp3) into public/assets/sfx.
|
|
2216
2540
|
genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
|
|
2541
|
+
genex image "<prompt>" [options] Generate an image (PNG); prints a public asset URL.
|
|
2542
|
+
genex video "<prompt>" [options] Generate a video (mp4); prints a public asset URL.
|
|
2217
2543
|
genex controller <type> [--force] Install a physics controller (character|car|drone)
|
|
2218
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.
|
|
2219
2549
|
genex explore ["<query>"] [options] Search the curated community gallery \u2014 proven
|
|
2220
2550
|
Three.js systems you can clone or borrow parts
|
|
2221
2551
|
from. No query lists the whole catalog.
|
|
2222
2552
|
|
|
2223
|
-
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
2553
|
+
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `video`)")}
|
|
2224
2554
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
2225
|
-
--duration <sec> (sfx) target clip length in seconds.
|
|
2555
|
+
--duration <sec> (sfx, video) target clip length in seconds.
|
|
2556
|
+
--transparent (image) transparent background (alpha) \u2014 for decals/stickers.
|
|
2557
|
+
--aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
|
|
2558
|
+
--loop (video) generate a seamless loop.
|
|
2226
2559
|
--no-wait Enqueue only; don't block waiting for the asset.
|
|
2227
2560
|
--api-url <url> Override the API base URL.
|
|
2228
2561
|
--env <path> Token env file (default: ~/.genex/env).
|
|
@@ -2300,6 +2633,9 @@ ${c.bold("Examples")}
|
|
|
2300
2633
|
genex skybox "golden hour over a misty mountain range"
|
|
2301
2634
|
genex sfx "punchy laser zap" --duration 2
|
|
2302
2635
|
genex texture "mossy cracked cobblestone" --terrain
|
|
2636
|
+
genex image "retro arcade poster art" --aspect portrait
|
|
2637
|
+
genex image "neon graffiti tag, spray-paint style" --transparent
|
|
2638
|
+
genex video "swirling neon plasma, seamless loop" --loop
|
|
2303
2639
|
genex controller character
|
|
2304
2640
|
genex explore "grass"
|
|
2305
2641
|
genex explore
|
|
@@ -2326,6 +2662,7 @@ function parseArgs(argv) {
|
|
|
2326
2662
|
"--categories",
|
|
2327
2663
|
"--timeout",
|
|
2328
2664
|
"--duration",
|
|
2665
|
+
"--aspect",
|
|
2329
2666
|
"--source-repo-url",
|
|
2330
2667
|
"--source-author",
|
|
2331
2668
|
"--license",
|
|
@@ -2360,9 +2697,21 @@ function parseArgs(argv) {
|
|
|
2360
2697
|
case "--terrain":
|
|
2361
2698
|
parsed.options.terrain = true;
|
|
2362
2699
|
break;
|
|
2700
|
+
case "--transparent":
|
|
2701
|
+
parsed.options.transparent = true;
|
|
2702
|
+
break;
|
|
2703
|
+
case "--loop":
|
|
2704
|
+
parsed.options.loop = true;
|
|
2705
|
+
break;
|
|
2363
2706
|
case "--force":
|
|
2364
2707
|
parsed.options.force = true;
|
|
2365
2708
|
break;
|
|
2709
|
+
case "--list":
|
|
2710
|
+
parsed.options.list = true;
|
|
2711
|
+
break;
|
|
2712
|
+
case "--reset":
|
|
2713
|
+
parsed.options.reset = true;
|
|
2714
|
+
break;
|
|
2366
2715
|
case "--regenerate-cover":
|
|
2367
2716
|
parsed.options.regenerateCover = true;
|
|
2368
2717
|
break;
|
|
@@ -2392,6 +2741,8 @@ function parseArgs(argv) {
|
|
|
2392
2741
|
parsed.options.name = arg;
|
|
2393
2742
|
} else if (parsed.command === "explore") {
|
|
2394
2743
|
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
2744
|
+
} else if (parsed.command === "controller") {
|
|
2745
|
+
(parsed.options.selectors ??= []).push(arg);
|
|
2395
2746
|
} else {
|
|
2396
2747
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
2397
2748
|
return parsed;
|
|
@@ -2471,6 +2822,9 @@ function applyValueFlag(options, flag, value) {
|
|
|
2471
2822
|
options.duration = n;
|
|
2472
2823
|
break;
|
|
2473
2824
|
}
|
|
2825
|
+
case "--aspect":
|
|
2826
|
+
options.aspect = value;
|
|
2827
|
+
break;
|
|
2474
2828
|
}
|
|
2475
2829
|
}
|
|
2476
2830
|
async function main() {
|
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
|