pepijn223 HF Staff commited on
Commit
eeeaa65
Β·
unverified Β·
1 Parent(s): e457eba

fix openarms urdf vis

Browse files
Files changed (1) hide show
  1. src/components/urdf-viewer.tsx +106 -93
src/components/urdf-viewer.tsx CHANGED
@@ -87,13 +87,10 @@ function autoMatchJoints(urdfJointNames: string[], columnKeys: string[]): Record
87
  return mapping;
88
  }
89
 
90
- // Tip link names to try (so101, so100, then openarm naming)
91
- const TIP_LINK_NAMES = [
92
- "gripper_frame_link", "gripperframe", "gripper_link", "gripper",
93
- "openarm_left_hand_tcp", "openarm_right_hand_tcp",
94
- ];
95
- const TRAIL_DURATION = 1.0; // seconds
96
- const TRAIL_COLOR = new THREE.Color("#ff6600");
97
  const MAX_TRAIL_POINTS = 300;
98
 
99
  // ─── Robot scene (imperative, inside Canvas) ───
@@ -109,47 +106,47 @@ function RobotScene({
109
  }) {
110
  const { scene, size } = useThree();
111
  const robotRef = useRef<URDFRobot | null>(null);
112
- const tipLinkRef = useRef<THREE.Object3D | null>(null);
113
  const [loading, setLoading] = useState(true);
114
  const [error, setError] = useState<string | null>(null);
115
 
116
- // Trail state
117
- const trailRef = useRef<{ positions: Float32Array; colors: Float32Array; times: number[]; count: number }>({
118
- positions: new Float32Array(MAX_TRAIL_POINTS * 3),
119
- colors: new Float32Array(MAX_TRAIL_POINTS * 3), // RGB, no alpha
120
- times: [],
121
- count: 0,
122
- });
123
- const lineRef = useRef<Line2 | null>(null);
124
- const trailMatRef = useRef<LineMaterial | null>(null);
125
 
126
- // Reset trail when episode changes
127
  useEffect(() => {
128
- trailRef.current.count = 0;
129
- trailRef.current.times = [];
130
- if (lineRef.current) lineRef.current.visible = false;
131
  }, [trailResetKey]);
132
 
133
- // Create trail Line2 object
134
- useEffect(() => {
135
- const geometry = new LineGeometry();
136
- const material = new LineMaterial({
137
- color: 0xffffff,
138
- linewidth: 4, // pixels
139
- vertexColors: true,
140
- transparent: true,
141
- worldUnits: false,
142
- });
143
- material.resolution.set(window.innerWidth, window.innerHeight);
144
- trailMatRef.current = material;
145
-
146
- const line = new Line2(geometry, material);
147
- line.frustumCulled = false;
148
- line.visible = false;
149
- lineRef.current = line;
150
- scene.add(line);
151
-
152
- return () => { scene.remove(line); geometry.dispose(); material.dispose(); };
 
 
 
 
 
153
  }, [scene]);
154
 
155
  useEffect(() => {
@@ -162,8 +159,22 @@ function RobotScene({
162
  // DAE (Collada) files β€” load with embedded materials
163
  if (url.endsWith(".dae")) {
164
  const colladaLoader = new ColladaLoader(mgr);
165
- colladaLoader.load(url, (collada) => onLoad(collada.scene), undefined,
166
- (err) => onLoad(new THREE.Object3D(), err as Error));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  return;
168
  }
169
  // STL files β€” apply custom materials
@@ -180,7 +191,7 @@ function RobotScene({
180
  color = url.includes("body_link0") ? "#3a3a4a" : "#f5f5f5";
181
  metalness = 0.15; roughness = 0.6;
182
  }
183
- const material = new THREE.MeshStandardMaterial({ color, metalness, roughness });
184
  onLoad(new THREE.Mesh(geometry, material));
185
  },
186
  undefined,
@@ -197,9 +208,14 @@ function RobotScene({
197
  robot.scale.set(scale, scale, scale);
198
  scene.add(robot);
199
 
200
- for (const name of TIP_LINK_NAMES) {
201
- if (robot.frames[name]) { tipLinkRef.current = robot.frames[name]; break; }
 
 
 
202
  }
 
 
203
 
204
  const movable = Object.values(robot.joints)
205
  .filter((j) => j.jointType === "revolute" || j.jointType === "continuous" || j.jointType === "prismatic")
@@ -212,9 +228,9 @@ function RobotScene({
212
  );
213
  return () => {
214
  if (robotRef.current) { scene.remove(robotRef.current); robotRef.current = null; }
215
- tipLinkRef.current = null;
216
  };
217
- }, [urdfUrl, scale, scene, onJointsLoaded]);
218
 
219
  const tipWorldPos = useMemo(() => new THREE.Vector3(), []);
220
 
@@ -222,61 +238,58 @@ function RobotScene({
222
  const robot = robotRef.current;
223
  if (!robot) return;
224
 
225
- // Apply joint values
226
  for (const [name, value] of Object.entries(jointValues)) {
227
  robot.setJointValue(name, value);
228
  }
229
  robot.updateMatrixWorld(true);
230
 
231
- // Update trail
232
- const line = lineRef.current;
233
- const tip = tipLinkRef.current;
234
- if (!line || !tip || !trailEnabled) {
235
- if (line) line.visible = false;
236
  return;
237
  }
238
 
239
- // Keep resolution in sync with viewport
240
- if (trailMatRef.current) trailMatRef.current.resolution.set(size.width, size.height);
241
-
242
- tip.getWorldPosition(tipWorldPos);
243
  const now = performance.now() / 1000;
244
- const trail = trailRef.current;
245
-
246
- // Add new point
247
- if (trail.count < MAX_TRAIL_POINTS) {
248
- trail.count++;
249
- } else {
250
- trail.positions.copyWithin(0, 3);
251
- trail.colors.copyWithin(0, 3);
252
- trail.times.shift();
253
- }
254
- const idx = trail.count - 1;
255
- trail.positions[idx * 3] = tipWorldPos.x;
256
- trail.positions[idx * 3 + 1] = tipWorldPos.y;
257
- trail.positions[idx * 3 + 2] = tipWorldPos.z;
258
- trail.times.push(now);
259
-
260
- // Update colors: fade from orange β†’ black based on age
261
- for (let i = 0; i < trail.count; i++) {
262
- const age = now - trail.times[i];
263
- const t = Math.max(0, 1 - age / TRAIL_DURATION);
264
- trail.colors[i * 3] = TRAIL_COLOR.r * t;
265
- trail.colors[i * 3 + 1] = TRAIL_COLOR.g * t;
266
- trail.colors[i * 3 + 2] = TRAIL_COLOR.b * t;
267
- }
 
 
 
 
 
 
268
 
269
- // Need at least 2 points for Line2
270
- if (trail.count < 2) { line.visible = false; return; }
271
-
272
- // Rebuild geometry (Line2 requires this)
273
- const geo = new LineGeometry();
274
- geo.setPositions(Array.from(trail.positions.subarray(0, trail.count * 3)));
275
- geo.setColors(Array.from(trail.colors.subarray(0, trail.count * 3)));
276
- line.geometry.dispose();
277
- line.geometry = geo;
278
- line.computeLineDistances();
279
- line.visible = true;
280
  });
281
 
282
  if (loading) return <Html center><span className="text-white text-lg">Loading robot…</span></Html>;
 
87
  return mapping;
88
  }
89
 
90
+ const SINGLE_ARM_TIP_NAMES = ["gripper_frame_link", "gripperframe", "gripper_link", "gripper"];
91
+ const DUAL_ARM_TIP_NAMES = ["openarm_left_hand_tcp", "openarm_right_hand_tcp"];
92
+ const TRAIL_DURATION = 1.0;
93
+ const TRAIL_COLORS = [new THREE.Color("#ff6600"), new THREE.Color("#00aaff")];
 
 
 
94
  const MAX_TRAIL_POINTS = 300;
95
 
96
  // ─── Robot scene (imperative, inside Canvas) ───
 
106
  }) {
107
  const { scene, size } = useThree();
108
  const robotRef = useRef<URDFRobot | null>(null);
109
+ const tipLinksRef = useRef<THREE.Object3D[]>([]);
110
  const [loading, setLoading] = useState(true);
111
  const [error, setError] = useState<string | null>(null);
112
 
113
+ type TrailState = { positions: Float32Array; colors: Float32Array; times: number[]; count: number };
114
+ const trailsRef = useRef<TrailState[]>([]);
115
+ const linesRef = useRef<Line2[]>([]);
116
+ const trailMatsRef = useRef<LineMaterial[]>([]);
117
+ const trailCountRef = useRef(0);
 
 
 
 
118
 
119
+ // Reset trails when episode changes
120
  useEffect(() => {
121
+ for (const t of trailsRef.current) { t.count = 0; t.times = []; }
122
+ for (const l of linesRef.current) l.visible = false;
 
123
  }, [trailResetKey]);
124
 
125
+ // Create/destroy trail Line2 objects when tip count changes
126
+ const ensureTrails = useCallback((count: number) => {
127
+ if (trailCountRef.current === count) return;
128
+ // Remove old
129
+ for (const l of linesRef.current) { scene.remove(l); l.geometry.dispose(); }
130
+ for (const m of trailMatsRef.current) m.dispose();
131
+ // Create new
132
+ const trails: TrailState[] = [];
133
+ const lines: Line2[] = [];
134
+ const mats: LineMaterial[] = [];
135
+ for (let i = 0; i < count; i++) {
136
+ trails.push({ positions: new Float32Array(MAX_TRAIL_POINTS * 3), colors: new Float32Array(MAX_TRAIL_POINTS * 3), times: [], count: 0 });
137
+ const mat = new LineMaterial({ color: 0xffffff, linewidth: 4, vertexColors: true, transparent: true, worldUnits: false });
138
+ mat.resolution.set(window.innerWidth, window.innerHeight);
139
+ mats.push(mat);
140
+ const line = new Line2(new LineGeometry(), mat);
141
+ line.frustumCulled = false;
142
+ line.visible = false;
143
+ lines.push(line);
144
+ scene.add(line);
145
+ }
146
+ trailsRef.current = trails;
147
+ linesRef.current = lines;
148
+ trailMatsRef.current = mats;
149
+ trailCountRef.current = count;
150
  }, [scene]);
151
 
152
  useEffect(() => {
 
159
  // DAE (Collada) files β€” load with embedded materials
160
  if (url.endsWith(".dae")) {
161
  const colladaLoader = new ColladaLoader(mgr);
162
+ colladaLoader.load(url, (collada) => {
163
+ if (isOpenArm) {
164
+ collada.scene.traverse((child) => {
165
+ if (child instanceof THREE.Mesh && child.material) {
166
+ const mat = child.material as THREE.MeshStandardMaterial;
167
+ if (mat.side !== undefined) mat.side = THREE.DoubleSide;
168
+ if (mat.color) {
169
+ const hsl = { h: 0, s: 0, l: 0 };
170
+ mat.color.getHSL(hsl);
171
+ if (hsl.l > 0.7) mat.color.setHSL(hsl.h, hsl.s, 0.55);
172
+ }
173
+ }
174
+ });
175
+ }
176
+ onLoad(collada.scene);
177
+ }, undefined, (err) => onLoad(new THREE.Object3D(), err as Error));
178
  return;
179
  }
180
  // STL files β€” apply custom materials
 
191
  color = url.includes("body_link0") ? "#3a3a4a" : "#f5f5f5";
192
  metalness = 0.15; roughness = 0.6;
193
  }
194
+ const material = new THREE.MeshStandardMaterial({ color, metalness, roughness, side: isOpenArm ? THREE.DoubleSide : THREE.FrontSide });
195
  onLoad(new THREE.Mesh(geometry, material));
196
  },
197
  undefined,
 
208
  robot.scale.set(scale, scale, scale);
209
  scene.add(robot);
210
 
211
+ const tipNames = isOpenArm ? DUAL_ARM_TIP_NAMES : SINGLE_ARM_TIP_NAMES;
212
+ const tips: THREE.Object3D[] = [];
213
+ for (const name of tipNames) {
214
+ if (robot.frames[name]) tips.push(robot.frames[name]);
215
+ if (!isOpenArm && tips.length === 1) break;
216
  }
217
+ tipLinksRef.current = tips;
218
+ ensureTrails(tips.length);
219
 
220
  const movable = Object.values(robot.joints)
221
  .filter((j) => j.jointType === "revolute" || j.jointType === "continuous" || j.jointType === "prismatic")
 
228
  );
229
  return () => {
230
  if (robotRef.current) { scene.remove(robotRef.current); robotRef.current = null; }
231
+ tipLinksRef.current = [];
232
  };
233
+ }, [urdfUrl, scale, scene, onJointsLoaded, ensureTrails]);
234
 
235
  const tipWorldPos = useMemo(() => new THREE.Vector3(), []);
236
 
 
238
  const robot = robotRef.current;
239
  if (!robot) return;
240
 
 
241
  for (const [name, value] of Object.entries(jointValues)) {
242
  robot.setJointValue(name, value);
243
  }
244
  robot.updateMatrixWorld(true);
245
 
246
+ const tips = tipLinksRef.current;
247
+ if (!trailEnabled || tips.length === 0) {
248
+ for (const l of linesRef.current) l.visible = false;
 
 
249
  return;
250
  }
251
 
 
 
 
 
252
  const now = performance.now() / 1000;
253
+ for (let ti = 0; ti < tips.length; ti++) {
254
+ const tip = tips[ti];
255
+ const trail = trailsRef.current[ti];
256
+ const line = linesRef.current[ti];
257
+ const mat = trailMatsRef.current[ti];
258
+ if (!trail || !line || !mat) continue;
259
+
260
+ mat.resolution.set(size.width, size.height);
261
+ tip.getWorldPosition(tipWorldPos);
262
+ const trailColor = TRAIL_COLORS[ti % TRAIL_COLORS.length];
263
+
264
+ if (trail.count < MAX_TRAIL_POINTS) {
265
+ trail.count++;
266
+ } else {
267
+ trail.positions.copyWithin(0, 3);
268
+ trail.colors.copyWithin(0, 3);
269
+ trail.times.shift();
270
+ }
271
+ const idx = trail.count - 1;
272
+ trail.positions[idx * 3] = tipWorldPos.x;
273
+ trail.positions[idx * 3 + 1] = tipWorldPos.y;
274
+ trail.positions[idx * 3 + 2] = tipWorldPos.z;
275
+ trail.times.push(now);
276
+
277
+ for (let i = 0; i < trail.count; i++) {
278
+ const t = Math.max(0, 1 - (now - trail.times[i]) / TRAIL_DURATION);
279
+ trail.colors[i * 3] = trailColor.r * t;
280
+ trail.colors[i * 3 + 1] = trailColor.g * t;
281
+ trail.colors[i * 3 + 2] = trailColor.b * t;
282
+ }
283
 
284
+ if (trail.count < 2) { line.visible = false; continue; }
285
+ const geo = new LineGeometry();
286
+ geo.setPositions(Array.from(trail.positions.subarray(0, trail.count * 3)));
287
+ geo.setColors(Array.from(trail.colors.subarray(0, trail.count * 3)));
288
+ line.geometry.dispose();
289
+ line.geometry = geo;
290
+ line.computeLineDistances();
291
+ line.visible = true;
292
+ }
 
 
293
  });
294
 
295
  if (loading) return <Html center><span className="text-white text-lg">Loading robot…</span></Html>;