title: 3D rotations to connect balls and cylinders
body: <p>I've been tasked with writing a python based plugin for a graph drawing program that generates an STL model of a graph.  A graph being an object made up of vertices and edges, where a vertex is represented by a 3D ball (a tessellated icosahedron), and an edge is represented with a cylinder that connects with two balls at either end.  The end result of the 3D model is that it will get dumped out to an STL file for 3D printing.  I'm able to generate the 3D models for the balls and cylinders without any issues, but I'm having some issues generating the overall model, and getting the balls and cylinders to connect properly.  </p>
  
  <p>My original idea was to create tessellated icosahedrons at the origin, then translate them out to the positions of the vertices.  This works fine.  I then, for each edge, I would create a cylinder at the origin, rotate it to the correct angle so that it points in the correct direction, then translate it to the midpoint between the two vertices so that the ends of the cylinders are embedded in the icosahedrons.  This is where things are going wrong.  I'm having some difficulties getting the rotations correct.  To calculate the rotations, I'm doing the following:</p>
  
  <p>First, I find the angle between the two points as follows (where source and target are both vertices in the graph, belonging to the edge that I'm currently processing):</p>
  
  <pre><code>        deltaX = source.x - target.x
          deltaY = source.y - target.y
          deltaZ = source.z - target.z
  
          xyAngle = math.atan2(deltaX, deltaY) 
          xzAngle = math.atan2(deltaX, deltaZ) 
          yzAngle = math.atan2(deltaY, deltaZ)
  </code></pre>
  
  <p>The angles being calculated seem reasonable, and as far as I can tell, do actually represent the angle between the vertices.  For example, if I have a vertex at (1, 1, 0) and another vertex at (3, 3, 0), the angle edge connecting them does show up as a 45 degree angle between the two vertices. (That, or -135 degrees, depending which vertex is the source and which is the target).</p>
  
  <p>Once I have the angles calculated, I create a cylinder and rotate it by the angles that have been calculated, like so, using some other classes that I've created:
                          c = cylinder()
                          c.createCylinder(edgeThickness, edgeLength)</p>
  
  <pre><code>        c.rotateX(-yzAngle)
          c.rotateY(xzAngle)
          c.rotateZ(-xyAngle)
          c.translate(edgePosition.x, edgePosition.y, edgePosition.z)
  </code></pre>
  
  <p>(Where edgePosition is the midpoint between the two vertices in the graph, edgeThickness is the radius of the cylinder being created, and edgeLength is the distance between the two vertices).</p>
  
  <p>As mentioned, its the rotating of the cylinders that doesn't work as expected.  It seems to do the correct rotation on the x/y plane, but as soon as an edge has vertices that differ in all three components (x, y, and z), the rotation fails.  Here's an example of a graph that differs in the x, and y components, but not in the z component:</p>
  
  <p><img src="https://i.stack.imgur.com/7wcxt.png" alt="The graph in the application"></p>
  
  <p>And here's the resulting STL file, as seen in Makerware (which is used to send the 3D models to the 3D printer):</p>
  
  <p><img src="https://i.stack.imgur.com/uWdQr.png" alt="Graph in Makerware"></p>
  
  <p>(The extra cylinder looking bit in the bottom left is something I've currently left in for testing purposes - a cylinder that points in the direction of the z axis, located at the origin).</p>
  
  <p>If I take that same graph and move the middle vertex out in the z axis, so now all the edges involve angles in all three axis, I get a result something like the following:</p>
  
  <p>As show in the app:</p>
  
  <p><img src="https://i.stack.imgur.com/mn4sZ.png" alt="enter image description here"></p>
  
  <p>The resulting STL file, as show in Makerware:</p>
  
  <p><img src="https://i.stack.imgur.com/OLBM7.png" alt="enter image description here"></p>
  
  <p>...and that same model as viewed from the side:</p>
  
  <p><img src="https://i.stack.imgur.com/yhvCE.png" alt="enter image description here"></p>
  
  <p>As you can see, the cylinders definitely aren't meeting up with the balls like I thought they would.  My question is this: Is my approach to doing this flawed, or is it some small but critical mistake that I'm making somewhere in my rotations?  I'm pretty sure it isn't a problem with the rotation functions themselves, as I've been able to independently verify that they work as expected.  I also tried creating a rotate function that takes in a yaw, pitch, and roll and does all three at once, and it seemed to generate the same result, like so:</p>
  
  <pre><code>c.rotateYawPitchRoll(xzAngle, -yzAngle, -xyAngle)
  </code></pre>
  
  <p>So... anyone have any ideas on what I might be doing wrong?</p>
  
  <p>UPDATE: As joojaa pointed out, it was a combination of calculating the correct angles as well as the order that they were applied.  In order to get things working, I first calculate the rotation on the x axis, as follows:</p>
  
  <pre><code>zyAngle = math.atan2(deltaVector.z, deltaVector.y)
  </code></pre>
  
  <p>where deltaVector is the difference between the target and source vectors.  This rotation is not yet applied though!  The next step is to calculate the rotation on the y axis, as follows:</p>
  
  <pre><code>angle = vector.angleBetweenVectors(vector(target.x - source.x, target.y - source.y, target.z - source.z), vector(target.x - source.x, target.y - source.y, 0.0))
  </code></pre>
  
  <p>Once both rotations are calculated, they are then applied... in the reverse order!  First, the x, then the y:</p>
  
  <pre><code>c.rotateY(angle)
  c.rotateX(-zyAngle) #... where c is a cylinder object
  </code></pre>
  
  <p>There still seems to be a few bugs, but this seems to at least work for a simple test case.</p>