@genex-ai/cli-demo 0.61.0-dev.141 → 0.62.0-dev.144
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 +5 -2
- package/dist/index.js +1199 -878
- package/package.json +1 -1
- package/templates/controllers/character/meshy/meshy-loader.ts +15 -1
- package/templates/skills/genex-ai-character/SKILL.md +19 -4
- package/templates/skills/genex-ai-menu/SKILL.md +13 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +11 -0
- package/templates/skills/genex-threejs-character-controller/references/animations.md +5 -0
- package/templates/skills/genex-threejs-game-content/SKILL.md +189 -0
- package/templates/skills/genex-threejs-game-content/references/content-tables.md +269 -0
- package/templates/skills/genex-threejs-open-world/SKILL.md +149 -0
- package/templates/skills/genex-threejs-open-world/references/terrain-streaming.md +215 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +20 -0
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +38 -4
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +4 -0
package/dist/index.js
CHANGED
|
@@ -1929,11 +1929,69 @@ async function runMakeRemixable(opts) {
|
|
|
1929
1929
|
}
|
|
1930
1930
|
|
|
1931
1931
|
// src/lib/detect-features.ts
|
|
1932
|
+
import fs11 from "fs/promises";
|
|
1933
|
+
import path12 from "path";
|
|
1934
|
+
|
|
1935
|
+
// src/lib/generation-ledger.ts
|
|
1932
1936
|
import fs10 from "fs/promises";
|
|
1933
1937
|
import path11 from "path";
|
|
1938
|
+
var ledgerPath = (cwd) => path11.join(cwd, ".genex", "generations.ndjson");
|
|
1939
|
+
async function append(cwd, event) {
|
|
1940
|
+
try {
|
|
1941
|
+
await fs10.access(path11.join(cwd, ".genex"));
|
|
1942
|
+
await fs10.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
1943
|
+
`, "utf8");
|
|
1944
|
+
} catch {
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
async function readLedger(cwd = process.cwd()) {
|
|
1948
|
+
let raw;
|
|
1949
|
+
try {
|
|
1950
|
+
raw = await fs10.readFile(ledgerPath(cwd), "utf8");
|
|
1951
|
+
} catch {
|
|
1952
|
+
return [];
|
|
1953
|
+
}
|
|
1954
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1955
|
+
for (const line of raw.split("\n")) {
|
|
1956
|
+
if (!line.trim()) continue;
|
|
1957
|
+
try {
|
|
1958
|
+
const e = JSON.parse(line);
|
|
1959
|
+
if (e.t === "q" && typeof e.id === "string" && typeof e.kind === "string") {
|
|
1960
|
+
if (!entries.has(e.id)) {
|
|
1961
|
+
entries.set(e.id, {
|
|
1962
|
+
id: e.id,
|
|
1963
|
+
kind: e.kind,
|
|
1964
|
+
prompt: typeof e.prompt === "string" ? e.prompt : "",
|
|
1965
|
+
queuedAt: typeof e.at === "string" ? e.at : "",
|
|
1966
|
+
status: "queued"
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
} else if (e.t === "done" && typeof e.id === "string") {
|
|
1970
|
+
const entry = entries.get(e.id);
|
|
1971
|
+
if (entry && (e.status === "completed" || e.status === "failed")) {
|
|
1972
|
+
entry.status = e.status;
|
|
1973
|
+
if (Array.isArray(e.urls)) entry.urls = e.urls.filter((u) => typeof u === "string");
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
} catch {
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
return [...entries.values()];
|
|
1980
|
+
}
|
|
1981
|
+
async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
|
|
1982
|
+
await append(cwd, { t: "q", id, kind, prompt: prompt.slice(0, 120), at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1983
|
+
}
|
|
1984
|
+
async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
1985
|
+
await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
|
|
1986
|
+
}
|
|
1987
|
+
async function countFailed(kind, cwd = process.cwd()) {
|
|
1988
|
+
return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/lib/detect-features.ts
|
|
1934
1992
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
1935
1993
|
try {
|
|
1936
|
-
const raw = await
|
|
1994
|
+
const raw = await fs11.readFile(path12.join(cwd, "package.json"), "utf8");
|
|
1937
1995
|
const pkg = JSON.parse(raw);
|
|
1938
1996
|
const version = pkg.dependencies?.["@genex-ai/embed-sdk"] ?? pkg.devDependencies?.["@genex-ai/embed-sdk"];
|
|
1939
1997
|
return typeof version === "string" && version ? version : null;
|
|
@@ -1943,7 +2001,7 @@ async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
|
1943
2001
|
}
|
|
1944
2002
|
async function detectMultiplayer(cwd = process.cwd()) {
|
|
1945
2003
|
try {
|
|
1946
|
-
const raw = await
|
|
2004
|
+
const raw = await fs11.readFile(path12.join(cwd, "package.json"), "utf8");
|
|
1947
2005
|
const pkg = JSON.parse(raw);
|
|
1948
2006
|
return Boolean(
|
|
1949
2007
|
pkg.dependencies?.["@genex-ai/multiplayer"] ?? pkg.devDependencies?.["@genex-ai/multiplayer"]
|
|
@@ -1955,7 +2013,7 @@ async function detectMultiplayer(cwd = process.cwd()) {
|
|
|
1955
2013
|
async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
1956
2014
|
let pkg;
|
|
1957
2015
|
try {
|
|
1958
|
-
pkg = JSON.parse(await
|
|
2016
|
+
pkg = JSON.parse(await fs11.readFile(path12.join(cwd, "package.json"), "utf8"));
|
|
1959
2017
|
} catch (err) {
|
|
1960
2018
|
log.dim(` (skipping matchmaking \u2014 couldn't read package.json: ${String(err)})`);
|
|
1961
2019
|
return null;
|
|
@@ -1972,10 +2030,10 @@ async function detectMatchmaking(log, cwd = process.cwd()) {
|
|
|
1972
2030
|
}
|
|
1973
2031
|
var GAME_STATE_CALLS = /savePlayerState|saveWorldState|submitScore/;
|
|
1974
2032
|
async function detectGameStateUsage(cwd = process.cwd()) {
|
|
1975
|
-
const srcDir =
|
|
2033
|
+
const srcDir = path12.join(cwd, "src");
|
|
1976
2034
|
let entries;
|
|
1977
2035
|
try {
|
|
1978
|
-
entries = await
|
|
2036
|
+
entries = await fs11.readdir(srcDir, { recursive: true });
|
|
1979
2037
|
} catch {
|
|
1980
2038
|
return false;
|
|
1981
2039
|
}
|
|
@@ -1983,7 +2041,7 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1983
2041
|
if (rel.includes("node_modules")) continue;
|
|
1984
2042
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
1985
2043
|
try {
|
|
1986
|
-
const content = await
|
|
2044
|
+
const content = await fs11.readFile(path12.join(srcDir, rel), "utf8");
|
|
1987
2045
|
if (GAME_STATE_CALLS.test(content)) return true;
|
|
1988
2046
|
} catch {
|
|
1989
2047
|
}
|
|
@@ -1993,14 +2051,14 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1993
2051
|
var UI_PHASE_MARKERS = /data-phase|setPhase/;
|
|
1994
2052
|
async function detectUiPhases(cwd = process.cwd()) {
|
|
1995
2053
|
try {
|
|
1996
|
-
const html = await
|
|
2054
|
+
const html = await fs11.readFile(path12.join(cwd, "index.html"), "utf8").catch(() => "");
|
|
1997
2055
|
if (UI_PHASE_MARKERS.test(html)) return true;
|
|
1998
|
-
const srcDir =
|
|
1999
|
-
const entries = await
|
|
2056
|
+
const srcDir = path12.join(cwd, "src");
|
|
2057
|
+
const entries = await fs11.readdir(srcDir, { recursive: true });
|
|
2000
2058
|
for (const rel of entries) {
|
|
2001
2059
|
if (rel.includes("node_modules")) continue;
|
|
2002
2060
|
if (!/\.(ts|js|mts|mjs|tsx|jsx|html|css)$/.test(rel)) continue;
|
|
2003
|
-
const content = await
|
|
2061
|
+
const content = await fs11.readFile(path12.join(srcDir, rel), "utf8").catch(() => "");
|
|
2004
2062
|
if (UI_PHASE_MARKERS.test(content)) return true;
|
|
2005
2063
|
}
|
|
2006
2064
|
return false;
|
|
@@ -2045,17 +2103,17 @@ var lineOf = (content, index) => content.slice(0, index).split("\n").length;
|
|
|
2045
2103
|
var DEPTH_RATIO_LIMIT = 1e6;
|
|
2046
2104
|
async function detectSurfaceScan(cwd = process.cwd()) {
|
|
2047
2105
|
const found = { guessedRepeat: [], squarePoints: [], depthRange: [] };
|
|
2048
|
-
const srcDir =
|
|
2106
|
+
const srcDir = path12.join(cwd, "src");
|
|
2049
2107
|
let entries;
|
|
2050
2108
|
try {
|
|
2051
|
-
entries = await
|
|
2109
|
+
entries = await fs11.readdir(srcDir, { recursive: true });
|
|
2052
2110
|
} catch {
|
|
2053
2111
|
return found;
|
|
2054
2112
|
}
|
|
2055
2113
|
for (const rel of entries) {
|
|
2056
2114
|
if (rel.includes("node_modules")) continue;
|
|
2057
2115
|
if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(rel)) continue;
|
|
2058
|
-
const raw = await
|
|
2116
|
+
const raw = await fs11.readFile(path12.join(srcDir, rel), "utf8").catch(() => "");
|
|
2059
2117
|
if (!raw) continue;
|
|
2060
2118
|
const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
|
|
2061
2119
|
const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
|
|
@@ -2093,6 +2151,54 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
2093
2151
|
}
|
|
2094
2152
|
return found;
|
|
2095
2153
|
}
|
|
2154
|
+
var WIRED_BY_URL = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "video"]);
|
|
2155
|
+
var UNPICKED_AFTER_MS = 15 * 60 * 1e3;
|
|
2156
|
+
async function detectGenerationAudit(cwd = process.cwd()) {
|
|
2157
|
+
const ledger = await readLedger(cwd);
|
|
2158
|
+
const audited = ledger.filter((e) => WIRED_BY_URL.has(e.kind));
|
|
2159
|
+
if (audited.length === 0) return { unwired: [], unpicked: [] };
|
|
2160
|
+
let haystack = "";
|
|
2161
|
+
const read = async (file) => {
|
|
2162
|
+
try {
|
|
2163
|
+
haystack += await fs11.readFile(file, "utf8");
|
|
2164
|
+
} catch {
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
try {
|
|
2168
|
+
for (const entry of await fs11.readdir(cwd, { withFileTypes: true })) {
|
|
2169
|
+
if (entry.isFile() && /\.(ts|tsx|js|jsx|css|html|json)$/.test(entry.name)) {
|
|
2170
|
+
await read(path12.join(cwd, entry.name));
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
} catch {
|
|
2174
|
+
}
|
|
2175
|
+
for (const sub of ["src", "public"]) {
|
|
2176
|
+
try {
|
|
2177
|
+
const entries = await fs11.readdir(path12.join(cwd, sub), { recursive: true });
|
|
2178
|
+
for (const rel of entries) {
|
|
2179
|
+
if (rel.includes("node_modules")) continue;
|
|
2180
|
+
if (!/\.(ts|tsx|js|jsx|css|html|json|txt)$/.test(rel)) continue;
|
|
2181
|
+
await read(path12.join(cwd, sub, rel));
|
|
2182
|
+
}
|
|
2183
|
+
} catch {
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
const ref = ({ id, kind, prompt }) => ({
|
|
2187
|
+
id,
|
|
2188
|
+
kind,
|
|
2189
|
+
prompt
|
|
2190
|
+
});
|
|
2191
|
+
const now = Date.now();
|
|
2192
|
+
return {
|
|
2193
|
+
unwired: audited.filter((e) => e.status === "completed" && !haystack.includes(e.id)).map(ref),
|
|
2194
|
+
// An id already referenced in the game was obviously picked up — a stale
|
|
2195
|
+
// "queued" record (e.g. `genex wait` ran from another directory) must not
|
|
2196
|
+
// nag about an asset that's wired in and working.
|
|
2197
|
+
unpicked: audited.filter(
|
|
2198
|
+
(e) => e.status === "queued" && !haystack.includes(e.id) && now - Date.parse(e.queuedAt) > UNPICKED_AFTER_MS
|
|
2199
|
+
).map(ref)
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2096
2202
|
async function detectFeatures(log, cwd = process.cwd()) {
|
|
2097
2203
|
return {
|
|
2098
2204
|
embedSdkVersion: await detectEmbedSdkVersion(cwd),
|
|
@@ -2100,7 +2206,8 @@ async function detectFeatures(log, cwd = process.cwd()) {
|
|
|
2100
2206
|
matchmaking: await detectMatchmaking(log, cwd),
|
|
2101
2207
|
gameStateUsed: await detectGameStateUsage(cwd),
|
|
2102
2208
|
uiPhases: await detectUiPhases(cwd),
|
|
2103
|
-
surfaces: await detectSurfaceScan(cwd)
|
|
2209
|
+
surfaces: await detectSurfaceScan(cwd),
|
|
2210
|
+
generations: await detectGenerationAudit(cwd)
|
|
2104
2211
|
};
|
|
2105
2212
|
}
|
|
2106
2213
|
function advisoryNudges(log, d) {
|
|
@@ -2125,6 +2232,20 @@ function advisoryNudges(log, d) {
|
|
|
2125
2232
|
);
|
|
2126
2233
|
}
|
|
2127
2234
|
surfaceNudges(log, d.surfaces);
|
|
2235
|
+
generationNudges(log, d.generations);
|
|
2236
|
+
}
|
|
2237
|
+
function generationNudges(log, g) {
|
|
2238
|
+
const describe = (refs) => refs.map((r) => `${r.kind} ${r.id} ("${r.prompt.slice(0, 48)}")`).join("; ");
|
|
2239
|
+
if (g.unpicked.length) {
|
|
2240
|
+
log.warn(
|
|
2241
|
+
`${g.unpicked.length} generation(s) enqueued 15+ minutes ago were never picked up: ${describe(g.unpicked)}. Run npx genex wait <id> on each \u2014 landed assets get wired in before the last push of a session, never parked for "later".`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
if (g.unwired.length) {
|
|
2245
|
+
log.warn(
|
|
2246
|
+
`${g.unwired.length} finished generation(s) are wired nowhere in the game: ${describe(g.unwired)}. Each was paid for \u2014 wire it in (its URL embeds the id) or tell the user in one plain line why it stays unused.`
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2128
2249
|
}
|
|
2129
2250
|
function surfaceNudges(log, s) {
|
|
2130
2251
|
if (s.guessedRepeat.length) {
|
|
@@ -2518,6 +2639,7 @@ async function runGenerate(kind, opts) {
|
|
|
2518
2639
|
process.exitCode = 1;
|
|
2519
2640
|
return;
|
|
2520
2641
|
}
|
|
2642
|
+
await recordQueued(id, kind, prompt);
|
|
2521
2643
|
if (opts.noWait) {
|
|
2522
2644
|
log.success(`Queued (${id}).`);
|
|
2523
2645
|
log.plain(` Pick it up any time with: ${c.cyan(`npx genex wait ${id}`)}`);
|
|
@@ -2544,6 +2666,13 @@ async function awaitAndReport(apiUrl, token, id, kind, log, open = false) {
|
|
|
2544
2666
|
async function reportTerminal(kind, view, log, open = false) {
|
|
2545
2667
|
if (view.status !== "completed") {
|
|
2546
2668
|
log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
|
|
2669
|
+
if (view.status === "failed") {
|
|
2670
|
+
await recordTerminal(view.id, "failed");
|
|
2671
|
+
if (kind === "video") {
|
|
2672
|
+
const failures = await countFailed("video");
|
|
2673
|
+
(failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2547
2676
|
process.exitCode = 1;
|
|
2548
2677
|
return;
|
|
2549
2678
|
}
|
|
@@ -2553,6 +2682,7 @@ async function reportTerminal(kind, view, log, open = false) {
|
|
|
2553
2682
|
process.exitCode = 1;
|
|
2554
2683
|
return;
|
|
2555
2684
|
}
|
|
2685
|
+
await recordTerminal(view.id, "completed", files.map((f) => f.url));
|
|
2556
2686
|
log.plain("");
|
|
2557
2687
|
log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
|
|
2558
2688
|
for (const f of files) {
|
|
@@ -2572,6 +2702,12 @@ async function reportTerminal(kind, view, log, open = false) {
|
|
|
2572
2702
|
printHint(kind, view, files, log);
|
|
2573
2703
|
await firstPreviewNudge(log);
|
|
2574
2704
|
}
|
|
2705
|
+
function videoFailureAdvice(failures) {
|
|
2706
|
+
if (failures >= 2) {
|
|
2707
|
+
return ` ${c.bold(`\u270B ${failures} failed videos in this project \u2014 don't queue another attempt of the same clip.`)} One retry per clip is the budget. Use its key-art still as the backdrop instead (the genex-ai-menu skill's built-in fallback; a slow CSS pan gives it life), tell the user in one plain line, and spend the minutes in the game.`;
|
|
2708
|
+
}
|
|
2709
|
+
return " Video renders fail server-side sometimes. One retry is fair \u2014 shorter clip (4\u20136 s), simpler motion. After a second failure, switch to the key-art still (genex-ai-menu's fallback) instead of a third attempt.";
|
|
2710
|
+
}
|
|
2575
2711
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2576
2712
|
var KIND_WAIT_TIMEOUT_MS = {
|
|
2577
2713
|
video: 15 * 60 * 1e3,
|
|
@@ -2752,659 +2888,65 @@ async function runWait(opts) {
|
|
|
2752
2888
|
}
|
|
2753
2889
|
|
|
2754
2890
|
// src/commands/controller.ts
|
|
2755
|
-
import
|
|
2756
|
-
import
|
|
2891
|
+
import fs13 from "fs/promises";
|
|
2892
|
+
import path14 from "path";
|
|
2757
2893
|
|
|
2758
|
-
// src/
|
|
2759
|
-
import
|
|
2760
|
-
import path12 from "path";
|
|
2761
|
-
var ANIMS_DEST = path12.join("public", "assets", "anims");
|
|
2762
|
-
var HIDDEN_TAG = "reference";
|
|
2763
|
-
async function runAnims(opts) {
|
|
2764
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
2765
|
-
const root = opts.cwd ?? process.cwd();
|
|
2766
|
-
const selectors = opts.selectors ?? [];
|
|
2767
|
-
log.plain(c.bold("genex controller anims"));
|
|
2768
|
-
log.plain("");
|
|
2769
|
-
const { manifest, source } = await loadManifest(opts.animsBase);
|
|
2770
|
-
if (source === "snapshot") {
|
|
2771
|
-
log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
|
|
2772
|
-
}
|
|
2773
|
-
if (opts.list) {
|
|
2774
|
-
printCatalog(log, manifest, selectors);
|
|
2775
|
-
return;
|
|
2776
|
-
}
|
|
2777
|
-
const controllerMarker = path12.join(root, "src", "controllers", "character");
|
|
2778
|
-
if (!await exists2(controllerMarker)) {
|
|
2779
|
-
log.error(
|
|
2780
|
-
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
2781
|
-
);
|
|
2782
|
-
log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
|
|
2783
|
-
process.exitCode = 1;
|
|
2784
|
-
return;
|
|
2785
|
-
}
|
|
2786
|
-
const destDir = path12.join(root, ANIMS_DEST);
|
|
2787
|
-
const gameManifestPath = path12.join(destDir, "manifest.json");
|
|
2788
|
-
if (opts.reset) {
|
|
2789
|
-
await fs11.rm(destDir, { recursive: true, force: true });
|
|
2790
|
-
log.step(`Cleared ${c.cyan(ANIMS_DEST + path12.sep)} (--reset)`);
|
|
2791
|
-
}
|
|
2792
|
-
if (selectors.length === 0) {
|
|
2793
|
-
const installed = await readGameManifest(gameManifestPath);
|
|
2794
|
-
if (installed === null || installed.clips.length === 0) {
|
|
2795
|
-
log.plain(" No animation packs installed yet.");
|
|
2796
|
-
} else {
|
|
2797
|
-
log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
|
|
2798
|
-
}
|
|
2799
|
-
log.plain("");
|
|
2800
|
-
log.plain(
|
|
2801
|
-
` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
|
|
2802
|
-
"genex controller anims --list"
|
|
2803
|
-
)}.`
|
|
2804
|
-
);
|
|
2805
|
-
return;
|
|
2806
|
-
}
|
|
2807
|
-
let resolved;
|
|
2808
|
-
try {
|
|
2809
|
-
resolved = resolveSelectors(manifest, selectors);
|
|
2810
|
-
} catch (err) {
|
|
2811
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
2812
|
-
process.exitCode = 1;
|
|
2813
|
-
return;
|
|
2814
|
-
}
|
|
2815
|
-
const coreNames = new Set(manifest.core);
|
|
2816
|
-
const byName = /* @__PURE__ */ new Map();
|
|
2817
|
-
let bundledSkips = 0;
|
|
2818
|
-
for (const entries of resolved.values()) {
|
|
2819
|
-
for (const entry of entries) {
|
|
2820
|
-
if (coreNames.has(entry.name)) {
|
|
2821
|
-
bundledSkips++;
|
|
2822
|
-
continue;
|
|
2823
|
-
}
|
|
2824
|
-
byName.set(entry.name, entry);
|
|
2825
|
-
}
|
|
2826
|
-
}
|
|
2827
|
-
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2828
|
-
const cacheDir = path12.join(
|
|
2829
|
-
opts.cacheDir ?? getAnimsCacheDir(),
|
|
2830
|
-
`${manifest.library}-v${manifest.version}`
|
|
2831
|
-
);
|
|
2832
|
-
await fs11.mkdir(cacheDir, { recursive: true });
|
|
2833
|
-
await fs11.mkdir(destDir, { recursive: true });
|
|
2834
|
-
const base = getAnimsBase(opts.animsBase);
|
|
2835
|
-
let installedCount = 0;
|
|
2836
|
-
let presentCount = 0;
|
|
2837
|
-
let addedBytes = 0;
|
|
2838
|
-
const failures = [];
|
|
2839
|
-
for (const entry of wanted) {
|
|
2840
|
-
const dest = path12.join(destDir, entry.file);
|
|
2841
|
-
if (await hasSize(dest, entry.bytes)) {
|
|
2842
|
-
presentCount++;
|
|
2843
|
-
continue;
|
|
2844
|
-
}
|
|
2845
|
-
try {
|
|
2846
|
-
const cached = path12.join(cacheDir, entry.file);
|
|
2847
|
-
if (!await hasSize(cached, entry.bytes)) {
|
|
2848
|
-
const res = await fetch(base + entry.file);
|
|
2849
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
2850
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
2851
|
-
await fs11.writeFile(cached, buf);
|
|
2852
|
-
}
|
|
2853
|
-
await fs11.copyFile(cached, dest);
|
|
2854
|
-
installedCount++;
|
|
2855
|
-
addedBytes += entry.bytes;
|
|
2856
|
-
log.dim(` ${path12.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
2857
|
-
} catch (err) {
|
|
2858
|
-
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
2859
|
-
}
|
|
2860
|
-
}
|
|
2861
|
-
const previous = await readGameManifest(gameManifestPath);
|
|
2862
|
-
const union = new Set(previous?.clips ?? []);
|
|
2863
|
-
for (const entry of wanted) {
|
|
2864
|
-
if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
|
|
2865
|
-
}
|
|
2866
|
-
const gameManifest = {
|
|
2867
|
-
schema: 1,
|
|
2868
|
-
library: manifest.library,
|
|
2869
|
-
version: manifest.version,
|
|
2870
|
-
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
2871
|
-
};
|
|
2872
|
-
await fs11.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
2873
|
-
log.plain("");
|
|
2874
|
-
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
2875
|
-
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
2876
|
-
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
2877
|
-
log.success(
|
|
2878
|
-
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path12.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
2879
|
-
);
|
|
2880
|
-
for (const [selector, entries] of resolved) {
|
|
2881
|
-
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
2882
|
-
if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
|
|
2883
|
-
}
|
|
2884
|
-
log.info(
|
|
2885
|
-
`Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
|
|
2886
|
-
);
|
|
2887
|
-
if (failures.length > 0) {
|
|
2888
|
-
log.plain("");
|
|
2889
|
-
log.error(
|
|
2890
|
-
`${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
|
|
2891
|
-
);
|
|
2892
|
-
log.plain(
|
|
2893
|
-
" Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
|
|
2894
|
-
);
|
|
2895
|
-
process.exitCode = 1;
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
async function loadManifest(baseOverride) {
|
|
2899
|
-
const base = getAnimsBase(baseOverride);
|
|
2900
|
-
try {
|
|
2901
|
-
const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
|
|
2902
|
-
if (res.ok) {
|
|
2903
|
-
const manifest2 = await res.json();
|
|
2904
|
-
if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
|
|
2905
|
-
return { manifest: manifest2, source: "cdn" };
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
} catch {
|
|
2909
|
-
}
|
|
2910
|
-
const snapshotPath = path12.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
2911
|
-
const manifest = JSON.parse(await fs11.readFile(snapshotPath, "utf8"));
|
|
2912
|
-
return { manifest, source: "snapshot" };
|
|
2913
|
-
}
|
|
2914
|
-
function resolveSelectors(manifest, selectors) {
|
|
2915
|
-
const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
|
|
2916
|
-
const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
|
|
2917
|
-
const tags = /* @__PURE__ */ new Map();
|
|
2918
|
-
for (const entry of manifest.clips) {
|
|
2919
|
-
for (const tag of entry.tags) {
|
|
2920
|
-
const list = tags.get(tag) ?? [];
|
|
2921
|
-
list.push(entry);
|
|
2922
|
-
tags.set(tag, list);
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
const out = /* @__PURE__ */ new Map();
|
|
2926
|
-
for (const selector of selectors) {
|
|
2927
|
-
const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
|
|
2928
|
-
if (exact) {
|
|
2929
|
-
out.set(selector, [exact]);
|
|
2930
|
-
continue;
|
|
2931
|
-
}
|
|
2932
|
-
const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
|
|
2933
|
-
if (tagHit) {
|
|
2934
|
-
out.set(selector, tagHit);
|
|
2935
|
-
continue;
|
|
2936
|
-
}
|
|
2937
|
-
const candidates = [...tags.keys(), ...byName.keys()];
|
|
2938
|
-
const close = suggest(selector, candidates);
|
|
2939
|
-
throw new Error(
|
|
2940
|
-
`unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
|
|
2941
|
-
"genex controller anims --list"
|
|
2942
|
-
)} for the catalog.`
|
|
2943
|
-
);
|
|
2944
|
-
}
|
|
2945
|
-
return out;
|
|
2946
|
-
}
|
|
2947
|
-
function suggest(input, candidates) {
|
|
2948
|
-
const lower = input.toLowerCase();
|
|
2949
|
-
const scored = [];
|
|
2950
|
-
for (const candidate of candidates) {
|
|
2951
|
-
const candidateLower = candidate.toLowerCase();
|
|
2952
|
-
if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
|
|
2953
|
-
scored.push({ name: candidate, score: 0 });
|
|
2954
|
-
continue;
|
|
2955
|
-
}
|
|
2956
|
-
const distance = levenshtein(lower, candidateLower, 2);
|
|
2957
|
-
if (distance <= 2) scored.push({ name: candidate, score: distance });
|
|
2958
|
-
}
|
|
2959
|
-
scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
|
2960
|
-
return scored.slice(0, 4).map((s) => s.name);
|
|
2961
|
-
}
|
|
2962
|
-
function levenshtein(a, b, max) {
|
|
2963
|
-
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
2964
|
-
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
2965
|
-
for (let i = 1; i <= a.length; i++) {
|
|
2966
|
-
const curr = [i];
|
|
2967
|
-
let rowMin = i;
|
|
2968
|
-
for (let j = 1; j <= b.length; j++) {
|
|
2969
|
-
curr[j] = Math.min(
|
|
2970
|
-
prev[j] + 1,
|
|
2971
|
-
curr[j - 1] + 1,
|
|
2972
|
-
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
2973
|
-
);
|
|
2974
|
-
if (curr[j] < rowMin) rowMin = curr[j];
|
|
2975
|
-
}
|
|
2976
|
-
if (rowMin > max) return max + 1;
|
|
2977
|
-
prev = curr;
|
|
2978
|
-
}
|
|
2979
|
-
return prev[b.length];
|
|
2980
|
-
}
|
|
2981
|
-
function printCatalog(log, manifest, selectors) {
|
|
2982
|
-
const coreNames = new Set(manifest.core);
|
|
2983
|
-
if (selectors.length > 0) {
|
|
2984
|
-
let resolved;
|
|
2985
|
-
try {
|
|
2986
|
-
resolved = resolveSelectors(manifest, selectors);
|
|
2987
|
-
} catch (err) {
|
|
2988
|
-
log.error(err instanceof Error ? err.message : String(err));
|
|
2989
|
-
process.exitCode = 1;
|
|
2990
|
-
return;
|
|
2991
|
-
}
|
|
2992
|
-
for (const [selector, entries] of resolved) {
|
|
2993
|
-
log.plain(c.bold(selector));
|
|
2994
|
-
for (const entry of entries) {
|
|
2995
|
-
const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
|
|
2996
|
-
log.plain(
|
|
2997
|
-
` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
|
|
2998
|
-
);
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
return;
|
|
3002
|
-
}
|
|
3003
|
-
const tags = /* @__PURE__ */ new Map();
|
|
3004
|
-
for (const entry of manifest.clips) {
|
|
3005
|
-
for (const tag of entry.tags) {
|
|
3006
|
-
if (tag === HIDDEN_TAG) continue;
|
|
3007
|
-
const list = tags.get(tag) ?? [];
|
|
3008
|
-
list.push(entry);
|
|
3009
|
-
tags.set(tag, list);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
log.plain(
|
|
3013
|
-
`${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
|
|
3014
|
-
);
|
|
3015
|
-
log.plain(
|
|
3016
|
-
` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
|
|
3017
|
-
"genex controller anims --list <tag>"
|
|
3018
|
-
)}`
|
|
3019
|
-
);
|
|
3020
|
-
log.plain("");
|
|
3021
|
-
for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
3022
|
-
const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
3023
|
-
const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
|
|
3024
|
-
log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
async function readGameManifest(file) {
|
|
3028
|
-
try {
|
|
3029
|
-
return JSON.parse(await fs11.readFile(file, "utf8"));
|
|
3030
|
-
} catch {
|
|
3031
|
-
return null;
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
async function hasSize(file, bytes) {
|
|
3035
|
-
try {
|
|
3036
|
-
return (await fs11.stat(file)).size === bytes;
|
|
3037
|
-
} catch {
|
|
3038
|
-
return false;
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
async function exists2(p) {
|
|
3042
|
-
try {
|
|
3043
|
-
await fs11.access(p);
|
|
3044
|
-
return true;
|
|
3045
|
-
} catch {
|
|
3046
|
-
return false;
|
|
3047
|
-
}
|
|
3048
|
-
}
|
|
3049
|
-
function formatMb(bytes) {
|
|
3050
|
-
return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
|
|
3051
|
-
}
|
|
2894
|
+
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
2895
|
+
import { createHash } from "crypto";
|
|
3052
2896
|
|
|
3053
|
-
// src/
|
|
3054
|
-
var
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
"touch/touch-joystick.ts",
|
|
3068
|
-
"touch/drag-zone.ts",
|
|
3069
|
-
"touch/rotate-overlay.ts"
|
|
3070
|
-
];
|
|
3071
|
-
var INPUT_AND_CAMERA = [
|
|
3072
|
-
"character/follow-camera.ts",
|
|
3073
|
-
"character/keyboard-input.ts",
|
|
3074
|
-
"character/touch-joystick.ts",
|
|
3075
|
-
...TOUCH_KIT
|
|
3076
|
-
];
|
|
3077
|
-
var NOTICE = "NOTICE.md";
|
|
3078
|
-
var CONTROLLER_FILE_SETS = {
|
|
3079
|
-
character: {
|
|
3080
|
-
code: [
|
|
3081
|
-
...SHARED,
|
|
3082
|
-
"character/character-controller.ts",
|
|
3083
|
-
"character/character-animations.ts",
|
|
3084
|
-
"character/animation-packs.ts",
|
|
3085
|
-
"character/motion-actions.ts",
|
|
3086
|
-
"character/meshy/meshy-loader.ts",
|
|
3087
|
-
"character/presets.ts",
|
|
3088
|
-
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
3089
|
-
// the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
|
|
3090
|
-
"character/vrm/vrm-loader.ts",
|
|
3091
|
-
"character/vrm/vrm-retarget.ts",
|
|
3092
|
-
"character/vrm/capsule-fit.ts",
|
|
3093
|
-
"character/vrm/foot-ik.ts",
|
|
3094
|
-
...INPUT_AND_CAMERA,
|
|
3095
|
-
NOTICE
|
|
3096
|
-
],
|
|
3097
|
-
// The player's VRM is written to public/assets/avatar.vrm at install time by
|
|
3098
|
-
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
3099
|
-
// a static manifest asset. animation-library.glb (the 12-clip core) still is;
|
|
3100
|
-
// extra clips arrive via `genex controller anims` into public/assets/anims/.
|
|
3101
|
-
assets: ["assets/animation-library.glb"],
|
|
3102
|
-
skill: "genex-threejs-character-controller",
|
|
3103
|
-
sketch: [
|
|
3104
|
-
`const physics = await PhysicsWorld.create();`,
|
|
3105
|
-
`const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
|
|
3106
|
-
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
3107
|
-
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
3108
|
-
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
3109
|
-
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
|
|
3110
|
-
]
|
|
2897
|
+
// ../../packages/meshy-animation-catalog/src/generated.ts
|
|
2898
|
+
var MESHY_ANIMATION_CATALOG = [
|
|
2899
|
+
{
|
|
2900
|
+
"actionId": -2,
|
|
2901
|
+
"key": "Walking_man",
|
|
2902
|
+
"name": "Walking",
|
|
2903
|
+
"category": "WalkAndRun",
|
|
2904
|
+
"subCategory": "Walking",
|
|
2905
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking.gif",
|
|
2906
|
+
"rigType": "biped",
|
|
2907
|
+
"tag": null,
|
|
2908
|
+
"isDefault": true,
|
|
2909
|
+
"isFree": true,
|
|
2910
|
+
"createdAt": 1750829487798
|
|
3111
2911
|
},
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
sketch: [
|
|
3125
|
-
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
3126
|
-
`const car = new VehicleController({ world: physics.world, position, carConfig: vehiclePresets["arcade-kart"].carConfig }); // + chassis colliders + car.addWheel(...) per preset slot`,
|
|
3127
|
-
`scene.add(car.chassisObject); physics.onBeforeStep(() => { car.setMovement(keyboard.getCarMovement()); car.update(); });`
|
|
3128
|
-
]
|
|
2912
|
+
{
|
|
2913
|
+
"actionId": -1,
|
|
2914
|
+
"key": "Running",
|
|
2915
|
+
"name": "Running",
|
|
2916
|
+
"category": "WalkAndRun",
|
|
2917
|
+
"subCategory": "Running",
|
|
2918
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Running.gif",
|
|
2919
|
+
"rigType": "biped",
|
|
2920
|
+
"tag": null,
|
|
2921
|
+
"isDefault": true,
|
|
2922
|
+
"isFree": true,
|
|
2923
|
+
"createdAt": 1750829487790
|
|
3129
2924
|
},
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
3143
|
-
`const drone = new DroneController({ world: physics.world, body, chassis, propellers, config: dronePresets["camera-drone"].config });`,
|
|
3144
|
-
`physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
|
|
3145
|
-
]
|
|
2925
|
+
{
|
|
2926
|
+
"actionId": 0,
|
|
2927
|
+
"key": "Idle",
|
|
2928
|
+
"name": "Idle",
|
|
2929
|
+
"category": "DailyActions",
|
|
2930
|
+
"subCategory": "Idle",
|
|
2931
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Idle.gif",
|
|
2932
|
+
"rigType": "style_01",
|
|
2933
|
+
"tag": null,
|
|
2934
|
+
"isDefault": false,
|
|
2935
|
+
"isFree": true,
|
|
2936
|
+
"createdAt": 1750829487810
|
|
3146
2937
|
},
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
code: [
|
|
3160
|
-
...SHARED,
|
|
3161
|
-
"network/pose.ts",
|
|
3162
|
-
"network/networked-pushable.ts",
|
|
3163
|
-
"network/networked-vehicle.ts",
|
|
3164
|
-
"NETWORKING.md",
|
|
3165
|
-
NOTICE
|
|
3166
|
-
],
|
|
3167
|
-
assets: [],
|
|
3168
|
-
skill: "genex-threejs-multiplayer",
|
|
3169
|
-
sketch: [
|
|
3170
|
-
`const box = new NetworkedPushable({ id: "box:1", room: () => room, body, object: mesh });`,
|
|
3171
|
-
`physics.onBeforeStep(() => box.update()); physics.onAfterStep(() => box.publish());`,
|
|
3172
|
-
`contacts.onChange((active) => box.setContact(active)); // retries held claims while contact persists`
|
|
3173
|
-
]
|
|
3174
|
-
}
|
|
3175
|
-
};
|
|
3176
|
-
var CODE_DEST = path13.join("src", "controllers");
|
|
3177
|
-
var ASSETS_DEST = path13.join("public", "assets");
|
|
3178
|
-
async function runController(opts) {
|
|
3179
|
-
const log = createLogger({ quiet: opts.quiet });
|
|
3180
|
-
if (opts.kind?.trim() === "anims") {
|
|
3181
|
-
await runAnims(opts);
|
|
3182
|
-
return;
|
|
3183
|
-
}
|
|
3184
|
-
const kind = opts.kind?.trim();
|
|
3185
|
-
if (!kind || !CONTROLLER_KINDS.includes(kind)) {
|
|
3186
|
-
log.error(
|
|
3187
|
-
`Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
|
|
3188
|
-
"genex controller <character|car|drone|touch|networked-physics> [--force]"
|
|
3189
|
-
)} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
|
|
3190
|
-
);
|
|
3191
|
-
process.exitCode = 1;
|
|
3192
|
-
return;
|
|
3193
|
-
}
|
|
3194
|
-
const srcDir = path13.join(getTemplatesDir(), "controllers");
|
|
3195
|
-
const root = opts.cwd ?? process.cwd();
|
|
3196
|
-
const set = CONTROLLER_FILE_SETS[kind];
|
|
3197
|
-
log.plain(c.bold(`genex controller ${kind}`));
|
|
3198
|
-
log.plain("");
|
|
3199
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path13.sep)}`);
|
|
3200
|
-
const plan = [
|
|
3201
|
-
...set.code.map((rel) => ({ from: rel, rel: path13.join(CODE_DEST, rel) })),
|
|
3202
|
-
...set.assets.map((rel) => ({
|
|
3203
|
-
from: rel,
|
|
3204
|
-
rel: path13.join(ASSETS_DEST, path13.basename(rel))
|
|
3205
|
-
}))
|
|
3206
|
-
];
|
|
3207
|
-
let copied = 0;
|
|
3208
|
-
let skipped = 0;
|
|
3209
|
-
try {
|
|
3210
|
-
for (const file of plan) {
|
|
3211
|
-
const dest = path13.join(root, file.rel);
|
|
3212
|
-
if (!opts.force && await exists3(dest)) {
|
|
3213
|
-
skipped++;
|
|
3214
|
-
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
3215
|
-
continue;
|
|
3216
|
-
}
|
|
3217
|
-
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
3218
|
-
await fs12.copyFile(path13.join(srcDir, file.from), dest);
|
|
3219
|
-
copied++;
|
|
3220
|
-
log.dim(` ${file.rel}`);
|
|
3221
|
-
}
|
|
3222
|
-
} catch (err) {
|
|
3223
|
-
log.error(`Copy failed: ${String(err)}`);
|
|
3224
|
-
process.exitCode = 1;
|
|
3225
|
-
return;
|
|
3226
|
-
}
|
|
3227
|
-
log.success(
|
|
3228
|
-
`Controller files ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`
|
|
3229
|
-
);
|
|
3230
|
-
log.plain("");
|
|
3231
|
-
if (kind === "character") {
|
|
3232
|
-
if (opts.character) {
|
|
3233
|
-
try {
|
|
3234
|
-
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
3235
|
-
if (!token) throw new Error("Not authorized. Run `genex init` before installing a Meshy character.");
|
|
3236
|
-
await installMeshyCharacterManifest({
|
|
3237
|
-
root,
|
|
3238
|
-
characterId: opts.character,
|
|
3239
|
-
apiUrl: getApiUrl(opts.apiUrl),
|
|
3240
|
-
token,
|
|
3241
|
-
log
|
|
3242
|
-
});
|
|
3243
|
-
} catch (error) {
|
|
3244
|
-
log.error(error instanceof Error ? error.message : String(error));
|
|
3245
|
-
process.exitCode = 1;
|
|
3246
|
-
return;
|
|
3247
|
-
}
|
|
3248
|
-
} else {
|
|
3249
|
-
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
3250
|
-
await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
|
|
3251
|
-
}
|
|
3252
|
-
log.plain("");
|
|
3253
|
-
}
|
|
3254
|
-
log.plain(c.bold("Next steps"));
|
|
3255
|
-
if (kind !== "touch") {
|
|
3256
|
-
log.plain(
|
|
3257
|
-
` 1. ${c.cyan(
|
|
3258
|
-
kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
|
|
3259
|
-
)} (three is already in the scaffold).`
|
|
3260
|
-
);
|
|
3261
|
-
}
|
|
3262
|
-
const stepOffset = kind === "touch" ? 0 : 1;
|
|
3263
|
-
log.plain(
|
|
3264
|
-
` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
|
|
3265
|
-
);
|
|
3266
|
-
log.plain(
|
|
3267
|
-
kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
|
|
3268
|
-
);
|
|
3269
|
-
const sketch = kind === "character" && opts.character ? [
|
|
3270
|
-
`const physics = await PhysicsWorld.create();`,
|
|
3271
|
-
`const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
|
|
3272
|
-
`const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
3273
|
-
`character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
|
|
3274
|
-
`physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
|
|
3275
|
-
] : set.sketch;
|
|
3276
|
-
for (const line of sketch) {
|
|
3277
|
-
log.dim(` ${line}`);
|
|
3278
|
-
}
|
|
3279
|
-
await firstPreviewNudge(log);
|
|
3280
|
-
}
|
|
3281
|
-
async function installMeshyCharacterManifest(args) {
|
|
3282
|
-
const response = await apiFetch(
|
|
3283
|
-
`${args.apiUrl}/api/characters/${encodeURIComponent(args.characterId)}/manifest`,
|
|
3284
|
-
{ headers: { Authorization: `Bearer ${args.token}` } }
|
|
3285
|
-
);
|
|
3286
|
-
if (response.status === 404) throw new Error(`Meshy character ${args.characterId} was not found on this account.`);
|
|
3287
|
-
if (!response.ok) throw new Error(`Couldn't fetch the Meshy character manifest (HTTP ${response.status}).`);
|
|
3288
|
-
const body = await response.json();
|
|
3289
|
-
const manifest = body.manifest;
|
|
3290
|
-
if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId) {
|
|
3291
|
-
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
3292
|
-
}
|
|
3293
|
-
const destination = path13.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
3294
|
-
await fs12.mkdir(path13.dirname(destination), { recursive: true });
|
|
3295
|
-
await fs12.writeFile(
|
|
3296
|
-
destination,
|
|
3297
|
-
`${JSON.stringify(manifest, null, 2)}
|
|
3298
|
-
`
|
|
3299
|
-
);
|
|
3300
|
-
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
3301
|
-
const pack = manifest.controllerPack;
|
|
3302
|
-
if (typeof pack?.key === "string" && typeof pack.version === "number") {
|
|
3303
|
-
args.log.success(`Meshy controller pack ${pack.key} v${pack.version}`);
|
|
3304
|
-
} else {
|
|
3305
|
-
args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the preview-reviewed neutral-v2 locomotion pack.");
|
|
3306
|
-
}
|
|
3307
|
-
const actionIds = (manifest.clips ?? []).map((clip) => clip.actionId).filter((actionId) => typeof actionId === "number");
|
|
3308
|
-
args.log.dim(` installed action ids: ${actionIds.length > 0 ? actionIds.join(", ") : "none"}`);
|
|
3309
|
-
const slots = Object.keys(manifest.locomotion?.bindings ?? manifest.locomotion?.slots ?? {}).sort();
|
|
3310
|
-
args.log.dim(` locomotion slots: ${slots.length > 0 ? slots.join(", ") : "none"}`);
|
|
3311
|
-
const crouchCovered = slots.includes("crouch.idle") && slots.includes("crouch.forward");
|
|
3312
|
-
if (crouchCovered) args.log.success("Visual crouch coverage: idle + move");
|
|
3313
|
-
else args.log.warn("Visual crouch coverage is incomplete; physics crouch may fall back to a standing pose.");
|
|
3314
|
-
}
|
|
3315
|
-
async function installOwnerAvatar(args) {
|
|
3316
|
-
const { root, srcDir, apiUrl, token, log } = args;
|
|
3317
|
-
const dest = path13.join(root, ASSETS_DEST, "avatar.vrm");
|
|
3318
|
-
await fs12.mkdir(path13.dirname(dest), { recursive: true });
|
|
3319
|
-
if (token) {
|
|
3320
|
-
try {
|
|
3321
|
-
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
3322
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
3323
|
-
});
|
|
3324
|
-
if (res.ok) {
|
|
3325
|
-
const me = await res.json();
|
|
3326
|
-
if (me.vrmUrl) {
|
|
3327
|
-
const vrmRes = await fetch(me.vrmUrl);
|
|
3328
|
-
if (vrmRes.ok) {
|
|
3329
|
-
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
3330
|
-
await fs12.writeFile(dest, buf);
|
|
3331
|
-
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
3332
|
-
return;
|
|
3333
|
-
}
|
|
3334
|
-
}
|
|
3335
|
-
}
|
|
3336
|
-
log.dim(" couldn't fetch your avatar; using the bundled default.");
|
|
3337
|
-
} catch {
|
|
3338
|
-
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
await fs12.copyFile(path13.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
3342
|
-
log.dim(
|
|
3343
|
-
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
3344
|
-
);
|
|
3345
|
-
}
|
|
3346
|
-
async function exists3(p) {
|
|
3347
|
-
try {
|
|
3348
|
-
await fs12.access(p);
|
|
3349
|
-
return true;
|
|
3350
|
-
} catch {
|
|
3351
|
-
return false;
|
|
3352
|
-
}
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
// ../../packages/meshy-animation-catalog/src/generated.ts
|
|
3356
|
-
var MESHY_ANIMATION_CATALOG = [
|
|
3357
|
-
{
|
|
3358
|
-
"actionId": -2,
|
|
3359
|
-
"key": "Walking_man",
|
|
3360
|
-
"name": "Walking",
|
|
3361
|
-
"category": "WalkAndRun",
|
|
3362
|
-
"subCategory": "Walking",
|
|
3363
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking.gif",
|
|
3364
|
-
"rigType": "biped",
|
|
3365
|
-
"tag": null,
|
|
3366
|
-
"isDefault": true,
|
|
3367
|
-
"isFree": true,
|
|
3368
|
-
"createdAt": 1750829487798
|
|
3369
|
-
},
|
|
3370
|
-
{
|
|
3371
|
-
"actionId": -1,
|
|
3372
|
-
"key": "Running",
|
|
3373
|
-
"name": "Running",
|
|
3374
|
-
"category": "WalkAndRun",
|
|
3375
|
-
"subCategory": "Running",
|
|
3376
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Running.gif",
|
|
3377
|
-
"rigType": "biped",
|
|
3378
|
-
"tag": null,
|
|
3379
|
-
"isDefault": true,
|
|
3380
|
-
"isFree": true,
|
|
3381
|
-
"createdAt": 1750829487790
|
|
3382
|
-
},
|
|
3383
|
-
{
|
|
3384
|
-
"actionId": 0,
|
|
3385
|
-
"key": "Idle",
|
|
3386
|
-
"name": "Idle",
|
|
3387
|
-
"category": "DailyActions",
|
|
3388
|
-
"subCategory": "Idle",
|
|
3389
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Idle.gif",
|
|
3390
|
-
"rigType": "style_01",
|
|
3391
|
-
"tag": null,
|
|
3392
|
-
"isDefault": false,
|
|
3393
|
-
"isFree": true,
|
|
3394
|
-
"createdAt": 1750829487810
|
|
3395
|
-
},
|
|
3396
|
-
{
|
|
3397
|
-
"actionId": 1,
|
|
3398
|
-
"key": "Walking_Woman",
|
|
3399
|
-
"name": "Walking Woman",
|
|
3400
|
-
"category": "WalkAndRun",
|
|
3401
|
-
"subCategory": "Walking",
|
|
3402
|
-
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking_Woman_woman.gif",
|
|
3403
|
-
"rigType": "style_01",
|
|
3404
|
-
"tag": null,
|
|
3405
|
-
"isDefault": false,
|
|
3406
|
-
"isFree": true,
|
|
3407
|
-
"createdAt": 1750829487808
|
|
2938
|
+
{
|
|
2939
|
+
"actionId": 1,
|
|
2940
|
+
"key": "Walking_Woman",
|
|
2941
|
+
"name": "Walking Woman",
|
|
2942
|
+
"category": "WalkAndRun",
|
|
2943
|
+
"subCategory": "Walking",
|
|
2944
|
+
"previewUrl": "https://cdn.meshy.ai/webapp-assets/feature-demo/animation/preview/biped/Walking_Woman_woman.gif",
|
|
2945
|
+
"rigType": "style_01",
|
|
2946
|
+
"tag": null,
|
|
2947
|
+
"isDefault": false,
|
|
2948
|
+
"isFree": true,
|
|
2949
|
+
"createdAt": 1750829487808
|
|
3408
2950
|
},
|
|
3409
2951
|
{
|
|
3410
2952
|
"actionId": 2,
|
|
@@ -12194,215 +11736,992 @@ var MESHY_ANIMATION_CATALOG = [
|
|
|
12194
11736
|
"isFree": false,
|
|
12195
11737
|
"createdAt": 1750829526124
|
|
12196
11738
|
}
|
|
12197
|
-
];
|
|
12198
|
-
|
|
12199
|
-
// ../../packages/meshy-animation-catalog/src/curation.ts
|
|
12200
|
-
var CURATED = {
|
|
12201
|
-
[-2]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["walk.forward"] },
|
|
12202
|
-
[-1]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["run.forward"] },
|
|
12203
|
-
[0]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["idle.default"] },
|
|
12204
|
-
[243]: {
|
|
12205
|
-
loop: true,
|
|
12206
|
-
motionPolicy: "controller-loop",
|
|
12207
|
-
controllerSlots: ["idle.default"],
|
|
12208
|
-
reviewStatus: "preview-reviewed"
|
|
12209
|
-
},
|
|
12210
|
-
[466]: {
|
|
12211
|
-
loop: false,
|
|
12212
|
-
motionPolicy: "anchored-action",
|
|
12213
|
-
controllerSlots: ["jump.full"],
|
|
12214
|
-
reviewStatus: "preview-reviewed"
|
|
12215
|
-
},
|
|
12216
|
-
[613]: {
|
|
12217
|
-
loop: true,
|
|
12218
|
-
motionPolicy: "controller-loop",
|
|
12219
|
-
controllerSlots: ["walk.forward"],
|
|
12220
|
-
reviewStatus: "preview-reviewed"
|
|
12221
|
-
},
|
|
12222
|
-
[616]: {
|
|
12223
|
-
loop: true,
|
|
12224
|
-
motionPolicy: "controller-loop",
|
|
12225
|
-
controllerSlots: ["crouch.forward"],
|
|
12226
|
-
reviewStatus: "preview-reviewed"
|
|
12227
|
-
},
|
|
12228
|
-
[657]: {
|
|
12229
|
-
loop: true,
|
|
12230
|
-
motionPolicy: "controller-loop",
|
|
12231
|
-
controllerSlots: ["run.forward"],
|
|
12232
|
-
reviewStatus: "preview-reviewed"
|
|
12233
|
-
},
|
|
12234
|
-
[658]: {
|
|
12235
|
-
loop: true,
|
|
12236
|
-
motionPolicy: "controller-loop",
|
|
12237
|
-
controllerSlots: [],
|
|
12238
|
-
reviewStatus: "rejected"
|
|
12239
|
-
},
|
|
12240
|
-
[659]: {
|
|
12241
|
-
loop: true,
|
|
12242
|
-
motionPolicy: "controller-loop",
|
|
12243
|
-
controllerSlots: [],
|
|
12244
|
-
reviewStatus: "rejected"
|
|
11739
|
+
];
|
|
11740
|
+
|
|
11741
|
+
// ../../packages/meshy-animation-catalog/src/curation.ts
|
|
11742
|
+
var CURATED = {
|
|
11743
|
+
[-2]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["walk.forward"] },
|
|
11744
|
+
[-1]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["run.forward"] },
|
|
11745
|
+
[0]: { loop: true, motionPolicy: "controller-loop", controllerSlots: ["idle.default"] },
|
|
11746
|
+
[243]: {
|
|
11747
|
+
loop: true,
|
|
11748
|
+
motionPolicy: "controller-loop",
|
|
11749
|
+
controllerSlots: ["idle.default"],
|
|
11750
|
+
reviewStatus: "preview-reviewed"
|
|
11751
|
+
},
|
|
11752
|
+
[466]: {
|
|
11753
|
+
loop: false,
|
|
11754
|
+
motionPolicy: "anchored-action",
|
|
11755
|
+
controllerSlots: ["jump.full"],
|
|
11756
|
+
reviewStatus: "preview-reviewed"
|
|
11757
|
+
},
|
|
11758
|
+
[613]: {
|
|
11759
|
+
loop: true,
|
|
11760
|
+
motionPolicy: "controller-loop",
|
|
11761
|
+
controllerSlots: ["walk.forward"],
|
|
11762
|
+
reviewStatus: "preview-reviewed"
|
|
11763
|
+
},
|
|
11764
|
+
[616]: {
|
|
11765
|
+
loop: true,
|
|
11766
|
+
motionPolicy: "controller-loop",
|
|
11767
|
+
controllerSlots: ["crouch.forward"],
|
|
11768
|
+
reviewStatus: "preview-reviewed"
|
|
11769
|
+
},
|
|
11770
|
+
[657]: {
|
|
11771
|
+
loop: true,
|
|
11772
|
+
motionPolicy: "controller-loop",
|
|
11773
|
+
controllerSlots: ["run.forward"],
|
|
11774
|
+
reviewStatus: "preview-reviewed"
|
|
11775
|
+
},
|
|
11776
|
+
[658]: {
|
|
11777
|
+
loop: true,
|
|
11778
|
+
motionPolicy: "controller-loop",
|
|
11779
|
+
controllerSlots: [],
|
|
11780
|
+
reviewStatus: "rejected"
|
|
11781
|
+
},
|
|
11782
|
+
[659]: {
|
|
11783
|
+
loop: true,
|
|
11784
|
+
motionPolicy: "controller-loop",
|
|
11785
|
+
controllerSlots: [],
|
|
11786
|
+
reviewStatus: "rejected"
|
|
11787
|
+
}
|
|
11788
|
+
};
|
|
11789
|
+
var LOOP_SUBCATEGORIES = /* @__PURE__ */ new Set(["Idle", "Walking", "Running", "CrouchWalking", "Swimming"]);
|
|
11790
|
+
var CHOREOGRAPHY_SUBCATEGORIES = /* @__PURE__ */ new Set([
|
|
11791
|
+
"Climbing",
|
|
11792
|
+
"HangingfromLedge",
|
|
11793
|
+
"VaultingOverObstacle",
|
|
11794
|
+
"Interacting",
|
|
11795
|
+
"PickingUpItem",
|
|
11796
|
+
"Pushing",
|
|
11797
|
+
"Sleeping"
|
|
11798
|
+
]);
|
|
11799
|
+
var PLANAR_ACTION = /(?:roll|dodge|lunge|charge|step[_ -](?:back|forward)|slide)/i;
|
|
11800
|
+
var NON_LOOP_ACTION = /(?:transition|start|stop|turn)/i;
|
|
11801
|
+
function words(value) {
|
|
11802
|
+
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
11803
|
+
}
|
|
11804
|
+
function inferRequirements(key, subCategory) {
|
|
11805
|
+
const lower = `${key} ${subCategory}`.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
|
11806
|
+
const props = [
|
|
11807
|
+
["gun", /\b(?:gun|rifle|pistol)\b/],
|
|
11808
|
+
["sword", /\b(?:sword|blade)\b/],
|
|
11809
|
+
["bow", /\b(?:bow|arrow)\b/],
|
|
11810
|
+
["shield", /\bshield\b/],
|
|
11811
|
+
["chair", /\b(?:chair|sit|sitting)\b/],
|
|
11812
|
+
["ladder", /\bladder\b/],
|
|
11813
|
+
["rope", /\brope\b/]
|
|
11814
|
+
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
11815
|
+
const environment = [
|
|
11816
|
+
["climbable", /\b(?:climb|climbing|ladder|ledge|wall|rope)\b/],
|
|
11817
|
+
["vault obstacle", /\b(?:vault|vaulting|obstacle)\b/],
|
|
11818
|
+
["water", /\b(?:swim|swimming)\b/],
|
|
11819
|
+
["seat", /\b(?:sit|sitting|chair)\b/]
|
|
11820
|
+
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
11821
|
+
return { props, environment, partner: /\b(?:partner|carry person|hug|handshake)\b/.test(lower) };
|
|
11822
|
+
}
|
|
11823
|
+
function inferSlots(key) {
|
|
11824
|
+
const lower = key.toLowerCase();
|
|
11825
|
+
const band = lower.includes("crouch") ? "crouch" : lower.includes("run") || lower.includes("sprint") ? "run" : lower.includes("walk") ? "walk" : null;
|
|
11826
|
+
if (!band) return [];
|
|
11827
|
+
const direction = lower.includes("backleft") ? "back-left" : lower.includes("backright") ? "back-right" : lower.includes("backward") || lower.includes("_back") ? "backward" : lower.includes("left") ? "left" : lower.includes("right") ? "right" : "forward";
|
|
11828
|
+
return [`${band}.${direction}`];
|
|
11829
|
+
}
|
|
11830
|
+
function curateMeshyAnimation(raw) {
|
|
11831
|
+
const inPlace = raw.tag === "InPlace";
|
|
11832
|
+
const requirements = inferRequirements(raw.key, raw.subCategory);
|
|
11833
|
+
const choreography = CHOREOGRAPHY_SUBCATEGORIES.has(raw.subCategory) || requirements.partner;
|
|
11834
|
+
const loop = inPlace && LOOP_SUBCATEGORIES.has(raw.subCategory) && !NON_LOOP_ACTION.test(`${raw.key} ${raw.name}`);
|
|
11835
|
+
const motionPolicy = loop ? "controller-loop" : choreography ? "choreography" : PLANAR_ACTION.test(`${raw.key} ${raw.name}`) ? "planar-root-action" : "anchored-action";
|
|
11836
|
+
const base = {
|
|
11837
|
+
actionId: raw.actionId,
|
|
11838
|
+
key: raw.key,
|
|
11839
|
+
name: raw.name,
|
|
11840
|
+
category: raw.category,
|
|
11841
|
+
subCategory: raw.subCategory,
|
|
11842
|
+
previewUrl: raw.previewUrl,
|
|
11843
|
+
rigType: raw.rigType,
|
|
11844
|
+
inPlace,
|
|
11845
|
+
isDefault: raw.isDefault,
|
|
11846
|
+
isFree: raw.isFree,
|
|
11847
|
+
createdAt: raw.createdAt,
|
|
11848
|
+
aliases: [.../* @__PURE__ */ new Set([raw.key.replaceAll("_", " "), raw.name])],
|
|
11849
|
+
gameplayTags: [.../* @__PURE__ */ new Set([...words(raw.category), ...words(raw.subCategory), ...inPlace ? ["in-place"] : [], ...requirements.props, ...requirements.environment])],
|
|
11850
|
+
loop,
|
|
11851
|
+
motionPolicy,
|
|
11852
|
+
rootMotionValidated: false,
|
|
11853
|
+
// A name that contains "walk" or "run" is not enough to make a safe
|
|
11854
|
+
// controller loop. Only provider-declared InPlace loops (plus explicit
|
|
11855
|
+
// measured overrides above) may populate automatic locomotion slots.
|
|
11856
|
+
controllerSlots: loop ? inferSlots(raw.key) : [],
|
|
11857
|
+
requirements,
|
|
11858
|
+
reviewStatus: "metadata-reviewed"
|
|
11859
|
+
};
|
|
11860
|
+
return { ...base, ...CURATED[raw.actionId], requirements };
|
|
11861
|
+
}
|
|
11862
|
+
|
|
11863
|
+
// ../../packages/meshy-animation-catalog/src/index.ts
|
|
11864
|
+
function normalizedPackDefinition(definition) {
|
|
11865
|
+
const key = definition.key.trim();
|
|
11866
|
+
if (!key) throw new Error("Meshy controller-pack key cannot be empty");
|
|
11867
|
+
if (!Number.isInteger(definition.version) || definition.version < 1) {
|
|
11868
|
+
throw new Error("Meshy controller-pack version must be a positive integer");
|
|
11869
|
+
}
|
|
11870
|
+
const actionIds = [...definition.actionIds];
|
|
11871
|
+
if (actionIds.some((actionId) => !Number.isInteger(actionId))) {
|
|
11872
|
+
throw new Error("Meshy controller-pack action IDs must be integers");
|
|
11873
|
+
}
|
|
11874
|
+
if (new Set(actionIds).size !== actionIds.length) {
|
|
11875
|
+
throw new Error("Meshy controller-pack action IDs must be unique");
|
|
11876
|
+
}
|
|
11877
|
+
const allowed = new Set(actionIds);
|
|
11878
|
+
const bindings = {};
|
|
11879
|
+
for (const [slot, binding] of Object.entries(definition.bindings).sort(([left], [right]) => left.localeCompare(right))) {
|
|
11880
|
+
if (!slot.trim()) throw new Error("Meshy controller-pack slots cannot be empty");
|
|
11881
|
+
if (!allowed.has(binding.actionId)) {
|
|
11882
|
+
throw new Error(`Meshy controller-pack slot ${slot} refers to action ${binding.actionId} outside its action set`);
|
|
11883
|
+
}
|
|
11884
|
+
if (binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
|
|
11885
|
+
throw new Error(`Meshy controller-pack slot ${slot} has an invalid playback mode`);
|
|
11886
|
+
}
|
|
11887
|
+
if (binding.phase !== void 0 && (!Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
|
|
11888
|
+
throw new Error(`Meshy controller-pack slot ${slot} has an invalid normalized phase`);
|
|
11889
|
+
}
|
|
11890
|
+
bindings[slot] = {
|
|
11891
|
+
actionId: binding.actionId,
|
|
11892
|
+
mode: binding.mode,
|
|
11893
|
+
...binding.phase === void 0 ? {} : { phase: binding.phase }
|
|
11894
|
+
};
|
|
11895
|
+
}
|
|
11896
|
+
return { key, version: definition.version, actionIds, bindings };
|
|
11897
|
+
}
|
|
11898
|
+
function packFingerprint(definition) {
|
|
11899
|
+
return createHash("sha256").update(JSON.stringify(definition)).digest("hex");
|
|
11900
|
+
}
|
|
11901
|
+
function createMeshyControllerPackSnapshot(definition) {
|
|
11902
|
+
const normalized = normalizedPackDefinition(definition);
|
|
11903
|
+
const actionIds = Object.freeze([...normalized.actionIds]);
|
|
11904
|
+
const bindings = Object.freeze(Object.fromEntries(
|
|
11905
|
+
Object.entries(normalized.bindings).map(([slot, binding]) => [slot, Object.freeze({ ...binding })])
|
|
11906
|
+
));
|
|
11907
|
+
return Object.freeze({
|
|
11908
|
+
...normalized,
|
|
11909
|
+
actionIds,
|
|
11910
|
+
bindings,
|
|
11911
|
+
fingerprint: packFingerprint(normalized)
|
|
11912
|
+
});
|
|
11913
|
+
}
|
|
11914
|
+
var NEUTRAL_CONTROLLER_BINDINGS = {
|
|
11915
|
+
"idle.default": { actionId: 243, mode: "loop" },
|
|
11916
|
+
"walk.forward": { actionId: 613, mode: "loop" },
|
|
11917
|
+
"run.forward": { actionId: 657, mode: "loop" },
|
|
11918
|
+
"crouch.forward": { actionId: 616, mode: "loop" },
|
|
11919
|
+
// The reviewed opening frame is a stable lowered stance; the loader samples
|
|
11920
|
+
// it into a separate static clip so crouch movement can keep the full loop.
|
|
11921
|
+
"crouch.idle": { actionId: 616, mode: "pose", phase: 0 },
|
|
11922
|
+
"jump.full": { actionId: 466, mode: "one-shot" }
|
|
11923
|
+
};
|
|
11924
|
+
var MESHY_CONTROLLER_PACK = createMeshyControllerPackSnapshot({
|
|
11925
|
+
key: "neutral-v3",
|
|
11926
|
+
version: 3,
|
|
11927
|
+
actionIds: [243, 613, 657, 616, 466],
|
|
11928
|
+
bindings: NEUTRAL_CONTROLLER_BINDINGS
|
|
11929
|
+
});
|
|
11930
|
+
var MESHY_CONTROLLER_PACK_KEY = MESHY_CONTROLLER_PACK.key;
|
|
11931
|
+
var MESHY_CONTROLLER_PACK_VERSION = MESHY_CONTROLLER_PACK.version;
|
|
11932
|
+
var MESHY_CONTROLLER_PACK_FINGERPRINT = MESHY_CONTROLLER_PACK.fingerprint;
|
|
11933
|
+
var MESHY_CONTROLLER_CORE_ACTION_IDS = MESHY_CONTROLLER_PACK.actionIds;
|
|
11934
|
+
var MESHY_CONTROLLER_BINDINGS = MESHY_CONTROLLER_PACK.bindings;
|
|
11935
|
+
var SYNONYMS = {
|
|
11936
|
+
attack: ["fight", "punch", "kick", "weapon", "combat"],
|
|
11937
|
+
combat: ["fight", "attack", "punch", "weapon"],
|
|
11938
|
+
crouch: ["sneak", "stealth"],
|
|
11939
|
+
die: ["death", "dying", "fall"],
|
|
11940
|
+
emote: ["gesture", "acting", "dance"],
|
|
11941
|
+
gun: ["rifle", "pistol", "shoot", "firearm"],
|
|
11942
|
+
idle: ["stand", "breathing"],
|
|
11943
|
+
jump: ["leap", "vault"],
|
|
11944
|
+
run: ["running", "jog", "sprint", "charge"],
|
|
11945
|
+
sit: ["sitting", "chair", "seat"],
|
|
11946
|
+
skate: ["skateboard", "skating", "board"],
|
|
11947
|
+
sword: ["blade", "weapon", "slash"],
|
|
11948
|
+
walk: ["walking", "stride", "stroll"],
|
|
11949
|
+
wave: ["hello", "greeting", "gesture"]
|
|
11950
|
+
};
|
|
11951
|
+
var STOP_WORDS = /* @__PURE__ */ new Set(["a", "an", "and", "for", "of", "the", "to", "with"]);
|
|
11952
|
+
function tokens(value) {
|
|
11953
|
+
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter((token) => token && !STOP_WORDS.has(token));
|
|
11954
|
+
}
|
|
11955
|
+
function tokenGroup(token) {
|
|
11956
|
+
const group = /* @__PURE__ */ new Set([token, ...SYNONYMS[token] ?? []]);
|
|
11957
|
+
for (const [canonical, synonyms] of Object.entries(SYNONYMS)) {
|
|
11958
|
+
if (synonyms.includes(token)) {
|
|
11959
|
+
group.add(canonical);
|
|
11960
|
+
for (const synonym of synonyms) group.add(synonym);
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
return group;
|
|
11964
|
+
}
|
|
11965
|
+
var MESHY_ANIMATIONS = MESHY_ANIMATION_CATALOG.map(curateMeshyAnimation);
|
|
11966
|
+
var MESHY_ANIMATIONS_BY_ID = new Map(MESHY_ANIMATIONS.map((entry) => [entry.actionId, entry]));
|
|
11967
|
+
var MESHY_ANIMATIONS_BY_KEY = new Map(MESHY_ANIMATIONS.map((entry) => [entry.key.toLowerCase(), entry]));
|
|
11968
|
+
function animationById(actionId) {
|
|
11969
|
+
return MESHY_ANIMATIONS_BY_ID.get(actionId);
|
|
11970
|
+
}
|
|
11971
|
+
function searchMeshyAnimations(query, options = {}) {
|
|
11972
|
+
const rawQuery = query.trim().toLowerCase();
|
|
11973
|
+
const queryTokens = tokens(query);
|
|
11974
|
+
const groups = queryTokens.map(tokenGroup);
|
|
11975
|
+
const expanded = new Set(groups.flatMap((group) => [...group]));
|
|
11976
|
+
const results = [];
|
|
11977
|
+
for (const entry of MESHY_ANIMATIONS) {
|
|
11978
|
+
if (options.category && entry.category.toLowerCase() !== options.category.toLowerCase()) continue;
|
|
11979
|
+
if (options.inPlace !== void 0 && entry.inPlace !== options.inPlace) continue;
|
|
11980
|
+
const matched = [];
|
|
11981
|
+
let score = 0;
|
|
11982
|
+
if (String(entry.actionId) === rawQuery) {
|
|
11983
|
+
score += 1e5;
|
|
11984
|
+
matched.push("action id");
|
|
11985
|
+
}
|
|
11986
|
+
if (entry.key.toLowerCase() === rawQuery) {
|
|
11987
|
+
score += 8e4;
|
|
11988
|
+
matched.push("stable key");
|
|
11989
|
+
}
|
|
11990
|
+
if (entry.name.toLowerCase() === rawQuery) {
|
|
11991
|
+
score += 6e4;
|
|
11992
|
+
matched.push("exact name");
|
|
11993
|
+
}
|
|
11994
|
+
const haystack = new Set(tokens([
|
|
11995
|
+
entry.key,
|
|
11996
|
+
entry.name,
|
|
11997
|
+
entry.category,
|
|
11998
|
+
entry.subCategory,
|
|
11999
|
+
...entry.aliases,
|
|
12000
|
+
...entry.gameplayTags
|
|
12001
|
+
].join(" ")));
|
|
12002
|
+
let coveredGroups = 0;
|
|
12003
|
+
for (let index = 0; index < groups.length; index++) {
|
|
12004
|
+
const hits = [...groups[index]].filter((token) => haystack.has(token));
|
|
12005
|
+
if (hits.length === 0) continue;
|
|
12006
|
+
coveredGroups++;
|
|
12007
|
+
const original = queryTokens[index];
|
|
12008
|
+
const best = hits.includes(original) ? original : hits[0];
|
|
12009
|
+
score += best === original ? 400 : 140;
|
|
12010
|
+
matched.push(best);
|
|
12011
|
+
}
|
|
12012
|
+
if (entry.inPlace && (expanded.has("walk") || expanded.has("run"))) score += 35;
|
|
12013
|
+
const exact = score >= 6e4;
|
|
12014
|
+
if (score > 0 && (exact || coveredGroups === groups.length)) {
|
|
12015
|
+
results.push({ entry, score, matched: [...new Set(matched)] });
|
|
12016
|
+
}
|
|
12017
|
+
}
|
|
12018
|
+
return results.sort((a, b) => b.score - a.score || a.entry.name.localeCompare(b.entry.name) || a.entry.actionId - b.entry.actionId).slice(0, options.limit ?? 20);
|
|
12019
|
+
}
|
|
12020
|
+
|
|
12021
|
+
// src/lib/anims.ts
|
|
12022
|
+
import fs12 from "fs/promises";
|
|
12023
|
+
import path13 from "path";
|
|
12024
|
+
var ANIMS_DEST = path13.join("public", "assets", "anims");
|
|
12025
|
+
var HIDDEN_TAG = "reference";
|
|
12026
|
+
async function runAnims(opts) {
|
|
12027
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
12028
|
+
const root = opts.cwd ?? process.cwd();
|
|
12029
|
+
const selectors = opts.selectors ?? [];
|
|
12030
|
+
log.plain(c.bold("genex controller anims"));
|
|
12031
|
+
log.plain("");
|
|
12032
|
+
const { manifest, source } = await loadManifest(opts.animsBase);
|
|
12033
|
+
if (source === "snapshot") {
|
|
12034
|
+
log.dim(" (offline or CDN unreachable \u2014 using the bundled catalog snapshot)");
|
|
12035
|
+
}
|
|
12036
|
+
if (opts.list) {
|
|
12037
|
+
printCatalog(log, manifest, selectors);
|
|
12038
|
+
return;
|
|
12039
|
+
}
|
|
12040
|
+
const controllerMarker = path13.join(root, "src", "controllers", "character");
|
|
12041
|
+
if (!await exists2(controllerMarker)) {
|
|
12042
|
+
log.error(
|
|
12043
|
+
`No character controller in this game (missing ${c.cyan("src/controllers/character/")}).`
|
|
12044
|
+
);
|
|
12045
|
+
log.plain(` Run ${c.cyan("genex controller character")} first, then re-run this command.`);
|
|
12046
|
+
process.exitCode = 1;
|
|
12047
|
+
return;
|
|
12048
|
+
}
|
|
12049
|
+
const destDir = path13.join(root, ANIMS_DEST);
|
|
12050
|
+
const gameManifestPath = path13.join(destDir, "manifest.json");
|
|
12051
|
+
if (opts.reset) {
|
|
12052
|
+
await fs12.rm(destDir, { recursive: true, force: true });
|
|
12053
|
+
log.step(`Cleared ${c.cyan(ANIMS_DEST + path13.sep)} (--reset)`);
|
|
12054
|
+
}
|
|
12055
|
+
if (selectors.length === 0) {
|
|
12056
|
+
const installed = await readGameManifest(gameManifestPath);
|
|
12057
|
+
if (installed === null || installed.clips.length === 0) {
|
|
12058
|
+
log.plain(" No animation packs installed yet.");
|
|
12059
|
+
} else {
|
|
12060
|
+
log.plain(` Installed (${installed.clips.length} clips): ${installed.clips.join(", ")}`);
|
|
12061
|
+
}
|
|
12062
|
+
log.plain("");
|
|
12063
|
+
log.plain(
|
|
12064
|
+
` Install with ${c.cyan("genex controller anims <tag|clip \u2026>")}; browse with ${c.cyan(
|
|
12065
|
+
"genex controller anims --list"
|
|
12066
|
+
)}.`
|
|
12067
|
+
);
|
|
12068
|
+
return;
|
|
12069
|
+
}
|
|
12070
|
+
let resolved;
|
|
12071
|
+
try {
|
|
12072
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
12073
|
+
} catch (err) {
|
|
12074
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
12075
|
+
process.exitCode = 1;
|
|
12076
|
+
return;
|
|
12077
|
+
}
|
|
12078
|
+
const coreNames = new Set(manifest.core);
|
|
12079
|
+
const byName = /* @__PURE__ */ new Map();
|
|
12080
|
+
let bundledSkips = 0;
|
|
12081
|
+
for (const entries of resolved.values()) {
|
|
12082
|
+
for (const entry of entries) {
|
|
12083
|
+
if (coreNames.has(entry.name)) {
|
|
12084
|
+
bundledSkips++;
|
|
12085
|
+
continue;
|
|
12086
|
+
}
|
|
12087
|
+
byName.set(entry.name, entry);
|
|
12088
|
+
}
|
|
12089
|
+
}
|
|
12090
|
+
const wanted = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
12091
|
+
const cacheDir = path13.join(
|
|
12092
|
+
opts.cacheDir ?? getAnimsCacheDir(),
|
|
12093
|
+
`${manifest.library}-v${manifest.version}`
|
|
12094
|
+
);
|
|
12095
|
+
await fs12.mkdir(cacheDir, { recursive: true });
|
|
12096
|
+
await fs12.mkdir(destDir, { recursive: true });
|
|
12097
|
+
const base = getAnimsBase(opts.animsBase);
|
|
12098
|
+
let installedCount = 0;
|
|
12099
|
+
let presentCount = 0;
|
|
12100
|
+
let addedBytes = 0;
|
|
12101
|
+
const failures = [];
|
|
12102
|
+
for (const entry of wanted) {
|
|
12103
|
+
const dest = path13.join(destDir, entry.file);
|
|
12104
|
+
if (await hasSize(dest, entry.bytes)) {
|
|
12105
|
+
presentCount++;
|
|
12106
|
+
continue;
|
|
12107
|
+
}
|
|
12108
|
+
try {
|
|
12109
|
+
const cached = path13.join(cacheDir, entry.file);
|
|
12110
|
+
if (!await hasSize(cached, entry.bytes)) {
|
|
12111
|
+
const res = await fetch(base + entry.file);
|
|
12112
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
12113
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
12114
|
+
await fs12.writeFile(cached, buf);
|
|
12115
|
+
}
|
|
12116
|
+
await fs12.copyFile(cached, dest);
|
|
12117
|
+
installedCount++;
|
|
12118
|
+
addedBytes += entry.bytes;
|
|
12119
|
+
log.dim(` ${path13.join(ANIMS_DEST, entry.file)} (${formatMb(entry.bytes)})`);
|
|
12120
|
+
} catch (err) {
|
|
12121
|
+
failures.push(`${entry.name} (${err instanceof Error ? err.message : String(err)})`);
|
|
12122
|
+
}
|
|
12123
|
+
}
|
|
12124
|
+
const previous = await readGameManifest(gameManifestPath);
|
|
12125
|
+
const union = new Set(previous?.clips ?? []);
|
|
12126
|
+
for (const entry of wanted) {
|
|
12127
|
+
if (!failures.some((f) => f.startsWith(`${entry.name} (`))) union.add(entry.name);
|
|
12128
|
+
}
|
|
12129
|
+
const gameManifest = {
|
|
12130
|
+
schema: 1,
|
|
12131
|
+
library: manifest.library,
|
|
12132
|
+
version: manifest.version,
|
|
12133
|
+
clips: [...union].sort((a, b) => a.localeCompare(b))
|
|
12134
|
+
};
|
|
12135
|
+
await fs12.writeFile(gameManifestPath, JSON.stringify(gameManifest, null, 2) + "\n");
|
|
12136
|
+
log.plain("");
|
|
12137
|
+
const parts = [`${installedCount} clip${installedCount === 1 ? "" : "s"} installed`];
|
|
12138
|
+
if (presentCount > 0) parts.push(`${presentCount} already present`);
|
|
12139
|
+
if (bundledSkips > 0) parts.push(`${bundledSkips} already bundled in animation-library.glb`);
|
|
12140
|
+
log.success(
|
|
12141
|
+
`${parts.join(", ")} \u2192 ${c.cyan(ANIMS_DEST + path13.sep)}${addedBytes > 0 ? ` (+${formatMb(addedBytes)})` : ""}`
|
|
12142
|
+
);
|
|
12143
|
+
for (const [selector, entries] of resolved) {
|
|
12144
|
+
const names = entries.filter((e) => !coreNames.has(e.name)).map((e) => e.name);
|
|
12145
|
+
if (names.length > 0) log.dim(` ${selector}: ${names.join(", ")}`);
|
|
12146
|
+
}
|
|
12147
|
+
log.info(
|
|
12148
|
+
`Wiring: load the ${c.cyan("genex-threejs-character-controller")} skill \u2192 "Animation packs" (loadCharacterClips picks these up automatically).`
|
|
12149
|
+
);
|
|
12150
|
+
if (failures.length > 0) {
|
|
12151
|
+
log.plain("");
|
|
12152
|
+
log.error(
|
|
12153
|
+
`${failures.length} clip${failures.length === 1 ? "" : "s"} failed to download: ${failures.join(", ")}`
|
|
12154
|
+
);
|
|
12155
|
+
log.plain(
|
|
12156
|
+
" Each clip needs the network once per machine \u2014 check your connection and re-run the same command (already-installed clips are skipped)."
|
|
12157
|
+
);
|
|
12158
|
+
process.exitCode = 1;
|
|
12159
|
+
}
|
|
12160
|
+
}
|
|
12161
|
+
async function loadManifest(baseOverride) {
|
|
12162
|
+
const base = getAnimsBase(baseOverride);
|
|
12163
|
+
try {
|
|
12164
|
+
const res = await fetch(base + "manifest.json", { signal: AbortSignal.timeout(5e3) });
|
|
12165
|
+
if (res.ok) {
|
|
12166
|
+
const manifest2 = await res.json();
|
|
12167
|
+
if (manifest2.schema === 1 && Array.isArray(manifest2.clips)) {
|
|
12168
|
+
return { manifest: manifest2, source: "cdn" };
|
|
12169
|
+
}
|
|
12170
|
+
}
|
|
12171
|
+
} catch {
|
|
12172
|
+
}
|
|
12173
|
+
const snapshotPath = path13.join(getTemplatesDir(), "controllers", "assets", "anims-manifest.json");
|
|
12174
|
+
const manifest = JSON.parse(await fs12.readFile(snapshotPath, "utf8"));
|
|
12175
|
+
return { manifest, source: "snapshot" };
|
|
12176
|
+
}
|
|
12177
|
+
function resolveSelectors(manifest, selectors) {
|
|
12178
|
+
const byName = new Map(manifest.clips.map((entry) => [entry.name, entry]));
|
|
12179
|
+
const byLowerName = new Map(manifest.clips.map((entry) => [entry.name.toLowerCase(), entry]));
|
|
12180
|
+
const tags = /* @__PURE__ */ new Map();
|
|
12181
|
+
for (const entry of manifest.clips) {
|
|
12182
|
+
for (const tag of entry.tags) {
|
|
12183
|
+
const list = tags.get(tag) ?? [];
|
|
12184
|
+
list.push(entry);
|
|
12185
|
+
tags.set(tag, list);
|
|
12186
|
+
}
|
|
12187
|
+
}
|
|
12188
|
+
const out = /* @__PURE__ */ new Map();
|
|
12189
|
+
for (const selector of selectors) {
|
|
12190
|
+
const exact = byName.get(selector) ?? byLowerName.get(selector.toLowerCase());
|
|
12191
|
+
if (exact) {
|
|
12192
|
+
out.set(selector, [exact]);
|
|
12193
|
+
continue;
|
|
12194
|
+
}
|
|
12195
|
+
const tagHit = tags.get(selector) ?? tags.get(selector.toLowerCase());
|
|
12196
|
+
if (tagHit) {
|
|
12197
|
+
out.set(selector, tagHit);
|
|
12198
|
+
continue;
|
|
12199
|
+
}
|
|
12200
|
+
const candidates = [...tags.keys(), ...byName.keys()];
|
|
12201
|
+
const close = suggest(selector, candidates);
|
|
12202
|
+
throw new Error(
|
|
12203
|
+
`unknown clip/tag "${selector}"${close.length > 0 ? ` \u2014 closest: ${close.join(", ")}` : ""}. Run ${c.cyan(
|
|
12204
|
+
"genex controller anims --list"
|
|
12205
|
+
)} for the catalog.`
|
|
12206
|
+
);
|
|
12207
|
+
}
|
|
12208
|
+
return out;
|
|
12209
|
+
}
|
|
12210
|
+
function suggest(input, candidates) {
|
|
12211
|
+
const lower = input.toLowerCase();
|
|
12212
|
+
const scored = [];
|
|
12213
|
+
for (const candidate of candidates) {
|
|
12214
|
+
const candidateLower = candidate.toLowerCase();
|
|
12215
|
+
if (candidateLower.includes(lower) || lower.includes(candidateLower)) {
|
|
12216
|
+
scored.push({ name: candidate, score: 0 });
|
|
12217
|
+
continue;
|
|
12218
|
+
}
|
|
12219
|
+
const distance = levenshtein(lower, candidateLower, 2);
|
|
12220
|
+
if (distance <= 2) scored.push({ name: candidate, score: distance });
|
|
12221
|
+
}
|
|
12222
|
+
scored.sort((a, b) => a.score - b.score || a.name.localeCompare(b.name));
|
|
12223
|
+
return scored.slice(0, 4).map((s) => s.name);
|
|
12224
|
+
}
|
|
12225
|
+
function levenshtein(a, b, max) {
|
|
12226
|
+
if (Math.abs(a.length - b.length) > max) return max + 1;
|
|
12227
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
12228
|
+
for (let i = 1; i <= a.length; i++) {
|
|
12229
|
+
const curr = [i];
|
|
12230
|
+
let rowMin = i;
|
|
12231
|
+
for (let j = 1; j <= b.length; j++) {
|
|
12232
|
+
curr[j] = Math.min(
|
|
12233
|
+
prev[j] + 1,
|
|
12234
|
+
curr[j - 1] + 1,
|
|
12235
|
+
prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
12236
|
+
);
|
|
12237
|
+
if (curr[j] < rowMin) rowMin = curr[j];
|
|
12238
|
+
}
|
|
12239
|
+
if (rowMin > max) return max + 1;
|
|
12240
|
+
prev = curr;
|
|
12241
|
+
}
|
|
12242
|
+
return prev[b.length];
|
|
12243
|
+
}
|
|
12244
|
+
function printCatalog(log, manifest, selectors) {
|
|
12245
|
+
const coreNames = new Set(manifest.core);
|
|
12246
|
+
if (selectors.length > 0) {
|
|
12247
|
+
let resolved;
|
|
12248
|
+
try {
|
|
12249
|
+
resolved = resolveSelectors(manifest, selectors);
|
|
12250
|
+
} catch (err) {
|
|
12251
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
12252
|
+
process.exitCode = 1;
|
|
12253
|
+
return;
|
|
12254
|
+
}
|
|
12255
|
+
for (const [selector, entries] of resolved) {
|
|
12256
|
+
log.plain(c.bold(selector));
|
|
12257
|
+
for (const entry of entries) {
|
|
12258
|
+
const bundled = coreNames.has(entry.name) ? " (bundled)" : "";
|
|
12259
|
+
log.plain(
|
|
12260
|
+
` ${entry.name.padEnd(26)} ${entry.duration.toFixed(1)}s ${formatMb(entry.bytes)}${bundled} ${c.dim(entry.desc)}`
|
|
12261
|
+
);
|
|
12262
|
+
}
|
|
12263
|
+
}
|
|
12264
|
+
return;
|
|
12265
|
+
}
|
|
12266
|
+
const tags = /* @__PURE__ */ new Map();
|
|
12267
|
+
for (const entry of manifest.clips) {
|
|
12268
|
+
for (const tag of entry.tags) {
|
|
12269
|
+
if (tag === HIDDEN_TAG) continue;
|
|
12270
|
+
const list = tags.get(tag) ?? [];
|
|
12271
|
+
list.push(entry);
|
|
12272
|
+
tags.set(tag, list);
|
|
12273
|
+
}
|
|
12274
|
+
}
|
|
12275
|
+
log.plain(
|
|
12276
|
+
`${c.bold(`Animation packs`)} (${manifest.library} v${manifest.version}, ${manifest.clips.length} clips)`
|
|
12277
|
+
);
|
|
12278
|
+
log.plain(
|
|
12279
|
+
` Install: ${c.cyan("genex controller anims <tag|clip \u2026>")} Details: ${c.cyan(
|
|
12280
|
+
"genex controller anims --list <tag>"
|
|
12281
|
+
)}`
|
|
12282
|
+
);
|
|
12283
|
+
log.plain("");
|
|
12284
|
+
for (const [tag, entries] of [...tags.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
12285
|
+
const bytes = entries.reduce((sum, entry) => sum + entry.bytes, 0);
|
|
12286
|
+
const suffix = tag === "core" ? " \u2014 bundled in animation-library.glb" : ` (${formatMb(bytes)})`;
|
|
12287
|
+
log.plain(` ${c.bold(tag.padEnd(18))} ${entries.map((e) => e.name).join(", ")}${suffix}`);
|
|
12288
|
+
}
|
|
12289
|
+
}
|
|
12290
|
+
async function readGameManifest(file) {
|
|
12291
|
+
try {
|
|
12292
|
+
return JSON.parse(await fs12.readFile(file, "utf8"));
|
|
12293
|
+
} catch {
|
|
12294
|
+
return null;
|
|
12245
12295
|
}
|
|
12246
|
-
};
|
|
12247
|
-
var LOOP_SUBCATEGORIES = /* @__PURE__ */ new Set(["Idle", "Walking", "Running", "CrouchWalking", "Swimming"]);
|
|
12248
|
-
var CHOREOGRAPHY_SUBCATEGORIES = /* @__PURE__ */ new Set([
|
|
12249
|
-
"Climbing",
|
|
12250
|
-
"HangingfromLedge",
|
|
12251
|
-
"VaultingOverObstacle",
|
|
12252
|
-
"Interacting",
|
|
12253
|
-
"PickingUpItem",
|
|
12254
|
-
"Pushing",
|
|
12255
|
-
"Sleeping"
|
|
12256
|
-
]);
|
|
12257
|
-
var PLANAR_ACTION = /(?:roll|dodge|lunge|charge|step[_ -](?:back|forward)|slide)/i;
|
|
12258
|
-
var NON_LOOP_ACTION = /(?:transition|start|stop|turn)/i;
|
|
12259
|
-
function words(value) {
|
|
12260
|
-
return value.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter(Boolean);
|
|
12261
12296
|
}
|
|
12262
|
-
function
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
|
|
12267
|
-
|
|
12268
|
-
["shield", /\bshield\b/],
|
|
12269
|
-
["chair", /\b(?:chair|sit|sitting)\b/],
|
|
12270
|
-
["ladder", /\bladder\b/],
|
|
12271
|
-
["rope", /\brope\b/]
|
|
12272
|
-
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
12273
|
-
const environment = [
|
|
12274
|
-
["climbable", /\b(?:climb|climbing|ladder|ledge|wall|rope)\b/],
|
|
12275
|
-
["vault obstacle", /\b(?:vault|vaulting|obstacle)\b/],
|
|
12276
|
-
["water", /\b(?:swim|swimming)\b/],
|
|
12277
|
-
["seat", /\b(?:sit|sitting|chair)\b/]
|
|
12278
|
-
].filter(([, pattern]) => pattern.test(lower)).map(([name]) => name);
|
|
12279
|
-
return { props, environment, partner: /\b(?:partner|carry person|hug|handshake)\b/.test(lower) };
|
|
12297
|
+
async function hasSize(file, bytes) {
|
|
12298
|
+
try {
|
|
12299
|
+
return (await fs12.stat(file)).size === bytes;
|
|
12300
|
+
} catch {
|
|
12301
|
+
return false;
|
|
12302
|
+
}
|
|
12280
12303
|
}
|
|
12281
|
-
function
|
|
12282
|
-
|
|
12283
|
-
|
|
12284
|
-
|
|
12285
|
-
|
|
12286
|
-
|
|
12304
|
+
async function exists2(p) {
|
|
12305
|
+
try {
|
|
12306
|
+
await fs12.access(p);
|
|
12307
|
+
return true;
|
|
12308
|
+
} catch {
|
|
12309
|
+
return false;
|
|
12310
|
+
}
|
|
12287
12311
|
}
|
|
12288
|
-
function
|
|
12289
|
-
|
|
12290
|
-
const requirements = inferRequirements(raw.key, raw.subCategory);
|
|
12291
|
-
const choreography = CHOREOGRAPHY_SUBCATEGORIES.has(raw.subCategory) || requirements.partner;
|
|
12292
|
-
const loop = inPlace && LOOP_SUBCATEGORIES.has(raw.subCategory) && !NON_LOOP_ACTION.test(`${raw.key} ${raw.name}`);
|
|
12293
|
-
const motionPolicy = loop ? "controller-loop" : choreography ? "choreography" : PLANAR_ACTION.test(`${raw.key} ${raw.name}`) ? "planar-root-action" : "anchored-action";
|
|
12294
|
-
const base = {
|
|
12295
|
-
actionId: raw.actionId,
|
|
12296
|
-
key: raw.key,
|
|
12297
|
-
name: raw.name,
|
|
12298
|
-
category: raw.category,
|
|
12299
|
-
subCategory: raw.subCategory,
|
|
12300
|
-
previewUrl: raw.previewUrl,
|
|
12301
|
-
rigType: raw.rigType,
|
|
12302
|
-
inPlace,
|
|
12303
|
-
isDefault: raw.isDefault,
|
|
12304
|
-
isFree: raw.isFree,
|
|
12305
|
-
createdAt: raw.createdAt,
|
|
12306
|
-
aliases: [.../* @__PURE__ */ new Set([raw.key.replaceAll("_", " "), raw.name])],
|
|
12307
|
-
gameplayTags: [.../* @__PURE__ */ new Set([...words(raw.category), ...words(raw.subCategory), ...inPlace ? ["in-place"] : [], ...requirements.props, ...requirements.environment])],
|
|
12308
|
-
loop,
|
|
12309
|
-
motionPolicy,
|
|
12310
|
-
rootMotionValidated: false,
|
|
12311
|
-
// A name that contains "walk" or "run" is not enough to make a safe
|
|
12312
|
-
// controller loop. Only provider-declared InPlace loops (plus explicit
|
|
12313
|
-
// measured overrides above) may populate automatic locomotion slots.
|
|
12314
|
-
controllerSlots: loop ? inferSlots(raw.key) : [],
|
|
12315
|
-
requirements,
|
|
12316
|
-
reviewStatus: "metadata-reviewed"
|
|
12317
|
-
};
|
|
12318
|
-
return { ...base, ...CURATED[raw.actionId], requirements };
|
|
12312
|
+
function formatMb(bytes) {
|
|
12313
|
+
return bytes >= 1e6 ? `${(bytes / 1e6).toFixed(1)} MB` : `${Math.round(bytes / 1e3)} KB`;
|
|
12319
12314
|
}
|
|
12320
12315
|
|
|
12321
|
-
//
|
|
12322
|
-
var
|
|
12323
|
-
|
|
12324
|
-
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12328
|
-
|
|
12329
|
-
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
12334
|
-
|
|
12335
|
-
|
|
12336
|
-
|
|
12316
|
+
// src/commands/controller.ts
|
|
12317
|
+
var CONTROLLER_KINDS = [
|
|
12318
|
+
"character",
|
|
12319
|
+
"car",
|
|
12320
|
+
"drone",
|
|
12321
|
+
"touch",
|
|
12322
|
+
"networked-physics"
|
|
12323
|
+
];
|
|
12324
|
+
var SHARED = [
|
|
12325
|
+
"shared/math.ts",
|
|
12326
|
+
"shared/physics-world.ts",
|
|
12327
|
+
"shared/colliders.ts"
|
|
12328
|
+
];
|
|
12329
|
+
var TOUCH_KIT = [
|
|
12330
|
+
"touch/touch-joystick.ts",
|
|
12331
|
+
"touch/drag-zone.ts",
|
|
12332
|
+
"touch/rotate-overlay.ts"
|
|
12333
|
+
];
|
|
12334
|
+
var INPUT_AND_CAMERA = [
|
|
12335
|
+
"character/follow-camera.ts",
|
|
12336
|
+
"character/keyboard-input.ts",
|
|
12337
|
+
"character/touch-joystick.ts",
|
|
12338
|
+
...TOUCH_KIT
|
|
12339
|
+
];
|
|
12340
|
+
var NOTICE = "NOTICE.md";
|
|
12341
|
+
var CONTROLLER_FILE_SETS = {
|
|
12342
|
+
character: {
|
|
12343
|
+
code: [
|
|
12344
|
+
...SHARED,
|
|
12345
|
+
"character/character-controller.ts",
|
|
12346
|
+
"character/character-animations.ts",
|
|
12347
|
+
"character/animation-packs.ts",
|
|
12348
|
+
"character/motion-actions.ts",
|
|
12349
|
+
"character/meshy/meshy-loader.ts",
|
|
12350
|
+
"character/presets.ts",
|
|
12351
|
+
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
12352
|
+
// the capsule + optional foot IK. Owner's avatar replaces the old mannequin.
|
|
12353
|
+
"character/vrm/vrm-loader.ts",
|
|
12354
|
+
"character/vrm/vrm-retarget.ts",
|
|
12355
|
+
"character/vrm/capsule-fit.ts",
|
|
12356
|
+
"character/vrm/foot-ik.ts",
|
|
12357
|
+
...INPUT_AND_CAMERA,
|
|
12358
|
+
NOTICE
|
|
12359
|
+
],
|
|
12360
|
+
// The player's VRM is written to public/assets/avatar.vrm at install time by
|
|
12361
|
+
// installOwnerAvatar (owner's avatar, or the bundled default) — so it is NOT
|
|
12362
|
+
// a static manifest asset. animation-library.glb (the 12-clip core) still is;
|
|
12363
|
+
// extra clips arrive via `genex controller anims` into public/assets/anims/.
|
|
12364
|
+
assets: ["assets/animation-library.glb"],
|
|
12365
|
+
skill: "genex-threejs-character-controller",
|
|
12366
|
+
sketch: [
|
|
12367
|
+
`const physics = await PhysicsWorld.create();`,
|
|
12368
|
+
`const { scene, vrm } = await loadVrm("./assets/avatar.vrm");`,
|
|
12369
|
+
`const clips = await loadCharacterClips(vrm); // core library + every genex-controller-anims pack`,
|
|
12370
|
+
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...capsuleFromModel(scene), position: { x: 0, y: 2, z: 0 } });`,
|
|
12371
|
+
`character.root.add(scene); const anims = new CharacterAnimations(scene, clips);`,
|
|
12372
|
+
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); vrm.update(dt);`
|
|
12373
|
+
]
|
|
12374
|
+
},
|
|
12375
|
+
car: {
|
|
12376
|
+
code: [
|
|
12377
|
+
...SHARED,
|
|
12378
|
+
"vehicle/vehicle-controller.ts",
|
|
12379
|
+
"vehicle/wheel.ts",
|
|
12380
|
+
"vehicle/presets.ts",
|
|
12381
|
+
"interact/enter-exit.ts",
|
|
12382
|
+
...INPUT_AND_CAMERA,
|
|
12383
|
+
NOTICE
|
|
12384
|
+
],
|
|
12385
|
+
assets: [],
|
|
12386
|
+
skill: "genex-threejs-vehicle-controllers",
|
|
12387
|
+
sketch: [
|
|
12388
|
+
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
12389
|
+
`const car = new VehicleController({ world: physics.world, position, carConfig: vehiclePresets["arcade-kart"].carConfig }); // + chassis colliders + car.addWheel(...) per preset slot`,
|
|
12390
|
+
`scene.add(car.chassisObject); physics.onBeforeStep(() => { car.setMovement(keyboard.getCarMovement()); car.update(); });`
|
|
12391
|
+
]
|
|
12392
|
+
},
|
|
12393
|
+
drone: {
|
|
12394
|
+
code: [
|
|
12395
|
+
...SHARED,
|
|
12396
|
+
"drone/drone-controller.ts",
|
|
12397
|
+
"drone/presets.ts",
|
|
12398
|
+
"interact/enter-exit.ts",
|
|
12399
|
+
...INPUT_AND_CAMERA,
|
|
12400
|
+
NOTICE
|
|
12401
|
+
],
|
|
12402
|
+
assets: [],
|
|
12403
|
+
skill: "genex-threejs-vehicle-controllers",
|
|
12404
|
+
sketch: [
|
|
12405
|
+
`const physics = await PhysicsWorld.create(); // then physics.step(delta) every frame`,
|
|
12406
|
+
`const drone = new DroneController({ world: physics.world, body, chassis, propellers, config: dronePresets["camera-drone"].config });`,
|
|
12407
|
+
`physics.onBeforeStep(() => { drone.setMovement(keyboard.getDroneMovement()); drone.update(); });`
|
|
12408
|
+
]
|
|
12409
|
+
},
|
|
12410
|
+
touch: {
|
|
12411
|
+
code: [...TOUCH_KIT, NOTICE],
|
|
12412
|
+
assets: [],
|
|
12413
|
+
skill: "genex-threejs-touch-controls",
|
|
12414
|
+
sketch: [
|
|
12415
|
+
`const joy = new TouchJoystick({ floating: true }); // safe-area-aware defaults; static circle without the flag`,
|
|
12416
|
+
`const jump = new VirtualButton({ label: "Jump", onPress: () => player.jump() });`,
|
|
12417
|
+
`const look = new DragZone(); // right half; per frame: const { dx, dy } = look.consumeDelta()`,
|
|
12418
|
+
`[joy, jump, look].forEach((w) => w.setVisible(navigator.maxTouchPoints > 0));`
|
|
12419
|
+
]
|
|
12420
|
+
},
|
|
12421
|
+
"networked-physics": {
|
|
12422
|
+
code: [
|
|
12423
|
+
...SHARED,
|
|
12424
|
+
"network/pose.ts",
|
|
12425
|
+
"network/networked-pushable.ts",
|
|
12426
|
+
"network/networked-vehicle.ts",
|
|
12427
|
+
"NETWORKING.md",
|
|
12428
|
+
NOTICE
|
|
12429
|
+
],
|
|
12430
|
+
assets: [],
|
|
12431
|
+
skill: "genex-threejs-multiplayer",
|
|
12432
|
+
sketch: [
|
|
12433
|
+
`const box = new NetworkedPushable({ id: "box:1", room: () => room, body, object: mesh });`,
|
|
12434
|
+
`physics.onBeforeStep(() => box.update()); physics.onAfterStep(() => box.publish());`,
|
|
12435
|
+
`contacts.onChange((active) => box.setContact(active)); // retries held claims while contact persists`
|
|
12436
|
+
]
|
|
12437
|
+
}
|
|
12337
12438
|
};
|
|
12338
|
-
var
|
|
12339
|
-
|
|
12340
|
-
|
|
12341
|
-
}
|
|
12342
|
-
|
|
12343
|
-
|
|
12344
|
-
|
|
12345
|
-
|
|
12346
|
-
|
|
12347
|
-
|
|
12439
|
+
var CODE_DEST = path14.join("src", "controllers");
|
|
12440
|
+
var ASSETS_DEST = path14.join("public", "assets");
|
|
12441
|
+
async function runController(opts) {
|
|
12442
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
12443
|
+
if (opts.kind?.trim() === "anims") {
|
|
12444
|
+
await runAnims(opts);
|
|
12445
|
+
return;
|
|
12446
|
+
}
|
|
12447
|
+
const kind = opts.kind?.trim();
|
|
12448
|
+
if (!kind || !CONTROLLER_KINDS.includes(kind)) {
|
|
12449
|
+
log.error(
|
|
12450
|
+
`Missing or unknown controller type${kind ? ` "${kind}"` : ""}. Usage: ${c.cyan(
|
|
12451
|
+
"genex controller <character|car|drone|touch|networked-physics> [--force]"
|
|
12452
|
+
)} or ${c.cyan("genex controller anims <tag|clip \u2026>")}`
|
|
12453
|
+
);
|
|
12454
|
+
process.exitCode = 1;
|
|
12455
|
+
return;
|
|
12456
|
+
}
|
|
12457
|
+
const srcDir = path14.join(getTemplatesDir(), "controllers");
|
|
12458
|
+
const root = opts.cwd ?? process.cwd();
|
|
12459
|
+
const set = CONTROLLER_FILE_SETS[kind];
|
|
12460
|
+
log.plain(c.bold(`genex controller ${kind}`));
|
|
12461
|
+
log.plain("");
|
|
12462
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path14.sep)}`);
|
|
12463
|
+
const plan = [
|
|
12464
|
+
...set.code.map((rel) => ({ from: rel, rel: path14.join(CODE_DEST, rel) })),
|
|
12465
|
+
...set.assets.map((rel) => ({
|
|
12466
|
+
from: rel,
|
|
12467
|
+
rel: path14.join(ASSETS_DEST, path14.basename(rel))
|
|
12468
|
+
}))
|
|
12469
|
+
];
|
|
12470
|
+
let copied = 0;
|
|
12471
|
+
let skipped = 0;
|
|
12472
|
+
try {
|
|
12473
|
+
for (const file of plan) {
|
|
12474
|
+
const dest = path14.join(root, file.rel);
|
|
12475
|
+
if (!opts.force && await exists3(dest)) {
|
|
12476
|
+
skipped++;
|
|
12477
|
+
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
12478
|
+
continue;
|
|
12479
|
+
}
|
|
12480
|
+
await fs13.mkdir(path14.dirname(dest), { recursive: true });
|
|
12481
|
+
await fs13.copyFile(path14.join(srcDir, file.from), dest);
|
|
12482
|
+
copied++;
|
|
12483
|
+
log.dim(` ${file.rel}`);
|
|
12348
12484
|
}
|
|
12485
|
+
} catch (err) {
|
|
12486
|
+
log.error(`Copy failed: ${String(err)}`);
|
|
12487
|
+
process.exitCode = 1;
|
|
12488
|
+
return;
|
|
12349
12489
|
}
|
|
12350
|
-
|
|
12351
|
-
}
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
12363
|
-
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
12367
|
-
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
12371
|
-
|
|
12490
|
+
log.success(
|
|
12491
|
+
`Controller files ready (${copied} copied${skipped > 0 ? `, ${skipped} skipped` : ""}).`
|
|
12492
|
+
);
|
|
12493
|
+
log.plain("");
|
|
12494
|
+
if (kind === "character") {
|
|
12495
|
+
if (opts.character) {
|
|
12496
|
+
try {
|
|
12497
|
+
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
12498
|
+
if (!token) throw new Error("Not authorized. Run `genex init` before installing a Meshy character.");
|
|
12499
|
+
await installMeshyCharacterManifest({
|
|
12500
|
+
root,
|
|
12501
|
+
characterId: opts.character,
|
|
12502
|
+
apiUrl: getApiUrl(opts.apiUrl),
|
|
12503
|
+
token,
|
|
12504
|
+
log
|
|
12505
|
+
});
|
|
12506
|
+
} catch (error) {
|
|
12507
|
+
log.error(error instanceof Error ? error.message : String(error));
|
|
12508
|
+
process.exitCode = 1;
|
|
12509
|
+
return;
|
|
12510
|
+
}
|
|
12511
|
+
} else {
|
|
12512
|
+
const token = opts.token !== void 0 ? opts.token : await readUserToken();
|
|
12513
|
+
await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
|
|
12372
12514
|
}
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12515
|
+
log.plain("");
|
|
12516
|
+
}
|
|
12517
|
+
log.plain(c.bold("Next steps"));
|
|
12518
|
+
if (kind !== "touch") {
|
|
12519
|
+
log.plain(
|
|
12520
|
+
` 1. ${c.cyan(
|
|
12521
|
+
kind === "character" ? "npm i @dimforge/rapier3d-compat @pixiv/three-vrm" : kind === "networked-physics" ? "npm i @dimforge/rapier3d-compat @genex-ai/multiplayer" : "npm i @dimforge/rapier3d-compat"
|
|
12522
|
+
)} (three is already in the scaffold).`
|
|
12523
|
+
);
|
|
12524
|
+
}
|
|
12525
|
+
const stepOffset = kind === "touch" ? 0 : 1;
|
|
12526
|
+
log.plain(
|
|
12527
|
+
` ${stepOffset + 1}. Load the ${c.cyan(set.skill)} skill for wiring, presets, and tuning.`
|
|
12528
|
+
);
|
|
12529
|
+
log.plain(
|
|
12530
|
+
kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
|
|
12531
|
+
);
|
|
12532
|
+
const sketch = kind === "character" && opts.character ? [
|
|
12533
|
+
`const physics = await PhysicsWorld.create();`,
|
|
12534
|
+
`const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
|
|
12535
|
+
`const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
12536
|
+
`character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
|
|
12537
|
+
`physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
|
|
12538
|
+
] : set.sketch;
|
|
12539
|
+
for (const line of sketch) {
|
|
12540
|
+
log.dim(` ${line}`);
|
|
12541
|
+
}
|
|
12542
|
+
await firstPreviewNudge(log);
|
|
12543
|
+
}
|
|
12544
|
+
async function installMeshyCharacterManifest(args) {
|
|
12545
|
+
const response = await apiFetch(
|
|
12546
|
+
`${args.apiUrl}/api/characters/${encodeURIComponent(args.characterId)}/manifest`,
|
|
12547
|
+
{ headers: { Authorization: `Bearer ${args.token}` } }
|
|
12548
|
+
);
|
|
12549
|
+
if (response.status === 404) throw new Error(`Meshy character ${args.characterId} was not found on this account.`);
|
|
12550
|
+
if (!response.ok) {
|
|
12551
|
+
let detail;
|
|
12552
|
+
try {
|
|
12553
|
+
const errorBody = await response.json();
|
|
12554
|
+
if (typeof errorBody.message === "string" && errorBody.message.length > 0) detail = errorBody.message;
|
|
12555
|
+
else if (typeof errorBody.error === "string" && errorBody.error.length > 0) detail = errorBody.error;
|
|
12556
|
+
} catch {
|
|
12376
12557
|
}
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12558
|
+
throw new Error(detail ?? `Couldn't fetch the Meshy character manifest (HTTP ${response.status}).`);
|
|
12559
|
+
}
|
|
12560
|
+
const body = await response.json();
|
|
12561
|
+
const manifest = body.manifest;
|
|
12562
|
+
if (!manifest || manifest.schema !== 1 || manifest.rig !== "meshy-biped" || manifest.characterId !== args.characterId || typeof manifest.model?.url !== "string" || typeof manifest.model.skeletonSignature !== "string" || !Array.isArray(manifest.clips) || !manifest.locomotion?.slots) {
|
|
12563
|
+
throw new Error("The API returned an invalid Meshy character manifest.");
|
|
12564
|
+
}
|
|
12565
|
+
assertCompleteMeshyControllerPack(manifest);
|
|
12566
|
+
const destination = path14.join(args.root, ASSETS_DEST, "meshy-character.json");
|
|
12567
|
+
await fs13.mkdir(path14.dirname(destination), { recursive: true });
|
|
12568
|
+
await fs13.writeFile(
|
|
12569
|
+
destination,
|
|
12570
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
12571
|
+
`
|
|
12572
|
+
);
|
|
12573
|
+
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
12574
|
+
const pack = manifest.controllerPack;
|
|
12575
|
+
if (typeof pack?.key === "string" && typeof pack.version === "number") {
|
|
12576
|
+
args.log.success(`Meshy controller pack ${pack.key} v${pack.version}`);
|
|
12577
|
+
} else {
|
|
12578
|
+
args.log.warn("Legacy Meshy manifest \u2014 regenerate the character for the immutable preview-reviewed neutral-v3 locomotion pack.");
|
|
12579
|
+
}
|
|
12580
|
+
const provenance = manifest.provenance;
|
|
12581
|
+
if (provenance?.provider === "meshy" && typeof provenance.aiModel === "string") {
|
|
12582
|
+
const apiVersion = typeof provenance.apiVersion === "string" && provenance.apiVersion.length > 0 ? `; API ${provenance.apiVersion}` : "";
|
|
12583
|
+
args.log.success(`Meshy model: ${provenance.aiModel}${apiVersion}`);
|
|
12584
|
+
if ((provenance.poseMode === "a-pose" || provenance.poseMode === "t-pose") && typeof provenance.shouldRemesh === "boolean" && typeof provenance.targetPolycount === "number") {
|
|
12585
|
+
args.log.dim(
|
|
12586
|
+
` generation: ${provenance.poseMode}, remesh ${provenance.shouldRemesh ? "on" : "off"}, target ${provenance.targetPolycount} polygons`
|
|
12587
|
+
);
|
|
12380
12588
|
}
|
|
12381
|
-
|
|
12382
|
-
|
|
12383
|
-
|
|
12384
|
-
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
].
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
coveredGroups++;
|
|
12394
|
-
const original = queryTokens[index];
|
|
12395
|
-
const best = hits.includes(original) ? original : hits[0];
|
|
12396
|
-
score += best === original ? 400 : 140;
|
|
12397
|
-
matched.push(best);
|
|
12589
|
+
} else {
|
|
12590
|
+
args.log.warn("Meshy model/version metadata is unavailable in this legacy manifest.");
|
|
12591
|
+
}
|
|
12592
|
+
const actionIds = (manifest.clips ?? []).map((clip) => clip.actionId).filter((actionId) => typeof actionId === "number");
|
|
12593
|
+
args.log.dim(` installed action ids: ${actionIds.length > 0 ? actionIds.join(", ") : "none"}`);
|
|
12594
|
+
const bindings = manifest.locomotion?.bindings;
|
|
12595
|
+
if (bindings) {
|
|
12596
|
+
for (const [slot, binding] of Object.entries(bindings).sort(([a], [b]) => a.localeCompare(b))) {
|
|
12597
|
+
if (typeof binding?.actionId === "number" && typeof binding.clip === "string" && typeof binding.mode === "string") {
|
|
12598
|
+
const phase = typeof binding.phase === "number" ? ` @ phase ${binding.phase}` : "";
|
|
12599
|
+
args.log.dim(` ${slot} -> action ${binding.actionId} (${binding.clip}, ${binding.mode}${phase})`);
|
|
12600
|
+
}
|
|
12398
12601
|
}
|
|
12399
|
-
|
|
12400
|
-
|
|
12401
|
-
|
|
12402
|
-
|
|
12602
|
+
}
|
|
12603
|
+
const slots = Object.keys(bindings ?? manifest.locomotion?.slots ?? {}).sort();
|
|
12604
|
+
args.log.dim(` locomotion slots: ${slots.length > 0 ? slots.join(", ") : "none"}`);
|
|
12605
|
+
const crouchCovered = slots.includes("crouch.idle") && slots.includes("crouch.forward");
|
|
12606
|
+
if (crouchCovered) args.log.success("Visual crouch coverage: idle + move");
|
|
12607
|
+
else args.log.warn("Visual crouch coverage is incomplete; physics crouch may fall back to a standing pose.");
|
|
12608
|
+
}
|
|
12609
|
+
var REQUIRED_MESHY_CONTROLLER_SLOTS = [
|
|
12610
|
+
"idle.default",
|
|
12611
|
+
"walk.forward",
|
|
12612
|
+
"run.forward",
|
|
12613
|
+
"crouch.forward",
|
|
12614
|
+
"crouch.idle",
|
|
12615
|
+
"jump.full"
|
|
12616
|
+
];
|
|
12617
|
+
function incompletePack(message) {
|
|
12618
|
+
throw new Error(`Incomplete Meshy controller pack: ${message}`);
|
|
12619
|
+
}
|
|
12620
|
+
function assertCompleteMeshyControllerPack(manifest) {
|
|
12621
|
+
const pack = manifest.controllerPack;
|
|
12622
|
+
if (pack === void 0) return;
|
|
12623
|
+
if (typeof pack.key !== "string" || pack.key.length === 0 || typeof pack.version !== "number" || !Number.isInteger(pack.version)) {
|
|
12624
|
+
incompletePack("invalid key or version.");
|
|
12625
|
+
}
|
|
12626
|
+
if (typeof pack.fingerprint !== "string" || pack.fingerprint.length === 0) {
|
|
12627
|
+
incompletePack(`${pack.key} v${pack.version} has no immutable fingerprint.`);
|
|
12628
|
+
}
|
|
12629
|
+
if (!Array.isArray(pack.actionIds) || pack.actionIds.length === 0 || pack.actionIds.some((actionId) => typeof actionId !== "number" || !Number.isInteger(actionId))) {
|
|
12630
|
+
incompletePack(`${pack.key} v${pack.version} has no valid action snapshot.`);
|
|
12631
|
+
}
|
|
12632
|
+
const actionIds = pack.actionIds;
|
|
12633
|
+
const packActionIds = new Set(actionIds);
|
|
12634
|
+
if (packActionIds.size !== actionIds.length) {
|
|
12635
|
+
incompletePack(`${pack.key} v${pack.version} repeats an action ID.`);
|
|
12636
|
+
}
|
|
12637
|
+
const bindings = manifest.locomotion?.bindings;
|
|
12638
|
+
if (!bindings) incompletePack(`${pack.key} v${pack.version} has no authoritative locomotion bindings.`);
|
|
12639
|
+
const missingSlots = REQUIRED_MESHY_CONTROLLER_SLOTS.filter((slot) => bindings[slot] === void 0);
|
|
12640
|
+
if (missingSlots.length > 0) {
|
|
12641
|
+
incompletePack(`${pack.key} v${pack.version} is missing ${missingSlots.join(", ")}.`);
|
|
12642
|
+
}
|
|
12643
|
+
const clips = manifest.clips ?? [];
|
|
12644
|
+
const boundActionIds = /* @__PURE__ */ new Set();
|
|
12645
|
+
const snapshotBindings = {};
|
|
12646
|
+
for (const [slot, binding] of Object.entries(bindings)) {
|
|
12647
|
+
if (typeof binding.actionId !== "number" || !Number.isInteger(binding.actionId) || typeof binding.clip !== "string" || binding.clip.length === 0 || binding.mode !== "loop" && binding.mode !== "one-shot" && binding.mode !== "pose") {
|
|
12648
|
+
incompletePack(`${pack.key} v${pack.version} has an invalid ${slot} binding.`);
|
|
12649
|
+
}
|
|
12650
|
+
if (binding.phase !== void 0 && (typeof binding.phase !== "number" || !Number.isFinite(binding.phase) || binding.phase < 0 || binding.phase > 1)) {
|
|
12651
|
+
incompletePack(`${pack.key} v${pack.version} has an invalid ${slot} phase.`);
|
|
12652
|
+
}
|
|
12653
|
+
const actionId = binding.actionId;
|
|
12654
|
+
boundActionIds.add(actionId);
|
|
12655
|
+
if (!packActionIds.has(actionId)) {
|
|
12656
|
+
incompletePack(`${slot} points at action ${actionId}, which is outside the stored pack snapshot.`);
|
|
12657
|
+
}
|
|
12658
|
+
const clip = clips.find((entry) => entry.actionId === actionId);
|
|
12659
|
+
if (!clip || clip.key !== binding.clip) {
|
|
12660
|
+
incompletePack(`${slot} expects action ${actionId} clip ${binding.clip}, but that exact clip is not installed.`);
|
|
12661
|
+
}
|
|
12662
|
+
snapshotBindings[slot] = {
|
|
12663
|
+
actionId,
|
|
12664
|
+
mode: binding.mode,
|
|
12665
|
+
...binding.phase === void 0 ? {} : { phase: binding.phase }
|
|
12666
|
+
};
|
|
12667
|
+
}
|
|
12668
|
+
const unboundActionIds = actionIds.filter((actionId) => !boundActionIds.has(actionId));
|
|
12669
|
+
if (unboundActionIds.length > 0) {
|
|
12670
|
+
incompletePack(`${pack.key} v${pack.version} action snapshot contains unbound actions ${unboundActionIds.join(", ")}.`);
|
|
12671
|
+
}
|
|
12672
|
+
let computedFingerprint;
|
|
12673
|
+
try {
|
|
12674
|
+
computedFingerprint = createMeshyControllerPackSnapshot({
|
|
12675
|
+
key: pack.key,
|
|
12676
|
+
version: pack.version,
|
|
12677
|
+
actionIds,
|
|
12678
|
+
bindings: snapshotBindings
|
|
12679
|
+
}).fingerprint;
|
|
12680
|
+
} catch (error) {
|
|
12681
|
+
incompletePack(error instanceof Error ? error.message : "the immutable snapshot is invalid.");
|
|
12682
|
+
}
|
|
12683
|
+
if (computedFingerprint !== pack.fingerprint) {
|
|
12684
|
+
incompletePack(`${pack.key} v${pack.version} fingerprint does not match its action bindings.`);
|
|
12685
|
+
}
|
|
12686
|
+
}
|
|
12687
|
+
async function installOwnerAvatar(args) {
|
|
12688
|
+
const { root, srcDir, apiUrl, token, log } = args;
|
|
12689
|
+
const dest = path14.join(root, ASSETS_DEST, "avatar.vrm");
|
|
12690
|
+
await fs13.mkdir(path14.dirname(dest), { recursive: true });
|
|
12691
|
+
if (token) {
|
|
12692
|
+
try {
|
|
12693
|
+
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
12694
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
12695
|
+
});
|
|
12696
|
+
if (res.ok) {
|
|
12697
|
+
const me = await res.json();
|
|
12698
|
+
if (me.vrmUrl) {
|
|
12699
|
+
const vrmRes = await fetch(me.vrmUrl);
|
|
12700
|
+
if (vrmRes.ok) {
|
|
12701
|
+
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
12702
|
+
await fs13.writeFile(dest, buf);
|
|
12703
|
+
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
12704
|
+
return;
|
|
12705
|
+
}
|
|
12706
|
+
}
|
|
12707
|
+
}
|
|
12708
|
+
log.dim(" couldn't fetch your avatar; using the bundled default.");
|
|
12709
|
+
} catch {
|
|
12710
|
+
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
12403
12711
|
}
|
|
12404
12712
|
}
|
|
12405
|
-
|
|
12713
|
+
await fs13.copyFile(path14.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
12714
|
+
log.dim(
|
|
12715
|
+
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
12716
|
+
);
|
|
12717
|
+
}
|
|
12718
|
+
async function exists3(p) {
|
|
12719
|
+
try {
|
|
12720
|
+
await fs13.access(p);
|
|
12721
|
+
return true;
|
|
12722
|
+
} catch {
|
|
12723
|
+
return false;
|
|
12724
|
+
}
|
|
12406
12725
|
}
|
|
12407
12726
|
|
|
12408
12727
|
// src/commands/character.ts
|
|
@@ -12487,8 +12806,10 @@ async function runCharacter(opts) {
|
|
|
12487
12806
|
log.plain(c.bold("Character quote"));
|
|
12488
12807
|
log.plain(` ${price.credits} Genex credits = base ${price.baseCredits} + actions ${price.animationCredits}`);
|
|
12489
12808
|
if (price.controllerPackKey) {
|
|
12809
|
+
const version = price.controllerPackVersion === void 0 ? "" : ` v${price.controllerPackVersion}`;
|
|
12810
|
+
const fingerprint = price.controllerPackFingerprint ? ` \xB7 fingerprint ${price.controllerPackFingerprint}` : "";
|
|
12490
12811
|
log.plain(
|
|
12491
|
-
` controller pack ${c.cyan(price.controllerPackKey)} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
|
|
12812
|
+
` controller pack ${c.cyan(`${price.controllerPackKey}${version}`)}${fingerprint} \xB7 provider actions ${(price.controllerActionIds ?? []).join(", ")}`
|
|
12492
12813
|
);
|
|
12493
12814
|
}
|
|
12494
12815
|
log.dim(` ${price.actionsGenerated} Meshy animation task${price.actionsGenerated === 1 ? "" : "s"} in this request.`);
|
|
@@ -12681,12 +13002,12 @@ function rank(items, query) {
|
|
|
12681
13002
|
}
|
|
12682
13003
|
|
|
12683
13004
|
// src/commands/ui.ts
|
|
12684
|
-
import
|
|
12685
|
-
import
|
|
13005
|
+
import fs15 from "fs/promises";
|
|
13006
|
+
import path15 from "path";
|
|
12686
13007
|
import { PNG as PNG3 } from "pngjs";
|
|
12687
13008
|
|
|
12688
13009
|
// src/lib/png-tools.ts
|
|
12689
|
-
import
|
|
13010
|
+
import fs14 from "fs/promises";
|
|
12690
13011
|
import { PNG as PNG2 } from "pngjs";
|
|
12691
13012
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
12692
13013
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -12697,12 +13018,12 @@ async function loadPng(input) {
|
|
|
12697
13018
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
12698
13019
|
buf = Buffer.from(await res.arrayBuffer());
|
|
12699
13020
|
} else {
|
|
12700
|
-
buf = await
|
|
13021
|
+
buf = await fs14.readFile(input);
|
|
12701
13022
|
}
|
|
12702
13023
|
return PNG2.sync.read(buf);
|
|
12703
13024
|
}
|
|
12704
13025
|
async function writePng(file, png) {
|
|
12705
|
-
await
|
|
13026
|
+
await fs14.writeFile(file, PNG2.sync.write(png));
|
|
12706
13027
|
}
|
|
12707
13028
|
function cropPng(image, box) {
|
|
12708
13029
|
const out = new PNG2({ width: box.w, height: box.h });
|
|
@@ -12908,7 +13229,7 @@ async function uiExtract(opts, log) {
|
|
|
12908
13229
|
const dilatePx = opts.dilate ?? 0;
|
|
12909
13230
|
const sheet = await loadPng(input);
|
|
12910
13231
|
const { width: W, height: H, data } = sheet;
|
|
12911
|
-
await
|
|
13232
|
+
await fs15.mkdir(outDir, { recursive: true });
|
|
12912
13233
|
log.plain(c.bold("genex ui extract"));
|
|
12913
13234
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
12914
13235
|
let hasTransparency = false;
|
|
@@ -13067,7 +13388,7 @@ async function uiExtract(opts, log) {
|
|
|
13067
13388
|
}
|
|
13068
13389
|
}
|
|
13069
13390
|
}
|
|
13070
|
-
const outPath =
|
|
13391
|
+
const outPath = path15.join(outDir, `${name}.png`);
|
|
13071
13392
|
await writePng(outPath, out);
|
|
13072
13393
|
const sidecar = {
|
|
13073
13394
|
name,
|
|
@@ -13085,7 +13406,7 @@ async function uiExtract(opts, log) {
|
|
|
13085
13406
|
componentPixels: comp.pixels
|
|
13086
13407
|
};
|
|
13087
13408
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
13088
|
-
await
|
|
13409
|
+
await fs15.writeFile(
|
|
13089
13410
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
13090
13411
|
JSON.stringify(sidecarBody, null, 2)
|
|
13091
13412
|
);
|
|
@@ -13094,8 +13415,8 @@ async function uiExtract(opts, log) {
|
|
|
13094
13415
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
13095
13416
|
);
|
|
13096
13417
|
}
|
|
13097
|
-
const debugPath =
|
|
13098
|
-
await
|
|
13418
|
+
const debugPath = path15.join(outDir, "extract-debug.json");
|
|
13419
|
+
await fs15.writeFile(
|
|
13099
13420
|
debugPath,
|
|
13100
13421
|
JSON.stringify(
|
|
13101
13422
|
{
|
|
@@ -13329,7 +13650,7 @@ async function uiMasks(opts, log) {
|
|
|
13329
13650
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
13330
13651
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
13331
13652
|
const image = await loadPng(input);
|
|
13332
|
-
await
|
|
13653
|
+
await fs15.mkdir(outDir, { recursive: true });
|
|
13333
13654
|
log.plain(c.bold("genex ui masks"));
|
|
13334
13655
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
13335
13656
|
const results = [];
|
|
@@ -13372,11 +13693,11 @@ async function uiMasks(opts, log) {
|
|
|
13372
13693
|
});
|
|
13373
13694
|
}
|
|
13374
13695
|
const overlay = makeOverlay(clean2, converted.png);
|
|
13375
|
-
const framePath =
|
|
13376
|
-
const maskPath =
|
|
13377
|
-
const annotatedPath =
|
|
13378
|
-
const overlayPath =
|
|
13379
|
-
const metaPath =
|
|
13696
|
+
const framePath = path15.join(outDir, `${pair.name}-frame.png`);
|
|
13697
|
+
const maskPath = path15.join(outDir, `${pair.name}-mask.png`);
|
|
13698
|
+
const annotatedPath = path15.join(outDir, `${pair.name}-annotated-source.png`);
|
|
13699
|
+
const overlayPath = path15.join(outDir, `${pair.name}-overlay.png`);
|
|
13700
|
+
const metaPath = path15.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
13380
13701
|
await writePng(framePath, clean2);
|
|
13381
13702
|
await writePng(maskPath, converted.png);
|
|
13382
13703
|
await writePng(annotatedPath, annotated);
|
|
@@ -13415,7 +13736,7 @@ async function uiMasks(opts, log) {
|
|
|
13415
13736
|
},
|
|
13416
13737
|
overlay: overlayPath
|
|
13417
13738
|
};
|
|
13418
|
-
await
|
|
13739
|
+
await fs15.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
13419
13740
|
`);
|
|
13420
13741
|
results.push(meta);
|
|
13421
13742
|
const fb = converted.bbox;
|
|
@@ -13424,8 +13745,8 @@ async function uiMasks(opts, log) {
|
|
|
13424
13745
|
`${pair.name}: coverage ${meta.mask.coverage},${fbText} ${components.length} component(s), registration \u0394 ${meta.registration.bboxDelta}`
|
|
13425
13746
|
);
|
|
13426
13747
|
}
|
|
13427
|
-
const indexPath =
|
|
13428
|
-
await
|
|
13748
|
+
const indexPath = path15.join(outDir, "annotated-progress.json");
|
|
13749
|
+
await fs15.writeFile(indexPath, `${JSON.stringify({ input, pairs: results }, null, 2)}
|
|
13429
13750
|
`);
|
|
13430
13751
|
log.plain("");
|
|
13431
13752
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -13546,7 +13867,7 @@ async function uiTextColor(opts, log) {
|
|
|
13546
13867
|
};
|
|
13547
13868
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
13548
13869
|
`);
|
|
13549
|
-
if (opts.out) await
|
|
13870
|
+
if (opts.out) await fs15.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
13550
13871
|
`);
|
|
13551
13872
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
13552
13873
|
}
|
|
@@ -13569,7 +13890,7 @@ async function uiTrim(opts, log) {
|
|
|
13569
13890
|
await writePng(outPath, trimmed);
|
|
13570
13891
|
const sidecar = computeBBoxes(trimmed);
|
|
13571
13892
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
13572
|
-
await
|
|
13893
|
+
await fs15.writeFile(sidecarPath, JSON.stringify(sidecar, null, 2));
|
|
13573
13894
|
log.success(
|
|
13574
13895
|
`Trimmed ${png.width}x${png.height} \u2192 ${trimmed.width}x${trimmed.height} (${outPath}).`
|
|
13575
13896
|
);
|