From 818a485ec28e87077380b65ff9f97435e6aacda6 Mon Sep 17 00:00:00 2001
From: MaxED <j.maxed@gmail.com>
Date: Mon, 8 Apr 2013 13:28:04 +0000
Subject: [PATCH] Added Draw Curve mode.

---
 Source/Core/Builder.csproj                    |   1 +
 Source/Core/Geometry/CurveTools.cs            | 359 ++++++++++++++++++
 .../Plugins/BuilderModes/BuilderModes.csproj  |   4 +
 .../ClassicModes/DrawCurveMode.cs             | 246 ++++++++++++
 .../BuilderModes/General/BuilderPlug.cs       |  13 +
 .../Properties/Resources.Designer.cs          |  31 +-
 .../BuilderModes/Properties/Resources.resx    |   3 +
 .../BuilderModes/Resources/DrawCurveMode.png  | Bin 0 -> 3092 bytes
 8 files changed, 645 insertions(+), 12 deletions(-)
 create mode 100644 Source/Core/Geometry/CurveTools.cs
 create mode 100644 Source/Plugins/BuilderModes/ClassicModes/DrawCurveMode.cs
 create mode 100644 Source/Plugins/BuilderModes/Resources/DrawCurveMode.png

diff --git a/Source/Core/Builder.csproj b/Source/Core/Builder.csproj
index e63f973a3..be8fb4f9e 100644
--- a/Source/Core/Builder.csproj
+++ b/Source/Core/Builder.csproj
@@ -696,6 +696,7 @@
     <Compile Include="General\ErrorItem.cs" />
     <Compile Include="General\ErrorLogger.cs" />
     <Compile Include="General\SavePurpose.cs" />
+    <Compile Include="Geometry\CurveTools.cs" />
     <Compile Include="GZBuilder\Controls\CustomLinedefColorProperties.cs">
       <SubType>UserControl</SubType>
     </Compile>
diff --git a/Source/Core/Geometry/CurveTools.cs b/Source/Core/Geometry/CurveTools.cs
new file mode 100644
index 000000000..9279e2ab0
--- /dev/null
+++ b/Source/Core/Geometry/CurveTools.cs
@@ -0,0 +1,359 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+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 / (float)Math.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 + (float)Math.PI / 2;	// 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 + (float)Math.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 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;
+
+			int px = (int)(m1 * p1.x + m2 * cp1.x + m3 * cp2.x + m4 * p2.x);
+			int py = (int)(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,
+	}
+}
diff --git a/Source/Plugins/BuilderModes/BuilderModes.csproj b/Source/Plugins/BuilderModes/BuilderModes.csproj
index c421d53aa..a5bd14c1a 100644
--- a/Source/Plugins/BuilderModes/BuilderModes.csproj
+++ b/Source/Plugins/BuilderModes/BuilderModes.csproj
@@ -226,6 +226,7 @@
   <ItemGroup>
     <Compile Include="ClassicModes\BridgeMode.cs" />
     <Compile Include="ClassicModes\CeilingAlignMode.cs" />
+    <Compile Include="ClassicModes\DrawCurveMode.cs" />
     <Compile Include="ClassicModes\DrawEllipseMode.cs" />
     <Compile Include="ClassicModes\DrawRectangleMode.cs" />
     <Compile Include="ClassicModes\FlatAlignMode.cs" />
@@ -396,6 +397,9 @@
   <ItemGroup>
     <None Include="Resources\AlignThings.png" />
   </ItemGroup>
+  <ItemGroup>
+    <None Include="Resources\DrawCurveMode.png" />
+  </ItemGroup>
   <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.
