Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2a7fc62
docs: design for decoupling XR from the render loop
bigmistqke May 29, 2026
638045d
docs: refine XR decoupling spec + add implementation plan
bigmistqke May 29, 2026
aaa14ee
fix(types): Context.render forwards the XRFrame argument
bigmistqke May 29, 2026
62fd800
test: remove obsolete internally-managed XR tests
bigmistqke May 29, 2026
f44441d
refactor(xr): decouple XR loop ownership from core
bigmistqke May 29, 2026
b5b1aba
test(xr): verify sessionend listener detaches on renderer swap
bigmistqke May 29, 2026
93badf4
docs: record naming decision (keep render); drop rename task
bigmistqke May 29, 2026
286552b
fix(xr): guard resize render; scope post-XR repaint; fix docs
bigmistqke May 29, 2026
b90c772
docs(spec): createXR — consumer-owned XR entry primitive
bigmistqke Jun 2, 2026
cd0c61e
docs(plan): createXR implementation plan (TDD, 6 tasks)
bigmistqke Jun 2, 2026
b7c1267
feat(utils): useRef honors a cleanup-returning ref
bigmistqke Jun 2, 2026
1a4ab86
feat(canvas): widen ref to allow a cleanup-returning callback
bigmistqke Jun 2, 2026
2bf302b
feat(xr): createXR state, sessionstart/sessionend wiring, connect
bigmistqke Jun 2, 2026
fa88565
feat(xr): createXR.enter — request-first ordering + provided-session …
bigmistqke Jun 2, 2026
5cce3d3
feat(xr): createXR.exit + isSupported
bigmistqke Jun 2, 2026
d691526
feat(xr): export createXR from package entry
bigmistqke Jun 2, 2026
9520f1d
docs(xr): document createXR (README + API site)
bigmistqke Jun 2, 2026
e61b351
docs(xr): trim createXR docs to reader altitude
bigmistqke Jun 2, 2026
9464f9d
refactor(xr): narrow connect to XRContext (gl + render), not full Con…
bigmistqke Jun 2, 2026
1dbe237
docs(xr): design spec for useXR + createXR().Provider (in-scene XR st…
bigmistqke Jun 2, 2026
92a0058
chore: exclude demo-xr scaffolding from typecheck
bigmistqke Jun 2, 2026
9b34e63
feat(xr): add xrContext + useXR hook (throws outside provider)
bigmistqke Jun 2, 2026
79b6520
feat(xr): createXR().Provider distributes XR state to useXR
bigmistqke Jun 2, 2026
1571ec8
feat(xr): export useXR and XRState from package entry
bigmistqke Jun 2, 2026
198972b
test(xr): bridge test — provider state crosses into the scene
bigmistqke Jun 2, 2026
22b4584
docs(xr): document useXR + createXR().Provider
bigmistqke Jun 2, 2026
b146647
chore: add docs/superpowers to gitignore
bigmistqke Jun 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ tests/**/__screenshots__/
site/.output
site/.nitro
site/.solid-start
site/.vinxi
site/.vinxi
docs/superpowers
139 changes: 137 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- **Declarative `three.js` Components**: Utilize `three.js` objects as JSX components.
- **Reactive Prop Updates**: Properties of 3D objects update reactively, promoting efficient re-renders.
- **Integrated Animation Loop**: `useFrame` hook allows for easy animations.
- **WebXR (VR & AR)**: `createXR` enters and exits immersive sessions on both `WebGLRenderer` and `WebGPURenderer`.
- **Comprehensive Event System**: Enhanced event handling with support for `three.js` pointer and mouse events.
- **Extensible and Customizable**: Easily extendable with additional `three.js` entities or custom behaviors.
- **Optimized for `solid-js`**: Leverages `solid-js`' fine-grained reactivity for optimal performance.
Expand All @@ -27,6 +28,8 @@
4. [Hooks](#hooks)
- [useThree](#usethree)
- [useFrame](#useframe)
- [createXR](#createxr)
- [useXR](#usexr)
- [useProps](#useprops)
5. [Utilities](#utilities)
- [Raycasters](#raycasters)
Expand Down Expand Up @@ -114,6 +117,7 @@ The `Canvas` component initializes the `three.js` rendering context and acts as
- `"never"`: Disables automatic rendering
- **style**: Custom CSS styles for the canvas container.
- **class**: CSS class names for the canvas container.
- **ref**: Receives the scene's [`Context`](#usethree) once the renderer is created, so code outside `<Canvas>` can reach it. A callback ref may return a cleanup that runs when the Canvas unmounts — [`createXR`](#createxr) uses this.
- **Event handlers**: All event handlers are supported on the Canvas component, allowing you to handle events that bubble through the entire scene (e.g., `onClick`, `onPointerMove`, `onClickMissed`, etc.)

<details>
Expand Down Expand Up @@ -485,10 +489,9 @@ const camera = useThree(ctx => ctx.camera)
- **gl** (`Renderer`): The active renderer — `WebGLRenderer | WebGPURenderer | RendererLike` by default. Narrow to a concrete type project-wide via [Register augmentation](#narrowing-the-renderer-type-project-wide).
- **raycaster** (`Raycaster`): The current raycaster used for pointer events.
- **setRaycaster** (`(raycaster: Raycaster) => () => void`): A setter-function for setting the current raycaster.
- **render** (`(delta: number) => void`): Function to manually trigger a render.
- **render** (`(timestamp: number, frame?: XRFrame) => void`): Drives a single frame — runs the registered `useFrame` callbacks, then renders the scene. The optional `frame` is forwarded to those callbacks; an active WebXR session passes it on each immersive frame.
- **requestRender** (`() => void`): Function to request a render on the next frame.
- **scene** (`Scene`): The root scene.
- **xr** (`{ connect: () => void; disconnect: () => void }`): WebXR connection management.

**Camera and Raycaster Stack System:**

Expand Down Expand Up @@ -593,6 +596,138 @@ useFrame(
```


### createXR

Enters and exits WebXR sessions (VR and AR) for the scene's renderer. The same code works on both `WebGLRenderer` and `WebGPURenderer`.

Unlike the `use*` hooks, `createXR` lives **outside** `<Canvas>` — next to the DOM "Enter XR" button that triggers a session. Call it in a component body (like `createSignal`), then connect it to the renderer with `<Canvas ref={xr.connect}>`.

It handles the ordering rules that make a session enter cleanly — most importantly, call `enter` directly from a click handler (see the example).

**Returns** an object with:

- **connect** (`(context: Context) => () => void`): A ref for `<Canvas ref={xr.connect}>`. Hands the renderer to `createXR` and returns a cleanup that runs on unmount.
- **enter** (`(mode, sessionInit?) => Promise<XRSession>`): Requests a session and enters it. `mode` is `"immersive-vr"` / `"immersive-ar"` / `"inline"`; `sessionInit` is passed straight through to `navigator.xr.requestSession`. Call it directly from a user-gesture handler (a click), so the immersive request keeps its user activation. You may also pass an `XRSession` you created yourself, to skip the request and just wire it.
- **exit** (`() => Promise<void>`): Ends the active session.
- **isPresenting** (`() => boolean`): Reactive — `true` while a session is presenting.
- **session** (`() => XRSession | undefined`): Reactive — the active session, or `undefined`.
- **isSupported** (`(mode) => Promise<boolean>`): Whether the device supports a session mode. Use it to decide whether to show the button; don't `await` it before calling `enter`.
- **Provider** (`(props: { children: JSX.Element }) => JSX.Element`): Distributes this `createXR`'s state to the scene. Wrap the subtree that needs in-scene XR access (typically `<Canvas>` and its button); read it inside with [`useXR`](#usexr).

<details>
<summary>Typescript Interface</summary>

```tsx
function createXR(): {
connect: (context: Context) => () => void
enter: (mode: XRSessionMode, sessionInit?: XRSessionInit) => Promise<XRSession>
enter: (session: XRSession) => Promise<XRSession>
exit: () => Promise<void>
isPresenting: () => boolean
session: () => XRSession | undefined
isSupported: (mode: XRSessionMode) => Promise<boolean>
Provider: (props: { children: JSX.Element }) => JSX.Element
}
```

</details>

**Usage:**

```tsx
import { Canvas, createXR } from "solid-three"
import { Show } from "solid-js"

function App() {
const xr = createXR()

return (
<>
{/* The button is DOM, outside the Canvas. onClick is the user gesture. */}
<button onClick={() => xr.enter("immersive-vr")}>Enter VR</button>
<Show when={xr.isPresenting()}>
<button onClick={() => xr.exit()}>Exit VR</button>
</Show>

<Canvas ref={xr.connect}>
<Scene />
</Canvas>
</>
)
}
```

**AR with feature descriptors, gated on support:**

```tsx
import { createResource, Show } from "solid-js"

const xr = createXR()
const [supported] = createResource(() => xr.isSupported("immersive-ar"))

<Show when={supported()}>
<button
onClick={() =>
xr.enter("immersive-ar", {
requiredFeatures: ["local-floor"],
optionalFeatures: ["hand-tracking", "depth-sensing"],
})
}
>
Enter AR
</button>
</Show>
```

> **Reading controller and hand poses:** you don't need `createXR` inside the scene. The third argument to [`useFrame`](#useframe) is the live `XRFrame` during a session — read poses from there.

> **`WebGPURenderer` note:** on three.js ≤ r184, driving a WebXR session through the WebGPU backend requires a WebGL2 fallback. `createXR` itself is backend-agnostic; configure the renderer at the `<Canvas gl={…}>` level.


### useXR

Reads the XR state distributed by [`createXR().Provider`](#createxr) from **inside the scene**. Use it when a component needs to react to the session or drive in-world UI — most importantly an in-VR exit control, since the DOM "Exit" button is not rendered while an immersive session is presenting.

Wrap the subtree with `<xr.Provider>`, then call `useXR()` in any descendant (including components inside `<Canvas>`):

```tsx
import { Canvas, createXR, useXR } from "solid-three"
import { Show } from "solid-js"

function ExitButton() {
const { isPresenting, exit } = useXR()
return (
<Show when={isPresenting()}>
<T.Mesh position={[0, 1.4, -1]} onClick={() => exit()}>
<T.BoxGeometry args={[0.3, 0.15, 0.02]} />
<T.MeshBasicMaterial color="crimson" />
</T.Mesh>
</Show>
)
}

function App() {
const xr = createXR()
return (
<xr.Provider>
<button onClick={() => xr.enter("immersive-vr")}>Enter VR</button>
<Canvas ref={xr.connect}>
<ExitButton />
</Canvas>
</xr.Provider>
)
}
```

**Returns** the read/control slice of the XR state:

- **isPresenting** (`() => boolean`): Reactive — `true` while a session is presenting.
- **session** (`() => XRSession | undefined`): Reactive — the active session, or `undefined`.
- **exit** (`() => Promise<void>`): Ends the active session.

> `useXR` throws if called outside a `<xr.Provider>`. For controller and hand poses, read the `XRFrame` from [`useFrame`](#useframe)'s third argument — that does not require `useXR`.


### useLoader

Manages asynchronous resource loading, such as textures or models, and integrates with `solid-js`' reactivity system. This hook can be used with Solid's `<Suspense>` to handle loading states.
Expand Down
Loading
Loading