File: System\Windows\Ink\IncrementalHitTester.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.Windows.Input;
using System.Windows.Ink;
using System.Windows.Media;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using MS.Internal.Ink;
using MS.Utility;
using MS.Internal;
using System.Diagnostics;
 
using SR=MS.Internal.PresentationCore.SR;
 
namespace System.Windows.Ink
{
    #region IncrementalHitTester Abstract Base Class
 
    /// <summary>
    /// This class serves as both the base class and public interface for
    /// incremental hit-testing implementaions.
    /// </summary>
    public abstract class IncrementalHitTester
    {
        #region Public API
 
        /// <summary>
        /// Adds a point representing an incremental move of the hit-testing tool
        /// </summary>
        /// <param name="point">a point that represents an incremental move of the hitting tool</param>
        public void AddPoint(Point point)
        {
            AddPoints(new Point[1] { point });
        }
 
        /// <summary>
        /// Adds an array of points representing an incremental move of the hit-testing tool
        /// </summary>
        /// <param name="points">points representing an incremental move of the hitting tool</param>
        public void AddPoints(IEnumerable<Point> points)
        {
            ArgumentNullException.ThrowIfNull(points);
 
            if (IEnumerablePointHelper.GetCount(points) == 0)
            {
                throw new System.ArgumentException(SR.EmptyArrayNotAllowedAsArgument, "points");
            }
 
            if (false == _fValid)
            {
                throw new System.InvalidOperationException(SR.EndHitTestingCalled);
            }
 
            System.Diagnostics.Debug.Assert(_strokes != null);
 
            AddPointsCore(points);
        }
 
        /// <summary>
        /// Adds a StylusPacket representing an incremental move of the hit-testing tool
        /// </summary>
        /// <param name="stylusPoints">stylusPoints</param>
        public void AddPoints(StylusPointCollection stylusPoints)
        {
            ArgumentNullException.ThrowIfNull(stylusPoints);
 
            if (stylusPoints.Count == 0)
            {
                throw new System.ArgumentException(SR.EmptyArrayNotAllowedAsArgument, "stylusPoints");
            }
 
            if (false == _fValid)
            {
                throw new System.InvalidOperationException(SR.EndHitTestingCalled);
            }
 
            System.Diagnostics.Debug.Assert(_strokes != null);
 
            Point[] points = new Point[stylusPoints.Count];
            for (int x = 0; x < stylusPoints.Count; x++)
            {
                points[x] = (Point)stylusPoints[x];
            }
 
            AddPointsCore(points);
        }
 
 
        /// <summary>
        /// Release as many resources as possible for this enumerator
        /// </summary>
        public void EndHitTesting()
        {
            if (_strokes != null)
            {
                // Detach the event handler
                _strokes.StrokesChangedInternal -= new StrokeCollectionChangedEventHandler(OnStrokesChanged);
                _strokes = null;
                int count = _strokeInfos.Count;
                for ( int i = 0; i < count; i++)
                {
                    _strokeInfos[i].Detach();
                }
                _strokeInfos = null;
            }
            _fValid = false;
        }
 
        /// <summary>
        /// Accessor to see if the Hit Tester is still valid
        /// </summary>
        public bool IsValid { get { return _fValid; } }
        #endregion
 
        #region Internal
 
        /// <summary>
        /// C-tor.
        /// </summary>
        /// <param name="strokes">strokes to hit-test</param>
        internal IncrementalHitTester(StrokeCollection strokes)
        {
            System.Diagnostics.Debug.Assert(strokes != null);
 
            // Create a StrokeInfo object for each stroke.
            _strokeInfos = new List<StrokeInfo>(strokes.Count);
            for (int x = 0; x < strokes.Count; x++)
            {
                Stroke stroke = strokes[x];
                _strokeInfos.Add(new StrokeInfo(stroke));
            }
 
            _strokes = strokes;
 
            // Attach an event handler to the strokes' changed event
            _strokes.StrokesChangedInternal += new StrokeCollectionChangedEventHandler(OnStrokesChanged);
        }
 
        /// <summary>
        /// The implementation behind AddPoint/AddPoints.
        /// Derived classes are supposed to override this method.
        /// </summary>
        protected abstract void AddPointsCore(IEnumerable<Point> points);
 
 
        /// <summary>
        /// Accessor to the internal collection of StrokeInfo objects
        /// </summary>
        internal List<StrokeInfo> StrokeInfos { get { return _strokeInfos; } }
 
