Add single line rendering.

This commit is contained in:
H. Utku Maden 2022-08-04 16:40:58 +03:00
parent a6465730c1
commit 772906bec7
14 changed files with 703 additions and 4 deletions

13
.idea/.idea.Quik/.idea/.gitignore vendored Normal file

@ -0,0 +1,13 @@
# Default ignored files
/shelf/
/workspace.xml
# Rider ignored files
/.idea.Quik.iml
/modules.xml
/projectSettingsUpdater.xml
/contentModel.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

7
Quik.OpenTK/Class1.cs Normal file

@ -0,0 +1,7 @@
namespace Quik.OpenTK
{
public class Class1
{
}
}

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK" Version="4.7.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Quik\Quik.csproj" />
</ItemGroup>
</Project>

@ -1,8 +1,12 @@

Microsoft Visual Studio Solution File, Format Version 12.00
#
#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik", "Quik\Quik.csproj", "{B86B2B99-DAE4-43CE-A040-1D8E143B94A7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Quik.OpenTK", "Quik.OpenTK\Quik.OpenTK.csproj", "{586E5E28-1D07-4CC2-B04F-0BC420564F57}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuikTestApplication", "QuikTestApplication\QuikTestApplication.csproj", "{49AEF502-692A-48A4-8076-EF2228925280}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -25,5 +29,29 @@ Global
{B86B2B99-DAE4-43CE-A040-1D8E143B94A7}.Release|x64.Build.0 = Release|Any CPU
{B86B2B99-DAE4-43CE-A040-1D8E143B94A7}.Release|x86.ActiveCfg = Release|Any CPU
{B86B2B99-DAE4-43CE-A040-1D8E143B94A7}.Release|x86.Build.0 = Release|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Debug|Any CPU.Build.0 = Debug|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Debug|x64.ActiveCfg = Debug|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Debug|x64.Build.0 = Debug|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Debug|x86.ActiveCfg = Debug|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Debug|x86.Build.0 = Debug|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Release|Any CPU.ActiveCfg = Release|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Release|Any CPU.Build.0 = Release|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Release|x64.ActiveCfg = Release|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Release|x64.Build.0 = Release|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Release|x86.ActiveCfg = Release|Any CPU
{586E5E28-1D07-4CC2-B04F-0BC420564F57}.Release|x86.Build.0 = Release|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Debug|x64.ActiveCfg = Debug|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Debug|x64.Build.0 = Debug|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Debug|x86.ActiveCfg = Debug|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Debug|x86.Build.0 = Debug|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Release|Any CPU.Build.0 = Release|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Release|x64.ActiveCfg = Release|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Release|x64.Build.0 = Release|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Release|x86.ActiveCfg = Release|Any CPU
{49AEF502-692A-48A4-8076-EF2228925280}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

@ -3,7 +3,8 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>disable</Nullable>
<LangVersion>7</LangVersion>
<LangVersion>7.3</LangVersion>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

@ -1,4 +1,3 @@
namespace Quik
{
/// <summary>

@ -9,5 +9,12 @@
/// Draw queue.
/// </summary>
public QuikDraw Draw { get; } = new QuikDraw();
public QuikStrokeStyle DefaultStroke { get; set; } = new QuikStrokeStyle(new QuikColor(0x000000FF), 4);
public QuikFillStyle DefaultFill { get; set; } = new QuikFillStyle()
{
Color = new QuikColor(0x101010FF)
};
}
}

@ -11,7 +11,7 @@ namespace Quik
/// <summary>
/// The draw command queue.
/// </summary>
private Queue<QuikCommand> Commands { get; } = new Queue<QuikCommand>();
public Queue<QuikCommand> Commands { get; } = new Queue<QuikCommand>();
public void Mask(QuikRectangle bounds) => Commands.Enqueue(new QuikCommandMask(bounds));
public void Line(QuikLine line) => Commands.Enqueue(new QuikCommandLine(line));

