File: System\Windows\Input\Stylus\Common\StylusPoint.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
 
using System;
using System.Windows;
using System.Diagnostics;
using System.Windows.Input;
using System.Windows.Media;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using MS.Utility;
using MS.Internal;
using SR = MS.Internal.PresentationCore.SR;
 
namespace System.Windows.Input
{
    /// <summary>
    /// Represents a single sampling point from a stylus input device
    /// </summary>
    public struct StylusPoint : IEquatable<StylusPoint>
    {
        internal const float DefaultPressure = 0.5f;
 
 
        private double _x;
        private double _y;
        private float _pressureFactor;
        private int[] _additionalValues;
        private StylusPointDescription _stylusPointDescription;
 
        #region Constructors
        /// <summary>
        /// StylusPoint
        /// </summary>
        /// <param name="x">x</param>
        /// <param name="y">y</param>
        public StylusPoint(double x, double y)
            : this(x, y, DefaultPressure, null, null, false, false)
        {
        }
 
        /// <summary>
        /// StylusPoint
        /// </summary>
        /// <param name="x">x</param>
        /// <param name="y">y</param>
        /// <param name="pressureFactor">pressureFactor</param>
        public StylusPoint(double x, double y, float pressureFactor)
            : this(x, y, pressureFactor, null, null, false, true)
        {
        }
        
 
        /// <summary>
        /// StylusPoint
        /// </summary>
        /// <param name="x">x</param>
        /// <param name="y">y</param>
        /// <param name="pressureFactor">pressureFactor</param>
        /// <param name="stylusPointDescription">stylusPointDescription</param>
        /// <param name="additionalValues">additionalValues</param>
        public StylusPoint(double x, double y, float pressureFactor, StylusPointDescription stylusPointDescription, int[] additionalValues)
            : this(x, y, pressureFactor, stylusPointDescription, additionalValues, true, true)
        {
        }
 
        /// <summary>
        /// internal ctor
        /// </summary>
        internal StylusPoint(   
            double x, 
            double y, 
            float pressureFactor, 
            StylusPointDescription stylusPointDescription, 
            int[] additionalValues, 
            bool validateAdditionalData,
            bool validatePressureFactor)
        {
            if (Double.IsNaN(x))
            {
                throw new ArgumentOutOfRangeException("x", SR.InvalidStylusPointXYNaN);
            }
            if (Double.IsNaN(y))
            {
                throw new ArgumentOutOfRangeException("y", SR.InvalidStylusPointXYNaN);
            }
 
 
            //we don't validate pressure when called by StylusPointDescription.Reformat
            if (validatePressureFactor &&
                (pressureFactor == Single.NaN || pressureFactor < 0.0f || pressureFactor > 1.0f))
            {
                throw new ArgumentOutOfRangeException("pressureFactor", SR.InvalidPressureValue);
            }
            //
            // only accept values between MaxXY and MinXY
            // we don't throw when passed a value outside of that range, we just silently trunctate
            //
            _x = GetClampedXYValue(x);
            _y = GetClampedXYValue(y);
            _stylusPointDescription = stylusPointDescription;
            _additionalValues = additionalValues;
            _pressureFactor = pressureFactor;
 
            if (validateAdditionalData)
            {
                //
                // called from the public verbose ctor
                //
                ArgumentNullException.ThrowIfNull(stylusPointDescription);
 
                //
                // additionalValues can be null if PropertyCount == 3 (X, Y, P)
                //
                if (stylusPointDescription.PropertyCount > StylusPointDescription.RequiredCountOfProperties)
                {
                    ArgumentNullException.ThrowIfNull(additionalValues);
                }
 
                if (additionalValues != null)
                {
                    ReadOnlyCollection<StylusPointPropertyInfo> properties
                        = stylusPointDescription.GetStylusPointProperties();
 
                    int expectedAdditionalValues = properties.Count - StylusPointDescription.RequiredCountOfProperties; //for x, y, pressure
                    if (additionalValues.Length != expectedAdditionalValues)
                    {
                        throw new ArgumentException(SR.InvalidAdditionalDataForStylusPoint, "additionalValues");
                    }
 
                    //
                    // any buttons passed in must each be in their own int.  We need to 
                    // pack them all into one int here
                    //
                    int[] newAdditionalValues =
                        new int[stylusPointDescription.GetExpectedAdditionalDataCount()];
 
                    _additionalValues = newAdditionalValues;
                    for (int i = StylusPointDescription.RequiredCountOfProperties, j = 0; i < properties.Count; i++, j++)
                    {
                        //
                        // use SetPropertyValue, it validates buttons, but does not copy the 
                        // int[] on writes (since we pass the bool flag)
                        //
                        SetPropertyValue(properties[i], additionalValues[j], false/*copy on write*/);
                    }
                }
            } 
        }
 
 
 
