npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@expofp/renderer

v3.45.9

Published

ExpoFP SDK internal: WebGL floorplan renderer

Readme

@expofp/renderer

Interactive floor plan renderer built on react-three-fiber.

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

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

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.