/*
<?xml version='1.0' standalone='yes' ?>

<script>
	<name>Gearweaver</name>
	<author>Enrique Perez after Thomee Wright and Forrest Higgs after Vik Olliver after Francois Guillet</author>
	<version>1.0</version>
	<date>25-Feb-2008</date>
	<description>
This script creates a couple of meshing involute profile n-toothed gears. Input: pitch radius, pressure angle, and profile definition.
    </description>
	<licenseType>gpl</licenseType>
</script>
*/

/**
Add a side to the bottom of the truncated shaft.

@param  bisectionRadius outer radius of shaft
@param  bottomShaftPolygon bottom shaft polygon
@param  currentAngle angle around the polygon
@param  nodeVector nodes of the trimesh
@param  sideAngle angle subtended by one side
*/

void addBottomSide( bisectionRadius, bottomShaftPolygon, currentAngle, nodeVector, sideAngle )
{
  bottom = - shaftRadius * ( 1.0 - shaftTruncationBottom );
  currentSide = getPolar( currentAngle, bisectionRadius );
  nextSide = getPolar( currentAngle + sideAngle, bisectionRadius );

  if ( bottom > nextSide.y ) {
    return;
  }

  if ( currentSide.y > bottom ) {
    node = new Vec3( currentSide.x, currentSide.y, 0 );
    bottomShaftPolygon.add( nodeVector.size() );
    nodeVector.add( node );

    return;
  }

  shaftMinusCurrent = bottom - currentSide.y;
  shaftMinusCurrent /= nextSide.y - currentSide.y;
  intersection = nextSide.minus( currentSide );
  intersection.scale( shaftMinusCurrent );
  intersection.add( currentSide );
  node = new Vec3( intersection.x, intersection.y, 0 );
  bottomShaftPolygon.add( nodeVector.size() );
  nodeVector.add( node );
}

/**
Adds a clockwise triangle to the face array.

@param  face three index array
@param  beginIndex beginning index of the triangle
@param  centerIndex center index of the triangle
@param  endIndex end index of the triangle
*/

void addFace( int beginIndex, int centerIndex, int endIndex, Vector faces )
{
  int[] face = new int[ 3 ];
  faces.add( face );
  face[ 0 ] = beginIndex;
  face[ 1 ] = centerIndex;
  face[ 2 ] = endIndex;
}

/**
Add faces between two layers of a convex concentric clockwise polygon and a convex anticlockwise polygon, both of which go around the origin.

@param  bottomOuterPolygon bottom outer polygon
@param  bottomInnerPolygon bottom inner polygon
@param  faceVector faces of the trimesh
@param  nodeVector nodes of the trimesh
@param  topOuterPolygon  top outer polygon
@param  topInnerPolygon  top inner polygon
*/

void addFacesBetweenConcentricPolygons( Vector bottomInnerPolygon, Vector bottomOuterPolygon, Vector faceVector, Vector nodeVector, Vector topInnerPolygon, Vector topOuterPolygon )
{
  oldClosestIndex = getClosestInnerIndex( bottomInnerPolygon, nodeVector, bottomOuterPolygon.elementAt( bottomOuterPolygon.size() - 1 ), bottomOuterPolygon.elementAt( 0 ) );

  for ( int pointIndex = 0; pointIndex < bottomOuterPolygon.size(); pointIndex++ ) {
    rimPlusOneRemainder = ( pointIndex + 1 ) % bottomOuterPolygon.size();
    bottomRimFirst = bottomOuterPolygon.elementAt( pointIndex );
    bottomRimSecond = bottomOuterPolygon.elementAt( rimPlusOneRemainder );
    topRimFirst = topOuterPolygon.elementAt( pointIndex );
    closestInnerIndex = getClosestInnerIndex( bottomInnerPolygon, nodeVector, bottomRimFirst, bottomRimSecond );
    addFace( bottomInnerPolygon.elementAt( closestInnerIndex ), bottomRimFirst, bottomRimSecond, faceVector );
    addFace( topInnerPolygon.elementAt( closestInnerIndex ), topOuterPolygon.elementAt( rimPlusOneRemainder ), topRimFirst, faceVector );

    if ( closestInnerIndex > oldClosestIndex ) {
      closestInnerIndex -= bottomInnerPolygon.size();
    }

    for ( int sideIndex = closestInnerIndex; sideIndex < oldClosestIndex; sideIndex++ ) {
      sideRemainder = ( sideIndex + bottomInnerPolygon.size() ) % bottomInnerPolygon.size();
      sidePlusOneRemainder = ( sideIndex + bottomInnerPolygon.size() + 1 ) % bottomInnerPolygon.size();
      addFace( bottomInnerPolygon.elementAt( sidePlusOneRemainder ), bottomRimFirst, bottomInnerPolygon.elementAt( sideRemainder ), faceVector );
      addFace( topInnerPolygon.elementAt( sidePlusOneRemainder ), topInnerPolygon.elementAt( sideRemainder ), topRimFirst, faceVector );
    }

    oldClosestIndex = closestInnerIndex;
  }

  return faceIndex;
}

/**
Add faces between a bottom and top polygon.

@param  bottomPolygon bottom polygon
@param  faceVector faces of the trimesh
@param  topPolygon top polygon
*/

void addFacesBetweenPolygons( Vector bottomPolygon, Vector faceVector, Vector topPolygon )
{
    for ( int beginIndex = 0; beginIndex < bottomPolygon.size(); beginIndex++ ) {
      beginBottomIndex = bottomPolygon.elementAt( beginIndex );
      beginTopIndex = topPolygon.elementAt( beginIndex );
      endBottomIndex = bottomPolygon.elementAt( ( beginIndex + 1 ) % bottomPolygon.size() );
      endTopIndex = topPolygon.elementAt( ( beginIndex + 1 ) % bottomPolygon.size() );
      addFace( beginBottomIndex, beginTopIndex, endTopIndex, faceVector );
      addFace( beginBottomIndex, endTopIndex, endBottomIndex, faceVector );
    }
}

/**
Add gear outline to scene.

@param  coordinateSystem coordinate system of the gear
@param  name name of the gear
@param  numberOfTeeth number of teeth in the gear
@param  toothProfile profile of one of the teeth of the gear
*/

void addGear( CoordinateSystem coordinateSystem, String name, double numberOfTeeth, Vec2[] toothProfile )
{
  int numberOfCircumferencePoints = toothProfile.length * numberOfTeeth;
  Vec3[] gearVertexPositions = new Vec3[ numberOfCircumferencePoints ];
  float[] smoothness = new float[ numberOfCircumferencePoints ];
  int index = 0;
  double toothAngleRadian = 2.0 * Math.PI / numberOfTeeth;

  for ( int tooth = 0; tooth < numberOfTeeth; tooth++ ) {
    double totalToothAngle = - toothAngleRadian * tooth;

    for ( int toothSurface = 0; toothSurface < toothProfile.length; toothSurface++ ) {
      rotatedVector = getRotatedVector( totalToothAngle, toothProfile[ toothSurface ] );
      gearVertexPositions[ index ] = new Vec3( rotatedVector.x, rotatedVector.y, 0 );
      smoothness[ index ] = 0;
      index++;
      }
    }

  curve = new Curve( gearVertexPositions, smoothness, Mesh.APPROXIMATING, true );
  window.addObject( curve, coordinateSystem, name, null );
}