        #endregion Constructors
 
        /// <summary>
        /// The Maximum X or Y value supported for backwards compatibility with previous inking platforms
        /// </summary>
        public static readonly double MaxXY = 81164736.28346430d;
 
        /// <summary>
        /// The Minimum X or Y value supported for backwards compatibility with previous inking platforms
        /// </summary>
        public static readonly double MinXY = -81164736.32125960d;
 
        /// <summary>
        /// X
        /// </summary>
        public double X 
        {
            get { return _x; }
            set 
            {
                if (Double.IsNaN(value))
                {
                    throw new ArgumentOutOfRangeException("X", SR.InvalidStylusPointXYNaN);
                }
                //
                // only accept values between MaxXY and MinXY
                // we don't throw when passed a value outside of that range, we just silently trunctate
                //
                _x = GetClampedXYValue(value); 
            }
        }
 
        /// <summary>
        /// Y
        /// </summary>
        public double Y
        {
            get { return _y; }
            set 
            {
                if (Double.IsNaN(value))
                {
                    throw new ArgumentOutOfRangeException("Y", SR.InvalidStylusPointXYNaN);
                }
                //
                // only accept values between MaxXY and MinXY
                // we don't throw when passed a value outside of that range, we just silently trunctate
                //
                _y = GetClampedXYValue(value); 
            }
        }
 
        /// <summary>
        /// PressureFactor.  A value between 0.0 (no pressure) and 1.0 (max pressure)
        /// </summary>
        public float PressureFactor
        {
            get 
            {
                //
                // note that pressure can be stored a > 1 or < 0. 
                // we need to clamp if this is the case
                //
                if (_pressureFactor > 1.0f)
                {
                    return 1.0f;
                }
                if (_pressureFactor < 0.0f)
                {
                    return 0.0f;
                }
                return _pressureFactor;
            }
            set 
            {
                if (value < 0.0f || value > 1.0f)
                {
                    throw new ArgumentOutOfRangeException("PressureFactor", SR.InvalidPressureValue);
                }
                _pressureFactor = value; 
            }
        }
 
        /// <summary>
        /// Describes the properties this StylusPoint contains
        /// </summary>
        public StylusPointDescription Description 
        {
            get
            {
                if (null == _stylusPointDescription)
                {
                    // this can happen when you call new StylusPoint() 
                    // a few of the ctor's lazy init this as well
                    _stylusPointDescription = new StylusPointDescription();
                }
                return _stylusPointDescription;
            }
            internal set
            {
                //
                // called by StylusPointCollection.Add / Set
                // to replace the StylusPoint.Description with the collections.
                //
                Debug.Assert(value != null &&
                    StylusPointDescription.AreCompatible(value, this.Description));
 
                _stylusPointDescription = value;
            }
        }
 
        /// <summary>
        /// Returns true if this StylusPoint supports the specified property
        /// </summary>
        /// <param name="stylusPointProperty">The StylusPointProperty to see if this StylusPoint supports</param>
        public bool HasProperty(StylusPointProperty stylusPointProperty)
        {
            return this.Description.HasProperty(stylusPointProperty);
        }
 
