Skip to content

tekr9d3r/robo-lab

Repository files navigation

Robo Lab

An educational robotics app running in the browser. A 3-axis industrial robot arm is controlled by a Claude AI brain. Users type natural-language commands; Claude generates keyframe motion plans; a spring-damper motor controller executes them at 60Hz through a forward-kinematics chain. The robot can physically interact with boxes via Rapier physics.

Tech Stack React Three Fiber Rapier Claude


Architecture

User command (natural language)
        │
        ▼
/api/brain  ──►  Claude Sonnet 4.6
                 Tool: execute_movement
                        │
                        ▼
               Keyframe array [{time_ms, joints}]
                        │
                        ▼
          motorController.startPlan(keyframes)
                        │
          useFrame() @ 60Hz
                        │
          ┌─────────────┴──────────────┐
          │  Interpolate between       │
          │  surrounding keyframes     │
          │  (linear lerp per joint)   │
          │             │              │
          │  applySpring(joint, target)│  ← spring-damper per DOF
          │             │              │
          │  clampToLimits(joint, val) │  ← hard joint limit enforcement
          │             │              │
          │  applyMotorTarget(joint)   │  ← writes to Three.js Group rotation
          └─────────────────────────── ┘
                        │
                        ▼
          FK chain propagates through scene graph
                        │
          ┌─────────────┴──────────────┐
          │  gripperTip.getWorldPos()  │  → liveGripperState
          │  armSegs.getWorldPos/Quat()│  → liveArmTransforms
          └─────────────────────────── ┘
                        │
                        ▼
          BoxArena (Rapier useBeforePhysicsStep)
          distance check → grab → kinematic carry → release

Tech Stack

Layer Choice
Framework Next.js 15 (App Router)
3D rendering React Three Fiber v9 + @react-three/drei
Physics @react-three/rapier v2.1 (Rapier WASM)
AI brain Claude Sonnet 4.6 via Anthropic API, tool_use forced output
State Zustand v5 with subscribeWithSelector
Memory localStorage via Zustand persist
Styling Tailwind CSS v4 + shadcn/ui

Robot

The arm is built entirely from Three.js geometric primitives — no imported models.

Base plate  (cylinder)
└── Base rotator  [Y-axis, ±π×4 — full spin]
    └── Shoulder joint  [X-axis, -0.1 … 1.40 rad]
        └── Upper arm  (box)
            └── Elbow joint  [X-axis, -0.2 … 1.80 rad]
                └── Forearm  (box)
                    └── Wrist joint  [X-axis, -2.2 … 1.6 rad]
                        └── Gripper palm  (box)
                            ├── Left fork jaw   [Z-axis,  0 … +0.7 rad]
                            └── Right fork jaw  [Z-axis, -0.7 … 0 rad]

Each joint is a Three.js <group> whose rotation axis is set by jointRegistry. The motor controller writes target angles to the registry; the FK chain propagates automatically through the scene graph.

Gripper: two-fork parallel gripper. Each jaw has a vertical prong pair (front + back, offset in ±Z) with rubber grip pads. Closed when leftFinger < 0.08 rad.


Motor Controller (src/lib/physics/motorController.ts)

The core loop runs inside React Three Fiber's useFrame at ~60Hz. No React state is written during this loop — only JS module-level singletons.

Spring-damper

Each DOF (degree of freedom) has independent position and velocity state:

acc = (target - cur) * k  -  vel * d
vel += acc * dt
cur += vel * dt

Spring parameters per joint family:

Joint Stiffness (k) Damping (d) Feel
baseRotate 22 7 Slightly underdamped — natural swing
shoulder 32 9 Underdamped — visible overshoot
elbow 38 10 Stiff, quick
wrist 30 8 Medium
fingers 55 20 Overdamped — no oscillation on grip

Fingers are overdamped (ζ > 1) deliberately: underdamped fingers oscillate across the grip threshold (0.08 rad), causing phantom release/grab cycles.

Keyframe interpolation

Claude returns an array of keyframes {time_ms, joints}. The controller linearly interpolates between the two surrounding keyframes each tick. After the last keyframe the pose is held in lastJoints so the arm doesn't snap back to rest.

Joint limit enforcement

clampToLimits is called every tick after the spring step. This prevents spring overshoot from ever exceeding the physical joint bounds — the arm cannot pass through the floor regardless of spring tuning.


