And your application is not just a WebGL demo, right? The domain is still modeled in ReScript, and three.js is “just” an unusual view layer, that is, 3D graphics instead of React components.
What I found works best for my app based on ReScript and three.js is having a dedicated layer that converts my domain objects (a drawing intended to be laser-cut) to three.js scene. The domain has no dependency on three.js.
The bindings are not puzzles but can look a bit verbose. Here’s some random snippet from the conversion layer to give an idea:
let bboxToSphere = bbox =>
Three.Sphere.make(Vector3.fromVector2(bbox->BBox.center, 0.0), bbox->BBox.diagonalLength /. 2.0)
let makeCurveWireframeGeometry = (
curve: 'curve,
lengthInUnits: 'curve => float,
tesselate: ('curve, int) => array<Vector2.t>,
~tesselationStep: float,
~thicknessInUnits: float,
~bbox: BBox.t,
) => {
let tesselationDivisions = (lengthInUnits(curve) /. tesselationStep)->Js.Math.ceil_int
let facePoints = tesselate(curve, tesselationDivisions)
let nFacePoints = facePoints->Array.length
let edgeEvery = 4 // only 1 of `edgeEvery` will be used to draw an edge
let edgePoints =
facePoints->Array.keepWithIndex((_, index) =>
mod(index, edgeEvery) == 0 || index == nFacePoints - 1
)
let zfar = -.thicknessInUnits
let bsphere = bboxToSphere(bbox)
let frontFaceGeo = Three.BufferGeometry.make()
frontFaceGeo->Three.BufferGeometry.setFromPoints2d(
facePoints->ArrayX.pairwise->ArrayX.flattenTuples2,
)
// Deal with https://github.com/mrdoob/three.js/issues/6288
frontFaceGeo->Three.BufferGeometry.setBoundingSphere(bsphere)
let backFaceGeo = frontFaceGeo->Three.BufferGeometry.clone
backFaceGeo->Three.BufferGeometry.translate(0.0, 0.0, zfar)
let edgesGeo = Three.BufferGeometry.make()
edgesGeo->Three.BufferGeometry.setFromPoints(
edgePoints
->Array.map(v2 => [Vector3.fromVector2(v2, 0.0), Vector3.fromVector2(v2, zfar)])
->Array.concatMany,
)
// Deal with https://github.com/mrdoob/three.js/issues/6288
edgesGeo->Three.BufferGeometry.setBoundingSphere(bsphere)
Three.BufferGeometryUtils.mergeBufferGeometries(
[frontFaceGeo, backFaceGeo, edgesGeo],
~useGroups=false,
(),
)
}
let makePathGeometry = (
path: Shape.Path.t,
~tesselationStep: float,
~thicknessInUnits: float,
~bbox: BBox.t,
) => {
let makeWire = makeCurveWireframeGeometry(~tesselationStep, ~thicknessInUnits, ~bbox)
switch path {
| Circle(circle) => makeWire(circle, Curves.Circle.length, Curves.Circle.tesselate)
| Ellipse(ellipse) => makeWire(ellipse, Curves.Ellipse.length, Curves.Ellipse.tesselate)
| Poly(segments) =>
let segmentsGeo = segments->Array.map(s =>
switch s {
| Line(line) => makeWire(line, Curves.Line.length, Curves.Line.tesselate)
| Arc(arc) => makeWire(arc, Curves.Arc.length, Curves.Arc.tesselate)
| Spline(spline) => makeWire(spline, Curves.Spline.length, Curves.Spline.tesselate)
| QuadraticBezier(bezier) =>
makeWire(bezier, Curves.QuadraticBezier.length, Curves.QuadraticBezier.tesselate)
| CubicBezier(bezier) =>
makeWire(bezier, Curves.CubicBezier.length, Curves.CubicBezier.tesselate)
}
)
Three.BufferGeometryUtils.mergeBufferGeometries(segmentsGeo, ())
}
}
let makeShapeGeometry = (
shape: Shape.t,
~tesselationStep: float,
~thicknessInUnits: float,
~bbox: BBox.t,
) => {
let makeWire = makePathGeometry(_, ~tesselationStep, ~thicknessInUnits, ~bbox)
Three.BufferGeometryUtils.mergeBufferGeometries(shape->Shape.allPaths->Array.map(makeWire), ())
}