/****************************************************************************** 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.ComponentModel; 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; namespace RJ_Demo_IK { /// /// 2D two bone analytic based inverse kinematics for one solution /// public partial class CalcIK_2D_TwoBoneAnalytic : UserControl, INotifyPropertyChanged { #region Private data // bone lengths private double m_bone1_length = 100; private double m_bone2_length = 40; // bone angles private double m_bone1_angle = 0; private double m_bone2_angle = 0; // IK solution mode private bool m_bone2_usePositiveAngle = true; // target position to reach for private Point m_targetPos = new Point(0,0); // result of current IK calculation private bool m_validSolution = false; #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 bool ValidSolution { get { return m_validSolution; } set { m_validSolution = value; NotifyPropertyChanged("ValidSolution"); // update the bound UI } } public double Bone1_Length { get { return m_bone1_length; } set { m_bone1_length = Math.Max(0,value); // only allow positive length values UpdateBonePositions(); // redraw the IK state } } public double Bone2_Length { get { return m_bone2_length; } set { m_bone2_length = Math.Max(0,value); // only allow positive length values UpdateBonePositions(); // redraw the IK state } } public bool Bone2_UsePositiveAngle { get { return m_bone2_usePositiveAngle; } set { m_bone2_usePositiveAngle = value; UpdateBonePositions(); // redraw the IK state } } public double TargetPosX { get { return m_targetPos.X; } set { m_targetPos.X = value; NotifyPropertyChanged("TargetPosX"); // update the bound UI UpdateBonePositions(); // redraw the IK state } } public double TargetPosY { get { return m_targetPos.Y; } set { m_targetPos.Y = value; NotifyPropertyChanged("TargetPosY"); // update the bound UI UpdateBonePositions(); // redraw the IK state } } public double Bone1_Degrees { get { return m_bone1_angle * 180.0 / Math.PI; } } public double Bone2_Degrees { get { return m_bone2_angle * 180.0 / Math.PI; } } #endregion #region Lifespan functions public CalcIK_2D_TwoBoneAnalytic() { InitializeComponent(); // iniitalize the desired position to the end point of the bones TargetPosX = m_bone1_length * Math.Cos(m_bone1_angle) + m_bone2_length * Math.Cos(m_bone1_angle + m_bone2_angle); TargetPosY = m_bone1_length * Math.Sin(m_bone1_angle) + m_bone2_length * Math.Sin(m_bone1_angle + m_bone2_angle); // update the display UpdateBonePositions(); } #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 /// /// Evaluate the IK problem based on the current parameters and update the display /// shapes inside the current viewport. /// private void UpdateBonePositions() { // calculate the bone angles ValidSolution = IKSolver.CalcIK_2D_TwoBoneAnalytic( out m_bone1_angle, out m_bone2_angle, m_bone2_usePositiveAngle, Bone1_Length, Bone2_Length, TargetPosX, TargetPosY ); // compute the the end points of the bones in logical space double cosAngle1 = Math.Cos(m_bone1_angle); double sinAngle1 = Math.Sin(m_bone1_angle); double cosAngle12 = Math.Cos(m_bone1_angle + m_bone2_angle); double sinAngle12 = Math.Sin(m_bone1_angle + m_bone2_angle); double bone1_endX = Bone1_Length * cosAngle1; double bone1_endY = Bone1_Length * sinAngle1; double bone2_endX = bone1_endX + Bone2_Length * cosAngle12; double bone2_endY = bone1_endY + Bone2_Length * sinAngle12; // output the coordinates in _viewport space _bone1_line.X1 = LogicalToViewportX( 0 ); _bone1_line.Y1 = LogicalToViewportY( 0 ); _bone1_line.X2 = LogicalToViewportX( bone1_endX ); _bone1_line.Y2 = LogicalToViewportY( bone1_endY ); _bone2_line.X1 = _bone1_line.X2; _bone2_line.Y1 = _bone1_line.Y2; _bone2_line.X2 = LogicalToViewportX( bone2_endX ); _bone2_line.Y2 = LogicalToViewportY( bone2_endY ); // compute the range of our bones double minRange = Math.Abs(Bone1_Length - Bone2_Length); double maxRange = Bone1_Length + Bone2_Length; // draw the boundaries Canvas.SetLeft( _minRangeEllipse, LogicalToViewportX(-minRange) ); Canvas.SetTop( _minRangeEllipse, LogicalToViewportY(minRange) ); _minRangeEllipse.Width = 2*minRange; _minRangeEllipse.Height = 2*minRange; Canvas.SetLeft( _maxRangeEllipse, LogicalToViewportX(-maxRange) ); Canvas.SetTop( _maxRangeEllipse, LogicalToViewportY(maxRange) ); _maxRangeEllipse.Width = 2*maxRange; _maxRangeEllipse.Height = 2*maxRange; // draw the range of bone2 Canvas.SetLeft( _bone2RangeEllipse, LogicalToViewportX(bone1_endX - Bone2_Length) ); Canvas.SetTop( _bone2RangeEllipse, LogicalToViewportY(bone1_endY + Bone2_Length) ); _bone2RangeEllipse.Width = 2*Bone2_Length; _bone2RangeEllipse.Height = 2*Bone2_Length; // draw the target Canvas.SetLeft( _targetEllipse, LogicalToViewportX(TargetPosX - _targetEllipse.Width/2) ); Canvas.SetTop( _targetEllipse, LogicalToViewportY(TargetPosY + _targetEllipse.Height/2) ); // draw the extended bone 1 line _bone1_extended_line.X1 = LogicalToViewportX( 0 ); _bone1_extended_line.Y1 = LogicalToViewportY( 0 ); _bone1_extended_line.X2 = LogicalToViewportX( (Bone1_Length + Bone2_Length) * cosAngle1 ); _bone1_extended_line.Y2 = LogicalToViewportY( (Bone1_Length + Bone2_Length) * sinAngle1 ); // 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; // draw the theta arcs { double theta1ArcRadius = 0.3 * Bone1_Length; PathGeometry theta1ArcPathGeo = _theta1ArcPath.Data as PathGeometry; PathFigure theta1ArcPathFigure = theta1ArcPathGeo.Figures[0]; ArcSegment theta1Arc = theta1ArcPathFigure.Segments[0] as ArcSegment; theta1Arc.Size = new Size(theta1ArcRadius,theta1ArcRadius); theta1ArcPathFigure.StartPoint = new Point( LogicalToViewportX(theta1ArcRadius), LogicalToViewportY(0) ); theta1Arc.Point = new Point( LogicalToViewportX( cosAngle1 * theta1ArcRadius ), LogicalToViewportY( sinAngle1 * theta1ArcRadius ) ); theta1Arc.SweepDirection = (m_bone1_angle >= 0) ? SweepDirection.Counterclockwise : SweepDirection.Clockwise; theta1Arc.IsLargeArc = (m_bone1_angle > Math.PI); } { double theta2ArcRadius = 0.3 * Math.Min( Bone1_Length, Bone2_Length ); PathGeometry theta2ArcPathGeo = _theta2ArcPath.Data as PathGeometry; PathFigure theta2ArcPathFigure = theta2ArcPathGeo.Figures[0]; ArcSegment theta2Arc = theta2ArcPathFigure.Segments[0] as ArcSegment; theta2Arc.Size = new Size(theta2ArcRadius,theta2ArcRadius); theta2ArcPathFigure.StartPoint = new Point( LogicalToViewportX( (Bone1_Length + theta2ArcRadius) * cosAngle1 ), LogicalToViewportY( (Bone1_Length + theta2ArcRadius) * sinAngle1 ) ); theta2Arc.Point = new Point( LogicalToViewportX( bone1_endX + cosAngle12 * theta2ArcRadius ), LogicalToViewportY( bone1_endY + sinAngle12 * theta2ArcRadius ) ); theta2Arc.SweepDirection = (Bone2_UsePositiveAngle) ? SweepDirection.Counterclockwise : SweepDirection.Clockwise; theta2Arc.IsLargeArc = false; } // notify the user interface of changes NotifyPropertyChanged("Bone1_Degrees"); NotifyPropertyChanged("Bone2_Degrees"); } #endregion #region Event Handlers 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 UpdateBonePositions(); } private void _websiteLink_Click(object sender, RoutedEventArgs e) { System.Diagnostics.Process.Start( "http://www.ryanjuckett.com" ); } #endregion } }