        #endregion
 
        #region Private
 
        /// <summary>
        /// Event handler associated with the stroke collection.
        /// </summary>
        /// <param name="sender">Stroke collection that was modified</param>
        /// <param name="args">Modification that occurred</param>
        /// <remarks>
        /// Update our _strokeInfos cache.  We get notified on StrokeCollection.StrokesChangedInternal which
        /// is raised first so we can assume we're the first delegate in the call chain
        /// </remarks>
        private void OnStrokesChanged(object sender, StrokeCollectionChangedEventArgs args)
        {
            System.Diagnostics.Debug.Assert((_strokes != null) && (_strokeInfos != null) && (_strokes == sender));
 
            StrokeCollection added = args.Added;
            StrokeCollection removed = args.Removed;
 
            if (added.Count > 0)
            {
                int firstIndex = _strokes.IndexOf(added[0]);
                for (int i = 0; i < added.Count; i++)
                {
                    _strokeInfos.Insert(firstIndex, new StrokeInfo(added[i]));
                    firstIndex++;
                }
            }
 
            if (removed.Count > 0)
            {
                StrokeCollection localRemoved = new StrokeCollection(removed);
                //we have to assume that removed strokes can be in any order in _strokes
                for (int i = 0; i < _strokeInfos.Count && localRemoved.Count > 0; )
                {
                    bool found = false;
                    for (int j = 0; j < localRemoved.Count; j++)
                    {
                        if (localRemoved[j] == _strokeInfos[i].Stroke)
                        {
                            _strokeInfos.RemoveAt(i);
                            localRemoved.RemoveAt(j);
 
                            found = true;
                        }
                    }
                    //we didn't find a removed stroke at index i in _strokeInfos, so advance i
                    if (!found)
                    {
                        i++;
                    }
                }
            }
 
            //validate our cache
            if (_strokes.Count != _strokeInfos.Count)
            {
                Debug.Assert(false, "Benign assert.  IncrementalHitTester's _strokeInfos cache is out of sync, rebuilding.");
                RebuildStrokeInfoCache();
                return;
            }
            for (int i = 0; i < _strokeInfos.Count; i++)
            {
                if (_strokeInfos[i].Stroke != _strokes[i])
                {
                    Debug.Assert(false, "Benign assert.  IncrementalHitTester's _strokeInfos cache is out of sync, rebuilding.");
                    RebuildStrokeInfoCache();
                    return;
                }
            }
        }
 
        /// <summary>
        /// IHT's can get into a state where their StrokeInfo cache is too 
        /// out of sync with the StrokeCollection to incrementally update it.
        /// When we detect this has happened, we just rebuild the entire cache.
        /// </summary>
        private void RebuildStrokeInfoCache()
        {
            List<StrokeInfo> newStrokeInfos = new List<StrokeInfo>(_strokes.Count);
            foreach (Stroke stroke in _strokes)
            {
                bool found = false;
                for (int x = 0; x < _strokeInfos.Count; x++)
                {
                    StrokeInfo strokeInfo = _strokeInfos[x];
                    if (strokeInfo != null && stroke == strokeInfo.Stroke)
                    {
                        newStrokeInfos.Add(strokeInfo);
                        //just set to null instead of removing and shifting
                        //we're about to GC _strokeInfos
                        _strokeInfos[x] = null;
                        found = true;
                        break;
                    }
                }
                if (!found)
                {
                    //we didn't find an existing strokeInfo
                    newStrokeInfos.Add(new StrokeInfo(stroke));
                }
            }
 
            //detach the remaining strokeInfo's from their strokes
            for (int x = 0; x < _strokeInfos.Count; x++)
            {
                StrokeInfo strokeInfo = _strokeInfos[x];
            
                if (strokeInfo != null)
                {
                    strokeInfo.Detach();
                }
            }
 
            _strokeInfos = newStrokeInfos;
 
#if DEBUG
            Debug.Assert(_strokeInfos.Count == _strokes.Count);
            for (int x = 0; x < _strokeInfos.Count; x++)
            {
                Debug.Assert(_strokeInfos[x].Stroke == _strokes[x]);
            }
#endif
        }
 
        #endregion
 
        #region Fields
 
