From 502eb95278ce8a664c0fa03cd2c0c25a40feefb5 Mon Sep 17 00:00:00 2001 From: "H. Utku Maden" Date: Sat, 6 Aug 2022 10:05:27 +0300 Subject: [PATCH] Create joint generating code for multi-segment lines and refactor code for end caps. --- Quik/QuikGeometry.cs | 51 ++- Quik/VertexGenerator/QuikVertexGenerator.cs | 462 ++++++++++++++++---- QuikTestApplication/Program.cs | 37 +- 3 files changed, 454 insertions(+), 96 deletions(-) diff --git a/Quik/QuikGeometry.cs b/Quik/QuikGeometry.cs index 2cfe0dc..b8e8304 100644 --- a/Quik/QuikGeometry.cs +++ b/Quik/QuikGeometry.cs @@ -1,4 +1,5 @@ using System; +using Quik.VertexGenerator; namespace Quik { @@ -10,9 +11,15 @@ namespace Quik public float X; public float Y; - public float Length() => MathF.Sqrt(X * X + Y * Y); + public float Magnitude => MathF.Sqrt(X * X + Y * Y); - public QuikVec2 Normalize() => this * (1.0f / this.Length()); + public QuikVec2(float x, float y) + { + X = x; + Y = y; + } + + public QuikVec2 Normalize() => this * (1.0f / Magnitude); public float Atan2() => MathF.Atan2(Y, X); public static QuikVec2 operator +(QuikVec2 a, QuikVec2 b) @@ -52,6 +59,32 @@ namespace Quik } public static QuikVec2 operator *(QuikVec2 a, float b) => b * a; + + public static bool operator ==(QuikVec2 a, QuikVec2 b) => a.X == b.X && a.Y == b.Y; + + public static bool operator !=(QuikVec2 a, QuikVec2 b) => a.X != b.X || a.Y != b.Y; + + public override bool Equals(object obj) + { + if (obj is QuikVec2) + { + return (QuikVec2) obj == this; + } + else + { + return false; + } + } + + public override int GetHashCode() + { + return 63671 * X.GetHashCode() ^ 81083 * Y.GetHashCode(); + } + + public static float Dot(QuikVec2 a, QuikVec2 b) + { + return a.X * b.X + a.Y * b.Y; + } } /// @@ -162,6 +195,20 @@ namespace Quik /// End point. /// public QuikVec2 End; + + public QuikLine(QuikVec2 start, QuikVec2 end) + { + Start = start; + End = end; + } + + public QuikLine(float startX, float startY, float endX, float endY) + { + Start.X = startX; + Start.Y = startY; + End.X = endX; + End.Y = endY; + } } /// diff --git a/Quik/VertexGenerator/QuikVertexGenerator.cs b/Quik/VertexGenerator/QuikVertexGenerator.cs index 599f9d0..082120b 100644 --- a/Quik/VertexGenerator/QuikVertexGenerator.cs +++ b/Quik/VertexGenerator/QuikVertexGenerator.cs @@ -189,7 +189,323 @@ namespace Quik.VertexGenerator } return null; } + + /// + /// Generates a basic line segment. + /// + /// The vertex base to modify when generating vertices. + /// The line segment. + /// The width of the line. + /// Start index of the generated line. + /// The line normal. + private void GenerateLineSegment( + QuikVertex baseVertex, + QuikLine line, + float width, + out short lineStartIndex, + out QuikVec2 normal) + { + QuikVec2 tangent = (line.End - line.Start).Normalize(); + normal = new QuikVec2() {X = -tangent.Y, Y = tangent.X}; + lineStartIndex = (short)_vertexBufferPointer; + + QuikVertex startA = baseVertex, startB = baseVertex; + QuikVertex endA = baseVertex, endB = baseVertex; + + startA.Position = line.Start + width / 2 * normal; + startB.Position = line.Start - width / 2 * normal; + endA.Position = line.End + width / 2 * normal; + endB.Position = line.End - width / 2 * normal; + + // Add the major line vertices. + AddVertex(startA, startB, endA, endB); + + // Add the line indices. + AddElement( + (short) (lineStartIndex + 1), + (short) (lineStartIndex + 2), + (short) (lineStartIndex + 0), + (short) (lineStartIndex + 1), + (short) (lineStartIndex + 3), + (short) (lineStartIndex + 2) + ); + } + /// + /// Gets the rounding resolution for a line segment. + /// + /// The width of the line. + /// The angle of the cap or joint arc. + /// The rounding resolution. + private int GetRoundingResolution(float width, float arc) + { + int endCapResolution = (int) Math.Ceiling(arc * width * CurveGranularity); + return endCapResolution; + } + + /// + /// Generate a round start cap on a line. + /// + /// The base vertex to modify for generation. + /// The start point. + /// The line normal. + /// The width of line to generate. + /// End cap resolution. + /// The positive vertex index of the line start. + /// The negative vertex index of the line start. + public void GenerateStartCap( + QuikVertex baseVertex, + QuikVec2 start, + QuikVec2 normal, + float width, + int resolution, + short lineStartPositiveIndex, + short lineStartNegativeIndex + ) + { + QuikVertex circlePoint = baseVertex; + short lastIndex = lineStartPositiveIndex; + for (int i = 0; i < resolution; i++) + { + float angle = (float) (i + 1) / (resolution + 1) * MathF.PI; + float cosT = MathF.Cos(angle); + float sinT = MathF.Sin(angle); + + QuikVec2 displacement = new QuikVec2() + { + X = normal.X * cosT - normal.Y * sinT, + Y = normal.X * sinT + normal.Y * cosT + } * (width / 2); + + circlePoint.Position = start + displacement; + + short segmentIndex = (short)_vertexBufferPointer; + + AddVertex(circlePoint); + AddElement( + lineStartNegativeIndex, + lastIndex, + segmentIndex); + + lastIndex = segmentIndex; + } + } + + /// + /// Generate a round line end cap. + /// + /// Base vertex to modify for generation. + /// The line end. + /// The line normal. + /// The line width. + /// Cap generation resolution. + /// Vertex index of the positive line end. + /// Vertex index of the negative line end. + public void GenerateEndCap( + QuikVertex baseVertex, + QuikVec2 end, + QuikVec2 normal, + float width, + int resolution, + short lineEndPositiveIndex, + short lineEndNegativeIndex + ) + { + QuikVertex circlePoint = baseVertex; + short lastIndex = lineEndPositiveIndex; + for (int i = 0; i < resolution; i++) + { + float angle = -(float) (i + 1) / (resolution + 1) * MathF.PI; + float cosT = MathF.Cos(angle); + float sinT = MathF.Sin(angle); + + QuikVec2 displacement = new QuikVec2() + { + X = normal.X * cosT - normal.Y * sinT, + Y = normal.X * sinT + normal.Y * cosT + } * (width / 2); + + circlePoint.Position = end + displacement; + + AddVertex(circlePoint); + AddElement( + lineEndNegativeIndex, + lastIndex, + (short) (_vertexBufferPointer - 1)); + + lastIndex = (short) (_vertexBufferPointer - 1); + } + } + + /// + /// Generate a joint on the line negative edge. + /// + /// Base vertex to modify for generation. + /// The focus of the joint. + /// With of the lines. + /// Index of the negative end vertex. + /// Index of the negative start vertex. + /// Joint resolution. + /// Arc length of the joint. + /// Normal of the previous line. + private void GenerateNegativeJoint( + QuikVertex baseVertex, + QuikVec2 focus, + float width, + short prevNegativeLineIndex, + short nextNegativeLineIndex, + int resolution, + float arc, + QuikVec2 prevNormal) + { + QuikVertex focusVertex = baseVertex; + short focusIndex = (short) _vertexBufferPointer; + short lastIndex = prevNegativeLineIndex; + + focusVertex.Position = focus; + AddVertex(focusVertex); + + for (int i = 0; i < resolution; i++) + { + float angle = (float) (i + 1) / (resolution + 1) * arc; + float cosT = MathF.Cos(angle); + float sinT = MathF.Sin(angle); + + QuikVec2 displacement = new QuikVec2() + { + X = -prevNormal.X * cosT + prevNormal.Y * sinT, + Y = -prevNormal.X * sinT - prevNormal.Y * cosT + } * (width / 2); + + QuikVertex segmentVertex = focusVertex; + segmentVertex.Position += displacement; + + short segmentIndex = (short) _vertexBufferPointer; + AddVertex(segmentVertex); + + AddElement(lastIndex, segmentIndex, focusIndex); + + lastIndex = segmentIndex; + } + + // Add final triangle. + AddElement(lastIndex, nextNegativeLineIndex, focusIndex); + } + + /// + /// Generate a joint on the line negative edge. + /// + /// Base vertex to modify for generation. + /// The focus of the joint. + /// With of the lines. + /// Index of the positive end vertex. + /// Index of the positive start vertex. + /// Joint resolution. + /// Arc length of the joint. + /// Normal of the next line. + private void GeneratePositiveJoint( + QuikVertex baseVertex, + QuikVec2 focus, + float width, + short prevPositiveLineIndex, + short nextPositiveLineIndex, + int resolution, + float arc, + QuikVec2 nextNormal) + { + QuikVertex focusVertex = baseVertex; + short focusIndex = (short) _vertexBufferPointer; + short lastIndex = nextPositiveLineIndex; + + focusVertex.Position = focus; + AddVertex(focusVertex); + + for (int i = 0; i < resolution; i++) + { + float angle = (float) (i + 1) / (resolution + 1) * arc; + float cosT = MathF.Cos(angle); + float sinT = MathF.Sin(angle); + + QuikVec2 displacement = new QuikVec2() + { + X = nextNormal.X * cosT - nextNormal.Y * sinT, + Y = nextNormal.X * sinT + nextNormal.Y * cosT + } * (width / 2); + + QuikVertex segmentVertex = focusVertex; + segmentVertex.Position += displacement; + + short segmentIndex = (short) _vertexBufferPointer; + AddVertex(segmentVertex); + + AddElement(lastIndex, segmentIndex, focusIndex); + + lastIndex = segmentIndex; + } + + // Add final triangle. + AddElement(lastIndex, prevPositiveLineIndex, focusIndex); + } + + /// + /// Generate a joint. + /// + /// Base vertex to modify. + /// Focus of the joint. + /// Tangent of the previous line segment. + /// Tangent of the next line segment. + /// Width of the lines. + /// Vertex index of the positive end. + /// Vertex index of the negative end. + /// Vertex index of the positive start. + /// Vertex index of the negative start. + public void GenerateJoint( + QuikVertex baseVertex, + QuikVec2 focus, + QuikVec2 prevTangent, + QuikVec2 nextTangent, + float width, + short prevPositiveEndIndex, + short prevNegativeEndIndex, + short nextPositiveStartIndex, + short nextNegativeStartIndex + ) + { + QuikVec2 prevNormal, nextNormal; + QuikVec2 averageTangent; + + prevNormal = new QuikVec2() { X = -prevTangent.Y, Y = prevTangent.X }; + nextNormal = new QuikVec2() { X = -nextTangent.Y, Y = nextTangent.X }; + + averageTangent = 0.5f * (prevTangent + nextTangent); + + // Figure out which side needs the joint. + QuikVec2 positiveEdge = ((focus + nextNormal) - (focus + prevNormal)).Normalize(); + QuikVec2 negativeEdge = ((focus - nextNormal) - (focus - prevNormal)).Normalize(); + + float positiveDot, negativeDot; + positiveDot = QuikVec2.Dot(averageTangent, positiveEdge); + negativeDot = QuikVec2.Dot(averageTangent, negativeEdge); + + float arc = MathF.Acos(QuikVec2.Dot(prevNormal, nextNormal)); + int resolution = GetRoundingResolution(width, arc); + + if (positiveDot < negativeDot) + { + // Negative edge rounding. + GenerateNegativeJoint(baseVertex, focus, width, prevNegativeEndIndex, nextNegativeStartIndex, resolution, arc, prevNormal); + } + else if (negativeDot < positiveDot) + { + // Positive edge rounding. + GeneratePositiveJoint(baseVertex, focus, width, prevPositiveEndIndex, nextPositiveStartIndex, resolution, arc, nextNormal); + } + else + { + // There is no need to generate anything because the lines are the same??? + } + } + /// /// Renders a line. /// @@ -198,98 +514,86 @@ namespace Quik.VertexGenerator private void RenderLine(ref QuikDrawCall call, QuikCommandLine line) { // Skip over stipple patterns for now. - QuikStrokeStyle style = line.Style ?? Context.DefaultStroke; - int endCapResolution; // Resolution of the end cap. - short startOffset = (short) _vertexBufferPointer; // Starting index pointer. - QuikVec2 tangent; - QuikVec2 normal; - - tangent = (line.Line.End - line.Line.Start).Normalize(); - normal = new QuikVec2() {X = -tangent.Y, Y = tangent.X}; - - QuikVertex baseVertex = new QuikVertex() {Color = style.Color }; - QuikVertex startA = baseVertex, startB = baseVertex; - QuikVertex endA = baseVertex, endB = baseVertex; - - startA.Position = line.Line.Start + style.Width / 2 * normal; - startB.Position = line.Line.Start - style.Width / 2 * normal; - endA.Position = line.Line.End + style.Width / 2 * normal; - endB.Position = line.Line.End - style.Width / 2 * normal; - - // Add the major line vertices. - AddVertex(startA, startB, endA, endB); + QuikStrokeStyle style = line.Style ?? Context.DefaultStroke; + QuikVertex baseVertex = new QuikVertex() { Color = style.Color }; - // Add the line indices. - AddElement( - (short) (startOffset + 1), - (short) (startOffset + 2), - (short) (startOffset + 0), - (short) (startOffset + 1), - (short) (startOffset + 3), - (short) (startOffset + 2) - ); + GenerateLineSegment(baseVertex, line.Line, style.Width, out short startOffset, out QuikVec2 normal); // Now calculate the end caps. - endCapResolution = (int)Math.Ceiling(MathF.PI * style.Width * CurveGranularity); + int endCapResolution = GetRoundingResolution(style.Width, MathF.PI); // Construct start cap. - QuikVertex circlePoint = baseVertex; - short lastIndex = startOffset; - for (int i = 0; i < endCapResolution; i++) - { - float angle = (float) (i + 1) / (endCapResolution + 1) * MathF.PI; - float cosT = MathF.Cos(angle); - float sinT = MathF.Sin(angle); - - QuikVec2 displacement = new QuikVec2() - { - X = normal.X * cosT - normal.Y * sinT, - Y = normal.X * sinT + normal.Y * cosT - } * (style.Width / 2); - - circlePoint.Position = line.Line.Start + displacement; - - AddVertex(circlePoint); - AddElement( - (short)(startOffset + 1), - lastIndex, - (short)(_vertexBufferPointer - 1)); - - lastIndex = (short) (_vertexBufferPointer - 1); - } + GenerateStartCap(baseVertex, line.Line.Start, normal, style.Width, endCapResolution, startOffset, (short) + (startOffset + 1)); // Construct end cap. - lastIndex = (short)(startOffset + 2); - for (int i = 0; i < endCapResolution; i++) - { - float angle = -(float) (i + 1) / (endCapResolution + 1) * MathF.PI; - float cosT = MathF.Cos(angle); - float sinT = MathF.Sin(angle); + GenerateEndCap(baseVertex, line.Line.End, normal, style.Width, endCapResolution, (short) (startOffset + 2),(short) + (startOffset + 3)); - QuikVec2 displacement = new QuikVec2() - { - X = normal.X * cosT - normal.Y * sinT, - Y = normal.X * sinT + normal.Y * cosT - } * (style.Width / 2); - - circlePoint.Position = line.Line.End + displacement; - - AddVertex(circlePoint); - AddElement( - (short)(startOffset + 3), - lastIndex, - (short)(_vertexBufferPointer - 1)); - - lastIndex = (short) (_vertexBufferPointer - 1); - } - - call.Offset =(short) (startOffset * 2); + call.Offset = (short) (startOffset * 2); call.Count = (short) (_elementBufferPointer - startOffset); } + /// + /// Render lines. + /// + /// The draw call to generate. + /// The lines command. private void RenderLine(ref QuikDrawCall call, QuikCommandLines lines) { - + // Skip over stipple patterns for now. + QuikStrokeStyle style = lines.Style ?? Context.DefaultStroke; + QuikVertex baseVertex = new QuikVertex() { Color = style.Color }; + bool isStart; + bool isEnd; + short startOffset = (short)_elementBufferPointer; + short lastStartIndex = 0; + short lineStartIndex; + QuikVec2 normal; + int resolution = GetRoundingResolution(style.Width, MathF.PI); + + for (int i = 0; i < lines.Lines.Length; i++) + { + QuikLine line = lines.Lines[i]; + + isStart = i == 0 || line.Start != lines.Lines[i - 1].End; + isEnd = i == lines.Lines.Length - 1 || line.End != lines.Lines[i + 1].Start; + + GenerateLineSegment(baseVertex, line, style.Width, out lineStartIndex, out normal); + + if (isStart) + { + GenerateStartCap(baseVertex, line.Start, normal, style.Width, resolution, lineStartIndex, + (short)(lineStartIndex + 1)); + } + else + { + QuikLine prev = lines.Lines[i - 1]; + QuikVec2 prevTangent = (prev.End - prev.Start).Normalize(); + QuikVec2 nextTangent = (line.End - line.Start).Normalize(); + + GenerateJoint( + baseVertex, + line.Start, + prevTangent, + nextTangent, + style.Width, + (short) (lastStartIndex + 2), + (short) (lastStartIndex + 3), + lineStartIndex, + (short)(lineStartIndex + 1)); + } + + if (isEnd) + { + GenerateEndCap(baseVertex, line.End, normal, style.Width, resolution, (short)(lineStartIndex + 2), (short)(lineStartIndex + 3)); + } + + lastStartIndex = lineStartIndex; + } + + call.Offset = (short) (startOffset * 2); + call.Count = (short) (_elementBufferPointer - startOffset); } } diff --git a/QuikTestApplication/Program.cs b/QuikTestApplication/Program.cs index 4ce6f62..fd201c8 100644 --- a/QuikTestApplication/Program.cs +++ b/QuikTestApplication/Program.cs @@ -127,27 +127,34 @@ void main() Matrix4 matrix = Matrix4.CreateOrthographicOffCenter( 0, - window.Size.X, + (float)window.Size.X / 2, 0, - window.Size.Y, + (float)window.Size.Y / 2, 1, -1); GL.UniformMatrix4(loc, false, ref matrix); + + context.DefaultStroke.Color = new QuikColor(40, 128, 60, 255); context.Draw.Line( - new QuikLine() - { - Start = new QuikVec2() - { - X = 20, - Y = 40 - }, - End = new QuikVec2() - { - X=100, - Y=100 - } - }); + // > + new QuikLine(10,10, 20, 20), + new QuikLine(20, 20, 10, 30), + + // s + new QuikLine(28, 10, 38, 10), + new QuikLine(38, 10, 38, 20), + new QuikLine(38, 20, 28, 20), + new QuikLine(28, 20, 28, 30), + new QuikLine(28, 30, 38, 30), + + // e + new QuikLine(64, 20, 74, 20), + new QuikLine(74, 20, 74, 30), + new QuikLine(74, 30, 64, 30), + new QuikLine(64, 30, 64, 10), + new QuikLine(64, 10, 74, 10) + ); QuikCommand command; while (context.Draw.Commands.TryDequeue(out command))