/**
Add the top side of the truncated shaft.

@param  bisectionRadius outer radius of the shaft
@param  bottomShaftPolygon bottom shaft polygon
@param  currentAngle current angle of the polygon point
@param  nodeVector nodes of the trimesh
@param  sideAngle angle subtended by a side of the polygon
*/

void addTopSide( bisectionRadius, bottomShaftPolygon, currentAngle, nodeVector, sideAngle )
{
  top = shaftRadius;

  if ( shaftSides % 2 == 1 ) {
    top = bisectionRadius;
  }

  top *= ( 1.0 - shaftTruncationTop );
  currentSide = getPolar( currentAngle, bisectionRadius );
  lastSide = getPolar( currentAngle - sideAngle, bisectionRadius );

  if ( lastSide.y > top ) {
    return;
  }

  if ( currentSide.y < top ) {
    gearNode = new Vec3( currentSide.x, currentSide.y, 0 );
    bottomShaftPolygon.add( nodeVector.size() );
    nodeVector.add( gearNode );
    return;
  }

  shaftMinusLast = top - lastSide.y;
  shaftMinusLast /= currentSide.y - lastSide.y;
  intersection = currentSide.minus( lastSide );
  intersection.scale( shaftMinusLast );
  intersection.add( lastSide );
  node = new Vec3( intersection.x, intersection.y, 0 );
  bottomShaftPolygon.add( nodeVector.size() );
  nodeVector.add( node );
}

/**
Add triangle pairs in two layers of a convex polygon.

@param  bottomPolygon bottom polygon
@param  faceVector faces of the trimesh
@param  topPolygon top polygon
*/

void addTrianglePairsInPolygon( Vector bottomPolygon, Vector faceVector, Vector topPolygon )
{
  for ( int begin = 0; begin < ( bottomPolygon.size() - 1 ) / 2; begin++ ) {
    end = bottomPolygon.size() - begin - 1;
    centerIndex = bottomPolygon.elementAt( end - 1 );
    quadrilateralBegin = bottomPolygon.elementAt( begin );
    addFace( quadrilateralBegin, centerIndex, bottomPolygon.elementAt( end ), faceVector );
    centerIndex = topPolygon.elementAt( end - 1 );
    quadrilateralBegin = topPolygon.elementAt( begin );
    addFace( quadrilateralBegin, topPolygon.elementAt( end ), centerIndex, faceVector );
  }

  for ( int begin = 0; begin < bottomPolygon.size() / 2 - 1; begin++ ) {
    end = bottomPolygon.size() - begin - 1;
    centerIndex = bottomPolygon.elementAt( end - 1 );
    quadrilateralBegin = bottomPolygon.elementAt( begin );
    addFace( quadrilateralBegin, bottomPolygon.elementAt( begin + 1 ), centerIndex, faceVector );
    centerIndex = topPolygon.elementAt( end - 1 );
    quadrilateralBegin = topPolygon.elementAt( begin );
    addFace( quadrilateralBegin, centerIndex, topPolygon.elementAt( begin + 1 ), faceVector );
  }
}

/**
Convert gear profile to thick gear.

@param  coordinateSystem coordinate system of the gear
@param  numberOfTeeth number of teeth in the gear
@param  pitchRadius pitch radius of the gear
@param  shovelDirection direction that the teeth curve
@param  thickName name of the solid gear, the name of the gear profile is the name of the solid gear plus " Profile"
*/

