using System; using System.Collections.Generic; using Quik.CommandMachine; namespace Quik.VertexGenerator { public class VertexGeneratorEngine : CommandEngine { public DrawQueue DrawQueue { get; } = new DrawQueue(); /// /// Granularity for rounded geometry. /// protected float CurveGranularity => (Style["-vertex-curve-granularity"] is float value) ? value : 1.0f; protected QuikVertex StrokeVertex => new QuikVertex() { ZIndex = Style.ZIndex ?? this.ZIndex, Color = Style.StrokeColor ?? QuikColor.Black, }; protected QuikVertex FillVertex => new QuikVertex() { ZIndex = Style.ZIndex ?? this.ZIndex, Color = Style.Color ?? QuikColor.White, }; protected override void ChildProcessCommand(Command name, CommandQueue queue) { base.ChildProcessCommand(name, queue); switch(name) { case Command.Line: LineProc(queue); break; default: break; } } /// /// Gets the rounding resolution for a line segment or border radius. /// /// The width of the line. /// The angle of the cap or joint arc. /// The rounding resolution. protected int GetRoundingResolution(float radius, float arc) { return (int) Math.Ceiling(arc * radius * CurveGranularity); } private readonly List LineList = new List(); private void LineProc(CommandQueue queue) { Frame frame = queue.Dequeue(); // Clear temporary vector list and retreive all line segments. LineList.Clear(); if (frame.Type == FrameType.IVec1) { int count = (int)frame; for (int i = 0; i < count; i++) { frame = queue.Dequeue(); LineList.Add((QuikLine)frame); } } else { LineList.Add((QuikLine)frame); } float width = Style.StrokeWidth ?? 1; DrawQueue.StartDrawCall(Viewport); LineInfo prevBase, nextBase = default; for (int i = 0; i < LineList.Count; i++) { QuikLine line = LineList[i]; // A line segment needs a start cap if it is the first segment in // the list, or the last end point is not the current start point. bool isStart = (i == 0 || line.Start != LineList[i - 1].End); // A line segment needs an end cap if it is the last line segment // in the list or if the next start point is not the current end point. bool isEnd = (i == LineList.Count - 1 || line.End != LineList[i+1].Start); // Generate the main line segment. prevBase = nextBase; nextBase = GenerateLineSegment(line); if (isStart) { // Then a start cap if necessary. GenerateCap(line.Start, line.Normal(), prevBase, false); } else { // Otherwise generate the required joint. GenerateJoint(line.Start, LineList[i-1].Normal(), line.Normal(), prevBase, nextBase); } if (isEnd) { // Then generate the end cap if necessary. GenerateCap(line.End, line.Normal(), nextBase, true); } } DrawQueue.EndDrawCall(); } private LineInfo GenerateLineSegment(in QuikLine line) { QuikVertex vertex = StrokeVertex; QuikVertex a, b, c, d; QuikVec2 normal = line.Normal(); float width = Style.StrokeWidth ?? 1; a = b = c = d = vertex; a.Position = line.Start + width / 2 * normal; b.Position = line.Start - width / 2 * normal; c.Position = line.End + width / 2 * normal; d.Position = line.End - width / 2 * normal; DrawQueue.RestoreOffset(); DrawQueue.AddVertex(a); DrawQueue.AddVertex(b); DrawQueue.AddVertex(c); DrawQueue.AddVertex(d); DrawQueue.AddElement(1); DrawQueue.AddElement(2); DrawQueue.AddElement(0); DrawQueue.AddElement(1); DrawQueue.AddElement(3); DrawQueue.AddElement(2); return new LineInfo(DrawQueue.BaseOffset, 0, 1, 2, 3); } private void GenerateJoint( in QuikVec2 center, in QuikVec2 prevNormal, in QuikVec2 nextNormal, in LineInfo prevInfo, in LineInfo nextInfo) { // Figure out which side needs the joint. QuikVec2 meanNormal = 0.5f * (prevNormal + nextNormal); QuikVec2 meanTangent = new QuikVec2(meanNormal.Y, -meanNormal.X); QuikVec2 positiveEdge = ((center + nextNormal) - (center + prevNormal)).Normalize(); QuikVec2 negativeEdge = ((center - nextNormal) - (center - prevNormal)).Normalize(); float positive, negative; positive = QuikVec2.Dot(meanTangent, positiveEdge); negative = QuikVec2.Dot(meanNormal, negativeEdge); if (positive == negative) { // To be fair this is highly unlikely considering the nature of // floats, but, generate an end cap to handle a cusp. GenerateCap(center, nextNormal, nextInfo, true); return; } QuikVertex vertex = StrokeVertex; float radius = Style.StrokeWidth/2 ?? 0.5f; float arc = MathF.Acos(QuikVec2.Dot(prevNormal, nextNormal)); int resolution = GetRoundingResolution(radius, arc); bool isNegative = positive < negative; vertex.Position = center; DrawQueue.RestoreOffset(); DrawQueue.AddVertex(vertex); int lastIndex, endIndex; if (isNegative) { lastIndex = DrawQueue.RelativeElement(prevInfo.BaseOffset, prevInfo.EndNegative); endIndex = DrawQueue.RelativeElement(nextInfo.BaseOffset, nextInfo.StartNegative); } else { lastIndex = DrawQueue.RelativeElement(nextInfo.BaseOffset, nextInfo.StartNegative); endIndex = DrawQueue.RelativeElement(prevInfo.BaseOffset, prevInfo.EndPositive); } for (int i = 0; i < resolution; i++) { float angle = (float)(i+1) / (resolution + 1) * arc; float cos = MathF.Cos(angle); float sin = MathF.Sin(angle); QuikVec2 displacement; if (isNegative) { displacement = new QuikVec2() { X = -prevNormal.X * cos + prevNormal.Y * sin, Y = -prevNormal.X * sin - prevNormal.Y * cos } * radius; } else { displacement = new QuikVec2() { X = nextNormal.X * cos - nextNormal.Y * sin, Y = nextNormal.X * sin + nextNormal.Y * cos } * radius; } vertex.Position = center + displacement; DrawQueue.AddVertex(vertex); DrawQueue.AddElement(lastIndex); DrawQueue.AddElement(i); DrawQueue.AddElement(0); lastIndex = i; } DrawQueue.AddElement(lastIndex); DrawQueue.AddElement(endIndex); DrawQueue.AddElement(0); } private void GenerateCap( in QuikVec2 center, in QuikVec2 normal, in LineInfo info, bool endCap) { int lastIndex, startIndex; QuikVertex vertex = StrokeVertex; float radius = Style.StrokeWidth ?? 1.0f; int resolution = GetRoundingResolution(radius, MathF.PI); DrawQueue.RestoreOffset(); if (endCap) { lastIndex = DrawQueue.RelativeElement(info.BaseOffset, info.EndPositive); startIndex = DrawQueue.RelativeElement(info.BaseOffset, info.EndNegative); } else { lastIndex = DrawQueue.RelativeElement(info.BaseOffset, info.StartPositive); startIndex = DrawQueue.RelativeElement(info.BaseOffset, info.StartNegative); } for (int i = 0; i < resolution; i++) { float angle = (float) (i + 1) / (resolution + 1) * MathF.PI; float cos = MathF.Cos(angle); float sin = MathF.Sin(angle); QuikVec2 displacement; if (endCap) { displacement = new QuikVec2() { X = normal.X * cos + normal.Y * sin, Y = -normal.X * sin + normal.Y * cos } * radius; } else { displacement = new QuikVec2() { X = normal.X * cos - normal.Y * sin, Y = normal.X * sin + normal.Y * cos } * radius; } vertex.Position = center + displacement; DrawQueue.AddVertex(vertex); DrawQueue.AddElement(startIndex); DrawQueue.AddElement(lastIndex); DrawQueue.AddElement(i); lastIndex = i; } } private readonly List BezierList = new List(); private void BezierProc(CommandQueue queue) { Frame a = queue.Dequeue(); Frame b; // Clear temporary vector list and retreive all bezier segments. BezierList.Clear(); if (a.Type == FrameType.IVec1) { int count = (int)a; for (int i = 0; i < count; i++) { a = queue.Dequeue(); b = queue.Dequeue(); BezierList.Add( new QuikBezier( new QuikVec2(a.GetF(0), a.GetF(1)), new QuikVec2(b.GetF(0), b.GetF(1)), new QuikVec2(b.GetF(2), b.GetF(3)), new QuikVec2(a.GetF(2), a.GetF(3)) ) ); } } else { b = queue.Dequeue(); BezierList.Add( new QuikBezier( new QuikVec2(a.GetF(0), a.GetF(1)), new QuikVec2(b.GetF(0), b.GetF(1)), new QuikVec2(b.GetF(2), b.GetF(3)), new QuikVec2(a.GetF(2), a.GetF(3)) ) ); } float width = Style.StrokeWidth ?? 1; DrawQueue.StartDrawCall(Viewport); LineInfo prevBase, nextBase = default; for (int i = 0; i < LineList.Count; i++) { QuikBezier bezier = BezierList[i]; // A line segment needs a start cap if it is the first segment in // the list, or the last end point is not the current start point. bool isStart = (i == 0 || bezier.Start != BezierList[i - 1].End); // A line segment needs an end cap if it is the last line segment // in the list or if the next start point is not the current end point. bool isEnd = (i == LineList.Count - 1 || bezier.End != BezierList[i+1].Start); // Generate the main line segment. prevBase = nextBase; nextBase = GenerateBezierSegment(bezier); if (isStart) { // Then a start cap if necessary. GenerateCap(bezier.Start, bezier.GetBezierNormal(0), prevBase, false); } else { // Otherwise generate the required joint. GenerateJoint(bezier.Start, BezierList[i-1].GetBezierNormal(1), bezier.GetBezierNormal(0), prevBase, nextBase); } if (isEnd) { // Then generate the end cap if necessary. GenerateCap(bezier.End, bezier.GetBezierNormal(1), nextBase, true); } } DrawQueue.EndDrawCall(); } private LineInfo GenerateBezierSegment(in QuikBezier bezier) { QuikVec2 startTangent = bezier.GetBezierTangent(0); QuikVec2 endTangent = bezier.GetBezierTangent(1); QuikVec2 startNormal = new QuikVec2(-startTangent.Y, startTangent.X).Normalize(); QuikVec2 endNormal = new QuikVec2(-endTangent.Y, endTangent.X).Normalize(); float width = Style.StrokeWidth ?? 1; float radius = 0.5f * width; int resolution = GetRoundingResolution(radius, bezier.RasterizationArc); DrawQueue.RestoreOffset(); QuikVertex v = StrokeVertex; int vbase = DrawQueue.BaseOffset; int index = 2; v.Position = bezier.Start + radius * startNormal; DrawQueue.AddVertex(v); v.Position = bezier.Start - radius * startNormal; DrawQueue.AddVertex(v); for (int i = 0; i < resolution; i++, index += 2) { float t = (i + 1.0f) / resolution; QuikVec2 at = bezier.GetBezierTangent(t).Normalize(); QuikVec2 a = bezier.GetBezierPoint(t); QuikVec2 an = radius * new QuikVec2(-at.Y, at.X); v.Position = a + an; DrawQueue.AddVertex(v); v.Position = a - an; DrawQueue.AddVertex(v); DrawQueue.AddElement(index - 2); DrawQueue.AddElement(index - 1); DrawQueue.AddElement(index + 0); DrawQueue.AddElement(index - 1); DrawQueue.AddElement(index + 1); DrawQueue.AddElement(index + 0); } return new LineInfo(vbase, 0, 1, index - 2, index - 1); } private struct LineInfo { public int BaseOffset { get; } public int StartPositive { get; } public int StartNegative { get; } public int EndPositive { get; } public int EndNegative { get; } public LineInfo(int baseOffset, int startPositive, int startNegative, int endPositive, int endNegative) { BaseOffset = baseOffset; StartPositive = startPositive; StartNegative = startNegative; EndPositive = endPositive; EndNegative = endNegative; } } } }