lecodes-assets 0.2.1 → 2.4.10

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.
Files changed (97) hide show
  1. package/README.md +203 -26
  2. package/dist/cli.js +17948 -11911
  3. package/dist/index.js +17525 -11479
  4. package/package.json +3 -1
  5. package/src/cli-kit/command.ts +100 -0
  6. package/src/cli-kit/help.ts +72 -0
  7. package/src/cli-kit/index.ts +4 -0
  8. package/src/cli-kit/parse.ts +72 -0
  9. package/src/cli-kit/run.ts +52 -0
  10. package/src/cli.ts +24 -1039
  11. package/src/commands/avatar.ts +77 -0
  12. package/src/commands/bvh.ts +26 -0
  13. package/src/commands/convert.ts +156 -0
  14. package/src/commands/doctor.ts +67 -0
  15. package/src/commands/geometry.ts +45 -0
  16. package/src/commands/index.ts +24 -0
  17. package/src/commands/marks.ts +38 -0
  18. package/src/commands/mirror.ts +35 -0
  19. package/src/commands/probe.ts +43 -0
  20. package/src/commands/retarget/donor.ts +63 -0
  21. package/src/commands/retarget/index.ts +169 -0
  22. package/src/commands/retarget/map.ts +81 -0
  23. package/src/commands/retarget/report.ts +77 -0
  24. package/src/commands/scene.ts +103 -0
  25. package/src/commands/shared.ts +121 -0
  26. package/src/commands/sky.ts +35 -0
  27. package/src/commands/terrainPack.ts +40 -0
  28. package/src/commands/unpack.ts +43 -0
  29. package/src/commands/unreal.ts +109 -0
  30. package/src/commands/unwrap.ts +56 -0
  31. package/src/doctor/cardTrim.ts +286 -0
  32. package/src/doctor/doctor.ts +94 -30
  33. package/src/doctor/merge/accessors.ts +30 -0
  34. package/src/doctor/merge/atlasLayout.ts +94 -0
  35. package/src/doctor/merge/atlasPixels.ts +114 -0
  36. package/src/doctor/merge/bindSpace.ts +59 -0
  37. package/src/doctor/merge/classify.ts +136 -0
  38. package/src/doctor/merge/geometry.ts +167 -0
  39. package/src/doctor/merge/index.ts +98 -0
  40. package/src/doctor/merge/matrix.ts +60 -0
  41. package/src/doctor/merge/plan.ts +88 -0
  42. package/src/doctor/merge/textures.ts +60 -0
  43. package/src/doctor/merge/types.ts +77 -0
  44. package/src/doctor/transcodeKtx2.ts +179 -28
  45. package/src/doctor/validateGlb.ts +2 -2
  46. package/src/doctor/vertexAo.ts +321 -0
  47. package/src/fbx/read.ts +2 -0
  48. package/src/fbx/toDocument.ts +227 -209
  49. package/src/geometry.ts +222 -0
  50. package/src/index.ts +2 -0
  51. package/src/retarget/heading.ts +238 -171
  52. package/src/retarget/humanoid.ts +572 -561
  53. package/src/retarget/retarget.ts +2375 -2272
  54. package/src/retarget/rig.ts +4 -1
  55. package/src/retarget/writeMarks.ts +134 -132
  56. package/src/sky/cmgen.ts +3 -3
  57. package/src/sky/index.ts +57 -8
  58. package/src/terrainPack.ts +61 -22
  59. package/src/unity/colliderFit.ts +287 -0
  60. package/src/unity/fbxModel.ts +194 -0
  61. package/src/unity/foliage.ts +63 -0
  62. package/src/unity/gameObjects.ts +81 -0
  63. package/src/unity/guid.ts +104 -0
  64. package/src/unity/image.ts +58 -0
  65. package/src/unity/kindGlb.ts +364 -0
  66. package/src/unity/lecodesLevel.ts +381 -0
  67. package/src/unity/levelTerrain.ts +116 -0
  68. package/src/unity/matFile.ts +179 -0
  69. package/src/unity/materialSlots.ts +53 -0
  70. package/src/unity/materialWriter.ts +306 -0
  71. package/src/unity/materials.ts +34 -429
  72. package/src/unity/math.ts +82 -0
  73. package/src/unity/prefabTree.ts +285 -0
  74. package/src/unity/prefabs.ts +10 -36
  75. package/src/unity/propertyPath.ts +43 -0
  76. package/src/unity/scene.ts +325 -304
  77. package/src/unity/serialized.ts +242 -0
  78. package/src/unity/terrain.ts +221 -0
  79. package/src/unity/triBudget.ts +77 -0
  80. package/src/unity/triplanar.ts +122 -0
  81. package/src/unity/unityMesh.ts +151 -0
  82. package/src/unity/unpack.ts +24 -5
  83. package/src/unity/yaml.ts +131 -0
  84. package/src/unreal/index.ts +6 -5
  85. package/src/unreal/materialGraph.ts +224 -0
  86. package/src/unreal/materials.ts +117 -9
  87. package/src/unreal/package.ts +30 -12
  88. package/src/unreal/resolve.ts +6 -4
  89. package/src/unreal/sound.ts +128 -0
  90. package/src/unreal/texture.ts +70 -3
  91. package/src/unreal/toDocument.ts +7 -0
  92. package/src/unreal/versions.ts +8 -0
  93. package/src/unwrap/index.ts +167 -0
  94. package/src/unwrap/lightmapUv.ts +761 -0
  95. package/src/wasm/fbxReader.js +1 -1
  96. package/src/wasm/lightmapUv.js +1 -1
  97. package/src/doctor/lightmapUv.ts +0 -452
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lecodes-assets",
3
- "version": "0.2.1",
3
+ "version": "2.4.10",
4
4
  "description": "LeCodes asset pipeline: FBX → GLB (ufbx), clip merging by bone name, texture embedding, GLB doctor (bone cap, texture normalize, KTX2).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,10 +9,12 @@
