Skip to content

Commit 36afc48

Browse files
fehnomenaltoddeTV
andauthored
feat: add cli for generating model code & add .glb support (#24)
Co-authored-by: Thorsten Seyschab <business@todde.tv>
1 parent ef78680 commit 36afc48

File tree

11 files changed

+198
-44
lines changed

11 files changed

+198
-44
lines changed

README.md

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ With this plugin you get:
2727
- ✅ Only the used models are bundled in the final product, not all included in your dev project.
2828
- ⚠️ Detects and handles [Draco Compression](https://github.com/google/draco) during type generation automatically,
2929
see [Draco Compression handling](#draco-compression-handling) below for more information.
30-
- ✅ Works with glTF Seperate (`.gltf` + `.bin` + textures) and glTF Embedded (only `.gltf`) files,
31-
see [glTF Versions and Representations](#gltf-versions-and-representations) below for more information.
30+
- ✅ Works with glTF Seperate (`.gltf` + `.bin` + textures), glTF Embedded (only `.gltf`) and glTF Binary (`.glb`)
31+
files, see [glTF Versions and Representations](#gltf-versions-and-representations) below for more information.
3232
- ✅ ESM ready.
3333
- ⚠️ Build tool & bundler agnostic thanks to [Unplugin](https://github.com/unjs/unplugin), so use it with your
3434
favorite one, but see chapter
@@ -119,14 +119,16 @@ to different folders, changing paths, or adjusting how the model is handled afte
119119
3. Start your dev to generate all files. With our example model we get:
120120

121121
```diff
122-
@/assets/models/MyModel.gltf
123-
@/assets/models/MyModel.bin
124-
@/assets/models/MyModel-texture1.png
125-
@/assets/models/MyModel-texture2.png
122+
@/assets/models/MyModel.gltf
123+
@/assets/models/MyModel.bin
124+
@/assets/models/MyModel-texture1.png
125+
@/assets/models/MyModel-texture2.png
126126
+@/assets/models/MyModel.gltf.d.ts <- the typing
127-
+@/assets/models/MyModel.gltf.js <- actual code with node get helper function and scene/ model graph representation
127+
+@/assets/models/MyModel.gltf.js <- actual code with node get helper function and model graph representation
128128
```
129129

130+
> Alternatively, you can run the script `gltf-codegen` supplied by the package to manually create those files. More details at [Binary scripts](#binary-scripts).
131+
130132
4. Import the type safe model in your code and use it, e.g.:
131133

132134
```ts
@@ -165,14 +167,14 @@ resulting in the following compatibility in our project:
165167

166168
## glTF Versions and Representations
167169

168-
(Legend: 🟢 Tested & Supported | 🟡 Not Yet Tested | 🔴 Not Supported)
170+
(Legend: 🟢 Tested & Supported | 🟡 Partially Supported | 🔴 Not Supported)
169171

170-
| glTF Version | File Representation | Status | Note |
171-
| ------------ | -------------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
172-
| glTF 1.0 | Any | 🔴 | glTF 2.0 was introduced in 2017 with major improvements. Avoid using the outdated glTF 1.0 standard in your projects. |
173-
| glTF 2.0 | Separate (`.gltf` + `.bin` + textures) | 🟢 | Recommended! Offers better performance, version control, caching, transferability, and debugging. |
174-
| glTF 2.0 | Embedded (only `.gltf`) | 🟢 | Assets are embedded directly into the `.gltf` file as base64 encoded `data:` sources within the `uri` fields, making single-file management simpler. |
175-
| glTF 2.0 | Binary (`.glb`) | 🔴 | Currently not supported because we scan JSON-encoded `.gltf` files for type generation and cannot yet process binary representations. Contributions welcome! ❤️ |
172+
| glTF Version | File Representation | Status | Note |
173+
| ------------ | -------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
174+
| glTF 1.0 | Any | 🔴 | glTF 2.0 was introduced in 2017 with major improvements. Avoid using the outdated glTF 1.0 standard in your projects. |
175+
| glTF 2.0 | Separate (`.gltf` + `.bin` + textures) | 🟢 | Recommended! Offers better performance, version control, caching, transferability, and debugging. |
176+
| glTF 2.0 | Embedded (only `.gltf`) | 🟢 | Assets are embedded directly into the `.gltf` file as base64 encoded `data:` sources within the `uri` fields, making single-file management simpler. |
177+
| glTF 2.0 | Binary (`.glb`) | 🟡 | Currently, only works with models that contain all referenced files in the binary chunk without external file references. Contributions welcome! ❤️ |
176178

177179
## Draco Compression handling
178180

@@ -210,6 +212,20 @@ const gltfLoader = new GLTFLoader().setDRACOLoader(dracoLoader)
210212
export default gltfLoader
211213
```
212214

215+
## Binary scripts
216+
217+
In addition to the commands in the `scripts` section of the `package.json`, this plugin also provides binary scripts.
218+
219+
Run them by adding this plugin to your project, be sure to have the dependencies installed and then
220+
add `npx` or `pnpx` before the commands.
221+
222+
### `gltf-codegen [DIR]`
223+
224+
This script generates types and runtime code for all models found in `DIR` and sub-directories. `DIR` defaults to
225+
the current directory.
226+
227+
Run `gltf-codegen --help` for more options and details.
228+
213229
## idea behind the scenes
214230

215231
On runtime it runs `useGLTF` under the hood. So no extra layer and therefore correct objects that

package.json

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@
9696
]
9797
}
9898
},
99+
"bin": {
100+
"gltf-codegen": "dist/cli.js"
101+
},
99102
"files": [
100103
"CHANGELOG.md",
101104
"LICENSE.md",
@@ -108,14 +111,15 @@
108111
"pnpm": "9"
109112
},
110113
"scripts": {
111-
"prepare": "run-s \"precompile-templates\"",
112-
"precompile-templates": "esno scripts/precompile-templates.ts",
114+
"bin:gltf-codegen": "esno src/cli.ts",
113115
"build": "tsup",
114-
"dev": "tsup --watch src",
115116
"build:post": "esno scripts/postbuild.ts",
117+
"dev": "tsup --watch src",
116118
"lint": "eslint .",
117119
"lint:fix": "run-s \"lint --fix\"",
118-
"playground": "pnpm -C playground run dev"
120+
"playground": "pnpm -C playground run dev",
121+
"precompile-templates": "esno scripts/precompile-templates.ts",
122+
"prepare": "run-s \"precompile-templates\""
119123
},
120124
"peerDependencies": {
121125
"@farmfe/core": ">=1",
@@ -160,6 +164,8 @@
160164
}
161165
},
162166
"dependencies": {
167+
"citty": "~0.1.6",
168+
"consola": "~3.4.0",
163169
"handlebars": "~4.7.8"
164170
},
165171
"devDependencies": {

pnpm-lock.yaml

Lines changed: 8 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cli.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env node
2+
3+
import { resolve } from 'node:path'
4+
import { cwd } from 'node:process'
5+
import { defineCommand, runMain } from 'citty'
6+
import { consola } from 'consola'
7+
import { version } from '../package.json'
8+
import { generateModelTypes } from './core/generate.js'
9+
import { findAllModelsInDir } from './core/utils/find-models.js'
10+
11+
const main = defineCommand({
12+
args: {
13+
dir: {
14+
default: cwd(),
15+
description: 'Directory to scan for *.gltf files',
16+
type: 'positional',
17+
},
18+
19+
loader: {
20+
description: 'Path to a module providing an GLTFLoader instance as default export.',
21+
type: 'string',
22+
},
23+
24+
verbose: {
25+
default: false,
26+
description: 'Print more info',
27+
type: 'boolean',
28+
},
29+
},
30+
31+
meta: {
32+
description: '',
33+
name: 'gltf-codegen',
34+
version,
35+
},
36+
37+
async run(context) {
38+
const dir = resolve(context.args.dir)
39+
40+
consola.start(`Looking for models in ${dir}`)
41+
42+
const models = await findAllModelsInDir(dir)
43+
44+
await Promise.all(
45+
models.map(modelFile => generateModelTypes(modelFile, {
46+
customGltfLoaderModule: context.args.loader,
47+
verbose: context.args.verbose,
48+
})),
49+
)
50+
51+
consola.success(`Generated types for ${models.length} model file${models.length === 1 ? '' : 's'}.`)
52+
},
53+
})
54+
55+
runMain(main)

src/core/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const BINARY_GLTF_MODEL_EXTENSION = '.glb'
2+
export const SEPARATE_GLTF_MODEL_EXTENSION = '.gltf'
3+
4+
export const GLTF_MODEL_EXTENSIONS = [BINARY_GLTF_MODEL_EXTENSION, SEPARATE_GLTF_MODEL_EXTENSION]

src/core/generate.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Options } from '../types.js'
2+
import { writeFile } from 'node:fs/promises'
3+
import { basename, dirname, relative, resolve } from 'node:path'
4+
import consola from 'consola'
5+
import { DRACOLoader, GLTFLoader } from 'three-stdlib'
6+
import { name } from '../../package.json'
7+
import { analyzeGltfModel, type GltfAnalysis } from '../core/analyze.js'
8+
import { parseGltfModel } from '../core/parse.js'
9+
import { generateModelDeclaration } from './generate/model-declaration.js'
10+
import { generateModelRuntime } from './generate/model-runtime.js'
11+
12+
const gltfLoader = new GLTFLoader().setDRACOLoader(new DRACOLoader())
13+
14+
// TODO: Make variable.
15+
const identifiers = {
16+
nodeGetter: 'getNode',
17+
}
18+
19+
export async function generateModelTypes(
20+
modelFile: string,
21+
options: Options | undefined,
22+
): Promise<void> {
23+
if (options?.verbose) {
24+
consola.info(`Parsing model ${modelFile}`)
25+
}
26+
27+
const parsedGltf = await parseGltfModel(gltfLoader, modelFile)
28+
const analysis = analyzeGltfModel(parsedGltf)
29+
30+
const declarationFile = resolve(dirname(modelFile), `${basename(modelFile)}.d.ts`)
31+
const runtimeFile = resolve(dirname(modelFile), `${basename(modelFile)}.js`)
32+
33+
await Promise.all([
34+
generateDeclarationFile(options, declarationFile, analysis),
35+
generateRuntimeFile(options, runtimeFile, analysis, modelFile),
36+
])
37+
}
38+
39+
async function generateDeclarationFile(options: Options | undefined, targetFile: string, analysis: GltfAnalysis): Promise<void> {
40+
if (options?.verbose) {
41+
consola.info(`Generating declaration file: ${targetFile}`)
42+
}
43+
44+
await writeFile(targetFile, `${generateModelDeclaration(analysis, identifiers)}\n`, {
45+
encoding: 'utf8',
46+
})
47+
}
48+
49+
async function generateRuntimeFile(options: Options | undefined, targetFile: string, analysis: GltfAnalysis, modelFile: string): Promise<void> {
50+
if (options?.verbose) {
51+
consola.info(`Generating runtime file: ${targetFile}`)
52+
}
53+
54+
let relativeGltfPath = relative(dirname(targetFile), modelFile)
55+
if (!relativeGltfPath.startsWith('.')) {
56+
relativeGltfPath = `./${relativeGltfPath}`
57+
}
58+
59+
const loaderPath = options?.customGltfLoaderModule ?? `${name}/gltf-loader`
60+
61+
await writeFile(
62+
targetFile,
63+
`${generateModelRuntime(analysis, relativeGltfPath, loaderPath, identifiers)}\n`,
64+
{ encoding: 'utf8' },
65+
)
66+
}

src/core/generate/model-runtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ export function generateModelRuntime(
1818

1919
return runtime({
2020
gltfLoaderPath: JSON.stringify(gltfLoaderPath),
21-
gltfPath: JSON.stringify(relativeGltfPath),
2221
identifiers: { ...identifiers, nodeName },
2322
imports: imports.toTemplateData(false),
23+
relativeGltfPath: JSON.stringify(relativeGltfPath),
2424
scenes,
2525
}).trim()
2626
}

src/core/generate/templates/runtime.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{{> imports.hbs}}
2-
import gltf from {{{gltfPath}}};
2+
import gltf from {{{relativeGltfPath}}};
33
import gltfLoader from {{{gltfLoaderPath}}};
44

55
{{!-- This symbol is needed to restrict node accessors to this model. --}}

src/core/parse.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { GLTF, GLTFLoader } from 'three-stdlib'
33
import { readFile, stat } from 'node:fs/promises'
44
import { dirname } from 'node:path'
55

6-
export async function parseGltfModel(gltf: GLTFLoader, modelFile: string): Promise<GLTF> {
6+
export async function parseGltfModel(gltfLoader: GLTFLoader, modelFile: string): Promise<GLTF> {
77
let stats: Stats
88
try {
99
stats = await stat(modelFile)
@@ -16,9 +16,9 @@ export async function parseGltfModel(gltf: GLTFLoader, modelFile: string): Promi
1616
throw new Error(`"${modelFile}" is not a file!`)
1717
}
1818

19-
const json = (await readFile(modelFile, { encoding: 'utf8' }))
19+
const gltf = (await readFile(modelFile)).buffer as ArrayBuffer
2020

21-
const model = await gltf.parseAsync(json, dirname(modelFile))
21+
const model = await gltfLoader.parseAsync(gltf, dirname(modelFile))
2222

2323
return model
2424
}

src/core/utils/find-models.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { readdir } from 'node:fs/promises'
2+
import { join } from 'node:path'
3+
import { GLTF_MODEL_EXTENSIONS } from '../constants.js'
4+
5+
export async function findAllModelsInDir(dir: string): Promise<string[]> {
6+
const entries = await readdir(dir, { recursive: true, withFileTypes: true })
7+
8+
return entries
9+
.filter(e => GLTF_MODEL_EXTENSIONS.some(ext => e.name.endsWith(ext)))
10+
.map(entry => join(entry.parentPath, entry.name))
11+
}

0 commit comments

Comments
 (0)