Documentation is work in progress and not completeEditor
3D System

Extending the 3D System

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.

Adding a New Geometry Type

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().

Adding a New Material Property

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 })}
/>

Adding a New File Format

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.