diff --git a/Source/Plugins/BuilderModes/ClassicModes/DrawCurveMode.cs b/Source/Plugins/BuilderModes/ClassicModes/DrawCurveMode.cs
new file mode 100644
index 000000000..3576062cd
--- /dev/null
+++ b/Source/Plugins/BuilderModes/ClassicModes/DrawCurveMode.cs
@@ -0,0 +1,246 @@
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Text;
+using System.Windows.Forms;
+using CodeImp.DoomBuilder.Actions;
+using CodeImp.DoomBuilder.Editing;
+using CodeImp.DoomBuilder.Geometry;
+using CodeImp.DoomBuilder.Map;
+using CodeImp.DoomBuilder.Rendering;
+using CodeImp.DoomBuilder.Windows;
+
+namespace CodeImp.DoomBuilder.BuilderModes.ClassicModes
+{
+	[EditMode(DisplayName = "Draw Curve Mode",
+			  SwitchAction = "drawcurvemode",
+			  AllowCopyPaste = false,
+			  Volatile = true,
+			  Optional = false)]
+
+	public class DrawCurveMode : DrawGeometryMode
+	{
+		private HintLabel hintLabel;
+		private Curve curve;
+		private static int segmentLength = 32;
+		private int minSegmentLength = 16;
+		private int maxSegmentLength = 4096; //just some arbitrary number
+
+		public DrawCurveMode() : base() {
+			hintLabel = new HintLabel();
+		}
+
+		public override void Dispose() {
+			if(!isdisposed && hintLabel != null)
+				hintLabel.Dispose();
+
+			base.Dispose();
+		}
+
+		protected override void Update() {
+			PixelColor stitchcolor = General.Colors.Highlight;
+			PixelColor losecolor = General.Colors.Selection;
+			PixelColor color;
+
+			snaptogrid = General.Interface.ShiftState ^ General.Interface.SnapToGrid;
+			snaptonearest = General.Interface.CtrlState ^ General.Interface.AutoMerge;
+
+			DrawnVertex curp = GetCurrentPosition();
+			float vsize = ((float)renderer.VertexSize + 1.0f) / renderer.Scale;
+			float vsizeborder = ((float)renderer.VertexSize + 3.0f) / renderer.Scale;
+
+			// The last label's end must go to the mouse cursor
+			if(labels.Count > 0)
+				labels[labels.Count - 1].End = curp.pos;
+
+			// Render drawing lines
+			if(renderer.StartOverlay(true)) {
+				// Go for all points to draw lines
+				if(points.Count > 0) {
+					//update curve
+					List<Vector2D> verts = new List<Vector2D>();
+					
+					for(int i = 0; i < points.Count; i++)
+						verts.Add(points[i].pos);
+
+					if(curp.pos != verts[verts.Count-1])
+						verts.Add(curp.pos);
+
+					curve = CurveTools.CurveThroughPoints(verts, 0.5f, 0.75f, segmentLength);
+
+					// Render lines
+					for(int i = 1; i < curve.Shape.Count; i++) {
+						// Determine line color
+						color = snaptonearest ? stitchcolor : losecolor;
+
+						// Render line
+						renderer.RenderLine(curve.Shape[i - 1], curve.Shape[i], LINE_THICKNESS, color, true);
+					}
+
+					//render "inactive" vertices
+					for(int i = 1; i < curve.Shape.Count - 1; i++) {
+						// Determine vertex color
+						color = !snaptonearest ? stitchcolor : losecolor;
+
+						// Render vertex
+						renderer.RenderRectangleFilled(new RectangleF(curve.Shape[i].x - vsize, curve.Shape[i].y - vsize, vsize * 2.0f, vsize * 2.0f), color, true);
+					}
+				}
+
+				if(points.Count > 0) {
+					// Render vertices
+					for(int i = 0; i < points.Count; i++) {
+						// Determine vertex color
+						color = points[i].stitch ? stitchcolor : losecolor;
+
+						// Render vertex
+						renderer.RenderRectangleFilled(new RectangleF(points[i].pos.x - vsize, points[i].pos.y - vsize, vsize * 2.0f, vsize * 2.0f), color, true);
+					}
+				}
+
+				// Determine point color
+				color = snaptonearest ? stitchcolor : losecolor;
+
+				// Render vertex at cursor
+				renderer.RenderRectangleFilled(new RectangleF(curp.pos.x - vsize, curp.pos.y - vsize, vsize * 2.0f, vsize * 2.0f), color, true);
+
+				// Go for all labels
+				foreach(LineLengthLabel l in labels)
+					renderer.RenderText(l.TextLabel);
+
+				//Render info label
+				hintLabel.Start = new Vector2D(mousemappos.x + (32 / renderer.Scale), mousemappos.y - (16 / renderer.Scale));
+				hintLabel.End = new Vector2D(mousemappos.x + (96 / renderer.Scale), mousemappos.y);
+				hintLabel.Text = "SEG LEN: " + segmentLength;
+				renderer.RenderText(hintLabel.TextLabel);
+
+				// Done
+				renderer.Finish();
+			}
+
+			// Done
+			renderer.Present();
+		}
+
+		public override void OnAccept() {
+			Cursor.Current = Cursors.AppStarting;
+
+			General.Settings.FindDefaultDrawSettings();
+
+			// When points have been drawn
+			if(points.Count > 0) {
+				// Make undo for the draw
+				General.Map.UndoRedo.CreateUndo("Curve draw");
+
+				// Make an analysis and show info
+				string[] adjectives = new string[]
+				{ "beautiful", "lovely", "romantic", "stylish", "cheerful", "comical",
+				  "awesome", "accurate", "adorable", "adventurous", "attractive", "cute",
+				  "elegant", "glamorous", "gorgeous", "handsome", "magnificent", "unusual",
+				  "outstanding", "mysterious", "amusing", "charming", "fantastic", "jolly" };
+				string word = adjectives[points.Count % adjectives.Length];
+				word = (points.Count > adjectives.Length) ? "very " + word : word;
+				string a = ((word[0] == 'a') || (word[0] == 'e') || (word[0] == 'o')) ? "an " : "a ";
+				General.Interface.DisplayStatus(StatusType.Action, "Created " + a + word + " drawing.");
+
+				List<DrawnVertex> verts = new List<DrawnVertex>();
+				
+				//if we have a curve...
+				if(points.Count > 2){
+					//is it a closed curve?
+					int lastPoint = 0;
+					if(points[0].pos == points[points.Count - 1].pos) {
+						lastPoint = curve.Segments.Count;
+					} else {
+						lastPoint = curve.Segments.Count - 1;
+					}
+
+					for(int i = 0; i < lastPoint; i++) {
+						int next = (i == curve.Segments.Count - 1 ? 0 : i + 1);
+						bool stitch = points[i].stitch && points[next].stitch;
+						bool stitchline = points[i].stitchline && points[next].stitchline;
+
+						//add segment points except the last one
+						for(int c = 0; c < curve.Segments[i].Points.Length - 1; c++) {
+							DrawnVertex dv = new DrawnVertex();
+							dv.pos = curve.Segments[i].Points[c];
+							dv.stitch = stitch;
+							dv.stitchline = stitchline;
+							verts.Add(dv);
+						}
+					}
+
+					//add last point
+					DrawnVertex end = new DrawnVertex();
+					end.pos = curve.Segments[lastPoint - 1].End;
+					end.stitch = verts[verts.Count - 1].stitch;
+					end.stitchline = verts[verts.Count - 1].stitchline;
+					verts.Add(end);
+				}else{
+					verts = points;
+				}
+
+				// Make the drawing
+				if(!Tools.DrawLines(verts, BuilderPlug.Me.AutoAlignTextureOffsetsOnCreate)) //mxd
+				{
+					// Drawing failed
+					// NOTE: I have to call this twice, because the first time only cancels this volatile mode
+					General.Map.UndoRedo.WithdrawUndo();
+					General.Map.UndoRedo.WithdrawUndo();
+					return;
+				}
+
+				// Snap to map format accuracy
+				General.Map.Map.SnapAllToAccuracy();
+
+				// Clear selection
+				General.Map.Map.ClearAllSelected();
+
+				// Update cached values
+				General.Map.Map.Update();
+
+				// Edit new sectors?
+				List<Sector> newsectors = General.Map.Map.GetMarkedSectors(true);
+				if(BuilderPlug.Me.EditNewSector && (newsectors.Count > 0))
+					General.Interface.ShowEditSectors(newsectors);
+
+				// Update the used textures
+				General.Map.Data.UpdateUsedTextures();
+
+				// Map is changed
+				General.Map.IsChanged = true;
+			}
+
+			// Done
+			Cursor.Current = Cursors.Default;
+
+			// Return to original mode
+			General.Editing.ChangeMode(General.Editing.PreviousStableMode.Name);
+		}
+
+		//ACTIONS
+		[BeginAction("increasesubdivlevel")]
+		protected virtual void increaseSubdivLevel() {
+			if(segmentLength < maxSegmentLength) {
+				int increment = Math.Max(minSegmentLength, segmentLength / 32 * 16);
+				segmentLength += increment;
+
+				if(segmentLength > maxSegmentLength)
+					segmentLength = maxSegmentLength;
+				Update();
+			}
+		}
+
+		[BeginAction("decreasesubdivlevel")]
+		protected virtual void decreaseSubdivLevel() {
+			if(segmentLength > minSegmentLength) {
+				int increment = Math.Max(minSegmentLength, segmentLength / 32 * 16);
+				segmentLength -= increment;
+
+				if(segmentLength < minSegmentLength)
+					segmentLength = minSegmentLength;
+				Update();
+			}
+		}
+	}
+}
diff --git a/Source/Plugins/BuilderModes/General/BuilderPlug.cs b/Source/Plugins/BuilderModes/General/BuilderPlug.cs
index b444ced00..4c5ad1b47 100644
--- a/Source/Plugins/BuilderModes/General/BuilderPlug.cs
+++ b/Source/Plugins/BuilderModes/General/BuilderPlug.cs
@@ -85,6 +85,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
         private ToolStripMenuItem drawLinesModeMenuItem;
         private ToolStripMenuItem drawRectModeMenuItem;
         private ToolStripMenuItem drawEllipseModeMenuItem;