        /// <summary> Reference to the stroke collection under test </summary>
        private StrokeCollection _strokes;
        /// <summary> A collection of helper objects mapped to the stroke colection</summary>
        private List<StrokeInfo> _strokeInfos;
 
        private bool _fValid = true;
 
        #endregion
    }
 
    #endregion
 
    #region IncrementalLassoHitTester
 
    /// <summary>
    /// IncrementalHitTester implementation for hit-testing with lasso
    /// </summary>
    public class IncrementalLassoHitTester : IncrementalHitTester
    {
        #region public APIs
        /// <summary>
        /// Event
        /// </summary>
        public event LassoSelectionChangedEventHandler SelectionChanged;
        #endregion
        #region C-tor and the overrides
 
        /// <summary>
        /// C-tor.
        /// </summary>
        /// <param name="strokes">strokes to hit-test</param>
        /// <param name="percentageWithinLasso">a hit-testing parameter that defines the minimal
        /// percent of nodes of a stroke to be inside the lasso to consider the stroke hit</param>
        internal IncrementalLassoHitTester(StrokeCollection strokes, int percentageWithinLasso)
            : base(strokes)
        {
            System.Diagnostics.Debug.Assert((percentageWithinLasso >= 0) && (percentageWithinLasso <= 100));
            _lasso = new SingleLoopLasso();
            _percentIntersect = percentageWithinLasso;
        }
 
        /// <summary>
        /// The implementation behind the public methods AddPoint/AddPoints
        /// </summary>
        /// <param name="points">new points to add to the lasso</param>
        protected override void AddPointsCore(IEnumerable<Point> points)
        {
            System.Diagnostics.Debug.Assert((points != null) && (IEnumerablePointHelper.GetCount(points)!= 0));
 
            // Add the new points to the lasso
            int lastPointIndex = (0 != _lasso.PointCount) ? (_lasso.PointCount - 1) : 0;
            _lasso.AddPoints(points);
 
            // Do nothing if there's not enough points, or there's nobody listening
            // The points may be filtered out, so if all the points are filtered out, (lastPointIndex == (_lasso.PointCount - 1).
            // For this case, check if the incremental lasso is disabled (i.e., points modified).
            if ((_lasso.IsEmpty) || (lastPointIndex == (_lasso.PointCount - 1) && false == _lasso.IsIncrementalLassoDirty)
                || (SelectionChanged == null))
            {
                return;
            }
 
            // Variables for possible HitChanged events to fire
            StrokeCollection strokesHit = null;
            StrokeCollection strokesUnhit = null;
 
            // Create a lasso that represents the current increment
            Lasso lassoUpdate = new Lasso();
 
            if (false == _lasso.IsIncrementalLassoDirty)
            {
                if (0 < lastPointIndex)
                {
                    lassoUpdate.AddPoint(_lasso[0]);
                }
 
                // Only the points the have been successfully added to _lasso will be added to
                // lassoUpdate.
                for (; lastPointIndex < _lasso.PointCount; lastPointIndex++)
                {
                    lassoUpdate.AddPoint(_lasso[lastPointIndex]);
                }
            }
 
            // Enumerate through the strokes and update their hit-test results
            foreach (StrokeInfo strokeInfo in this.StrokeInfos)
            {
                Lasso lasso;
                if (true == strokeInfo.IsDirty || true == _lasso.IsIncrementalLassoDirty)
                {
                    // If this is the first time this stroke gets hit-tested with this lasso,
                    // or if the stroke (or its DAs) has changed since the last hit-testing,
                    // or if the lasso points have been modified,
                    // then (re)hit-test this stroke against the entire lasso.
                    lasso = _lasso;
                    strokeInfo.IsDirty = false;
                }
                else
                {
                    // Otherwise, hit-test it against the lasso increment first and then only
                    // those ink points that are in that small lasso need to be hit-tested
                    // against the big (entire) lasso.
                    // This is supposed to be a significant piece of optimization, since
                    // lasso increments are usually very small, they are defined by just
                    // a few points and they don't capture and/or release too many ink nodes.
                    lasso = lassoUpdate;
                }
 
                // Skip those stroke which bounding box doesn't even intersects with the lasso bounds
                double hitWeightChange = 0f;
                if (lasso.Bounds.IntersectsWith(strokeInfo.StrokeBounds))
                {
                    // Get the stroke node points for the hit-testing.
                    StylusPointCollection stylusPoints = strokeInfo.StylusPoints;
 
                    // Find out if the lasso update has changed the hit count of the stroke.
                    for (int i = 0; i < stylusPoints.Count; i++)
                    {
                        // Consider only the points that become captured/released with this particular update
                        if (true == lasso.Contains((Point)stylusPoints[i]))
                        {
                            double weight = strokeInfo.GetPointWeight(i);
 
                            if (lasso == _lasso || _lasso.Contains((Point)stylusPoints[i]))
                            {
                                hitWeightChange += weight;
                            }
                            else
                            {
                                hitWeightChange -= weight;
                            }
                        }
                    }
                }
 
                // Update the stroke hit weight and check whether it has crossed the margin
                // in either direction since the last update.
                if ((hitWeightChange != 0) || (lasso == _lasso))
                {
                    strokeInfo.HitWeight = (lasso == _lasso) ? hitWeightChange : (strokeInfo.HitWeight + hitWeightChange);
                    bool isHit = DoubleUtil.GreaterThanOrClose(strokeInfo.HitWeight, strokeInfo.TotalWeight * _percentIntersect / 100f - Stroke.PercentageTolerance);
 
                    if (strokeInfo.IsHit != isHit)
                    {
                        strokeInfo.IsHit = isHit;
                        if (isHit)
                        {
                            // The hit count became greater than the margin percentage, the stroke
                            // needs to be reported for selection
                            if (null == strokesHit)
                            {
                                strokesHit = new StrokeCollection();
                            }
                            strokesHit.Add(strokeInfo.Stroke);
                        }
                        else
                        {
                            // The hit count just became less than the margin percentage,
                            // the stroke needs to be reported for de-selection
                            if (null == strokesUnhit)
                            {
                                strokesUnhit = new StrokeCollection();
                            }
                            strokesUnhit.Add(strokeInfo.Stroke);
                        }
                    }
                }
            }
 
            _lasso.IsIncrementalLassoDirty = false;
            // Raise StrokesHitChanged event if any strokes has changed thier
            // hit status and there're the event subscribers.
            if ((null != strokesHit) || (null != strokesUnhit))
            {
                OnSelectionChanged(new LassoSelectionChangedEventArgs (strokesHit, strokesUnhit));
            }
        }
 
