/// <summary> /// Merge two crossings into one. /// </summary> /// <param name="crossing"></param> /// <returns>Return true if these two crossings are actually overlapping and merged; false otherwise</returns> public bool Merge(LassoCrossing crossing) { if (crossing.IsEmpty) { return(false); } if (FIndices.IsEmpty && !crossing.IsEmpty) { FIndices = crossing.FIndices; StartNode = crossing.StartNode; EndNode = crossing.EndNode; return(true); } if (DoubleUtil.GreaterThanOrClose(crossing.FIndices.EndFIndex, FIndices.BeginFIndex) && DoubleUtil.GreaterThanOrClose(FIndices.EndFIndex, crossing.FIndices.BeginFIndex)) { if (DoubleUtil.LessThan(crossing.FIndices.BeginFIndex, FIndices.BeginFIndex)) { FIndices.BeginFIndex = crossing.FIndices.BeginFIndex; StartNode = crossing.StartNode; } if (DoubleUtil.GreaterThan(crossing.FIndices.EndFIndex, FIndices.EndFIndex)) { FIndices.EndFIndex = crossing.FIndices.EndFIndex; EndNode = crossing.EndNode; } return(true); } return(false); }
/// <summary> /// Constructor /// </summary> /// <param name="newFIndices"></param> /// <param name="strokeNode"></param> public LassoCrossing(StrokeFIndices newFIndices, StrokeNode strokeNode) { System.Diagnostics.Debug.Assert(!newFIndices.IsEmpty); System.Diagnostics.Debug.Assert(strokeNode.IsValid); FIndices = newFIndices; StartNode = EndNode = strokeNode; }
/// <summary> /// Finds out if a given node intersects with this one, /// and returns findices of the intersection. /// </summary> /// <param name="hitNode"></param> /// <returns></returns> internal StrokeFIndices CutTest(StrokeNode hitNode) { if ((IsValid == false) || (hitNode.IsValid == false)) { return(StrokeFIndices.Empty); } IEnumerable <ContourSegment> hittingContour = hitNode.GetContourSegments(); // If the node contours intersect, the result is a pair of findices // this segment should be cut at to let the hitNode's contour through it. StrokeFIndices cutAt = _operations.CutTest(_lastNode, _thisNode, ConnectingQuad, hittingContour); return((_index == 0) ? cutAt : BindFIndices(cutAt)); }
/// <summary> /// Finds out if a given linear segment intersects with the contour of this node /// (including connecting quadrangle), and returns findices of the intersection. /// </summary> /// <param name="begin"></param> /// <param name="end"></param> /// <returns></returns> internal StrokeFIndices CutTest(Point begin, Point end) { if (IsValid == false) { return(StrokeFIndices.Empty); } // If the node contours intersect, the result is a pair of findices // this segment should be cut at to let the hitNode's contour through it. StrokeFIndices cutAt = _operations.CutTest(_lastNode, _thisNode, ConnectingQuad, begin, end); System.Diagnostics.Debug.Assert(!double.IsNaN(cutAt.BeginFIndex) && !double.IsNaN(cutAt.EndFIndex)); // Bind the found findices to the node and return the result return(BindFIndicesForLassoHitTest(cutAt)); }
/// <summary> /// Binds a local fragment to this node by setting the integer part of the /// fragment findices equal to the index of the previous node /// </summary> /// <param name="fragment"></param> /// <returns></returns> private StrokeFIndices BindFIndices(StrokeFIndices fragment) { System.Diagnostics.Debug.Assert(IsValid && (_index >= 0)); if (fragment.IsEmpty == false) { // Adjust only findices which are on this segment of thew spine (i.e. between 0 and 1) if (!DoubleUtil.AreClose(fragment.BeginFIndex, StrokeFIndices.BeforeFirst)) { System.Diagnostics.Debug.Assert(fragment.BeginFIndex >= 0 && fragment.BeginFIndex <= 1); fragment.BeginFIndex += _index - 1; } if (!DoubleUtil.AreClose(fragment.EndFIndex, StrokeFIndices.AfterLast)) { System.Diagnostics.Debug.Assert(fragment.EndFIndex >= 0 && fragment.EndFIndex <= 1); fragment.EndFIndex += _index - 1; } } return(fragment); }
/// <summary> /// Bind the StrokeFIndices for lasso hit test results. /// </summary> /// <param name="fragment"></param> /// <returns></returns> private StrokeFIndices BindFIndicesForLassoHitTest(StrokeFIndices fragment) { System.Diagnostics.Debug.Assert(IsValid); if (!fragment.IsEmpty) { // Adjust BeginFIndex if (DoubleUtil.AreClose(fragment.BeginFIndex, StrokeFIndices.BeforeFirst)) { // set it to be the index of the previous node, indicating intersection start from previous node fragment.BeginFIndex = (_index == 0 ? StrokeFIndices.BeforeFirst:_index - 1); } else { // Adjust findices which are on this segment of the spine (i.e. between 0 and 1) System.Diagnostics.Debug.Assert(DoubleUtil.GreaterThanOrClose(fragment.BeginFIndex, 0f)); System.Diagnostics.Debug.Assert(DoubleUtil.LessThanOrClose(fragment.BeginFIndex, 1f)); // Adjust the value to consider index, say from 0.75 to 3.75 (for _index = 4) fragment.BeginFIndex += _index - 1; } //Adjust EndFIndex if (DoubleUtil.AreClose(fragment.EndFIndex, StrokeFIndices.AfterLast)) { // set it to be the index of the current node, indicating the intersection cover the end of the node fragment.EndFIndex = (_isLastNode ? StrokeFIndices.AfterLast:_index); } else { System.Diagnostics.Debug.Assert(DoubleUtil.GreaterThanOrClose(fragment.EndFIndex, 0f)); System.Diagnostics.Debug.Assert(DoubleUtil.LessThanOrClose(fragment.EndFIndex, 1f)); // Ajust the value to consider the index fragment.EndFIndex += _index - 1; } } return(fragment); }
/// <summary> /// Helper method to calculate the exact location to cut /// </summary> /// <param name="spineVector">Vector the relative location of the two inking nodes</param> /// <param name="hitBegin">the begin point of the hitting segment</param> /// <param name="hitEnd">the end point of the hitting segment</param> /// <param name="endRadius">endNode radius</param> /// <param name="beginRadius">beginNode radius</param> /// <param name="result">StrokeFIndices representing the location for cutting</param> private void CalculateCutLocations( Vector spineVector, Vector hitBegin, Vector hitEnd, double endRadius, double beginRadius, ref StrokeFIndices result) { // Find out whether the {hitBegin, hitEnd} segment intersects with the contour // of the stroke segment, and find the lower index of the fragment to cut out. if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { if (WhereIsNodeAboutSegment(spineVector, hitBegin, hitEnd) == HitResult.Left) { double findex = 1 - ClipTest(spineVector, endRadius, beginRadius, hitBegin, hitEnd); if (findex > result.EndFIndex) { result.EndFIndex = findex; } } } // Find out whether the {hitBegin, hitEnd} segment intersects with the contour // of the stroke segment, and find the higher index of the fragment to cut out. if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)) { hitBegin -= spineVector; hitEnd -= spineVector; if (WhereIsNodeAboutSegment(-spineVector, hitBegin, hitEnd) == HitResult.Left) { double findex = ClipTest(-spineVector, beginRadius, endRadius, hitBegin, hitEnd); if (findex < result.BeginFIndex) { result.BeginFIndex = findex; } } } }
/// <summary> /// CutTest an inking StrokeNode segment (two nodes and a connecting quadrangle) against a hitting contour /// (represented by an enumerator of Contoursegments). /// </summary> /// <param name="beginNode">The begin StrokeNodeData</param> /// <param name="endNode">The end StrokeNodeData</param> /// <param name="quad">Connecing quadrangle between the begin and end inking node</param> /// <param name="hitContour">The hitting ContourSegments</param> /// <returns>StrokeFIndices representing the location for cutting</returns> internal override StrokeFIndices CutTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, IEnumerable <ContourSegment> hitContour) { // Compute the positions of the beginNode relative to the endNode. Vector spineVector = beginNode.IsEmpty ? new Vector(0, 0) : (beginNode.Position - endNode.Position); // If the node shape is an ellipse, transform the scene to turn the shape to a circle if (_nodeShapeToCircle.IsIdentity == false) { spineVector = _nodeShapeToCircle.Transform(spineVector); } double beginRadius = 0, endRadius; double beginRadiusSquared = 0, endRadiusSquared; endRadius = _radius * endNode.PressureFactor; endRadiusSquared = endRadius * endRadius; if (beginNode.IsEmpty == false) { beginRadius = _radius * beginNode.PressureFactor; beginRadiusSquared = beginRadius * beginRadius; } bool isInside = true; StrokeFIndices result = StrokeFIndices.Empty; foreach (ContourSegment hitSegment in hitContour) { if (hitSegment.IsArc) { // ISSUE-2004/06/15-vsmirnov - ellipse vs arc hit-testing is not implemented // and currently disabled in ErasingStroke } else { Vector hitBegin = hitSegment.Begin - endNode.Position; Vector hitEnd = hitBegin + hitSegment.Vector; // If the node shape is an ellipse, transform the scene to turn // the shape into circle. if (_nodeShapeToCircle.IsIdentity == false) { hitBegin = _nodeShapeToCircle.Transform(hitBegin); hitEnd = _nodeShapeToCircle.Transform(hitEnd); } bool isHit = false; // Hit-test the end node Vector nearest = GetNearest(hitBegin, hitEnd); if (nearest.LengthSquared < endRadiusSquared) { isHit = true; if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { result.EndFIndex = StrokeFIndices.AfterLast; if (beginNode.IsEmpty) { result.BeginFIndex = StrokeFIndices.BeforeFirst; break; } if (DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)) { break; } } } if ((beginNode.IsEmpty == false) && (!isHit || !DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))) { // Hit-test the first node nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector); if (nearest.LengthSquared < beginRadiusSquared) { isHit = true; if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)) { result.BeginFIndex = StrokeFIndices.BeforeFirst; if (DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { break; } } } } // If both nodes are hit or nothing is hit at all, return. if (beginNode.IsEmpty || (!isHit && (quad.IsEmpty || (HitTestQuadSegment(quad, hitSegment.Begin, hitSegment.End) == false)))) { if (isInside && (WhereIsVectorAboutVector( endNode.Position - hitSegment.Begin, hitSegment.Vector) != HitResult.Right)) { isInside = false; } continue; } isInside = false; // NTRAID#Window OS bug-1029694-2004/10/18-xiaotu, refactor the code to make it a method // to increase the maintainability of the program. FxCop bug. // Calculate the exact locations to cut. CalculateCutLocations(spineVector, hitBegin, hitEnd, endRadius, beginRadius, ref result); if (result.IsFull) { break; } } } // if (!result.IsFull) { if (isInside == true) { System.Diagnostics.Debug.Assert(result.IsEmpty); result = StrokeFIndices.Full; } else if ((DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst)) && (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.AfterLast))) { result.EndFIndex = StrokeFIndices.AfterLast; } else if ((DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.AfterLast)) && (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst))) { result.BeginFIndex = StrokeFIndices.BeforeFirst; } } if (IsInvalidCutTestResult(result)) { return(StrokeFIndices.Empty); } return(result); }
/// <summary> /// Cut-test ink segment defined by two nodes and a connecting quad against a linear segment /// </summary> /// <param name="beginNode">Begin node of the ink segment</param> /// <param name="endNode">End node of the ink segment</param> /// <param name="quad">Pre-computed quadrangle connecting the two ink nodes</param> /// <param name="hitBeginPoint">egin point of the hitting segment</param> /// <param name="hitEndPoint">End point of the hitting segment</param> /// <returns>Exact location to cut at represented by StrokeFIndices</returns> internal override StrokeFIndices CutTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint) { // Compute the positions of the involved points relative to the endNode. Vector spineVector = beginNode.IsEmpty ? new Vector(0, 0) : (beginNode.Position - endNode.Position); Vector hitBegin = hitBeginPoint - endNode.Position; Vector hitEnd = hitEndPoint - endNode.Position; // If the node shape is an ellipse, transform the scene to turn the shape to a circle if (_nodeShapeToCircle.IsIdentity == false) { spineVector = _nodeShapeToCircle.Transform(spineVector); hitBegin = _nodeShapeToCircle.Transform(hitBegin); hitEnd = _nodeShapeToCircle.Transform(hitEnd); } StrokeFIndices result = StrokeFIndices.Empty; // Hit-test the end node double beginRadius = 0, endRadius = _radius * endNode.PressureFactor; Vector nearest = GetNearest(hitBegin, hitEnd); if (nearest.LengthSquared <= (endRadius * endRadius)) { result.EndFIndex = StrokeFIndices.AfterLast; result.BeginFIndex = beginNode.IsEmpty ? StrokeFIndices.BeforeFirst : 1; } if (beginNode.IsEmpty == false) { // Hit-test the first node beginRadius = _radius * beginNode.PressureFactor; nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector); if (nearest.LengthSquared <= (beginRadius * beginRadius)) { result.BeginFIndex = StrokeFIndices.BeforeFirst; if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { result.EndFIndex = 0; } } } // If both nodes are hit or nothing is hit at all, return. if (result.IsFull || quad.IsEmpty || (result.IsEmpty && (HitTestQuadSegment(quad, hitBeginPoint, hitEndPoint) == false))) { return(result); } // Find out whether the {begin, end} segment intersects with the contour // of the stroke segment {_lastNode, _thisNode}, and find the lower index // of the fragment to cut out. if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)) { result.BeginFIndex = ClipTest(-spineVector, beginRadius, endRadius, hitBegin - spineVector, hitEnd - spineVector); } if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { result.EndFIndex = 1 - ClipTest(spineVector, endRadius, beginRadius, hitBegin, hitEnd); } if (IsInvalidCutTestResult(result)) { return(StrokeFIndices.Empty); } return(result); }
/// <summary> /// Hit-testing for point erase. /// </summary> /// <param name="iterator"></param> /// <param name="intersections"></param> /// <returns></returns> internal bool EraseTest(StrokeNodeIterator iterator, List <StrokeIntersection> intersections) { System.Diagnostics.Debug.Assert(iterator != null); System.Diagnostics.Debug.Assert(intersections != null); intersections.Clear(); List <StrokeFIndices> eraseAt = new List <StrokeFIndices>(); if ((_erasingStrokeNodes == null) || (_erasingStrokeNodes.Count == 0)) { return(false); } Rect inkSegmentBounds = Rect.Empty; for (int x = 0; x < iterator.Count; x++) { StrokeNode inkStrokeNode = iterator[x]; Rect inkNodeBounds = inkStrokeNode.GetBounds(); inkSegmentBounds.Union(inkNodeBounds); if (inkSegmentBounds.IntersectsWith(_bounds)) { // int index = eraseAt.Count; foreach (StrokeNode erasingStrokeNode in _erasingStrokeNodes) { if (false == inkSegmentBounds.IntersectsWith(erasingStrokeNode.GetBoundsConnected())) { continue; } StrokeFIndices fragment = inkStrokeNode.CutTest(erasingStrokeNode); if (fragment.IsEmpty) { continue; } // Merge it with the other results for this ink segment bool inserted = false; for (int i = index; i < eraseAt.Count; i++) { StrokeFIndices lastFragment = eraseAt[i]; if (fragment.BeginFIndex < lastFragment.EndFIndex) { // If the fragments overlap, merge them if (fragment.EndFIndex > lastFragment.BeginFIndex) { fragment = new StrokeFIndices( Math.Min(lastFragment.BeginFIndex, fragment.BeginFIndex), Math.Max(lastFragment.EndFIndex, fragment.EndFIndex)); // If the fragment doesn't go beyond lastFragment, break if ((fragment.EndFIndex <= lastFragment.EndFIndex) || ((i + 1) == eraseAt.Count)) { inserted = true; eraseAt[i] = fragment; break; } else { eraseAt.RemoveAt(i); i--; } } // insert otherwise else { eraseAt.Insert(i, fragment); inserted = true; break; } } } // If not merged nor inserted, add it to the end of the list if (false == inserted) { eraseAt.Add(fragment); } // Break out if the entire ink segment is hit - {BeforeFirst, AfterLast} if (eraseAt[eraseAt.Count - 1].IsFull) { break; } } // Merge inter-segment overlapping fragments if ((index > 0) && (index < eraseAt.Count)) { StrokeFIndices lastFragment = eraseAt[index - 1]; if (DoubleUtil.AreClose(lastFragment.EndFIndex, StrokeFIndices.AfterLast)) { if (DoubleUtil.AreClose(eraseAt[index].BeginFIndex, StrokeFIndices.BeforeFirst)) { lastFragment.EndFIndex = eraseAt[index].EndFIndex; eraseAt[index - 1] = lastFragment; eraseAt.RemoveAt(index); } else { lastFragment.EndFIndex = inkStrokeNode.Index; eraseAt[index - 1] = lastFragment; } } } } // Start next ink segment inkSegmentBounds = inkNodeBounds; } if (eraseAt.Count != 0) { foreach (StrokeFIndices segment in eraseAt) { intersections.Add(new StrokeIntersection(segment.BeginFIndex, StrokeFIndices.AfterLast, StrokeFIndices.BeforeFirst, segment.EndFIndex)); } } return(eraseAt.Count != 0); }
internal StrokeIntersection[] HitTest(StrokeNodeIterator iterator) { System.Diagnostics.Debug.Assert(_points != null); System.Diagnostics.Debug.Assert(iterator != null); if (_points.Count < 3) { // // it takes at least 3 points to create a lasso // return(Array.Empty <StrokeIntersection>()); } // // We're about to perform hit testing with a lasso. // To do so we need to iterate through each StrokeNode. // As we do, we calculate the bounding rect between it // and the previous StrokeNode and store this in 'currentStrokeSegmentBounds' // // Next, we check to see if that StrokeNode pair's bounding box intersects // with the bounding box of the Lasso points. If not, we continue iterating through // StrokeNode pairs. // // If it does, we do a more granular hit test by pairing points in the Lasso, getting // their bounding box and seeing if that bounding box intersects our current StrokeNode // pair // Point lastNodePosition = new Point(); Point lassoLastPoint = _points[_points.Count - 1]; Rect currentStrokeSegmentBounds = Rect.Empty; // Initilize the current crossing to be an empty one LassoCrossing currentCrossing = LassoCrossing.EmptyCrossing; // Creat a list to hold all the crossings List <LassoCrossing> crossingList = new List <LassoCrossing>(); for (int i = 0; i < iterator.Count; i++) { StrokeNode strokeNode = iterator[i]; Rect nodeBounds = strokeNode.GetBounds(); currentStrokeSegmentBounds.Union(nodeBounds); // Skip the node if it's outside of the lasso's bounds if (currentStrokeSegmentBounds.IntersectsWith(_bounds) == true) { // currentStrokeSegmentBounds, made up of the bounding box of // this StrokeNode unioned with the last StrokeNode, // intersects the lasso bounding box. // // Now we need to iterate through the lasso points and find out where they cross // Point lastPoint = lassoLastPoint; foreach (Point point in _points) { // // calculate a segment of the lasso from the last point // to the current point // Rect lassoSegmentBounds = new Rect(lastPoint, point); // // see if this lasso segment intersects with the current stroke segment // if (!currentStrokeSegmentBounds.IntersectsWith(lassoSegmentBounds)) { lastPoint = point; continue; } // // the lasso segment DOES intersect with the current stroke segment // find out precisely where // StrokeFIndices strokeFIndices = strokeNode.CutTest(lastPoint, point); lastPoint = point; if (strokeFIndices.IsEmpty) { // current lasso segment does not hit the stroke segment, continue with the next lasso point continue; } // Create a potentially new crossing for the current hit testing result. LassoCrossing potentialNewCrossing = new LassoCrossing(strokeFIndices, strokeNode); // Try to merge with the current crossing. If the merge is succussful (return true), the new crossing is actually // continueing the current crossing, so do not start a new crossing. Otherwise, start a new one and add the existing // one to the list. if (!currentCrossing.Merge(potentialNewCrossing)) { // start a new crossing and add the existing on to the list crossingList.Add(currentCrossing); currentCrossing = potentialNewCrossing; } } } // Continue with the next node currentStrokeSegmentBounds = nodeBounds; lastNodePosition = strokeNode.Position; } // Adding the last crossing to the list, if valid if (!currentCrossing.IsEmpty) { crossingList.Add(currentCrossing); } // Handle the special case of no intersection at all if (crossingList.Count == 0) { // the stroke was either completely inside the lasso // or outside the lasso if (this.Contains(lastNodePosition)) { StrokeIntersection[] strokeIntersections = new StrokeIntersection[1]; strokeIntersections[0] = StrokeIntersection.Full; return(strokeIntersections); } else { return(Array.Empty <StrokeIntersection>()); } } // It is still possible that the current crossing list is not sorted or overlapping. // Sort the list and merge the overlapping ones. SortAndMerge(ref crossingList); // Produce the hit test results and store them in a list List <StrokeIntersection> strokeIntersectionList = new List <StrokeIntersection>(); ProduceHitTestResults(crossingList, strokeIntersectionList); return(strokeIntersectionList.ToArray()); }