Spaces:
Running
Running
fix openarms urdf vis
Browse files- 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 |
-
|
| 91 |
-
const
|
| 92 |
-
|
| 93 |
-
|
| 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
|
| 113 |
const [loading, setLoading] = useState(true);
|
| 114 |
const [error, setError] = useState<string | null>(null);
|
| 115 |
|
| 116 |
-
|
| 117 |
-
const
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
count: 0,
|
| 122 |
-
});
|
| 123 |
-
const lineRef = useRef<Line2 | null>(null);
|
| 124 |
-
const trailMatRef = useRef<LineMaterial | null>(null);
|
| 125 |
|
| 126 |
-
// Reset
|
| 127 |
useEffect(() => {
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
if (lineRef.current) lineRef.current.visible = false;
|
| 131 |
}, [trailResetKey]);
|
| 132 |
|
| 133 |
-
// Create trail Line2
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) =>
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 232 |
-
|
| 233 |
-
|
| 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 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
const
|
| 263 |
-
|
| 264 |
-
trail.
|
| 265 |
-
trail.
|
| 266 |
-
trail.
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 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>;
|