        /// <summary>
        /// SelectionChanged event raiser
        /// </summary>
        /// <param name="eventArgs"></param>
        protected void OnSelectionChanged(LassoSelectionChangedEventArgs  eventArgs)
        {
            System.Diagnostics.Debug.Assert(eventArgs != null);
            if (SelectionChanged != null)
            {
                SelectionChanged(this, eventArgs);
            }
        }
 
 
        #endregion
        #region Fields
 
        private Lasso   _lasso;
        private int     _percentIntersect;
 
        #endregion
    }
 
    #endregion
 
    #region IncrementalStrokeHitTester
 
    /// <summary>
    /// IncrementalHitTester implementation for hit-testing with a shape, PointErasing .
    /// </summary>
    public class IncrementalStrokeHitTester  : IncrementalHitTester
    {
        /// <summary>
        ///
        /// </summary>
        public event StrokeHitEventHandler StrokeHit;
 
        #region C-tor and the overrides
 
        /// <summary>
        /// C-tor
        /// </summary>
        /// <param name="strokes">strokes to hit-test for erasing</param>
        /// <param name="eraserShape">erasing shape</param>
        internal IncrementalStrokeHitTester(StrokeCollection strokes, StylusShape eraserShape)
            : base(strokes)
        {
            System.Diagnostics.Debug.Assert(eraserShape != null);
 
            // Create an ErasingStroke objects that implements the actual hit-testing
            _erasingStroke = new ErasingStroke(eraserShape);
        }
 
