/// <summary> /// Gets a StrokeNode at the specified index that connects to a stroke at the previousIndex /// previousIndex can be -1 to signify it should be empty (first strokeNode) /// </summary> /// <returns></returns> internal StrokeNode this[int index, int previousIndex] { get { if (_stylusPoints == null || index < 0 || index >= _stylusPoints.Count || previousIndex < -1 || previousIndex >= index) { throw new IndexOutOfRangeException(); } StylusPoint stylusPoint = _stylusPoints[index]; StylusPoint previousStylusPoint = (previousIndex == -1 ? new StylusPoint() : _stylusPoints[previousIndex]); float pressureFactor = 1.0f; float previousPressureFactor = 1.0f; if (_usePressure) { pressureFactor = StrokeNodeIterator.GetNormalizedPressureFactor(stylusPoint.PressureFactor); previousPressureFactor = StrokeNodeIterator.GetNormalizedPressureFactor(previousStylusPoint.PressureFactor); } StrokeNodeData nodeData = new StrokeNodeData((Point)stylusPoint, pressureFactor); StrokeNodeData lastNodeData = StrokeNodeData.Empty; if (previousIndex != -1) { lastNodeData = new StrokeNodeData((Point)previousStylusPoint, previousPressureFactor); } //we use previousIndex+1 because index can skip ahead return(new StrokeNode(_operations, previousIndex + 1, nodeData, lastNodeData, index == _stylusPoints.Count - 1 /*Is this the last node?*/)); } }
/// <summary> /// Computes the bounds of a node /// </summary> /// <param name="node">node to compute bounds of</param> /// <returns>bounds of the node</returns> internal Rect GetNodeBounds(StrokeNodeData node) { if (_shapeBounds.IsEmpty) { int i; for (i = 0; (i + 1) < _vertices.Length; i += 2) { _shapeBounds.Union(new Rect((Point)_vertices[i], (Point)_vertices[i + 1])); } if (i < _vertices.Length) { _shapeBounds.Union((Point)_vertices[i]); } } Rect boundingBox = _shapeBounds; System.Diagnostics.Debug.Assert((boundingBox.X <= 0) && (boundingBox.Y <= 0)); double pressureFactor = node.PressureFactor; if (!DoubleUtil.AreClose(pressureFactor,1d)) { boundingBox = new Rect( _shapeBounds.X * pressureFactor, _shapeBounds.Y * pressureFactor, _shapeBounds.Width * pressureFactor, _shapeBounds.Height * pressureFactor); } boundingBox.Location += (Vector)node.Position; return boundingBox; }
/// <summary> /// Hit-tests a stroke segment defined by two nodes against a linear segment. /// </summary> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <param name="quad">Pre-computed quadrangle connecting the two nodes. /// Can be empty if the begion node is empty or when one node is entirely inside the other</param> /// <param name="hitBeginPoint">an end point of the hitting linear segment</param> /// <param name="hitEndPoint">an end point of the hitting linear segment</param> /// <returns>true if the hitting segment intersect the contour comprised of the two stroke nodes</returns> internal override bool HitTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint) { StrokeNodeData bigNode, smallNode; if (beginNode.IsEmpty || (quad.IsEmpty && (endNode.PressureFactor > beginNode.PressureFactor))) { // Need to test one node only bigNode = endNode; smallNode = StrokeNodeData.Empty; } else { // In this case the size doesn't matter. bigNode = beginNode; smallNode = endNode; } // Compute the positions of the involved points relative to bigNode. Vector hitBegin = hitBeginPoint - bigNode.Position; Vector hitEnd = hitEndPoint - bigNode.Position; // If the node shape is an ellipse, transform the scene to turn the shape to a circle if (_nodeShapeToCircle.IsIdentity == false) { hitBegin = _nodeShapeToCircle.Transform(hitBegin); hitEnd = _nodeShapeToCircle.Transform(hitEnd); } bool isHit = false; // Hit-test the big node double bigRadius = _radius * bigNode.PressureFactor; Vector nearest = GetNearest(hitBegin, hitEnd); if (nearest.LengthSquared <= (bigRadius * bigRadius)) { isHit = true; } else if (quad.IsEmpty == false) { // Hit-test the other node Vector spineVector = smallNode.Position - bigNode.Position; if (_nodeShapeToCircle.IsIdentity == false) { spineVector = _nodeShapeToCircle.Transform(spineVector); } double smallRadius = _radius * smallNode.PressureFactor; nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector); if ((nearest.LengthSquared <= (smallRadius * smallRadius)) || HitTestQuadSegment(quad, hitBeginPoint, hitEndPoint)) { isHit = true; } } return(isHit); }
/// <summary> /// Constructor. /// </summary> /// <param name="operations">StrokeNodeOperations object created for particular rendering</param> /// <param name="index">Index of the node on the stroke spine</param> /// <param name="nodeData">StrokeNodeData for this node</param> /// <param name="lastNodeData">StrokeNodeData for the precedeng node</param> /// <param name="isLastNode">Whether the current node is the last node</param> internal StrokeNode( StrokeNodeOperations operations, int index, StrokeNodeData nodeData, StrokeNodeData lastNodeData, bool isLastNode) { System.Diagnostics.Debug.Assert(operations != null); System.Diagnostics.Debug.Assert((nodeData.IsEmpty == false) && (index >= 0)); _operations = operations; _index = index; _thisNode = nodeData; _lastNode = lastNodeData; _isQuadCached = lastNodeData.IsEmpty; _connectingQuad = Quad.Empty; _isLastNode = isLastNode; }
/// <summary> /// Constructor. /// </summary> /// <param name="operations">StrokeNodeOperations object created for particular rendering</param> /// <param name="index">Index of the node on the stroke spine</param> /// <param name="nodeData">StrokeNodeData for this node</param> /// <param name="lastNodeData">StrokeNodeData for the precedeng node</param> /// <param name="isLastNode">Whether the current node is the last node</param> internal StrokeNode( StrokeNodeOperations operations, int index, StrokeNodeData nodeData, StrokeNodeData lastNodeData, bool isLastNode) { System.Diagnostics.Debug.Assert(operations != null); System.Diagnostics.Debug.Assert((nodeData.IsEmpty == false) && (index >= 0)); _operations = operations; _index = index; _thisNode = nodeData; _lastNode = lastNodeData; _isQuadCached = lastNodeData.IsEmpty; _connectingQuad = Quad.Empty; _isLastNode = isLastNode; }
/// <summary> /// /// </summary> /// <param name="node"></param> /// <param name="quad"></param> /// <returns></returns> internal override IEnumerable <ContourSegment> GetContourSegments(StrokeNodeData node, Quad quad) { System.Diagnostics.Debug.Assert(node.IsEmpty == false); if (quad.IsEmpty) { Point point = node.Position; point.X += _radius; yield return(new ContourSegment(point, point, node.Position)); } else if (_nodeShapeToCircle.IsIdentity) { yield return(new ContourSegment(quad.A, quad.B)); yield return(new ContourSegment(quad.B, quad.C, node.Position)); yield return(new ContourSegment(quad.C, quad.D)); yield return(new ContourSegment(quad.D, quad.A)); } }
/// <summary> /// Gets a StrokeNode at the specified index that connects to a stroke at the previousIndex /// previousIndex can be -1 to signify it should be empty (first strokeNode) /// </summary> /// <returns></returns> internal StrokeNode this[int index, int previousIndex] { get { if (_stylusPoints == null || index < 0 || index >= _stylusPoints.Count || previousIndex < -1 || previousIndex >= index) { throw new IndexOutOfRangeException(); } StylusPoint stylusPoint = _stylusPoints[index]; StylusPoint previousStylusPoint = (previousIndex == -1 ? new StylusPoint() : _stylusPoints[previousIndex]); float pressureFactor = 1.0f; float previousPressureFactor = 1.0f; if (_usePressure) { pressureFactor = StrokeNodeIterator.GetNormalizedPressureFactor(stylusPoint.PressureFactor); previousPressureFactor = StrokeNodeIterator.GetNormalizedPressureFactor(previousStylusPoint.PressureFactor); } StrokeNodeData nodeData = new StrokeNodeData((Point)stylusPoint, pressureFactor); StrokeNodeData lastNodeData = StrokeNodeData.Empty; if (previousIndex != -1) { lastNodeData = new StrokeNodeData((Point)previousStylusPoint, previousPressureFactor); } //we use previousIndex+1 because index can skip ahead return new StrokeNode(_operations, previousIndex + 1, nodeData, lastNodeData, index == _stylusPoints.Count - 1 /*Is this the last node?*/); } }
/// <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> /// Hit-tests a stroke segment defined by two nodes against another stroke segment. /// </summary> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <param name="quad">Pre-computed quadrangle connecting the two nodes. /// Can be empty if the begion node is empty or when one node is entirely inside the other</param> /// <param name="hitContour">a collection of basic segments outlining the hitting contour</param> /// <returns>true if the contours intersect or overlap</returns> internal override bool HitTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, IEnumerable<ContourSegment> hitContour) { StrokeNodeData bigNode, smallNode; double bigRadiusSquared, smallRadiusSquared = 0; Vector spineVector; if (beginNode.IsEmpty || (quad.IsEmpty && (endNode.PressureFactor > beginNode.PressureFactor))) { // Need to test one node only bigNode = endNode; smallNode = StrokeNodeData.Empty; spineVector = new Vector(); } else { // In this case the size doesn't matter. bigNode = beginNode; smallNode = endNode; smallRadiusSquared = _radius * smallNode.PressureFactor; smallRadiusSquared *= smallRadiusSquared; // Find position of smallNode relative to the bigNode. spineVector = smallNode.Position - bigNode.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); } } bigRadiusSquared = _radius * bigNode.PressureFactor; bigRadiusSquared *= bigRadiusSquared; bool isHit = false; // When hit-testing a contour against another contour, like in this case, // the default implementation checks whether any edge (segment) of the hitting // contour intersects with the contour of the ink segment. But this doesn't cover // the case when the ink segment is entirely inside of the hitting segment. // The bool variable isInside is used here to track that case. It answers the question // 'Is ink contour inside if the hitting contour?'. It's initialized to 'true" // and then verified for each edge of the hitting contour until there's a hit or // until it's false. bool isInside = true; 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 { // Find position of the hitting segment relative to bigNode transformed to circle. Vector hitBegin = hitSegment.Begin - bigNode.Position; Vector hitEnd = hitBegin + hitSegment.Vector; if (_nodeShapeToCircle.IsIdentity == false) { hitBegin = _nodeShapeToCircle.Transform(hitBegin); hitEnd = _nodeShapeToCircle.Transform(hitEnd); } // Hit-test the big node Vector nearest = GetNearest(hitBegin, hitEnd); if (nearest.LengthSquared <= bigRadiusSquared) { isHit = true; break; } // Hit-test the other node if (quad.IsEmpty == false) { nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector); if ((nearest.LengthSquared <= smallRadiusSquared) || HitTestQuadSegment(quad, hitSegment.Begin, hitSegment.End)) { isHit = true; break; } } // While there's still a chance to find the both nodes inside the hitting contour, // continue checking on position of the endNode relative to the edges of the hitting contour. if (isInside && (WhereIsVectorAboutVector(endNode.Position - hitSegment.Begin, hitSegment.Vector) != HitResult.Right)) { isInside = false; } } } return (isHit || isInside); }
/// <summary> /// ISSUE-2004/06/15- temporary workaround to avoid hit-testing ellipses with ellipses /// </summary> /// <param name="beginNode"></param> /// <param name="endNode"></param> /// <returns></returns> internal override IEnumerable<ContourSegment> GetNonBezierContourSegments(StrokeNodeData beginNode, StrokeNodeData endNode) { Quad quad = beginNode.IsEmpty ? Quad.Empty : base.GetConnectingQuad(beginNode, endNode); return base.GetContourSegments(endNode, quad); }
internal void GetNodeContourPoints(StrokeNodeData node, List<Point> pointBuffer) { double pressureFactor = node.PressureFactor; if (DoubleUtil.AreClose(pressureFactor, 1d)) { for (int i = 0; i < _vertices.Length; i++) { pointBuffer.Add(node.Position + _vertices[i]); } } else { for (int i = 0; i < _vertices.Length; i++) { pointBuffer.Add(node.Position + (_vertices[i] * pressureFactor)); } } }
/// <summary> /// Hit-tests ink segment defined by two nodes 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">Begin 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 virtual StrokeFIndices CutTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint) { StrokeFIndices result = StrokeFIndices.Empty; // First, find out if the hitting segment intersects with either of the ink nodes for (int node = (beginNode.IsEmpty ? 1 : 0); node < 2; node++) { Point position = (node == 0) ? beginNode.Position : endNode.Position; double pressureFactor = (node == 0) ? beginNode.PressureFactor : endNode.PressureFactor; // Adjust the segment for the node's pressure factor Vector hitBegin = hitBeginPoint - position; Vector hitEnd = hitEndPoint - position; if (pressureFactor != 1) { System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false); hitBegin /= pressureFactor; hitEnd /= pressureFactor; } // Hit-test the node against the segment if (true == HitTestPolygonSegment(_vertices, hitBegin, hitEnd)) { if (node == 0) { result.BeginFIndex = StrokeFIndices.BeforeFirst; result.EndFIndex = 0; } else { result.EndFIndex = StrokeFIndices.AfterLast; if (beginNode.IsEmpty) { result.BeginFIndex = StrokeFIndices.BeforeFirst; } else if (result.BeginFIndex != StrokeFIndices.BeforeFirst) { result.BeginFIndex = 1; } } } } // If both nodes are hit, return. if (result.IsFull) { return result; } // If there's no hit at all, return. if (result.IsEmpty && (quad.IsEmpty || !HitTestQuadSegment(quad, hitBeginPoint, hitEndPoint))) { return result; } // The segments do intersect. Find findices on the ink segment to cut it at. if (result.BeginFIndex != StrokeFIndices.BeforeFirst) { // The begin node is not hit, i.e. the begin findex is on this spine segment, find it. result.BeginFIndex = ClipTest( (endNode.Position - beginNode.Position) / beginNode.PressureFactor, (endNode.PressureFactor / beginNode.PressureFactor) - 1, (hitBeginPoint - beginNode.Position) / beginNode.PressureFactor, (hitEndPoint - beginNode.Position) / beginNode.PressureFactor); } if (result.EndFIndex != StrokeFIndices.AfterLast) { // The end node is not hit, i.e. the end findex is on this spine segment, find it. result.EndFIndex = 1 - ClipTest( (beginNode.Position - endNode.Position) / endNode.PressureFactor, (beginNode.PressureFactor / endNode.PressureFactor) - 1, (hitBeginPoint - endNode.Position) / endNode.PressureFactor, (hitEndPoint - endNode.Position) / endNode.PressureFactor); } 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-tests a stroke segment defined by two nodes against another stroke segment. /// </summary> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <param name="quad">Pre-computed quadrangle connecting the two nodes. /// Can be empty if the begion node is empty or when one node is entirely inside the other</param> /// <param name="hitContour">a collection of basic segments outlining the hitting contour</param> /// <returns>true if the contours intersect or overlap</returns> internal override bool HitTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, IEnumerable <ContourSegment> hitContour) { StrokeNodeData bigNode, smallNode; double bigRadiusSquared, smallRadiusSquared = 0; Vector spineVector; if (beginNode.IsEmpty || (quad.IsEmpty && (endNode.PressureFactor > beginNode.PressureFactor))) { // Need to test one node only bigNode = endNode; smallNode = StrokeNodeData.Empty; spineVector = new Vector(); } else { // In this case the size doesn't matter. bigNode = beginNode; smallNode = endNode; smallRadiusSquared = _radius * smallNode.PressureFactor; smallRadiusSquared *= smallRadiusSquared; // Find position of smallNode relative to the bigNode. spineVector = smallNode.Position - bigNode.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); } } bigRadiusSquared = _radius * bigNode.PressureFactor; bigRadiusSquared *= bigRadiusSquared; bool isHit = false; // When hit-testing a contour against another contour, like in this case, // the default implementation checks whether any edge (segment) of the hitting // contour intersects with the contour of the ink segment. But this doesn't cover // the case when the ink segment is entirely inside of the hitting segment. // The bool variable isInside is used here to track that case. It answers the question // 'Is ink contour inside if the hitting contour?'. It's initialized to 'true" // and then verified for each edge of the hitting contour until there's a hit or // until it's false. bool isInside = true; 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 { // Find position of the hitting segment relative to bigNode transformed to circle. Vector hitBegin = hitSegment.Begin - bigNode.Position; Vector hitEnd = hitBegin + hitSegment.Vector; if (_nodeShapeToCircle.IsIdentity == false) { hitBegin = _nodeShapeToCircle.Transform(hitBegin); hitEnd = _nodeShapeToCircle.Transform(hitEnd); } // Hit-test the big node Vector nearest = GetNearest(hitBegin, hitEnd); if (nearest.LengthSquared <= bigRadiusSquared) { isHit = true; break; } // Hit-test the other node if (quad.IsEmpty == false) { nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector); if ((nearest.LengthSquared <= smallRadiusSquared) || HitTestQuadSegment(quad, hitSegment.Begin, hitSegment.End)) { isHit = true; break; } } // While there's still a chance to find the both nodes inside the hitting contour, // continue checking on position of the endNode relative to the edges of the hitting contour. if (isInside && (WhereIsVectorAboutVector(endNode.Position - hitSegment.Begin, hitSegment.Vector) != HitResult.Right)) { isInside = false; } } } return(isHit || isInside); }
/// <summary> /// ISSUE-2004/06/15- temporary workaround to avoid hit-testing ellipses with ellipses /// </summary> /// <param name="beginNode"></param> /// <param name="endNode"></param> /// <returns></returns> internal override IEnumerable <ContourSegment> GetNonBezierContourSegments(StrokeNodeData beginNode, StrokeNodeData endNode) { Quad quad = beginNode.IsEmpty ? Quad.Empty : base.GetConnectingQuad(beginNode, endNode); return(base.GetContourSegments(endNode, quad)); }
/// <summary> /// Hit-tests ink segment defined by two nodes 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">Begin point of the hitting segment</param> /// <param name="hitEndPoint">End point of the hitting segment</param> /// <returns>true if there's intersection, false otherwise</returns> internal virtual bool HitTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint) { // Check for special cases when the endNode is the very first one (beginNode.IsEmpty) // or one node is completely inside the other. In either case the connecting quad // would be Empty and we need to hit-test against the biggest node (the one with // the greater PressureFactor) if (quad.IsEmpty) { Point position; double pressureFactor; if (beginNode.IsEmpty || (endNode.PressureFactor > beginNode.PressureFactor)) { position = endNode.Position; pressureFactor = endNode.PressureFactor; } else { position = beginNode.Position; pressureFactor = beginNode.PressureFactor; } // Find the coordinates of the hitting segment relative to the ink node Vector hitBegin = hitBeginPoint - position, hitEnd = hitEndPoint - position; if (pressureFactor != 1) { // Instead of applying pressure to the node, do reverse scaling on // the hitting segment. This allows us use the original array of vertices // in hit-testing. System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false); hitBegin /= pressureFactor; hitEnd /= pressureFactor; } return HitTestPolygonSegment(_vertices, hitBegin, hitEnd); } else { // Iterate through the vertices of the contour of the ink segment // check where the hitting segment is about them, return false if it's // on the outer (left) side of the ink contour. This implementation might // look more complex than straightforward separated hit-testing of three // polygons (beginNode, quad, endNode), but it's supposed to be more optimal // because the number of edges it hit-tests is approximately twice less // than with the straightforward implementation. // Start with the segment quad.C->quad.D Vector hitBegin = hitBeginPoint - beginNode.Position; Vector hitEnd = hitEndPoint - beginNode.Position; HitResult hitResult = WhereIsSegmentAboutSegment( hitBegin, hitEnd, quad.C - beginNode.Position, quad.D - beginNode.Position); if (HitResult.Left == hitResult) { return false; } // Continue clockwise from quad.D to quad.C HitResult firstResult = hitResult, lastResult = hitResult; double pressureFactor = beginNode.PressureFactor; // Find the index of the vertex that is quad.D // Use count var to avoid infinite loop, normally it shouldn't // happen but it doesn't hurt to check it just in case. int i = 0, count = _vertices.Length; Vector vertex = new Vector(); for (i = 0; i < count; i++) { vertex = _vertices[i] * pressureFactor; // Here and in a few more places down the code, when comparing // a quad's vertex vs a scaled shape vertex, it's important to // compute them the same way as in GetConnectingQuad, so that not // hit that double's computation error. For instance, sometimes the // expression (vertex == quad.D - beginNode.Position) gives 'false' // while the expression below gives 'true'. (Another workaround is to // use DoubleUtil.AreClose but that;d be less performant) if ((beginNode.Position + vertex) == quad.D) { break; } } System.Diagnostics.Debug.Assert(count > 0); // This loop does the iteration thru the edges of the ink segment // clockwise from quad.D to quad.C. for (int node = 0; node < 2; node++) { Point nodePosition = (node == 0) ? beginNode.Position : endNode.Position; Point end = (node == 0) ? quad.A : quad.C; count = _vertices.Length; while (((nodePosition + vertex) != end) && (count != 0)) { i = (i + 1) % _vertices.Length; Vector nextVertex = (pressureFactor == 1) ? _vertices[i] : (_vertices[i] * pressureFactor); hitResult = WhereIsSegmentAboutSegment(hitBegin, hitEnd, vertex, nextVertex); if (HitResult.Hit == hitResult) { return true; } if (true == IsOutside(hitResult, lastResult)) { return false; } lastResult = hitResult; vertex = nextVertex; count--; } System.Diagnostics.Debug.Assert(count > 0); if (node == 0) { // The first iteration is done thru the outer segments of beginNode // and ends at quad.A, for the second one make some adjustments // to continue iterating through quad.AB and the outer segments of // endNode up to quad.C pressureFactor = endNode.PressureFactor; Vector spineVector = endNode.Position - beginNode.Position; vertex -= spineVector; hitBegin -= spineVector; hitEnd -= spineVector; // Find the index of the vertex that is quad.B count = _vertices.Length; while (((endNode.Position + _vertices[i] * pressureFactor) != quad.B) && (count != 0)) { i = (i + 1) % _vertices.Length; count--; } System.Diagnostics.Debug.Assert(count > 0); i--; } } return (false == IsOutside(firstResult, hitResult)); } }
/// <summary> /// Hit-tests a stroke segment defined by two nodes against another stroke segment. /// </summary> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <param name="quad">Pre-computed quadrangle connecting the two nodes. /// Can be empty if the begion node is empty or when one node is entirely inside the other</param> /// <param name="hitContour">a collection of basic segments outlining the hitting contour</param> /// <returns>true if the contours intersect or overlap</returns> internal virtual bool HitTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, IEnumerable<ContourSegment> hitContour) { // Check for special cases when the endNode is the very first one (beginNode.IsEmpty) // or one node is completely inside the other. In either case the connecting quad // would be Empty and we need to hittest against the biggest node (the one with // the greater PressureFactor) if (quad.IsEmpty) { // NTRAID#Window OS bug-1029694-2004/10/15-xiaotu, refactor the code to make it a method // to increase the maintainability of the program. FxCop bug. // Make a call to hit-test the biggest node the hitting contour. return HitTestPolygonContourSegments(hitContour, beginNode, endNode); } else { // NTRAID#Window OS bug-1029694-2004/10/15-xiaotu, refactor the code to make it a method // to increase the maintainability of the program. FxCop bug. // HitTest the the hitting contour against the inking contour return HitTestInkContour(hitContour, quad, beginNode, endNode); } }
/// <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> /// CutTest /// </summary> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <param name="quad">Pre-computed quadrangle connecting the two nodes. /// Can be empty if the begion node is empty or when one node is entirely inside the other</param> /// <param name="hitContour">a collection of basic segments outlining the hitting contour</param> /// <returns></returns> internal virtual StrokeFIndices CutTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, IEnumerable<ContourSegment> hitContour) { if (beginNode.IsEmpty) { if (HitTest(beginNode, endNode, quad, hitContour) == true) { return StrokeFIndices.Full; } return StrokeFIndices.Empty; } StrokeFIndices result = StrokeFIndices.Empty; bool isInside = true; Vector spineVector = (endNode.Position - beginNode.Position) / beginNode.PressureFactor; Vector spineVectorReversed = (beginNode.Position - endNode.Position) / endNode.PressureFactor; double pressureDelta = (endNode.PressureFactor / beginNode.PressureFactor) - 1; double pressureDeltaReversed = (beginNode.PressureFactor / endNode.PressureFactor) - 1; foreach (ContourSegment hitSegment in hitContour) { // NTRAID#Window OS bug-1029694-2004/10/19-xiaotu, refactor the code to make it a method // to increase the maintainability of the program. FxCop bug. // First, find out if hitSegment intersects with either of the ink nodes bool isHit = HitTestStrokeNodes(hitSegment,beginNode,endNode, ref result); // If both nodes are hit, return. if (result.IsFull) { return result; } // If neither of the nodes is hit, hit-test the connecting quad if (isHit == false) { // If neither of the nodes is hit and the contour of one node is entirely // inside the contour of the other node, then done with this hitting segment if (!quad.IsEmpty) { isHit = hitSegment.IsArc ? HitTestQuadCircle(quad, hitSegment.Begin + hitSegment.Radius, hitSegment.Radius) : HitTestQuadSegment(quad, hitSegment.Begin, hitSegment.End); } if (isHit == false) { if (isInside == true) { isInside = hitSegment.IsArc ? (WhereIsVectorAboutArc(endNode.Position - hitSegment.Begin - hitSegment.Radius, -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) != HitResult.Hit) : (WhereIsVectorAboutVector( endNode.Position - hitSegment.Begin, hitSegment.Vector) == HitResult.Right); } continue; } } isInside = false; // NTRAID#Window OS bug-1029694-2004/10/15-xiaotu, refactor the code to make it a new method // CalculateClipLocation to increase the maintainability of the program. FxCop bug. // If the begin node is not hit, find the begin findex on the ink segment to cut it at if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)) { double findex = CalculateClipLocation(hitSegment, beginNode, spineVector, pressureDelta); if (findex != StrokeFIndices.BeforeFirst) { System.Diagnostics.Debug.Assert(findex >= 0 && findex <= 1); if (result.BeginFIndex > findex) { result.BeginFIndex = findex; } } } // If the end node is not hit, find the end findex on the ink segment to cut it at if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { double findex = CalculateClipLocation(hitSegment, endNode, spineVectorReversed, pressureDeltaReversed); if (findex != StrokeFIndices.BeforeFirst) { System.Diagnostics.Debug.Assert(findex >= 0 && findex <= 1); findex = 1 - findex; if (result.EndFIndex < findex) { result.EndFIndex = findex; } } } } if (DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.AfterLast)) { if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst)) { result.BeginFIndex = StrokeFIndices.BeforeFirst; } } else if (DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst)) { result.EndFIndex = StrokeFIndices.AfterLast; } if (IsInvalidCutTestResult(result)) { return StrokeFIndices.Empty; } return (result.IsEmpty && isInside) ? StrokeFIndices.Full : result; }
/// <summary> /// Finds connecting points for a pair of stroke nodes /// </summary> /// <param name="beginNode">a node to connect</param> /// <param name="endNode">another node, next to beginNode</param> /// <returns>connecting quadrangle</returns> internal override Quad GetConnectingQuad(StrokeNodeData beginNode, StrokeNodeData endNode) { if (beginNode.IsEmpty || endNode.IsEmpty || DoubleUtil.AreClose(beginNode.Position, endNode.Position)) { return(Quad.Empty); } // Get the vector between the node positions Vector spine = endNode.Position - beginNode.Position; if (_nodeShapeToCircle.IsIdentity == false) { spine = _nodeShapeToCircle.Transform(spine); } double beginRadius = _radius * beginNode.PressureFactor; double endRadius = _radius * endNode.PressureFactor; // Get the vector and the distance between the node positions double distanceSquared = spine.LengthSquared; double delta = endRadius - beginRadius; double deltaSquared = DoubleUtil.IsZero(delta) ? 0 : (delta * delta); if (DoubleUtil.LessThanOrClose(distanceSquared, deltaSquared)) { // One circle is contained within the other return(Quad.Empty); } // Thus, at this point, distance > 0, which avoids the DivideByZero error // Also, here, distanceSquared > deltaSquared // Thus, 0 <= rSin < 1 // Get the components of the radius vectors double distance = Math.Sqrt(distanceSquared); spine /= distance; Vector rad = spine; // Turn left double temp = rad.Y; rad.Y = -rad.X; rad.X = temp; Vector vectorToLeftTangent, vectorToRightTangent; double rSinSquared = deltaSquared / distanceSquared; if (DoubleUtil.IsZero(rSinSquared)) { vectorToLeftTangent = rad; vectorToRightTangent = -rad; } else { rad *= Math.Sqrt(1 - rSinSquared); spine *= Math.Sqrt(rSinSquared); if (beginNode.PressureFactor < endNode.PressureFactor) { spine = -spine; } vectorToLeftTangent = spine + rad; vectorToRightTangent = spine - rad; } // Get the common tangent points if (_circleToNodeShape.IsIdentity == false) { vectorToLeftTangent = _circleToNodeShape.Transform(vectorToLeftTangent); vectorToRightTangent = _circleToNodeShape.Transform(vectorToRightTangent); } return(new Quad(beginNode.Position + (vectorToLeftTangent * beginRadius), endNode.Position + (vectorToLeftTangent * endRadius), endNode.Position + (vectorToRightTangent * endRadius), beginNode.Position + (vectorToRightTangent * beginRadius))); }
/// <summary> /// Helper function to hit-test the biggest node against hitting contour segments /// </summary> /// <param name="hitContour">a collection of basic segments outlining the hitting contour</param> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <returns>true if hit; false otherwise</returns> private bool HitTestPolygonContourSegments( IEnumerable<ContourSegment> hitContour, StrokeNodeData beginNode, StrokeNodeData endNode) { bool isHit = false; // The bool variable isInside is used here to track that case. It answers to // 'Is ink contour inside if the hitting contour?'. It's initialized to 'true" // and then verified for each edge of the hitting contour until there's a hit or // until it's false. bool isInside = true; Point position; double pressureFactor; if (beginNode.IsEmpty || endNode.PressureFactor > beginNode.PressureFactor) { position = endNode.Position; pressureFactor = endNode.PressureFactor; } else { position = beginNode.Position; pressureFactor = beginNode.PressureFactor; } // Enumerate through the segments of the hitting contour and test them // one by one against the contour of the ink node. foreach (ContourSegment hitSegment in hitContour) { if (hitSegment.IsArc) { // Adjust the arc for the node' pressure factor. Vector hitCenter = hitSegment.Begin + hitSegment.Radius - position; Vector hitRadius = hitSegment.Radius; if (!DoubleUtil.AreClose(pressureFactor, 1d)) { System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false); hitCenter /= pressureFactor; hitRadius /= pressureFactor; } // If the segment is an arc, hit-test against the entire circle the arc is part of. if (true == HitTestPolygonCircle(_vertices, hitCenter, hitRadius)) { isHit = true; break; } // if (isInside && (WhereIsVectorAboutArc( position - hitSegment.Begin - hitSegment.Radius, -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) == HitResult.Hit)) { isInside = false; } } else { // Adjust the segment for the node's pressure factor Vector hitBegin = hitSegment.Begin - position; Vector hitEnd = hitBegin + hitSegment.Vector; if (!DoubleUtil.AreClose(pressureFactor, 1d)) { System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false); hitBegin /= pressureFactor; hitEnd /= pressureFactor; } // Hit-test the node against the segment if (true == HitTestPolygonSegment(_vertices, hitBegin, hitEnd)) { isHit = true; break; } // if (isInside && WhereIsVectorAboutVector( position - hitSegment.Begin, hitSegment.Vector) != HitResult.Right) { isInside = false; } } } return (isInside || isHit); }
/// <summary> /// Helper function to HitTest the the hitting contour against the inking contour /// </summary> /// <param name="hitContour">a collection of basic segments outlining the hitting contour</param> /// <param name="quad">A connecting quad</param> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <returns>true if hit; false otherwise</returns> private bool HitTestInkContour( IEnumerable<ContourSegment> hitContour, Quad quad, StrokeNodeData beginNode, StrokeNodeData endNode) { System.Diagnostics.Debug.Assert(!quad.IsEmpty); bool isHit = false; // When hit-testing a contour against another contour, like in this case, // the default implementation checks whether any edge (segment) of the hitting // contour intersects with the contour of the ink segment. But this doesn't cover // the case when the ink segment is entirely inside of the hitting segment. // The bool variable isInside is used here to track that case. It answers to // 'Is ink contour inside if the hitting contour?'. It's initialized to 'true" // and then verified for each edge of the hitting contour until there's a hit or // until it's false. bool isInside = true; // The ink connecting quad is not empty, enumerate through the segments of the // hitting contour and hit-test them one by one against the ink contour. foreach (ContourSegment hitSegment in hitContour) { // Iterate through the vertices of the contour of the ink segment // check where the hit segment is about them, return false if it's // on the left side off either of the ink contour segments. Vector hitBegin, hitEnd; HitResult hitResult; // Start with the segment quad.C->quad.D if (hitSegment.IsArc) { hitBegin = hitSegment.Begin + hitSegment.Radius - beginNode.Position; hitEnd = hitSegment.Radius; hitResult = WhereIsCircleAboutSegment( hitBegin, hitEnd, quad.C - beginNode.Position, quad.D - beginNode.Position); } else { hitBegin = hitSegment.Begin - beginNode.Position; hitEnd = hitBegin + hitSegment.Vector; hitResult = WhereIsSegmentAboutSegment( hitBegin, hitEnd, quad.C - beginNode.Position, quad.D - beginNode.Position); } if (HitResult.Left == hitResult) { if (isInside) { isInside = hitSegment.IsArc ? (WhereIsVectorAboutArc(-hitBegin, -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) != HitResult.Hit) : (WhereIsVectorAboutVector(-hitBegin, hitSegment.Vector) == HitResult.Right); } // This hitSegment is completely outside of the ink contour, // continue with the next one. continue; } // Continue clockwise from quad.D to quad.A, then to quad.B, ..., quad.C HitResult firstResult = hitResult, lastResult = hitResult; double pressureFactor = beginNode.PressureFactor; // Find the index of the vertex that is quad.D // Use count var to avoid infinite loop, normally this shouldn't // happen but it doesn't hurt to check it just in case. int i = 0, count = _vertices.Length; Vector vertex = new Vector(); for (i = 0; i < count; i++) { vertex = _vertices[i] * pressureFactor; if (DoubleUtil.AreClose((beginNode.Position + vertex), quad.D)) { break; } } System.Diagnostics.Debug.Assert(i < count); int k; for (k = 0; k < 2; k++) { count = _vertices.Length; Point nodePosition = (k == 0) ? beginNode.Position : endNode.Position; Point end = (k == 0) ? quad.A : quad.C; // Iterate over the vertices on // beginNode(k=0)from quad.D to quad.A // or // endNode(k=1)from quad.A to quad.B ... to quad.C while (((nodePosition + vertex) != end) && (count != 0)) { // Find out the next vertex i = (i + 1) % _vertices.Length; Vector nextVertex = _vertices[i] * pressureFactor; // Hit-test the hitting segment against the current edge hitResult = hitSegment.IsArc ? WhereIsCircleAboutSegment(hitBegin, hitEnd, vertex, nextVertex) : WhereIsSegmentAboutSegment(hitBegin, hitEnd, vertex, nextVertex); if (HitResult.Hit == hitResult) { return true; //Got a hit } if (true == IsOutside(hitResult, lastResult)) { // This hitSegment is definitely outside the ink contour, drop it. // Change k to something > 2 to leave the for loop and skip // IsOutside at the bottom k = 3; break; } lastResult = hitResult; vertex = nextVertex; count--; } System.Diagnostics.Debug.Assert(count > 0); if (k == 0) { // Make some adjustments for the second one to continue iterating through // quad.AB and the outer segments of endNode up to quad.C pressureFactor = endNode.PressureFactor; Vector spineVector = endNode.Position - beginNode.Position; vertex -= spineVector; // now vertex = quad.A - spineVector hitBegin -= spineVector; // adjust hitBegin to the space of endNode if (hitSegment.IsArc == false) { hitEnd -= spineVector; } // Find the index of the vertex that is quad.B count = _vertices.Length; while (!DoubleUtil.AreClose((endNode.Position + _vertices[i] * pressureFactor), quad.B) && (count != 0)) { i = (i + 1) % _vertices.Length; count--; } System.Diagnostics.Debug.Assert(count > 0); i--; } } if ((k == 2) && (false == IsOutside(firstResult, hitResult))) { isHit = true; break; } // if (isInside) { isInside = hitSegment.IsArc ? (WhereIsVectorAboutArc(-hitBegin, -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) != HitResult.Hit) : (WhereIsVectorAboutVector(-hitBegin, hitSegment.Vector) == HitResult.Right); } } return (isHit||isInside); }
/// <summary> /// /// </summary> /// <param name="node"></param> /// <param name="quad"></param> /// <returns></returns> internal override IEnumerable<ContourSegment> GetContourSegments(StrokeNodeData node, Quad quad) { System.Diagnostics.Debug.Assert(node.IsEmpty == false); if (quad.IsEmpty) { Point point = node.Position; point.X += _radius; yield return new ContourSegment(point, point, node.Position); } else if (_nodeShapeToCircle.IsIdentity) { yield return new ContourSegment(quad.A, quad.B); yield return new ContourSegment(quad.B, quad.C, node.Position); yield return new ContourSegment(quad.C, quad.D); yield return new ContourSegment(quad.D, quad.A); } }
/// <summary> /// Helper function to Hit-test against the two stroke nodes only (excluding the connecting quad). /// </summary> /// <param name="hitSegment"></param> /// <param name="beginNode"></param> /// <param name="endNode"></param> /// <param name="result"></param> /// <returns></returns> private bool HitTestStrokeNodes( ContourSegment hitSegment, StrokeNodeData beginNode, StrokeNodeData endNode, ref StrokeFIndices result) { // First, find out if hitSegment intersects with either of the ink nodes bool isHit = false; for (int node = 0; node < 2; node++) { Point position; double pressureFactor; if (node == 0) { if (isHit && DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)) { continue; } position = beginNode.Position; pressureFactor = beginNode.PressureFactor; } else { if (isHit && DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { continue; } position = endNode.Position; pressureFactor = endNode.PressureFactor; } Vector hitBegin, hitEnd; // Adjust the segment for the node's pressure factor if (hitSegment.IsArc) { hitBegin = hitSegment.Begin - position + hitSegment.Radius; hitEnd = hitSegment.Radius; } else { hitBegin = hitSegment.Begin - position; hitEnd = hitBegin + hitSegment.Vector; } if (pressureFactor != 1) { System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false); hitBegin /= pressureFactor; hitEnd /= pressureFactor; } // Hit-test the node against the segment if (hitSegment.IsArc ? HitTestPolygonCircle(_vertices, hitBegin, hitEnd) : HitTestPolygonSegment(_vertices, hitBegin, hitEnd)) { isHit = true; if (node == 0) { result.BeginFIndex = StrokeFIndices.BeforeFirst; if (DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast)) { break; } } else { result.EndFIndex = StrokeFIndices.AfterLast; if (beginNode.IsEmpty) { result.BeginFIndex = StrokeFIndices.BeforeFirst; break; } if (DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)) { break; } } } } return isHit; }
/// <summary> /// Hit-tests a stroke segment defined by two nodes against a linear segment. /// </summary> /// <param name="beginNode">Begin node of the stroke segment to hit-test. Can be empty (none)</param> /// <param name="endNode">End node of the stroke segment</param> /// <param name="quad">Pre-computed quadrangle connecting the two nodes. /// Can be empty if the begion node is empty or when one node is entirely inside the other</param> /// <param name="hitBeginPoint">an end point of the hitting linear segment</param> /// <param name="hitEndPoint">an end point of the hitting linear segment</param> /// <returns>true if the hitting segment intersect the contour comprised of the two stroke nodes</returns> internal override bool HitTest( StrokeNodeData beginNode, StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint) { StrokeNodeData bigNode, smallNode; if (beginNode.IsEmpty || (quad.IsEmpty && (endNode.PressureFactor > beginNode.PressureFactor))) { // Need to test one node only bigNode = endNode; smallNode = StrokeNodeData.Empty; } else { // In this case the size doesn't matter. bigNode = beginNode; smallNode = endNode; } // Compute the positions of the involved points relative to bigNode. Vector hitBegin = hitBeginPoint - bigNode.Position; Vector hitEnd = hitEndPoint - bigNode.Position; // If the node shape is an ellipse, transform the scene to turn the shape to a circle if (_nodeShapeToCircle.IsIdentity == false) { hitBegin = _nodeShapeToCircle.Transform(hitBegin); hitEnd = _nodeShapeToCircle.Transform(hitEnd); } bool isHit = false; // Hit-test the big node double bigRadius = _radius * bigNode.PressureFactor; Vector nearest = GetNearest(hitBegin, hitEnd); if (nearest.LengthSquared <= (bigRadius * bigRadius)) { isHit = true; } else if (quad.IsEmpty == false) { // Hit-test the other node Vector spineVector = smallNode.Position - bigNode.Position; if (_nodeShapeToCircle.IsIdentity == false) { spineVector = _nodeShapeToCircle.Transform(spineVector); } double smallRadius = _radius * smallNode.PressureFactor; nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector); if ((nearest.LengthSquared <= (smallRadius * smallRadius)) || HitTestQuadSegment(quad, hitBeginPoint, hitEndPoint)) { isHit = true; } } return isHit; }
/// <summary> /// Returns an enumerator for edges of the contour comprised by a given node /// and its connecting quadrangle. /// Used for hit-testing a stroke against an other stroke (stroke and point erasing) /// </summary> /// <param name="node">node</param> /// <param name="quad">quadrangle connecting the node to the preceeding node</param> /// <returns>contour segments enumerator</returns> internal virtual IEnumerable<ContourSegment> GetContourSegments(StrokeNodeData node, Quad quad) { System.Diagnostics.Debug.Assert(node.IsEmpty == false); if (quad.IsEmpty) { Point vertex = node.Position + (_vertices[_vertices.Length - 1] * node.PressureFactor); for (int i = 0; i < _vertices.Length; i++) { Point nextVertex = node.Position + (_vertices[i] * node.PressureFactor); yield return new ContourSegment(vertex, nextVertex); vertex = nextVertex; } } else { yield return new ContourSegment(quad.A, quad.B); for (int i = 0, count = _vertices.Length; i < count; i++) { Point vertex = node.Position + (_vertices[i] * node.PressureFactor); if (vertex == quad.B) { for (int j = 0; (j < count) && (vertex != quad.C); j++) { i = (i + 1) % count; Point nextVertex = node.Position + (_vertices[i] * node.PressureFactor); yield return new ContourSegment(vertex, nextVertex); vertex = nextVertex; } break; } } yield return new ContourSegment(quad.C, quad.D); yield return new ContourSegment(quad.D, quad.A); } }
/// <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> /// Calculate the clip location /// </summary> /// <param name="hitSegment">the hitting segment</param> /// <param name="beginNode">begin node</param> /// <param name="spineVector"></param> /// <param name="pressureDelta"></param> /// <returns>the clip location. not-clip if return StrokeFIndices.BeforeFirst</returns> private double CalculateClipLocation( ContourSegment hitSegment, StrokeNodeData beginNode, Vector spineVector, double pressureDelta) { double findex = StrokeFIndices.BeforeFirst; bool clipIt = hitSegment.IsArc ? true //? (WhereIsVectorAboutArc(beginNode.Position - hitSegment.Begin - hitSegment.Radius, // -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) == HitResult.Hit) : (WhereIsVectorAboutVector( beginNode.Position - hitSegment.Begin, hitSegment.Vector) == HitResult.Left); if (clipIt) { findex = hitSegment.IsArc ? ClipTestArc(spineVector, pressureDelta, (hitSegment.Begin + hitSegment.Radius - beginNode.Position) / beginNode.PressureFactor, hitSegment.Radius / beginNode.PressureFactor) : ClipTest(spineVector, pressureDelta, (hitSegment.Begin - beginNode.Position) / beginNode.PressureFactor, (hitSegment.End - beginNode.Position) / beginNode.PressureFactor); // NTRAID#WINDOWS-1384646-2005/11/17-WAYNEZEN, // ClipTest returns StrokeFIndices.AfterLast to indicate a false hit test. // But the caller CutTest expects StrokeFIndices.BeforeFirst when there is no hit. if ( findex == StrokeFIndices.AfterLast ) { findex = StrokeFIndices.BeforeFirst; } else { System.Diagnostics.Debug.Assert(findex >= 0 && findex <= 1); } } return findex; }
/// <summary> /// Finds connecting points for a pair of stroke nodes /// </summary> /// <param name="beginNode">a node to connect</param> /// <param name="endNode">another node, next to beginNode</param> /// <returns>connecting quadrangle</returns> internal override Quad GetConnectingQuad(StrokeNodeData beginNode, StrokeNodeData endNode) { if (beginNode.IsEmpty || endNode.IsEmpty || DoubleUtil.AreClose(beginNode.Position, endNode.Position)) { return Quad.Empty; } // Get the vector between the node positions Vector spine = endNode.Position - beginNode.Position; if (_nodeShapeToCircle.IsIdentity == false) { spine = _nodeShapeToCircle.Transform(spine); } double beginRadius = _radius * beginNode.PressureFactor; double endRadius = _radius * endNode.PressureFactor; // Get the vector and the distance between the node positions double distanceSquared = spine.LengthSquared; double delta = endRadius - beginRadius; double deltaSquared = DoubleUtil.IsZero(delta) ? 0 : (delta * delta); if (DoubleUtil.LessThanOrClose(distanceSquared, deltaSquared)) { // One circle is contained within the other return Quad.Empty; } // Thus, at this point, distance > 0, which avoids the DivideByZero error // Also, here, distanceSquared > deltaSquared // Thus, 0 <= rSin < 1 // Get the components of the radius vectors double distance = Math.Sqrt(distanceSquared); spine /= distance; Vector rad = spine; // Turn left double temp = rad.Y; rad.Y = -rad.X; rad.X = temp; Vector vectorToLeftTangent, vectorToRightTangent; double rSinSquared = deltaSquared / distanceSquared; if (DoubleUtil.IsZero(rSinSquared)) { vectorToLeftTangent = rad; vectorToRightTangent = -rad; } else { rad *= Math.Sqrt(1 - rSinSquared); spine *= Math.Sqrt(rSinSquared); if (beginNode.PressureFactor < endNode.PressureFactor) { spine = -spine; } vectorToLeftTangent = spine + rad; vectorToRightTangent = spine - rad; } // Get the common tangent points if (_circleToNodeShape.IsIdentity == false) { vectorToLeftTangent = _circleToNodeShape.Transform(vectorToLeftTangent); vectorToRightTangent = _circleToNodeShape.Transform(vectorToRightTangent); } return new Quad(beginNode.Position + (vectorToLeftTangent * beginRadius), endNode.Position + (vectorToLeftTangent * endRadius), endNode.Position + (vectorToRightTangent * endRadius), beginNode.Position + (vectorToRightTangent * beginRadius)); }
/// <summary> /// Finds connecting points for a pair of stroke nodes (of a polygonal shape) /// </summary> /// <param name="beginNode">a node to connect</param> /// <param name="endNode">another node, next to beginNode</param> /// <returns>connecting quadrangle, that can be empty if one node is inside the other</returns> internal virtual Quad GetConnectingQuad(StrokeNodeData beginNode, StrokeNodeData endNode) { // Return an empty quad if either of the nodes is empty (not a node) // or if both nodes are at the same position. if (beginNode.IsEmpty || endNode.IsEmpty || DoubleUtil.AreClose(beginNode.Position, endNode.Position)) { return Quad.Empty; } // By definition, Quad's vertices (A,B,C,D) are ordered clockwise with points A and D located // on the beginNode and B and C on the endNode. Basically, we're looking for segments AB and CD. // We iterate through the vertices of the beginNode, at each vertex we analyze location of the // connecting segment relative to the node's edges at the vertex, and enforce these rules: // - if the vector of the connecting segment at a vertex V[i] is on the left from vector V[i]V[i+1] // and not on the left from vector V[i-1]V[i], then it's the AB segment of the quad (V[i] == A). // - if the vector of the connecting segment at a vertex V[i] is on the left from vector V[i-1]V[i] // and not on the left from vector V[i]V[i+1], then it's the CD segment of the quad (V[i] == D). // Quad quad = Quad.Empty; bool foundAB = false, foundCD = false; // There's no need to build shapes of the two nodes in order to find their connecting quad. // It's the spine vector between the nodes and their scaling diff (pressure delta) is all // that matters here. Vector spine = endNode.Position - beginNode.Position; double pressureDelta = endNode.PressureFactor - beginNode.PressureFactor; // Iterate through the vertices of the default shape int count = _vertices.Length; for (int i = 0, j = count - 1; i < count; i++, j = ((j + 1) % count)) { // Compute vector of the connecting segment at the vertex [i] Vector connection = spine + _vertices[i] * pressureDelta; if ((pressureDelta != 0) && (connection.X == 0) && (connection.Y == 0)) { // One of the nodes, |----| // as well as the connecting quad, |__ | // is entirely inside the other node. | | | // [i] --> |__|_| return Quad.Empty; } // Find out where this vector is about the node edge [i][i+1] // (The vars names "goingTo" and "comingFrom" refer direction of the line defined // by the connecting vector applied at vertex [i], relative to the contour of the node shape. // Using these terms, (comingFrom != Right && goingTo == Left) corresponds to the segment AB, // and (comingFrom == Right && goingTo != Left) describes the DC. HitResult goingTo = WhereIsVectorAboutVector(connection, _vertices[(i + 1) % count] - _vertices[i]); if (goingTo == HitResult.Left) { if (false == foundAB) { // Find out where the node edge [i-1][i] is about the connecting vector HitResult comingFrom = WhereIsVectorAboutVector(_vertices[i] - _vertices[j], connection); if (HitResult.Right != comingFrom) { foundAB = true; quad.A = beginNode.Position + _vertices[i] * beginNode.PressureFactor; quad.B = endNode.Position + _vertices[i] * endNode.PressureFactor; if (true == foundCD) { // Found all 4 points. Break out from the 'for' loop. break; } } } } else { if (false == foundCD) { // Find out where the node edge [i-1][i] is about the connecting vector HitResult comingFrom = WhereIsVectorAboutVector(_vertices[i] - _vertices[j], connection); if (HitResult.Right == comingFrom) { foundCD = true; quad.C = endNode.Position + _vertices[i] * endNode.PressureFactor; quad.D = beginNode.Position + _vertices[i] * beginNode.PressureFactor; if (true == foundAB) { // Found all 4 points. Break out from the 'for' loop. break; } } } } } if (!foundAB || !foundCD || // (2) ((pressureDelta != 0) && Vector.Determinant(quad.B - quad.A, quad.D - quad.A) == 0)) // (1) { // _____ _______ // One of the nodes, (1) |__ | (2) | ___ | // as well as the connecting quad, | | | | | | | // is entirely inside the other node. |__| | | |__| | // |____| |___ __| return Quad.Empty; } return quad; }