        /// <summary>
        /// Provides read access to all stylus properties
        /// </summary>
        /// <param name="stylusPointProperty">The StylusPointPropertyIds of the property to retrieve</param>
        public int GetPropertyValue(StylusPointProperty stylusPointProperty)
        {
            ArgumentNullException.ThrowIfNull(stylusPointProperty);
            if (stylusPointProperty.Id == StylusPointPropertyIds.X)
            {
                return (int)_x;
            }
            else if (stylusPointProperty.Id == StylusPointPropertyIds.Y)
            {
                return (int)_y;
            }
            else if (stylusPointProperty.Id == StylusPointPropertyIds.NormalPressure)
            {
                StylusPointPropertyInfo info =
                    this.Description.GetPropertyInfo(StylusPointProperties.NormalPressure);
 
                int max = info.Maximum;
                return (int)(_pressureFactor * (float)max);
            }
            else
            {
                int propertyIndex = this.Description.GetPropertyIndex(stylusPointProperty.Id);
                if (-1 == propertyIndex)
                {
                    throw new ArgumentException(SR.InvalidStylusPointProperty, "stylusPointProperty");
                }
                if (stylusPointProperty.IsButton)
                {
                    //
                    // we get button data from a single int in the array
                    //
                    int buttonData = _additionalValues[_additionalValues.Length - 1];
                    int buttonBitPosition = this.Description.GetButtonBitPosition(stylusPointProperty);
                    int bit = 1 << buttonBitPosition;
                    if ((buttonData & bit) != 0)
                    {
                        return 1;
                    }
                    else
                    {
                        return 0;
                    }
                }
                else
                {
                    return _additionalValues[propertyIndex - 3];
                }
            }
        }
 
        /// <summary>
        /// Allows supported properties to be set
        /// </summary>
        /// <param name="stylusPointProperty">The property to set, it must exist on this StylusPoint</param>
        /// <param name="value">value</param>
        public void SetPropertyValue(StylusPointProperty stylusPointProperty, int value)
        {
            SetPropertyValue(stylusPointProperty, value, true);
        }
        /// <summary>
        /// Optimization that lets the ctor call setvalue repeatly without causing a copy of the int[]
        /// </summary>
        /// <param name="stylusPointProperty">stylusPointProperty</param>
        /// <param name="value">value</param>
        /// <param name="copyBeforeWrite"></param>
        internal void SetPropertyValue(StylusPointProperty stylusPointProperty, int value, bool copyBeforeWrite)
        {
            ArgumentNullException.ThrowIfNull(stylusPointProperty);
            if (stylusPointProperty.Id == StylusPointPropertyIds.X)
            {
                double dVal = (double)value;
                //
                // only accept values between MaxXY and MinXY
                // we don't throw when passed a value outside of that range, we just silently trunctate
                //
                _x = GetClampedXYValue(dVal); 
            }
            else if (stylusPointProperty.Id == StylusPointPropertyIds.Y)
            {
                double dVal = (double)value;
                //
                // only accept values between MaxXY and MinXY
                // we don't throw when passed a value outside of that range, we just silently trunctate
                //
                _y = GetClampedXYValue(dVal); 
            }
            else if (stylusPointProperty.Id == StylusPointPropertyIds.NormalPressure)
            {
                StylusPointPropertyInfo info =
                    this.Description.GetPropertyInfo(StylusPointProperties.NormalPressure);
 
                int min = info.Minimum;
                int max = info.Maximum;
                if (max == 0)
                {
                    _pressureFactor = 0.0f;
                }
                else
                {
                    _pressureFactor = (float)(Convert.ToSingle(min + value) / Convert.ToSingle(max));
                }
            }
            else
            {
                int propertyIndex = this.Description.GetPropertyIndex(stylusPointProperty.Id);
                if (-1 == propertyIndex)
                {
                    throw new ArgumentException(SR.InvalidStylusPointProperty, "propertyId");
                }
                if (stylusPointProperty.IsButton)
                {
                    if (value < 0 || value > 1)
                    {
                        throw new ArgumentOutOfRangeException("value", SR.InvalidMinMaxForButton);
                    }
 
                    if (copyBeforeWrite)
                    {
                        CopyAdditionalData();
                    }
 
                    //
                    // we get button data from a single int in the array
                    //
                    int buttonData = _additionalValues[_additionalValues.Length - 1];
                    int buttonBitPosition = this.Description.GetButtonBitPosition(stylusPointProperty);
                    int bit = 1 << buttonBitPosition;
                    if (value == 0)
                    {
                        //turn the bit off
                        buttonData &= ~bit;
                    }
                    else
                    {
                        //turn the bit on
                        buttonData |= bit;
                    }
                    _additionalValues[_additionalValues.Length - 1] = buttonData;
                }
                else
                {
                    if (copyBeforeWrite)
                    {
                        CopyAdditionalData();
                    }
                    _additionalValues[propertyIndex - 3] = value;
                }
            }
        }
 
