scad-gltf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -0
- package/bin/scad-convert.js +221 -0
- package/bin/scad-godot.js +247 -0
- package/bin/scad-mcp.js +508 -0
- package/bin/scad-serve.js +225 -0
- package/bin/scad-web.js +206 -0
- package/editor/dist/aristea_wreck_puresky_2k.hdr +0 -0
- package/editor/dist/assets/OutputPass-Bvl6NigM.js +4317 -0
- package/editor/dist/assets/index-V9cEH4KX.js +4318 -0
- package/editor/dist/assets/index-t9MYrExo.css +1 -0
- package/editor/dist/assets/openscad-CdBCY4mx.wasm +0 -0
- package/editor/dist/assets/preview-C1zc24MJ.js +1 -0
- package/editor/dist/assets/preview-fQfL_FJ-.css +1 -0
- package/editor/dist/assets/prompt-ui-8EFRz0ju.js +126 -0
- package/editor/dist/content-loader.js +4 -0
- package/editor/dist/content.css +255 -0
- package/editor/dist/content.js +24 -0
- package/editor/dist/icon.png +0 -0
- package/editor/dist/index.html +187 -0
- package/editor/dist/manifest.json +23 -0
- package/editor/dist/manifest.webmanifest +1 -0
- package/editor/dist/preview.html +27 -0
- package/editor/dist/registerSW.js +1 -0
- package/editor/dist/sw.js +1 -0
- package/editor/dist/workbox-9c191d2f.js +1 -0
- package/godot/README.md +64 -0
- package/godot/addons/scad_importer/plugin.cfg +7 -0
- package/godot/addons/scad_importer/scad_importer.gd +197 -0
- package/godot/addons/scad_importer/scad_plugin.gd +12 -0
- package/godot/examples/README.md +82 -0
- package/godot/examples/fruit_fusion_3d.js +1574 -0
- package/godot/examples/package.json +5 -0
- package/package.json +63 -0
- package/src/convert.js +94 -0
- package/src/ext/openscad.js +14 -0
- package/src/ext/openscad.wasm +0 -0
- package/src/prompt.js +229 -0
package/bin/scad-mcp.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import {
|
|
8
|
+
CallToolRequestSchema,
|
|
9
|
+
ListToolsRequestSchema,
|
|
10
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
|
+
import puppeteer from "puppeteer";
|
|
12
|
+
import { convertScadToGltf } from "../src/convert.js";
|
|
13
|
+
import { generatePrompt } from "../src/prompt.js";
|
|
14
|
+
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
const wasmPath = path.resolve(__dirname, "../src/ext/openscad.wasm");
|
|
18
|
+
|
|
19
|
+
// Polyfill fetch so the WASM loader works natively in Node.js
|
|
20
|
+
const originalFetch = global.fetch;
|
|
21
|
+
global.fetch = async (url, options) => {
|
|
22
|
+
const urlStr = url.toString();
|
|
23
|
+
if (urlStr.startsWith("file://") || urlStr.endsWith(".wasm")) {
|
|
24
|
+
const normalizedPath = urlStr.startsWith("file://")
|
|
25
|
+
? fileURLToPath(urlStr)
|
|
26
|
+
: urlStr;
|
|
27
|
+
|
|
28
|
+
const buffer = fs.readFileSync(normalizedPath);
|
|
29
|
+
return new Response(buffer, {
|
|
30
|
+
status: 200,
|
|
31
|
+
headers: { "Content-Type": "application/wasm" },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return originalFetch ? originalFetch(url, options) : undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Initialize MCP Server
|
|
38
|
+
const server = new Server(
|
|
39
|
+
{
|
|
40
|
+
name: "scad-mcp-server",
|
|
41
|
+
version: "1.2.0",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
capabilities: {
|
|
45
|
+
tools: {},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Define MCP Tools
|
|
51
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
52
|
+
return {
|
|
53
|
+
tools: [
|
|
54
|
+
{
|
|
55
|
+
name: "get_scad_prompt",
|
|
56
|
+
description:
|
|
57
|
+
"Generates the specialized instruction prompt containing the required syntax rules for PBR, Animations, and Texture Baking in this custom OpenSCAD environment.",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
description: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "The description of the object you want to design.",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: ["description"],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "render_scad_model",
|
|
71
|
+
description:
|
|
72
|
+
"Converts OpenSCAD code to GLTF and uses a 3D renderer to capture images from requested camera angles. Analyze these returned images to verify your design, including specific frames of your animations.",
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: "object",
|
|
75
|
+
properties: {
|
|
76
|
+
scad_code: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "The raw OpenSCAD code to convert and render.",
|
|
79
|
+
},
|
|
80
|
+
camera_angles: {
|
|
81
|
+
type: "array",
|
|
82
|
+
items: {
|
|
83
|
+
type: "string",
|
|
84
|
+
enum: [
|
|
85
|
+
"front",
|
|
86
|
+
"back",
|
|
87
|
+
"left",
|
|
88
|
+
"right",
|
|
89
|
+
"top",
|
|
90
|
+
"bottom",
|
|
91
|
+
"isometric",
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
description:
|
|
95
|
+
"Array of camera angles to render. Defaults to ['front', 'top', 'isometric']. Use this to inspect specific sides of your model.",
|
|
96
|
+
},
|
|
97
|
+
animation_time: {
|
|
98
|
+
type: "number",
|
|
99
|
+
description:
|
|
100
|
+
"The time in seconds to evaluate the animation at (e.g. 1.5). Default is 0.0. Useful for verifying moving parts.",
|
|
101
|
+
},
|
|
102
|
+
animation_index: {
|
|
103
|
+
type: "number",
|
|
104
|
+
description:
|
|
105
|
+
"The index of the animation track to play if multiple exist. Default is 0.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ["scad_code"],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Tool Handlers
|
|
116
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
117
|
+
const { name, arguments: args } = request.params;
|
|
118
|
+
|
|
119
|
+
// ------------------------------------------
|
|
120
|
+
// TOOL 1: get_scad_prompt
|
|
121
|
+
// ------------------------------------------
|
|
122
|
+
if (name === "get_scad_prompt") {
|
|
123
|
+
try {
|
|
124
|
+
const promptText = generatePrompt(args.description, {});
|
|
125
|
+
return {
|
|
126
|
+
content: [
|
|
127
|
+
{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: promptText,
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{ type: "text", text: `Error generating prompt: ${error.message}` },
|
|
137
|
+
],
|
|
138
|
+
isError: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ------------------------------------------
|
|
144
|
+
// TOOL 2: render_scad_model
|
|
145
|
+
// ------------------------------------------
|
|
146
|
+
if (name === "render_scad_model") {
|
|
147
|
+
let browser;
|
|
148
|
+
try {
|
|
149
|
+
const scadCode = args.scad_code;
|
|
150
|
+
const requestedAngles =
|
|
151
|
+
args.camera_angles && args.camera_angles.length > 0
|
|
152
|
+
? args.camera_angles
|
|
153
|
+
: ["front", "top", "isometric"];
|
|
154
|
+
const animTime = args.animation_time || 0.0;
|
|
155
|
+
const animIndex = args.animation_index || 0;
|
|
156
|
+
|
|
157
|
+
// 1. Convert SCAD to GLB ArrayBuffer
|
|
158
|
+
const glbDataArray = await convertScadToGltf(scadCode, {
|
|
159
|
+
wasmUrl: `file://${wasmPath}`,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Convert Uint8Array to base64
|
|
163
|
+
const glbBase64 = Buffer.from(glbDataArray).toString("base64");
|
|
164
|
+
|
|
165
|
+
// 2. Launch Puppeteer Headless to Render using Three.js
|
|
166
|
+
browser = await puppeteer.launch({
|
|
167
|
+
headless: true,
|
|
168
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"],
|
|
169
|
+
});
|
|
170
|
+
const page = await browser.newPage();
|
|
171
|
+
|
|
172
|
+
// Provide an importmap so Puppeteer can load three.js modules cleanly
|
|
173
|
+
const html = `
|
|
174
|
+
<!DOCTYPE html>
|
|
175
|
+
<html>
|
|
176
|
+
<head>
|
|
177
|
+
<style>body{margin:0; overflow:hidden;}</style>
|
|
178
|
+
<script type="importmap">
|
|
179
|
+
{
|
|
180
|
+
"imports": {
|
|
181
|
+
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
|
|
182
|
+
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
</script>
|
|
186
|
+
</head>
|
|
187
|
+
<body></body>
|
|
188
|
+
</html>
|
|
189
|
+
`;
|
|
190
|
+
await page.goto(`data:text/html,${encodeURIComponent(html)}`);
|
|
191
|
+
|
|
192
|
+
// 3. Inject rendering logic and return base64 snapshots
|
|
193
|
+
const snapshots = await page.evaluate(
|
|
194
|
+
async (base64Glb, angles, targetAnimTime, targetAnimIndex) => {
|
|
195
|
+
const THREE = await import("three");
|
|
196
|
+
const { GLTFLoader } =
|
|
197
|
+
await import("three/addons/loaders/GLTFLoader.js");
|
|
198
|
+
const { RoomEnvironment } =
|
|
199
|
+
await import("three/addons/environments/RoomEnvironment.js");
|
|
200
|
+
|
|
201
|
+
const width = 512;
|
|
202
|
+
const height = 512;
|
|
203
|
+
const renderer = new THREE.WebGLRenderer({
|
|
204
|
+
antialias: true,
|
|
205
|
+
alpha: false,
|
|
206
|
+
preserveDrawingBuffer: true,
|
|
207
|
+
});
|
|
208
|
+
renderer.setSize(width, height);
|
|
209
|
+
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
210
|
+
document.body.appendChild(renderer.domElement);
|
|
211
|
+
|
|
212
|
+
const scene = new THREE.Scene();
|
|
213
|
+
scene.background = new THREE.Color(0x222222);
|
|
214
|
+
|
|
215
|
+
const pmremGenerator = new THREE.PMREMGenerator(renderer);
|
|
216
|
+
scene.environment = pmremGenerator.fromScene(
|
|
217
|
+
new RoomEnvironment(),
|
|
218
|
+
0.04,
|
|
219
|
+
).texture;
|
|
220
|
+
|
|
221
|
+
// Soft global ambient light
|
|
222
|
+
const ambientLight = new THREE.AmbientLight(0xffffff, 0.3);
|
|
223
|
+
scene.add(ambientLight);
|
|
224
|
+
|
|
225
|
+
// Hemisphere light for natural sky/ground illumination
|
|
226
|
+
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.4);
|
|
227
|
+
hemiLight.position.set(0, 20, 0);
|
|
228
|
+
scene.add(hemiLight);
|
|
229
|
+
|
|
230
|
+
const camera = new THREE.PerspectiveCamera(
|
|
231
|
+
45,
|
|
232
|
+
width / height,
|
|
233
|
+
0.1,
|
|
234
|
+
1000,
|
|
235
|
+
);
|
|
236
|
+
scene.add(camera);
|
|
237
|
+
|
|
238
|
+
// 3-Point Lighting Rig attached to camera for consistent illumination from all angles
|
|
239
|
+
|
|
240
|
+
// Key Light (Main illumination from top-right)
|
|
241
|
+
const keyLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
242
|
+
keyLight.position.set(10, 10, 5);
|
|
243
|
+
camera.add(keyLight);
|
|
244
|
+
camera.add(keyLight.target);
|
|
245
|
+
keyLight.target.position.set(0, 0, -5);
|
|
246
|
+
|
|
247
|
+
// Fill Light (Softer light from bottom-left to reduce shadows)
|
|
248
|
+
const fillLight = new THREE.DirectionalLight(0xd0e0ff, 0.6);
|
|
249
|
+
fillLight.position.set(-10, -2, 5);
|
|
250
|
+
camera.add(fillLight);
|
|
251
|
+
camera.add(fillLight.target);
|
|
252
|
+
fillLight.target.position.set(0, 0, -5);
|
|
253
|
+
|
|
254
|
+
// Rim Light (Highlight edges from behind the object)
|
|
255
|
+
const rimLight = new THREE.DirectionalLight(0xffeedd, 0.8);
|
|
256
|
+
rimLight.position.set(0, 10, -15);
|
|
257
|
+
camera.add(rimLight);
|
|
258
|
+
camera.add(rimLight.target);
|
|
259
|
+
rimLight.target.position.set(0, 0, -5);
|
|
260
|
+
|
|
261
|
+
// Load the model from base64
|
|
262
|
+
const loader = new GLTFLoader();
|
|
263
|
+
const dataUrl = "data:application/octet-stream;base64," + base64Glb;
|
|
264
|
+
const gltf = await loader.loadAsync(dataUrl);
|
|
265
|
+
scene.add(gltf.scene);
|
|
266
|
+
|
|
267
|
+
// --- Apply Animation State if requested ---
|
|
268
|
+
let appliedAnim = false;
|
|
269
|
+
if (gltf.animations && gltf.animations.length > 0) {
|
|
270
|
+
const mixer = new THREE.AnimationMixer(gltf.scene);
|
|
271
|
+
// Clamp index to available animations
|
|
272
|
+
const clipIndex = Math.min(
|
|
273
|
+
Math.max(targetAnimIndex, 0),
|
|
274
|
+
gltf.animations.length - 1,
|
|
275
|
+
);
|
|
276
|
+
const clip = gltf.animations[clipIndex];
|
|
277
|
+
|
|
278
|
+
if (clip) {
|
|
279
|
+
const action = mixer.clipAction(clip);
|
|
280
|
+
action.play();
|
|
281
|
+
// Advance the mixer exactly to the requested time.
|
|
282
|
+
// If targetAnimTime > duration, modulo it so it loops naturally like a real animation.
|
|
283
|
+
const duration = clip.duration;
|
|
284
|
+
const finalTime = duration > 0 ? targetAnimTime % duration : 0;
|
|
285
|
+
|
|
286
|
+
mixer.setTime(finalTime);
|
|
287
|
+
appliedAnim = true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Wait for matrices to update properly after animation applied
|
|
292
|
+
scene.updateMatrixWorld(true);
|
|
293
|
+
|
|
294
|
+
// Calculate bounding box to find initial anchor center and scale
|
|
295
|
+
const box = new THREE.Box3().setFromObject(gltf.scene);
|
|
296
|
+
if (box.isEmpty()) {
|
|
297
|
+
box.setFromCenterAndSize(
|
|
298
|
+
new THREE.Vector3(0, 0, 0),
|
|
299
|
+
new THREE.Vector3(1, 1, 1),
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const center = box.getCenter(new THREE.Vector3());
|
|
303
|
+
const size = box.getSize(new THREE.Vector3());
|
|
304
|
+
const initialDim = Math.max(size.x, size.y, size.z) || 10;
|
|
305
|
+
|
|
306
|
+
// Extract all world-space vertices
|
|
307
|
+
const vertices = [];
|
|
308
|
+
const v3 = new THREE.Vector3();
|
|
309
|
+
gltf.scene.traverse((child) => {
|
|
310
|
+
if (
|
|
311
|
+
child.isMesh &&
|
|
312
|
+
child.geometry &&
|
|
313
|
+
child.geometry.attributes.position
|
|
314
|
+
) {
|
|
315
|
+
const pos = child.geometry.attributes.position;
|
|
316
|
+
const matrix = child.matrixWorld;
|
|
317
|
+
for (let i = 0; i < pos.count; i++) {
|
|
318
|
+
v3.fromBufferAttribute(pos, i).applyMatrix4(matrix);
|
|
319
|
+
vertices.push(v3.x, v3.y, v3.z);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (vertices.length === 0) {
|
|
325
|
+
vertices.push(center.x, center.y, center.z);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const fovY = (camera.fov * Math.PI) / 180;
|
|
329
|
+
const tanY = Math.tan(fovY / 2);
|
|
330
|
+
const tanX = tanY * camera.aspect;
|
|
331
|
+
|
|
332
|
+
const results = [];
|
|
333
|
+
for (const angle of angles) {
|
|
334
|
+
const dir = new THREE.Vector3();
|
|
335
|
+
switch (angle.toLowerCase()) {
|
|
336
|
+
case "front":
|
|
337
|
+
dir.set(0, 0, 1);
|
|
338
|
+
break;
|
|
339
|
+
case "back":
|
|
340
|
+
dir.set(0, 0, -1);
|
|
341
|
+
break;
|
|
342
|
+
case "left":
|
|
343
|
+
dir.set(-1, 0, 0);
|
|
344
|
+
break;
|
|
345
|
+
case "right":
|
|
346
|
+
dir.set(1, 0, 0);
|
|
347
|
+
break;
|
|
348
|
+
case "top":
|
|
349
|
+
dir.set(0, 1, 0);
|
|
350
|
+
break;
|
|
351
|
+
case "bottom":
|
|
352
|
+
dir.set(0, -1, 0);
|
|
353
|
+
break;
|
|
354
|
+
case "isometric":
|
|
355
|
+
default:
|
|
356
|
+
dir.set(1, 1, 1).normalize();
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let viewCenter = center.clone();
|
|
361
|
+
let distance = initialDim * 2.5;
|
|
362
|
+
|
|
363
|
+
// Iterate to converge perspective NDC screen bounds to exact center and margin
|
|
364
|
+
for (let iter = 0; iter < 5; iter++) {
|
|
365
|
+
camera.near = Math.max(0.01, initialDim * 0.01);
|
|
366
|
+
camera.far = Math.max(1000, distance * 10);
|
|
367
|
+
camera.updateProjectionMatrix();
|
|
368
|
+
|
|
369
|
+
camera.position
|
|
370
|
+
.copy(viewCenter)
|
|
371
|
+
.add(dir.clone().multiplyScalar(distance));
|
|
372
|
+
camera.lookAt(viewCenter);
|
|
373
|
+
camera.updateMatrixWorld();
|
|
374
|
+
camera.matrixWorldInverse.copy(camera.matrixWorld).invert();
|
|
375
|
+
|
|
376
|
+
const pvMatrix = new THREE.Matrix4().multiplyMatrices(
|
|
377
|
+
camera.projectionMatrix,
|
|
378
|
+
camera.matrixWorldInverse,
|
|
379
|
+
);
|
|
380
|
+
const pme = pvMatrix.elements;
|
|
381
|
+
|
|
382
|
+
let minNdcX = Infinity,
|
|
383
|
+
maxNdcX = -Infinity;
|
|
384
|
+
let minNdcY = Infinity,
|
|
385
|
+
maxNdcY = -Infinity;
|
|
386
|
+
|
|
387
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
388
|
+
const x = vertices[i];
|
|
389
|
+
const y = vertices[i + 1];
|
|
390
|
+
const z = vertices[i + 2];
|
|
391
|
+
|
|
392
|
+
const nx = x * pme[0] + y * pme[4] + z * pme[8] + pme[12];
|
|
393
|
+
const ny = x * pme[1] + y * pme[5] + z * pme[9] + pme[13];
|
|
394
|
+
const nw = x * pme[3] + y * pme[7] + z * pme[11] + pme[15];
|
|
395
|
+
|
|
396
|
+
if (nw > 0) {
|
|
397
|
+
const ndcX = nx / nw;
|
|
398
|
+
const ndcY = ny / nw;
|
|
399
|
+
if (ndcX < minNdcX) minNdcX = ndcX;
|
|
400
|
+
if (ndcX > maxNdcX) maxNdcX = ndcX;
|
|
401
|
+
if (ndcY < minNdcY) minNdcY = ndcY;
|
|
402
|
+
if (ndcY > maxNdcY) maxNdcY = ndcY;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (minNdcX === Infinity) break;
|
|
407
|
+
|
|
408
|
+
const midNdcX = (minNdcX + maxNdcX) / 2;
|
|
409
|
+
const midNdcY = (minNdcY + maxNdcY) / 2;
|
|
410
|
+
const spanX = (maxNdcX - minNdcX) / 2;
|
|
411
|
+
const spanY = (maxNdcY - minNdcY) / 2;
|
|
412
|
+
|
|
413
|
+
const worldShift = new THREE.Vector3(
|
|
414
|
+
midNdcX * distance * tanX,
|
|
415
|
+
midNdcY * distance * tanY,
|
|
416
|
+
0,
|
|
417
|
+
).applyQuaternion(camera.quaternion);
|
|
418
|
+
|
|
419
|
+
viewCenter.add(worldShift);
|
|
420
|
+
const maxSpan = Math.max(spanX, spanY);
|
|
421
|
+
distance = Math.max(distance * maxSpan * 1.08, camera.near + 0.1);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
camera.near = Math.max(0.01, initialDim * 0.01);
|
|
425
|
+
camera.far = Math.max(1000, distance * 10);
|
|
426
|
+
camera.updateProjectionMatrix();
|
|
427
|
+
|
|
428
|
+
// Final render with converged camera setup
|
|
429
|
+
camera.position
|
|
430
|
+
.copy(viewCenter)
|
|
431
|
+
.add(dir.clone().multiplyScalar(distance));
|
|
432
|
+
camera.lookAt(viewCenter);
|
|
433
|
+
camera.updateMatrixWorld();
|
|
434
|
+
|
|
435
|
+
renderer.render(scene, camera);
|
|
436
|
+
|
|
437
|
+
const b64 = renderer.domElement
|
|
438
|
+
.toDataURL("image/png")
|
|
439
|
+
.split(",")[1];
|
|
440
|
+
results.push({ name: angle, data: b64, appliedAnim });
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return results;
|
|
444
|
+
},
|
|
445
|
+
glbBase64,
|
|
446
|
+
requestedAngles,
|
|
447
|
+
animTime,
|
|
448
|
+
animIndex,
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
// 4. Close browser
|
|
452
|
+
await browser.close();
|
|
453
|
+
|
|
454
|
+
// 5. Construct the MCP Response
|
|
455
|
+
const appliedAnimStr = snapshots[0].appliedAnim
|
|
456
|
+
? ` at animation time ${animTime}s (Track ${animIndex})`
|
|
457
|
+
: ` (Static Model)`;
|
|
458
|
+
|
|
459
|
+
const content = [
|
|
460
|
+
{
|
|
461
|
+
type: "text",
|
|
462
|
+
text: `Successfully compiled SCAD and rendered ${snapshots.length} camera angle(s)${appliedAnimStr}. Please analyze these visual results to determine your next adjustments.`,
|
|
463
|
+
},
|
|
464
|
+
];
|
|
465
|
+
|
|
466
|
+
for (const snap of snapshots) {
|
|
467
|
+
const angleName =
|
|
468
|
+
snap.name.charAt(0).toUpperCase() + snap.name.slice(1);
|
|
469
|
+
content.push({
|
|
470
|
+
type: "text",
|
|
471
|
+
text: `${angleName} View:`,
|
|
472
|
+
});
|
|
473
|
+
content.push({
|
|
474
|
+
type: "image",
|
|
475
|
+
data: snap.data,
|
|
476
|
+
mimeType: "image/png",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return { content };
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (browser) await browser.close();
|
|
483
|
+
return {
|
|
484
|
+
content: [
|
|
485
|
+
{
|
|
486
|
+
type: "text",
|
|
487
|
+
text: `Failed to compile or render OpenSCAD model. Error: ${error.message}\nIf this is a syntax error, review your OpenSCAD code and try again.`,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
isError: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
throw new Error(`Tool not found: ${name}`);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// Run Server
|
|
499
|
+
async function main() {
|
|
500
|
+
const transport = new StdioServerTransport();
|
|
501
|
+
await server.connect(transport);
|
|
502
|
+
console.error("🚀 SCAD MCP Server running on stdio");
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
main().catch((error) => {
|
|
506
|
+
console.error("Server error:", error);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
});
|