January 2024

Getting aligned with three.js and react-three-fiber

The problem with alignment

In the world of three.js everything is aligned centred on each axis by default. This means that if you place an object at a position of interest it will likely overlap something else in a way you did not intend it to. In the above example I'm rendering a simple box at position [0, 0, 0] and, as you can see, it appears to have sunk in to the floor. I suspect this is expected if you think in 3d but I don't so let's see if we can fix it.

Getting aligned

To fix this I'm using an <Alignment /> component and withAxisAlignment hook that can shift the position of a child component to align it how you want

const ExampleTwo = () => {
  const { CustomProperties, customProperties } = useCustomProperties([
    '--🎨-background',
    '--🎨-art-accent',
  ])

  return (
    <CustomProperties>
      <ExampleScene>
        <DebugBox
          color={customProperties?.['--🎨-background'] || 'green'}
          position={new Vector3(0, 0, 0)}
          size={[0.5, 0.5, 0.5]}
        />

        <Alignment
          x={AxisAlignment.CENTER}
          y={AxisAlignment.CENTER}
          z={AxisAlignment.CENTER}
        >
          <DebugBox
            color={customProperties?.['--🎨-art-accent'] || 'red'}
            position={new Vector3(0.25, 0.25, 0.25)}
            size={[0.5, 0.5, 0.5]}
          />
        </Alignment>
      </ExampleScene>
    </CustomProperties>
  )
}
const Alignment = ({
  children,
  x = AxisAlignment.CENTER,
  y = AxisAlignment.CENTER,
  z = AxisAlignment.CENTER,
  offsets,
}: AlignmentProps) => {
  if (!isValidElement(children)) {
    throw new Error('Alignment requires a single React element as child')
  }

  const { position = new Vector3(0, 0, 0), size } = children.props

  if (!size) {
    throw new Error('Child component must have a size prop')
  }

  const alignedPosition = withAxisAlignment({
    position,
    size,
    x,
    y,
    z,
    offsets,
  })

  return cloneElement(children, {
    position: alignedPosition,
  })
}
const withAxisAlignment = ({
  position = new Vector3(0, 0, 0),
  size,
  x = AxisAlignment.CENTER,
  y = AxisAlignment.CENTER,
  z = AxisAlignment.CENTER,
  offsets = new Vector3(0, 0, 0),
}: {
  position?: Vector3
  size: BoxSize
  x?: AxisAlignment
  y?: AxisAlignment
  z?: AxisAlignment
  offsets?: Vector3
}) => {
  const [width, height, depth] = size
  const positionCopy = position.clone()

  // Center alignment is the default
  let alignedX = 0
  let alignedY = 0
  let alignedZ = 0

  if (x === AxisAlignment.START) alignedX = width / 2
  if (x === AxisAlignment.END) alignedX = -width / 2
  if (y === AxisAlignment.START) alignedY = -height / 2
  if (y === AxisAlignment.END) alignedY = height / 2
  if (z === AxisAlignment.START) alignedZ = -depth / 2
  if (z === AxisAlignment.END) alignedZ = depth / 2

  return positionCopy
    .add(new Vector3(alignedX, alignedY, alignedZ))
    .add(offsets)
}