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>
)
}
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>
)
}
And we can render model like this.
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
.
// 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
.
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.
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
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.
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
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 and implemented relay
// App.tsx
const App = () => {
return (
<div>
<ExporterStoreProvider>
<World />
</ExporterStoreProvider>
</div>
)
}
// 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.
And printing!
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)