@expofp/renderer

中文文档
3.49.2发布于 2026/11/14MITTypeScriptES Module近 30 天下载 8633

ExpoFP SDK internal: WebGL floorplan renderer

原始使用文档
文档目录6 节
  1. Architecture
  2. Navigation
  3. Adding GUI controls from a component
  4. Recommended extensions
  5. Running unit tests
  6. Limits of the published package

@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]);
  // …
}

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.

版本与发布记录

@expofp/renderer 共发布 113 个版本,首次发布于 2025/11/26;过去 12 个月发布了 113 个版本。

版本发布日期距今
3.49.2latest2026/11/14今天
0.0.0-experimental.3b0db16feat/wayfinding-compiler2026/11/13今天
3.49.12026/11/121 天前
3.49.02026/11/112 天前
3.48.02026/11/112 天前
3.47.22026/11/103 天前

其他发布通道:multi-venue → 0.0.0-experimental.cf063fc,stable → 3.47.0,eurotier → 0.0.0-experimental.6dabc65,feat/wayfinding-compiler → 0.0.0-experimental.3b0db16。

关于 @expofp/renderer 的常见问题

@expofp/renderer 是什么?

ExpoFP SDK internal: WebGL floorplan renderer

如何安装 @expofp/renderer?

在项目目录运行 npm install @expofp/renderer,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:debug(^4.4.3)、react(^19.2.8)、react-dom(^19.2.8)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@expofp/renderer 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@expofp/renderer 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@expofp/renderer 可以商用吗?

@expofp/renderer 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@expofp/renderer 的下载量有多少?

根据 npm 官方下载统计,@expofp/renderer 在 2026/10/14 – 2026/11/12 这 30 天内被下载 8,633 次,日均约 288 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@expofp/renderer 还在维护吗?

最新版本 3.49.2 发布于 2026/11/14(今天);至今共发布 113 个版本,其中过去 12 个月发布了 113 个;首次发布于 2025/11/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。