        /// <summary>
        /// The implementation behind the public methods AddPoint/AddPoints
        /// </summary>
        /// <param name="points">a set of points representing the last increment
        /// in the moving of the erasing shape</param>
        protected override void AddPointsCore(IEnumerable<Point> points)
        {
            System.Diagnostics.Debug.Assert((points != null) && (IEnumerablePointHelper.GetCount(points) != 0));
            System.Diagnostics.Debug.Assert(_erasingStroke != null);
 
            // Move the shape through the new points and build the contour of the move.
            _erasingStroke.MoveTo(points);
            Rect erasingBounds = _erasingStroke.Bounds;
            if (erasingBounds.IsEmpty)
            {
                return;
            }
 
            List<StrokeHitEventArgs> strokeHitEventArgCollection = null;
            // Do nothing if there's nobody listening to the events
            if (StrokeHit != null)
            {
                List<StrokeIntersection> eraseAt = new List<StrokeIntersection>();
 
                // Test stroke by stroke and collect the results.
                for (int x = 0; x < this.StrokeInfos.Count; x++)
                {
                    StrokeInfo strokeInfo = this.StrokeInfos[x];
 
                    // Skip the stroke if its bounding box doesn't intersect with the one of the hitting shape.
                    if ((erasingBounds.IntersectsWith(strokeInfo.StrokeBounds) == false) ||
                        (_erasingStroke.EraseTest(StrokeNodeIterator.GetIterator(strokeInfo.Stroke, strokeInfo.Stroke.DrawingAttributes), eraseAt) == false))
                    {
                        continue;
                    }
 
                    // Create an event args to raise after done with hit-testing
                    // We don't fire these events right away because user is expected to
                    // modify the stroke collection in her event handler, and that would
                    // invalidate this foreach loop.
                    if (strokeHitEventArgCollection == null)
                    {
                        strokeHitEventArgCollection = new List<StrokeHitEventArgs>();
                    }
                    strokeHitEventArgCollection.Add(new StrokeHitEventArgs(strokeInfo.Stroke, eraseAt.ToArray()));
                    // We must clear eraseAt or it will contain invalid results for the next strokes
                    eraseAt.Clear();
                }
            }
 
            // Raise StrokeHit event if needed.
            if (strokeHitEventArgCollection != null)
            {
                System.Diagnostics.Debug.Assert(strokeHitEventArgCollection.Count != 0);
                for (int x = 0; x < strokeHitEventArgCollection.Count; x++)
                {
                    StrokeHitEventArgs eventArgs = strokeHitEventArgCollection[x];
 
                    System.Diagnostics.Debug.Assert(eventArgs.HitStroke != null);
                    OnStrokeHit(eventArgs);
                }
            }
        }
 
        /// <summary>
        /// Event raiser for StrokeHit
        /// </summary>
        protected void OnStrokeHit(StrokeHitEventArgs eventArgs)
        {
            System.Diagnostics.Debug.Assert(eventArgs != null);
            if (StrokeHit != null)
            {
                StrokeHit(this, eventArgs);
            }
        }
 
        #endregion
 
        #region Fields
 
        private ErasingStroke _erasingStroke;
 
        #endregion
    }
 
 
    #endregion
 
    #region EventArgs and delegates
 
    /// <summary>
    /// Declaration for LassoSelectionChanged event handler. Used in lasso-selection
    /// </summary>
    public delegate void LassoSelectionChangedEventHandler(object sender, LassoSelectionChangedEventArgs e);
 
 
    /// <summary>
    /// Declaration for StrokeHit event handler. Used in point-erasing
    /// </summary>
    public delegate void StrokeHitEventHandler(object sender, StrokeHitEventArgs e);
 
 
    /// <summary>
    /// Event arguments for LassoSelectionChanged event
    /// </summary>
    public class LassoSelectionChangedEventArgs  : EventArgs
    {
        internal LassoSelectionChangedEventArgs(StrokeCollection selectedStrokes, StrokeCollection deselectedStrokes)
        {
            _selectedStrokes = selectedStrokes;
            _deselectedStrokes = deselectedStrokes;
        }
 
        /// <summary>
        /// Collection of strokes which were hit with the last increment
        /// </summary>
        public StrokeCollection SelectedStrokes
        {
            get
            {
                if (_selectedStrokes != null)
                {
                    StrokeCollection sc = new StrokeCollection();
                    sc.Add(_selectedStrokes);
                    return sc;
                }
                else
                {
                    return  new StrokeCollection();
                }
            }
        }
 
        /// <summary>
        /// Collection of strokes which were unhit with the last increment
        /// </summary>
        public StrokeCollection DeselectedStrokes
        {
            get
            {
                if (_deselectedStrokes != null)
                {
                    StrokeCollection sc = new StrokeCollection();
                    sc.Add(_deselectedStrokes);
                    return sc;
                }
                else
                {
                    return new StrokeCollection();
                }
            }
        }
 
