File size: 14,561 Bytes
ce32766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Workbench · Arm Playground</title>
<meta name="description" content="Drive the SO-101 robot arm in a 3D bench, record your driving as a LeRobot-style episode, replay it blind, and teach a small policy from your own demonstrations. Seven missions, a companion to the Hugging Face Robotics Course.">
<meta property="og:title" content="Workbench: drive the SO-101, record it, replay it, teach it">
<meta property="og:description" content="A playable 3D bench for the Hugging Face Robotics Course: teleoperate the real arm's kinematics, record a demonstration, watch a blind replay miss, and train a toy policy on your own data.">
<meta property="og:image" content="https://bakulbadwal.github.io/arm-playground/docs/workbench.png">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%2315171b'/%3E%3Cpath d='M14 50 L30 34 L50 16' stroke='%23f2b705' stroke-width='7' stroke-linecap='round' fill='none'/%3E%3Ccircle cx='14' cy='50' r='5' fill='%23fff'/%3E%3Ccircle cx='30' cy='34' r='5' fill='%23fff'/%3E%3C/svg%3E">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow+Semi+Condensed:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
/* One lit workshop scene edge to edge; instrument-style panels pinned to its corners. A single daylight look by design. */
:root {
  color-scheme: light;
  --ink: #1d2730; --ink-soft: #51606c; --panel: rgba(250, 249, 244, 0.94); --panel-2: rgba(240, 238, 230, 0.9); --line: rgba(29, 39, 48, 0.16);
  --arm: #f2b705; --mat: #3d7c76; --bin: #2f6fdb; --cube: #d4472f; --ok: #23804f; --bad: #b93a26; --sky: #cfd9e2; --accent: #2f6fdb;
  --label: "Barlow Semi Condensed", "Arial Narrow", "Helvetica Neue", Arial, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
*, *::before, *::after { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { height: 100%; margin: 0; }
body { background: var(--sky); color: var(--ink); font-family: var(--label); font-size: 15px; overflow: hidden; -webkit-font-smoothing: antialiased; }
a { color: var(--accent); }
#stage { position: relative; width: 100%; height: 100%; overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none; }
#gl { position: absolute; inset: 0; width: 100%; height: 100%; display: block; cursor: grab; }
#gl:active { cursor: grabbing; }
.panel { position: absolute; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 10px 12px; box-shadow: 0 6px 22px rgba(29, 39, 48, 0.14); }
.eyebrow { font-size: 11.5px; font-weight: 600; letter-spacing: 0.11em; text-transform: uppercase; color: var(--ink-soft); }
.eyebrow a { color: inherit; text-decoration: none; } .eyebrow a:hover { text-decoration: underline; }
#head { top: 16px; left: 16px; width: min(440px, calc(100% - 32px)); max-height: calc(100% - 210px); overflow-y: auto; display: grid; gap: 6px; }
#note2 { display: none; font-size: 11.5px; color: var(--ink-soft); line-height: 1.35; border-top: 1px solid var(--line); padding-top: 5px; }
#head h1 { margin: 0; font-size: 24px; font-weight: 700; letter-spacing: 0.01em; line-height: 1.05; display: flex; align-items: center; justify-content: space-between; gap: 10px; }
#head h1 small { font-size: 12px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-soft); white-space: nowrap; }
.dots { display: flex; gap: 5px; }
.dots button { width: 22px; height: 6px; padding: 0; border-radius: 3px; background: rgba(29, 39, 48, 0.18); border: 0; }
.dots button.done { background: var(--ok); } .dots button.cur { background: var(--ink); }
#mTitle { font-size: 19px; font-weight: 700; line-height: 1.15; }
#mGoal { font-size: 15px; line-height: 1.4; }
#score { font-family: var(--mono); font-size: 12.5px; color: var(--ink-soft); font-variant-numeric: tabular-nums; }
.ask { background: var(--panel-2); border: 1px dashed var(--line); border-radius: 6px; padding: 8px 10px; display: grid; gap: 6px; }
.ask .q { font-weight: 600; }
.ask .opts { display: grid; gap: 5px; }
.ask .opts button { text-align: left; font-weight: 500; }
.ask .opts button.right { border-color: var(--ok); background: #e6f3ea; } .ask .opts button.wrong { border-color: var(--bad); background: #f7e4e0; }
.ask input[type=number] { font: 500 14px var(--mono); width: 96px; padding: 5px 8px; border-radius: 4px; border: 1px solid rgba(29, 39, 48, 0.3); }
.fb { font-size: 14px; line-height: 1.4; } .fb.good { color: var(--ok); } .fb.no { color: var(--bad); } .fb .why { color: var(--ink); display: block; margin-top: 3px; }
.btns { display: flex; flex-wrap: wrap; gap: 6px; }
button { font: 600 14px var(--label); letter-spacing: 0.02em; color: var(--ink); background: #fff; border: 1px solid rgba(29, 39, 48, 0.3); border-radius: 4px; padding: 6px 10px; cursor: pointer; }
button:hover { border-color: var(--ink); }
button:focus-visible { outline: 2px solid var(--bin); outline-offset: 2px; }
button.primary { background: var(--ink); color: #fff; border-color: var(--ink); }
button[aria-pressed="true"] { background: var(--ink); color: #fff; border-color: var(--ink); }
button.rec[aria-pressed="true"] { background: var(--bad); border-color: var(--bad); }
button:disabled { opacity: 0.45; cursor: default; }
#data { top: 16px; right: 16px; width: 280px; display: grid; gap: 6px; }
#data table { border-collapse: collapse; width: 100%; font-family: var(--mono); font-size: 12px; font-variant-numeric: tabular-nums; }
#data th { text-align: right; font: 600 11px var(--label); letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-soft); padding: 0 0 2px; }
#data th:first-child, #data td:first-child { text-align: left; }
#data td { padding: 1.5px 0; text-align: right; }
#episode { font-family: var(--mono); font-size: 12px; color: var(--ink-soft); border-top: 1px solid var(--line); padding-top: 6px; line-height: 1.5; }
#episode b { color: var(--ink); font-weight: 500; }
#toast { left: 50%; bottom: 22px; transform: translateX(-50%); font-size: 17px; font-weight: 600; padding: 9px 16px; max-width: calc(100% - 32px); text-align: center; }
#toast[data-kind="ok"] { background: var(--ok); color: #fff; border-color: transparent; }
#toast[data-kind="bad"] { background: var(--bad); color: #fff; border-color: transparent; }
#toast[data-kind="rec"] { background: var(--ink); color: #fff; border-color: transparent; }
#keys { left: 16px; bottom: 16px; font-size: 13.5px; display: grid; gap: 4px; max-width: 320px; }
kbd { font: 500 11.5px var(--mono); background: #fff; border: 1px solid rgba(29, 39, 48, 0.3); border-bottom-width: 2px; border-radius: 4px; padding: 0 5px; }
#note { font-size: 12px; color: var(--ink-soft); line-height: 1.35; border-top: 1px solid var(--line); padding-top: 5px; margin-top: 2px; }
#inset { position: absolute; border: 2px solid #fff; border-radius: 4px; box-shadow: 0 6px 22px rgba(29, 39, 48, 0.3); pointer-events: none; }
#inset span { position: absolute; left: 0; top: 0; font: 600 11px var(--label); letter-spacing: 0.08em; text-transform: uppercase; background: #fff; color: var(--ink); padding: 2px 6px; border-bottom-right-radius: 4px; }
#touch { position: absolute; inset: 0; pointer-events: none; }
#stick { position: absolute; left: 20px; bottom: 26px; width: 118px; height: 118px; border-radius: 50%; background: rgba(250, 249, 244, 0.72); border: 1px solid var(--line); pointer-events: auto; touch-action: none; }
#knob { position: absolute; left: 50%; top: 50%; width: 46px; height: 46px; margin: -23px 0 0 -23px; border-radius: 50%; background: var(--ink); opacity: 0.85; }
#tbtns { position: absolute; left: 152px; bottom: 26px; display: grid; grid-template-columns: 56px 56px; gap: 6px; pointer-events: auto; }
#tbtns button { height: 56px; padding: 0; font-size: 14px; touch-action: none; }
#tbtns #tgrip { grid-column: span 2; background: var(--arm); border-color: transparent; }
#joints { position: absolute; left: 16px; bottom: 16px; display: grid; gap: 4px; pointer-events: auto; }
#joints .jrow { display: grid; grid-template-columns: 42px 110px 42px 56px; gap: 6px; align-items: center; font-size: 13px; }
#joints .jrow button { height: 34px; padding: 0; font-size: 16px; touch-action: none; }
#joints .jrow output { font-family: var(--mono); font-size: 12px; text-align: right; }
#joints .jname { display: flex; align-items: center; gap: 6px; }
.sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; border: 1px solid var(--line); }
#teach { display: grid; gap: 6px; border-top: 1px solid var(--line); padding-top: 6px; }
#teachLog { font-family: var(--mono); font-size: 12px; line-height: 1.5; color: var(--ink-soft); }
#teachLog b { color: var(--ink); font-weight: 500; }
#fail { inset: 16px; display: grid; place-content: center; text-align: center; font-size: 17px; gap: 8px; max-width: 520px; margin: auto; }
#start { position: absolute; inset: 0; display: grid; place-content: center; background: rgba(29, 39, 48, 0.28); cursor: pointer; }
#start div { background: var(--panel); border-radius: 8px; padding: 16px 22px; font-size: 18px; font-weight: 600; box-shadow: 0 10px 30px rgba(0,0,0,0.25); text-align: center; max-width: 360px; line-height: 1.35; }
#start div small { display: block; font-weight: 500; font-size: 14px; color: var(--ink-soft); margin-top: 6px; }
@media (max-width: 760px), (pointer: coarse) {
  #keys { display: none; }
  #data { top: auto; right: 16px; left: 16px; width: auto; bottom: 160px; padding: 6px 10px; }
  #data table, #data .eyebrow { display: none; }
  #episode { border-top: 0; padding-top: 0; }
  #head { padding: 8px 10px; gap: 5px; max-height: 46%; }
  #head h1 { font-size: 20px; } #mTitle { font-size: 17px; } #mGoal { font-size: 14px; }
  #toast { bottom: auto; top: 50%; font-size: 15px; }
  button { padding: 6px 8px; font-size: 13.5px; }
  #joints { bottom: 200px; left: 16px; right: auto; }
  #joints .jrow { grid-template-columns: 38px 52px 38px 0; font-size: 11px; } #joints .jrow output { display: none; }
  #note2 { display: block; }
}
@media (min-width: 761px) and (pointer: fine) { #touch { display: none; } }
</style>
</head>
<body>
<div id="stage">
  <canvas id="gl" aria-label="A 3D workbench with an SO-101 robot arm, a red cube and a blue bin. Drag to look around."></canvas>

  <div class="panel" id="head">
    <div class="eyebrow"><a href="index.html#r3&step=6" title="Back to the five tabs">← Arm Playground</a> · Workbench · <span id="mUnit"></span></div>
    <h1>Workbench <small id="mCount"></small></h1>
    <div class="dots" id="mDots"></div>
    <div id="mTitle"></div>
    <div id="mGoal"></div>
    <div id="score"></div>
    <div id="mAsk"></div>
    <div class="btns" id="mBtns"></div>
    <div id="teach" hidden>
      <div class="btns">
        <button id="bDemo" class="rec" aria-pressed="false" title="Drive the task yourself while it records (R)">● Record a demo</button>
        <button id="bShow" title="Let the built-in demonstrator do one and record it">Show me a demo</button>
        <button id="bTrain" disabled title="Train the small policy on the demos recorded so far">Train</button>
        <button id="bRun" disabled title="Put the cube on a spot the policy has never seen and let it drive (P)">Run the policy</button>
      </div>
      <div id="teachLog">no demonstrations yet</div>
    </div>
    <div id="note2">The arm's joints, lengths and limits are the SO‑101 URDF's (same code as tab 5); its shapes are simplified. Grasping and falling are toys: no contact physics. Mission 7's policy is a small network that is handed the cube's position.</div>
    <div class="btns">
      <button id="bPrev">← Back</button>
      <button id="bNext" class="primary">Next mission →</button>
      <button id="bView" aria-pressed="false" title="See through the camera on the gripper (V)">Wrist view</button>
      <button id="bReset" title="A fresh cube for this mission">Reset cube</button>
    </div>
  </div>

  <div class="panel" id="data">
    <div class="eyebrow">One frame of a LeRobot episode</div>
    <table>
      <thead><tr><th>joint</th><th>state</th><th>action</th></tr></thead>
      <tbody id="rows"></tbody>
    </table>
    <div id="episode">no episode recorded yet</div>
  </div>

  <div class="panel" id="keys">
    <div id="keysDrive"><kbd>W</kbd> <kbd>A</kbd> <kbd>S</kbd> <kbd>D</kbd> move the hand · <kbd>E</kbd> up · <kbd>Q</kbd> down · <kbd>Space</kbd> grip (the jaws take a cube they close around)</div>
    <div id="keysJoint" hidden><kbd>1</kbd>–<kbd>5</kbd> turn a joint · hold <kbd>Shift</kbd> to turn it the other way · <kbd>Space</kbd> grip</div>
    <div><kbd>V</kbd> wrist view · <kbd>R</kbd> record · <kbd>P</kbd> replay / run · <kbd>N</kbd> next mission</div>
    <div>Click the scene once so the keys reach it. Drag it to look around; scroll to zoom. The mat's bold lines are 100 mm apart.</div>
    <div id="note">The arm's joints, lengths and limits are the SO‑101 URDF's, the same code as tab 5. Its shapes are simplified. Grasping and falling are toys: no contact physics. The gripper camera is this bench's own; the real SO‑101 dataset used two fixed cameras. The policy in mission 7 is a small network that is handed the cube's position.</div>
  </div>

  <div id="inset"><span id="insetLabel">wrist camera · what a policy sees</span></div>
  <div class="panel" id="toast" hidden></div>

  <div id="touch">
    <div id="stick" aria-label="Move the hand"><div id="knob"></div></div>
    <div id="tbtns">
      <button id="tup" aria-label="Hand up">▲ up</button><button id="tdown" aria-label="Hand down">▼ down</button>
      <button id="tgrip" aria-label="Close or open the gripper">GRIP</button>
    </div>
  </div>
  <div class="panel" id="joints" hidden></div>
  <div id="start" hidden><div>Click to start driving<small>W A S D move the hand · E up · Q down · Space grips</small></div></div>
  <div class="panel" id="fail" hidden><b>This page needs WebGL, and this browser didn't start it.</b><span>The rest of Arm Playground works without it: <a href="index.html#r3&step=1">tab 5 shows the same arm in 3D</a> on a plain canvas.</span></div>
</div>

<script src="docs/vendor/three.min.js"></script>
<script src="arm3d.js"></script>
<script src="workbench.js"></script>
</body>
</html>