This guide explains how to use the "Bring Your Own Controls" (BYOC) pattern to integrate custom transform controls with @wendylabsinc/react-three-mesh-editor.
The MeshEditor component uses render props to allow you to provide your own transform controls. This gives you full flexibility to:
MeshEditor provides three render props for custom controls:
| Prop | Mode | Description |
|---|---|---|
renderVertexControl |
Vertex | Called for each selected vertex |
renderEdgeControl |
Edge | Called for each selected edge |
renderFaceControl |
Face | Called for each selected face |
For vertices, only translation makes sense. A single point cannot be rotated or scaled.
interface VertexControlRenderProps {
/** The vertex data */
vertex: VertexData;
/** Callback when vertex position changes (absolute position) */
onMove: (position: [number, number, number]) => void;
/** Callback when drag starts */
onDragStart?: () => void;
/** Callback when drag ends */
onDragEnd?: () => void;
}
import { useRef } from 'react';
import { PivotControls } from '@react-three/drei';
import { Matrix4, Vector3 } from 'three';
import { MeshEditor } from '@wendylabsinc/react-three-mesh-editor';
import type { VertexControlRenderProps } from '@wendylabsinc/react-three-mesh-editor';
function VertexPivotControl({ vertex, onMove }: VertexControlRenderProps) {
const matrixRef = useRef(new Matrix4());
// Update matrix to vertex position
matrixRef.current.setPosition(
vertex.position[0],
vertex.position[1],
vertex.position[2]
);
return (
<PivotControls
matrix={matrixRef.current}
anchor={[0, 0, 0]}
depthTest={false}
scale={0.4}
autoTransform={false}
disableRotations // No rotation for single vertices
disableScaling // No scaling for single vertices
onDrag={(matrix) => {
const position = new Vector3();
position.setFromMatrixPosition(matrix);
onMove([position.x, position.y, position.z]);
}}
>
<mesh visible={false}>
<sphereGeometry args={[0.01]} />
</mesh>
</PivotControls>
);
}
// Usage
<MeshEditor
geometry={geometry}
mode="edit"
editMode="vertex"
renderVertexControl={(props) => <VertexPivotControl {...props} />}
/>
Edges and faces support translation, rotation, and scale since they involve multiple vertices that can be transformed relative to their center.
interface EdgeControlRenderProps {
/** The edge data */
edge: EdgeData;
/** Array of vertices for position lookup */
vertices: VertexData[];
/** Center position of the edge */
center: [number, number, number];
/** Callback to move edge vertices by a delta */
onMoveByDelta: (delta: [number, number, number]) => void;
/** Callback to transform vertices (rotation/scale around center) */
onTransform: (
rotation: { x: number; y: number; z: number; w: number },
scale: [number, number, number]
) => void;
/** Callback to capture initial positions before transform */
onCaptureInitialPositions: () => void;
/** Callback when drag starts */
onDragStart?: () => void;
/** Callback when drag ends */
onDragEnd?: () => void;
}
// FaceControlRenderProps is similar, with 'face' instead of 'edge'
import { useRef, useCallback, useMemo } from 'react';
import { PivotControls } from '@react-three/drei';
import { Matrix4, Vector3, Quaternion } from 'three';
import type { EdgeControlRenderProps, FaceControlRenderProps } from '@wendylabsinc/react-three-mesh-editor';
function TransformPivotControl({
center,
onMoveByDelta,
onTransform,
onCaptureInitialPositions,
}: EdgeControlRenderProps | FaceControlRenderProps) {
const initialMatrixRef = useRef<Matrix4 | null>(null);
const appliedDeltaRef = useRef<[number, number, number]>([0, 0, 0]);
// Create matrix at center position
const matrix = useMemo(() => {
const m = new Matrix4();
m.setPosition(center[0], center[1], center[2]);
return m;
}, [center]);
const handleDragStart = useCallback(() => {
initialMatrixRef.current = matrix.clone();
appliedDeltaRef.current = [0, 0, 0];
onCaptureInitialPositions();
}, [matrix, onCaptureInitialPositions]);
const handleDrag = useCallback(
(localMatrix: Matrix4) => {
const position = new Vector3();
const quaternion = new Quaternion();
const scale = new Vector3();
localMatrix.decompose(position, quaternion, scale);
const initialPos = new Vector3();
if (initialMatrixRef.current) {
initialPos.setFromMatrixPosition(initialMatrixRef.current);
}
// Check if there's rotation or scale
const hasRotation =
Math.abs(quaternion.x) > 0.0001 ||
Math.abs(quaternion.y) > 0.0001 ||
Math.abs(quaternion.z) > 0.0001 ||
Math.abs(quaternion.w - 1) > 0.0001;
const hasScale =
Math.abs(scale.x - 1) > 0.0001 ||
Math.abs(scale.y - 1) > 0.0001 ||
Math.abs(scale.z - 1) > 0.0001;
if (hasRotation || hasScale) {
// Apply rotation/scale transformation
onTransform(
{ x: quaternion.x, y: quaternion.y, z: quaternion.z, w: quaternion.w },
[scale.x, scale.y, scale.z]
);
} else {
// Apply incremental translation
const totalDelta: [number, number, number] = [
position.x - initialPos.x,
position.y - initialPos.y,
position.z - initialPos.z,
];
const incrementalDelta: [number, number, number] = [
totalDelta[0] - appliedDeltaRef.current[0],
totalDelta[1] - appliedDeltaRef.current[1],
totalDelta[2] - appliedDeltaRef.current[2],
];
appliedDeltaRef.current = totalDelta;
if (
Math.abs(incrementalDelta[0]) > 0.0001 ||
Math.abs(incrementalDelta[1]) > 0.0001 ||
Math.abs(incrementalDelta[2]) > 0.0001
) {
onMoveByDelta(incrementalDelta);
}
}
},
[onMoveByDelta, onTransform]
);
return (
<PivotControls
matrix={matrix}
anchor={[0, 0, 0]}
depthTest={false}
scale={0.3}
autoTransform={false}
onDragStart={handleDragStart}
onDrag={handleDrag}
>
<mesh visible={false}>
<sphereGeometry args={[0.01]} />
</mesh>
</PivotControls>
);
}
// Usage
<MeshEditor
geometry={geometry}
mode="edit"
editMode="edge"
renderEdgeControl={(props) => <TransformPivotControl {...props} />}
renderFaceControl={(props) => <TransformPivotControl {...props} />}
/>
If you want selection behavior without any transform controls, simply omit the render props:
<MeshEditor
geometry={geometry}
mode="edit"
editMode="vertex"
// No renderVertexControl = no transform controls
/>
This is useful when you want to:
You can use any Three.js transform control library. Here's an example structure:
function MyCustomControl({ center, onMoveByDelta }) {
// Your control implementation
return (
<MyControlLibrary
position={center}
onChange={(newPosition) => {
const delta = [
newPosition.x - center[0],
newPosition.y - center[1],
newPosition.z - center[2],
];
onMoveByDelta(delta);
}}
/>
);
}
See the Storybook examples for live demonstrations.