        private StrokeCollection _selectedStrokes;
        private StrokeCollection _deselectedStrokes;
    }
 
    /// <summary>
    /// Event arguments for StrokeHit event
    /// </summary>
    public class StrokeHitEventArgs : EventArgs
    {
        /// <summary>
        /// C-tor
        /// </summary>
        internal StrokeHitEventArgs(Stroke stroke, StrokeIntersection[] hitFragments)
        {
            System.Diagnostics.Debug.Assert(stroke != null && hitFragments != null && hitFragments.Length > 0);
            _stroke = stroke;
            _hitFragments = hitFragments;
        }
 
        /// <summary>Stroke that was hit</summary>
        public Stroke HitStroke { get { return _stroke; } }
 
        /// <summary>
        ///
        /// </summary>
        /// <returns></returns>
        public StrokeCollection GetPointEraseResults()
        {
            return _stroke.Erase(_hitFragments);
        }
 
        private Stroke                  _stroke;
        private StrokeIntersection[]    _hitFragments;
}
 
    #endregion
}
 
namespace MS.Internal.Ink
{
    #region StrokeInfo
 
    /// <summary>
    /// A helper class associated with a stroke. Used for caching the stroke's
    /// bounding box, hit-testing results, and for keeping an eye on the stroke changes
    /// </summary>
    internal class StrokeInfo
    {
        #region API (used by incremental hit-testers)
 
        /// <summary>
        /// StrokeInfo
        /// </summary>
        internal StrokeInfo(Stroke stroke)
        {
            System.Diagnostics.Debug.Assert(stroke != null);
            _stroke = stroke;
            _bounds = stroke.GetBounds();
 
            // Start listening to the stroke events
            _stroke.DrawingAttributesChanged += new PropertyDataChangedEventHandler(OnStrokeDrawingAttributesChanged);
            _stroke.StylusPointsReplaced += new StylusPointsReplacedEventHandler(OnStylusPointsReplaced);
            _stroke.StylusPoints.Changed += new EventHandler(OnStylusPointsChanged);
            _stroke.DrawingAttributesReplaced += new DrawingAttributesReplacedEventHandler(OnDrawingAttributesReplaced);
        }
 
        /// <summary>The stroke object associated with this helper structure</summary>
        internal Stroke Stroke { get { return _stroke; } }
 
        /// <summary>Pre-calculated bounds of the stroke </summary>
        internal Rect StrokeBounds { get { return _bounds; } }
 
        /// <summary>Tells whether the stroke or its drawing attributes have been modified
        /// since the last use (hit-testing)</summary>
        internal bool IsDirty
        {
            get { return _isDirty; }
            set { _isDirty = value; }
        }
 
        /// <summary>Tells whether the stroke was found (and reported) as hit </summary>
        internal bool IsHit
        {
            get { return _isHit; }
            set { _isHit = value; }
        }
 
        /// <summary>
        /// Cache teh stroke points
        /// </summary>
        internal StylusPointCollection StylusPoints
        {
            get
            {
                if (_stylusPoints == null)
                {
                    if (_stroke.DrawingAttributes.FitToCurve)
                    {
                        _stylusPoints = _stroke.GetBezierStylusPoints();
                    }
                    else
                    {
                        _stylusPoints = _stroke.StylusPoints;
                    }
                }
                return _stylusPoints;
            }
        }
 
        /// <summary>
        /// Holds the current hit-testing result for the stroke. Represents the length of
        /// the stroke "inside" and "hit" by the lasso
        /// </summary>
        internal double HitWeight
        {
            get { return _hitWeight; }
            set
            {
                // it is ok to clamp this off, rounding error sends it over or under by a minimal amount.
                if (DoubleUtil.GreaterThan(value, TotalWeight))
                {
                    _hitWeight = TotalWeight;
                }
                else if (DoubleUtil.LessThan(value, 0f))
                {
                    _hitWeight = 0f;
                }
                else
                {
                    _hitWeight = value;
                }
            }
        }
 
        /// <summary>
        /// Get the total weight of the stroke. For this implementation, it is the total length of the stroke.
        /// </summary>
        /// <returns></returns>
        internal double TotalWeight
        {
            get
            {
                if (!_totalWeightCached)
                {
                    _totalWeight= 0;
                    for (int i = 0; i < StylusPoints.Count; i++)
                    {
                        _totalWeight += this.GetPointWeight(i);
                    }
                    _totalWeightCached = true;
                }
                return _totalWeight;
            }
        }
 
