2013-04-08 13:28:04 +00:00
using System ;
using System.Collections.Generic ;
namespace CodeImp.DoomBuilder.Geometry
{
/// <summary>
/// mxd. Tools to work with curves.
/// </summary>
public static class CurveTools
{
//mxd. Ported from Cubic Bezier curve tools by Andy Woodruff (http://cartogrammar.com/source/CubicBezier.as)
//"default" values: z = 0.5, angleFactor = 0.75; if targetSegmentLength <= 0, will return lines
public static Curve CurveThroughPoints ( List < Vector2D > points , float z , float angleFactor , int targetSegmentLength ) {
Curve result = new Curve ( ) ;
// First calculate all the curve control points
// None of this junk will do any good if there are only two points
if ( points . Count > 2 & & targetSegmentLength > 0 ) {
List < List < Vector2D > > controlPts = new List < List < Vector2D > > ( ) ; // An array to store the two control points (of a cubic Bézier curve) for each point
// Make sure z is between 0 and 1 (too messy otherwise)
if ( z < = 0 )
z = 0.1f ;
else if ( z > 1 )
z = 1 ;
// Make sure angleFactor is between 0 and 1
if ( angleFactor < 0 )
angleFactor = 0 ;
else if ( angleFactor > 1 )
angleFactor = 1 ;
// Ordinarily, curve calculations will start with the second point and go through the second-to-last point
int firstPt = 1 ;
int lastPt = points . Count - 1 ;
// Check if this is a closed line (the first and last points are the same)
if ( points [ 0 ] . x = = points [ points . Count - 1 ] . x & & points [ 0 ] . y = = points [ points . Count - 1 ] . y ) {
// Include first and last points in curve calculations
firstPt = 0 ;
lastPt = points . Count ;
} else {
controlPts . Add ( new List < Vector2D > ( ) ) ; //add a dummy entry
}
// Loop through all the points (except the first and last if not a closed line) to get curve control points for each.
for ( int i = firstPt ; i < lastPt ; i + + ) {
// The previous, current, and next points
Vector2D p0 = ( i - 1 < 0 ) ? points [ points . Count - 2 ] : points [ i - 1 ] ; // If the first point (of a closed line), use the second-to-last point as the previous point
Vector2D p1 = points [ i ] ;
Vector2D p2 = ( i + 1 = = points . Count ) ? points [ 1 ] : points [ i + 1 ] ; // If the last point (of a closed line), use the second point as the next point
float a = Vector2D . Distance ( p0 , p1 ) ; // Distance from previous point to current point
if ( a < 0.001 )
a = 0.001f ; // Correct for near-zero distances, a cheap way to prevent division by zero
float b = Vector2D . Distance ( p1 , p2 ) ; // Distance from current point to next point
if ( b < 0.001 )
b = 0.001f ;
float c = Vector2D . Distance ( p0 , p2 ) ; // Distance from previous point to next point
if ( c < 0.001 )
c = 0.001f ;
float cos = ( b * b + a * a - c * c ) / ( 2 * b * a ) ;
// Make sure above value is between -1 and 1 so that Math.acos will work
if ( cos < - 1 )
cos = - 1 ;
else if ( cos > 1 )
cos = 1 ;
float C = ( float ) Math . Acos ( cos ) ; // Angle formed by the two sides of the triangle (described by the three points above) adjacent to the current point
// Duplicate set of points. Start by giving previous and next points values RELATIVE to the current point.
Vector2D aPt = new Vector2D ( p0 . x - p1 . x , p0 . y - p1 . y ) ;
Vector2D bPt = new Vector2D ( p1 . x , p1 . y ) ;
Vector2D cPt = new Vector2D ( p2 . x - p1 . x , p2 . y - p1 . y ) ;
/ *
We ' ll be adding adding the vectors from the previous and next points to the current point ,
but we don ' t want differing magnitudes ( i . e . line segment lengths ) to affect the direction
of the new vector . Therefore we make sure the segments we use , based on the duplicate points
created above , are of equal length . The angle of the new vector will thus bisect angle C
( defined above ) and the perpendicular to this is nice for the line tangent to the curve .
The curve control points will be along that tangent line .
* /
if ( a > b )
aPt = aPt . GetNormal ( ) * b ; // Scale the segment to aPt (bPt to aPt) to the size of b (bPt to cPt) if b is shorter.
else if ( b > a )
cPt = cPt . GetNormal ( ) * a ; // Scale the segment to cPt (bPt to cPt) to the size of a (aPt to bPt) if a is shorter.
// Offset aPt and cPt by the current point to get them back to their absolute position.
aPt + = p1 ;
cPt + = p1 ;
// Get the sum of the two vectors, which is perpendicular to the line along which our curve control points will lie.
float ax = bPt . x - aPt . x ; // x component of the segment from previous to current point
float ay = bPt . y - aPt . y ;
float bx = bPt . x - cPt . x ; // x component of the segment from next to current point
float by = bPt . y - cPt . y ;
float rx = ax + bx ; // sum of x components
float ry = ay + by ;
// Correct for three points in a line by finding the angle between just two of them
if ( rx = = 0 & & ry = = 0 ) {
rx = - bx ; // Really not sure why this seems to have to be negative
ry = by ;
}
// Switch rx and ry when y or x difference is 0. This seems to prevent the angle from being perpendicular to what it should be.
if ( ay = = 0 & & by = = 0 ) {
rx = 0 ;
ry = 1 ;
} else if ( ax = = 0 & & bx = = 0 ) {
rx = 1 ;
ry = 0 ;
}
2013-07-19 15:30:58 +00:00
//float r = (float)Math.Sqrt(rx * rx + ry * ry); // length of the summed vector - not being used, but there it is anyway
2013-04-08 13:28:04 +00:00
float theta = ( float ) Math . Atan2 ( ry , rx ) ; // angle of the new vector
float controlDist = Math . Min ( a , b ) * z ; // Distance of curve control points from current point: a fraction the length of the shorter adjacent triangle side
2013-04-11 09:27:16 +00:00
float controlScaleFactor = C / Angle2D . PI ; // Scale the distance based on the acuteness of the angle. Prevents big loops around long, sharp-angled triangles.
2013-04-08 13:28:04 +00:00
controlDist * = ( ( 1 - angleFactor ) + angleFactor * controlScaleFactor ) ; // Mess with this for some fine-tuning
2013-04-11 09:27:16 +00:00
float controlAngle = theta + Angle2D . PIHALF ; // The angle from the current point to control points: the new vector angle plus 90 degrees (tangent to the curve).
2013-04-08 13:28:04 +00:00
Vector2D controlPoint2 = new Vector2D ( controlDist , 0 ) ;
Vector2D controlPoint1 = new Vector2D ( controlDist , 0 ) ;
controlPoint2 = controlPoint2 . GetRotated ( controlAngle ) ;
2013-04-11 09:27:16 +00:00
controlPoint1 = controlPoint1 . GetRotated ( controlAngle + Angle2D . PI ) ;
2013-04-08 13:28:04 +00:00
// Offset control points to put them in the correct absolute position
controlPoint1 + = p1 ;
controlPoint2 + = p1 ;
/ *
Haven ' t quite worked out how this happens , but some control points will be reversed .
In this case controlPoint2 will be farther from the next point than controlPoint1 is .
Check for that and switch them if it ' s true .
* /
if ( Vector2D . Distance ( controlPoint2 , p2 ) > Vector2D . Distance ( controlPoint1 , p2 ) )
controlPts . Add ( new List < Vector2D > ( ) { controlPoint2 , controlPoint1 } ) ;
else
controlPts . Add ( new List < Vector2D > ( ) { controlPoint1 , controlPoint2 } ) ;
}
// If this isn't a closed line, draw a regular quadratic Bézier curve from the first to second points, using the first control point of the second point
if ( firstPt = = 1 ) {
float length = ( points [ 1 ] - points [ 0 ] ) . GetLength ( ) ;
int numSteps = Math . Max ( 1 , ( int ) Math . Round ( length / targetSegmentLength ) ) ;
CurveSegment segment = new CurveSegment ( ) ;
segment . Start = points [ 0 ] ;
segment . CPMid = controlPts [ 1 ] [ 0 ] ;
segment . End = points [ 1 ] ;
CreateQuadraticCurve ( segment , numSteps ) ;
result . Segments . Add ( segment ) ;
}
// Loop through points to draw cubic Bézier curves through the penultimate point, or through the last point if the line is closed.
for ( int i = firstPt ; i < lastPt - 1 ; i + + ) {
float length = ( points [ i + 1 ] - points [ i ] ) . GetLength ( ) ;
int numSteps = Math . Max ( 1 , ( int ) Math . Round ( length / targetSegmentLength ) ) ;
CurveSegment segment = new CurveSegment ( ) ;
segment . CPStart = controlPts [ i ] [ 1 ] ;
segment . CPEnd = controlPts [ i + 1 ] [ 0 ] ;
segment . Start = points [ i ] ;
segment . End = points [ i + 1 ] ;
CreateCubicCurve ( segment , numSteps ) ;
result . Segments . Add ( segment ) ;
}
// If this isn't a closed line, curve to the last point using the second control point of the penultimate point.
if ( lastPt = = points . Count - 1 ) {
float length = ( points [ lastPt ] - points [ lastPt - 1 ] ) . GetLength ( ) ;
int numSteps = Math . Max ( 1 , ( int ) Math . Round ( length / targetSegmentLength ) ) ;
CurveSegment segment = new CurveSegment ( ) ;
segment . Start = points [ lastPt - 1 ] ;
segment . CPMid = controlPts [ lastPt - 1 ] [ 1 ] ;
segment . End = points [ lastPt ] ;
CreateQuadraticCurve ( segment , numSteps ) ;
result . Segments . Add ( segment ) ;
}
// create lines
} else if ( points . Count > = 2 ) {
for ( int i = 0 ; i < points . Count - 1 ; i + + ) {
CurveSegment segment = new CurveSegment ( ) ;
segment . Start = points [ i ] ;
segment . End = points [ i + 1 ] ;
segment . Points = new Vector2D [ ] { segment . Start , segment . End } ;
segment . UpdateLength ( ) ;
result . Segments . Add ( segment ) ;
}
}
result . UpdateShape ( ) ;
return result ;
}
public static void CreateQuadraticCurve ( CurveSegment segment , int steps ) {
segment . CurveType = CurveSegmentType . QUADRATIC ;
segment . Points = GetQuadraticCurve ( segment . Start , segment . CPMid , segment . End , steps ) ;
segment . UpdateLength ( ) ;
}
//this returns array of Vector2D to draw 3-point bezier curve
public static Vector2D [ ] GetQuadraticCurve ( Vector2D p1 , Vector2D p2 , Vector2D p3 , int steps ) {
if ( steps < 0 )
return null ;
int totalSteps = steps + 1 ;
Vector2D [ ] points = new Vector2D [ totalSteps ] ;
float step = 1f / ( float ) steps ;
float curStep = 0f ;
for ( int i = 0 ; i < totalSteps ; i + + ) {
points [ i ] = GetPointOnQuadraticCurve ( p1 , p2 , p3 , curStep ) ;
curStep + = step ;
}
return points ;
}
public static void CreateCubicCurve ( CurveSegment segment , int steps ) {
segment . CurveType = CurveSegmentType . CUBIC ;
segment . Points = GetCubicCurve ( segment . Start , segment . End , segment . CPStart , segment . CPEnd , steps ) ;
segment . UpdateLength ( ) ;
}
//this returns array of Vector2D to draw 4-point bezier curve
public static Vector2D [ ] GetCubicCurve ( Vector2D p1 , Vector2D p2 , Vector2D cp1 , Vector2D cp2 , int steps ) {
if ( steps < 0 )
return null ;
int totalSteps = steps + 1 ;
Vector2D [ ] points = new Vector2D [ totalSteps ] ;
float step = 1f / ( float ) steps ;
float curStep = 0f ;
for ( int i = 0 ; i < totalSteps ; i + + ) {
points [ i ] = GetPointOnCubicCurve ( p1 , p2 , cp1 , cp2 , curStep ) ;
curStep + = step ;
}
return points ;
}
public static Vector2D GetPointOnCurve ( CurveSegment segment , float delta ) {
if ( segment . CurveType = = CurveSegmentType . QUADRATIC )
return GetPointOnQuadraticCurve ( segment . Start , segment . CPMid , segment . End , delta ) ;
if ( segment . CurveType = = CurveSegmentType . CUBIC )
return GetPointOnCubicCurve ( segment . Start , segment . End , segment . CPStart , segment . CPEnd , delta ) ;
if ( segment . CurveType = = CurveSegmentType . LINE )
return GetPointOnLine ( segment . Start , segment . End , delta ) ;
throw new Exception ( "GetPointOnCurve: got unknown curve type: " + segment . CurveType ) ;
}
public static Vector2D GetPointOnQuadraticCurve ( Vector2D p1 , Vector2D p2 , Vector2D p3 , float delta ) {
float invDelta = 1f - delta ;
float m1 = invDelta * invDelta ;
float m2 = 2 * invDelta * delta ;
float m3 = delta * delta ;
int px = ( int ) ( m1 * p1 . x + m2 * p2 . x + m3 * p3 . x ) ;
int py = ( int ) ( m1 * p1 . y + m2 * p2 . y + m3 * p3 . y ) ;
return new Vector2D ( px , py ) ;
}
public static Vector2D GetPointOnCubicCurve ( Vector2D p1 , Vector2D p2 , Vector2D cp1 , Vector2D cp2 , float delta ) {
float invDelta = 1f - delta ;
float m1 = invDelta * invDelta * invDelta ;
float m2 = 3 * delta * invDelta * invDelta ;
float m3 = 3 * delta * delta * invDelta ;
float m4 = delta * delta * delta ;
2013-09-27 10:56:44 +00:00
int px = ( int ) Math . Round ( m1 * p1 . x + m2 * cp1 . x + m3 * cp2 . x + m4 * p2 . x ) ;
int py = ( int ) Math . Round ( m1 * p1 . y + m2 * cp1 . y + m3 * cp2 . y + m4 * p2 . y ) ;
2013-04-08 13:28:04 +00:00
return new Vector2D ( px , py ) ;
}
//it's basically 2-point bezier curve
public static Vector2D GetPointOnLine ( Vector2D p1 , Vector2D p2 , float delta ) {
return new Vector2D ( ( int ) ( ( 1f - delta ) * p1 . x + delta * p2 . x ) , ( int ) ( ( 1f - delta ) * p1 . y + delta * p2 . y ) ) ;
}
}
public class Curve
{
public List < CurveSegment > Segments ;
public List < Vector2D > Shape ;
public float Length ;
public Curve ( ) {
Segments = new List < CurveSegment > ( ) ;
}
public void UpdateShape ( ) {
Shape = new List < Vector2D > ( ) ;
Length = 0 ;
foreach ( CurveSegment segment in Segments ) {
Length + = segment . Length ;
foreach ( Vector2D point in segment . Points ) {
if ( Shape . Count = = 0 | | point ! = Shape [ Shape . Count - 1 ] )
Shape . Add ( point ) ;
}
}
float curDelta = 0 ;
for ( int i = 0 ; i < Segments . Count ; i + + ) {
Segments [ i ] . Delta = Segments [ i ] . Length / Length ;
curDelta + = Segments [ i ] . Delta ;
Segments [ i ] . GlobalDelta = curDelta ;
}
}
}
public class CurveSegment
{
public Vector2D [ ] Points ;
public Vector2D Start ;
public Vector2D End ;
public Vector2D CPStart ;
public Vector2D CPMid ;
public Vector2D CPEnd ;
public float Length ;
public float Delta ; //length of this segment / total curve length
public float GlobalDelta ; //length of this segment / total curve length + deltas of previous segments
public CurveSegmentType CurveType ;
public void UpdateLength ( ) {
if ( Points . Length < 2 )
return ;
Length = 0 ;
for ( int i = 1 ; i < Points . Length ; i + + )
Length + = Vector2D . Distance ( Points [ i ] , Points [ i - 1 ] ) ;
}
}
public enum CurveSegmentType
{
LINE ,
QUADRATIC ,
CUBIC ,
}
}