Three common extensions, each touching a predictable set of files. The pattern is always the same: widen the type, add the default, implement the behaviour, then surface it in the UI.
Step 1 — add the type to the GeometryType union in src/3d/types.ts:
// Before
export type GeometryType = 'box' | 'sphere' | 'cylinder' | 'torus' | 'cone' | 'capsule';
// After
export type GeometryType = 'box' | 'sphere' | 'cylinder' | 'torus' | 'cone' | 'capsule' | 'octahedron';Step 2 — add any new geometry-specific parameters to GeometryConfig and update DEFAULT_GEOMETRY_CONFIG with defaults.
Step 3 — add a case to createPrimitiveGeometry() in GeometryFactory.ts:
case 'octahedron':
return new THREE.OctahedronGeometry(config.radius, config.segments);Step 4 — add an entry to the SHAPES array in ThreeDShapePicker.tsx, with a matching SVG icon component in the same file.
{
type: 'octahedron',
label: 'Octahedron',
description: 'An eight-faced polyhedron.',
icon: <ShapeIconOctahedron />,
},Step 5 — add geometry controls to ThreeDPropertiesPanel.tsx inside the geometry section conditional.
Step 6 — SceneManager.updateGeometry() needs no change; it already routes through createPrimitiveGeometry().
Step 1 — add the property to the MaterialConfig interface in types.ts, and Step 2 — give it a default in DEFAULT_MATERIAL_CONFIG.
Step 3 — apply it in createMaterial() in MaterialSystem.ts, inside the branch for the material types it applies to:
if (config.type === 'physical') {
const physConfig = {
// ... existing
anisotropy: config.anisotropyStrength,
};
}Step 4 — add a UI control in ThreeDPropertiesPanel.tsx, inside the conditional block for that material type:
<SliderRow
label="Anisotropy"
value={mat.anisotropyStrength}
min={0} max={1} step={0.01}
onChange={v => handleMaterial({ anisotropyStrength: v })}
/>Step 1 — import the loader from three/examples/jsm/loaders/, or install it if needed.
Step 2 — add a lazy-initialized loader getter in ModelLoader.ts:
import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';
let threeMFLoader: ThreeMFLoader | null = null;
function getThreeMFLoader(): ThreeMFLoader {
if (!threeMFLoader) threeMFLoader = new ThreeMFLoader();
return threeMFLoader;
}Step 3 — add a case to the switch in loadModel(), and Step 4 — add the extension to SUPPORTED_EXTENSIONS.
case '3mf': {
object = await getThreeMFLoader().loadAsync(url);
break;
}
const SUPPORTED_EXTENSIONS = ['glb', 'gltf', 'obj', 'fbx', 'stl', '3mf'];Step 5 — update the file input accept attribute in ThreeDShapePicker.tsx and the label text below the import button.
Step 6 — if the loader returns a geometry rather than a scene, as STL does, wrap it in a THREE.Mesh with a default material before returning.