DEV Community

Cover image for 3D Printing JSX model with react-three-renderer
terrierscript
terrierscript

Posted on • Edited on

3D Printing JSX model with react-three-renderer

I succeed 3D printing JSX with
react-three-fiber and three.js Exporter!

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:

function Thing() {
  return (
    <mesh>
      <boxBufferGeometry attach="geometry" args={[1, 1, 1]} />
      <meshNormalMaterial attach="material" />
    </mesh>
  )
}
Enter fullscreen mode Exit fullscreen mode

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.

Details

Build model

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

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

And we can render model like this.

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

  return (
    <Canvas>
      <Model />
    </Canvas>
  )
}
Enter fullscreen mode Exit fullscreen mode

Traverse scene and export STL

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

// 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>
}
Enter fullscreen mode Exit fullscreen mode

Export STL data when append inside Canvas.

const App = () => {
  // ...
  return (
    <Canvas>
      <Model />
      <ExportStl />
    </Canvas>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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

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>
}
Enter fullscreen mode Exit fullscreen mode

We can write this logic as hooks if you need.

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
}
Enter fullscreen mode Exit fullscreen mode

When convert to glTF, like this

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>
}
Enter fullscreen mode Exit fullscreen mode

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 and implemented relay

// App.tsx

const App = () => {
  return (
    <div>
      <ExporterStoreProvider>
        <World />
      </ExporterStoreProvider>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode
// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

Do printing!

This section is not related react.

My printer need convert STL to gcode.
I use Ultimaker cura.

And printing!

img

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.

I think it's useful that as a partial parts building and we can use another CAD tools like tinkercad.

Top comments (0)