        /// <summary>
        /// Calculate the weight of a point.
        /// </summary>
        internal double GetPointWeight(int index)
        {
            StylusPointCollection stylusPoints = this.StylusPoints;
            DrawingAttributes da = this.Stroke.DrawingAttributes;
            System.Diagnostics.Debug.Assert(stylusPoints != null && index >= 0 && index < stylusPoints.Count);
 
            double weight = 0f;
            if (index == 0)
            {
                weight += Math.Sqrt(da.Width*da.Width + da.Height*da.Height) / 2.0f;
            }
            else
            {
                Vector spine = (Point)stylusPoints[index] - (Point)stylusPoints[index - 1];
                weight += Math.Sqrt(spine.LengthSquared) / 2.0f;
            }
 
            if (index == stylusPoints.Count - 1)
            {
                weight += Math.Sqrt(da.Width*da.Width + da.Height*da.Height) / 2.0f;
            }
            else
            {
                Vector spine = (Point)stylusPoints[index + 1] - (Point)stylusPoints[index];
                weight += Math.Sqrt(spine.LengthSquared) / 2.0f;
            }
 
            return weight;
        }
        /// <summary>
        /// A kind of disposing method
        /// </summary>
        internal void Detach()
        {
            if (_stroke != null)
            {
                // Detach the event handlers
                _stroke.DrawingAttributesChanged -= new PropertyDataChangedEventHandler(OnStrokeDrawingAttributesChanged);
                _stroke.StylusPointsReplaced -= new StylusPointsReplacedEventHandler(OnStylusPointsReplaced);
                _stroke.StylusPoints.Changed -= new EventHandler(OnStylusPointsChanged);
                _stroke.DrawingAttributesReplaced -= new DrawingAttributesReplacedEventHandler(OnDrawingAttributesReplaced);
 
                _stroke = null;
            }
        }
 
        #endregion
 
        #region Stroke event handlers (Private)
 
        /// <summary>Event handler for stroke data changed events</summary>
        private void OnStylusPointsChanged(object sender, EventArgs args)
        {
            Invalidate();
        }
 
        /// <summary>Event handler for stroke data changed events</summary>
        private void OnStylusPointsReplaced(object sender, StylusPointsReplacedEventArgs args)
        {
            Invalidate();
        }
 
        /// <summary>
        /// Event handler for stroke's drawing attributes changes.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnStrokeDrawingAttributesChanged(object sender, PropertyDataChangedEventArgs args)
        {
            // Only enforce rehittesting of the whole stroke when the DrawingAttribute change may affect hittesting
            if(DrawingAttributes.IsGeometricalDaGuid(args.PropertyGuid))
            {
                Invalidate();
            }
        }
 
        private void OnDrawingAttributesReplaced(Object sender, DrawingAttributesReplacedEventArgs args)
        {
            // If the drawing attributes change involves Width, Height, StylusTipTransform, IgnorePressure, or FitToCurve,
            // we need to invalidate
            if (false == DrawingAttributes.GeometricallyEqual(args.NewDrawingAttributes, args.PreviousDrawingAttributes))
            {
                Invalidate();
            }
        }
 
        /// <summary>Implementation for the event handlers above</summary>
        private void Invalidate()
        {
            _totalWeightCached = false;
            _stylusPoints = null;
            _hitWeight = 0;
 
            // Let the hit-tester know that it should not use incremental hit-testing
            _isDirty = true;
 
            // The Stroke.GetBounds may be overriden in the 3rd party code.
            // The out-side code could throw exception. If an exception is thrown, _bounds will keep the original value.
            // Re-compute the stroke bounds
            _bounds = _stroke.GetBounds();
        }
        #endregion
 
        #region Fields
 
        private Stroke                      _stroke;
        private Rect                        _bounds;
        private double                      _hitWeight = 0f;
        private bool                        _isHit = false;
        private bool                        _isDirty = true;
        private StylusPointCollection       _stylusPoints;   // Cache the stroke rendering points
        private double                      _totalWeight = 0f;
        private bool                        _totalWeightCached = false;
        #endregion
    }
 
    #endregion // StrokeInfo
}
 