@ -1,3 +1,5 @@
using System;
namespace Quik
{
/// <summary>
@ -7,6 +9,49 @@ namespace Quik
{
public float X;
public float Y;
public float Length() => MathF.Sqrt(X * X + Y * Y);
public QuikVec2 Normalize() => this * (1.0f / this.Length());
public float Atan2() => MathF.Atan2(Y, X);
public static QuikVec2 operator +(QuikVec2 a, QuikVec2 b)
{
return new QuikVec2()
{
X = a.X + b.X,
Y = a.Y + b.Y
};
}
public static QuikVec2 operator -(QuikVec2 a)
{
return new QuikVec2()
{
X = -a.X,
Y = -a.Y
};
}
public static QuikVec2 operator -(QuikVec2 a, QuikVec2 b)
{
return new QuikVec2()
{
X = a.X - b.X,
Y = a.Y - b.Y
};
}
public static QuikVec2 operator *(float a, QuikVec2 b)
{
return new QuikVec2()
{
X = a * b.X,
Y = a * b.Y
};
}
public static QuikVec2 operator *(QuikVec2 a, float b) => b * a;
}
/// <summary>
@ -30,6 +75,24 @@ namespace Quik
/// Alpha channel.
/// </summary>
public byte A;
public QuikColor(byte r, byte g, byte b, byte a)
{
R = r;
G = g;
B = b;
A = a;
}
public QuikColor(byte r, byte g, byte b) : this(r, g, b, 1) { }
public QuikColor(int hexCode)
{
R = (byte)((hexCode >> 24) & 0xFF);
G = (byte)((hexCode >> 16) & 0xFF);
B = (byte)((hexCode >> 8 ) & 0xFF);
A = (byte)((hexCode >> 0 ) & 0xFF);
}
}
/// <summary>
@ -56,6 +119,33 @@ namespace Quik
/// Segment end point.
/// </summary>
public QuikVec2 End;
/// <summary>
/// Get a point in the curve segment.
/// </summary>
/// <param name="t">Control parameter (between 0 and 1)</param>
/// <returns>The point on the curve.</returns>
public QuikVec2 GetBezierPoint(float t)
{
return
(1 - t) * (1 - t) * (1 - t) * Start +
(1 - t) * (1 - t) * t * ControlA +
(1 - t) * t * t * ControlB +
t * t * t * End;
}
/// <summary>
/// Get the tangent on the curve.
/// </summary>
/// <param name="t">Control parameter (between 0 and 1)</param>
/// <returns>The tangent curve.</returns>
public QuikVec2 GetBezierTangent(float t)
{
return
3 * (1 - t) * (1 - t) * (ControlA - Start) +
6 * (1 - t) * (ControlB - ControlA) +
3 * t * t * (End - ControlB);
}
}
/// <summary>
@ -88,6 +178,12 @@ namespace Quik
/// Rectangle maximum point.
/// </summary>
public QuikVec2 Max;
public QuikRectangle(float l, float t, float r, float b)
{
Min = new QuikVec2() {X = r, Y = b};
Max = new QuikVec2() {X = l, Y = t};
}
}
/// <summary>

@ -0,0 +1,30 @@
using System.Runtime.CompilerServices;
namespace Quik.VertexGenerator
{
/// <summary>
/// Represents a GPU vertex.
/// </summary>
public struct QuikVertex
{
/// <summary>
/// Position value.
/// </summary>
public QuikVec2 Position;
/// <summary>
/// Texture Coordinates.
/// </summary>
public QuikVec2 TextureCoordinates;
/// <summary>
/// Per vertex color value.
/// </summary>
public QuikColor Color;
public static int PositionOffset => 0;
public static unsafe int TextureCoordinatesOffset => sizeof(QuikVec2);
public static unsafe int ColorOffset => 2 * sizeof(QuikVec2);
public static unsafe int Stride => sizeof(QuikVertex);
}
}