+		private ToolStripMenuItem drawCurveModeMenuItem;
 		
 		// Settings
 		private int showvisualthings;			// 0 = none, 1 = sprite only, 2 = sprite caged
@@ -249,6 +250,14 @@ namespace CodeImp.DoomBuilder.BuilderModes
             drawRectModeMenuItem.Enabled = false;
             General.Interface.AddMenu(drawRectModeMenuItem, MenuSection.ModeDrawModes);
 
+			//draw curve 
+			drawCurveModeMenuItem = new ToolStripMenuItem("Draw Curve");
+			drawCurveModeMenuItem.Tag = "drawcurvemode";
+			drawCurveModeMenuItem.Click += new EventHandler(InvokeTaggedAction);
+			drawCurveModeMenuItem.Image = CodeImp.DoomBuilder.BuilderModes.Properties.Resources.DrawCurveMode;
+			drawCurveModeMenuItem.Enabled = false;
+			General.Interface.AddMenu(drawCurveModeMenuItem, MenuSection.ModeDrawModes);
+
             //draw lines 
             drawLinesModeMenuItem = new ToolStripMenuItem("Draw Lines");
             drawLinesModeMenuItem.Tag = "drawlinesmode";
@@ -274,6 +283,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
 				General.Interface.RemoveMenu(exportToObjMenuItem);
                 General.Interface.RemoveMenu(snapModeMenuItem);
                 General.Interface.RemoveMenu(drawLinesModeMenuItem);
