Dashboard/Quik/VertexGenerator/VertexCommandEngine.cs

410 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using Quik.CommandMachine;
namespace Quik.VertexGenerator
{
public class VertexGeneratorEngine : CommandEngine
{
public DrawQueue DrawQueue { get; } = new DrawQueue();
/// <summary>
/// Granularity for rounded geometry.
/// </summary>
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;
}
}
/// <summary>
/// Gets the rounding resolution for a line segment or border radius.
/// </summary>
/// <param name="radius">The width of the line.</param>
/// <param name="arc">The angle of the cap or joint arc.</param>
/// <returns>The rounding resolution.</returns>
protected int GetRoundingResolution(float radius, float arc)
{
return (int) Math.Ceiling(arc * radius * CurveGranularity);
}
private readonly List<QuikLine> LineList = new List<QuikLine>();
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<QuikBezier> BezierList = new List<QuikBezier>();
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;
}
}
}
}