// The following code is for Stroke-Erasing scenario. Currently the IncrementalStrokeHitTester
// can be used for Stroke-erasing but the following class is faster. If in the future there's a
// perf issue with Stroke-Erasing, consider adding the following code.
//#region Commented Code for IncrementalStrokeHitTester
//#region IncrementalStrokeHitTester
 
///// <summary>
///// IncrementalHitTester implementation for hit-testing with a shape, StrokeErasing .
///// </summary>
//public class IncrementalStrokeHitTester : IncrementalHitTester
//{
//    /// <summary>
//    /// event
//    /// </summary>
//    public event StrokesHitEventHandler StrokesHit;
 
//    #region C-tor and the overrides
 
//    /// <summary>
//    /// C-tor
//    /// </summary>
//    /// <param name="strokes">strokes to hit-test for erasing</param>
//    /// <param name="eraserShape">erasing shape</param>
//    internal IncrementalStrokeHitTester(StrokeCollection strokes, StylusShape eraserShape)
//        : base(strokes)
//    {
//        System.Diagnostics.Debug.Assert(eraserShape != null);
 
//        // Create an ErasingStroke objects that implements the actual hit-testing
//        _erasingStroke = new ErasingStroke(eraserShape);
//    }
 
//    /// <summary>
//    ///
//    /// </summary>
//    /// <param name="eventArgs"></param>
//    internal protected void OnStrokesHit(StrokesHitEventArgs eventArgs)
//    {
//        if (StrokesHit != null)
//        {
//            StrokesHit(this, eventArgs);
//        }
//    }
 
//    /// <summary>
//    /// The implementation behind the public methods AddPoint/AddPoints
//    /// </summary>
//    /// <param name="points">a set of points representing the last increment
//    /// in the moving of the erasing shape</param>
//    internal protected override void AddPointsCore(Point[] points)
//    {
//        System.Diagnostics.Debug.Assert((points != null) && (points.Length != 0));
//        System.Diagnostics.Debug.Assert(_erasingStroke != null);
 
//        // Move the shape through the new points and build the contour of the move.
//        _erasingStroke.MoveTo(points);
//        Rect erasingBounds = _erasingStroke.Bounds;
//        if (erasingBounds.IsEmpty)
//        {
//            return;
//        }
 
//        StrokeCollection strokesHit = null;
//        if (StrokesHit != null)
//        {
//            // Test stroke by stroke and collect hits.
//            foreach (StrokeInfo strokeInfo in StrokeInfos)
//            {
//                // Skip strokes that have already been reported hit or which bounds
//                // don't intersect with the bounds of the erasing stroke.
//                if ((strokeInfo.IsHit == false) && erasingBounds.IntersectsWith(strokeInfo.StrokeBounds)
//                    && _erasingStroke.HitTest(StrokeNodeIterator.GetIterator(strokeInfo.Stroke, strokeInfo.Overrides)))
//                {
//                    if (strokesHit == null)
//                    {
//                        strokesHit = new StrokeCollection();
//                    }
//                    strokesHit.Add(strokeInfo.Stroke);
//                    strokeInfo.IsHit = true;
//                }
//            }
//        }
 
//        // Raise StrokesHitChanged event if any strokes have been hit and there're listeners to the event.
//        if (strokesHit != null)
//        {
//            System.Diagnostics.Debug.Assert(strokesHit.Count != 0);
//            OnStrokesHit(new StrokesHitEventArgs(strokesHit));
//        }
//    }
 
//    #endregion
 
//    #region Fields
 
//    private ErasingStroke _erasingStroke;
 
//    #endregion
//}
 
//#endregion
 
///// <summary>
///// Declaration for StrokesHit event handler. Used in stroke-erasing
///// </summary>
//public delegate void StrokesHitEventHandler(object sender, StrokesHitEventArgs e);
 
///// <summary>
///// Event arguments for StrokesHit event
///// </summary>
//public class StrokesHitEventArgs : EventArgs
//{
//    internal StrokesHitEventArgs(StrokeCollection hitStrokes)
//    {
//        System.Diagnostics.Debug.Assert(hitStrokes != null && hitStrokes.Count > 0);
//        _hitStrokes = hitStrokes;
//    }
 
//    /// <summary>
//    ///
//    /// </summary>
//    public StrokeCollection HitStrokes
//    {
//        get { return _hitStrokes; }
//    }
 
//    private StrokeCollection _hitStrokes;
//}
 
//#endregion