void convertToSolid( CoordinateSystem coordinateSystem, int numberOfTeeth, double pitchRadius, double shovelDirection, String thickName )
{
  scene = window.getScene();

  gearProfile = scene.getObject( thickName + " Profile" );
  gearVertexPositions = gearProfile.object.getVertexPositions();
  innerRadius = getInnerRadius( numberOfTeeth, pitchRadius );
  layers = 1 + shovelSegments;
  topVertexPositionOffsets = gearVertexPositions.length * shovelSegments;

  Vector gearFaceVector = new Vector();
  Vector gearNodeVector = new Vector();

  for ( int vertexIndex = 0; vertexIndex < gearVertexPositions.length; vertexIndex++ ) {
    gearNode = new Vec3( gearVertexPositions[ vertexIndex ] );
    gearNodeVector.add( gearNode );
  }

  for ( int segmentIndex = 1; segmentIndex < layers; segmentIndex++ ) {
    polarShovel = getPolar( shovelAngle, 1.0 );
    shovelWidth = 2.0 * polarShovel.y;
    upRatio = ( double )segmentIndex / ( double )shovelSegments;
    digY = polarShovel.y - upRatio * shovelWidth;
    digRatio = Math.sqrt( 1.0 - digY * digY ) - polarShovel.x;
    digRotation = shovelDirection * digRatio * thickness / pitchRadius;
    up = upRatio * thickness;

    for ( int vertexIndex = 0; vertexIndex < gearVertexPositions.length; vertexIndex++ ) {
      gearNode = gearNodeVector.elementAt( vertexIndex );
      rotatedGearNode = getRotatedVector( digRotation, new Vec2( gearNode.x, gearNode.y ) );
      layerNode = new Vec3( rotatedGearNode.x, rotatedGearNode.y, up );
      gearNodeVector.add( layerNode );
    }
  }

  Vector bottomShaftVector = new Vector();
  Vector topShaftVector = new Vector();
  bisectionRadius = shaftRadius;

  if ( bisectionRadius > innerRadius ) {
    bisectionRadius = 0.0;
    shaftRadius = 0.0;
    print( "The requested shaft is larger than the gear, so the shaft will not be removed." );
  }

  if ( shaftRadius > 0.0 ) {
    halfSideAngle = Math.PI / shaftSides;
    shaftStart = 1.5 * Math.PI + halfSideAngle;
    sideAngle = 2.0 * halfSideAngle;
    bisectionRadius /= Math.cos( halfSideAngle );
    int sideIndex = 0;

    for ( ; shaftStart + sideIndex * sideAngle < 2 * Math.PI; sideIndex++ ) {
      currentAngle = shaftStart + sideIndex * sideAngle;
      addBottomSide( bisectionRadius, bottomShaftVector, currentAngle, gearNodeVector, sideAngle );
    }

    for ( ; shaftStart + sideIndex * sideAngle < 2.5 * Math.PI + halfSideAngle / 2; sideIndex++ ) {
      currentAngle = shaftStart + sideIndex * sideAngle;
      addTopSide( bisectionRadius, bottomShaftVector, currentAngle, gearNodeVector, sideAngle );
    }

    originalHalfShaftLength = bottomShaftVector.size();
    mirrorStart = originalHalfShaftLength;

    if ( Math.abs( gearNodeVector.elementAt( bottomShaftVector.elementAt( originalHalfShaftLength - 1 ) ).x ) < 0.00001 ) {
      mirrorStart++;
    }

    for ( int sideIndex = mirrorStart; sideIndex < 2 * originalHalfShaftLength; sideIndex++ ) {
      gearShaftNode = gearNodeVector.elementAt( bottomShaftVector.elementAt( 2 * originalHalfShaftLength - sideIndex - 1 ) );
      bottomShaftVector.add( gearNodeVector.size() );
      gearNodeVector.add( new Vec3( - gearShaftNode.x, gearShaftNode.y, gearShaftNode.z ) );
    }

    copyHigh( bottomShaftVector, gearNodeVector, thickness, topShaftVector );
  }

  pointsPerTooth = ( int )Math.round( gearVertexPositions.length / numberOfTeeth );

  // Add faces around the perimeter.

  for ( int segmentIndex = 0; segmentIndex < shovelSegments; segmentIndex++ ) {
    vertexPositionOffset = segmentIndex * gearVertexPositions.length;
    Vector bottomPerimeter = new Vector();
    Vector topPerimeter = new Vector();

    for ( int beginIndex = vertexPositionOffset; beginIndex < vertexPositionOffset + gearVertexPositions.length; beginIndex++ ) {
      bottomPerimeter.add( beginIndex );
      topPerimeter.add( beginIndex + gearVertexPositions.length );
    }

    addFacesBetweenPolygons( bottomPerimeter, gearFaceVector, topPerimeter );
  }

  // Add faces to the top and bottom.

  Vector bottomRim = new Vector();
  Vector topRim = new Vector();

  for ( int tooth = 0; tooth < numberOfTeeth; tooth++ ) {
    toothStart = tooth * pointsPerTooth;
    bottomRim.add( toothStart );
    topRim.add( toothStart + topVertexPositionOffsets );

    Vector bottomTooth = new Vector();
    Vector topTooth = new Vector();

    for ( int beginIndex = 0; beginIndex <= pointsPerTooth; beginIndex++ ) {
      toothStartBegin = ( toothStart + beginIndex ) % gearVertexPositions.length;
      bottomTooth.add( toothStartBegin );
      topTooth.add( toothStartBegin + topVertexPositionOffsets );
    }

    addTrianglePairsInPolygon( bottomTooth, gearFaceVector, topTooth );
  }

  // Add faces around cutout circles.

  rimThickness = minimumRimThickness * innerRadius;
  centerThickness = bisectionRadius + rimThickness;
  remainingCutout = ( 1.0 - 2 * minimumRimThickness );
  holeRadius = ( innerRadius - rimThickness - centerThickness ) / 2;

  if ( holeRadius > minimumRimCutoutRadius ) {
    int cutoutCircles = 5;
    int cutoutSides = 9;
    double cutoutCircleAngle = 2.0 * Math.PI / ( double )cutoutCircles;
    int sideBetweenStart = ( int )Math.round( ( double )cutoutSides / 7.0 );
    int sideBetweenStartMirror = cutoutSides - sideBetweenStart;
    int sideBetweenEnd = ( int )Math.round( 3.0 * ( double )cutoutSides / 8.0 );
    int sideBetweenEndMirror = cutoutSides - sideBetweenEnd;
    double cutoutSideAngle = 2.0 * Math.PI / ( double )cutoutSides;
    double holeY = innerRadius - rimThickness - holeRadius;
    double sinHalfCutoutCircleAngle = Math.sin( cutoutCircleAngle / 2 );
    double overlap = holeRadius + rimThickness / 2 - holeY * sinHalfCutoutCircleAngle;
    Vector[] bottomCutoutVector = new Vector[ cutoutCircles ];
    Vector[] topCutoutVector = new Vector[ cutoutCircles ];

    if ( overlap > 0.0 ) {
      holeRadius -= overlap / ( 1.0 + sinHalfCutoutCircleAngle );
      holeY = innerRadius - rimThickness - holeRadius;
    }

    for ( int circle = 0; circle < cutoutCircles; circle++ ) {
      bottomCutoutVector[ circle ] = new Vector();
      topCutoutVector[ circle ] = new Vector();

      for ( int side = 0; side < cutoutSides; side++ ) {
        cutoutPlanePoint = getPolar( Math.PI * 1.5 + cutoutSideAngle * side, holeRadius );
        cutoutPlanePoint.y += holeY;
        cutoutPlanePoint = getRotatedVector( - cutoutCircleAngle * circle, cutoutPlanePoint );
        gearNode = new Vec3( cutoutPlanePoint.x, cutoutPlanePoint.y, 0.0 );
        bottomCutoutVector[ circle ].add( gearNodeVector.size() );
        gearNodeVector.add( gearNode );
      }

      copyHigh( bottomCutoutVector[ circle ], gearNodeVector, thickness, topCutoutVector[ circle ] );

      addFacesBetweenPolygons( bottomCutoutVector[ circle ], gearFaceVector, topCutoutVector[ circle ] );
    }

    Vector bottomOuterVector = new Vector();
    Vector topOuterVector = new Vector();

    for ( int circle = cutoutCircles - 1; circle >= 0; circle-- ) {

      for ( int side = sideBetweenEnd; side <= sideBetweenEndMirror; side++ ) {
        bottomOuterVector.add( bottomCutoutVector[ circle ].elementAt( side ) );
        topOuterVector.add( topCutoutVector[ circle ].elementAt( side ) );
      }
    }

    addFacesBetweenConcentricPolygons( bottomOuterVector, bottomRim, gearFaceVector, gearNodeVector, topOuterVector, topRim );

  // Add outside of cutout circles.

    bottomRim = new Vector();
    topRim = new Vector();

    for ( int side = 0; side <= sideBetweenStart; side++ ) {
      bottomRim.add( bottomCutoutVector[ 0 ].elementAt( side ) );
      topRim.add( topCutoutVector[ 0 ].elementAt( side ) );
    }

    for ( int circle = 1; circle < cutoutCircles; circle++ ) {

      for ( int side = sideBetweenStartMirror; side <= sideBetweenStart + cutoutSides; side++ ) {
        sideRemainder = side % cutoutSides;
        bottomRim.add( bottomCutoutVector[ circle ].elementAt( sideRemainder ) );
        topRim.add( topCutoutVector[ circle ].elementAt( sideRemainder ) );
      }
    }

    for ( int side = sideBetweenStartMirror; side < cutoutSides; side++ ) {
      bottomRim.add( bottomCutoutVector[ 0 ].elementAt( side ) );
      topRim.add( topCutoutVector[ 0 ].elementAt( side ) );
    }

    for ( int circle = 0; circle < cutoutCircles; circle++ ) {
      Vector bottomSides = new Vector();
      Vector topSides = new Vector();

      for ( int side = sideBetweenStart; side <= sideBetweenEnd; side++ ) {
        bottomSides.add( bottomCutoutVector[ circle ].elementAt( side ) );
        topSides.add( topCutoutVector[ circle ].elementAt( side ) );
      }

      for ( int side = sideBetweenEndMirror; side <= sideBetweenStartMirror; side++ ) {
        nextCircle = ( circle + 1 ) % cutoutCircles;
        bottomSides.add( bottomCutoutVector[ nextCircle ].elementAt( side ) );
        topSides.add( topCutoutVector[ nextCircle ].elementAt( side ) );
      }

      addTrianglePairsInPolygon( bottomSides, gearFaceVector, topSides );
    }
  }

  if ( shaftRadius == 0.0 ) {
    addTrianglePairsInPolygon( bottomRim, gearFaceVector, topRim );
  }
  else {
    addFacesBetweenConcentricPolygons( bottomShaftVector, bottomRim, gearFaceVector, gearNodeVector, topShaftVector, topRim );
    addFacesBetweenPolygons( bottomShaftVector, gearFaceVector, topShaftVector );
  }

  int[][] gearFaceArray = gearFaceVector.toArray( new int[ gearFaceVector.size() ][ 3 ] );
  Vec3[] gearNodeArray = gearNodeVector.toArray( new Vec3[ gearNodeVector.size() ] );
  mesh = new TriangleMesh ( gearNodeArray, gearFaceArray );
  mesh.setSmoothingMethod( Mesh.NO_SMOOTHING );
  window.addObject( mesh, coordinateSystem, thickName, null );
  window.removeObject( scene.indexOf( gearProfile ), null );
}

/**
Copy the bottom polygon into a top polygon which is thickness high.

@param  bottomPolygon bottom polygon
@param  nodeVector nodes of the trimesh
@param  thickness height of the top polygon
@param  topPolygon top polygon
*/

void copyHigh( Vector bottomPolygon, Vector nodeVector, double thickness, Vector topPolygon )
{
  for ( int pointIndex = 0; pointIndex < bottomPolygon.size(); pointIndex++ ) {
    node = nodeVector.elementAt( bottomPolygon.elementAt( pointIndex ) );
    topPolygon.add( nodeVector.size() );
    nodeVector.add( new Vec3( node.x, node.y, thickness ) );
  }
}

/**
End the turning gear profile simulation and create the finished gear.
*/

void endTurnGear()
{
  scene = window.getScene();
  timer.cancel();
  firstGearCoordinateSystem.copyCoords( firstGearCoordinateSystemOld );
  secondGearCoordinateSystem.copyCoords( secondGearCoordinateSystemOld );

  if ( sceneCameraInfo != null ) {
    sceneCameraInfo.coords.copyCoords( sceneCameraCoordsOld );

    for ( propertyIndex = 0; propertyIndex < sceneCameraPropertiesOld.length; propertyIndex++ ) {
      sceneCameraInfo.object.setPropertyValue( propertyIndex, sceneCameraPropertiesOld[ propertyIndex ] );
    }

    scene.objectModified( sceneCameraInfo.object );
  }

  window.updateImage();

  if ( thickness > 0.0 ) {
    convertToSolid( firstGearCoordinateSystemOld, ( int )numberOfTeethFirstGear, pitchRadiusFirstGear, 1.0, "First Gear" );
    convertToSolid( secondGearCoordinateSystemOld, ( int )numberOfTeethSecondGear, pitchRadiusSecondGear, - 1.0, "Second Gear" );
  }

  undoAdd = new UndoRecord ( window, false, UndoRecord.DELETE_OBJECT, new Object[] { new Integer( scene.getNumObjects() - 2 ) } );
  window.setUndoRecord( undoAdd );
  undoAdd = new UndoRecord ( window, false, UndoRecord.DELETE_OBJECT, new Object[] { new Integer( scene.getNumObjects() - 1 ) } );
  window.setUndoRecord( undoAdd );
  window.updateImage();

// number of faces:
// for each edge in our perimeter, two triangles to connect
// the front and back, so 2*numberSides
// for each end cap, we connect each vertex to the point.  The
// first and last vertices are connected by a perimeter edge,
// leaving numberSides-2 edges to connect across the middle,
// creating a face, for numberSides-2 triangles per end cap,
// or (numberSides-2)*2 triangles for both end caps
// Thus:
// 2*numberSides + 2*(numberSides-2)
// 2*numberSides + 2*numberSides - 4
// 4*numberSides - 4
//Vec3[] v = new Vec3[numberSides * 2];
//int[][] faces = new int[4*numberSides-4][3];
//v[0] = new Vec3(0, y0, 0);
//v[1] = new Vec3(0, y0, -length);
}

/**
Get the Camera Info from the first camera in the scene.
*/

ObjectInfo getCameraInfo()
{
  scene = window.getScene();
  for ( int objectIndex = 0; objectIndex < scene.getNumObjects(); objectIndex++ ) {
    objectInfo = scene.getObject( objectIndex );
    if ( objectInfo.object instanceof SceneCamera ) {
      return objectInfo;
    }
  }

  return null;
}

/**
Get the index of the point on the inner polygon which is closest to the average angle of the first and second points on the outer polygon.

@param  innerPolygon inner polygon index array
@param  nodes Vector of nodes
@param  outerPolygonFirstIndex index of first point on outer polygon
@param  outerPolygonSecondIndex index of second point on outer polygon
@return  closest index on the inner polygon to the average angle of the first and second points on the outer polygon
*/

int getClosestInnerIndex( Vector innerPolygon, Vector nodes, int outerPolygonFirstIndex, int outerPolygonSecondIndex )
{
  closestIndex = 0;
  largestDot = - 9999999.0;
  rimFirstPlane = nodes.elementAt( outerPolygonFirstIndex ).dropAxis( 2 );
  rimSecondPlane = nodes.elementAt( outerPolygonSecondIndex ).dropAxis( 2 );
  rimFirstPlane.normalize();
  rimSecondPlane.normalize();
  rimFirstPlane.add( rimSecondPlane );
  rimFirstPlane.normalize();

  for ( int innerIndex = 0; innerIndex < innerPolygon.size(); innerIndex++ ) {
    innerPlane = nodes.elementAt( innerPolygon.elementAt( innerIndex ) ).dropAxis( 2 );
    innerPlane.normalize();
    innerDot = innerPlane.dot( rimFirstPlane );

    if ( innerDot > largestDot ) {
      largestDot = innerDot;
      closestIndex = innerIndex;
    }
  }

  return closestIndex;
}

/**
Get number of teeth from the field value if it is not zero bounded to a minimum of 3, otherwise get number from the prime list.

@param  primeList scroll list of prime numbers
@param  numberOfTeethValueField number of gear teeth value field
@return  number of gear teeth
*/

double getNumberOfTeeth( BList primeList, ValueField numberOfTeethValueField )
{
  if ( numberOfTeethValueField.getValue() < 1.0 ) {
    return Double.valueOf( primeList.getSelectedValue() );
  }

  return Math.max( 2, Math.round( numberOfTeethValueField.getValue() ) );
}

/**
Get gear inner radius.

@param  numberOfTeeth number of gear teeth
@param  pitchRadius pitch radius of the gear
@return  gear inner radius
*/

double getInnerRadius( double numberOfTeeth, double pitchRadius )
{
  return pitchRadius * ( 1 - Math.PI / numberOfTeeth );
}

/**
Get ratio outside of the pitch.

@param  numberOfTeeth number of gear teeth
@param  pitchRadius pitch radius of the gear
@return  ratio outside of the pitch
*/

double getOutsidePitch( double numberOfTeeth )
{
  return 2.0 / numberOfTeeth;
}

/**
Get ratio of second gear according to the number of teeth.

@param  numberOfTeethFirstGear number of gear teeth on the first gear
@param  numberOfTeethSecondGear number of gear teeth on the second gear
@param  pitchRadiusFirstGear pitch radius of the first gear
@return  ratio of second gear
*/

double getPitchRadiusSecondGear( double numberOfTeethFirstGear, double numberOfTeethSecondGear, double pitchRadiusFirstGear )
{
  return pitchRadiusFirstGear * numberOfTeethSecondGear / numberOfTeethFirstGear;;
}

/**
Get polar Vec2 from counterclockwise angle from 1, 0 and radius.

@param  angle counterclockwise angle from 1, 0
@param  radius radius of vector
@return  polar vector
*/

Vec2 getPolar( double angle, double radius )
{
  return new Vec2( radius * Math.cos( angle ), radius * Math.sin( angle ) );
}

/**
Get scroll list of prime numbers, with visible selection.

@param  selectedIndex selected index of the scroll list
@return  scroll list of prime numbers
*/

BList getPrimeList( int selectedIndex )
{
  String[] primeNumberStrings = new String[ primeNumbers.length ];

  for ( int primeIndex = 0; primeIndex <  primeNumbers.length; primeIndex++ ) {
    primeNumberStrings[ primeIndex ] = primeNumbers[ primeIndex ].toString();
  }

  primeList = new BList( primeNumberStrings );
  primeList.setPreferredVisibleRows( ( int )Math.max( 4.0, selectedIndex + 1 ) );
  primeList.setMultipleSelectionEnabled( false );
  primeList.setSelected( selectedIndex, true );
  primeList.scrollToItem( selectedIndex );

  return primeList;
}

/**
Get rotated Vec2 from counterclockwise angle and vector.

@param  angle counterclockwise angle from 1, 0
@param  vector2 plane vector
@return  rotated vector
*/

Vec2 getRotatedVector( double angle, Vec2 vector2 )
{
  x = Math.cos( angle );
  y = Math.sin( angle );

  return new Vec2( vector2.x * x - vector2.y * y, vector2.x * y + vector2.y * x );
}

/**
Get profile for one tooth.

@param  numberOfTeeth number of gear teeth
@param  pitchRadius pitch radius of the gear
@param  pressureAngle pressure angle of the gear tooth
@param  profileDefinitionSurfaces profile definition surfaces, which should be at least six
@return  profile for one tooth
*/

Vec2[] getToothProfile( double numberOfTeeth, double pitchRadius, double pressureAngle, int profileDefinitionSurfaces )
{
  double toothAngleRadian = 2.0 * Math.PI / numberOfTeeth;
  double innerRadius = getInnerRadius( numberOfTeeth, pitchRadius );
  double outerRadius = pitchRadius * ( 1 + getOutsidePitch( numberOfTeeth ) );
  double bevelRadius = pitchRadius * Math.cos( pressureAngle );
  double angleBeginRadian = Math.PI / 2 - toothAngleRadian / 4 + pressureAngle - pitchRadius * Math.sin( pressureAngle ) / bevelRadius;
  double angleEndRadian = Math.sqrt( outerRadius * outerRadius - bevelRadius * bevelRadius ) / bevelRadius + angleBeginRadian;
  double angleDifferenceRadian = ( angleEndRadian - angleBeginRadian ) / ( profileDefinitionSurfaces - 1 );
  Vector toothProfileVector = new Vector();

  toothProfileVector.add( getPolar( Math.PI - angleBeginRadian, innerRadius ) );

  angleRadian = angleBeginRadian;
  abovePoint = null;
  pointMoved = false;

  for ( int profileIndex = 0; profileIndex < profileDefinitionSurfaces; profileIndex++ ) {
    length = bevelRadius * ( angleRadian - angleBeginRadian );
    toothPoint = new Vec2( - bevelRadius, length );
    toothPoint = getRotatedVector( angleRadian, toothPoint );
    toothPoint.y = - toothPoint.y;
    toothProfileVector.add( toothPoint );
    angleRadian = angleRadian + angleDifferenceRadian;

    if ( toothPoint.length() > innerRadius ) {

      if ( abovePoint == null ) {
        abovePoint = toothPoint;
      }
    }
  }

  // Move out, remove or replace the points which are inside the inner radius.

  for ( int profileIndex = profileDefinitionSurfaces-1; profileIndex >= 1; profileIndex-- ) {
    toothProfileLength = toothProfileVector.elementAt( profileIndex ).length();

    if ( toothProfileLength < innerRadius ) {

      if ( pointMoved ) {
        toothProfileVector.removeElementAt( profileIndex );
      }
      else {
        lengthBelow = innerRadius - toothProfileLength;
        toothPoint = toothProfileVector.elementAt( profileIndex );
        lastMinusShort = abovePoint.minus( toothPoint );
        lastMinusShort.scale( lengthBelow / lastMinusShort.length() );
        toothPoint.add( lastMinusShort );
        pointMoved = true;
      }
    }
  }

  if ( pointMoved ) {
    toothProfileVector.removeElementAt( 0 );
  }

  //  Mirror the tooth profile.

  int toothProfileLimit = toothProfileVector.size() - 1;

  for ( int profileIndex = toothProfileLimit; profileIndex >= 0; profileIndex-- ) {
    toothPoint = toothProfileVector.elementAt( profileIndex );
    toothProfileVector.add( new Vec2( - toothPoint.x, toothPoint.y ) );
  }

  return toothProfileVector.toArray( new Vec2[ toothProfileVector.size() ] );
}

/**
Add radio button groups to the preference widgets.

@param  radioButtonGroups radio button groups which will be added to the memorable widgets
@param  widgetVector memorable widgets
*/

void preferencesAddRadioButtonGroups( RadioButtonGroup[] radioButtonGroups, Vector widgetVector )
{
  for ( int radioIndex = 0; radioIndex < radioButtonGroups.length; radioIndex++ ) {
    radioButtonGroup = radioButtonGroups[ radioIndex ];
    radioButtonGroupIterator = radioButtonGroup.getRadioButtons();

    while ( radioButtonGroupIterator.hasNext() ) {
      radioButton = radioButtonGroupIterator.next();
      preferencesAddWidgetWithString( radioButton, radioButton.getText(), widgetVector );
    }
  }
}

/**
Add widgets which have titles.

@param  widgets widgets which have titles
@param  widgetStrings widget titles
@param  widgetVector memorable widgets
*/

void preferencesAddWidgetsWithStrings( Widget[] widgets, String[] widgetStrings, Vector widgetVector )
{
  for ( int widgetIndex = 0; widgetIndex < widgets.length; widgetIndex++ ) {
    widget = widgets[ widgetIndex ];

    if ( widget instanceof BCheckBox || widget instanceof BOutline || widget instanceof BTextField || widget instanceof ValueField ) {
      preferencesAddWidgetWithString( widget, widgetStrings[ widgetIndex ], widgetVector );
    }
  }
}

/**
Give the widget a name and add it to the widget vector.

@param  widget widget which will be given a name
@param  widgetStrings widget name
@param  widgetVector memorable widgets
*/

void preferencesAddWidgetWithString( Widget widget, String widgetString, Vector widgetVector )
{
  widget.setName( widgetString );
  widgetVector.add( widget );
}

/**
Read widget settings from preferences file.

@param  preferencesFilename preferences filename
@param  widgetVector memorable widgets
*/

void preferencesRead( String preferencesFilename, Vector widgetVector )
{
  preferencesFile = new File( preferencesFilename );

  if ( !preferencesFile.canRead() ) {
    return;
  }

  BufferedReader preferencesReader = new BufferedReader( new FileReader( preferencesFile ) );

  line = preferencesReader.readLine();

  while ( line != null ) {
    preferencesReadLine( line, widgetVector );
    line = preferencesReader.readLine();
  }
}

/**
Read line of preferences and set widget to that line.

@param  line line of preferences
@param  widgetVector memorable widgets
*/

void preferencesReadLine( String line, Vector widgetVector )
{
  splitLine = line.split( "\t" );

  if ( splitLine.length < 2 ) {
    return;
  }

  name = splitLine[ 0 ];

  for ( int widgetIndex = 0; widgetIndex < widgetVector.size(); widgetIndex++ ) {
    widget = widgetVector.elementAt( widgetIndex );

    if ( widget.getName().equals( name ) ) {
      preferencesReadWidget( splitLine[ 1 ], widget );

      return;
    }
  }
}

/**
Set widget to preferences value.

@param  value preferences value
@param  widget widget to be set to value
*/

void preferencesReadWidget( String value, Widget widget )
{
  if ( widget instanceof BCheckBox || widget instanceof BRadioButton ) {
    widget.setState( Boolean.valueOf( value ) );

    return;
  }

  if ( widget instanceof BOutline ) { // it would be better to save the value instead of index because the list might change, but I'm lazy
    bList = widget.getContent().getContent();
    selectedIndex = Integer.valueOf( value );
    bList.setSelected( selectedIndex, true );
    bList.scrollToItem( selectedIndex );

    return;
  }

  if ( widget instanceof BTextField ) {
    widget.setText( value );

    return;
  }

  if ( widget instanceof ValueField ) {
    widget.setValue( Double.valueOf( value ) );
  }
}

/**
Write widget settings to preferences file.

@param  preferencesFilename preferences filename
@param  widgetVector memorable widgets
*/

void preferencesWrite( String preferencesFilename, Vector widgetVector )
{
  preferencesFile = new File( preferencesFilename );

  if ( preferencesFile == null ) {
    print( "Can not write preferences to " + preferencesFilename );

    return;
  }

  BufferedWriter preferencesWriter = new BufferedWriter( new FileWriter( preferencesFile ) );

  for ( int widgetIndex = 0; widgetIndex < widgetVector.size(); widgetIndex++ ) {
    widget = widgetVector.elementAt( widgetIndex );
    preferencesWriteWidget( preferencesWriter, widget );
  }

  //Close the output stream
  preferencesWriter.close();
}

/**
Write widget settings to line of preferences.

@param  preferencesWriter buffered preferences file writer
@param  widget widget to be written
*/

void preferencesWriteWidget( BufferedWriter preferencesWriter, Widget widget )
{
  widgetString = widget.getName() + "\t";

  if ( widget instanceof BCheckBox || widget instanceof BRadioButton ) {
    preferencesWriter.write( widgetString + widget.getState().toString() + "\n" );

    return;
  }

  if ( widget instanceof BOutline ) { // it would be better to save the value because the list might change, but I'm lazy
    BList bList = widget.getContent().getContent();
    bList = widget.getContent().getContent();
    preferencesWriter.write( widgetString + bList.getSelectedIndex().toString() + "\n" );

    return;
  }

  if ( widget instanceof BTextField ) {
    preferencesWriter.write( widgetString + widget.getText() + "\n" );

    return;
  }

  if ( widget instanceof ValueField ) {
    preferencesWriter.write( widgetString + widget.getValue().toString() + "\n" );
  }
}

/**
Set the coordinate system to a rotation around the z axis in degrees.

@param  coordinateSystem coordinate system with rotations defined around the x, y, z axes in degrees
@param  rotationDegree axis rotation angle in degrees
*/

void setCoordinateSystemToRotationDegree( CoordinateSystem coordinateSystem, double rotationDegree )
{
  coordinateSystem.setOrientation( 0.0, 0.0, rotationDegree );
}

// Trim outer tooth of the first gear which collides with the second gear.

void trimCollision( double numberOfTeethFirstGear, double numberOfTeethSecondGear, double pitchRadiusFirstGear, Vec2[] toothProfileFirstGear, Vec2[] toothProfileSecondGear )
{
  maximumAngle = Math.PI / 8;

  if ( numberOfTeethFirstGear > numberOfTeethSecondGear ) {
    maximumAngle *= numberOfTeethSecondGear / numberOfTeethFirstGear;
  }

  for ( double angle = 0.0; angle < maximumAngle; angle += 0.01 ) {
    trimCollisionAtAngle( angle, numberOfTeethFirstGear, numberOfTeethSecondGear, pitchRadiusFirstGear, toothProfileFirstGear, toothProfileSecondGear );
  }
}

// Trim outer tooth of the first gear which collides with the second gear at an angle.

void trimCollisionAtAngle( double angle, double numberOfTeethFirstGear, double numberOfTeethSecondGear, double pitchRadiusFirstGear, Vec2[] toothProfileFirstGear, Vec2[] toothProfileSecondGear )
{
  double pitchRadiusSecondGear = getPitchRadiusSecondGear( numberOfTeethFirstGear, numberOfTeethSecondGear, pitchRadiusFirstGear );

  toothProfileSecondGearRotated = new Vec2[ toothProfileSecondGear.length ];
  rotationSecondGear = Math.PI - Math.PI / numberOfTeethSecondGear - angle * numberOfTeethFirstGear / numberOfTeethSecondGear;

  for ( int profilePointIndex = 0; profilePointIndex <  toothProfileSecondGear.length; profilePointIndex++ ) {
    rotated = getRotatedVector( rotationSecondGear, toothProfileSecondGear[ profilePointIndex ] );
    rotated.y += pitchRadiusFirstGear + pitchRadiusSecondGear;
    toothProfileSecondGearRotated[ profilePointIndex ] = getRotatedVector( - angle, rotated );
  }

  for ( int profilePointIndex = 0; profilePointIndex <  toothProfileFirstGear.length; profilePointIndex++ ) {
    trimCollisionProfilePoint( profilePointIndex, toothProfileFirstGear, toothProfileSecondGearRotated );
  }
}

// Trim point from first gear which collides with the second gear.

void trimCollisionProfilePoint( int profilePointIndex, Vec2[] toothProfileFirstGear, Vec2[] toothProfileSecondGearRotated )
{
  firstGearProfilePoint = toothProfileFirstGear[ profilePointIndex ];

  if ( firstGearProfilePoint.length() < pitchRadiusFirstGear ) {
    return;
  }

  if ( firstGearProfilePoint.x > 0.0 ) {
    return;
  }

  for ( int secondGearPointIndex = 0; secondGearPointIndex <  toothProfileSecondGearRotated.length - 1; secondGearPointIndex++ ) {
    firstPointSecondGear = toothProfileSecondGearRotated[ secondGearPointIndex ];
    secondPointSecondGear = toothProfileSecondGearRotated[ secondGearPointIndex + 1 ];
    firstPointAbove = firstPointSecondGear.y - firstGearProfilePoint.y;
    secondPointAbove = secondPointSecondGear.y - firstGearProfilePoint.y;

    if ( firstPointAbove * secondPointAbove <= 0.0 ) {
      verticalSeparation = firstPointSecondGear.y - secondPointSecondGear.y;
      horizontalToothAxisIntersection = firstPointSecondGear.minus( secondPointSecondGear );
      horizontalToothAxisIntersection.scale( - secondPointAbove / verticalSeparation );
      horizontalToothAxisIntersection.add( secondPointSecondGear );
      collision = horizontalToothAxisIntersection.x - firstGearProfilePoint.x;

      if ( collision > 0.0 ) {
        firstGearProfilePoint.x += collision;
        mirrorPointIndex = toothProfileFirstGear.length - 1 - profilePointIndex;
        toothProfileFirstGear[ mirrorPointIndex ].x -= collision;
      }
    }
  }
}

// Trim outer tooth by tolerance.

void trimTolerance( double numberOfTeeth, double pitchRadius, double tolerance, Vec2[] toothProfile )
{
  if ( tolerance == 0.0 ) {
    return;
  }

  double otherGearStart = pitchRadius * ( 1 - getOutsidePitch( numberOfTeeth ) ) - tolerance;
  double innerRadius = getInnerRadius( numberOfTeeth, pitchRadius );

  for ( int profilePointIndex = 0; profilePointIndex <  toothProfile.length; profilePointIndex++ ) {
    toothProfilePoint = toothProfile[ profilePointIndex ];
    toothProfilePointLength = toothProfilePoint.length();
    shortenRatio = 1.0;

    if ( toothProfilePointLength < otherGearStart ) {
      shortenRatio = ( toothProfilePointLength - innerRadius ) / ( otherGearStart - innerRadius );
      shortenRatio = Math.max( 0.0, shortenRatio );
    }

    xAbsolute = Math.abs( toothProfilePoint.x );
    xAbsoluteTrimmed = xAbsolute - tolerance * shortenRatio;
    xAbsoluteTrimmed = Math.max( 0.0, xAbsoluteTrimmed );

    if ( xAbsolute > 0.0 ) {
      toothProfilePoint.x *= xAbsoluteTrimmed / xAbsolute;
    }
  }
}

// Get parameters.

int[] primeNumbers = { 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149 };
firstGearPrimeList = getPrimeList( 6 );
numberOfTeethFirstGearValueField = new ValueField( 0, ValueField.NONNEGATIVE );
pitchRadiusFirstGearValueField = new ValueField( 75, ValueField.NONNEGATIVE );
secondGearPrimeList = getPrimeList( 1 );
numberOfTeethSecondGearValueField = new ValueField( 0, ValueField.NONNEGATIVE );
minimumRimCutoutRadiusValueField = new ValueField( 2.0, ValueField.NONNEGATIVE );
minimumRimThicknessValueField = new ValueField( 0.2, ValueField.NONNEGATIVE );
String preferencesFilename = "gearweaver_preferences.csv";
pressureAngleDegreeValueField = new ValueField( 20, ValueField.NONNEGATIVE );
profileDefinitionValueField = new ValueField( 9, ValueField.NONNEGATIVE );
shaftRadiusValueField = new ValueField( 5.0, ValueField.NONNEGATIVE);
shaftSidesValueField = new ValueField( 4.0, ValueField.NONNEGATIVE);
shaftTruncationBottomValueField = new ValueField( 0.0, ValueField.NONNEGATIVE);
shaftTruncationTopValueField = new ValueField( 0.0, ValueField.NONNEGATIVE);
shovelAngleValueField = new ValueField( 20.0, ValueField.NONNEGATIVE);
shovelSegmentsValueField = new ValueField( 3, ValueField.NONNEGATIVE);
//simulationDurationValueField = new ValueField( 10, ValueField.NONNEGATIVE);
simulationDurationValueField = new ValueField( 0, ValueField.NONNEGATIVE);
simulationRotationRateValueField = new ValueField( 0.1, ValueField.NONNEGATIVE );
thicknessValueField = new ValueField( 30.0, ValueField.NONNEGATIVE );
//thicknessValueField = new ValueField( 0.0, ValueField.NONNEGATIVE );
toleranceValueField = new ValueField( 0.0, ValueField.NONNEGATIVE );

Widget[] widgets = new Widget [] {	UIUtilities.createScrollingList( firstGearPrimeList ),
						numberOfTeethFirstGearValueField,
						pitchRadiusFirstGearValueField,
						UIUtilities.createScrollingList( secondGearPrimeList ),
						numberOfTeethSecondGearValueField,
						minimumRimCutoutRadiusValueField,
						minimumRimThicknessValueField,
						pressureAngleDegreeValueField,
						profileDefinitionValueField,
						shaftRadiusValueField,
						shaftSidesValueField,
						shaftTruncationBottomValueField,
						shaftTruncationTopValueField,
						shovelAngleValueField,
						shovelSegmentsValueField,
						simulationDurationValueField,
						simulationRotationRateValueField,
						thicknessValueField,
						toleranceValueField };

String[] widgetStrings = new String [] {	"Number of Teeth for First Gear (prime):",
						"or Number of Teeth for First Gear (integer):",
						"Pitch Radius First Gear (mm):",
						"Number of Teeth for Second Gear (prime):",
						"or Number of Teeth for Second Gear (integer):",
						"Minimum Rim Cutout Radius (mm):",
						"Minimum Rim Thickness (ratio):",
						"Pressure Angle (degrees):",
						"Profile Definition (surfaces):",
						"Shaft Radius (mm):",
						"Shaft Sides (surfaces):",
						"Shaft Truncation Bottom (ratio):",
						"Shaft Truncation Top (ratio):",
						"Shovel Angle (degrees):",
						"Shovel Segments (surfaces):",
						"Simulation Duration (seconds):",
						"Simulation Rotation Rate (teeth / second):",
						"Thickness (mm):",
						"Tolerance (mm):" };

// change the user interface parameters from default to preferences
Vector widgetVector = new Vector();
preferencesAddWidgetsWithStrings( widgets, widgetStrings, widgetVector );
preferencesRead( preferencesFilename, widgetVector );

dialog = new ComponentsDialog( window, "Gearweaver Generator", widgets, widgetStrings );

if ( !dialog.clickedOk() ) return;

preferencesWrite( preferencesFilename, widgetVector );

double numberOfTeethFirstGear = getNumberOfTeeth( firstGearPrimeList, numberOfTeethFirstGearValueField );
double numberOfTeethSecondGear = getNumberOfTeeth( secondGearPrimeList, numberOfTeethSecondGearValueField );
double pitchRadiusFirstGear = pitchRadiusFirstGearValueField.getValue();
double pitchRadiusSecondGear = getPitchRadiusSecondGear( numberOfTeethFirstGear, numberOfTeethSecondGear, pitchRadiusFirstGear );
double minimumRimCutoutRadius = minimumRimCutoutRadiusValueField.getValue();
double minimumRimThickness = minimumRimThicknessValueField.getValue();
int profileDefinitionSurfaces = ( int )Math.max( 1.0, Math.min( 30.0, profileDefinitionValueField.getValue() ) );
double pressureAngle = pressureAngleDegreeValueField.getValue() * Math.PI / 180.0;
double shaftRadius = shaftRadiusValueField.getValue();
int shaftSides = ( int )Math.max( 3.0, shaftSidesValueField.getValue() );
double shaftTruncationBottom = shaftTruncationBottomValueField.getValue();
double shaftTruncationTop = shaftTruncationTopValueField.getValue();
double shovelAngle = shovelAngleValueField.getValue() * Math.PI / 180.0;
int shovelSegments = ( int )Math.max( 1.0, shovelSegmentsValueField.getValue() );
double simulationDuration = simulationDurationValueField.getValue();
double simulationRotationRate = simulationRotationRateValueField.getValue();
double thickness = thicknessValueField.getValue();
double tolerance = toleranceValueField.getValue();

//  Create gear pair.

toothProfileFirstGear = getToothProfile( numberOfTeethFirstGear, pitchRadiusFirstGear, pressureAngle, profileDefinitionSurfaces );
toothProfileSecondGear = getToothProfile( numberOfTeethSecondGear, pitchRadiusSecondGear, pressureAngle, profileDefinitionSurfaces );

trimCollision( numberOfTeethFirstGear, numberOfTeethSecondGear, pitchRadiusFirstGear, toothProfileFirstGear, toothProfileSecondGear );
trimCollision( numberOfTeethSecondGear, numberOfTeethFirstGear, pitchRadiusSecondGear, toothProfileSecondGear, toothProfileFirstGear );

firstGearCoordinateSystem = new CoordinateSystem();
firstGearCoordinateSystemOld = firstGearCoordinateSystem.duplicate();
trimTolerance( numberOfTeethFirstGear, pitchRadiusFirstGear, tolerance, toothProfileFirstGear );
addGear( firstGearCoordinateSystem, "First Gear Profile", numberOfTeethFirstGear, toothProfileFirstGear );

secondGearCoordinateSystem = new CoordinateSystem();
secondGearCoordinateSystem.setOrigin( new Vec3( 0.0, pitchRadiusFirstGear + pitchRadiusSecondGear, 0.0 ) );
secondGearCoordinateOffsetDegree = 180.0 * ( numberOfTeethSecondGear % 2 + 1 ) / numberOfTeethSecondGear;
setCoordinateSystemToRotationDegree( secondGearCoordinateSystem, secondGearCoordinateOffsetDegree );
secondGearCoordinateSystemOld = secondGearCoordinateSystem.duplicate();
trimTolerance( numberOfTeethSecondGear, pitchRadiusSecondGear, tolerance, toothProfileSecondGear );
addGear( secondGearCoordinateSystem, "Second Gear Profile", numberOfTeethSecondGear, toothProfileSecondGear );

import java.util.Timer;
import java.util.TimerTask;

Timer timer = new Timer();

startTime = System.currentTimeMillis();
sceneCameraInfo = getCameraInfo();
sceneCameraCoordsOld = null;
sceneCameraPropertiesOld = null;

if ( sceneCameraInfo != null ) {
  sceneCameraCoordsOld = sceneCameraInfo.coords.duplicate();
  sceneCamera = sceneCameraInfo.object;
  properties = sceneCamera.getProperties();
  propertiesLength = properties.length;
  sceneCameraPropertiesOld = new double[ propertiesLength ];

  for ( propertyIndex = 0; propertyIndex < propertiesLength; propertyIndex++ ) {
    sceneCameraPropertiesOld[ propertyIndex ] = sceneCamera.getPropertyValue( propertyIndex );
    propertyName = properties[ propertyIndex ].getName();

    if ( propertyName.equals( "Depth of Field" ) ) {
      sceneCamera.setPropertyValue( propertyIndex, 10.0 );
    }

    if ( propertyName.equals( "Field of View" ) ) {
      sceneCamera.setPropertyValue( propertyIndex, 30.0 );
    }

    if ( propertyName.equals( "Focal Distance" ) ) {
      sceneCamera.setPropertyValue( propertyIndex, 20.0 );
    }
  }
  sceneCameraInfo.coords.setOrigin( new Vec3( 0.0, pitchRadiusFirstGear, 15.0 * pitchRadiusFirstGear / numberOfTeethFirstGear ) );
  sceneCameraInfo.coords.setOrientation( 0.0, 180.0, 0.0 );
  sceneCameraInfo.coords.setOrientation( 0.0, 180.0, 0.0 );
}

public final class TurnGear extends TimerTask {
  /**
  * Implements TimerTask's abstract run method.
  */
  public void run() {
    elapsed = ( System.currentTimeMillis() - startTime ) / 1000.0;
    elapsedRotation = 360.0 * elapsed * simulationRotationRate;
    setCoordinateSystemToRotationDegree( firstGearCoordinateSystem, - elapsedRotation / numberOfTeethFirstGear );
    setCoordinateSystemToRotationDegree( secondGearCoordinateSystem, elapsedRotation / numberOfTeethSecondGear + secondGearCoordinateOffsetDegree );
    scene = window.getScene();
    scene.objectModified( scene.getObject( "First Gear Profile" ).object );
    scene.objectModified( scene.getObject( "Second Gear Profile" ).object );
    window.updateImage();

    if ( elapsed > simulationDuration ) {
      endTurnGear();
    }
  }
}

TimerTask turnGear  = new TurnGear();
timer.scheduleAtFixedRate( turnGear, 0, 13 );