Plan Pad
plan-padControlsN labelled points on a scaled top-down plan.
$ pnpm dlx shadcn@latest add @pluginui/core-plan-padInstall
Copies the source into your project. Anything it depends on comes with it. Run this in your React project with shadcn initialized; no registry configuration is needed.
Pulls in
Install using the registry URL
$ pnpm dlx shadcn@latest add https://pluginui.com/r/core-plan-pad.jsonUsage
Connect the documented props to your application or host.
import { PlanPad, planPadParamHandlers } from "@/components/ui/plan-pad"
// In a module face: one edit gesture brackets every parameter a drag moves.
const edit = useFaceParamEditGesture(face)
const value = (path: string) => faceParamBinding(face, path).value
<PlanPad
width={value("room.width")}
depth={value("room.depth")}
x={{ min: 0, max: 1 }}
y={{ min: 0, max: 1 }}
elevation={{ height: value("room.height"), z: { min: 0, max: 1 } }}
points={[
{ id: "source", label: "Source", x: value("source.x"), y: value("source.y"), z: value("source.z") },
{ id: "mic", label: "Mic", x: value("mic.x"), y: value("mic.y"), z: value("mic.z"),
direction: value("mic.azimuth"), directivity: 0.5 },
]}
{...planPadParamHandlers(
{
source: { x: "source.x", y: "source.y", z: "source.z" },
mic: { x: "mic.x", y: "mic.y", z: "mic.z", direction: "mic.azimuth" },
},
edit,
)}
/>About Plan Pad
Several points share one plane, each bound to two parameters. Use it for a source and microphones in a room, or speakers around a listener. The plane keeps the caller's width × depth aspect, so a 6 × 4 room draws as 3:2 at any size and a heading measured on screen matches a heading in the room. A point can carry a height, edited on an elevation strip drawn to the same horizontal scale, and a heading, drawn as a first-order polar glyph with a rotate handle. Values are clamped to per-point limits and snapped to the axis step before they leave the pad. Underlay and overlay slots draw in world units, including walls, image sources and rays. The room-acoustics face composes this general-purpose control.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| width / depth | number | — | The plane's extent in world units (metres, say). Sets the aspect ratio and the SVG user units of the underlay and overlay. |
| points | PlanPadPoint[] | — | Controlled points: id, label, x, y, and optionally z, direction (degrees, 0 along +x, counter-clockwise), directivity (1 omni … 0 figure-8), mark, color, shape, disabled, limits and double-click defaults. |
| onPointChange | (id, patch, next) => void | — | Fires on every move with only the fields that changed, already clamped and snapped. |
| onPointChangeEnd | (id, point, fields) => void | — | Fires once per gesture (a drag, a key press, a double-click) with every field it touched. |
| x / y | PlanPadAxis | — | Value axes: min, max, step, label, unit, format. Default to 0..width and 0..depth; pass 0..1 for normalised parameters and the plane still draws to scale. |
| elevation | { height; z?; label?; minHeight?; maxHeight? } | — | Adds an elevation strip under the plan, to the same horizontal scale, for every point with a z. |
| direction | { min?; max?; step?; format? } | -180..180 | Bounds of every heading. A full turn wraps; a narrower range clamps. |
| constrain | (id, next, current) => PlanPadPoint | — | Final check for a proposed point. Keep two mics apart or keep a source off a mic. The result is clamped and snapped again. |
| selectedId / onSelectedChange | string | null | — | The selected point: highlighted, shown in the readout, and the one `pressEmpty="move-selected"` moves. Uncontrolled with defaultSelectedId. |
| viewMargin | number | 0 | World units shown beyond the bounds on every side, for drawing outside them. The bounds are outlined when there is a margin. |
| gridSpacing / grid | number | grid 4 | Grid lines every gridSpacing world units (1 for a metre grid), or grid divisions per axis. |
| maxHeight / maxWidth | number | 360 / 420 | Size caps. The width shrinks to keep the aspect when the plan would be taller than maxHeight. |
| hitRadius | number | 18 | How near a press in open space must land to grab a point, in px. |
| pressEmpty | "none" | "move-selected" | "none" | What a press out of reach of every point does: nothing, or move the selected point there as an XY pad would. |
| underlay / overlay | (plane: PlanPadPlane) => ReactNode | — | SVG drawn under or over the points in world units (y down, origin at the top-left corner of the bounds). plane.toSvg places axis values; neither layer takes pointer events. |
Interaction
- Drag a point Moves it from where it was grabbed, without a jump, clamped to its limits and snapped to the step.
- Press near a point Within hitRadius grabs the nearest one; further out does nothing unless pressEmpty is move-selected.
- Drag the ring handle Turns the point's heading; the polar glyph follows.
- Elevation strip Drag a marker up or down to change that point's height.
- Arrows / Shift / Home / End On a focused point, the XY pad's keys: arrows move five steps (5%), Shift one (1%), Home and End park at the corners. [ and ] turn it.
- Tab Each point, its heading handle and its elevation marker are sliders in tab order.
- Double-click Returns a point, heading or height to its defaults.