{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "fabricator",
  "title": "Fabricator",
  "description": "An additive build cell that makes its own workpiece: a three-axis deposition head over a build plate, laying a sampled solid voxel by voxel, with a resolution axis that rebuilds the same object out of smaller cells and four camera angles.",
  "registryDependencies": [
    "https://robocn.dev/r/robot-kinematics.json",
    "https://robocn.dev/r/robot-style.json",
    "https://robocn.dev/r/use-robot-motion.json",
    "https://robocn.dev/r/voxel-geometry.json"
  ],
  "files": [
    {
      "path": "src/components/ui/fabricator.tsx",
      "content": "\"use client\"\n\n/**\n * fabricator — an additive build cell that makes its own workpiece.\n *\n * Everything else in the set moves; this one makes something. The object on\n * the plate is not artwork: it is a continuous occupancy field sampled at\n * `resolution` and laid cell by cell, so turning the resolution up builds the\n * same object out of smaller voxels rather than drawing a different picture.\n * Every voxel is an SVG path, so the whole thing scales without pixels.\n *\n * The three axes all come off one number. Deposition order is a serpentine\n * raster through the solid, progress is an index into it, and the cell at that\n * index is where the bridge, the carriage and the quill are pointing — in\n * controlled mode as much as under a behaviour.\n *\n * The cell is modelled once in world units — x starboard, y up, z toward the\n * tail — and pushed through `robotCamera(view)`.\n */\n\nimport * as React from \"react\"\n\nimport { arrowStep, useRobotDrag, useRobotScalar } from \"@/hooks/use-robot-motion\"\nimport { clamp, type Vec2 } from \"@/lib/robocn/kinematics\"\nimport {\n  extrudedPath,\n  px,\n  resolveRobotPalette,\n  resolveRobotSize,\n  robotCamera,\n  robotSurface,\n  roundedFootprint,\n  type RobotPaletteProps,\n  type RobotSize,\n  type RobotVariant,\n  type RobotView,\n} from \"@/lib/robocn/style\"\nimport {\n  VOXEL_DEFAULT_RESOLUTION,\n  voxelBuild,\n  voxelGoal,\n  voxelLaid,\n  voxelPaths,\n  voxelRefine,\n  voxelResolution,\n  voxelSite,\n  voxelSolid,\n  type VoxelBehavior,\n  type VoxelShape,\n} from \"@/lib/robocn/voxel\"\nimport { cn } from \"@/lib/utils\"\n\nconst VIEW = 200\n/** Build volume: half-extent in x and z, so the cube runs y 0 to 2 × HALF. */\nconst HALF = 32\nconst VOLUME = HALF * 2\n/** Build plate, and how far it hangs below the build surface. */\nconst PLATE_HALF = 39\nconst PLATE_DROP = 8\n/** Enclosure columns and the rails that tie their heads together. */\nconst POST = 46\nconst POST_HALF = 2.5\nconst RAIL_Y = 90\n/** The bridge beam the carriage traverses. */\nconst BRIDGE_Y = 78\n/** Nozzle standoff above the cell being laid, and where it parks with none. */\nconst NOZZLE_GAP = 7\nconst PARK_Y = 70\n/** Build fraction per second while the machine catches up with its goal. */\nconst BUILD_RATE = 0.6\n\n/** Screen y of the world origin, chosen so each view fills the frame. */\nconst anchors: Record<RobotView, number> = {\n  plan: 100,\n  front: 140,\n  profile: 140,\n  iso: 137,\n}\n\nconst viewNames: Record<RobotView, string> = {\n  plan: \"plan view\",\n  front: \"front elevation\",\n  profile: \"side elevation\",\n  iso: \"isometric view\",\n}\n\nexport interface FabricatorProps\n  extends Omit<React.ComponentProps<\"svg\">, \"color\">,\n    RobotPaletteProps {\n  /** What the cell is making. Each is a field, sampled — not a stored model. */\n  shape?: VoxelShape\n  /**\n   * Voxels along one edge, clamped to 2–14. Omit it and the machine uses its\n   * default, or animates it under the `refine` behavior.\n   */\n  resolution?: number\n  /** Controlled build fraction, 0–1. Omit it and the build runs `behavior`. */\n  progress?: number\n  /** What the cell does when `progress` is not supplied. */\n  behavior?: VoxelBehavior\n  /** Build cycles per second. */\n  speed?: number\n  animate?: boolean\n  paused?: boolean\n  phase?: number\n  /** Drag up and down to lay or strip material; arrow keys step a layer. */\n  interactive?: boolean\n  onProgressChange?: (progress: number) => void\n  /** Where the camera stands. */\n  view?: RobotView\n  /** The columns and rails around the build volume. */\n  showEnclosure?: boolean\n  showGround?: boolean\n  /** The build line readout under the plate. */\n  showReadout?: boolean\n  /** Status lamp on the carriage. Omit it and it lights while material is landing. */\n  signal?: \"idle\" | \"ready\" | \"warning\"\n  label?: string\n  size?: RobotSize | number\n  variant?: RobotVariant\n}\n\nfunction Fabricator({\n  shape = \"sphere\", resolution, progress, behavior = \"build\", speed = 0.12,\n  animate = true, paused = false, phase = 0,\n  interactive = false, onProgressChange,\n  view = \"iso\", showEnclosure = true, showGround = true, showReadout = true,\n  signal, label, size = \"md\", variant = \"solid\",\n  color, accent, metal, dark, glow, grid, palette: paletteOverride,\n  className, style, role, tabIndex, onKeyDown, onBlur, ...props\n}: FabricatorProps) {\n  const palette = resolveRobotPalette({ color, accent, metal, dark, glow, grid, palette: paletteOverride })\n  const width = resolveRobotSize(size)\n  const svgRef = React.useRef<SVGSVGElement>(null)\n  const [held, setHeld] = React.useState<number | null>(null)\n\n  const controlled = progress !== undefined\n  const hold = controlled\n    ? (Number.isFinite(progress) ? clamp(progress, 0, 1) : 0)\n    : held\n  const goal = React.useCallback(\n    (clock: number) => voxelGoal(behavior, clock),\n    [behavior],\n  )\n  const motion = useRobotScalar(goal, {\n    rate: BUILD_RATE,\n    hold,\n    speed,\n    animate: animate && !controlled && behavior !== \"static\",\n    paused,\n    phase,\n  })\n  const built = clamp(motion.value, 0, 1)\n\n  // `refine` is the one behavior that drives the resolution rather than the\n  // build line; a supplied resolution wins over it the way progress does.\n  const cells = resolution !== undefined\n    ? voxelResolution(resolution)\n    : behavior === \"refine\"\n      ? voxelResolution(voxelRefine(motion.clock))\n      : VOXEL_DEFAULT_RESOLUTION\n\n  const solid = React.useMemo(() => voxelSolid({ shape, resolution: cells }), [shape, cells])\n  const laid = voxelLaid(solid, built)\n  const part = React.useMemo(() => voxelBuild(solid, laid), [solid, laid])\n\n  const apply = React.useCallback((next: number) => {\n    const bounded = clamp(Number.isFinite(next) ? next : 0, 0, 1)\n    setHeld(bounded)\n    onProgressChange?.(bounded)\n  }, [onProgressChange, setHeld])\n  const dragging = useRobotDrag(svgRef, {\n    enabled: interactive,\n    // Up is more material, whichever way the camera is looking.\n    onDrag: React.useCallback((unit: Vec2) => apply(1 - unit.y), [apply]),\n    onDragEnd: React.useCallback(() => setHeld(null), [setHeld]),\n  })\n\n  const camera = React.useMemo(() => robotCamera(view), [view])\n  const anchor = anchors[view] ?? anchors.iso\n  const tipped = camera.lift > 0.01\n  const at = (x: number, y: number, z: number) => camera.project(x, y, z)\n  const line = (a: Vec2, b: Vec2) => `M ${px(a.x)} ${px(a.y)} L ${px(b.x)} ${px(b.y)}`\n\n  const cursor = part.active\n  const head = cursor ? voxelSite(cursor, solid.resolution, HALF) : null\n  const headX = head ? head.x : 0\n  const headZ = head ? head.z : 0\n  const crown = head ? head.top : 0\n  const nozzleY = head ? crown + NOZZLE_GAP : PARK_Y\n\n  const shell = robotSurface(\"shell\", variant, palette)\n  const machined = robotSurface(\"metal\", variant, palette)\n  const cast = robotSurface(\"dark\", variant, palette)\n  const shade = variant === \"solid\" && tipped\n  const lamp = signal === \"warning\"\n    ? palette.shell\n    : signal === \"idle\"\n      ? palette.metal\n      : signal === \"ready\" || head\n        ? palette.accent\n        : palette.metal\n\n  // One path per drawn cell, painted back to front. Rebuilt only when the laid\n  // count, the shape or the camera changes — not every frame.\n  const voxels = React.useMemo(\n    () => voxelPaths(part.surface, solid.resolution, camera, HALF),\n    [part, solid, camera],\n  )\n\n  const posts = [-POST, POST].flatMap((x) => [-POST, POST].map((z) => ({\n    id: `${x}-${z}`,\n    path: extrudedPath(square(x, z, POST_HALF), camera, RAIL_Y, -PLATE_DROP),\n    depth: camera.depth(x, 0, z),\n  })))\n  const plateFootprint = roundedFootprint(PLATE_HALF, PLATE_HALF, 7, 5)\n  const bridge = extrudedPath(\n    roundedFootprint(POST, 4, 3, 4).map((point) => ({ x: point.x, y: point.y + headZ })),\n    camera, BRIDGE_Y + 3.5, BRIDGE_Y - 3.5,\n  )\n  const carriage = extrudedPath(\n    roundedFootprint(7, 6, 2, 4).map((point) => ({ x: point.x + headX, y: point.y + headZ })),\n    camera, BRIDGE_Y + 6, BRIDGE_Y - 5,\n  )\n  const nozzle = extrudedPath(\n    square(headX, headZ, 2.6), camera, nozzleY + 6, nozzleY,\n  )\n  const quillTop = at(headX, BRIDGE_Y - 4, headZ)\n  const quillTip = at(headX, nozzleY + 6, headZ)\n  const spout = at(headX, nozzleY, headZ)\n  const target = head ? at(headX, crown, headZ) : spout\n  const shadow = at(0, -PLATE_DROP, 0)\n\n  const percent = Math.round(built * 100)\n  const readout = `${percent}% · ${solid.resolution}³`\n  const frame = `translate(${VIEW / 2} ${px(anchor)})`\n\n  return (\n    <svg ref={svgRef}\n      role={role ?? (interactive ? \"slider\" : \"img\")}\n      aria-label={`Voxel fabricator building a ${solid.shape}, ${solid.resolution} voxels per edge, ${percent} per cent complete, ${viewNames[view] ?? viewNames.iso}`}\n      aria-valuemin={interactive ? 0 : undefined}\n      aria-valuemax={interactive ? 100 : undefined}\n      aria-valuenow={interactive ? percent : undefined}\n      aria-valuetext={interactive ? `${percent} per cent, layer ${Math.min(part.layer + 1, solid.layers)} of ${solid.layers}` : undefined}\n      tabIndex={tabIndex ?? (interactive ? 0 : undefined)}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (!interactive || event.defaultPrevented) return\n        // Arrows move the build line a whole layer, which is what the machine\n        // actually does between passes.\n        const layer = 1 / Math.max(1, solid.layers)\n        const delta = arrowStep(event.key, event.shiftKey ? layer * 3 : layer, layer * 5)\n        if (delta !== 0) apply(built + delta)\n        else if (event.key === \"Home\") apply(0)\n        else if (event.key === \"End\") apply(1)\n        else return\n        event.preventDefault()\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        if (!dragging) setHeld(null)\n      }}\n      viewBox=\"0 0 200 200\" width={width} height={width}\n      className={cn(\"max-w-full select-none\", interactive && \"cursor-grab touch-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[currentColor]\", dragging && \"cursor-grabbing\", className)}\n      style={{ color: palette.foreground, ...style }} {...props}>\n\n      <g data-cell transform={frame}>\n      {variant === \"blueprint\" && (\n        <g data-annotation fill=\"none\" stroke={palette.grid} strokeWidth={0.5} strokeDasharray=\"2 3\" opacity={0.6}>\n          {/* The build volume itself: the box the field is sampled inside. */}\n          {[0, VOLUME].map((y) => (\n            <path key={y} d={polygon([\n              at(-HALF, y, -HALF), at(HALF, y, -HALF), at(HALF, y, HALF), at(-HALF, y, HALF),\n            ])} />\n          ))}\n          {[-HALF, HALF].flatMap((x) => [-HALF, HALF].map((z) => (\n            <path key={`${x}-${z}`} d={line(at(x, 0, z), at(x, VOLUME, z))} />\n          )))}\n        </g>\n      )}\n\n      {showGround && variant !== \"wire\" && (\n        <ellipse data-shadow cx={px(shadow.x)} cy={px(shadow.y + 5)}\n          rx={PLATE_HALF + 6} ry={px((PLATE_HALF + 6) * Math.max(0.12, camera.flatten))}\n          fill={palette.dark} opacity={0.12} />\n      )}\n\n      {showEnclosure && (\n        <g data-enclosure=\"far\">\n          {posts.filter((post) => post.depth <= 0).map((post) => (\n            <path key={post.id} d={post.path} {...cast} opacity={tipped ? 0.7 : 1} />\n          ))}\n        </g>\n      )}\n\n      <g data-plate>\n        <path d={extrudedPath(plateFootprint, camera, 0, -PLATE_DROP)} {...cast} />\n        <path d={polygon(plateFootprint.map((point) => camera.project(point.x, 0, point.y)))} {...machined} />\n        {/* The footprint of the build volume, printed on the plate. */}\n        <path d={polygon([\n          at(-HALF, 0.1, -HALF), at(HALF, 0.1, -HALF), at(HALF, 0.1, HALF), at(-HALF, 0.1, HALF),\n        ])} fill=\"none\" stroke={palette.grid} strokeWidth={0.7} strokeDasharray=\"3 2\" />\n      </g>\n\n      <g data-workpiece data-view={view}>\n        {voxels.map((voxel) => (\n          <React.Fragment key={voxel.id}>\n            <path data-voxel data-layer={voxel.layer} d={voxel.hull} {...shell} strokeWidth={px(Math.max(0.2, 3 / solid.resolution))} />\n            {shade && <path d={voxel.hull} fill={palette.dark} opacity={0.2} />}\n            {voxel.cap && <path data-face=\"top\" d={voxel.cap} {...shell} strokeWidth={px(Math.max(0.2, 3 / solid.resolution))} />}\n          </React.Fragment>\n        ))}\n      </g>\n\n      {head && variant !== \"wire\" && (\n        <g data-beam>\n          <path d={line(spout, target)} stroke={palette.glow} strokeWidth={4} strokeLinecap=\"round\" opacity={0.25} />\n          <path d={line(spout, target)} stroke={palette.accent} strokeWidth={1.4} strokeLinecap=\"round\" />\n        </g>\n      )}\n\n      <g data-gantry>\n        <path data-bridge d={bridge} {...machined} />\n        <g data-head>\n          {/* The quill: the Y axis, visible as the column that extends. */}\n          <path d={line(quillTop, quillTip)} stroke={palette.dark} strokeWidth={7} strokeLinecap=\"round\" />\n          <path d={line(quillTop, quillTip)} stroke={palette.metal} strokeWidth={4} strokeLinecap=\"round\" />\n          <path data-nozzle d={nozzle} {...shell} />\n        </g>\n        <path data-carriage d={carriage} {...shell} />\n        <circle data-lamp cx={px(at(headX, BRIDGE_Y + 6, headZ).x)} cy={px(at(headX, BRIDGE_Y + 6, headZ).y)} r={2.4} fill={lamp} />\n      </g>\n\n      {showEnclosure && (\n        <g data-enclosure=\"near\">\n          {posts.filter((post) => post.depth > 0).map((post) => (\n            <path key={post.id} d={post.path} {...cast} />\n          ))}\n          {[-POST, POST].map((z) => (\n            <path key={`x${z}`} d={extrudedPath(\n              roundedFootprint(POST, 2, 1.5, 3).map((point) => ({ x: point.x, y: point.y + z })),\n              camera, RAIL_Y, RAIL_Y - 4,\n            )} {...cast} />\n          ))}\n        </g>\n      )}\n\n      </g>\n\n      {showReadout && (\n        <text data-readout x={100} y={191} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\"\n          fontSize={5.5} fill={palette.foreground}>{readout}</text>\n      )}\n      {label && (\n        <text x={100} y={198} textAnchor=\"middle\" fontFamily=\"ui-monospace, monospace\"\n          fontSize={4.5} fill={palette.foreground} opacity={0.75}>{label}</text>\n      )}\n    </svg>\n  )\n}\n\n/** A cell's plan-view footprint: x starboard, y toward the tail. */\nconst square = (x: number, z: number, half: number): Vec2[] => [\n  { x: x - half, y: z - half },\n  { x: x + half, y: z - half },\n  { x: x + half, y: z + half },\n  { x: x - half, y: z + half },\n]\n\nconst polygon = (points: Vec2[]) =>\n  `${points.map((point, index) => `${index ? \"L\" : \"M\"} ${px(point.x)} ${px(point.y)}`).join(\" \")} Z`\n\nexport { Fabricator }\n",
      "type": "registry:ui",
      "target": "@ui/fabricator.tsx"
    }
  ],
  "categories": [
    "robotics",
    "fabrication"
  ],
  "type": "registry:ui"
}