Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
16 KiB
name, description
| name | description |
|---|---|
| threejs-geometry | Three.js geometry creation and manipulation adapted for React Three Fiber and Suaveplan R3F packages. Covers declarative mesh/geometry JSX, BufferGeometry with React refs, InstancedMesh patterns for high-performance rendering, custom geometry construction, and @react-three/drei geometry helpers. |
This skill covers geometry creation and manipulation in Three.js within the React Three Fiber (R3F) context used by the Suaveplan monorepo. All code targets React 19 + TypeScript + R3F and follows Suaveplan CLAUDE.md conventions.
The user needs to create, modify, or optimize 3D geometry in an R3F scene. This includes built-in primitives, custom BufferGeometry, instanced rendering, and parametric shapes.
Core Concepts
Three.js Geometry System
Three.js geometry is based on BufferGeometry, which stores vertex data in typed arrays:
- position:
Float32Arrayof[x, y, z]per vertex - normal:
Float32Arrayof[nx, ny, nz]per vertex - uv:
Float32Arrayof[u, v]per vertex - index:
Uint16ArrayorUint32Arrayof triangle vertex indices - color:
Float32Arrayof[r, g, b]per vertex (optional)
R3F Geometry Approach
In R3F, geometries are declared as JSX children of <mesh>:
| Three.js (Imperative) | R3F (Declarative) |
|---|---|
new BoxGeometry(1, 1, 1) |
<boxGeometry args={[1, 1, 1]} /> |
mesh.geometry = geometry |
<mesh><boxGeometry /></mesh> |
geometry.setAttribute(...) |
Via ref or attach prop |
new InstancedMesh(g, m, n) |
<instancedMesh args={[null, null, n]}> |
Suaveplan R3F Packages
| Package | Purpose | Import |
|---|---|---|
@suaveplan/r3f |
Core utilities, geometry helpers | import { useR3F } from '@suaveplan/r3f' |
@suaveplan/r3f-performance |
Geometry LOD, draw call monitoring | import { usePerformanceMonitor } from '@suaveplan/r3f-performance' |
Built-in Geometry Primitives
All Three.js geometry classes are available as lowercase JSX elements. Constructor arguments pass via args.
Box
<mesh>
<boxGeometry args={[
1, // width
1, // height
1, // depth
1, // widthSegments (optional)
1, // heightSegments (optional)
1, // depthSegments (optional)
]} />
<meshStandardMaterial color="tomato" />
</mesh>
Sphere
<mesh>
<sphereGeometry args={[
0.5, // radius
32, // widthSegments
32, // heightSegments
0, // phiStart (optional)
Math.PI * 2, // phiLength (optional)
0, // thetaStart (optional)
Math.PI, // thetaLength (optional)
]} />
<meshStandardMaterial color="dodgerblue" />
</mesh>
Common Primitives Reference
| Geometry | JSX | Key Args |
|---|---|---|
| Box | <boxGeometry args={[w, h, d]} /> |
width, height, depth |
| Sphere | <sphereGeometry args={[r, wSeg, hSeg]} /> |
radius, segments |
| Cylinder | <cylinderGeometry args={[rTop, rBot, h, rSeg]} /> |
radii, height, segments |
| Cone | <coneGeometry args={[r, h, rSeg]} /> |
radius, height |
| Torus | <torusGeometry args={[r, tube, rSeg, tSeg]} /> |
radius, tube radius |
| Plane | <planeGeometry args={[w, h, wSeg, hSeg]} /> |
width, height |
| Ring | <ringGeometry args={[inner, outer, tSeg]} /> |
inner/outer radius |
| Circle | <circleGeometry args={[r, seg]} /> |
radius, segments |
| TorusKnot | <torusKnotGeometry args={[r, tube, tSeg, rSeg, p, q]} /> |
radius, tube, knot params |
| Dodecahedron | <dodecahedronGeometry args={[r, detail]} /> |
radius, detail level |
| Icosahedron | <icosahedronGeometry args={[r, detail]} /> |
radius, detail level |
| Octahedron | <octahedronGeometry args={[r, detail]} /> |
radius, detail level |
| Tetrahedron | <tetrahedronGeometry args={[r, detail]} /> |
radius, detail level |
Custom BufferGeometry
Declarative with Refs
import { useRef, useEffect, useMemo } from 'react';
import { BufferAttribute } from 'three';
import type { BufferGeometry } from 'three';
interface CustomTriangleProps {
size?: number;
color?: string;
}
export function CustomTriangle({ size = 1, color = 'limegreen' }: CustomTriangleProps) {
const geometryRef = useRef<BufferGeometry>(null);
const { positions, normals, uvs } = useMemo(() => {
const s = size;
return {
positions: new Float32Array([
-s, -s, 0, // bottom-left
s, -s, 0, // bottom-right
0, s, 0, // top-center
]),
normals: new Float32Array([
0, 0, 1,
0, 0, 1,
0, 0, 1,
]),
uvs: new Float32Array([
0, 0,
1, 0,
0.5, 1,
]),
};
}, [size]);
return (
<mesh>
<bufferGeometry ref={geometryRef}>
<bufferAttribute
attach="attributes-position"
args={[positions, 3]}
/>
<bufferAttribute
attach="attributes-normal"
args={[normals, 3]}
/>
<bufferAttribute
attach="attributes-uv"
args={[uvs, 2]}
/>
</bufferGeometry>
<meshStandardMaterial color={color} side={2} />
</mesh>
);
}
Indexed Geometry
For geometry with shared vertices (reduces memory and improves performance):
import { useMemo } from 'react';
export function IndexedQuad({ size = 1 }: { size?: number }) {
const { positions, indices, uvs } = useMemo(() => {
const s = size / 2;
return {
positions: new Float32Array([
-s, -s, 0, // 0: bottom-left
s, -s, 0, // 1: bottom-right
s, s, 0, // 2: top-right
-s, s, 0, // 3: top-left
]),
indices: new Uint16Array([
0, 1, 2, // first triangle
0, 2, 3, // second triangle
]),
uvs: new Float32Array([
0, 0,
1, 0,
1, 1,
0, 1,
]),
};
}, [size]);
return (
<mesh>
<bufferGeometry>
<bufferAttribute attach="attributes-position" args={[positions, 3]} />
<bufferAttribute attach="attributes-uv" args={[uvs, 2]} />
<bufferAttribute attach="index" args={[indices, 1]} />
</bufferGeometry>
<meshStandardMaterial color="coral" />
</mesh>
);
}
Dynamic Geometry Updates
Animating Vertices in useFrame
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import type { BufferGeometry } from 'three';
export function WavingPlane({ width = 5, segments = 32 }: { width?: number; segments?: number }) {
const geometryRef = useRef<BufferGeometry>(null);
useFrame(({ clock }) => {
if (!geometryRef.current) return;
const positions = geometryRef.current.attributes.position;
const time = clock.elapsedTime;
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const z = positions.getZ(i);
const y = Math.sin(x * 2 + time) * 0.3 + Math.cos(z * 2 + time * 0.7) * 0.2;
positions.setY(i, y);
}
positions.needsUpdate = true;
geometryRef.current.computeVertexNormals();
});
return (
<mesh rotation={[-Math.PI / 2, 0, 0]}>
<planeGeometry ref={geometryRef} args={[width, width, segments, segments]} />
<meshStandardMaterial color="steelblue" wireframe />
</mesh>
);
}
InstancedMesh: High-Performance Rendering
InstancedMesh renders many copies of the same geometry with a single draw call. Each instance can have its own transform and color.
Basic Instancing
import { useRef, useEffect, useMemo } from 'react';
import { Object3D, Color, Matrix4 } from 'three';
import type { InstancedMesh } from 'three';
interface InstanceFieldProps {
count: number;
spread?: number;
}
export function InstanceField({ count, spread = 10 }: InstanceFieldProps) {
const meshRef = useRef<InstancedMesh>(null);
const dummy = useMemo(() => new Object3D(), []);
const color = useMemo(() => new Color(), []);
useEffect(() => {
if (!meshRef.current) return;
for (let i = 0; i < count; i++) {
dummy.position.set(
(Math.random() - 0.5) * spread,
Math.random() * 2,
(Math.random() - 0.5) * spread
);
dummy.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
0
);
dummy.scale.setScalar(0.2 + Math.random() * 0.8);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
color.setHSL(Math.random(), 0.7, 0.5);
meshRef.current.setColorAt(i, color);
}
meshRef.current.instanceMatrix.needsUpdate = true;
if (meshRef.current.instanceColor) {
meshRef.current.instanceColor.needsUpdate = true;
}
}, [count, spread, dummy, color]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial />
</instancedMesh>
);
}
Animated Instances
import { useRef, useMemo } from 'react';
import { useFrame } from '@react-three/fiber';
import { Object3D } from 'three';
import type { InstancedMesh } from 'three';
export function FloatingParticles({ count = 500 }: { count?: number }) {
const meshRef = useRef<InstancedMesh>(null);
const dummy = useMemo(() => new Object3D(), []);
const offsets = useMemo(() => {
return Array.from({ length: count }, () => ({
x: (Math.random() - 0.5) * 20,
y: Math.random() * 10,
z: (Math.random() - 0.5) * 20,
speed: 0.2 + Math.random() * 0.8,
phase: Math.random() * Math.PI * 2,
}));
}, [count]);
useFrame(({ clock }) => {
if (!meshRef.current) return;
const t = clock.elapsedTime;
for (let i = 0; i < count; i++) {
const { x, y, z, speed, phase } = offsets[i];
dummy.position.set(
x + Math.sin(t * speed + phase) * 0.5,
y + Math.sin(t * speed * 0.7 + phase) * 0.3,
z + Math.cos(t * speed + phase) * 0.5
);
dummy.scale.setScalar(0.05 + Math.sin(t * speed + phase) * 0.02);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
});
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
<sphereGeometry args={[1, 8, 8]} />
<meshBasicMaterial color="white" transparent opacity={0.6} />
</instancedMesh>
);
}
Drei Geometry Helpers
@react-three/drei provides higher-level geometry components:
RoundedBox
import { RoundedBox } from '@react-three/drei';
<RoundedBox args={[1, 1, 1]} radius={0.1} smoothness={4}>
<meshStandardMaterial color="royalblue" />
</RoundedBox>
Text3D
import { Text3D, Center } from '@react-three/drei';
<Center>
<Text3D
font="/fonts/helvetiker_regular.typeface.json"
size={0.5}
height={0.1}
bevelEnabled
bevelSize={0.02}
bevelThickness={0.01}
>
Hello R3F
<meshStandardMaterial color="gold" />
</Text3D>
</Center>
Parametric Shapes
import { Extrude, Lathe } from '@react-three/drei';
import { Shape, Vector2 } from 'three';
import { useMemo } from 'react';
export function CustomExtrusion() {
const shape = useMemo(() => {
const s = new Shape();
s.moveTo(0, 0);
s.lineTo(1, 0);
s.lineTo(1, 1);
s.lineTo(0.5, 1.5);
s.lineTo(0, 1);
s.closePath();
return s;
}, []);
return (
<Extrude args={[shape, { depth: 0.5, bevelEnabled: true, bevelSize: 0.05 }]}>
<meshStandardMaterial color="mediumpurple" />
</Extrude>
);
}
Geometry Merging for Performance
Merge static geometries to reduce draw calls:
import { useRef, useEffect } from 'react';
import { BoxGeometry, Matrix4 } from 'three';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
import type { BufferGeometry } from 'three';
export function MergedBoxes({ positions }: { positions: [number, number, number][] }) {
const geometryRef = useRef<BufferGeometry>(null);
useEffect(() => {
const geometries = positions.map(([x, y, z]) => {
const box = new BoxGeometry(1, 1, 1);
box.applyMatrix4(new Matrix4().makeTranslation(x, y, z));
return box;
});
const merged = mergeGeometries(geometries, false);
if (merged && geometryRef.current) {
geometryRef.current.copy(merged);
geometries.forEach((g) => g.dispose());
merged.dispose();
}
}, [positions]);
return (
<mesh>
<bufferGeometry ref={geometryRef} />
<meshStandardMaterial color="sandybrown" />
</mesh>
);
}
Geometry Bounding Volumes
Bounding volumes are essential for culling, collision detection, and raycasting:
import { useRef, useEffect } from 'react';
import type { Mesh } from 'three';
export function BoundedMesh() {
const meshRef = useRef<Mesh>(null);
useEffect(() => {
if (!meshRef.current) return;
const geometry = meshRef.current.geometry;
// Compute bounding box
geometry.computeBoundingBox();
const box = geometry.boundingBox;
// box.min, box.max, box.getCenter(), box.getSize()
// Compute bounding sphere
geometry.computeBoundingSphere();
const sphere = geometry.boundingSphere;
// sphere.center, sphere.radius
}, []);
return (
<mesh ref={meshRef}>
<torusKnotGeometry args={[1, 0.3, 128, 32]} />
<meshStandardMaterial color="orchid" />
</mesh>
);
}
Level of Detail (LOD)
Use <Detailed> from drei or Three.js LOD for distance-based geometry switching:
import { Detailed } from '@react-three/drei';
export function LODObject() {
return (
<Detailed distances={[0, 10, 25, 50]}>
{/* Closest: high detail */}
<mesh><sphereGeometry args={[1, 64, 64]} /><meshStandardMaterial /></mesh>
{/* Medium distance */}
<mesh><sphereGeometry args={[1, 32, 32]} /><meshStandardMaterial /></mesh>
{/* Far distance */}
<mesh><sphereGeometry args={[1, 16, 16]} /><meshStandardMaterial /></mesh>
{/* Very far: billboard or point */}
<mesh><sphereGeometry args={[1, 8, 8]} /><meshStandardMaterial /></mesh>
</Detailed>
);
}
Testing Geometry
Vitest Tests
import { describe, it, expect } from 'vitest';
import { BoxGeometry, SphereGeometry } from 'three';
describe('Geometry fundamentals', () => {
it('creates box with correct vertex count', () => {
const geom = new BoxGeometry(1, 1, 1);
expect(geom.attributes.position.count).toBe(24); // 6 faces x 4 vertices
});
it('creates indexed sphere', () => {
const geom = new SphereGeometry(1, 16, 16);
expect(geom.index).not.toBeNull();
expect(geom.attributes.position.count).toBeGreaterThan(0);
});
it('computes bounding box', () => {
const geom = new BoxGeometry(2, 3, 4);
geom.computeBoundingBox();
expect(geom.boundingBox!.max.x).toBeCloseTo(1);
expect(geom.boundingBox!.max.y).toBeCloseTo(1.5);
expect(geom.boundingBox!.max.z).toBeCloseTo(2);
});
});
Storybook Stories
import type { Meta, StoryObj } from '@storybook/react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import { InstanceField } from './instance-field';
const meta: Meta<typeof InstanceField> = {
title: 'R3F/Geometry/InstanceField',
component: InstanceField,
decorators: [
(Story) => (
<Canvas camera={{ position: [5, 5, 5] }} style={{ height: 400 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
<Story />
<OrbitControls />
</Canvas>
),
],
};
export default meta;
export const Default: StoryObj<typeof InstanceField> = {
args: { count: 100, spread: 10 },
};
export const Dense: StoryObj<typeof InstanceField> = {
args: { count: 1000, spread: 5 },
};
Checklist
When working with geometry in R3F:
- Use built-in geometry JSX elements with
argsfor standard shapes - Use
useMemofor custom BufferGeometry data arrays - Set
needsUpdate = trueon attributes after modifying vertex data - Use InstancedMesh for rendering many identical objects (1 draw call)
- Pre-allocate
Object3DandColorobjects outsideuseFramefor instancing - Merge static geometries with
mergeGeometriesto reduce draw calls - Compute bounding volumes after geometry modifications
- Use LOD components for distance-based detail levels
- Index geometry when vertices are shared between triangles
- Keep geometry component files under 250 lines