mirror of
https://git.do.srb2.org/STJr/ZoneBuilder.git
synced 2024-11-14 16:41:26 +00:00
376 lines
13 KiB
C#
376 lines
13 KiB
C#
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;
|
|
}
|
|
|
|
//float r = (float)Math.Sqrt(rx * rx + ry * ry); // length of the summed vector - not being used, but there it is anyway
|
|
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
|
|
float controlScaleFactor = C / Angle2D.PI; // Scale the distance based on the acuteness of the angle. Prevents big loops around long, sharp-angled triangles.
|
|
controlDist *= ((1 - angleFactor) + angleFactor * controlScaleFactor); // Mess with this for some fine-tuning
|
|
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).
|
|
|
|
Vector2D controlPoint2 = new Vector2D(controlDist, 0);
|
|
Vector2D controlPoint1 = new Vector2D(controlDist, 0);
|
|
controlPoint2 = controlPoint2.GetRotated(controlAngle);
|
|
controlPoint1 = controlPoint1.GetRotated(controlAngle + Angle2D.PI);
|
|
|
|
// 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[] { 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 / 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 / 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;
|
|
|
|
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);
|
|
|
|
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,
|
|
}
|
|
}
|