9
9
  "exports": {
10
10
  ".": {
11
11
  "types": "./src/index.ts",
12
+ "bun": "./src/index.ts",
12
13
  "default": "./dist/index.js"
13
14
  },
14
15
  "./cli": {
15
16
  "types": "./src/cli.ts",
17
+ "bun": "./src/cli.ts",
16
18
  "default": "./dist/cli.js"
17
19
  },
18
20
  "./fbx/*": "./src/fbx/*.ts",
@@ -0,0 +1,100 @@
1
+ /*
2
+ * A command is DATA: its name, one summary line, the flags it takes, its subcommands, and `run`.
3
+ * The parser (parse.ts) reads the argv AGAINST that declaration — which flag takes a value, which
4
+ * takes several, which may stand alone — so a flag the code reads and the parser does not know
5
+ * cannot exist, and the help (help.ts) is rendered from the same data. The shape follows
6
+ * lecodes-cli's `src/cli` (this package stays standalone, so it carries its own small copy).
7
+ *
8
+ * export default defineCommand({
9
+ * name: "mirror",
10
+ * summary: "Add mirrored copies of clips (left ↔ right)",
11
+ * usage: "<model.glb>",
12
+ * flags: {
13
+ * clips: list("only these clips", { value: "a b …" }),
14
+ * suffix: str("appended to the mirrored clip's name", { value: "_M" }),
15
+ * o: str("output (default: in place)", { value: "out.glb" }),
16
+ * },
17
+ * run: async ({ args, flags }) => { … flags.clips: string[], flags.suffix: string | undefined … return 0 },
18
+ * })
19
+ *
20
+ * Flag names are their command-line spelling without the dashes (`"clip-names"`, `"o"` for `-o`);
21
+ * read them as `flags["clip-names"]`. Groups of flags several commands share are plain objects
22
+ * spread into `flags:` (see `../commands/shared.ts`).
23
+ */
24
+
25
+ type Base = {
26
+ help: string
27
+ /** The placeholder shown in help (`<file>`, `N`). */
28
+ value?: string
29
+ /** A heading the help prints above this flag (and the ones after it, until the next heading). */
30
+ group?: string
31
+ }
32
+
33
+ export type FlagSpec =
34
+ | Base & { type: "string", /** value when the flag stands alone (`--split` next to `--split dir`) */ bare?: string }
35
+ | Base & { type: "number", bare?: number, integer?: boolean, /** must be > 0 */ positive?: boolean }
36
+ | Base & { type: "boolean" }
37
+ | Base & { type: "list" }
38
+
39
+ export type Flags = Record<string, FlagSpec>
40
+
41
+ type Opts = { value?: string, group?: string }
42
+ /** A string-valued flag (`-o out.glb`). `bare` lets it stand alone and names what it then reads as. */
43
+ export const str = (help: string, opts?: Opts & { bare?: string }): FlagSpec & { type: "string" } => ({ type: "string", help, ...opts })
44
+ /** A numeric flag, parsed and validated (`--scale 0.01`); `integer` / `positive` narrow it. */
45
+ export const num = (help: string, opts?: Opts & { bare?: number, integer?: boolean, positive?: boolean }): FlagSpec & { type: "number" } => ({ type: "number", help, ...opts })
46
+ /** A whole number (`--max-size 1024`). */
47
+ export const int = (help: string, opts?: Opts & { bare?: number, positive?: boolean }): FlagSpec & { type: "number" } => ({ type: "number", help, integer: true, ...opts })
48
+ /** A switch (`--force`). */
49
+ export const bool = (help: string, opts?: { group?: string }): FlagSpec & { type: "boolean" } => ({ type: "boolean", help, ...opts })
50
+ /** A flag that takes every value up to the next flag (`--clips a.fbx b.fbx`); repeats accumulate. */
51
+ export const list = (help: string, opts?: Opts): FlagSpec & { type: "list" } => ({ type: "list", help, ...opts })
52
+
53
+ type ValueOf<S extends FlagSpec> =
54
+ S extends { type: "boolean" } ? boolean
55
+ : S extends { type: "list" } ? string[]
56
+ : S extends { type: "number" } ? number | undefined
57
+ : string | undefined
58
+
59
+ export type FlagValues<F extends Flags> = { [K in keyof F]: ValueOf<F[K]> }
60
+
61
+ export type Context<F extends Flags = Flags> = {
62
+ /** Positional arguments after the command path. */
63
+ args: string[]
64
+ /** The declared flags, typed and validated. */
65
+ flags: FlagValues<F>
66
+ /** Which declared flags were on the line at all (a `bare` flag reads the same as its default). */
67
+ given: Set<keyof F & string>
68
+ /** The argv after the command path, untouched. */
69
+ argv: string[]
70
+ /** The command path that was run (`["unreal", "tex"]`). */
71
+ path: string[]
72
+ }
73
+
74
+ export type Command<F extends Flags = Flags> = {
75
+ name: string
76
+ /** One line for the command list. */
77
+ summary: string
78
+ /** The positional syntax after the name: `"<model.glb>"`. Omit for none. */
79
+ usage?: string
80
+ /** Paragraphs for `<name> --help`; blank lines separate paragraphs. */
81
+ description?: string
82
+ flags?: F
83
+ commands?: Command<any>[]
84
+ examples?: string[]
85
+ /** What runs; resolves to the exit code. A command with subcommands and no `run` prints its help. */
86
+ run?: (ctx: Context<F>) => Promise<number> | number
87
+ }
88
+
89
+ export const defineCommand = <F extends Flags>(cmd: Command<F>): Command<F> => cmd
90
+
91
+ /** A failure with its exit code: 1 = the work failed, 2 = the command line is wrong. The runner prints `✗ message`. */
92
+ export class CliError extends Error {
93
+ constructor(message: string, readonly code = 1, /** print the command's help page after the message */ readonly showHelp = false) { super(message) }
94
+ }
95
+ /** The command line is wrong (exit 2). */
96
+ export const usageError = (message: string): CliError => new CliError(message, 2)
97
+ /** Something the command cannot start without is missing (exit 2, followed by the command's help). */
98
+ export const missing = (what: string): CliError => new CliError(`missing ${what}`, 2, true)
99
+ /** The input path does not exist (exit 1). */
100
+ export const notFound = (path: string): CliError => new CliError(`not found: ${path}`)
@@ -0,0 +1,72 @@
1
+ import type { Command, FlagSpec } from "./command"
2
+ import { spell } from "./parse"
3
+
4
+ // Two levels, both rendered from the command declarations: the command list (bare call / --help at
5
+ // the top) and one command's page (`<command> --help`, and what a usage error prints).
6
+
7
+ const WIDTH = 100
8
+ const wrap = (text: string, indent: number): string[] => {
9
+ const room = Math.max(30, WIDTH - indent)
10
+ const out: string[] = []
11
+ for (const para of text.split("\n")) {
12
+ let line = ""
13
+ for (const word of para.split(/\s+/).filter(Boolean)) {
14
+ if (line && line.length + 1 + word.length > room) { out.push(line); line = word } else line = line ? `${line} ${word}` : word
15
+ }
16
+ out.push(line)
17
+ }
18
+ return out
19
+ }
20
+
21
+ const flagLabel = (name: string, s: FlagSpec): string => {
22
+ if (s.type === "boolean") return spell(name)
23
+ const value = s.value ?? (s.type === "number" ? "N" : s.type === "list" ? "a b …" : "<value>")
24
+ return `${spell(name)} ${"bare" in s && s.bare !== undefined ? `[${value}]` : value}`
25
+ }
26
+
27
+ const rows = (pairs: [string, string][], indent: number, column: number): string[] => {
28
+ const out: string[] = []
29
+ const pad = " ".repeat(indent)
30
+ for (const [ label, help ] of pairs) {
31
+ const lines = wrap(help, indent + column)
32
+ if (label.length < column - 1) out.push(`${pad}${label.padEnd(column)}${lines[0] ?? ""}`)
33
+ else { out.push(`${pad}${label}`); out.push(`${pad}${" ".repeat(column)}${lines[0] ?? ""}`) }
34
+ for (const l of lines.slice(1)) out.push(`${pad}${" ".repeat(column)}${l}`)
35
+ }
36
+ return out
37
+ }
38
+
39
+ export const PROGRAM = "lecodes assets"
40
+
41
+ /** The command list. */
42
+ export const renderRoot = (title: string, commands: Command<any>[]): string => [
43
+ title,
44
+ "",
45
+ `Usage: ${PROGRAM} <command> [args…] (${PROGRAM} <command> --help for its flags)`,
46
+ "",
47
+ ...rows(commands.map((c) => [ c.name, c.summary ]), 2, 16),
48
+ ].join("\n")
49
+
50
+ /** One command's page: syntax, description, subcommands, flags by group, examples. */
51
+ export const renderCommand = (cmd: Command<any>, path: string[]): string => {
52
+ const out: string[] = []
53
+ const full = `${PROGRAM} ${path.join(" ")}`
54
+ out.push(`${full}${cmd.commands?.length ? " <command>" : ""}${cmd.usage ? ` ${cmd.usage}` : ""}${cmd.flags && Object.keys(cmd.flags).length ? " [flags]" : ""}`)
55
+ out.push("", ...wrap(cmd.summary, 2).map((l) => ` ${l}`))
56
+ if (cmd.description) for (const para of cmd.description.trim().split(/\n\s*\n/)) out.push("", ...wrap(para.replace(/\s*\n\s*/g, " "), 2).map((l) => ` ${l}`))
57
+ if (cmd.commands?.length) out.push("", "Commands:", ...rows(cmd.commands.map((c) => [ `${c.name}${c.usage ? ` ${c.usage}` : ""}`, c.summary ]), 2, 30))
58
+ const flags = Object.entries((cmd.flags ?? {}) as Record<string, FlagSpec>)
59
+ if (flags.length) {
60
+ let group: string | undefined
61
+ let batch: [string, string][] = []
62
+ const flush = (): void => { if (batch.length) out.push(...rows(batch, 2, 30)); batch = [] }
63
+ out.push("", "Flags:")
64
+ for (const [ name, s ] of flags) {
65
+ if (s.group !== undefined && s.group !== group) { flush(); group = s.group; out.push("", ` ${group}`) }
66
+ batch.push([ flagLabel(name, s), s.help ])
67
+ }
68
+ flush()
69
+ }
70
+ if (cmd.examples?.length) out.push("", "Examples:", ...cmd.examples.map((e) => ` ${e}`))
71
+ return out.join("\n")
72
+ }
@@ -0,0 +1,4 @@
1
+ export { defineCommand, str, num, int, bool, list, CliError, usageError, missing, notFound } from "./command"
2
+ export type { Command, Context, Flags, FlagSpec, FlagValues } from "./command"
3
+ export { runProgram } from "./run"
4
+ export type { Program } from "./run"
@@ -0,0 +1,72 @@
1
+ import { usageError, type Flags, type FlagSpec, type FlagValues } from "./command"
2
+
3
+ // `--region -10,0,50,60` / `--scale -1`: "-" + digit is a negative number, not the next flag.
4
+ const isValue = (s: string): boolean => !s.startsWith("-") || /^-(\d|\.\d)/.test(s)
5
+ const flagName = (a: string): string => a.replace(/^--?/, "")
6
+ /** How a flag is spelled on the line: one letter takes one dash. */
7
+ export const spell = (name: string): string => (name.length === 1 ? `-${name}` : `--${name}`)
8
+
9
+ export type ParsedArgs<F extends Flags> = { args: string[], flags: FlagValues<F>, given: Set<keyof F & string>, unknown: string[] }
10
+
11
+ const readNumber = (name: string, s: FlagSpec & { type: "number" }, text: string): number => {
12
+ const n = Number(text)
13
+ if (!Number.isFinite(n)) throw usageError(`${spell(name)} needs a number, got "${text}"`)
14
+ if (s.integer && !Number.isInteger(n)) throw usageError(`${spell(name)} needs a whole number, got "${text}"`)
15
+ if (s.positive && n <= 0) throw usageError(`${spell(name)} needs a positive number, got "${text}"`)
16
+ return n
17
+ }
18
+
19
+ /** Read an argv against a flag declaration. Unknown flags are collected (the runner warns), never
20
+ * guessed at: an undeclared `--x 4` used to leave `4` behind as a positional. */
21
+ export const parseArgs = <F extends Flags>(spec: F, argv: string[]): ParsedArgs<F> => {
22
+ const args: string[] = []
23
+ const unknown: string[] = []
24
+ const given = new Set<string>()
25
+ const out: Record<string, unknown> = {}
26
+ for (const [ name, s ] of Object.entries(spec)) out[name] = s.type === "boolean" ? false : s.type === "list" ? [] : undefined
27
+ for (let i = 0; i < argv.length; i++) {
28
+ const a = argv[i]
29
+ if (isValue(a)) { args.push(a); continue }
30
+ const name = flagName(a)
31
+ const s = spec[name]
32
+ if (!s || spell(name) !== a) { unknown.push(a); continue }
33
+ given.add(name)
34
+ if (s.type === "boolean") { out[name] = true; continue }
35
+ if (s.type === "list") {
36
+ const values = out[name] as string[]
37
+ while (i + 1 < argv.length && isValue(argv[i + 1])) values.push(argv[++i])
38
+ continue
39
+ }
40
+ const next = i + 1 < argv.length && isValue(argv[i + 1]) ? argv[++i] : undefined
41
+ if (next === undefined) {
42
+ if (s.bare === undefined) throw usageError(`${a} needs a value${s.value ? ` (${s.value})` : ""}`)
43
+ out[name] = s.bare
44
+ } else out[name] = s.type === "number" ? readNumber(name, s, next) : next
45
+ }
46
+ return { args, flags: out as FlagValues<F>, given: given as Set<keyof F & string>, unknown }
47
+ }
48
+
49
+ /** The declared flag closest to a misspelled one (edit distance ≤ 2), for the warning. */
50
+ export const nearestFlag = (spec: Flags, typed: string): string | undefined => {
51
+ const want = flagName(typed)
52
+ let best: string | undefined, bd = 3
53
+ for (const name of Object.keys(spec)) {
54
+ const d = distance(want, name)
55
+ if (d < bd) { bd = d; best = name }
56
+ }
57
+ return best ? spell(best) : undefined
58
+ }
59
+
60
+ const distance = (a: string, b: string): number => {
61
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i)
62
+ for (let i = 1; i <= a.length; i++) {
63
+ let prev = row[0]
64
+ row[0] = i
65
+ for (let j = 1; j <= b.length; j++) {
66
+ const cur = row[j]
67
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1))
68
+ prev = cur
69
+ }
70
+ }
71
+ return row[b.length]
72
+ }
@@ -0,0 +1,52 @@
1
+ import { CliError, usageError, type Command, type Flags } from "./command"
2
+ import { PROGRAM, renderCommand, renderRoot } from "./help"
3
+ import { nearestFlag, parseArgs } from "./parse"
4
+
5
+ // The tree walk: `unreal tex x.uasset -o y.png` → the `tex` command under `unreal`, its flags read
6
+ // against its own declaration. stdout = the result, stderr = diagnostics (errors, warnings).
7
+
8
+ export type Program = { title: string, commands: Command<any>[] }
9
+
10
+ const isHelp = (a: string | undefined): boolean => a === "--help" || a === "-h" || a === "help"
11
+
12
+ export const runProgram = async (program: Program, argv: string[]): Promise<number> => {
13
+ const [ first ] = argv
14
+ if (first === undefined || isHelp(first)) { console.log(renderRoot(program.title, program.commands)); return 0 }
15
+ const top = program.commands.find((c) => c.name === first)
16
+ if (!top) {
17
+ console.error(`✗ unknown command "${first}"\n`)
18
+ console.log(renderRoot(program.title, program.commands))
19
+ return 2
20
+ }
21
+ let cmd: Command<any> = top
22
+ const path = [ first ]
23
+ let rest = argv.slice(1)
24
+ // descend while the next word names a subcommand
25
+ for (;;) {
26
+ const sub: Command<any> | undefined = cmd.commands?.find((c: Command<any>) => c.name === rest[0])
27
+ if (!sub) break
28
+ cmd = sub
29
+ path.push(rest[0])
30
+ rest = rest.slice(1)
31
+ }
32
+ if (rest.some((a) => a === "--help" || a === "-h")) { console.log(renderCommand(cmd, path)); return 0 }
33
+ if (!cmd.run) { console.log(renderCommand(cmd, path)); return rest.length ? 2 : 0 }
34
+ try {
35
+ const spec = (cmd.flags ?? {}) as Flags
36
+ const parsed = parseArgs(spec, rest)
37
+ // an unknown flag is an error, not a warning: whether it takes a value cannot be known, so the word
38
+ // after it would be read as a positional (a second input file)
39
+ if (parsed.unknown.length) {
40
+ const notes = parsed.unknown.map((u) => { const near = nearestFlag(spec, u); return near ? `${u} (did you mean ${near}?)` : u })
41
+ throw usageError(`unknown flag${notes.length > 1 ? "s" : ""}: ${notes.join(", ")} — ${PROGRAM} ${path.join(" ")} --help lists them`)
42
+ }
43
+ return await cmd.run({ args: parsed.args, flags: parsed.flags, given: parsed.given, argv: rest, path })
44
+ } catch (e) {
45
+ const err = e as Error
46
+ console.error(`✗ ${err?.message ?? e}`)
47
+ if (!(e instanceof CliError)) { if (process.env.LECODES_DEBUG && err?.stack) console.error(err.stack); return 1 }
48
+ // a wrong command line gets the command's own page, not the whole manual
49
+ if (e.showHelp) console.log(`\n${renderCommand(cmd, path)}`)
50
+ return e.code
51
+ }
52
+ }