+				General.Interface.RemoveMenu(drawCurveModeMenuItem);
                 General.Interface.RemoveMenu(drawRectModeMenuItem);
                 General.Interface.RemoveMenu(drawEllipseModeMenuItem);
 
@@ -439,6 +449,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
 			exportToObjMenuItem.Enabled = true;
             snapModeMenuItem.Enabled = true;
             drawLinesModeMenuItem.Enabled = true;
+			drawCurveModeMenuItem.Enabled = true;
             drawRectModeMenuItem.Enabled = true;
             drawEllipseModeMenuItem.Enabled = true;
 		}
@@ -454,6 +465,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
 			exportToObjMenuItem.Enabled = true;
             snapModeMenuItem.Enabled = true;
             drawLinesModeMenuItem.Enabled = true;
+			drawCurveModeMenuItem.Enabled = true;
             drawRectModeMenuItem.Enabled = true;
             drawEllipseModeMenuItem.Enabled = true;
 		}
@@ -468,6 +480,7 @@ namespace CodeImp.DoomBuilder.BuilderModes
 			exportToObjMenuItem.Enabled = false;
             snapModeMenuItem.Enabled = false;
             drawLinesModeMenuItem.Enabled = false;
+			drawCurveModeMenuItem.Enabled = false;
             drawRectModeMenuItem.Enabled = false;
             drawEllipseModeMenuItem.Enabled = false;
 		}
