Skip to content

Repository files navigation

solid-rmsl

npm

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>
  )
}

Installation

pnpm add @random-mesh/solid-rmsl solid-js @solidjs/signals @solidjs/web @random-mesh/rmsl

Requires 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.

The catalogue

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

<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, ... }).

Elements

  • Object3D children are added/removed with parent.add(child) / parent.remove(child) and reordered to match JSX order.
  • BufferGeometry / Material children default-attach to mesh.geometry / mesh.material (use attach for anything else, e.g. attach="map" on a texture, or a dotted path like attach="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 like rotation-x={Math.PI / 2}, and live signals.
  • Node slots (colorNode, roughnessNode, emissiveNode, fragmentNode, ...) take an RMSL builder function or node and set material.needsUpdate = true so the shader rebuilds.
  • Cameras: changing fov/aspect/near/far (or ortho extents) calls updateProjectionMatrix() automatically.
  • Constructor args go in args: <T.BoxGeometry args={[1, 1, 1]} />.
  • Refs receive the instance: ref={(mesh) => mesh.position.y = 1}.
  • key remounts an element; unmount disposes the object (and the renderer's GPU resources when the object has a dispose, e.g. WebGLRenderer).

Hooks

  • useFrame(callback, { stage?, priority? }) — register a per-frame callback (context, delta) => void; runs "before" (default) or "after" the render, ordered by priority.
  • useThree() — the render Context: { gl, scene, camera, bounds, viewport, clock, props, render, requestRender }. Accepts an optional selector: useThree(({ camera }) => camera).

Events

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).

Development

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 demo

Note (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 $$component stores the component in a createSignal, 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.

Project structure

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

How it works

No custom renderer — solid-rmsl reuses Solid 2.0's own reactivity and JSX.

  1. createT wraps each RMSL class as a Solid component via createEntity, which builds the instance inside a createMemo (recreated on key change) tagged with meta() ($S3C symbol carrying { props, parent, children }).
  2. useProps creates per-key createRenderEffects that read props[key] and write into the object through applyProp — the three.js/RMSL-savvy assignment logic (nested paths, color dispatch, copy/fromArray/setScalar, node-slot rebuild flags, camera projection refresh).
  3. useSceneGraph resolves children with Solid's children(), attaches them with applySceneGraph (attach-prop → instanceof defaults → parent.add), and returns cleanups so unmount detaches properly.
  4. <Canvas> calls createRenderer, which creates the rmsl WebGLRenderer/Scene/ camera, drives the rAF loop with frameloop modes, re-sizes via ResizeObserver (updating the camera aspect + projection matrix), and publishes the useThree/ useFrame contexts.
  5. JSX types are inferred from the RMSL classes by the Props<T>/Representation<T> mapped types — no codegen.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages