/******************************************************************************
Copyright (c) 2008-2009 Ryan Juckett
http://www.ryanjuckett.com/
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
******************************************************************************/
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace RJ_Demo_IK
{
///
/// 2D cyclic coordinate descent based inverse kinematics
///
public partial class CalcIK_2D_CCD : UserControl, INotifyPropertyChanged
{
#region Internal types
// this class represents a bone in it's parent space
public class BoneData : INotifyPropertyChanged
{
private double m_length = 0;
private double m_angle = 0;
#region INotifyPropertyChanged interface
// event used by the user interface to bind to our properties
public event PropertyChangedEventHandler PropertyChanged;
// helper function to notify PropertyChanged subscribers
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
public double Length
{
get { return m_length; }
set { m_length = value; NotifyPropertyChanged("Length"); }
}
public double Radians
{
get { return m_angle; }
set { m_angle = value; NotifyPropertyChanged("Radians"); NotifyPropertyChanged("Degrees"); }
}
public double Degrees
{
get { return m_angle * 180.0 / Math.PI; }
set { m_angle = value * Math.PI / 180.0; NotifyPropertyChanged("Radians"); NotifyPropertyChanged("Degrees"); }
}
}
#endregion
#region Private data
private static Brush[] s_boneLineColors =
{
Brushes.Black,
Brushes.GreenYellow,
Brushes.DarkBlue,
Brushes.DarkRed,
Brushes.MintCream,
};
// number of CCD iterations to perform on each update
private int m_iterationsPerUpdate = 0;
// bone lengths
private ObservableCollection m_bones = new ObservableCollection();
// lines used to draw the bones
private List m_boneLines = new List();
// target position to reach for
private Point m_targetPos = new Point(0,0);
// max distance from end effector to target for a valid solution
private double m_arrivalDist = 1.0;
// result of current IK calculation
private string m_ccdResult = "";
private DispatcherTimer m_updateTimer;
#endregion
#region INotifyPropertyChanged interface
// event used by the user interface to bind to our properties
public event PropertyChangedEventHandler PropertyChanged;
// helper function to notify PropertyChanged subscribers
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region Public properties
public ObservableCollection Bones
{
get { return m_bones; }
}
public int IterationsPerUpdate
{
get { return m_iterationsPerUpdate; }
set
{
m_iterationsPerUpdate = Math.Max(1,value);
NotifyPropertyChanged("IterationsPerUpdate"); // update the bound UI
}
}
public string CCDResult
{
get { return m_ccdResult; }
set
{
m_ccdResult = value;
NotifyPropertyChanged("CCDResult"); // update the bound UI
}
}
public double TargetPosX
{
get { return m_targetPos.X; }
set
{
m_targetPos.X = value;
NotifyPropertyChanged("TargetPosX"); // update the bound UI
UpdateDisplay(); // redraw
}
}
public double TargetPosY
{
get { return m_targetPos.Y; }
set
{
m_targetPos.Y = value;
NotifyPropertyChanged("TargetPosY"); // update the bound UI
UpdateDisplay(); // redraw
}
}
public double ArrivalDist
{
get { return m_arrivalDist; }
set
{
m_arrivalDist = value;
NotifyPropertyChanged("ArrivalDist"); // update the bound UI
UpdateDisplay(); // redraw
}
}
#endregion
#region Lifespan functions
public CalcIK_2D_CCD()
{
InitializeComponent();
// set the iteration number
IterationsPerUpdate = 1;
// create the timer
m_updateTimer = new DispatcherTimer(DispatcherPriority.Normal);
m_updateTimer.Tick += new EventHandler(UpdateTimer_Tick);
m_updateTimer.Interval = new TimeSpan(0, 0, 0, 0, 100);
// add the initial bones
AddBone();
AddBone();
TargetPosX = 100;
TargetPosY = 0;
// update the display
UpdateDisplay();
}
#endregion
#region Coordinate Conversion
// compute the logical origin in _viewport coordinated
private double ViewportWidth { get { return _viewportColumn.ActualWidth; } }
private double ViewportHieght { get { return _mainGrid.ActualHeight; } }
private double ViewportCenterX { get { return ViewportWidth / 2; } }
private double ViewportCenterY { get { return ViewportHieght / 2; } }
// convert logical coordinates to _viewport coordinates
private double LogicalToViewportX(double logicalX) { return logicalX + ViewportCenterX; }
private double LogicalToViewportY(double logicalY) { return -logicalY + ViewportCenterY; }
// convert _viewport coordinates to logical coordinates
private double ViewportToLogicalX(double viewportX) { return viewportX - ViewportCenterX; }
private double ViewportToLogicalY(double viewportY) { return -viewportY + ViewportCenterY; }
#endregion
#region Logic Functions
///
/// Add a new bone to the chain at the selected location or at the end if no location is selected.
///
void AddBone()
{
BoneData newBone = new BoneData();
newBone.Length = 50;
newBone.PropertyChanged += BonePropertyChanged;
// insert at the end if no bone is selected
if( _boneList.SelectedIndex == -1 )
Bones.Add( newBone );
else
Bones.Insert( _boneList.SelectedIndex, newBone );
}
///
/// Remove a new bone from the chain at the selected location or from the end if no location is selected.
///
private void RemoveBone()
{
if( Bones.Count == 0 )
return;
// remove the end bone if no bone is selected
int removeIdx = _boneList.SelectedIndex;
if( removeIdx == -1 )
removeIdx = (Bones.Count - 1);
Bones[removeIdx].PropertyChanged -= BonePropertyChanged;
Bones.RemoveAt( removeIdx );
}
///
/// Perform an iteration of IK
///
private void UpdateIK()
{
int numBones = Bones.Count;
if( numBones == 0 )
return;
// calculate the bone angles
List< IKSolver.Bone_2D_CCD > ccdBones = new List< IKSolver.Bone_2D_CCD >();
for( int boneIdx = 0; boneIdx <= numBones; ++boneIdx )
{
IKSolver.Bone_2D_CCD newCcdBone = new IKSolver.Bone_2D_CCD();
newCcdBone.angle = (boneIdx < numBones) ? Bones[boneIdx].Radians : 0;
newCcdBone.x = (boneIdx > 0) ? Bones[boneIdx-1].Length : 0;
newCcdBone.y = 0;
ccdBones.Add( newCcdBone );
}
// iterate CCD until limit is reached or we find a valid solution
for( int itrCount = 0; itrCount < IterationsPerUpdate; ++itrCount )
{
IKSolver.CCDResult result = IKSolver.CalcIK_2D_CCD( ref ccdBones, TargetPosX, TargetPosY, ArrivalDist );
if( result == IKSolver.CCDResult.Processing )
{
CCDResult = "Processing";
}
else if( result == IKSolver.CCDResult.Success )
{
CCDResult = "Success";
break;
}
else if( result == IKSolver.CCDResult.Failure )
{
CCDResult = "Failure";
break;
}
else
{
Debug.Assert(false);
CCDResult = "[UNKNOWN]";
break;
}
}
// extract the new bone data from the results
for( int boneIdx = 0; boneIdx < numBones; ++boneIdx )
{
Bones[boneIdx].Radians = ccdBones[boneIdx].angle;
}
}
///
/// Update the scene displayed in the viewport
///
private void UpdateDisplay()
{
int numBones = Bones.Count;
// resize the number of bone lines
while( m_boneLines.Count > numBones )
{
_viewport.Children.Remove( m_boneLines[m_boneLines.Count-1] );
m_boneLines.RemoveAt( m_boneLines.Count-1 );
}
while( m_boneLines.Count < numBones )
{
Line newBoneLine = new Line();
newBoneLine.Stroke = s_boneLineColors[m_boneLines.Count % s_boneLineColors.Length];
newBoneLine.StrokeThickness = 3;
newBoneLine.SetValue( Panel.ZIndexProperty, 100 );
m_boneLines.Add( newBoneLine );
_viewport.Children.Add( newBoneLine );
}
// compute the orientations of the bone lines in logical space
double curAngle = 0;
for( int boneIdx = 0; boneIdx < numBones; ++boneIdx )
{
BoneData curBone = Bones[boneIdx];
curAngle += curBone.Radians;
double cosAngle = Math.Cos( curAngle );
double sinAngle = Math.Sin( curAngle );
if( boneIdx > 0 )
{
m_boneLines[boneIdx].X1 = m_boneLines[boneIdx-1].X2;
m_boneLines[boneIdx].Y1 = m_boneLines[boneIdx-1].Y2;
}
else
{
m_boneLines[boneIdx].X1 = 0;
m_boneLines[boneIdx].Y1 = 0;
}
m_boneLines[boneIdx].X2 = m_boneLines[boneIdx].X1 + cosAngle*curBone.Length;
m_boneLines[boneIdx].Y2 = m_boneLines[boneIdx].Y1 + sinAngle*curBone.Length;
}
// convert the bone positions to viewport space
foreach( Line curLine in m_boneLines )
{
curLine.X1 = LogicalToViewportX(curLine.X1);
curLine.Y1 = LogicalToViewportY(curLine.Y1);
curLine.X2 = LogicalToViewportX(curLine.X2);
curLine.Y2 = LogicalToViewportY(curLine.Y2);
}
// draw the arrival distance
Canvas.SetLeft( _arrivalEllipse, LogicalToViewportX(TargetPosX - ArrivalDist) );
Canvas.SetTop( _arrivalEllipse, LogicalToViewportY(TargetPosY + ArrivalDist) );
_arrivalEllipse.Width = 2.0 * ArrivalDist;
_arrivalEllipse.Height = 2.0 * ArrivalDist;
// draw the target
Canvas.SetLeft( _targetEllipse, LogicalToViewportX(TargetPosX - _targetEllipse.Width/2) );
Canvas.SetTop( _targetEllipse, LogicalToViewportY(TargetPosY + _targetEllipse.Height/2) );
// draw the axes
_xAxisLine.X1 = 0;
_xAxisLine.Y1 = ViewportCenterY;
_xAxisLine.X2 = ViewportWidth;
_xAxisLine.Y2 = ViewportCenterY;
_yAxisLine.X1 = ViewportCenterX;
_yAxisLine.Y1 = 0;
_yAxisLine.X2 = ViewportCenterX;
_yAxisLine.Y2 = ViewportHieght;
}
///
/// Update logic at a set interval
///
private void UpdateTimer_Tick(object sender, EventArgs e)
{
UpdateIK();
UpdateDisplay();
}
#endregion
#region Event Handlers
private void BonePropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Radians":
UpdateDisplay();
break;
case "Length":
UpdateDisplay();
break;
}
}
private void viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// capture the mouse to keep grabing MouseMove events if the user drags the
// mouse outside of the _viewport bounds
if (!_viewport.IsMouseCaptured)
{
_viewport.CaptureMouse();
}
// update the target position
Point viewportPos = e.GetPosition(_viewport);
TargetPosX = ViewportToLogicalX( viewportPos.X );
TargetPosY = ViewportToLogicalY( viewportPos.Y );
}
private void viewport_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// release the captured mouse
if (_viewport.IsMouseCaptured)
{
_viewport.ReleaseMouseCapture();
}
}
private void viewport_MouseMove(object sender, MouseEventArgs e)
{
// update the target position if we are still in a captured state
// (i.e. the user has not released the mouse button)
if (_viewport.IsMouseCaptured)
{
Point viewportPos = e.GetPosition(_viewport);
TargetPosX = ViewportToLogicalX( viewportPos.X );
TargetPosY = ViewportToLogicalY( viewportPos.Y );
}
}
private void _thisWindow_SizeChanged(object sender, SizeChangedEventArgs e)
{
// update the display shapes based on the new window size
UpdateDisplay();
}
private void _thisWindow_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if( m_updateTimer != null )
{
if( this.IsVisible )
{
if( _playRadioButton.IsChecked == true )
m_updateTimer.Start();
}
else
{
m_updateTimer.Stop();
}
}
}
private void _websiteLink_Click(object sender, RoutedEventArgs e)
{
System.Diagnostics.Process.Start( "http://www.ryanjuckett.com" );
}
private void _addBoneButton_Click(object sender, RoutedEventArgs e)
{
AddBone();
UpdateDisplay();
}
private void _removeBoneButton_Click(object sender, RoutedEventArgs e)
{
RemoveBone();
UpdateDisplay();
}
private void _playRadioButton_Checked(object sender, RoutedEventArgs e)
{
if( m_updateTimer != null )
m_updateTimer.Start();
}
private void _pauseRadioButton_Checked(object sender, RoutedEventArgs e)
{
if( m_updateTimer != null )
m_updateTimer.Stop();
}
private void _singleUpdateButton_Click(object sender, RoutedEventArgs e)
{
_pauseRadioButton.IsChecked = true;
UpdateIK();
UpdateDisplay();
}
#endregion
}
}