Box Physics (src/components/scene/BoxArena.tsx)

25 wooden crates (0.28m cube, 1.5kg each) are dynamic Rapier RigidBody instances.

Grab logic (runs in useBeforePhysicsStep):

  1. Each frame, read gripper tip world position from liveGripperState
  2. On rising edge of isGripping: find nearest box within 0.5m
  3. Switch box to kinematicPosition, set heldIdx
  4. Every frame while held: setNextKinematicTranslation to gripper tip position
  5. On release: debounce 12 frames (~200ms) before switching back to dynamic

The debounce absorbs the underdamped finger spring briefly crossing the 0.08 rad threshold on approach, which would otherwise cause immediate release.


AI Brain (src/app/api/brain/route.ts)

Claude is called once per command via a POST to /api/brain. The response is a single execute_movement tool call with:

{
  movement_plan: string,       // reasoning
  keyframes: [{
    time_ms: number,
    joints: {
      [JointName]: number | [number, number, number]
    }
  }],
  educational_note: string,    // shown in UI
  success_criteria: string
}

tool_choice: { type: "tool", name: "execute_movement" } forces structured output — no free-text fallback.

The system prompt (~2000 tokens) is marked with cache_control: { type: "ephemeral" } and includes the full joint schema, coordinate system, IK guide, and few-shot examples. Prompt caching makes repeat calls significantly cheaper.


Memory System

Past successful movements are stored in localStorage via Zustand persist. On each new command, the 3 most similar past movements are retrieved using Jaccard similarity on tokenized command strings and injected into the Claude prompt. Claude is explicitly asked to build upon them.


Manual Control

A game-controller style panel lets you jog individual joints with buttons or keyboard:

Key Joint Direction
Q / E Base rotate CCW / CW
W / S Shoulder Up / Down
R / F Elbow Extend / Fold
T / G Wrist Up / Down
C / V Grip Open / Close
0 Reset all boxes

Key repeat is implemented with a 50ms setInterval holding a Set<string> of active keys — no reliance on browser key-repeat timing.


Vision Panel

Reads liveBoxPositions (populated each frame by BoxArena) at 5Hz. Shows the 8 nearest boxes with distance and coordinates. "Pick Up" buttons submit natural-language commands to the AI brain.


Environment Variables

ANTHROPIC_API_KEY=sk-ant-...

Local Development

npm install
npm run dev

Requires Node 20+. Rapier WASM is loaded asynchronously on first render (~2MB). The canvas is wrapped in <Suspense> to handle this.


Project Structure

src/
├── app/
│   ├── page.tsx                  # Main UI
│   ├── live/page.tsx             # Read-only visitor view (SSE)
│   └── api/
│       ├── brain/route.ts        # Claude keyframe generation
│       └── sse/route.ts          # Live viewer sync
├── components/
│   ├── robot/RobotArm.tsx        # FK arm assembly + gripper
│   ├── scene/
│   │   ├── HumanoidScene.tsx     # R3F Canvas + Physics wrapper
│   │   ├── BoxArena.tsx          # 25 physics boxes + grab logic
│   │   ├── Room.tsx              # 7×7×2.8m lab room geometry
│   │   └── ArmPhysicsBody.tsx    # Kinematic Rapier bodies tracking FK
│   └── ui/
│       ├── CommandInput.tsx
│       ├── ManualControlPanel.tsx
│       ├── VisionPanel.tsx
│       ├── JointReadouts.tsx
│       ├── KeyframeTimeline.tsx
│       └── MemoryPanel.tsx
├── lib/
│   ├── physics/
│   │   ├── motorController.ts    # Spring-damper FK driver
│   │   ├── jointRegistry.ts      # Joint name → Three.js Group map
│   │   ├── liveGripperState.ts   # Gripper tip world position
│   │   ├── liveArmTransforms.ts  # All segment world transforms
│   │   └── liveBoxPositions.ts   # Box world positions for vision
│   └── constants/
│       ├── jointLimits.ts        # Joint bounds + REST_POSE
│       └── bodyDimensions.ts
└── store/
    ├── robotStore.ts             # Joint readouts, plan status, UI flags
    └── memoryStore.ts            # Movement library + localStorage persist

About

A browser app where a Claude AI brain controls a physics-driven robot arm using natural-language commands, keyframe motion planning, and spring-damper forward kinematics.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages