Create and modify 3D parametric models in the threedee design environment. Use when: (1) creating new 3D objects, geometries, or meshes, (2) modifying parameters like dimensions, colors, materials, (3) adding primitives (boxes, spheres, cylinders, torus knots), (4) setting up lighting or camera, (5) exporting models to STL/OBJ/glTF, (6) working with Three.js code in this project. Triggers on requests involving 3D modeling, CAD-like operations, parametric design, or mesh creation.
A live-updating 3D viewport for parametric modeling with AI assistance. Edit code in VS Code, see results instantly in the browser.
threedee/
├── src/
│ ├── main.js # Main scene, camera, lighting, model loader
│ └── exporters.js # STL/OBJ/glTF export utilities
├── user/
│ ├── models/ # User's parametric model definitions (auto-discovered!)
│ └── exports/ # Generated mesh files (STL, OBJ, glTF)
└── index.html # Viewport UI with model selector
Models are automatically discovered! Simply create a new .js file in user/models/ and it will appear in the UI dropdown after a page refresh.
Create a new file in user/models/ with a descriptive kebab-case name (e.g., my-bracket.js).
The filename becomes the model ID and is converted to Title Case in the UI.
Every model file must export a createMesh() function:
// user/models/my-bracket.js
import * as THREE from 'three';
export const parameters = {
width: 40,
height: 20,
thickness: 5,
color: 0x4a90d9,
};
export function createMesh(userParams = {}) {
const params = { ...parameters, ...userParams };
const geometry = new THREE.BoxGeometry(
params.width,
params.height,
params.thickness
);
const material = new THREE.MeshStandardMaterial({ color: params.color });
const mesh = new THREE.Mesh(geometry, material);
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.position.y = params.height / 2; // Sit on ground plane
return mesh;
}
const geometry = new THREE.BoxGeometry(2, 1, 3); // width, height, depth
const geometry = new THREE.SphereGeometry(1, 32, 32); // radius, segments
const geometry = new THREE.CylinderGeometry(1, 1, 2, 32); // topR, bottomR, height
const geometry = new THREE.TorusGeometry(1, 0.4, 16, 100); // radius, tube, segments
const geometry = new THREE.TorusKnotGeometry(1, 0.35, 128, 32);
Change object color:
material.color.setHex(0x22d3ee); // cyan accent
Position object:
mesh.position.y = 1.5; // lift above ground
mesh.position.set(0, 1.5, 0); // x, y, z
Scale object:
mesh.scale.set(2, 1, 1); // stretch in x
Rotate object:
mesh.rotation.y = Math.PI / 4; // 45 degrees
For reusable models, always merge with default parameters to handle partial updates correctly:
export const parameters = {
width: 40,
height: 20,
depth: 40,
color: 0x4a90d9,
};
export function createGeometry(userParams = {}) {
const params = { ...parameters, ...userParams };
return new THREE.BoxGeometry(params.width, params.height, params.depth);
}
export function createMesh(userParams = {}) {
const params = { ...parameters, ...userParams };
const geometry = createGeometry(params);
const material = new THREE.MeshStandardMaterial({ color: params.color });
return new THREE.Mesh(geometry, material);
}
When combining different geometry types (e.g., mixing BoxGeometry with ExtrudeGeometry), they often have incompatible index buffers which causes mergeGeometries to fail.
Always convert to non-indexed before merging to ensure compatibility:
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
// ... create geometries ...
// Convert all to non-indexed before merging to avoid attribute/index conflicts
const compatibleGeometries = [geo1, geo2, geo3].map(g => g.toNonIndexed());
const merged = mergeGeometries(compatibleGeometries);
// Optional: Weld vertices afterwards if smooth shading is needed
// import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
// const smooth = mergeVertices(merged);
Export the active 3D model to various formats using the browser console or programmatically:
// STL (3D printing)
threedeeExport.stl(mesh, "my-part") // Binary (recommended)
threedeeExport.stlAscii(mesh, "my-part") // ASCII text format
// General interchange
threedeeExport.obj(mesh, "my-part") // OBJ with UVs and normals
// Web and AR
threedeeExport.gltf(mesh, "my-part") // glTF (JSON)
threedeeExport.glb(mesh, "my-part") // GLB (binary, compact)
// CAD interchange
threedeeExport.step(mesh, "my-part") // STEP Faceted B-Rep
// Export to STL, OBJ, glTF, and STEP simultaneously
await threedeeExport.all(mesh, "my-part")
// Export to specific formats
await threedeeExport.all(mesh, "my-part", ['stl', 'step', 'obj'])
my-part_2025-12-30_23-02-15.stlmesh variable is always available in the console and accessible via window.meshkeyLight.intensity = 1.5; // brighter
keyLight.position.set(10, 15, 10); // move light
rimLight.color.setHex(0xff6600); // orange rim
When creating complex procedural shapes (like gyroids or lattices) intended for 3D printing, follow these best practices to ensure "watertight" models and high-quality exports.
If using MarchingCubes, configure it for high resolution and clean boundaries:
maxPolyCount (e.g., 1,000,000) for high-resolution meshes to prevent model sections from being "cut off" due to buffer limits.Math.max for a cylinder boundary), use a multiplier (e.g., * 5 or * 10) for the boundary distance to create a smooth gradient that the interpolator can follow.mergeVertices from BufferGeometryUtils after mc.update() to weld coincident vertices. This fixes "cracks" and "open edges" that slicers often report.import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
// ... inside createMesh ...
const mc = new MarchingCubes(resolution, material, false, false, 1000000);
// ... fill field ...
mc.update();
// Weld and recompute normals
const welded = mergeVertices(mc.geometry, 1e-5);
welded.computeVertexNormals();
mc.geometry.dispose();
mc.geometry = welded;
The src/exporters.js utilities include an automated preparation step for STL exports that handles:
IMPORTANT: When asked to improve, refine, or iterate on an existing model:
user/models/tree.js)tree-v2.js, tree-improved.js, realistic-tree.jsOnly create a new file when the user explicitly asks for a new, separate model.
user/models/User says: "Make the tree more realistic"
Action: Edit user/models/tree.js to improve the existing code
User says: "Add more branches and better textures"
Action: Edit user/models/tree.js to add the requested features
User says: "Create a pine tree (separate from the oak tree)"
Action: Create a new file user/models/pine-tree.js
Models can also be loaded programmatically:
// Load a model by ID (filename without .js)
await loadModel('shelf-bracket');
// List all available models
console.log(modelList);
// Access current mesh
window.mesh
http://localhost:3002 (or next available port)user/models/*.js — no manual registration needed!my-widget.js) and display as Title Case in the UIy > 0 to sit above the gridmesh variable is exported and accessible in console via window.meshFor advanced geometry and CSG operations, see references/geometry.md.