Plan Pad

plan-padControls

N labelled points on a scaled top-down plan.

$ pnpm dlx shadcn@latest add @pluginui/core-plan-pad
Loading interactive preview…

Install

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

plane-geometrycontrol-mathcontrol-formatcontrol-themeuse-surface-pointeruse-element-size
Install using the registry URL
$ pnpm dlx shadcn@latest add https://pluginui.com/r/core-plan-pad.json

Usage

Connect the documented props to your application or host.

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

PropTypeDefaultDescription
width / depthnumber—The plane's extent in world units (metres, say). Sets the aspect ratio and the SVG user units of the underlay and overlay.
pointsPlanPadPoint[]—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 / yPlanPadAxis—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..180Bounds 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 / onSelectedChangestring | null—The selected point: highlighted, shown in the readout, and the one `pressEmpty="move-selected"` moves. Uncontrolled with defaultSelectedId.
viewMarginnumber0World units shown beyond the bounds on every side, for drawing outside them. The bounds are outlined when there is a margin.
gridSpacing / gridnumbergrid 4Grid lines every gridSpacing world units (1 for a metre grid), or grid divisions per axis.
maxHeight / maxWidthnumber360 / 420Size caps. The width shrinks to keep the aspect when the plan would be taller than maxHeight.
hitRadiusnumber18How 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.