# @expofp/renderer

> ExpoFP SDK internal: WebGL floorplan renderer

Latest version **3.45.9** (published 2026-10-20) · MIT license · 0 weekly downloads

## Install

```sh
npm install @expofp/renderer
pnpm add @expofp/renderer
yarn add @expofp/renderer
bun add @expofp/renderer
```

## Health

**Score 55/100 (C)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads.

Negative: insecure dependencies.

## Facts

| | |
|---|---|
| Version | 3.45.9 |
| Published | 2026-10-20 |
| First published | 2025-11-25 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 12 |
| Unpacked size | 1.4 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | alexsh_expofp, natalliakaranevichexpofp, daniel.blinder, timur_expofp, rodionnikolaev, korchemkin, 1evgenyt |

## Links

- npm: https://www.npmjs.com/package/@expofp/renderer
- Repository: https://github.com/expofp/efp-app
- Homepage: https://developer.expofp.com/
- npm.io page: https://npm.io/package/@expofp/renderer

## Dependencies (12)

- [three](https://npm.io/package/three.md) ^0.185.0
- [tslib](https://npm.io/package/tslib.md) ^2.3.0
- [colord](https://npm.io/package/colord.md) ^2.9.3
- [lil-gui](https://npm.io/package/lil-gui.md) ^0.21.0
- [stats-gl](https://npm.io/package/stats-gl.md) ^4.2.1
- [mjolnir.js](https://npm.io/package/mjolnir.js.md) ^3.1.0
- [webgl-memory](https://npm.io/package/webgl-memory.md) ^1.1.3
- [camera-controls](https://npm.io/package/camera-controls.md) github:expofp/camera-controls#v3.1.3-expofp.7
- [maxrects-packer](https://npm.io/package/maxrects-packer.md) ^2.7.3
- [@expofp/geometry](https://npm.io/package/@expofp/geometry.md) 3.45.9
- [troika-three-text](https://npm.io/package/troika-three-text.md) ^0.52.5
- [@react-three/fiber](https://npm.io/package/@react-three/fiber.md) ^9.6.1

## Recent versions

- 3.45.9 (latest) — 2026-10-20
- 3.44.5 (stable) — 2026-10-08
- 3.45.8 — 2026-10-20
- 3.45.7 — 2026-10-19
- 3.45.6 — 2026-10-16
- 3.45.5 — 2026-10-15
- 3.45.4 — 2026-10-15
- 3.45.3 — 2026-10-14
- 3.45.2 — 2026-10-13
- 3.45.1 — 2026-10-13
- 3.45.0 — 2026-10-13
- 3.44.8 — 2026-10-12
- 3.44.7 — 2026-10-12
- 3.44.6 — 2026-10-09
- 3.44.4 — 2026-10-08
- … 74 more at https://npm.io/package/@expofp/renderer/versions

## README

# @expofp/renderer

Interactive floor plan renderer built on [react-three-fiber](https://r3f.docs.pmnd.rs/).

## Architecture

- **`Renderer`** (`src/Renderer.tsx`) — the public entry: mounts the R3F
  `Canvas` and the scene (`controls/`, `lighting/`, `batch/`). Pass `debug` to
  enable the debug overlay.
- **Scene topics** live in their own folders (`batch/`, `controls/`,
  `lighting/`, …); add new rendering features as sibling topic folders.
- **`debug/`** — the debug overlay (lil-gui panel + stats-gl overlay), loaded
  through a single dynamic `import()` in `Renderer`. Everything under `debug/`
  (and its heavier deps, lil-gui + stats-gl) is code-split into one lazily
  loaded chunk that is **never fetched when `debug` is off**. Add new debug
  pieces to `debug/index.tsx` so they ride along in the same chunk.

## Navigation

`RendererProps` carries two different kinds of thing:

- **Camera limits** — `minZoom` / `maxZoom` / `minPitch` / `maxPitch`, declared as
  `CameraProps` in `src/space/Camera.tsx`. They constrain the camera itself, so
  they hold for a programmatic move as much as for a dragged one. `Camera` writes
  them and defaults them, beside the clipping planes derived from the same
  numbers; `maxPitch` both defaults to and is capped at `MAX_PITCH` (degrees, in
  `clipping.ts`), the angle every depth calculation in the renderer is sized
  against.
- **`controls`** — what a _user_ may do. Each handler takes `false`, `true` or
  omitted for its defaults, or an options object.

```tsx
<Renderer
  scene={scene}
  minZoom={0.1}
  maxZoom={100}
  minPitch={0}
  maxPitch={85}
  controls={{
    pan: { inertia: { durationMs: 600 } },
    zoom: true,
    roll: { threshold: 25 },
    pitch: false,
  }}
/>
```

Pan and zoom default on; **roll and pitch default on only in a development
build**, so this package's Storybook and dev server have them and a production
plan does not until a caller asks. An explicit value always wins.

Each handler is a component under `src/controls/handlers/`, mirroring the
imperative renderer's `ui/handlers/`, and owns its own config type and defaults.
Mounting is enabling: a handler claims its mouse and touch actions on mount and
gives them back on unmount, and the controls are built with every action switched
off. To add a gesture, write a sibling component and a field on `ControlsConfig`.

`Camera` writes the limits in two effects — the pitch limits, and the framing
(distance limits, far plane, and the dolly that preserves zoom across a resize),
which is one transaction because the dolly must follow the limits.

The controls are reached two ways and no other: `useCameraControls()` reads
them, and `useControlsEffect(effect)` runs an effect against them once they
exist, handing them over as an argument. The React Compiler rejects assigning to
a _field_ of a hook's return value — a method call on it, or passing it to a
function that does the assigning, is fine — so a component with a field to write
either routes it through a helper or uses `useControlsEffect`. Two-finger
gestures come off `useGesture(type, handler)`.

## Adding GUI controls from a component

`debug/gui-context.ts` exposes `useGui()`. It returns the active lil-gui
instance, or `null` when the overlay is disabled or still loading — so guard on a
non-null value. `useGui` is imported from the tiny `gui-context` module (a
type-only lil-gui import), so consuming it does **not** pull lil-gui into the
main chunk.

```tsx
function MyThing() {
  const gui = useGui();
  useEffect(() => {
    if (!gui) return;
    const folder = gui.addFolder('My thing');
    folder.add(/* … */);
    return () => folder.destroy();
  }, [gui]);
  // …
}
```

## Recommended extensions

Two browser extensions make what the renderer draws inspectable:

- **[Three.js DevTools](https://chromewebstore.google.com/detail/threejs-devtools/jechbjkglifdaldbdbigibihfaclnkbo)**
  adds a DevTools tab showing the live scene tree — the quickest way to see how
  Defs translate into three.js objects. Hovering a mesh there highlights its
  geometry on the plan.
- **[SpectorJS](https://spector.babylonjs.com/)** lists every WebGL draw call of
  a captured frame, with the intermediate canvas state between them. It takes a
  little coaxing: tick **Show offscreen canvas(es)** to see the canvases React
  renders (this works inconsistently — closing and reopening the extension a
  couple of times usually gets there), pick the canvas from the **Choose
  canvas…** dropdown by matching its dimensions, then press the red **Record**
  button. In debug mode the capture opens immediately, because the scene
  re-renders every frame; without it you have to trigger a frame first, most
  easily by panning or zooming slightly.

## Running unit tests

Run `nx test @expofp/renderer` to execute the unit tests via [Vitest](https://vitest.dev/).

## Limits of the published package

**`dist` is not supported outside this repository. It does not even resolve.**
`dist/objects/images/worker/client.js` holds
`import AtlasWorker from './atlas-worker?worker&inline'`, and `dist/index.js`
reaches that file. The `?worker&inline` specifier is a Vite instruction, so
only Vite can answer it. Any other consumer — a different bundler, plain Node,
a browser loading the files as they are — fails to resolve the import and stops
there. Nothing in the package loads, so there is no smaller part of it to fall
back on.

This package has no bundler. Its `build` target is plain `tsc`, so `dist` is
file-per-module TypeScript output and nothing else runs over it. `tsc` copies
the specifier through unchanged, which is where the broken import comes from.
The same absence of a bundler means the React Compiler never runs on `dist`
either, so the components there ship unoptimized.

Nothing in this repository is affected, because nothing in this repository
consumes `dist`. The renderer is compiled from its own source instead.
Floorplan's browser build reaches `src` through the `@expofp/source` condition
in `vite.config.mts`, so Vite answers the specifier and inlines the worker
script into the bundle; floorplan also applies its own React Compiler pass.
This package's own unit tests and Storybook import `src` by relative path, so
Vite answers the specifier for them too: the Storybook run starts the real
worker, and the unit tests drive the protocol against a fake one. Wayfinding
imports types only, which are erased at emit. The offline CLI copies
floorplan's browser bundle rather than importing anything.

To lift the limit, give this package a Vite library build in the shape
floorplan already uses. Keep `build` (tsc) for the types. Add `build-browser`
(Vite) for the bundle. Put a `browser` condition in the exports map, above
`import`. That answers the worker specifier and runs the React Compiler, so
both problems above go with it.

---
_Source: https://npm.io/package/@expofp/renderer · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