        /// <summary>
        /// Explicit cast converter between StylusPoint and Point
        /// </summary>
        /// <param name="stylusPoint">stylusPoint</param>
        public static explicit operator Point(StylusPoint stylusPoint)
        {
            return new Point(stylusPoint.X, stylusPoint.Y);
        }
 
        /// <summary>
        /// Allows languages that don't support operator overloading
        /// to convert to a point
        /// </summary>
        public Point ToPoint()
        {
            return new Point(this.X, this.Y);
        }
 
 
        /// <summary>
        /// Compares two StylusPoint instances for exact equality.
        /// Note that double values can acquire error when operated upon, such that
        /// an exact comparison between two values which are logically equal may fail.
        /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
        /// Descriptions must match for equality to succeed and additional values must match
        /// </summary>
        /// <returns>
        /// bool - true if the two Stylus instances are exactly equal, false otherwise
        /// </returns>
        /// <param name='stylusPoint1'>The first StylusPoint to compare</param>
        /// <param name='stylusPoint2'>The second StylusPoint to compare</param>
        public static bool operator ==(StylusPoint stylusPoint1, StylusPoint stylusPoint2)
        {
            return StylusPoint.Equals(stylusPoint1, stylusPoint2);
        }
 
        /// <summary>
        /// Compares two StylusPoint instances for exact inequality.
        /// Note that double values can acquire error when operated upon, such that
        /// an exact comparison between two values which are logically equal may fail.
        /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
        /// </summary>
        /// <returns>
        /// bool - true if the two Stylus instances are exactly inequal, false otherwise
        /// </returns>
        /// <param name='stylusPoint1'>The first StylusPoint to compare</param>
        /// <param name='stylusPoint2'>The second StylusPoint to compare</param>
        public static bool operator !=(StylusPoint stylusPoint1, StylusPoint stylusPoint2)
        {
            return !StylusPoint.Equals(stylusPoint1, stylusPoint2);
        }
 
        /// <summary>
        /// Compares two StylusPoint instances for exact equality.
        /// Note that double values can acquire error when operated upon, such that
        /// an exact comparison between two values which are logically equal may fail.
        /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
        /// Descriptions must match for equality to succeed and additional values must match
        /// </summary>
        /// <returns>
        /// bool - true if the two Stylus instances are exactly equal, false otherwise
        /// </returns>
        /// <param name='stylusPoint1'>The first StylusPoint to compare</param>
        /// <param name='stylusPoint2'>The second StylusPoint to compare</param>
        public static bool Equals(StylusPoint stylusPoint1, StylusPoint stylusPoint2)
        {
            //
            // do the cheap comparison first
            //
            bool membersEqual =
                stylusPoint1._x == stylusPoint2._x &&
                stylusPoint1._y == stylusPoint2._y &&
                stylusPoint1._pressureFactor == stylusPoint2._pressureFactor;
 
            if (!membersEqual)
            {
                return false;
            }
 
            //
            // before we go checking the descriptions... check to see if both additionalData's are null
            // we can infer that the SPD's are just X,Y,P and that they are compatible.
            //
            if (stylusPoint1._additionalValues == null &&
                stylusPoint2._additionalValues == null)
            {
                Debug.Assert(StylusPointDescription.AreCompatible(stylusPoint1.Description, stylusPoint2.Description));
                return true;
            }
 
            //
            // ok, the members are equal.  compare the description and then additional data
            //
            if (object.ReferenceEquals(stylusPoint1.Description, stylusPoint2.Description) ||
                StylusPointDescription.AreCompatible(stylusPoint1.Description, stylusPoint2.Description))
            {
                //
                // descriptions match and there are equal numbers of additional values
                // let's check the values
                //
                for (int x = 0; x < stylusPoint1._additionalValues.Length; x++)
                {
                    if (stylusPoint1._additionalValues[x] != stylusPoint2._additionalValues[x])
                    {
                        return false;
                    }
                }
 
                //
                // Ok, ok already, we're equal
                //
                return true;
            }
 
            return false;
        }
 
