Skip to content

Commit 04bfe1c

Browse files
committed
Fix jointless link mapping
1 parent bbee9d4 commit 04bfe1c

File tree

1 file changed

+115
-84
lines changed

1 file changed

+115
-84
lines changed

src/links/linkManager.ts

Lines changed: 115 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as THREE from 'three';
2-
import { URDFRobot } from 'urdf-loader';
2+
import { URDFRobot, URDFVisual } from 'urdf-loader';
33

44
/**
55
* Manages the visual representation of links
@@ -8,6 +8,8 @@ export class LinkManager {
88
private _robot: URDFRobot | null = null;
99
private _frameHelpers: THREE.Group;
1010
private _redrawCallback: () => void;
11+
private _linkToMeshes: Map<string, THREE.Mesh[]> = new Map();
12+
private _correctLinkMap: Map<string, THREE.Object3D> = new Map(); // Use base Object3D for flexibility
1113

1214
constructor(scene: THREE.Scene, redrawCallback: () => void) {
1315
this._frameHelpers = new THREE.Group();
@@ -16,13 +18,22 @@ export class LinkManager {
1618
}
1719

1820
/**
19-
* Sets the current robot model for the manager to operate on.
20-
* @param robot The URDFRobot model.
21+
* Sets the current robot model, builds a correct map of all links,
22+
* and then maps those links to their meshes.
2123
*/
2224
public setRobot(robot: URDFRobot | null): void {
25+
// If there's an old robot, remove it from the scene completely.
26+
if (this._robot && this._robot.parent) {
27+
this._robot.parent.remove(this._robot);
28+
}
29+
2330
this._robot = robot;
2431
this._frameHelpers.clear();
32+
this._linkToMeshes.clear();
33+
this._correctLinkMap.clear();
34+
2535
if (robot) {
36+
this._buildLinkAndMeshMaps(robot);
2637
this.updateAllFramePositions();
2738
}
2839
}
@@ -37,7 +48,7 @@ export class LinkManager {
3748

3849
this._frameHelpers.children.forEach((frameGroup: any) => {
3950
const linkName = frameGroup.userData.linkName;
40-
const link = this._robot?.links[linkName];
51+
const link = this._correctLinkMap.get(linkName);
4152

4253
if (link) {
4354
const worldPosition = new THREE.Vector3();
@@ -73,7 +84,7 @@ export class LinkManager {
7384
return;
7485
}
7586

76-
const link = this._robot.links[linkName];
87+
const link = this._correctLinkMap.get(linkName);
7788

7889
if (link) {
7990
const axes = this._createCustomAxesHelper(size);
@@ -97,22 +108,33 @@ export class LinkManager {
97108
}
98109

99110
/**
100-
* Sets the opacity of a specific link.
111+
* Sets the opacity of a specific link using our custom mesh mapping.
101112
*/
102113
public setLinkOpacity(linkName: string, opacity: number): void {
103-
if (!this._robot) {
114+
const meshes = this._linkToMeshes.get(linkName);
115+
if (!meshes || meshes.length === 0) {
104116
return;
105117
}
106118

107-
const link = this._robot.links[linkName];
119+
meshes.forEach(mesh => {
120+
const materials = Array.isArray(mesh.material)
121+
? mesh.material
122+
: [mesh.material];
108123

109-
if (link) {
110-
link.children.forEach((linkChild: any) => {
111-
if (!linkChild.isURDFLink) {
112-
this._setMeshOpacity(linkChild, opacity);
124+
materials.forEach(material => {
125+
if (material) {
126+
if (opacity < 1.0) {
127+
material.transparent = true;
128+
material.depthWrite = false;
129+
} else {
130+
material.transparent = false;
131+
material.depthWrite = true;
132+
}
133+
material.opacity = opacity;
134+
material.needsUpdate = true;
113135
}
114136
});
115-
}
137+
});
116138

117139
this._redrawCallback();
118140
}
@@ -123,6 +145,86 @@ export class LinkManager {
123145
public dispose(): void {
124146
this._frameHelpers.clear();
125147
this._frameHelpers.parent?.remove(this._frameHelpers);
148+
this._linkToMeshes.clear();
149+
this._correctLinkMap.clear();
150+
}
151+
152+
/**
153+
* This builds the link and mesh maps by using the
154+
* URDF's XML structure as the absolute ground truth.
155+
*/
156+
private _buildLinkAndMeshMaps(robot: URDFRobot): void {
157+
this._correctLinkMap.clear();
158+
this._linkToMeshes.clear();
159+
160+
const rootXml = robot.urdfRobotNode;
161+
if (!rootXml) {
162+
return;
163+
}
164+
165+
// Step 1: Build a map from the <link> XML Element to the THREE.URDFVisual object.
166+
// This is the bridge from the XML world to the THREE.js world.
167+
const linkXmlToVisualMap = new Map<Element, URDFVisual>();
168+
robot.traverse(node => {
169+
if ((node as any).isURDFVisual) {
170+
const visual = node as URDFVisual;
171+
const visualXml = visual.urdfNode;
172+
// The parent of a <visual> tag is its <link> tag.
173+
if (visualXml && visualXml.parentElement) {
174+
linkXmlToVisualMap.set(visualXml.parentElement, visual);
175+
}
176+
}
177+
});
178+
179+
// Step 2: Get all <link> tags from the XML. This is our ground truth list of links.
180+
const allLinkElements = rootXml.querySelectorAll('link');
181+
182+
// Step 3: Iterate through the ground truth list and populate our maps.
183+
allLinkElements.forEach(linkElement => {
184+
const linkName = linkElement.getAttribute('name');
185+
if (!linkName) {
186+
return;
187+
}
188+
189+
const visual = linkXmlToVisualMap.get(linkElement);
190+
191+
if (visual) {
192+
const meshes: THREE.Mesh[] = [];
193+
visual.traverse(child => {
194+
if (child instanceof THREE.Mesh) {
195+
meshes.push(child);
196+
}
197+
});
198+
this._linkToMeshes.set(linkName, meshes);
199+
200+
// Map the transform object.
201+
if (visual.parent && visual.parent !== robot) {
202+
// Jointed link: the parent is the distinct URDFLink object.
203+
this._correctLinkMap.set(linkName, visual.parent);
204+
} else {
205+
// Jointless link: the visual itself is the best object representing the transform.
206+
this._correctLinkMap.set(linkName, visual);
207+
}
208+
} else {
209+
// This link has no visual component (like 'world').
210+
this._linkToMeshes.set(linkName, []);
211+
// The root URDFRobot object itself acts as the 'world' link.
212+
if (linkName === 'world') {
213+
this._correctLinkMap.set(linkName, robot);
214+
}
215+
}
216+
});
217+
218+
// Step 4: Clone materials for all found meshes to ensure uniqueness.
219+
for (const meshes of this._linkToMeshes.values()) {
220+
meshes.forEach(mesh => {
221+
if (Array.isArray(mesh.material)) {
222+
mesh.material = mesh.material.map(mat => (mat ? mat.clone() : mat));
223+
} else if (mesh.material) {
224+
mesh.material = mesh.material.clone();
225+
}
226+
});
227+
}
126228
}
127229

128230
/**
@@ -148,75 +250,4 @@ export class LinkManager {
148250
axesGroup.add(xAxis, yAxis, zAxis);
149251
return axesGroup;
150252
}
151-
152-
/**
153-
* Helper method to recursively set opacity on meshes within a single link.
154-
* This function clones materials to ensure that opacity changes on one link
155-
* do not affect other links that might share the same material.
156-
*/
157-
private _setMeshOpacity(object: THREE.Object3D, opacity: number): void {
158-
if ((object as any).isURDFLink) {
159-
// Stop recursion if we encounter another link.
160-
// This check is important if the initial call is not on a visual node.
161-
return;
162-
}
163-
164-
if (object instanceof THREE.Mesh) {
165-
const mesh = object as THREE.Mesh;
166-
const materials = Array.isArray(mesh.material)
167-
? mesh.material
168-
: [mesh.material];
169-
170-
const newMaterials = materials.map(material => {
171-
if (!material) {
172-
return material;
173-
}
174-
175-
// If we're making it transparent, clone the material to avoid side effects.
176-
if (opacity < 1.0) {
177-
if (!material.userData.isOpacityClone) {
178-
const clonedMaterial = material.clone();
179-
clonedMaterial.userData.isOpacityClone = true;
180-
clonedMaterial.userData.originalMaterial = material;
181-
182-
clonedMaterial.transparent = true;
183-
clonedMaterial.depthWrite = false;
184-
clonedMaterial.opacity = opacity;
185-
clonedMaterial.needsUpdate = true;
186-
return clonedMaterial;
187-
} else {
188-
material.opacity = opacity;
189-
material.needsUpdate = true;
190-
return material;
191-
}
192-
} else {
193-
if (
194-
material.userData.isOpacityClone &&
195-
material.userData.originalMaterial
196-
) {
197-
return material.userData.originalMaterial;
198-
} else {
199-
material.transparent = false;
200-
material.depthWrite = true;
201-
material.opacity = 1.0;
202-
material.needsUpdate = true;
203-
return material;
204-
}
205-
}
206-
});
207-
208-
if (Array.isArray(mesh.material)) {
209-
mesh.material = newMaterials;
210-
} else {
211-
mesh.material = newMaterials[0];
212-
}
213-
}
214-
215-
// Recurse through children, but stop if a child is another URDFLink
216-
object.children.forEach(child => {
217-
if (!(child as any).isURDFLink) {
218-
this._setMeshOpacity(child, opacity);
219-
}
220-
});
221-
}
222253
}

0 commit comments

Comments
 (0)