diff --git a/Source/Plugins/BuilderModes/Properties/Resources.Designer.cs b/Source/Plugins/BuilderModes/Properties/Resources.Designer.cs
index 79a7fc5a9..6d00c21a3 100644
--- a/Source/Plugins/BuilderModes/Properties/Resources.Designer.cs
+++ b/Source/Plugins/BuilderModes/Properties/Resources.Designer.cs
@@ -1,10 +1,10 @@
 //------------------------------------------------------------------------------
 // <auto-generated>
-//     This code was generated by a tool.
-//     Runtime Version:2.0.50727.5466
+//     Этот код создан программой.
+//     Исполняемая версия:2.0.50727.4927
 //
-//     Changes to this file may cause incorrect behavior and will be lost if
-//     the code is regenerated.
+//     Изменения в этом файле могут привести к неправильной работе и будут потеряны в случае
+//     повторной генерации кода.
 // </auto-generated>
 //------------------------------------------------------------------------------
 
@@ -13,12 +13,12 @@ namespace CodeImp.DoomBuilder.BuilderModes.Properties {
     
     
     /// <summary>
-    ///   A strongly-typed resource class, for looking up localized strings, etc.
+    ///   Класс ресурса со строгой типизацией для поиска локализованных строк и т.д.
     /// </summary>
-    // This class was auto-generated by the StronglyTypedResourceBuilder
-    // class via a tool like ResGen or Visual Studio.
-    // To add or remove a member, edit your .ResX file then rerun ResGen
-    // with the /str option, or rebuild your VS project.
+    // Этот класс создан автоматически классом StronglyTypedResourceBuilder
+    // с помощью такого средства, как ResGen или Visual Studio.
+    // Чтобы добавить или удалить член, измените файл .ResX и снова запустите ResGen
+    // с параметром /str или перестройте свой проект VS.
     [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")]
     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
@@ -33,7 +33,7 @@ namespace CodeImp.DoomBuilder.BuilderModes.Properties {
         }
         
         /// <summary>
-        ///   Returns the cached ResourceManager instance used by this class.
+        ///   Возвращает кэшированный экземпляр ResourceManager, использованный этим классом.
         /// </summary>
         [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
         internal static global::System.Resources.ResourceManager ResourceManager {
@@ -47,8 +47,8 @@ namespace CodeImp.DoomBuilder.BuilderModes.Properties {
         }
         
         /// <summary>
-        ///   Overrides the current thread's CurrentUICulture property for all
-        ///   resource lookups using this strongly typed resource class.
+        ///   Перезаписывает свойство CurrentUICulture текущего потока для всех
+        ///   обращений к ресурсу с помощью этого класса ресурса со строгой типизацией.
         /// </summary>
         [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
         internal static global::System.Globalization.CultureInfo Culture {
@@ -116,6 +116,13 @@ namespace CodeImp.DoomBuilder.BuilderModes.Properties {
             }
         }
         
+        internal static System.Drawing.Bitmap DrawCurveMode {
+            get {
+                object obj = ResourceManager.GetObject("DrawCurveMode", resourceCulture);
+                return ((System.Drawing.Bitmap)(obj));
+            }
+        }
+        
         internal static System.Drawing.Bitmap DrawEllipseMode {
             get {
                 object obj = ResourceManager.GetObject("DrawEllipseMode", resourceCulture);
diff --git a/Source/Plugins/BuilderModes/Properties/Resources.resx b/Source/Plugins/BuilderModes/Properties/Resources.resx
index 459ae28d8..bc0b49ea4 100644
--- a/Source/Plugins/BuilderModes/Properties/Resources.resx
+++ b/Source/Plugins/BuilderModes/Properties/Resources.resx
@@ -190,4 +190,7 @@
   <data name="AlignThings" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>..\Resources\AlignThings.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
   </data>
+  <data name="DrawCurveMode" type="System.Resources.ResXFileRef, System.Windows.Forms">
+    <value>..\Resources\DrawCurveMode.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
+  </data>
 </root>
\ No newline at end of file
diff --git a/Source/Plugins/BuilderModes/Resources/DrawCurveMode.png b/Source/Plugins/BuilderModes/Resources/DrawCurveMode.png
new file mode 100644
index 0000000000000000000000000000000000000000..ce2cc70348cc269b4809ec2e40e23b77dc76a90f
GIT binary patch
literal 3092
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

literal 0
HcmV?d00001

-- 
GitLab