3D Printing JSX model with react-three-renderer
Tags: react, threejs, 3dprinting, javascript
Published: 2019-12-30 10:47:36

I succeed 3D printing JSX with
[react-three-fiber](https://github.com/react-spring/react-three-fiber) and [three.js Exporter](https://github.com/mrdoob/three.js/blob/dev/examples/jsm/exporters/)!


## What?

`react-three-fiber` is great library that treat `three.js` on React.
When we use this library, we use JSX for build `mesh` and `geometry` like this:

```jsx
function Thing() {
  return (
    <mesh>
      <boxBufferGeometry attach="geometry" args={[1, 1, 1]} />
      <meshNormalMaterial attach="material" />
    </mesh>
  )
}
```

We need convert and output polygon data to STL or glTF if 3D printing.

Fortunately, three.js include exporters `STLExporter` and `GLTFExporter` (But some exporter is undocumented and may unstable)

We combine those two thing, it's enable "3D printing JSX"!

## Demo

This demo can edit `src/model/Model.tsx`
Be careful demo maybe heavy.

{% codesandbox ey0wt %}

## Details

### Build model

I generate for example model below.
Models can render both `BufferGeometry` and `Geometry`, can nest mesh and split as component.

```tsx
// Model.tsx
import { Canvas, useFrame, useThree } from "react-three-fiber"
import React from "react"

export const Model = () => {
  return (
    <mesh>
      <Model1 />
      <Model2 />
    </mesh>
  )
}
const Model1 = () => {
  return (
    <mesh position={[0, 0, 0]}>
      <cylinderBufferGeometry attach="geometry" args={[5, 5, 5]} />
      <meshNormalMaterial attach="material" />
    </mesh>
  )
}
const Model2 = () => {
  return (
    <mesh>
      <mesh position={[-5, -1.5, -3]}>
        <boxBufferGeometry attach="geometry" args={[6, 2, 5]} />
        <meshNormalMaterial attach="material" />
      </mesh>
      <mesh>
        <mesh position={[0, 3, -1]}>
          <octahedronBufferGeometry attach="geometry" args={[4]} />
          <meshNormalMaterial attach="material" />
        </mesh>
        <mesh position={[3, 0.5, 3]}>
          <sphereGeometry attach="geometry" args={[3, 10, 32]} />
          <meshNormalMaterial attach="material" />
        </mesh>
      </mesh>
    </mesh>
  )
}
```

And we can render model like this.

```tsx
const App = () => {
  const ref = useRef()
  const { gl } = useThree()
  gl.setClearColor("#ff99cc")

  return (
    <Canvas>
      <Model />
    </Canvas>
  )
}
```


### Traverse scene and export STL

We can got `scene` from `useThree` and can convert to STL with `STLExporter`.

```jsx
// ExportStl.tsx
import { STLExporter } from "three/examples/jsm/exporters/STLExporter"

export const ExportStl = () => {
  const { scene } = useThree()
  useEffect(() => {
    const stl = new STLExporter().parse(scene)
    console.log(stl)
  }, [scene])
  return <mesh></mesh>
}
```

Export STL data when append inside `Canvas`.

```tsx
const App = () => {
  // ...
  return (
    <Canvas>
      <Model />
      <ExportStl />
    </Canvas>
  )
}
```

But bear geometries occure error or another some problem.
I try to convert and merge geometry for output.

```js
export const toRenderble = (scene: Scene): Scene => {
  let tmpGeometry = new Geometry()

  const cloneScene = scene.clone()
  cloneScene.traverse((mesh) => {
    if (!isMesh(mesh)) return
    if (!mesh.geometry) {
      return
    }

    // Convert geometry
    const appendGeom = toRenderableGeometry(mesh.geometry)
    if (!appendGeom) {
      return null
    }

    // merge parent matrix
    if (mesh.parent) {
      mesh.parent.updateMatrixWorld()
      mesh.applyMatrix(mesh.parent.matrixWorld)
    }

    mesh.geometry = appendGeom
    tmpGeometry.mergeMesh(mesh)
  })

  // generate output scene
  const outputScene = new Scene()
  const buf = new BufferGeometry().fromGeometry(tmpGeometry)
  const mesh = new Mesh(buf, new MeshBasicMaterial())
  outputScene.add(mesh)
  return outputScene
}

// convert BufferGeometry -> Geometry
const toRenderableGeometry = (
  geom: Geometry | BufferGeometry
): Geometry | null => {
  if (isGeometry(geom)) {
    return geom
  }
  if (geom.index === null && !geom.getAttribute("position")) {
    return null
  }

  // Try to convert BufferGeometry (not stable...)
  try {
    const buf = new Geometry().fromBufferGeometry(geom)
    return buf
  } catch (e) {
    console.warn(`skip: ${geom}`)
    return null
  }
}
```

After this, we can those on component.
This time, pass result to `React.Context`

```jsx
export const ExportStl = () => {
  const { scene } = useThree()
  const { setStl } = useExporterStore()
  useEffect(() => {
    const copyScene = toRenderble(scene)
    const stl = new STLExporter().parse(copyScene)
    setStl(stl)
  }, [scene])
  return <mesh></mesh>
}
```

We can write this logic as hooks if you need.

```js
export const useSTLExporter = () => {
  const { scene } = useThree()
  const [result, setResult] = useState()
  useEffect(() => {
    const copyScene = toRenderble(scene)
    const stl = new STLExporter().parse(copyScene)
    setResult(stl)
  }, [scene])
  return result
}
```

When convert to glTF, like this

```tsx
const exportGltf = (scene, cb) => {
  return new GLTFExporter().parse(
    scene,
    (obj) => {
      cb(JSON.stringify(obj, null, 2))
    },
    { trs: true }
  )
}

export const ExportGltf = () => {
  const { scene } = useThree()
  useEffect(() => {
    const copyScene = toRenderble(scene)
    exportGltf(copyScene, (glTF) => {
      console.log(glTF)
    })
  }, [scene])
  return <mesh></mesh>
}
```

### Output model data to outsitde react-three-fiber

In above seciton, I talk about use `React.Context`, but in real `react-three-fiber` use `React.Reconciler` and cannot default hooks normaly in `<Canvas>` children.

I refer [this issue](https://github.com/react-spring/react-three-fiber/issues/262) and implemented relay

```jsx
// App.tsx

const App = () => {
  return (
    <div>
      <ExporterStoreProvider>
        <World />
      </ExporterStoreProvider>
    </div>
  )
}
```

```jsx
// World.tsx

export const World = () => {
  const value = useExporterStore() // get value
  return (
    <Canvas camera={{ position: [0, 0, 30] }}>
      <ExportPassProvider value={value}>
        {" "}
        {/* and pass inside Canvas */}
        <Model />
        <ExportStl />
      </ExportPassProvider>
    </Canvas>
  )
}
```

## Do printing!

This section is not related react.

My printer need convert STL to gcode.
I use [Ultimaker cura](https://ultimaker.com/software).

<img width="500" src="https://user-images.githubusercontent.com/13282103/71564664-bb83a400-2ae7-11ea-88b7-1926ac9b06d5.png">

And printing!

![img](https://user-images.githubusercontent.com/13282103/71566577-132c0a80-2afc-11ea-89b7-ce669e952855.png)

## Conclusion

This PoC is not good performance and some geometry pattern cannot converted, but we can "3D printing JSX".

It is hard to build everything with JSX and those hasn't real sizes but so good on little regular shapes model like [this articles cover react logo](https://github.com/terrierscript/poc-react-three-stl-exporter/blob/master/src/model/ModelReactLogoCoin.tsx#L6-L101).

I think it's useful that as a partial parts building and we can use another CAD tools like [tinkercad](https://www.tinkercad.com/).