@ -0,0 +1,310 @@
using System;
namespace Quik.VertexGenerator
{
/// <summary>
/// Generates vertices from draw commands for GPU APIs like OpenGL.
/// </summary>
public class QuikVertexGenerator
{
// There is a very specific reason I am not using lists like a regular
// person would use. It has to do with the fact that there is no way
// to access the internal pointer of a System.Collections.Generic.List<>
// in older versions of .NET. Avoiding a copy of an entire vertex buffer
// would be very much appreciated by many devs. So please don't be
// "smart" around this code.
// - mixed.
/// <summary>
/// Controls the buffer granularity.
/// </summary>
private const int BufferGranularity = 4096;
/// <summary>
/// List of vertices.
/// </summary>
private QuikVertex[] _vertexBuffer = new QuikVertex[BufferGranularity];
/// <summary>
/// Pointer into the vertex buffer.
/// </summary>
private int _vertexBufferPointer = 0;
private float _vertexBufferUsage = 0;
/// <summary>
/// List of element indices.
/// </summary>
private short[] _elementBuffer = new short[BufferGranularity];
/// <summary>
/// Pointer into the element buffer.
/// </summary>
private int _elementBufferPointer = 0;
private float _elementBufferUsage;
private long _bufferUsageCounter;
/// <summary>
/// Get a reference to the vertex buffer.
/// </summary>
public QuikVertex[] VertexBuffer => _vertexBuffer;
/// <summary>
/// Number of vertices in the vertex buffer.
/// </summary>
public int VertexCount => _vertexBufferPointer;
/// <summary>
/// Get a reference to the element buffer.
/// </summary>
public short[] ElementBuffer => _elementBuffer;
/// <summary>
/// Number of elements in the element buffer.
/// </summary>
public int ElementCount => _elementBufferPointer;
public float CurveGranularity { get; set; } = 0.5f;
public QuikContext Context { get; }
public QuikVertexGenerator(QuikContext context)
{
Context = context;
}
/// <summary>
/// Expands the vertex buffer by the buffer granularity constant.
/// </summary>
private void ExpandVertexBuffer()
{
Array.Resize(ref _vertexBuffer, _vertexBuffer.Length + BufferGranularity);
}
/// <summary>
/// Expands the element buffer by the buffer granularity constant.
/// </summary>
private void ExpandElementBuffer()
{
Array.Resize(ref _elementBuffer, _elementBuffer.Length + BufferGranularity);
}
/// <summary>
/// Add vertices to the list.
/// </summary>
/// <param name="vertices">The list of vertices to add.</param>
private void AddVertex(params QuikVertex[] vertices)
{
int requiredCapacity = _vertexBufferPointer + vertices.Length;
while (requiredCapacity > _vertexBuffer.Length)
{
ExpandVertexBuffer();
}
Array.Copy(vertices, 0, _vertexBuffer, _vertexBufferPointer, vertices.Length);
_vertexBufferPointer += vertices.Length;
}
/// <summary>
/// Add element indices to the list.
/// </summary>
/// <param name="indices">The list of indices to add.</param>
private void AddElement(params short[] indices)
{
int requiredCapacity = _elementBufferPointer + indices.Length;
while (requiredCapacity > _elementBuffer.Length)
{
ExpandElementBuffer();
}
Array.Copy(indices, 0, _elementBuffer, _elementBufferPointer, indices.Length);
_elementBufferPointer += indices.Length;
}
private void MovingAverage(ref float average, long sampleCounter, int newSample)
{
// Thanks to stackoverflow for a neat formula.
// https://stackoverflow.com/questions/12636613/how-to-calculate-moving-average-without-keeping-the-count-and-data-total
const float order = 4;
average = average + (newSample - average) / Math.Min(sampleCounter + 1, order);
}
private bool _renderStencilMask = false;
private QuikRectangle _bounds = new QuikRectangle(
float.PositiveInfinity, float.PositiveInfinity,
float.NegativeInfinity, float.NegativeInfinity);
/// <summary>
/// Clear the drawing buffers.
/// </summary>
public void Clear()
{
int newVertexSize;
int newElementSize;
_bufferUsageCounter++;
MovingAverage(ref _vertexBufferUsage, _bufferUsageCounter, _elementBufferPointer);
MovingAverage(ref _elementBufferUsage, _bufferUsageCounter, _elementBufferPointer);
newVertexSize = (int)(Math.Ceiling(_vertexBufferUsage / BufferGranularity) * BufferGranularity);
newElementSize = (int)(Math.Ceiling(_elementBufferUsage / BufferGranularity) * BufferGranularity);
Array.Resize(ref _vertexBuffer, newVertexSize);
Array.Resize(ref _elementBuffer, newElementSize);
_vertexBufferPointer = 0;
_elementBufferPointer = 0;
}
public QuikDrawCall? ConsumeCommand(QuikCommand command)
{
QuikDrawCall call = new QuikDrawCall()
{
Target = _renderStencilMask ? QuikRenderTarget.Stencil : QuikRenderTarget.Color
};
switch (command.Type)
{
case QuikCommandType.StencilMaskClear:
call.ClearStencil = true;
break;
case QuikCommandType.StencilMaskBegin:
_renderStencilMask = true;
call.Target = QuikRenderTarget.Stencil;
break;
case QuikCommandType.StencilMaskEnd:
_renderStencilMask = false;
call.Target = QuikRenderTarget.Color;
break;
case QuikCommandType.Line:
RenderLine(ref call, command as QuikCommandLine);
return call;
case QuikCommandType.Lines:
RenderLine(ref call, command as QuikCommandLines);
return call;
}
return null;
}
/// <summary>
/// Renders a line.
/// </summary>
/// <param name="call">The draw call to generate.</param>
/// <param name="line">The line to draw.</param>
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);
// Add the line indices.
AddElement(
(short) (startOffset + 1),
(short) (startOffset + 2),
(short) (startOffset + 0),
(short) (startOffset + 1),
(short) (startOffset + 3),
(short) (startOffset + 2)
);
// Now calculate the end caps.
endCapResolution = (int)Math.Ceiling(MathF.PI * style.Width * CurveGranularity);
// 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);
}
// 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);
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.Count = (short) (_elementBufferPointer - startOffset);
}
private void RenderLine(ref QuikDrawCall call, QuikCommandLines lines)
{
}
}
public enum QuikRenderTarget
{
Color,
Stencil
}
public struct QuikDrawCall
{
public QuikRenderTarget Target;
public short Offset;
public short Count;
public QuikRectangle Bounds;
public bool ClearStencil;
}
}

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using Quik;
using Quik.VertexGenerator;
using OpenTK.Graphics.OpenGL4;
using OpenTK.Mathematics;
using OpenTK.Windowing.Common;
using OpenTK.Windowing.Desktop;
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace QuikTestApplication
{
public class Program
{
public const string vertex =
@"#version 140
uniform mat4 matrix;
in vec2 position;
in vec2 texcoord;
in vec4 color;
out vec2 ftexcoord;
out vec4 fcolor;
void main()
{
fcolor = color;
ftexcoord = texcoord;
gl_Position = matrix * vec4(position.xy, 0, 1);
}
";
public const string fragment =
@"#version 140
in vec2 ftexcoord;
in vec4 fcolor;
out vec4 outcolor;
void main()
{
outcolor = fcolor;
}
";
public static void Main(string[] args)
{
NativeWindowSettings windowSettings = NativeWindowSettings.Default;
windowSettings.NumberOfSamples = 4;
NativeWindow window = new NativeWindow(windowSettings);
window.Context.MakeCurrent();
GL.LoadBindings(new GLFWBindingsContext());
QuikContext context = new QuikContext();
QuikVertexGenerator gen = new QuikVertexGenerator(context);
GL.Enable(EnableCap.Multisample);
int sp;
{
int vs, fs;
sp = GL.CreateProgram();
vs = GL.CreateShader(ShaderType.VertexShader);
fs = GL.CreateShader(ShaderType.FragmentShader);
GL.ShaderSource(vs, vertex);
GL.CompileShader(vs);
GL.ShaderSource(fs, fragment);
GL.CompileShader(fs);
GL.AttachShader(sp, vs);
GL.AttachShader(sp, fs);
GL.LinkProgram(sp);
GL.UseProgram(sp);
}
int vbo, ebo, vao;
vbo = GL.GenBuffer();
ebo = GL.GenBuffer();
vao = GL.GenVertexArray();
GL.BindVertexArray(vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo);
int loc;
GL.VertexAttribPointer(
loc = GL.GetAttribLocation(sp, "position"),
2,
VertexAttribPointerType.Float,
false,
QuikVertex.Stride,
QuikVertex.PositionOffset);
GL.EnableVertexAttribArray(loc);
GL.VertexAttribPointer(
loc = GL.GetAttribLocation(sp, "texcoords"),
2,
VertexAttribPointerType.Float,
false,
QuikVertex.Stride,
QuikVertex.TextureCoordinatesOffset);
GL.EnableVertexAttribArray(loc);
GL.VertexAttribPointer(
loc = GL.GetAttribLocation(sp, "color"),
4,
VertexAttribPointerType.UnsignedByte,
true,
QuikVertex.Stride,
QuikVertex.ColorOffset);
GL.EnableVertexAttribArray(loc);
loc = GL.GetUniformLocation(sp, "matrix");
List<QuikDrawCall> calls = new List<QuikDrawCall>();
for (;!window.IsExiting;)
{
NativeWindow.ProcessWindowEvents(false);
GL.Viewport(0, 0, window.Size.X, window.Size.Y);
GL.ClearColor(1,1,1,1);
GL.Clear(ClearBufferMask.ColorBufferBit);
Matrix4 matrix = Matrix4.CreateOrthographicOffCenter(
0,
window.Size.X,
0,
window.Size.Y,
1,
-1);
GL.UniformMatrix4(loc, false, ref matrix);
context.Draw.Line(
new QuikLine()
{
Start = new QuikVec2()
{
X = 20,
Y = 40
},
End = new QuikVec2()
{
X=100,
Y=100
}
});
QuikCommand command;
while (context.Draw.Commands.TryDequeue(out command))
{
QuikDrawCall? call = gen.ConsumeCommand(command);
if (call.HasValue) calls.Add(call.Value);
}
GL.BufferData(BufferTarget.ArrayBuffer, gen.VertexCount * QuikVertex.Stride, ref gen.VertexBuffer[0], BufferUsageHint.StreamDraw);
GL.BufferData(BufferTarget.ElementArrayBuffer, gen.ElementCount * 2, ref gen.ElementBuffer[0], BufferUsageHint.StreamDraw);
foreach (QuikDrawCall call in calls)
{
GL.DrawElements(BeginMode.Triangles, call.Count, DrawElementsType.UnsignedShort, call.Offset);
}
gen.Clear();
calls.Clear();
window.Context.SwapBuffers();
}
}
}
}

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Quik.OpenTK\Quik.OpenTK.csproj" />
</ItemGroup>
</Project>

@ -16,3 +16,8 @@ QUIK is not intended to replace the aforementioned libraries for the C or C++
developer, however it is intended to make a similar library available to C#
users without having to battle the un/managed barrier. It also comes with the
advantage of not requiring any platform specific native libraries.
On top of that, QUIK targets not just .NET 6.0, but old-school .NET framework
in mind as well. Whilst I do not promise the golden .NET Framework 2.0 support
I do wish to make the library available for .NET Framework 4.7.2 in the future.
Therefore the language version of the project is strictly limited to 7.3.