        /// <summary>
        /// Compares two StylusPoint instances for exact equality.
        /// Note that double values can acquire error when operated upon, such that
        /// an exact comparison between two values which are logically equal may fail.
        /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
        /// Descriptions must match for equality to succeed and additional values must match
        /// </summary>
        /// <returns>
        /// bool - true if the object is an instance of StylusPoint and if it's equal to "this".
        /// </returns>
        /// <param name='o'>The object to compare to "this"</param>
        public override bool Equals(object o)
        {
            if ((null == o) || !(o is StylusPoint))
            {
                return false;
            }
 
            StylusPoint value = (StylusPoint)o;
            return StylusPoint.Equals(this, value);
        }
 
        /// <summary>
        /// Equals - compares this StylusPoint with the passed in object.  In this equality
        /// Double.NaN is equal to itself, unlike in numeric equality.
        /// Note that double values can acquire error when operated upon, such that
        /// an exact comparison between two values which
        /// are logically equal may fail.
        /// </summary>
        /// <returns>
        /// bool - true if "value" is equal to "this".
        /// </returns>
        /// <param name='value'>The StylusPoint to compare to "this"</param>
        public bool Equals(StylusPoint value)
        {
            return StylusPoint.Equals(this, value);
        }
        /// <summary>
        /// Returns the HashCode for this StylusPoint
        /// </summary>
        /// <returns>
        /// int - the HashCode for this StylusPoint
        /// </returns>
        public override int GetHashCode()
        {
            int hash = 
                _x.GetHashCode() ^
                _y.GetHashCode() ^
                _pressureFactor.GetHashCode();
 
            if (_stylusPointDescription != null)
            {
                hash ^= _stylusPointDescription.GetHashCode();
            }
 
            if (_additionalValues != null)
            {
                for (int x = 0; x < _additionalValues.Length; x++)
                {
                    hash ^= _additionalValues[x]; //don't call GetHashCode on integers, it just returns the int
                }
            }
 
            return hash;
        }
 
        /// <summary>
        /// Used by the StylusPointCollection.ToHimetricArray method
        /// </summary>
        /// <returns></returns>
        internal int[] GetAdditionalData()
        {
            //return a direct ref
            return _additionalValues;
        }
 
        /// <summary>
        /// Internal helper used by SPC.Reformat to preserve the pressureFactor
        /// </summary>
        internal float GetUntruncatedPressureFactor()
        {
            return _pressureFactor;
        }
 
        /// <summary>
        /// GetPacketData - returns avalon space packet data with true pressure if it exists
        /// </summary>
        internal int[] GetPacketData()
        {
            int count = 2; //x, y
            if (_additionalValues != null)
            {
                count += _additionalValues.Length;
            }
            if (this.Description.ContainsTruePressure)
            {
                count++;
            }
            int[] data = new int[count];
            data[0] = (int)_x;
            data[1] = (int)_y;
            int startIndex = 2;
            if (this.Description.ContainsTruePressure)
            {
                startIndex = 3;
                data[2] = GetPropertyValue(StylusPointProperties.NormalPressure);
            }
            if (_additionalValues != null)
            {
                for (int x = 0; x < _additionalValues.Length; x++)
                {
                    data[x + startIndex] = _additionalValues[x];
                }
            }
            return data;
        }
 
        /// <summary>
        /// Internal helper to determine if a stroke has default pressure
        /// This is used by ISF serialization to not serialize pressure
        /// </summary>
        internal bool HasDefaultPressure
        {
            get
            {
                return (_pressureFactor == DefaultPressure);
            }
        }
 
        /// <summary>
        /// Used by the SetPropertyData to make a copy of the data
        /// before modifying it.  This is required so that we don't 
        /// have two StylusPoint's sharing the same int[]
        /// which can happen when you call: StylusPoint p = otherStylusPoint
        /// because the CLR just does a memberwise copy
        /// </summary>
        /// <returns></returns>        
        private void CopyAdditionalData()
        {
            _additionalValues = (int[])_additionalValues?.Clone();
        }
 
        /// <summary>
        /// Private helper that returns a double clamped to MaxXY or MinXY
        /// We only accept values in this range to support ISF serialization
        /// </summary>
        private static double GetClampedXYValue(double xyValue)
        {
            if (xyValue > MaxXY)
            {
                return MaxXY;
            }
            if (xyValue < MinXY)
            {
                return MinXY;
            }
 
            return xyValue;
        }
    }
}