Solid 2.0 JSX/TSX bindings for the RMSL scene graph
(@random-mesh/rmsl/scene), modeled on
solid-three's next-solid-2 architecture.
Declare a 3D scene in JSX. Each RMSL scene class becomes a Solid component —
<T.Mesh />, <T.BoxGeometry />, <T.MeshStandardMaterial />, <T.Scene />,
<T.PerspectiveCamera />, <T.AmbientLight />, ... — reconciled through Solid's own
reactivity. Props are reactive (signals, getters, or plain values), children attach by
instanceof (geometry/material) or Object3D.add, and node-based materials accept
RMSL node-graph slots like colorNode/fragmentNode directly in JSX.
import { vec3 } from "@random-mesh/rmsl"
import { Canvas, createT, SCENE, useFrame } from "@random-mesh/solid-rmsl"
const T = createT(SCENE)
function Spinner() {
useFrame(({ camera, clock }) => {
const t = clock.elapsedTime
camera.position.set(5 * Math.sin(t), 2, 5 * Math.cos(t))
camera.lookAt(0, 0, 0)
})
return null
}
export default function App() {
return (
<Canvas camera={{ fov: 50, position: [5, 2, 0] }}>
<Spinner />
<T.Mesh>
<T.BoxGeometry args={[1.6, 1.6, 1.6]} />
<T.MeshStandardMaterial
color={0xff5533}
roughness={0.25}
metalness={0.6}
colorNode={() => vec3(0.9, 0.4, 0.1)}
/>
</T.Mesh>
</Canvas>
)
}pnpm add @random-mesh/solid-rmsl solid-js @solidjs/signals @solidjs/web @random-mesh/rmslRequires solid-js@2.0.0-beta (Solid 2.0), a Solid 2.0-capable build setup
(vite-plugin-solid@2.x with a babel-preset-solid@2.0.0-beta override, or the native
@solidjs/vite-plugin), and @random-mesh/rmsl@^1.6.
createT(catalogue) turns any object of constructors into a Proxy of Solid components.
Pass the whole @random-mesh/rmsl/scene namespace:
import * as SCENE from "@random-mesh/rmsl/scene"
const T = createT(SCENE) // <T.Mesh />, <T.Scene />, <T.DataTexture />, ...SCENE is also re-exported from the package root. Components not used are never created,
and unknown tags evaluate to undefined rather than throwing.
<Canvas> owns the renderer, scene and camera and is the required root of every scene.
Hooks (useThree, useFrame) and <T/> components must live beneath it.
| Prop | Type | Default | Description |
|---|---|---|---|
camera |
Partial<Props<PerspectiveCamera> | Props<OrthographicCamera>> | Camera |
new PerspectiveCamera() |
Camera or camera props (props are applied through the same reconciler, incl. updateProjectionMatrix). |
gl |
{ antialias?, depth? } | (canvas) => WebGLRenderer | WebGLRenderer |
new WebGLRenderer(canvas, {}) |
rmsl renderer options, factory, or instance. |
orthographic |
boolean |
false |
Use an orthographic camera. |
scene |
Partial<Props<Scene>> | Scene |
new Scene() |
Scene or scene props. |
frameloop |
"always" | "demand" | "never" |
"always" |
rAF loop, render-on-demand, or manual render(). |
style, class |
Applied to the container div. | ||
ref |
Context |
Receives the reactive Context ({ gl, scene, camera, ... }). |
- Object3D children are added/removed with
parent.add(child)/parent.remove(child)and reordered to match JSX order. BufferGeometry/Materialchildren default-attach tomesh.geometry/mesh.material(useattachfor anything else, e.g.attach="map"on a texture, or a dotted path likeattach="material-color").- Props accept the RMSL representation:
position={[1, 2, 3]},scale={2}(setScalar),color={0xff5533}/"#ff5533"/[1, 0.5, 0.25], nested dotted props likerotation-x={Math.PI / 2}, and live signals. - Node slots (
colorNode,roughnessNode,emissiveNode,fragmentNode, ...) take an RMSL builder function or node and setmaterial.needsUpdate = trueso the shader rebuilds. - Cameras: changing
fov/aspect/near/far(or ortho extents) callsupdateProjectionMatrix()automatically. - Constructor args go in
args:<T.BoxGeometry args={[1, 1, 1]} />. - Refs receive the instance:
ref={(mesh) => mesh.position.y = 1}. keyremounts an element; unmount disposes the object (and the renderer's GPU resources when the object has adispose, e.g.WebGLRenderer).
useFrame(callback, { stage?, priority? })— register a per-frame callback(context, delta) => void; runs"before"(default) or"after"the render, ordered bypriority.useThree()— the renderContext:{ gl, scene, camera, bounds, viewport, clock, props, render, requestRender }. Accepts an optional selector:useThree(({ camera }) => camera).
Pointer/click events (onPointerDown, onClick, ...) are deferred: rmsl's scene
module has no raycaster yet. The EventHandlers seam is in the types so the event system
can be added without breaking the public API. Until then, attach DOM listeners to
context.canvas (see the demo's orbit controls).
pnpm install
pnpm type-check # tsc --noEmit (incl. tests/types.test-d.tsx)
pnpm test # vitest (jsdom + WebGL mock)
pnpm build # tsup: index / index.dev / index.dev.solid + dts
pnpm demo # vite dev server for apps/demo
pnpm demo:build # production build of the demoNote (dev server + Solid 2.0 beta): the HMR wrapper shipped by
vite-plugin-solid@2.x(solid-refresh@0.6.x) breaks component props under Solid 2.0 beta — its$$componentstores the component in acreateSignal, which now invokes function values, so a component gets called without props. The demo's vite config disables it (hot: false), trading HMR for full-page reloads. Use@solidjs/vite-plugin(Solid 2.0-native) when it is available on your platform.
solid-rmsl/
├── src/
│ ├── index.ts # public API + SCENE catalogue
│ ├── create-t.tsx # createT(catalogue) Proxy + createEntity
│ ├── props.ts # the reconciler: useProps / useSceneGraph / applyProp / applySceneGraph
│ ├── create-render.tsx # createRenderer: renderer+scene+camera, render loop, contexts
│ ├── canvas.tsx # <Canvas> DOM component
│ ├── components.tsx # Entity / Portal
│ ├── hooks.ts # useFrame / useThree / createClock
│ ├── types.ts # Props<T> / Representation<T> (inferred JSX types)
│ ├── internal-context.ts # event/portal context stubs (events deferred)
│ └── utils/ # meta()/resolve()/useMeasure ...
├── apps/demo/ # the rmsl scene-demo, ported to JSX
└── tests/ # core reconciler tests + compile-time JSX checks
No custom renderer — solid-rmsl reuses Solid 2.0's own reactivity and JSX.
createTwraps each RMSL class as a Solid component viacreateEntity, which builds the instance inside acreateMemo(recreated onkeychange) tagged withmeta()($S3Csymbol carrying{ props, parent, children }).usePropscreates per-keycreateRenderEffects that readprops[key]and write into the object throughapplyProp— the three.js/RMSL-savvy assignment logic (nested paths, color dispatch,copy/fromArray/setScalar, node-slot rebuild flags, camera projection refresh).useSceneGraphresolves children with Solid'schildren(), attaches them withapplySceneGraph(attach-prop → instanceof defaults →parent.add), and returns cleanups so unmount detaches properly.<Canvas>callscreateRenderer, which creates the rmslWebGLRenderer/Scene/ camera, drives the rAF loop withframeloopmodes, re-sizes viaResizeObserver(updating the camera aspect + projection matrix), and publishes theuseThree/useFramecontexts.- JSX types are inferred from the RMSL classes by the
Props<T>/Representation<T>mapped types — no codegen.
MIT