/**
* Scene Manager — Tracks 3D objects and generates OpenSCAD code
*/
import { v4 as uuid } from 'uuid';
export type Vec3 = [number, number, number];
export interface SceneObject {
id: string;
name: string;
type: 'primitive' | 'boolean' | 'transform' | 'extrude' | 'text3d' | 'import';
scadCode: string;
children: string[]; // child object IDs
metadata: Record<string, unknown>;
createdAt: number;
}
export class Scene {
private objects = new Map<string, SceneObject>();
private rootObjects: string[] = []; // top-level objects (not children of others)
addObject(obj: Omit<SceneObject, 'id' | 'createdAt'>): SceneObject {
const id = uuid().slice(0, 8);
const sceneObj: SceneObject = {
...obj,
id,
createdAt: Date.now(),
};
this.objects.set(id, sceneObj);
// Remove children from root list
for (const childId of obj.children) {
const idx = this.rootObjects.indexOf(childId);
if (idx !== -1) this.rootObjects.splice(idx, 1);
}
this.rootObjects.push(id);
return sceneObj;
}
getObject(id: string): SceneObject | undefined {
return this.objects.get(id);
}
removeObject(id: string): boolean {
const obj = this.objects.get(id);
if (!obj) return false;
// Remove from root list
const idx = this.rootObjects.indexOf(id);
if (idx !== -1) this.rootObjects.splice(idx, 1);
// Restore children to root if they exist
for (const childId of obj.children) {
if (this.objects.has(childId)) {
this.rootObjects.push(childId);
}
}
this.objects.delete(id);
return true;
}
listObjects(): SceneObject[] {
return Array.from(this.objects.values());
}
clear(): void {
this.objects.clear();
this.rootObjects.length = 0;
}
/**
* Generate complete OpenSCAD code for the scene
*/
toScad(): string {
if (this.rootObjects.length === 0) return '// Empty scene\n';
const lines: string[] = [
'// Generated by 3d-mcp-server',
`// Objects: ${this.objects.size}`,
`// Generated at: ${new Date().toISOString()}`,
'',
];
for (const rootId of this.rootObjects) {
lines.push(this.resolveScad(rootId));
lines.push('');
}
return lines.join('\n');
}
/**
* Generate OpenSCAD for a specific object (resolving children)
*/
private resolveScad(id: string): string {
const obj = this.objects.get(id);
if (!obj) return `// Missing object: ${id}`;
return obj.scadCode;
}
get objectCount(): number {
return this.objects.size;
}
get rootCount(): number {
return this.rootObjects.length;
}
}
// --- Primitive generators ---
export function cubeScad(size: Vec3, center = true): string {
return `cube([${size.join(', ')}], center = ${center});`;
}
export function sphereScad(radius: number, fn = 32): string {
return `sphere(r = ${radius}, $fn = ${fn});`;
}
export function cylinderScad(height: number, radius: number, radius2?: number, fn = 32, center = true): string {
if (radius2 !== undefined && radius2 !== radius) {
return `cylinder(h = ${height}, r1 = ${radius}, r2 = ${radius2}, center = ${center}, $fn = ${fn});`;
}
return `cylinder(h = ${height}, r = ${radius}, center = ${center}, $fn = ${fn});`;
}
export function coneScad(height: number, radius: number, fn = 32, center = true): string {
return cylinderScad(height, radius, 0, fn, center);
}
export function torusScad(majorRadius: number, minorRadius: number, fn = 32): string {
return `rotate_extrude($fn = ${fn}) translate([${majorRadius}, 0, 0]) circle(r = ${minorRadius}, $fn = ${fn});`;
}
export function polyhedronScad(points: Vec3[], faces: number[][]): string {
const pts = points.map(p => `[${p.join(', ')}]`).join(',\n ');
const fcs = faces.map(f => `[${f.join(', ')}]`).join(',\n ');
return `polyhedron(\n points = [\n ${pts}\n ],\n faces = [\n ${fcs}\n ]\n);`;
}
// --- Text ---
export function text3dScad(text: string, size = 10, height = 2, font = 'Liberation Sans', fn = 32): string {
return `linear_extrude(height = ${height}, $fn = ${fn})\n text("${text}", size = ${size}, font = "${font}", halign = "center", valign = "center");`;
}
// --- Transforms ---
export function translateScad(offset: Vec3, childCode: string): string {
return `translate([${offset.join(', ')}])\n ${childCode.replace(/\n/g, '\n ')}`;
}
export function rotateScad(angles: Vec3, childCode: string): string {
return `rotate([${angles.join(', ')}])\n ${childCode.replace(/\n/g, '\n ')}`;
}
export function scaleScad(factors: Vec3, childCode: string): string {
return `scale([${factors.join(', ')}])\n ${childCode.replace(/\n/g, '\n ')}`;
}
export function mirrorScad(axis: Vec3, childCode: string): string {
return `mirror([${axis.join(', ')}])\n ${childCode.replace(/\n/g, '\n ')}`;
}
export function colorScad(color: string, alpha: number, childCode: string): string {
return `color("${color}", alpha = ${alpha})\n ${childCode.replace(/\n/g, '\n ')}`;
}
// --- Boolean operations ---
export function booleanScad(op: 'union' | 'difference' | 'intersection', childrenCode: string[]): string {
const body = childrenCode.map(c => ` ${c.replace(/\n/g, '\n ')}`).join('\n');
return `${op}() {\n${body}\n}`;
}
// --- Extrude ---
export function linearExtrudeScad(height: number, twist = 0, scale = 1, fn = 32, childCode: string): string {
return `linear_extrude(height = ${height}, twist = ${twist}, scale = ${scale}, $fn = ${fn})\n ${childCode.replace(/\n/g, '\n ')}`;
}
export function rotateExtrudeScad(angle = 360, fn = 32, childCode: string): string {
return `rotate_extrude(angle = ${angle}, $fn = ${fn})\n ${childCode.replace(/\n/g, '\n ')}`;
}
// --- 2D shapes for extrusion ---
export function circleScad(radius: number, fn = 32): string {
return `circle(r = ${radius}, $fn = ${fn});`;
}
export function squareScad(size: [number, number], center = true): string {
return `square([${size.join(', ')}], center = ${center});`;
}
export function polygonScad(points: [number, number][]): string {
const pts = points.map(p => `[${p.join(', ')}]`).join(', ');
return `polygon(points = [${pts}]);`;
}