11import * 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