gltf-mcp 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +71 -0
- package/build/index.js.map +1 -0
- package/build/renderer/renderer.html +235 -0
- package/build/tools/get-gltf-info.d.ts +14 -0
- package/build/tools/get-gltf-info.d.ts.map +1 -0
- package/build/tools/get-gltf-info.js +205 -0
- package/build/tools/get-gltf-info.js.map +1 -0
- package/build/tools/render-gltf.d.ts +53 -0
- package/build/tools/render-gltf.d.ts.map +1 -0
- package/build/tools/render-gltf.js +364 -0
- package/build/tools/render-gltf.js.map +1 -0
- package/build/utils/gltf-io.d.ts +6 -0
- package/build/utils/gltf-io.d.ts.map +1 -0
- package/build/utils/gltf-io.js +13 -0
- package/build/utils/gltf-io.js.map +1 -0
- package/build/utils/path-security.d.ts +6 -0
- package/build/utils/path-security.d.ts.map +1 -0
- package/build/utils/path-security.js +25 -0
- package/build/utils/path-security.js.map +1 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 EliXR Games
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# gltf-mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io/) server that lets AI assistants inspect and render glTF/GLB 3D model files.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Inspect models** - Get triangle counts, bounding boxes, materials, animations, scene hierarchy, and potential issues
|
|
8
|
+
- **Render to PNG** - PBR rendering with studio lighting, wireframe, and normal visualization modes
|
|
9
|
+
- **Texture overrides** - Swap material textures at render time without modifying the model
|
|
10
|
+
- **Full format support** - glTF 2.0, GLB, external textures, Draco, Meshopt, KTX2/Basis
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
### Claude Desktop
|
|
15
|
+
|
|
16
|
+
Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"gltf-mcp": {
|
|
22
|
+
"command": "npx",
|
|
23
|
+
"args": ["gltf-mcp"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Claude Code
|
|
30
|
+
|
|
31
|
+
Create `.mcp.json` in your project root:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"mcpServers": {
|
|
36
|
+
"gltf-mcp": {
|
|
37
|
+
"command": "npx",
|
|
38
|
+
"args": ["gltf-mcp"]
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Tools
|
|
45
|
+
|
|
46
|
+
### `get_gltf_info`
|
|
47
|
+
|
|
48
|
+
Inspect a glTF/GLB file and return structured metadata.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
file_path: "/path/to/model.glb"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Returns: file size, triangle/vertex counts, bounding box, materials, animations, skins, scene hierarchy, compression state, and potential issues.
|
|
55
|
+
|
|
56
|
+
### `render_gltf`
|
|
57
|
+
|
|
58
|
+
Render a glTF/GLB file to a PNG image.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
file_path: "/path/to/model.glb"
|
|
62
|
+
mode: "default" | "wireframe" | "normals"
|
|
63
|
+
yaw: 0 # camera rotation in degrees
|
|
64
|
+
pitch: 0
|
|
65
|
+
roll: 0
|
|
66
|
+
resolution: 800 # output size in pixels
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Returns: PNG image with metadata (normalization info, warnings).
|
|
70
|
+
|
|
71
|
+
## Requirements
|
|
72
|
+
|
|
73
|
+
- Node.js 18+
|
|
74
|
+
- Puppeteer downloads Chromium (~200MB) on first install
|
|
75
|
+
|
|
76
|
+
## Documentation
|
|
77
|
+
|
|
78
|
+
Full documentation: [elixr-games.github.io/gltf-mcp](https://elixr-games.github.io/gltf-mcp/)
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
[MIT](LICENSE)
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getGltfInfo } from "./tools/get-gltf-info.js";
|
|
6
|
+
import { renderGltf } from "./tools/render-gltf.js";
|
|
7
|
+
const server = new McpServer({
|
|
8
|
+
name: "gltf-mcp",
|
|
9
|
+
version: "0.2.0",
|
|
10
|
+
});
|
|
11
|
+
server.tool("get_gltf_info", "Inspect a glTF/GLB file and return structured metadata including a summary (triangle/vertex counts, bounding box, draw call estimate, texture memory, compression state, potential issues) plus detailed sections for materials, animations, skins, and scene hierarchy with transforms.", {
|
|
12
|
+
file_path: z.string().describe("Path to a .gltf or .glb file"),
|
|
13
|
+
}, async ({ file_path }) => {
|
|
14
|
+
return getGltfInfo(file_path);
|
|
15
|
+
});
|
|
16
|
+
server.tool("render_gltf", "Render a glTF/GLB file to a PNG image. Supports PBR rendering with neutral studio lighting (RoomEnvironment IBL), wireframe mode, and normal visualization. Full glTF extension support including KTX2 textures, Draco mesh compression, and Meshopt. The model is normalized to unit size. Returns JSON metadata and a base64-encoded PNG.", {
|
|
17
|
+
file_path: z.string().describe("Path to a .gltf or .glb file"),
|
|
18
|
+
yaw: z
|
|
19
|
+
.number()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(0)
|
|
22
|
+
.describe("Camera yaw rotation in degrees (around Y axis)"),
|
|
23
|
+
pitch: z
|
|
24
|
+
.number()
|
|
25
|
+
.optional()
|
|
26
|
+
.default(0)
|
|
27
|
+
.describe("Camera pitch rotation in degrees (around X axis)"),
|
|
28
|
+
roll: z
|
|
29
|
+
.number()
|
|
30
|
+
.optional()
|
|
31
|
+
.default(0)
|
|
32
|
+
.describe("Camera roll rotation in degrees (around Z axis)"),
|
|
33
|
+
resolution: z
|
|
34
|
+
.number()
|
|
35
|
+
.optional()
|
|
36
|
+
.default(800)
|
|
37
|
+
.describe("Output image resolution in pixels (square)"),
|
|
38
|
+
mode: z
|
|
39
|
+
.enum(["default", "wireframe", "normals"])
|
|
40
|
+
.optional()
|
|
41
|
+
.default("default")
|
|
42
|
+
.describe("Render mode: 'default' (PBR with neutral studio lighting), 'wireframe' (mesh topology), 'normals' (surface orientation as RGB)"),
|
|
43
|
+
texture_overrides: z
|
|
44
|
+
.object({
|
|
45
|
+
baseColor: z.string().optional().describe("Path to base color / albedo texture image"),
|
|
46
|
+
normal: z.string().optional().describe("Path to normal map image"),
|
|
47
|
+
metallicRoughness: z.string().optional().describe("Path to metallic-roughness map image"),
|
|
48
|
+
occlusion: z.string().optional().describe("Path to ambient occlusion map image"),
|
|
49
|
+
emissive: z.string().optional().describe("Path to emissive map image"),
|
|
50
|
+
})
|
|
51
|
+
.optional()
|
|
52
|
+
.describe("Override material textures by slot. Applied to all materials in the model."),
|
|
53
|
+
}, async ({ file_path, yaw, pitch, roll, resolution, mode, texture_overrides }) => {
|
|
54
|
+
return renderGltf(file_path, {
|
|
55
|
+
yaw,
|
|
56
|
+
pitch,
|
|
57
|
+
roll,
|
|
58
|
+
resolution,
|
|
59
|
+
mode,
|
|
60
|
+
textureOverrides: texture_overrides,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
async function main() {
|
|
64
|
+
const transport = new StdioServerTransport();
|
|
65
|
+
await server.connect(transport);
|
|
66
|
+
}
|
|
67
|
+
main().catch((error) => {
|
|
68
|
+
console.error("Fatal error:", error);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<style>html, body { margin: 0; padding: 0; overflow: hidden; background: transparent; }</style>
|
|
6
|
+
<script type="importmap">
|
|
7
|
+
{
|
|
8
|
+
"imports": {
|
|
9
|
+
"three": "/node_modules/three/build/three.module.js",
|
|
10
|
+
"three/addons/": "/node_modules/three/examples/jsm/"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<script type="module">
|
|
17
|
+
import * as THREE from 'three';
|
|
18
|
+
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
19
|
+
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
20
|
+
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
|
21
|
+
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
22
|
+
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
|
23
|
+
|
|
24
|
+
// Initialize renderer
|
|
25
|
+
const renderer = new THREE.WebGLRenderer({
|
|
26
|
+
alpha: true,
|
|
27
|
+
antialias: true,
|
|
28
|
+
preserveDrawingBuffer: true,
|
|
29
|
+
});
|
|
30
|
+
renderer.setPixelRatio(1);
|
|
31
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
32
|
+
renderer.toneMappingExposure = 1.0;
|
|
33
|
+
document.body.appendChild(renderer.domElement);
|
|
34
|
+
|
|
35
|
+
// Create neutral environment map (PBR IBL)
|
|
36
|
+
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
|
37
|
+
const neutralEnvMap = pmremGenerator.fromScene(new RoomEnvironment()).texture;
|
|
38
|
+
pmremGenerator.dispose();
|
|
39
|
+
|
|
40
|
+
// Configure loaders
|
|
41
|
+
const dracoLoader = new DRACOLoader();
|
|
42
|
+
dracoLoader.setDecoderPath('/node_modules/three/examples/jsm/libs/draco/gltf/');
|
|
43
|
+
|
|
44
|
+
const ktx2Loader = new KTX2Loader();
|
|
45
|
+
ktx2Loader.setTranscoderPath('/node_modules/three/examples/jsm/libs/basis/');
|
|
46
|
+
ktx2Loader.detectSupport(renderer);
|
|
47
|
+
|
|
48
|
+
const loader = new GLTFLoader();
|
|
49
|
+
loader.setDRACOLoader(dracoLoader);
|
|
50
|
+
loader.setKTX2Loader(ktx2Loader);
|
|
51
|
+
loader.setMeshoptDecoder(MeshoptDecoder);
|
|
52
|
+
|
|
53
|
+
window.renderModel = async (options) => {
|
|
54
|
+
const { glbUrl, width, height, yaw, pitch, roll, mode, textureOverrideUrls } = options;
|
|
55
|
+
|
|
56
|
+
// Resize
|
|
57
|
+
renderer.setSize(width, height);
|
|
58
|
+
|
|
59
|
+
// Create scene
|
|
60
|
+
const scene = new THREE.Scene();
|
|
61
|
+
scene.background = null;
|
|
62
|
+
scene.environment = neutralEnvMap;
|
|
63
|
+
|
|
64
|
+
// Track warnings from texture load failures
|
|
65
|
+
const warnings = [];
|
|
66
|
+
const origWarn = console.warn;
|
|
67
|
+
const origError = console.error;
|
|
68
|
+
const warnHandler = (...args) => {
|
|
69
|
+
const msg = args.map(a => String(a)).join(' ');
|
|
70
|
+
if (msg.includes('texture') || msg.includes('Texture') || msg.includes('image') || msg.includes('404') || msg.includes('Failed to load')) {
|
|
71
|
+
warnings.push(msg);
|
|
72
|
+
}
|
|
73
|
+
origWarn.apply(console, args);
|
|
74
|
+
};
|
|
75
|
+
const errorHandler = (...args) => {
|
|
76
|
+
const msg = args.map(a => String(a)).join(' ');
|
|
77
|
+
if (msg.includes('texture') || msg.includes('Texture') || msg.includes('image') || msg.includes('404') || msg.includes('Failed to load')) {
|
|
78
|
+
warnings.push(msg);
|
|
79
|
+
}
|
|
80
|
+
origError.apply(console, args);
|
|
81
|
+
};
|
|
82
|
+
console.warn = warnHandler;
|
|
83
|
+
console.error = errorHandler;
|
|
84
|
+
|
|
85
|
+
// Load GLB/glTF
|
|
86
|
+
let gltf;
|
|
87
|
+
try {
|
|
88
|
+
gltf = await loader.loadAsync(glbUrl);
|
|
89
|
+
} finally {
|
|
90
|
+
console.warn = origWarn;
|
|
91
|
+
console.error = origError;
|
|
92
|
+
}
|
|
93
|
+
const model = gltf.scene;
|
|
94
|
+
|
|
95
|
+
// Apply texture overrides (before render mode, since normals mode replaces materials)
|
|
96
|
+
if (textureOverrideUrls && Object.keys(textureOverrideUrls).length > 0) {
|
|
97
|
+
const texLoader = new THREE.TextureLoader();
|
|
98
|
+
const slotMap = {
|
|
99
|
+
baseColor: 'map',
|
|
100
|
+
normal: 'normalMap',
|
|
101
|
+
metallicRoughness: ['metalnessMap', 'roughnessMap'],
|
|
102
|
+
occlusion: 'aoMap',
|
|
103
|
+
emissive: 'emissiveMap',
|
|
104
|
+
};
|
|
105
|
+
const srgbSlots = new Set(['baseColor', 'emissive']);
|
|
106
|
+
|
|
107
|
+
for (const [slot, url] of Object.entries(textureOverrideUrls)) {
|
|
108
|
+
try {
|
|
109
|
+
const tex = await texLoader.loadAsync(url);
|
|
110
|
+
tex.flipY = false;
|
|
111
|
+
tex.colorSpace = srgbSlots.has(slot) ? THREE.SRGBColorSpace : THREE.LinearSRGBColorSpace;
|
|
112
|
+
|
|
113
|
+
model.traverse((node) => {
|
|
114
|
+
if (!node.isMesh || !node.material) return;
|
|
115
|
+
const mats = Array.isArray(node.material) ? node.material : [node.material];
|
|
116
|
+
mats.forEach((mat) => {
|
|
117
|
+
const matSlots = slotMap[slot];
|
|
118
|
+
if (Array.isArray(matSlots)) {
|
|
119
|
+
matSlots.forEach(s => { mat[s] = tex; });
|
|
120
|
+
} else if (matSlots) {
|
|
121
|
+
mat[matSlots] = tex;
|
|
122
|
+
}
|
|
123
|
+
if (slot === 'emissive') {
|
|
124
|
+
mat.emissiveIntensity = 1.0;
|
|
125
|
+
mat.emissive = new THREE.Color(1, 1, 1);
|
|
126
|
+
}
|
|
127
|
+
mat.needsUpdate = true;
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
} catch (err) {
|
|
131
|
+
warnings.push(`Failed to load texture override for '${slot}': ${err.message || err}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Apply render mode
|
|
137
|
+
if (mode === 'wireframe') {
|
|
138
|
+
model.traverse((node) => {
|
|
139
|
+
if (node.isMesh && node.material) {
|
|
140
|
+
const mats = Array.isArray(node.material) ? node.material : [node.material];
|
|
141
|
+
mats.forEach((mat) => { mat.wireframe = true; });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
} else if (mode === 'normals') {
|
|
145
|
+
model.traverse((node) => {
|
|
146
|
+
if (node.isMesh && node.material) {
|
|
147
|
+
const origMat = Array.isArray(node.material) ? node.material[0] : node.material;
|
|
148
|
+
node.material = new THREE.MeshNormalMaterial({ side: origMat.side });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
// 'default' mode: keep PBR materials as-is
|
|
153
|
+
|
|
154
|
+
// Normalize model to unit cube centered at origin
|
|
155
|
+
model.updateMatrixWorld(true);
|
|
156
|
+
const box = new THREE.Box3().setFromObject(model);
|
|
157
|
+
const size = box.getSize(new THREE.Vector3());
|
|
158
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
159
|
+
const maxDim = Math.max(size.x, size.y, size.z);
|
|
160
|
+
const scaleFactor = maxDim > 0 ? 1.0 / maxDim : 1.0;
|
|
161
|
+
|
|
162
|
+
const normalization = {
|
|
163
|
+
scaleFactor,
|
|
164
|
+
originalDimensions: {
|
|
165
|
+
x: parseFloat(size.x.toFixed(6)),
|
|
166
|
+
y: parseFloat(size.y.toFixed(6)),
|
|
167
|
+
z: parseFloat(size.z.toFixed(6)),
|
|
168
|
+
},
|
|
169
|
+
originalCenter: {
|
|
170
|
+
x: parseFloat(center.x.toFixed(6)),
|
|
171
|
+
y: parseFloat(center.y.toFixed(6)),
|
|
172
|
+
z: parseFloat(center.z.toFixed(6)),
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
model.scale.multiplyScalar(scaleFactor);
|
|
177
|
+
model.position.copy(center).multiplyScalar(-scaleFactor);
|
|
178
|
+
model.updateMatrixWorld(true);
|
|
179
|
+
|
|
180
|
+
scene.add(model);
|
|
181
|
+
|
|
182
|
+
// Camera setup
|
|
183
|
+
const camera = new THREE.PerspectiveCamera(60, 1, 0.01, 100);
|
|
184
|
+
const cameraDist = 1.5;
|
|
185
|
+
|
|
186
|
+
const yawRad = (yaw * Math.PI) / 180;
|
|
187
|
+
const pitchRad = (pitch * Math.PI) / 180;
|
|
188
|
+
const baseTheta = Math.PI / 4;
|
|
189
|
+
const basePhi = Math.PI / 2 - 0.3;
|
|
190
|
+
|
|
191
|
+
const theta = baseTheta + yawRad;
|
|
192
|
+
const phi = Math.max(0.01, Math.min(Math.PI - 0.01, basePhi - pitchRad));
|
|
193
|
+
|
|
194
|
+
camera.position.set(
|
|
195
|
+
cameraDist * Math.sin(phi) * Math.sin(theta),
|
|
196
|
+
cameraDist * Math.cos(phi),
|
|
197
|
+
cameraDist * Math.sin(phi) * Math.cos(theta)
|
|
198
|
+
);
|
|
199
|
+
camera.lookAt(0, 0, 0);
|
|
200
|
+
|
|
201
|
+
if (roll !== 0) {
|
|
202
|
+
const rollRad = (roll * Math.PI) / 180;
|
|
203
|
+
camera.rotateZ(rollRad);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Render
|
|
207
|
+
renderer.render(scene, camera);
|
|
208
|
+
|
|
209
|
+
// Capture as data URL
|
|
210
|
+
const dataUrl = renderer.domElement.toDataURL('image/png');
|
|
211
|
+
|
|
212
|
+
// Cleanup - dispose scene objects
|
|
213
|
+
scene.remove(model);
|
|
214
|
+
model.traverse((node) => {
|
|
215
|
+
if (node.geometry) node.geometry.dispose();
|
|
216
|
+
if (node.material) {
|
|
217
|
+
const materials = Array.isArray(node.material) ? node.material : [node.material];
|
|
218
|
+
materials.forEach((mat) => {
|
|
219
|
+
for (const key in mat) {
|
|
220
|
+
if (mat[key]?.isTexture && mat[key] !== neutralEnvMap) {
|
|
221
|
+
mat[key].dispose();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
mat.dispose();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return { normalization, dataUrl, warnings };
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
window.rendererReady = true;
|
|
233
|
+
</script>
|
|
234
|
+
</body>
|
|
235
|
+
</html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function getGltfInfo(filePath: string): Promise<{
|
|
2
|
+
content: {
|
|
3
|
+
type: "text";
|
|
4
|
+
text: string;
|
|
5
|
+
}[];
|
|
6
|
+
isError?: undefined;
|
|
7
|
+
} | {
|
|
8
|
+
content: {
|
|
9
|
+
type: "text";
|
|
10
|
+
text: string;
|
|
11
|
+
}[];
|
|
12
|
+
isError: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=get-gltf-info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-gltf-info.d.ts","sourceRoot":"","sources":["../../src/tools/get-gltf-info.ts"],"names":[],"mappings":"AAqBA,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM;;;;;;;;;;;;GAyNjD"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { getBounds, inspect } from "@gltf-transform/functions";
|
|
3
|
+
import { createIO } from "../utils/gltf-io.js";
|
|
4
|
+
import { validateFilePath } from "../utils/path-security.js";
|
|
5
|
+
export async function getGltfInfo(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
const resolvedPath = validateFilePath(filePath);
|
|
8
|
+
const io = await createIO();
|
|
9
|
+
const doc = await io.read(resolvedPath);
|
|
10
|
+
const root = doc.getRoot();
|
|
11
|
+
// Run inspect for structured report
|
|
12
|
+
const report = inspect(doc);
|
|
13
|
+
// File size
|
|
14
|
+
const fileSize = fs.statSync(resolvedPath).size;
|
|
15
|
+
// Asset info
|
|
16
|
+
const asset = root.getAsset();
|
|
17
|
+
const generator = asset.generator || "unknown";
|
|
18
|
+
const gltfVersion = asset.version || "2.0";
|
|
19
|
+
// Get bounding box from default scene
|
|
20
|
+
const scene = root.listScenes()[0];
|
|
21
|
+
let boundingBox = null;
|
|
22
|
+
if (scene) {
|
|
23
|
+
const bounds = getBounds(scene);
|
|
24
|
+
boundingBox = {
|
|
25
|
+
min: bounds.min,
|
|
26
|
+
max: bounds.max,
|
|
27
|
+
dimensions: {
|
|
28
|
+
x: bounds.max[0] - bounds.min[0],
|
|
29
|
+
y: bounds.max[1] - bounds.min[1],
|
|
30
|
+
z: bounds.max[2] - bounds.min[2],
|
|
31
|
+
},
|
|
32
|
+
center: {
|
|
33
|
+
x: (bounds.min[0] + bounds.max[0]) / 2,
|
|
34
|
+
y: (bounds.min[1] + bounds.max[1]) / 2,
|
|
35
|
+
z: (bounds.min[2] + bounds.max[2]) / 2,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Compute aggregate counts
|
|
40
|
+
let totalVertices = 0;
|
|
41
|
+
let totalTriangles = 0;
|
|
42
|
+
let drawCallEstimate = 0;
|
|
43
|
+
for (const mesh of report.meshes.properties) {
|
|
44
|
+
totalVertices += mesh.vertices;
|
|
45
|
+
totalTriangles += mesh.glPrimitives;
|
|
46
|
+
drawCallEstimate += mesh.meshPrimitives;
|
|
47
|
+
}
|
|
48
|
+
const meshCount = report.meshes.properties.length;
|
|
49
|
+
const materialCount = report.materials.properties.length;
|
|
50
|
+
const textureCount = report.textures.properties.length;
|
|
51
|
+
const animationCount = report.animations.properties.length;
|
|
52
|
+
const skinCount = root.listSkins().length;
|
|
53
|
+
// Total texture memory (from inspect gpuSize)
|
|
54
|
+
let totalTextureMemoryBytes = 0;
|
|
55
|
+
for (const tex of report.textures.properties) {
|
|
56
|
+
const texExt = tex;
|
|
57
|
+
if (texExt.gpuSize) {
|
|
58
|
+
totalTextureMemoryBytes += texExt.gpuSize;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const totalTextureMemoryMB = parseFloat((totalTextureMemoryBytes / (1024 * 1024)).toFixed(2));
|
|
62
|
+
// Extensions
|
|
63
|
+
const extensionsUsed = root.listExtensionsUsed().map((ext) => ext.extensionName);
|
|
64
|
+
const extensionsRequired = root.listExtensionsRequired().map((ext) => ext.extensionName);
|
|
65
|
+
// Compression state
|
|
66
|
+
const compressionState = {
|
|
67
|
+
draco: extensionsUsed.includes("KHR_draco_mesh_compression"),
|
|
68
|
+
meshopt: extensionsUsed.includes("EXT_meshopt_compression"),
|
|
69
|
+
ktx2Basis: extensionsUsed.includes("KHR_texture_basisu"),
|
|
70
|
+
};
|
|
71
|
+
// Potential issues
|
|
72
|
+
const potentialIssues = [];
|
|
73
|
+
if (totalTriangles > 500000) {
|
|
74
|
+
potentialIssues.push(`High triangle count (${totalTriangles.toLocaleString()})`);
|
|
75
|
+
}
|
|
76
|
+
if (drawCallEstimate > 100) {
|
|
77
|
+
potentialIssues.push(`High draw call count (~${drawCallEstimate})`);
|
|
78
|
+
}
|
|
79
|
+
for (const tex of report.textures.properties) {
|
|
80
|
+
const texExt = tex;
|
|
81
|
+
const size = texExt.resolution;
|
|
82
|
+
if (size) {
|
|
83
|
+
const match = String(size).match(/(\d+)x(\d+)/);
|
|
84
|
+
if (match) {
|
|
85
|
+
const maxRes = Math.max(parseInt(match[1], 10), parseInt(match[2], 10));
|
|
86
|
+
if (maxRes > 4096) {
|
|
87
|
+
potentialIssues.push(`Texture "${tex.name || "(unnamed)"}" exceeds 4K (${size})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!compressionState.draco && !compressionState.meshopt) {
|
|
93
|
+
potentialIssues.push("No mesh compression (Draco/meshopt could reduce size)");
|
|
94
|
+
}
|
|
95
|
+
if (!compressionState.ktx2Basis) {
|
|
96
|
+
potentialIssues.push("No texture compression (KTX2/Basis could reduce GPU memory)");
|
|
97
|
+
}
|
|
98
|
+
// Summary
|
|
99
|
+
const summary = {
|
|
100
|
+
fileSize,
|
|
101
|
+
generator,
|
|
102
|
+
gltfVersion,
|
|
103
|
+
totalTriangles,
|
|
104
|
+
totalVertices,
|
|
105
|
+
meshCount,
|
|
106
|
+
materialCount,
|
|
107
|
+
textureCount,
|
|
108
|
+
animationCount,
|
|
109
|
+
skinCount,
|
|
110
|
+
drawCallEstimate,
|
|
111
|
+
totalTextureMemoryMB,
|
|
112
|
+
boundingBox,
|
|
113
|
+
compressionState,
|
|
114
|
+
potentialIssues,
|
|
115
|
+
};
|
|
116
|
+
// Material details
|
|
117
|
+
const materials = root.listMaterials().map((mat) => {
|
|
118
|
+
const baseColorTex = mat.getBaseColorTexture();
|
|
119
|
+
const normalTex = mat.getNormalTexture();
|
|
120
|
+
const mrTex = mat.getMetallicRoughnessTexture();
|
|
121
|
+
const occlusionTex = mat.getOcclusionTexture();
|
|
122
|
+
const emissiveTex = mat.getEmissiveTexture();
|
|
123
|
+
return {
|
|
124
|
+
name: mat.getName() || "(unnamed)",
|
|
125
|
+
alphaMode: mat.getAlphaMode(),
|
|
126
|
+
doubleSided: mat.getDoubleSided(),
|
|
127
|
+
baseColorFactor: Array.from(mat.getBaseColorFactor()),
|
|
128
|
+
metallicFactor: mat.getMetallicFactor(),
|
|
129
|
+
roughnessFactor: mat.getRoughnessFactor(),
|
|
130
|
+
emissiveFactor: Array.from(mat.getEmissiveFactor()),
|
|
131
|
+
textures: {
|
|
132
|
+
baseColor: baseColorTex ? baseColorTex.getName() || true : null,
|
|
133
|
+
normal: normalTex ? normalTex.getName() || true : null,
|
|
134
|
+
metallicRoughness: mrTex ? mrTex.getName() || true : null,
|
|
135
|
+
occlusion: occlusionTex ? occlusionTex.getName() || true : null,
|
|
136
|
+
emissive: emissiveTex ? emissiveTex.getName() || true : null,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
// Animation details
|
|
141
|
+
const animations = root.listAnimations().map((anim) => {
|
|
142
|
+
const channels = anim.listChannels().map((ch) => {
|
|
143
|
+
const targetNode = ch.getTargetNode();
|
|
144
|
+
return {
|
|
145
|
+
targetNode: targetNode ? targetNode.getName() || "(unnamed)" : null,
|
|
146
|
+
targetPath: ch.getTargetPath(),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
name: anim.getName() || "(unnamed)",
|
|
151
|
+
channelCount: channels.length,
|
|
152
|
+
samplerCount: anim.listSamplers().length,
|
|
153
|
+
channels,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
// Skin details
|
|
157
|
+
const skins = root.listSkins().map((skin) => {
|
|
158
|
+
const skeleton = skin.getSkeleton();
|
|
159
|
+
return {
|
|
160
|
+
name: skin.getName() || "(unnamed)",
|
|
161
|
+
jointCount: skin.listJoints().length,
|
|
162
|
+
skeleton: skeleton ? skeleton.getName() || "(unnamed)" : null,
|
|
163
|
+
jointNames: skin.listJoints().map((j) => j.getName() || "(unnamed)"),
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
// Scene hierarchy with transforms
|
|
167
|
+
function buildHierarchy(node) {
|
|
168
|
+
const mesh = node.getMesh();
|
|
169
|
+
const children = node.listChildren().map((child) => buildHierarchy(child));
|
|
170
|
+
return {
|
|
171
|
+
name: node.getName() || "(unnamed)",
|
|
172
|
+
type: mesh ? "Mesh" : "Node",
|
|
173
|
+
translation: Array.from(node.getTranslation()),
|
|
174
|
+
rotation: Array.from(node.getRotation()),
|
|
175
|
+
scale: Array.from(node.getScale()),
|
|
176
|
+
children,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const hierarchy = scene ? scene.listChildren().map((child) => buildHierarchy(child)) : [];
|
|
180
|
+
const result = {
|
|
181
|
+
file: resolvedPath,
|
|
182
|
+
summary,
|
|
183
|
+
materials,
|
|
184
|
+
textures: report.textures.properties,
|
|
185
|
+
animations,
|
|
186
|
+
skins,
|
|
187
|
+
scenes: report.scenes.properties,
|
|
188
|
+
meshes: report.meshes.properties,
|
|
189
|
+
extensionsUsed,
|
|
190
|
+
extensionsRequired,
|
|
191
|
+
hierarchy,
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
199
|
+
return {
|
|
200
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
201
|
+
isError: true,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=get-gltf-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-gltf-info.js","sourceRoot":"","sources":["../../src/tools/get-gltf-info.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export interface RenderOptions {
|
|
2
|
+
yaw: number;
|
|
3
|
+
pitch: number;
|
|
4
|
+
roll: number;
|
|
5
|
+
resolution: number;
|
|
6
|
+
mode: "default" | "wireframe" | "normals";
|
|
7
|
+
textureOverrides?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export interface NormalizationInfo {
|
|
10
|
+
scaleFactor: number;
|
|
11
|
+
originalDimensions: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
z: number;
|
|
15
|
+
};
|
|
16
|
+
originalCenter: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
z: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export declare function renderGltf(filePath: string, options: RenderOptions): Promise<{
|
|
23
|
+
content: ({
|
|
24
|
+
type: "text";
|
|
25
|
+
text: string;
|
|
26
|
+
data?: undefined;
|
|
27
|
+
mimeType?: undefined;
|
|
28
|
+
} | {
|
|
29
|
+
type: "image";
|
|
30
|
+
data: string;
|
|
31
|
+
mimeType: string;
|
|
32
|
+
text?: undefined;
|
|
33
|
+
})[];
|
|
34
|
+
isError?: undefined;
|
|
35
|
+
} | {
|
|
36
|
+
content: {
|
|
37
|
+
type: "text";
|
|
38
|
+
text: string;
|
|
39
|
+
}[];
|
|
40
|
+
isError: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
/** Exposed for tests to shut down the singleton */
|
|
43
|
+
export declare function disposeRenderer(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Start a standalone HTTP server (no browser) for testing routes directly.
|
|
46
|
+
* Returns the base URL and a dispose function.
|
|
47
|
+
*/
|
|
48
|
+
export declare function startTestServer(): Promise<{
|
|
49
|
+
baseUrl: string;
|
|
50
|
+
buildAssetUrl: (filePath: string) => string;
|
|
51
|
+
dispose: () => void;
|
|
52
|
+
}>;
|
|
53
|
+
//# sourceMappingURL=render-gltf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-gltf.d.ts","sourceRoot":"","sources":["../../src/tools/render-gltf.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;IAC1C,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,kBAAkB,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,cAAc,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrD;AA4QD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa;;;;;;;;;;;;;;;;;;;GAiDxE;AAED,mDAAmD;AACnD,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAErD;AAED;;;GAGG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC,CA6FD"}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { validateFilePath } from "../utils/path-security.js";
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const MIME_TYPES = {
|
|
10
|
+
".html": "text/html",
|
|
11
|
+
".js": "application/javascript",
|
|
12
|
+
".mjs": "application/javascript",
|
|
13
|
+
".json": "application/json",
|
|
14
|
+
".wasm": "application/wasm",
|
|
15
|
+
".glb": "model/gltf-binary",
|
|
16
|
+
".gltf": "model/gltf+json",
|
|
17
|
+
".bin": "application/octet-stream",
|
|
18
|
+
".png": "image/png",
|
|
19
|
+
".jpg": "image/jpeg",
|
|
20
|
+
".jpeg": "image/jpeg",
|
|
21
|
+
".ktx2": "image/ktx2",
|
|
22
|
+
".webp": "image/webp",
|
|
23
|
+
".bmp": "image/bmp",
|
|
24
|
+
};
|
|
25
|
+
class PuppeteerRenderer {
|
|
26
|
+
server = null;
|
|
27
|
+
browser = null;
|
|
28
|
+
page = null;
|
|
29
|
+
port = 0;
|
|
30
|
+
readyPromise = null;
|
|
31
|
+
renderLock = Promise.resolve();
|
|
32
|
+
threePath = "";
|
|
33
|
+
async ensureReady() {
|
|
34
|
+
if (this.readyPromise)
|
|
35
|
+
return this.readyPromise;
|
|
36
|
+
this.readyPromise = this._init();
|
|
37
|
+
return this.readyPromise;
|
|
38
|
+
}
|
|
39
|
+
async _init() {
|
|
40
|
+
// Resolve three.js path — use require.resolve for vitest compatibility
|
|
41
|
+
const threeEntry = require.resolve("three");
|
|
42
|
+
// threeEntry is like /path/to/node_modules/three/build/three.module.js
|
|
43
|
+
this.threePath = path.resolve(threeEntry, "..", "..");
|
|
44
|
+
// Start local HTTP server
|
|
45
|
+
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
46
|
+
await new Promise((resolve) => {
|
|
47
|
+
this.server?.listen(0, "127.0.0.1", () => resolve());
|
|
48
|
+
});
|
|
49
|
+
const addr = this.server.address();
|
|
50
|
+
this.port = addr.port;
|
|
51
|
+
// Launch Puppeteer
|
|
52
|
+
const puppeteer = await import("puppeteer");
|
|
53
|
+
this.browser = await puppeteer.default.launch({
|
|
54
|
+
headless: true,
|
|
55
|
+
args: [
|
|
56
|
+
"--no-sandbox",
|
|
57
|
+
"--disable-setuid-sandbox",
|
|
58
|
+
"--disable-dev-shm-usage",
|
|
59
|
+
"--use-gl=angle",
|
|
60
|
+
"--use-angle=default",
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
this.page = await this.browser.newPage();
|
|
64
|
+
// Navigate to renderer page
|
|
65
|
+
await this.page.goto(`http://127.0.0.1:${this.port}/renderer.html`, {
|
|
66
|
+
waitUntil: "networkidle0",
|
|
67
|
+
});
|
|
68
|
+
// Wait for renderer to be ready
|
|
69
|
+
await this.page.waitForFunction("window.rendererReady === true", {
|
|
70
|
+
timeout: 15000,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
handleRequest(req, res) {
|
|
74
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${this.port}`);
|
|
75
|
+
const pathname = url.pathname;
|
|
76
|
+
if (pathname === "/renderer.html") {
|
|
77
|
+
const htmlPath = path.join(__dirname, "..", "renderer", "renderer.html");
|
|
78
|
+
this.serveFile(htmlPath, res);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (pathname.startsWith("/node_modules/three/")) {
|
|
82
|
+
const relPath = pathname.replace("/node_modules/three/", "");
|
|
83
|
+
const filePath = path.join(this.threePath, relPath);
|
|
84
|
+
// Security: ensure resolved path stays within three package
|
|
85
|
+
const resolved = path.resolve(filePath);
|
|
86
|
+
if (!resolved.startsWith(this.threePath)) {
|
|
87
|
+
res.writeHead(403);
|
|
88
|
+
res.end("Forbidden");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.serveFile(resolved, res);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (pathname.startsWith("/assets/")) {
|
|
95
|
+
// Route: /assets/{base64url-encoded-dir}/{relative-path}
|
|
96
|
+
const rest = pathname.slice("/assets/".length);
|
|
97
|
+
const slashIdx = rest.indexOf("/");
|
|
98
|
+
if (slashIdx === -1) {
|
|
99
|
+
res.writeHead(400);
|
|
100
|
+
res.end("Missing relative path");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const encodedDir = rest.slice(0, slashIdx);
|
|
104
|
+
const relPath = decodeURIComponent(rest.slice(slashIdx + 1));
|
|
105
|
+
let baseDir;
|
|
106
|
+
try {
|
|
107
|
+
baseDir = Buffer.from(encodedDir, "base64url").toString("utf-8");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
res.writeHead(400);
|
|
111
|
+
res.end("Invalid base64url directory encoding");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const filePath = path.resolve(baseDir, relPath);
|
|
115
|
+
// Security: ensure resolved path stays within the base directory
|
|
116
|
+
if (!filePath.startsWith(path.resolve(baseDir) + path.sep) &&
|
|
117
|
+
filePath !== path.resolve(baseDir)) {
|
|
118
|
+
res.writeHead(403);
|
|
119
|
+
res.end("Forbidden: path escapes base directory");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.serveFile(filePath, res);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
res.writeHead(404);
|
|
126
|
+
res.end("Not found");
|
|
127
|
+
}
|
|
128
|
+
serveFile(filePath, res) {
|
|
129
|
+
try {
|
|
130
|
+
const data = fs.readFileSync(filePath);
|
|
131
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
132
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
133
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
134
|
+
res.end(data);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
res.writeHead(404);
|
|
138
|
+
res.end("File not found");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
buildAssetUrl(filePath) {
|
|
142
|
+
const dir = path.dirname(filePath);
|
|
143
|
+
const filename = path.basename(filePath);
|
|
144
|
+
const encodedDir = Buffer.from(dir, "utf-8").toString("base64url");
|
|
145
|
+
return `http://127.0.0.1:${this.port}/assets/${encodedDir}/${encodeURIComponent(filename)}`;
|
|
146
|
+
}
|
|
147
|
+
async render(resolvedPath, options) {
|
|
148
|
+
await this.ensureReady();
|
|
149
|
+
// Serialize concurrent render calls
|
|
150
|
+
const task = this.renderLock.then(async () => {
|
|
151
|
+
const modelUrl = this.buildAssetUrl(resolvedPath);
|
|
152
|
+
// Build texture override URLs
|
|
153
|
+
let textureOverrideUrls;
|
|
154
|
+
if (options.textureOverrides) {
|
|
155
|
+
textureOverrideUrls = {};
|
|
156
|
+
for (const [slot, texPath] of Object.entries(options.textureOverrides)) {
|
|
157
|
+
const resolvedTexPath = path.resolve(texPath);
|
|
158
|
+
textureOverrideUrls[slot] = this.buildAssetUrl(resolvedTexPath);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// biome-ignore lint/style/noNonNullAssertion: page is guaranteed non-null after ensureReady()
|
|
162
|
+
const renderResult = await this.page.evaluate(async (opts) => {
|
|
163
|
+
const w = window;
|
|
164
|
+
return await w.renderModel(opts);
|
|
165
|
+
}, {
|
|
166
|
+
glbUrl: modelUrl,
|
|
167
|
+
width: options.resolution,
|
|
168
|
+
height: options.resolution,
|
|
169
|
+
yaw: options.yaw,
|
|
170
|
+
pitch: options.pitch,
|
|
171
|
+
roll: options.roll,
|
|
172
|
+
mode: options.mode,
|
|
173
|
+
textureOverrideUrls,
|
|
174
|
+
});
|
|
175
|
+
// Extract base64 from data URL
|
|
176
|
+
const base64 = renderResult.dataUrl.replace(/^data:image\/png;base64,/, "");
|
|
177
|
+
return {
|
|
178
|
+
normalization: renderResult.normalization,
|
|
179
|
+
pngBase64: base64,
|
|
180
|
+
warnings: renderResult.warnings || [],
|
|
181
|
+
};
|
|
182
|
+
});
|
|
183
|
+
this.renderLock = task;
|
|
184
|
+
return task;
|
|
185
|
+
}
|
|
186
|
+
async dispose() {
|
|
187
|
+
if (this.page) {
|
|
188
|
+
try {
|
|
189
|
+
await this.page.close();
|
|
190
|
+
}
|
|
191
|
+
catch { }
|
|
192
|
+
this.page = null;
|
|
193
|
+
}
|
|
194
|
+
if (this.browser) {
|
|
195
|
+
try {
|
|
196
|
+
await this.browser.close();
|
|
197
|
+
}
|
|
198
|
+
catch { }
|
|
199
|
+
this.browser = null;
|
|
200
|
+
}
|
|
201
|
+
if (this.server) {
|
|
202
|
+
this.server.close();
|
|
203
|
+
this.server = null;
|
|
204
|
+
}
|
|
205
|
+
this.readyPromise = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Module-level singleton
|
|
209
|
+
const renderer = new PuppeteerRenderer();
|
|
210
|
+
// Cleanup on process exit
|
|
211
|
+
function cleanup() {
|
|
212
|
+
renderer.dispose().catch(() => { });
|
|
213
|
+
}
|
|
214
|
+
process.on("exit", cleanup);
|
|
215
|
+
process.on("SIGINT", () => {
|
|
216
|
+
cleanup();
|
|
217
|
+
process.exit(0);
|
|
218
|
+
});
|
|
219
|
+
process.on("SIGTERM", () => {
|
|
220
|
+
cleanup();
|
|
221
|
+
process.exit(0);
|
|
222
|
+
});
|
|
223
|
+
export async function renderGltf(filePath, options) {
|
|
224
|
+
try {
|
|
225
|
+
const resolvedPath = validateFilePath(filePath);
|
|
226
|
+
// Validate texture override paths if provided
|
|
227
|
+
if (options.textureOverrides) {
|
|
228
|
+
const supportedExts = [".png", ".jpg", ".jpeg", ".webp", ".bmp", ".ktx2"];
|
|
229
|
+
for (const [slot, texPath] of Object.entries(options.textureOverrides)) {
|
|
230
|
+
const resolvedTexPath = path.resolve(texPath);
|
|
231
|
+
if (!fs.existsSync(resolvedTexPath)) {
|
|
232
|
+
throw new Error(`Texture override for '${slot}' not found: ${resolvedTexPath}`);
|
|
233
|
+
}
|
|
234
|
+
const ext = path.extname(resolvedTexPath).toLowerCase();
|
|
235
|
+
if (!supportedExts.includes(ext)) {
|
|
236
|
+
throw new Error(`Unsupported texture format for '${slot}': ${ext}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const { normalization, pngBase64, warnings } = await renderer.render(resolvedPath, options);
|
|
241
|
+
const metadata = {
|
|
242
|
+
file: path.basename(resolvedPath),
|
|
243
|
+
resolution: options.resolution,
|
|
244
|
+
mode: options.mode,
|
|
245
|
+
normalization,
|
|
246
|
+
warnings,
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
content: [
|
|
250
|
+
{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: JSON.stringify(metadata),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
type: "image",
|
|
256
|
+
data: pngBase64,
|
|
257
|
+
mimeType: "image/png",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
264
|
+
return {
|
|
265
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
266
|
+
isError: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/** Exposed for tests to shut down the singleton */
|
|
271
|
+
export async function disposeRenderer() {
|
|
272
|
+
await renderer.dispose();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Start a standalone HTTP server (no browser) for testing routes directly.
|
|
276
|
+
* Returns the base URL and a dispose function.
|
|
277
|
+
*/
|
|
278
|
+
export async function startTestServer() {
|
|
279
|
+
const threeEntry = require.resolve("three");
|
|
280
|
+
const threePath = path.resolve(threeEntry, "..", "..");
|
|
281
|
+
let port = 0;
|
|
282
|
+
const server = http.createServer((req, res) => {
|
|
283
|
+
const url = new URL(req.url || "/", `http://127.0.0.1:${port}`);
|
|
284
|
+
const pathname = url.pathname;
|
|
285
|
+
if (pathname === "/renderer.html") {
|
|
286
|
+
const htmlPath = path.join(__dirname, "..", "renderer", "renderer.html");
|
|
287
|
+
serveStatic(htmlPath, res);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (pathname.startsWith("/node_modules/three/")) {
|
|
291
|
+
const relPath = pathname.replace("/node_modules/three/", "");
|
|
292
|
+
const filePath = path.join(threePath, relPath);
|
|
293
|
+
const resolved = path.resolve(filePath);
|
|
294
|
+
if (!resolved.startsWith(threePath)) {
|
|
295
|
+
res.writeHead(403);
|
|
296
|
+
res.end("Forbidden");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
serveStatic(resolved, res);
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (pathname.startsWith("/assets/")) {
|
|
303
|
+
const rest = pathname.slice("/assets/".length);
|
|
304
|
+
const slashIdx = rest.indexOf("/");
|
|
305
|
+
if (slashIdx === -1) {
|
|
306
|
+
res.writeHead(400);
|
|
307
|
+
res.end("Missing relative path");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const encodedDir = rest.slice(0, slashIdx);
|
|
311
|
+
const relPath = decodeURIComponent(rest.slice(slashIdx + 1));
|
|
312
|
+
let baseDir;
|
|
313
|
+
try {
|
|
314
|
+
baseDir = Buffer.from(encodedDir, "base64url").toString("utf-8");
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
res.writeHead(400);
|
|
318
|
+
res.end("Invalid base64url directory encoding");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const filePath = path.resolve(baseDir, relPath);
|
|
322
|
+
if (!filePath.startsWith(path.resolve(baseDir) + path.sep) &&
|
|
323
|
+
filePath !== path.resolve(baseDir)) {
|
|
324
|
+
res.writeHead(403);
|
|
325
|
+
res.end("Forbidden: path escapes base directory");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
serveStatic(filePath, res);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
res.writeHead(404);
|
|
332
|
+
res.end("Not found");
|
|
333
|
+
});
|
|
334
|
+
function serveStatic(filePath, res) {
|
|
335
|
+
try {
|
|
336
|
+
const data = fs.readFileSync(filePath);
|
|
337
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
338
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
339
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
340
|
+
res.end(data);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
res.writeHead(404);
|
|
344
|
+
res.end("File not found");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
await new Promise((resolve) => {
|
|
348
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
349
|
+
});
|
|
350
|
+
port = server.address().port;
|
|
351
|
+
return {
|
|
352
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
353
|
+
buildAssetUrl(filePath) {
|
|
354
|
+
const dir = path.dirname(filePath);
|
|
355
|
+
const filename = path.basename(filePath);
|
|
356
|
+
const encodedDir = Buffer.from(dir, "utf-8").toString("base64url");
|
|
357
|
+
return `http://127.0.0.1:${port}/assets/${encodedDir}/${encodeURIComponent(filename)}`;
|
|
358
|
+
},
|
|
359
|
+
dispose() {
|
|
360
|
+
server.close();
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
//# sourceMappingURL=render-gltf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render-gltf.js","sourceRoot":"","sources":["../../src/tools/render-gltf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAE7D,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAiB/C,MAAM,UAAU,GAA2B;IACzC,OAAO,EAAE,WAAW;IACpB,KAAK,EAAE,wBAAwB;IAC/B,MAAM,EAAE,wBAAwB;IAChC,OAAO,EAAE,kBAAkB;IAC3B,OAAO,EAAE,kBAAkB;IAC3B,MAAM,EAAE,mBAAmB;IAC3B,OAAO,EAAE,iBAAiB;IAC1B,MAAM,EAAE,0BAA0B;IAClC,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;CACpB,CAAC;AAEF,MAAM,iBAAiB;IACb,MAAM,GAAuB,IAAI,CAAC;IAClC,OAAO,GAAmB,IAAI,CAAC;IAC/B,IAAI,GAAgB,IAAI,CAAC;IACzB,IAAI,GAAG,CAAC,CAAC;IACT,YAAY,GAAyB,IAAI,CAAC;IAC1C,UAAU,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IACjD,SAAS,GAAW,EAAE,CAAC;IAE/B,KAAK,CAAC,WAAW;QACf,IAAI,IAAI,CAAC,YAAY;YAAE,OAAO,IAAI,CAAC,YAAY,CAAC;QAChD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QACjC,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,uEAAuE;QACvE,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC5C,uEAAuE;QACvE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAEtD,0BAA0B;QAC1B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QAC5E,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,EAAsB,CAAC;QACvD,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QAEtB,mBAAmB;QACnB,MAAM,SAAS,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC;YAC5C,QAAQ,EAAE,IAAI;YACd,IAAI,EAAE;gBACJ,cAAc;gBACd,0BAA0B;gBAC1B,yBAAyB;gBACzB,gBAAgB;gBAChB,qBAAqB;aACtB;SACF,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;QAEzC,4BAA4B;QAC5B,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,oBAAoB,IAAI,CAAC,IAAI,gBAAgB,EAAE;YAClE,SAAS,EAAE,cAAc;SAC1B,CAAC,CAAC;QAEH,gCAAgC;QAChC,MAAM,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,+BAA+B,EAAE;YAC/D,OAAO,EAAE,KAAK;SACf,CAAC,CAAC;IACL,CAAC;IAEO,aAAa,CAAC,GAAyB,EAAE,GAAwB;QACvE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,oBAAoB,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACrE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAE9B,IAAI,QAAQ,KAAK,gBAAgB,EAAE,CAAC;YAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;YACzE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,IAAI,QAAQ,CAAC,UAAU,CAAC,sBAAsB,CAAC,EAAE,CAAC;YAChD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;YAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACpD,4DAA4D;YAC5D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;gBACzC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACrB,OAAO;YACT,CAAC;YACD,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,IAAI,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACpC,yDAAyD;YACzD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;gBACpB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;gBACjC,OAAO;YACT,CAAC;YACD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;YAC3C,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;YAC7D,IAAI,OAAe,CAAC;YACpB,IAAI,CAAC;gBACH,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACnE,CAAC;YAAC,MAAM,CAAC;gBACP,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;gBAChD,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAChD,iEAAiE;YACjE,IACE,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;gBACtD,QAAQ,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAClC,CAAC;gBACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;gBAClD,OAAO;YACT,CAAC;YACD,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC9B,OAAO;QACT,CAAC;QAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvB,CAAC;IAEO,SAAS,CAAC,QAAgB,EAAE,GAAwB;QAC1D,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;YACjD,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC;YAClE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;YACpD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAEO,aAAa,CAAC,QAAgB;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACzC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;QACnE,OAAO,oBAAoB,IAAI,CAAC,IAAI,WAAW,UAAU,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC9F,CAAC;IAED,KAAK,CAAC,MAAM,CACV,YAAoB,EACpB,OAAsB;QAMtB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QAEzB,oCAAoC;QACpC,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;YAElD,8BAA8B;YAC9B,IAAI,mBAAuD,CAAC;YAC5D,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;gBAC7B,mBAAmB,GAAG,EAAE,CAAC;gBACzB,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;oBACvE,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;oBAC9C,mBAAmB,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC;gBAClE,CAAC;YACH,CAAC;YAED,8FAA8F;YAC9F,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,IAAK,CAAC,QAAQ,CAC5C,KAAK,EAAE,IASN,EAAE,EAAE;gBACH,MAAM,CAAC,GAAG,MAUT,CAAC;gBACF,OAAO,MAAM,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACnC,CAAC,EACD;gBACE,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE,OAAO,CAAC,UAAU;gBACzB,MAAM,EAAE,OAAO,CAAC,UAAU;gBAC1B,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,IAAI,EAAE,OAAO,CAAC,IAAI;gBAClB,mBAAmB;aACpB,CACF,CAAC;YAEF,+BAA+B;YAC/B,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,0BAA0B,EAAE,EAAE,CAAC,CAAC;YAE5E,OAAO;gBACL,aAAa,EAAE,YAAY,CAAC,aAAa;gBACzC,SAAS,EAAE,MAAM;gBACjB,QAAQ,EAAE,YAAY,CAAC,QAAQ,IAAI,EAAE;aACtC,CAAC;QACJ,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QAEvB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACd,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAC1B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACnB,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACV,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC;IAC3B,CAAC;CACF;AAED,yBAAyB;AACzB,MAAM,QAAQ,GAAG,IAAI,iBAAiB,EAAE,CAAC;AAEzC,0BAA0B;AAC1B,SAAS,OAAO;IACd,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;AACrC,CAAC;AACD,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAC5B,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;IACxB,OAAO,EAAE,CAAC;IACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AACH,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACzB,OAAO,EAAE,CAAC;IACV,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgB,EAAE,OAAsB;IACvE,IAAI,CAAC;QACH,MAAM,YAAY,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAEhD,8CAA8C;QAC9C,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;YAC7B,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;YAC1E,KAAK,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBACvE,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;gBAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;oBACpC,MAAM,IAAI,KAAK,CAAC,yBAAyB,IAAI,gBAAgB,eAAe,EAAE,CAAC,CAAC;gBAClF,CAAC;gBACD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,WAAW,EAAE,CAAC;gBACxD,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;oBACjC,MAAM,IAAI,KAAK,CAAC,mCAAmC,IAAI,MAAM,GAAG,EAAE,CAAC,CAAC;gBACtE,CAAC;YACH,CAAC;QACH,CAAC;QAED,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAE5F,MAAM,QAAQ,GAAG;YACf,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;YACjC,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,aAAa;YACb,QAAQ;SACT,CAAC;QAEF,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;iBAC/B;gBACD;oBACE,IAAI,EAAE,OAAgB;oBACtB,IAAI,EAAE,SAAS;oBACf,QAAQ,EAAE,WAAW;iBACtB;aACF;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,KAAc,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,IAAI,EAAE,UAAU,OAAO,EAAE,EAAE,CAAC;YAC/D,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC;AAED,mDAAmD;AACnD,MAAM,CAAC,KAAK,UAAU,eAAe;IACnC,MAAM,QAAQ,CAAC,OAAO,EAAE,CAAC;AAC3B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe;IAKnC,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAEvD,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QAC5C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,oBAAoB,IAAI,EAAE,CAAC,CAAC;QAChE,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAE9B,IAAI,QAAQ,KAAK,gBAAgB,EAAE,CAAC;YAClC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;YACzE,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,IAAI,QAAQ,CAAC,UAAU,CAAC,sBAAsB,CAAC,EAAE,CAAC;YAChD,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,sBAAsB,EAAE,EAAE,CAAC,CAAC;YAC7D,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACxC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBACpC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBACrB,OAAO;YACT,CAAC;YACD,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,IAAI,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACpC,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACnC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;gBACpB,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;gBACjC,OAAO;YACT,CAAC;YACD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;YAC3C,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC;YAC7D,IAAI,OAAe,CAAC;YACpB,IAAI,CAAC;gBACH,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YACnE,CAAC;YAAC,MAAM,CAAC;gBACP,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;gBAChD,OAAO;YACT,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAChD,IACE,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC;gBACtD,QAAQ,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAClC,CAAC;gBACD,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBACnB,GAAG,CAAC,GAAG,CAAC,wCAAwC,CAAC,CAAC;gBAClD,OAAO;YACT,CAAC;YACD,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC3B,OAAO;QACT,CAAC;QAED,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACnB,GAAG,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACvB,CAAC,CAAC,CAAC;IAEH,SAAS,WAAW,CAAC,QAAgB,EAAE,GAAwB;QAC7D,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;YACvC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;YACjD,MAAM,WAAW,GAAG,UAAU,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC;YAClE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC;YACpD,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChB,CAAC;QAAC,MAAM,CAAC;YACP,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACnB,GAAG,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAED,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QAClC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IACH,IAAI,GAAI,MAAM,CAAC,OAAO,EAAuB,CAAC,IAAI,CAAC;IAEnD,OAAO;QACL,OAAO,EAAE,oBAAoB,IAAI,EAAE;QACnC,aAAa,CAAC,QAAgB;YAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;YACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YACzC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;YACnE,OAAO,oBAAoB,IAAI,WAAW,UAAU,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzF,CAAC;QACD,OAAO;YACL,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gltf-io.d.ts","sourceRoot":"","sources":["../../src/utils/gltf-io.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAI9C;;GAEG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAOhD"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NodeIO } from "@gltf-transform/core";
|
|
2
|
+
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
|
3
|
+
import draco3d from "draco3dgltf";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a NodeIO instance configured with all extensions and Draco decoder.
|
|
6
|
+
*/
|
|
7
|
+
export async function createIO() {
|
|
8
|
+
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
|
9
|
+
const decoderModule = await draco3d.createDecoderModule();
|
|
10
|
+
io.registerDependencies({ "draco3d.decoder": decoderModule });
|
|
11
|
+
return io;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=gltf-io.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gltf-io.js","sourceRoot":"","sources":["../../src/utils/gltf-io.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,OAAO,MAAM,aAAa,CAAC;AAElC;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,EAAE,GAAG,IAAI,MAAM,EAAE,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAE3D,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAC1D,EAAE,CAAC,oBAAoB,CAAC,EAAE,iBAAiB,EAAE,aAAa,EAAE,CAAC,CAAC;IAE9D,OAAO,EAAE,CAAC;AACZ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-security.d.ts","sourceRoot":"","sources":["../../src/utils/path-security.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAsBzD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Validates that a file path points to a readable glTF/GLB file.
|
|
5
|
+
* Returns the resolved absolute path.
|
|
6
|
+
*/
|
|
7
|
+
export function validateFilePath(filePath) {
|
|
8
|
+
const resolved = path.resolve(filePath);
|
|
9
|
+
if (!fs.existsSync(resolved)) {
|
|
10
|
+
throw new Error(`File not found: ${resolved}`);
|
|
11
|
+
}
|
|
12
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
13
|
+
if (ext !== ".gltf" && ext !== ".glb") {
|
|
14
|
+
throw new Error(`Not a glTF file (expected .gltf or .glb, got ${ext || "no extension"}): ${resolved}`);
|
|
15
|
+
}
|
|
16
|
+
// Check readability
|
|
17
|
+
try {
|
|
18
|
+
fs.accessSync(resolved, fs.constants.R_OK);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error(`File is not readable: ${resolved}`);
|
|
22
|
+
}
|
|
23
|
+
return resolved;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=path-security.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-security.js","sourceRoot":"","sources":["../../src/utils/path-security.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAExC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,mBAAmB,QAAQ,EAAE,CAAC,CAAC;IACjD,CAAC;IAED,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IACjD,IAAI,GAAG,KAAK,OAAO,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CACb,gDAAgD,GAAG,IAAI,cAAc,MAAM,QAAQ,EAAE,CACtF,CAAC;IACJ,CAAC;IAED,oBAAoB;IACpB,IAAI,CAAC;QACH,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,yBAAyB,QAAQ,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gltf-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for inspecting and rendering glTF/GLB files",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gltf-mcp": "./build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./build/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"build"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc && mkdir -p build/renderer && cp src/renderer/renderer.html build/renderer/renderer.html",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"start": "node build/index.js",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"test:coverage": "vitest run --coverage",
|
|
20
|
+
"lint": "biome check src/ tests/",
|
|
21
|
+
"lint:fix": "biome check --write src/ tests/",
|
|
22
|
+
"format": "biome format --write src/ tests/",
|
|
23
|
+
"docs:dev": "vitepress dev docs",
|
|
24
|
+
"docs:build": "vitepress build docs",
|
|
25
|
+
"docs:preview": "vitepress preview docs",
|
|
26
|
+
"prepublishOnly": "npm run build && npm test"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"gltf",
|
|
31
|
+
"glb",
|
|
32
|
+
"3d",
|
|
33
|
+
"model-context-protocol"
|
|
34
|
+
],
|
|
35
|
+
"author": "Felix Z",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/elixr-games/gltf-mcp.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://elixr-games.github.io/gltf-mcp/",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/elixr-games/gltf-mcp/issues"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@gltf-transform/core": "^4.3.0",
|
|
50
|
+
"@gltf-transform/extensions": "^4.3.0",
|
|
51
|
+
"@gltf-transform/functions": "^4.3.0",
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
53
|
+
"draco3dgltf": "^1.5.7",
|
|
54
|
+
"puppeteer": "^24.2.0",
|
|
55
|
+
"three": "^0.172.0",
|
|
56
|
+
"zod": "^3.24.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@biomejs/biome": "^2.3.14",
|
|
60
|
+
"@types/node": "^22.0.0",
|
|
61
|
+
"@types/pngjs": "^6.0.5",
|
|
62
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
63
|
+
"pngjs": "^7.0.0",
|
|
64
|
+
"sharp": "^0.34.0",
|
|
65
|
+
"typescript": "^5.7.0",
|
|
66
|
+
"vitepress": "^1.6.4",
|
|
67
|
+
"vitest": "^3.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|