diff --git a/Core/Interfaces/IGame.cs b/Core/Abstractions/IGame.cs similarity index 100% rename from Core/Interfaces/IGame.cs rename to Core/Abstractions/IGame.cs diff --git a/Core/Abstractions/IRenderable.cs b/Core/Abstractions/IRenderable.cs new file mode 100644 index 0000000..6b00f6f --- /dev/null +++ b/Core/Abstractions/IRenderable.cs @@ -0,0 +1,28 @@ +using SharpEngine.Core.Entities.Properties.Meshes; + +namespace SharpEngine.Core.Interfaces; + +// TODO: Make an abstract class and move Render method here? + +/// +/// Contains definitions for rendering objects. +/// +public interface IRenderable +{ + /// + /// Gets or sets the vertex array object of the object. + /// + public uint VAO { get; set; } + + /// + /// Initializes the necessary buffers for the object. + /// + /// The mesh that should the buffers should be initialized with. + public void InitializeBuffers(Mesh mesh); + + /// + /// Binds the VAO to the current context. + /// + public void Bind(); + +} diff --git a/Core/Interfaces/ISettings.cs b/Core/Abstractions/ISettings.cs similarity index 90% rename from Core/Interfaces/ISettings.cs rename to Core/Abstractions/ISettings.cs index 17aa676..23888d2 100644 --- a/Core/Interfaces/ISettings.cs +++ b/Core/Abstractions/ISettings.cs @@ -22,7 +22,10 @@ public interface ISettings /// Gets or sets which renderer should be enabled for the current window. public RenderFlags RendererFlags { get; set; } + /// Gets or sets the primary mouse button. public MouseButton PrimaryButton { get; set; } + + /// Gets or sets the secondary mouse button. public MouseButton SecondaryButton { get; set; } } diff --git a/Core/Entities/BezierCurve.cs b/Core/Entities/BezierCurve.cs new file mode 100644 index 0000000..cd2547c --- /dev/null +++ b/Core/Entities/BezierCurve.cs @@ -0,0 +1,65 @@ +using Silk.NET.Maths; +using System; +using System.Collections.Generic; + +namespace SharpEngine.Core.Entities; + +/// +/// Represents a cubic Bezier curve. +/// +public class BezierCurve +{ + private Vector3D P0, P1, P2, P3; + + /// Gets the start point of the curve. + public Vector3D Start => P0; + + /// Gets the end point of the curve. + + public Vector3D End => P3; + + /// + /// Initializes a new instance of . + /// + /// The point where the curve should be created. + public BezierCurve(Vector3D initializationPosition) + { + const float defaultWidth = 1; + + P0 = initializationPosition; + P1 = new Vector3D(initializationPosition.X + defaultWidth, initializationPosition.Y + defaultWidth, initializationPosition.Z); + P2 = new Vector3D(initializationPosition.X + defaultWidth, initializationPosition.Y - defaultWidth, initializationPosition.Z); + P3 = new Vector3D(initializationPosition.X + defaultWidth, initializationPosition.Y, initializationPosition.Z); + } + + /// + /// Gets the points of the curve. + /// + /// The distance between each point, defines the roughness of the curve. + /// The points for the curve with the given . + public List> GetPoints(uint resolution) + { + var points = new List>(); + for (var i = 0; i <= resolution; i++) + { + var t = i / (float)resolution; + points.Add(GetPointAt(t)); + } + + return points; + } + + private Vector3D GetPointAt(float t) + { + var x = GetCoordinateAt(t, P0.X, P1.X, P2.X, P3.X); + var y = GetCoordinateAt(t, P0.Y, P1.Y, P2.Y, P3.Y); + var z = GetCoordinateAt(t, P0.Z, P1.Z, P2.Z, P3.Z); + return new(x, y, z); + } + + private static float GetCoordinateAt(float t, float p0, float p1, float p2, float p3) + { + float u = 1 - t; + return MathF.Pow(u, 3) * p0 + 3 * MathF.Pow(u, 2) * t * p1 + 3 * u * MathF.Pow(t, 2) * p2 + MathF.Pow(t, 3) * p3; + } +} diff --git a/Core/Entities/GameObject.cs b/Core/Entities/GameObject.cs index b69e257..c228615 100644 --- a/Core/Entities/GameObject.cs +++ b/Core/Entities/GameObject.cs @@ -1,53 +1,52 @@ using SharpEngine.Core.Attributes; using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Extensions; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Numerics; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; using SharpEngine.Core.Textures; +using SharpEngine.Core.Windowing; using Silk.NET.OpenGL; + using System.Collections.Generic; -using System.IO; -using System.Reflection; using System.Threading.Tasks; namespace SharpEngine.Core.Entities; /// -/// Represents a game object in the scene. +/// Represents a game object in the scene. /// -public class GameObject : SceneNode +public class GameObject : EmptyNode, IRenderable { /// /// Initializes a new instance of the . /// - public GameObject() + public GameObject() : base(string.Empty) { - var shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/lighting.frag"), "lighting"); - var diffuse = TextureService.Instance.LoadTexture(PathExtensions.GetAssemblyPath("Textures/DefaultTextures/debug.JPG")); + // TODO: Using lightning shader here will most likely break stuff, it needs to be refactored out. + var shader = ShaderService.Instance.LoadShader(_Resources.Default.VertexShader, _Resources.Default.FragmentShader, "lighting"); + var diffuse = TextureService.Instance.LoadTexture(_Resources.Default.DebugTexture); Material = new(shader, diffuse); BoundingBox = BoundingBox.CalculateBoundingBox(Transform); } /// - /// Initializes a new instance of the with specified textures and shaders. + /// Initializes a new instance of the with specified textures and shaders. /// /// The file path of the diffuse map texture. /// The file path of the specular map texture. /// The file path of the vertex shader. /// The file path of the fragment shader. - public GameObject(string diffuseMapFile, string specularMapFile, string vertShaderFile, string fragShaderFile) + public GameObject(string? diffuseMapFile = null, string? specularMapFile = null, string? vertShaderFile = null, string? fragShaderFile = null) : base(string.Empty) { - var diffuse = TextureService.Instance.LoadTexture(diffuseMapFile); - var specularMap = TextureService.Instance.LoadTexture(specularMapFile); - var shader = ShaderService.Instance.LoadShader(vertShaderFile, fragShaderFile, "lighting"); - - Material = new Material(shader, diffuse) - { - SpecularMap = specularMap - }; + var shader = ShaderService.Instance.LoadShader(vertShaderFile ?? _Resources.Default.VertexShader, fragShaderFile ?? _Resources.Default.FragmentShader, "lighting"); + var diffuse = TextureService.Instance.LoadTexture(diffuseMapFile ?? _Resources.Default.DebugTexture); + var specular = string.IsNullOrEmpty(specularMapFile) ? null : TextureService.Instance.LoadTexture(specularMapFile!); + Material = new Material(shader, diffuse, specular); BoundingBox = BoundingBox.CalculateBoundingBox(Transform); } @@ -55,20 +54,19 @@ public GameObject(string diffuseMapFile, string specularMapFile, string vertShad /// /// Gets or sets the mesh of the game object. /// - public List Mesh { get; set; } = []; + public List Meshes { get; set; } = []; + // TODO: Each mesh should have its own material. /// /// Gets or sets the material of the game object. /// [Inspector(DisplayInInspector = false)] public Material Material { get; set; } - private Transform _transform = new(); - /// /// Gets or sets the transform of the game object. /// - public Transform Transform + public override Transform Transform { get => _transform; set @@ -77,6 +75,8 @@ public Transform Transform BoundingBox = BoundingBox.CalculateBoundingBox(_transform); } } + + private Transform _transform = new(); /// /// Gets the bounding box of the game object. @@ -85,22 +85,65 @@ public Transform Transform public BoundingBox BoundingBox { get; set; } /// - public override Task Render() + public uint VAO { get; set; } + + /// + public void Initialize() + { + VAO = Window.GL.GenVertexArray(); + Bind(); + + foreach (var mesh in Meshes) + InitializeBuffers(mesh); + + Material.Shader.Use(); + } + + /// + public void Bind() + { + Window.GL.BindVertexArray(VAO); + } + + /// + public void InitializeBuffers(Mesh mesh) + { + var vertexBufferObject = Window.GL.GenBuffer(); + Window.GL.BindBuffer(GLEnum.ArrayBuffer, vertexBufferObject); + Window.GL.BufferData(GLEnum.ArrayBuffer, mesh.GetVertices(), GLEnum.StaticDraw); + + var elementBufferObject = Window.GL.GenBuffer(); + Window.GL.BindBuffer(GLEnum.ElementArrayBuffer, elementBufferObject); + Window.GL.BufferData(GLEnum.ElementArrayBuffer, mesh.Indices, GLEnum.StaticDraw); + } + + /// + public override Task Render(CameraView camera, Window window) { Material.DiffuseMap.Use(TextureUnit.Texture0); Material.Shader.SetInt("material.diffuse", Material.diffuseUnit); - if (Material.SpecularMap is not null) + if (Material.UseSpecularMap) { Material.SpecularMap.Use(TextureUnit.Texture1); Material.Shader.SetInt("material.specular", Material.specularUnit); Material.Shader.SetVector3("material.specular", Material.Specular); Material.Shader.SetFloat("material.shininess", Material.Shininess); } + else + { + Material.Shader.SetInt("material.specular", 0); + Material.Shader.SetVector3("material.specular", new System.Numerics.Vector3(0)); + Material.Shader.SetFloat("material.shininess", 0); + } Material.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + Window.GL.DrawArrays(PrimitiveType.Triangles, 0, 36); + // foreach (var mesh in Meshes) + // Window.GL.DrawElements(PrimitiveType.Triangles, (uint)mesh.Indices.Length, DrawElementsType.UnsignedInt, []); + return Task.CompletedTask; } } diff --git a/Core/Entities/Lights/DirectionalLight.cs b/Core/Entities/Lights/DirectionalLight.cs index 4fcb63b..432f980 100644 --- a/Core/Entities/Lights/DirectionalLight.cs +++ b/Core/Entities/Lights/DirectionalLight.cs @@ -1,6 +1,8 @@ -using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; using System.Numerics; using System.Threading.Tasks; @@ -28,7 +30,7 @@ public DirectionalLight() } /// - public override Task Render() + public override Task Render(CameraView camera, Window window) { Material.Shader.SetVector3("dirLight.direction", Direction); Material.Shader.SetVector3("dirLight.ambient", Ambient); diff --git a/Core/Entities/Lights/PointLight.cs b/Core/Entities/Lights/PointLight.cs index 858dbf8..970d32b 100644 --- a/Core/Entities/Lights/PointLight.cs +++ b/Core/Entities/Lights/PointLight.cs @@ -1,5 +1,7 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; using System.Numerics; using System.Threading.Tasks; @@ -18,7 +20,7 @@ public class PointLight : Light public PointLight(Vector3 position, int index) { Transform.Scale = new(0.2f, 0.2f, 0.2f); - Transform.Position = position; + Transform.Position = (SharpEngine.Core.Numerics.Vector3)position; Constant = 1.0f; Linear = 0.09f; Quadratic = 0.032f; @@ -50,9 +52,9 @@ public PointLight(Vector3 position, int index) public float Quadratic { get; set; } /// - public override Task Render() + public override Task Render(CameraView camera, Window window) { - Material.Shader.SetVector3($"pointLights[{_index}].position", Transform.Position); + Material.Shader.SetVector3($"pointLights[{_index}].position", (Vector3)Transform.Position); Material.Shader.SetVector3($"pointLights[{_index}].ambient", Ambient); Material.Shader.SetVector3($"pointLights[{_index}].diffuse", Diffuse); Material.Shader.SetVector3($"pointLights[{_index}].specular", Specular); @@ -60,8 +62,8 @@ public override Task Render() Material.Shader.SetFloat($"pointLights[{_index}].linear", Linear); Material.Shader.SetFloat($"pointLights[{_index}].quadratic", Quadratic); - var lampMatrix = Matrix4x4.CreateScale(Transform.Scale); - lampMatrix *= Matrix4x4.CreateTranslation(Transform.Position); + var lampMatrix = Matrix4x4.CreateScale((Vector3)Transform.Scale); + lampMatrix *= Matrix4x4.CreateTranslation((Vector3)Transform.Position); LampShader.Shader?.SetMatrix4(ShaderAttributes.Model, lampMatrix); diff --git a/Core/Entities/Lights/SpotLight.cs b/Core/Entities/Lights/SpotLight.cs index 502952d..b2ef9c9 100644 --- a/Core/Entities/Lights/SpotLight.cs +++ b/Core/Entities/Lights/SpotLight.cs @@ -1,5 +1,7 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Extensions; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; using System.Numerics; using System.Threading.Tasks; @@ -56,9 +58,9 @@ public SpotLight() public float Quadratic { get; set; } /// - public override Task Render() + public override Task Render(CameraView camera, Window window) { - Material.Shader.SetVector3("spotLight.position", Transform.Position); + Material.Shader.SetVector3("spotLight.position", (Vector3)Transform.Position); Material.Shader.SetVector3("spotLight.direction", Direction); Material.Shader.SetVector3("spotLight.ambient", Ambient); Material.Shader.SetVector3("spotLight.diffuse", Diffuse); diff --git a/Core/Entities/Properties/BoundingBox.cs b/Core/Entities/Properties/BoundingBox.cs index 1119c28..e0a4e67 100644 --- a/Core/Entities/Properties/BoundingBox.cs +++ b/Core/Entities/Properties/BoundingBox.cs @@ -28,14 +28,15 @@ public BoundingBox(Vector3 min, Vector3 max) Max = max; } + // TODO: This should be calculated by the mesh. /// /// Calculates the bounding box of the game object. /// /// The bounding box of the game object. - public static BoundingBox CalculateBoundingBox(Transform transform) + public static BoundingBox CalculateBoundingBox(ITransform transform) { var min = transform.Position - (transform.Scale / 2); var max = transform.Position + (transform.Scale / 2); - return new BoundingBox(min, max); + return new BoundingBox((Vector3)min, (Vector3)max); } } diff --git a/Core/Entities/Properties/ITransform.cs b/Core/Entities/Properties/ITransform.cs new file mode 100644 index 0000000..a3b5dbd --- /dev/null +++ b/Core/Entities/Properties/ITransform.cs @@ -0,0 +1,23 @@ +using SharpEngine.Core.Numerics; +using System.Numerics; + +namespace SharpEngine.Core.Entities.Properties; + +/// +/// Represents the transform of an object in dimensions. +/// +/// +public interface ITransform where TVector : IVector, new() +{ + /// Gets or set the the position of the object. + public TVector Position { get; set; } + + /// Gets or set the the scale of the object. + public TVector Scale { get; set; } + + /// Gets or set the the rotation of the object. + public Quaternion Rotation { get; set; } + + /// Gets or set the the model matrix of the object. + public Matrix4x4 ModelMatrix { get; } +} \ No newline at end of file diff --git a/Core/Entities/Properties/Material.cs b/Core/Entities/Properties/Material.cs index b30f02b..5f9083e 100644 --- a/Core/Entities/Properties/Material.cs +++ b/Core/Entities/Properties/Material.cs @@ -12,10 +12,11 @@ public class Material /// /// Initializes a new instance of . /// - public Material(Shader shader, Texture diffuseMap) + public Material(Shader shader, Texture diffuseMap, Texture? specularMap = null) { Shader = shader; DiffuseMap = diffuseMap; + SpecularMap = specularMap ?? diffuseMap; Specular = new(0.5f, 0.5f, 0.5f); Shininess = 32.0f; @@ -29,7 +30,10 @@ public Material(Shader shader, Texture diffuseMap) /// /// Gets or sets the specular map texture. /// - public Texture? SpecularMap { get; set; } + public Texture SpecularMap { get; set; } + + /// Gets whether the material uses a specular map. + public bool UseSpecularMap => SpecularMap.Handle != DiffuseMap.Handle; /// /// Gets or sets the shader used to render the material. diff --git a/Core/Entities/Properties/Meshes/Mesh.cs b/Core/Entities/Properties/Meshes/Mesh.cs index 5685204..dcd8d94 100644 --- a/Core/Entities/Properties/Meshes/Mesh.cs +++ b/Core/Entities/Properties/Meshes/Mesh.cs @@ -7,7 +7,7 @@ namespace SharpEngine.Core.Entities.Properties.Meshes; /// public class Mesh { - /// Gets or sets a indetifying name for the mesh. + /// Gets or sets a identifying name for the mesh. public string Name { get; set; } = "New Mesh"; /// @@ -27,4 +27,10 @@ public class Mesh /// [Inspector(DisplayInInspector = false)] public float[] TextureCoordinates { get; set; } = []; + + /// + /// Gets or sets the indicies of the mesh. + /// + [Inspector(DisplayInInspector = false)] + public uint[] Indices { get; set; } = []; } \ No newline at end of file diff --git a/Core/Entities/Properties/Meshes/MeshService.cs b/Core/Entities/Properties/Meshes/MeshService.cs index 67e5300..9b34f87 100644 --- a/Core/Entities/Properties/Meshes/MeshService.cs +++ b/Core/Entities/Properties/Meshes/MeshService.cs @@ -1,5 +1,9 @@ -using Silk.NET.OpenGL; +using SharpEngine.Core.Windowing; +using Silk.NET.OpenGL; +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; namespace SharpEngine.Core.Entities.Properties.Meshes; @@ -36,5 +40,91 @@ public Mesh LoadMesh(string identifier, Mesh mesh) return mesh; } - // TODO: #3 & #2 Load mesh from file + const string FbxExtension = ".fbx"; + const string ObjExtension = ".obj"; + + /// + /// Loads a mesh based on the file extension of the provided path. + /// + /// Used to specify the unique identifier for the mesh being loaded. + /// Specifies the file path of the mesh to be loaded, which determines the loading method based on its extension. + /// A collection of objects loaded from the specified file. + /// Thrown when the file extension of the provided path is not recognized as a supported mesh format. + public IEnumerable LoadMesh(string identifier, string meshFilePath) + => Path.GetExtension(meshFilePath) switch + { + FbxExtension => LoadFbx(identifier, meshFilePath), + ObjExtension => LoadObj(identifier, meshFilePath), + _ => throw new NotSupportedException($"The file extension {Path.GetExtension(meshFilePath)} is not a supported mesh file.") + }; + + // TODO: #2 Load obj mesh from file + private IEnumerable LoadObj(string identifier, string meshFilePath) + { + var vertices = new List(); + var normals = new List(); + var textureCoordinates = new List(); + var indices = new List(); + + Mesh LoadMeshIntoBuffer() + { + var mesh = LoadMesh(identifier, new Mesh + { + Vertices = [.. vertices], + Normals = [.. normals], + TextureCoordinates = [.. textureCoordinates], + Indices = [.. indices] + }); + + vertices.Clear(); + normals.Clear(); + textureCoordinates.Clear(); + indices.Clear(); + + return mesh; + } + + static IEnumerable GetData(string[] parts) => parts.Skip(1).Select(float.Parse); + + var contents = File.ReadAllLines(meshFilePath); + foreach (var line in contents) + { + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) continue; + + switch (parts[0]) + { + case ObjMeshData.VERTEX: + vertices.AddRange(GetData(parts)); + break; + case ObjMeshData.VERTEX_NORMAL: + normals.AddRange(GetData(parts)); + break; + case ObjMeshData.VERTEX_TEXTURE: + textureCoordinates.AddRange(GetData(parts)); + break; + case ObjMeshData.FACE: // OBJ faces are defined as a list of vertex indices + foreach (var part in parts.Skip(1)) + { + var indicesParts = part.Split('/'); + indices.Add(uint.Parse(indicesParts[0]) - 1); // OBJ indices are 1-based + } + break; + case ObjMeshData.OBJECT: + if (vertices.Count > 0) + yield return LoadMeshIntoBuffer(); + break; + } + } + + if (vertices.Count > 0) + yield return LoadMeshIntoBuffer(); + } + + // TODO: #3 Load fbx mesh from file + private IEnumerable LoadFbx(string identifier, string meshFilePath) + { + throw new NotImplementedException(); + } + } \ No newline at end of file diff --git a/Core/Entities/Properties/Meshes/ObjMeshData.cs b/Core/Entities/Properties/Meshes/ObjMeshData.cs new file mode 100644 index 0000000..e1e45de --- /dev/null +++ b/Core/Entities/Properties/Meshes/ObjMeshData.cs @@ -0,0 +1,22 @@ +namespace SharpEngine.Core.Entities.Properties.Meshes; + +/// +/// Contains the different types of data the .obj file contains. +/// +public static class ObjMeshData +{ + /// Represents a line of vertex data. + public const string VERTEX = "v"; + + /// Represents a line of vertex normal data. + public const string VERTEX_NORMAL = "vn"; + + /// Represents a line of uv coordinate data. + public const string VERTEX_TEXTURE = "vt"; + + /// Represents a line of vertex indicie data. + public const string FACE = "f"; + + /// Represents that a new object begins. + public const string OBJECT = "o"; +} \ No newline at end of file diff --git a/Core/Entities/Properties/VertexData.cs b/Core/Entities/Properties/Meshes/VertexData.cs similarity index 90% rename from Core/Entities/Properties/VertexData.cs rename to Core/Entities/Properties/Meshes/VertexData.cs index c25e5d4..f9172c2 100644 --- a/Core/Entities/Properties/VertexData.cs +++ b/Core/Entities/Properties/Meshes/VertexData.cs @@ -1,11 +1,11 @@ -namespace SharpEngine.Core.Entities.Properties; +namespace SharpEngine.Core.Entities.Properties.Meshes; /// /// Represents the data of a vertex. /// public struct VertexData { - /// The stride or lenght of the vertex data object. + /// The stride or length of the vertex data object. public const int Stride = 8 * sizeof(float); /// The size of the vertices in the vertex data object. diff --git a/Core/Entities/Properties/Transform.cs b/Core/Entities/Properties/Transform.cs index 50b8bea..81f0467 100644 --- a/Core/Entities/Properties/Transform.cs +++ b/Core/Entities/Properties/Transform.cs @@ -1,12 +1,18 @@ -using System.Numerics; +using SharpEngine.Core.Numerics; +using Matrix4x4 = System.Numerics.Matrix4x4; namespace SharpEngine.Core.Entities.Properties; /// /// Represents a game object transformation in 3D space. /// -public class Transform +public class Transform : ITransform { + /// + /// Initializes a new instance of the class. + /// + public Transform() { } + /// /// Gets or sets the position of the game object. /// @@ -25,7 +31,7 @@ public class Transform /// /// Gets the transformation of the game object as a model matrix. /// - public Matrix4x4 ModelMatrix => Matrix4x4.CreateScale(Scale) * + public Matrix4x4 ModelMatrix => Matrix4x4.CreateScale(new System.Numerics.Vector3(Scale.X, Scale.Y, Scale.Z)) * Matrix4x4.CreateFromAxisAngle(Rotation.Axis, Math.DegreesToRadians(Rotation.Angle)) * - Matrix4x4.CreateTranslation(Position); + Matrix4x4.CreateTranslation(new System.Numerics.Vector3(Position.X, Position.Y, Position.Z)); } diff --git a/Core/Entities/Properties/Transform2D.cs b/Core/Entities/Properties/Transform2D.cs index cbcbbee..8a44d56 100644 --- a/Core/Entities/Properties/Transform2D.cs +++ b/Core/Entities/Properties/Transform2D.cs @@ -1,16 +1,17 @@ using System.Numerics; +using Vector2 = SharpEngine.Core.Numerics.Vector2; namespace SharpEngine.Core.Entities.Properties; /// /// Represents a game object transformation in 2D space. /// -public class Transform2D +public class Transform2D : ITransform { /// /// Gets or sets the position of the game object. /// - public Vector2 Position { get; set; } + public Vector2 Position { get; set; } = new(0, 0); /// /// Gets or sets the scale of the game object. @@ -20,12 +21,14 @@ public class Transform2D /// /// Gets or sets the rotation of the game object. /// - public float Rotation { get; set; } + public Quaternion Rotation { get; set; } = new() { Angle = 0 }; /// /// Gets the transformation of the game object as a model matrix. /// public Matrix4x4 ModelMatrix => Matrix4x4.CreateScale(new Vector3(Scale.X, Scale.Y, 0)) * - Matrix4x4.CreateRotationZ(Rotation) * - Matrix4x4.CreateTranslation(new Vector3(Position.X, Position.Y, 0)); + // Matrix4x4.CreateRotationZ(Math.DegreesToRadians(Rotation.Angle)) * + Matrix4x4.CreateRotationZ(0) * + // Matrix4x4.CreateTranslation(new Vector3(Position.X, Position.Y, 0)); + Matrix4x4.CreateTranslation(new Vector3(0, 0, 0)); } \ No newline at end of file diff --git a/Core/Entities/UI/Layouts/FlexLayout.cs b/Core/Entities/UI/Layouts/FlexLayout.cs new file mode 100644 index 0000000..b0dded0 --- /dev/null +++ b/Core/Entities/UI/Layouts/FlexLayout.cs @@ -0,0 +1,24 @@ +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Scenes; +using System; +using System.Numerics; + +namespace SharpEngine.Core.Entities.UI.Layouts; + +/// +/// Represents a layout that dynamically distributes items in a flex layout. +/// +/// The type of items in the collection. +/// The amount of dimensions in the content area. +public class FlexLayout : LayoutBase where T : SceneNode, new() where TVector : IVector, new() +{ + /// Gets or sets the gap between each object. + public int Gap { get; set; } + + /// Gets or sets the dimensions of the content area. + public required IVector ContentAreaSize { get; set; } + + /// + /// + public override T[][] GetValues() => throw new NotImplementedException(); +} diff --git a/Core/Entities/UI/Layouts/GridLayout.cs b/Core/Entities/UI/Layouts/GridLayout.cs new file mode 100644 index 0000000..993a73c --- /dev/null +++ b/Core/Entities/UI/Layouts/GridLayout.cs @@ -0,0 +1,88 @@ +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Scenes; +using System; + +namespace SharpEngine.Core.Entities.UI.Layouts; + +/// +/// Represents a grid layout. +/// +/// The type of items that can be stored and retrieved within the grid. +public class GridLayout : LayoutBase where TItem : SceneNode, new() +{ + /// Gets or sets the amount of rows in the grid. + public uint Rows { get; set; } = 2; + + /// Gets or sets the amount of columns in the grid. + public uint Columns { get; set; } = 3; + + /// + /// Retrieves the item at [, ]. + /// + /// The row where the items should be retrieved. + /// The column where the item should be retrieved. + /// The item at [, ]. + public TItem this[uint row, uint column] + { + get + { + uint index = GetIndex(row, column); + return Items[(int)index]; + } + set + { + uint index = GetIndex(row, column); + Items[(int)index] = value; + } + } + + /// + public override void AddItem(TItem item) + { + base.AddItem(item); + + var offset = -100; // TODO: Fix this, this is just a temporary solution. + + if (Items.Count % Columns != 0) + offset = offset + (Items.Count * 30); + + // item.Transform.Position.X = offset; + } + + /// + /// Calculates the index in the flat list based on row and column. + /// + /// The row where the items should be retrieved. + /// The column where the item should be retrieved. + /// The index of the item at [, ]. + /// + private uint GetIndex(uint row, uint column) + { + if (row < 0 || column < 0 || column >= Columns) + throw new ArgumentOutOfRangeException(nameof(row), "Invalid row or column index."); + + var index = row * Columns + column; + if (index >= Items.Count) + throw new ArgumentOutOfRangeException(nameof(row), "Index exceeds the number of items."); + + return index; + } + + /// + public override TItem[][] GetValues() + { + var values = new TItem[Rows][]; + for (uint i = 0; i < Rows; i++) + { + values[i] = new TItem[Columns]; + for (uint j = 0; j < Columns; j++) + { + var item = this[i, j]; + values[i][j] = item; + } + } + + return values; + } +} diff --git a/Core/Entities/UI/Layouts/LayoutBase.cs b/Core/Entities/UI/Layouts/LayoutBase.cs new file mode 100644 index 0000000..bb65554 --- /dev/null +++ b/Core/Entities/UI/Layouts/LayoutBase.cs @@ -0,0 +1,44 @@ +using SharpEngine.Core.Scenes; +using System.Collections.Generic; + +namespace SharpEngine.Core.Entities.UI.Layouts; + +/// +/// Represents the base class for all layout types. +/// +/// +public abstract class LayoutBase : SceneNode where TItem : SceneNode, new() +{ + /// Represents the items in the layout. + public List Items { get; set; } = []; + + /// + /// Adds a new item to the layout. + /// + /// The items to be added. + /// The object itself. + public override LayoutBase AddChild(params SceneNode[] nodes) + { + base.AddChild(nodes); + + foreach (var node in nodes) + AddItem((TItem)node); + + return this; + } + + /// + /// Adds and item to the container. + /// + /// The item to be added. + public virtual void AddItem(TItem item) + { + Items.Add(item); + } + + /// + /// Gets the items in the container. + /// + /// The items as a 2D array representation. + public abstract TItem[][] GetValues(); +} diff --git a/Core/Entities/UI/UIElement.cs b/Core/Entities/UI/UIElement.cs new file mode 100644 index 0000000..068a40f --- /dev/null +++ b/Core/Entities/UI/UIElement.cs @@ -0,0 +1,110 @@ +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; + +using Silk.NET.OpenGL; +using System.Numerics; +using System.Threading.Tasks; +using Vector2 = SharpEngine.Core.Numerics.Vector2; + +namespace SharpEngine.Core.Entities.UI; + +/// +/// Represents a User Interface entity. +/// +public class UIElement : EmptyNode, IRenderable +{ + /// + /// Initializes a new instance of . + /// + public UIElement() : this("UIElement") { } + + /// + /// Initializes a new instance of . + /// + /// The name of the UI element. + public UIElement(string name) : base(name) + { + // TODO: Support custom meshes? + Mesh = MeshService.Instance.LoadMesh(nameof(Primitives.Plane), Primitives.Plane.Mesh); + + Initialize(); + } + + private readonly UIShader _uiShader = new(); + + /// Gets or sets the width of the ui element. + public float Width { get; set; } = 10; + + /// Gets or sets the height of the ui element. + public float Height { get; set; } = 10; + + /// Gets or sets the mesh of the UI element. + public Mesh Mesh { get; set; } + + /// + public uint VAO { get; set; } + + /// + public void Initialize() + { + VAO = Window.GL.GenVertexArray(); + Bind(); + + InitializeBuffers(Mesh); + + _uiShader.Shader?.Use(); + _uiShader.SetAttributes(); + } + + /// + public void Bind() + { + Window.GL.BindVertexArray(VAO); + } + + /// + public void InitializeBuffers(Mesh mesh) + { + var vertexBufferObject = Window.GL.GenBuffer(); + Window.GL.BindBuffer(GLEnum.ArrayBuffer, vertexBufferObject); + Window.GL.BufferData(GLEnum.ArrayBuffer, mesh.GetVertices(), GLEnum.StaticDraw); + + var elementBufferObject = Window.GL.GenBuffer(); + Window.GL.BindBuffer(GLEnum.ElementArrayBuffer, elementBufferObject); + Window.GL.BufferData(GLEnum.ElementArrayBuffer, mesh.Indices, GLEnum.StaticDraw); + } + + Matrix4x4 OrthoMatrix = Matrix4x4.CreateOrthographicOffCenter(-1, 1, -1, 1, -1, 1); + + /// + /// Render the UI element. + /// + public override Task Render(CameraView camera, Window window) + { + if (_uiShader.Shader is null) + return Task.CompletedTask; + + Bind(); + + // TODO: These should come from somewhere else. + const float screenWidth = 1280; + const float screenHeight = 720; + + _uiShader.Shader.SetFloat("width", Width); + _uiShader.Shader.SetFloat("height", Height); + _uiShader.Shader.SetVector2("screenSize", new System.Numerics.Vector2(screenWidth, screenHeight)); + _uiShader.Shader.SetVector2("position", (System.Numerics.Vector2)Transform.Position); + _uiShader.Shader.SetFloat("rotation", Math.DegreesToRadians(Transform.Rotation.Angle)); + _uiShader.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); + _uiShader.Shader.SetMatrix4("orthoMatrix", OrthoMatrix); // Pass the orthographic matrix to the shader + + Window.GL.DrawElements(PrimitiveType.Triangles, (uint)Mesh.Indices.Length, DrawElementsType.UnsignedInt, []); + + return Task.CompletedTask; + } +} diff --git a/Core/Entities/UIElement.cs b/Core/Entities/UIElement.cs deleted file mode 100644 index 1664569..0000000 --- a/Core/Entities/UIElement.cs +++ /dev/null @@ -1,94 +0,0 @@ -using SharpEngine.Core.Entities.Properties; -using SharpEngine.Core.Entities.Properties.Meshes; -using SharpEngine.Core.Scenes; -using SharpEngine.Core.Shaders; - -using Silk.NET.OpenGL; -using System; -using System.Threading.Tasks; - -namespace SharpEngine.Core.Entities; - -/// -/// Represents a User Interface entity. -/// -public class UIElement : SceneNode -{ - /// - /// Initializes a new instance of . - /// - /// The name of the UI element. - public UIElement(string name) - { - Name = name; - // Mesh = MeshService.Instance.LoadMesh("plane", Primitives.Plane.Mesh); - } - - private readonly UIShader _uIShader = new(); - - /// Gets or sets the 2D space transformation of the UI element. - public Transform2D Transform { get; set; } = new() - { - Rotation = 180 - }; - - // TODO: Use actual mesh - /// Gets or sets the mesh of the UI element. - public Mesh Mesh { get; set; } // = new(); - - private readonly float[] _vertices = - [ - // Position Texture coordinates - 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // top right - 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom right - -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // bottom left - -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, // top left - ]; - - private readonly uint[] _indices = - [ - 0, 1, 3, - 1, 2, 3 - ]; - - private uint _vertexArrayObject; - - /// - public void Initialize() - { - _vertexArrayObject = Window.GL.GenVertexArray(); - Bind(); - - InitializeBuffers(); - } - - public void Bind() - { - Window.GL.BindVertexArray(_vertexArrayObject); - } - - private void InitializeBuffers() - { - var vertexBufferObject = Window.GL.GenBuffer(); - Window.GL.BindBuffer(GLEnum.ArrayBuffer, vertexBufferObject); - Window.GL.BufferData(GLEnum.ArrayBuffer, _vertices, GLEnum.StaticDraw); - - var elementBufferObject = Window.GL.GenBuffer(); - Window.GL.BindBuffer(GLEnum.ElementArrayBuffer, elementBufferObject); - Window.GL.BufferData(GLEnum.ElementArrayBuffer, _indices, GLEnum.StaticDraw); - } - - /// - /// Render the UI element. - /// - public override Task Render() - { - Window.GL.BindVertexArray(_vertexArrayObject); - - _uIShader.Shader.SetMatrix4(ShaderAttributes.Model, Transform.ModelMatrix); - - Window.GL.DrawElements(PrimitiveType.Triangles, (uint)_indices.Length, DrawElementsType.UnsignedInt, []); - - return Task.CompletedTask; - } -} diff --git a/Core/Entities/Views/View.cs b/Core/Entities/Views/View.cs index 8f78b02..88992a0 100644 --- a/Core/Entities/Views/View.cs +++ b/Core/Entities/Views/View.cs @@ -21,7 +21,6 @@ public View(IViewSettings settings) Settings = settings; } - // Those vectors are directions pointing outwards from the camera to define how it rotated. protected Vector3 _front = -Vector3.UnitZ; protected Vector3 _up = Vector3.UnitY; protected Vector3 _right = Vector3.UnitX; @@ -35,15 +34,15 @@ public View(IViewSettings settings) /// Gets the right vector of the camera. public Vector3 Right => _right; + /// Gets or sets the position of the camera. + public Vector3 Position { get; set; } + /// Gets or sets the settings for the view. public IViewSettings Settings { get; set; } /// Gets or sets the aspect ratio of the viewport, used for the projection matrix. public float AspectRatio { get; set; } - /// Gets or sets the position of the camera. - public Vector3 Position { get; set; } - private protected bool firstMove; private protected Vector2 lastPos; diff --git a/Core/Extensions/CollectionExtensions.cs b/Core/Extensions/CollectionExtensions.cs index aab5eb9..fa8686d 100644 --- a/Core/Extensions/CollectionExtensions.cs +++ b/Core/Extensions/CollectionExtensions.cs @@ -13,7 +13,7 @@ public static class CollectionExtensions /// /// The type of the item. /// The item which is being checked if it's in the collection. - /// The collection where the item's existance is being checked. + /// The collection where the item's existence is being checked. /// if the item is in the collection; otherwise, . public static bool IsAnyOf(this T item, params T[] items) => items.Contains(item); diff --git a/Core/GlobalSuppressions.cs b/Core/GlobalSuppressions.cs deleted file mode 100644 index b944386..0000000 --- a/Core/GlobalSuppressions.cs +++ /dev/null @@ -1,8 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Minor Code Smell", "S2344:Enumeration type names should not have \"Flags\" or \"Enum\" suffixes", Justification = "", Scope = "type", Target = "~T:SharpEngine.Core.Renderers.RenderFlags")] diff --git a/Core/Math.cs b/Core/Math.cs index 5006ec2..7b74cad 100644 --- a/Core/Math.cs +++ b/Core/Math.cs @@ -27,7 +27,7 @@ public static ReadOnlySpan ToSpan(this Matrix4x4 matrix) /// /// Converts a value in to radians. /// - /// The degress to be converted + /// The degrees to be converted. /// The given in radians. public static double DegreesToRadians(double degrees) => System.Math.PI / 180 * degrees; @@ -40,7 +40,7 @@ public static float DegreesToRadians(float degrees) /// Converts a value in to degrees. /// /// The radians to convert. - /// The given as degress. + /// The given as degrees. public static float RadiansToDegrees(float radians) => radians * (180 / MathF.PI); } diff --git a/Core/Numerics/HashCodeHelper.cs b/Core/Numerics/HashCodeHelper.cs new file mode 100644 index 0000000..8731224 --- /dev/null +++ b/Core/Numerics/HashCodeHelper.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/HashCodeHelper.cs + +namespace SharpEngine.Core.Numerics +{ + internal static class HashCodeHelper + { + /// + /// Combines two hash codes, useful for combining hash codes of individual vector elements + /// + internal static int CombineHashCodes(int h1, int h2) + { + return (((h1 << 5) + h1) ^ h2); + } + } +} \ No newline at end of file diff --git a/Core/Numerics/IVector.cs b/Core/Numerics/IVector.cs new file mode 100644 index 0000000..c581a9b --- /dev/null +++ b/Core/Numerics/IVector.cs @@ -0,0 +1,15 @@ +using System; + +namespace SharpEngine.Core.Numerics; + +/// +/// An abstraction of the different vector types. +/// +public interface IVector +{ + /// + public Single X { get; set; } + + /// + public Single Y { get; set; } +} \ No newline at end of file diff --git a/Core/Numerics/JitIntrinsicAttribute.cs b/Core/Numerics/JitIntrinsicAttribute.cs new file mode 100644 index 0000000..0e53d20 --- /dev/null +++ b/Core/Numerics/JitIntrinsicAttribute.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/JITIntrinsicAttribute.cs + +using System; + +namespace SharpEngine.Core.Numerics +{ + /// + /// An attribute that can be attached to JIT Intrinsic methods/properties + /// + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Property)] + internal class JitIntrinsicAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/Core/Numerics/Vector2.cs b/Core/Numerics/Vector2.cs new file mode 100644 index 0000000..70b52ef --- /dev/null +++ b/Core/Numerics/Vector2.cs @@ -0,0 +1,459 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/Vector2.cs + +using System; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; + +using Vector = System.Numerics.Vector; +using Matrix4x4 = System.Numerics.Matrix4x4; +using Matrix3x2 = System.Numerics.Matrix3x2; +using Quaternion = System.Numerics.Quaternion; + +namespace SharpEngine.Core.Numerics +{ + /// + /// A structure encapsulating two single precision floating point values and provides hardware accelerated methods. + /// + public partial struct Vector2 : IEquatable, IFormattable + { + #region Public Static Properties + /// + /// Returns the vector (0,0). + /// + public static Vector2 Zero => new(); + /// + /// Returns the vector (1,1). + /// + public static Vector2 One => new Vector2(1.0f, 1.0f); + /// + /// Returns the vector (1,0). + /// + public static Vector2 UnitX { get { return new Vector2(1.0f, 0.0f); } } + /// + /// Returns the vector (0,1). + /// + public static Vector2 UnitY { get { return new Vector2(0.0f, 1.0f); } } + #endregion Public Static Properties + + #region Public instance methods + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + int hash = this.X.GetHashCode(); + hash = HashCodeHelper.CombineHashCodes(hash, this.Y.GetHashCode()); + return hash; + } + + /// + /// Returns a boolean indicating whether the given Object is equal to this Vector2 instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this Vector2; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (obj is not Vector2) + return false; + return Equals((Vector2)obj); + } + + /// + /// Returns a String representing this Vector2 instance. + /// + /// The string representation. + public override string ToString() + { + return ToString("G", CultureInfo.CurrentCulture); + } + + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements. + /// + /// The format of individual elements. + /// The string representation. + public string ToString(string format) + { + return ToString(format, CultureInfo.CurrentCulture); + } + + /// + /// Returns a String representing this Vector2 instance, using the specified format to format individual elements + /// and the given IFormatProvider. + /// + /// The format of individual elements. + /// The format provider to use when formatting elements. + /// The string representation. + public string ToString(string? format, IFormatProvider? formatProvider) + { + StringBuilder sb = new StringBuilder(); + string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; + sb.Append('<'); + sb.Append(this.X.ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(this.Y.ToString(format, formatProvider)); + sb.Append('>'); + return sb.ToString(); + } + + /// + /// Returns the length of the vector. + /// + /// The vector's length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Length() + { + if (Vector.IsHardwareAccelerated) + { + float ls = Vector2.Dot(this, this); + return (float)MathF.Sqrt(ls); + } + else + { + float ls = X * X + Y * Y; + return MathF.Sqrt(ls); + } + } + + /// + /// Returns the length of the vector squared. This operation is cheaper than Length(). + /// + /// The vector's length squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float LengthSquared() + { + if (Vector.IsHardwareAccelerated) + { + return Vector2.Dot(this, this); + } + else + { + return X * X + Y * Y; + } + } + #endregion Public Instance Methods + + #region Public Static Methods + /// + /// Returns the Euclidean distance between the two given points. + /// + /// The first point. + /// The second point. + /// The distance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Distance(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) + { + Vector2 difference = value1 - value2; + float ls = Vector2.Dot(difference, difference); + return (float)System.MathF.Sqrt(ls); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + + float ls = dx * dx + dy * dy; + + return MathF.Sqrt(ls); + } + } + + /// + /// Returns the Euclidean distance squared between the two given points. + /// + /// The first point. + /// The second point. + /// The distance squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceSquared(Vector2 value1, Vector2 value2) + { + if (Vector.IsHardwareAccelerated) + { + Vector2 difference = value1 - value2; + return Vector2.Dot(difference, difference); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + + return dx * dx + dy * dy; + } + } + + /// + /// Returns a vector with the same direction as the given vector, but with a length of 1. + /// + /// The vector to normalize. + /// The normalized vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Normalize(Vector2 value) + { + if (Vector.IsHardwareAccelerated) + { + float length = value.Length(); + return value / length; + } + else + { + float ls = value.X * value.X + value.Y * value.Y; + float invNorm = 1.0f / MathF.Sqrt(ls); + + return new Vector2( + value.X * invNorm, + value.Y * invNorm); + } + } + + /// + /// Returns the reflection of a vector off a surface that has the specified normal. + /// + /// The source vector. + /// The normal of the surface being reflected off. + /// The reflected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Reflect(Vector2 vector, Vector2 normal) + { + if (Vector.IsHardwareAccelerated) + { + float dot = Vector2.Dot(vector, normal); + return vector - (2 * dot * normal); + } + else + { + float dot = vector.X * normal.X + vector.Y * normal.Y; + + return new Vector2( + vector.X - 2.0f * dot * normal.X, + vector.Y - 2.0f * dot * normal.Y); + } + } + + /// + /// Restricts a vector between a min and max value. + /// + /// The source vector. + /// The minimum value. + /// The maximum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Clamp(Vector2 value1, Vector2 min, Vector2 max) + { + // This compare order is very important!!! + // We must follow HLSL behavior in the case user specified min value is bigger than max value. + float x = value1.X; + x = (x > max.X) ? max.X : x; + x = (x < min.X) ? min.X : x; + + float y = value1.Y; + y = (y > max.Y) ? max.Y : y; + y = (y < min.Y) ? min.Y : y; + + return new Vector2(x, y); + } + + /// + /// Linearly interpolates between two vectors based on the given weighting. + /// + /// The first source vector. + /// The second source vector. + /// Value between 0 and 1 indicating the weight of the second source vector. + /// The interpolated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Lerp(Vector2 value1, Vector2 value2, float amount) + { + return new Vector2( + value1.X + (value2.X - value1.X) * amount, + value1.Y + (value2.Y - value1.Y) * amount); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix3x2 matrix) + { + return new Vector2( + position.X * matrix.M11 + position.Y * matrix.M21 + matrix.M31, + position.X * matrix.M12 + position.Y * matrix.M22 + matrix.M32); + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 position, Matrix4x4 matrix) + { + return new Vector2( + position.X * matrix.M11 + position.Y * matrix.M21 + matrix.M41, + position.X * matrix.M12 + position.Y * matrix.M22 + matrix.M42); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix3x2 matrix) + { + return new Vector2( + normal.X * matrix.M11 + normal.Y * matrix.M21, + normal.X * matrix.M12 + normal.Y * matrix.M22); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 TransformNormal(Vector2 normal, Matrix4x4 matrix) + { + return new Vector2( + normal.X * matrix.M11 + normal.Y * matrix.M21, + normal.X * matrix.M12 + normal.Y * matrix.M22); + } + + /// + /// Transforms a vector by the given Quaternion rotation value. + /// + /// The source vector to be rotated. + /// The rotation to apply. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Transform(Vector2 value, Quaternion rotation) + { + float x2 = rotation.X + rotation.X; + float y2 = rotation.Y + rotation.Y; + float z2 = rotation.Z + rotation.Z; + + float wz2 = rotation.W * z2; + float xx2 = rotation.X * x2; + float xy2 = rotation.X * y2; + float yy2 = rotation.Y * y2; + float zz2 = rotation.Z * z2; + + return new Vector2( + value.X * (1.0f - yy2 - zz2) + value.Y * (xy2 - wz2), + value.X * (xy2 + wz2) + value.Y * (1.0f - xx2 - zz2)); + } + #endregion Public Static Methods + + #region Public operator methods + // all the below methods should be inlined as they are + // implemented over JIT intrinsics + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Add(Vector2 left, Vector2 right) + { + return left + right; + } + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Subtract(Vector2 left, Vector2 right) + { + return left - right; + } + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, Vector2 right) + { + return left * right; + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Vector2 left, Single right) + { + return left * right; + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Multiply(Single left, Vector2 right) + { + return left * right; + } + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, Vector2 right) + { + return left / right; + } + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Divide(Vector2 left, Single divisor) + { + return left / divisor; + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Negate(Vector2 value) + { + return -value; + } + + #endregion Public operator methods + } +} \ No newline at end of file diff --git a/Core/Numerics/Vector2_Intrinsics.cs b/Core/Numerics/Vector2_Intrinsics.cs new file mode 100644 index 0000000..2ac4fb6 --- /dev/null +++ b/Core/Numerics/Vector2_Intrinsics.cs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using SharpEngine.Core.Numerics; +using System; +using System.Runtime.CompilerServices; + +namespace SharpEngine.Core.Numerics +{ + // This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. + // The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. + // The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. + // Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() + + public partial struct Vector2 : IVector + { + /// + /// The X component of the vector. + /// + public Single X { get; set; } + + /// + /// The Y component of the vector. + /// + public Single Y { get; set; } + + #region Constructors + /// + /// Constructs a vector whose elements are all the single specified value. + /// + /// The element to fill the vector with. + [JitIntrinsic] + public Vector2(Single value) : this(value, value) { } + + /// + /// Constructs a vector with the given individual elements. + /// + /// The X component. + /// The Y component. + [JitIntrinsic] + public Vector2(Single x, Single y) + { + X = x; + Y = y; + } + #endregion Constructors + + #region Public Instance Methods + /// + /// Copies the contents of the vector into the given array. + /// + /// The destination array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyTo(Single[] array) + { + CopyTo(array, 0); + } + + /// + /// Copies the contents of the vector into the given array, starting from the given index. + /// + /// If array is null. + /// If array is multidimensional. + /// If index is greater than end of the array or index is less than zero. + /// If number of elements in source vector is greater than those available in destination array + /// or if there are not enough elements to copy. + public void CopyTo(Single[] array, int index) + { + if (array == null) + { + // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. + throw new NullReferenceException(); + } + if (index < 0 || index >= array.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + if ((array.Length - index) < 2) + { + throw new ArgumentException(nameof(index)); + } + array[index] = X; + array[index + 1] = Y; + } + + /// + /// Returns a boolean indicating whether the given Vector2 is equal to this Vector2 instance. + /// + /// The Vector2 to compare this instance to. + /// True if the other Vector2 is equal to this instance; False otherwise. + [JitIntrinsic] + public bool Equals(Vector2 other) + { + return this.X == other.X && this.Y == other.Y; + } + #endregion Public Instance Methods + + #region Public Static Methods + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(Vector2 value1, Vector2 value2) + { + return value1.X * value2.X + + value1.Y * value2.Y; + } + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The minimized vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Min(Vector2 value1, Vector2 value2) + { + return new Vector2( + (value1.X < value2.X) ? value1.X : value2.X, + (value1.Y < value2.Y) ? value1.Y : value2.Y); + } + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors + /// + /// The first source vector + /// The second source vector + /// The maximized vector + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Max(Vector2 value1, Vector2 value2) + { + return new Vector2( + (value1.X > value2.X) ? value1.X : value2.X, + (value1.Y > value2.Y) ? value1.Y : value2.Y); + } + + /// + /// Returns a vector whose elements are the absolute values of each of the source vector's elements. + /// + /// The source vector. + /// The absolute value vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 Abs(Vector2 value) + { + return new Vector2(MathF.Abs(value.X), MathF.Abs(value.Y)); + } + + /// + /// Returns a vector whose elements are the square root of each of the source vector's elements. + /// + /// The source vector. + /// The square root vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 SquareRoot(Vector2 value) + { + return new Vector2((Single)MathF.Sqrt(value.X), (Single)MathF.Sqrt(value.Y)); + } + #endregion Public Static Methods + + #region Public Static Operators + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator +(Vector2 left, Vector2 right) + { + return new Vector2(left.X + right.X, left.Y + right.Y); + } + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator -(Vector2 left, Vector2 right) + { + return new Vector2(left.X - right.X, left.Y - right.Y); + } + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator *(Vector2 left, Vector2 right) + { + return new Vector2(left.X * right.X, left.Y * right.Y); + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator *(Single left, Vector2 right) + { + return new Vector2(left, left) * right; + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator *(Vector2 left, Single right) + { + return left * new Vector2(right, right); + } + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator /(Vector2 left, Vector2 right) + { + return new Vector2(left.X / right.X, left.Y / right.Y); + } + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator /(Vector2 value1, float value2) + { + float invDiv = 1.0f / value2; + return new Vector2( + value1.X * invDiv, + value1.Y * invDiv); + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 operator -(Vector2 value) + { + return Zero - value; + } + + /// + /// Returns a boolean indicating whether the two given vectors are equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are equal; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector2 left, Vector2 right) + { + return left.Equals(right); + } + + /// + /// Returns a boolean indicating whether the two given vectors are not equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are not equal; False if they are equal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector2 left, Vector2 right) + { + return !(left == right); + } + + /// + /// Converts the SharpEngine representation to a System.Numerics representation. + /// + /// The value to convert + public static explicit operator System.Numerics.Vector2(Vector2 value) + => new(value.X, value.Y); + + /// + /// Converts the System.Numerics representation to a SharpEngine representation. + /// + /// The value to convert + public static explicit operator Vector2(System.Numerics.Vector2 value) + => new(value.X, value.Y); + + #endregion Public Static Operators + } +} \ No newline at end of file diff --git a/Core/Numerics/Vector3.cs b/Core/Numerics/Vector3.cs new file mode 100644 index 0000000..f34601a --- /dev/null +++ b/Core/Numerics/Vector3.cs @@ -0,0 +1,477 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// https://github.com/microsoft/referencesource/blob/master/System.Numerics/System/Numerics/Vector3.cs + +using System; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; + +using Vector = System.Numerics.Vector; +using Matrix4x4 = System.Numerics.Matrix4x4; +using Quaternion = System.Numerics.Quaternion; + +namespace SharpEngine.Core.Numerics +{ + /// + /// A structure encapsulating three single precision floating point values and provides hardware accelerated methods. + /// + public partial struct Vector3 : IEquatable, IFormattable + { + #region Public Static Properties + /// + /// Returns the vector (0,0,0). + /// + public static Vector3 Zero { get { return new Vector3(); } } + /// + /// Returns the vector (1,1,1). + /// + public static Vector3 One { get { return new Vector3(1.0f, 1.0f, 1.0f); } } + /// + /// Returns the vector (1,0,0). + /// + public static Vector3 UnitX { get { return new Vector3(1.0f, 0.0f, 0.0f); } } + /// + /// Returns the vector (0,1,0). + /// + public static Vector3 UnitY { get { return new Vector3(0.0f, 1.0f, 0.0f); } } + /// + /// Returns the vector (0,0,1). + /// + public static Vector3 UnitZ { get { return new Vector3(0.0f, 0.0f, 1.0f); } } + #endregion Public Static Properties + + #region Public Instance Methods + + /// + /// Returns the hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + int hash = this.X.GetHashCode(); + hash = HashCodeHelper.CombineHashCodes(hash, this.Y.GetHashCode()); + hash = HashCodeHelper.CombineHashCodes(hash, this.Z.GetHashCode()); + return hash; + } + + /// + /// Returns a boolean indicating whether the given Object is equal to this Vector3 instance. + /// + /// The Object to compare against. + /// True if the Object is equal to this Vector3; False otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Equals(object? obj) + { + if (!(obj is Vector3)) + return false; + return Equals((Vector3)obj); + } + + /// + /// Returns a String representing this Vector3 instance. + /// + /// The string representation. + public override string ToString() + { + return ToString("G", CultureInfo.CurrentCulture); + } + + /// + /// Returns a String representing this Vector3 instance, using the specified format to format individual elements. + /// + /// The format of individual elements. + /// The string representation. + public string ToString(string format) + { + return ToString(format, CultureInfo.CurrentCulture); + } + + /// + /// Returns a String representing this Vector3 instance, using the specified format to format individual elements + /// and the given IFormatProvider. + /// + /// The format of individual elements. + /// The format provider to use when formatting elements. + /// The string representation. + public string ToString(string? format, IFormatProvider? formatProvider) + { + StringBuilder sb = new StringBuilder(); + string separator = NumberFormatInfo.GetInstance(formatProvider).NumberGroupSeparator; + sb.Append('<'); + sb.Append(((IFormattable)this.X).ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(((IFormattable)this.Y).ToString(format, formatProvider)); + sb.Append(separator); + sb.Append(' '); + sb.Append(((IFormattable)this.Z).ToString(format, formatProvider)); + sb.Append('>'); + return sb.ToString(); + } + + /// + /// Returns the length of the vector. + /// + /// The vector's length. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float Length() + { + if (Vector.IsHardwareAccelerated) + { + float ls = Vector3.Dot(this, this); + return (float)System.Math.Sqrt(ls); + } + else + { + float ls = X * X + Y * Y + Z * Z; + return (float)System.Math.Sqrt(ls); + } + } + + /// + /// Returns the length of the vector squared. This operation is cheaper than Length(). + /// + /// The vector's length squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public float LengthSquared() + { + if (Vector.IsHardwareAccelerated) + { + return Vector3.Dot(this, this); + } + else + { + return X * X + Y * Y + Z * Z; + } + } + #endregion Public Instance Methods + + #region Public Static Methods + /// + /// Returns the Euclidean distance between the two given points. + /// + /// The first point. + /// The second point. + /// The distance. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Distance(Vector3 value1, Vector3 value2) + { + if (Vector.IsHardwareAccelerated) + { + Vector3 difference = value1 - value2; + float ls = Vector3.Dot(difference, difference); + return (float)System.Math.Sqrt(ls); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + float dz = value1.Z - value2.Z; + + float ls = dx * dx + dy * dy + dz * dz; + + return (float)System.Math.Sqrt((double)ls); + } + } + + /// + /// Returns the Euclidean distance squared between the two given points. + /// + /// The first point. + /// The second point. + /// The distance squared. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float DistanceSquared(Vector3 value1, Vector3 value2) + { + if (Vector.IsHardwareAccelerated) + { + Vector3 difference = value1 - value2; + return Vector3.Dot(difference, difference); + } + else + { + float dx = value1.X - value2.X; + float dy = value1.Y - value2.Y; + float dz = value1.Z - value2.Z; + + return dx * dx + dy * dy + dz * dz; + } + } + + /// + /// Returns a vector with the same direction as the given vector, but with a length of 1. + /// + /// The vector to normalize. + /// The normalized vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Normalize(Vector3 value) + { + if (Vector.IsHardwareAccelerated) + { + float length = value.Length(); + return value / length; + } + else + { + float ls = value.X * value.X + value.Y * value.Y + value.Z * value.Z; + float length = (float)System.Math.Sqrt(ls); + return new Vector3(value.X / length, value.Y / length, value.Z / length); + } + } + + /// + /// Computes the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Cross(Vector3 vector1, Vector3 vector2) + { + return new Vector3( + vector1.Y * vector2.Z - vector1.Z * vector2.Y, + vector1.Z * vector2.X - vector1.X * vector2.Z, + vector1.X * vector2.Y - vector1.Y * vector2.X); + } + + /// + /// Returns the reflection of a vector off a surface that has the specified normal. + /// + /// The source vector. + /// The normal of the surface being reflected off. + /// The reflected vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Reflect(Vector3 vector, Vector3 normal) + { + if (System.Numerics.Vector.IsHardwareAccelerated) + { + float dot = Vector3.Dot(vector, normal); + Vector3 temp = normal * dot * 2f; + return vector - temp; + } + else + { + float dot = vector.X * normal.X + vector.Y * normal.Y + vector.Z * normal.Z; + float tempX = normal.X * dot * 2f; + float tempY = normal.Y * dot * 2f; + float tempZ = normal.Z * dot * 2f; + return new Vector3(vector.X - tempX, vector.Y - tempY, vector.Z - tempZ); + } + } + + /// + /// Restricts a vector between a min and max value. + /// + /// The source vector. + /// The minimum value. + /// The maximum value. + /// The restricted vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Clamp(Vector3 value1, Vector3 min, Vector3 max) + { + // This compare order is very important!!! + // We must follow HLSL behavior in the case user specified min value is bigger than max value. + + float x = value1.X; + x = (x > max.X) ? max.X : x; + x = (x < min.X) ? min.X : x; + + float y = value1.Y; + y = (y > max.Y) ? max.Y : y; + y = (y < min.Y) ? min.Y : y; + + float z = value1.Z; + z = (z > max.Z) ? max.Z : z; + z = (z < min.Z) ? min.Z : z; + + return new Vector3(x, y, z); + } + + /// + /// Linearly interpolates between two vectors based on the given weighting. + /// + /// The first source vector. + /// The second source vector. + /// Value between 0 and 1 indicating the weight of the second source vector. + /// The interpolated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Lerp(Vector3 value1, Vector3 value2, float amount) + { + if (Vector.IsHardwareAccelerated) + { + Vector3 firstInfluence = value1 * (1f - amount); + Vector3 secondInfluence = value2 * amount; + return firstInfluence + secondInfluence; + } + else + { + return new Vector3( + value1.X + (value2.X - value1.X) * amount, + value1.Y + (value2.Y - value1.Y) * amount, + value1.Z + (value2.Z - value1.Z) * amount); + } + } + + /// + /// Transforms a vector by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Transform(Vector3 position, Matrix4x4 matrix) + { + return new Vector3( + position.X * matrix.M11 + position.Y * matrix.M21 + position.Z * matrix.M31 + matrix.M41, + position.X * matrix.M12 + position.Y * matrix.M22 + position.Z * matrix.M32 + matrix.M42, + position.X * matrix.M13 + position.Y * matrix.M23 + position.Z * matrix.M33 + matrix.M43); + } + + /// + /// Transforms a vector normal by the given matrix. + /// + /// The source vector. + /// The transformation matrix. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 TransformNormal(Vector3 normal, Matrix4x4 matrix) + { + return new Vector3( + normal.X * matrix.M11 + normal.Y * matrix.M21 + normal.Z * matrix.M31, + normal.X * matrix.M12 + normal.Y * matrix.M22 + normal.Z * matrix.M32, + normal.X * matrix.M13 + normal.Y * matrix.M23 + normal.Z * matrix.M33); + } + + /// + /// Transforms a vector by the given Quaternion rotation value. + /// + /// The source vector to be rotated. + /// The rotation to apply. + /// The transformed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Transform(Vector3 value, Quaternion rotation) + { + float x2 = rotation.X + rotation.X; + float y2 = rotation.Y + rotation.Y; + float z2 = rotation.Z + rotation.Z; + + float wx2 = rotation.W * x2; + float wy2 = rotation.W * y2; + float wz2 = rotation.W * z2; + float xx2 = rotation.X * x2; + float xy2 = rotation.X * y2; + float xz2 = rotation.X * z2; + float yy2 = rotation.Y * y2; + float yz2 = rotation.Y * z2; + float zz2 = rotation.Z * z2; + + return new Vector3( + value.X * (1.0f - yy2 - zz2) + value.Y * (xy2 - wz2) + value.Z * (xz2 + wy2), + value.X * (xy2 + wz2) + value.Y * (1.0f - xx2 - zz2) + value.Z * (yz2 - wx2), + value.X * (xz2 - wy2) + value.Y * (yz2 + wx2) + value.Z * (1.0f - xx2 - yy2)); + } + #endregion Public Static Methods + + #region Public operator methods + + // All these methods should be inlined as they are implemented + // over JIT intrinsics + + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Add(Vector3 left, Vector3 right) + { + return left + right; + } + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Subtract(Vector3 left, Vector3 right) + { + return left - right; + } + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Multiply(Vector3 left, Vector3 right) + { + return left * right; + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Multiply(Vector3 left, Single right) + { + return left * right; + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Multiply(Single left, Vector3 right) + { + return left * right; + } + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Divide(Vector3 left, Vector3 right) + { + return left / right; + } + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Divide(Vector3 left, Single divisor) + { + return left / divisor; + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Negate(Vector3 value) + { + return -value; + } + #endregion Public operator methods + } +} \ No newline at end of file diff --git a/Core/Numerics/Vector3_Intrinsics.cs b/Core/Numerics/Vector3_Intrinsics.cs new file mode 100644 index 0000000..5cccbcc --- /dev/null +++ b/Core/Numerics/Vector3_Intrinsics.cs @@ -0,0 +1,341 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Runtime.CompilerServices; + +namespace SharpEngine.Core.Numerics +{ + // This file contains the definitions for all of the JIT intrinsic methods and properties that are recognized by the current x64 JIT compiler. + // The implementation defined here is used in any circumstance where the JIT fails to recognize these members as intrinsic. + // The JIT recognizes these methods and properties by name and signature: if either is changed, the JIT will no longer recognize the member. + // Some methods declared here are not strictly intrinsic, but delegate to an intrinsic method. For example, only one overload of CopyTo() + // is actually recognized by the JIT, but both are here for simplicity. + + public partial struct Vector3 : IVector + { + /// + /// The X component of the vector. + /// + public Single X { get; set; } + + /// + /// The Y component of the vector. + /// + public Single Y { get; set; } + + /// + /// The Z component of the vector. + /// + public Single Z { get; set; } + + #region Constructors + /// + /// Constructs a vector whose elements are all the single specified value. + /// + /// The element to fill the vector with. + [JitIntrinsic] + public Vector3(Single value) : this(value, value, value) { } + + /// + /// Constructs a Vector3 from the given Vector2 and a third value. + /// + /// The Vector to extract X and Y components from. + /// The Z component. + public Vector3(Vector2 value, float z) : this(value.X, value.Y, z) { } + + /// + /// Constructs a vector with the given individual elements. + /// + /// The X component. + /// The Y component. + /// The Z component. + [JitIntrinsic] + public Vector3(Single x, Single y, Single z) + { + X = x; + Y = y; + Z = z; + } + #endregion Constructors + + #region Public Instance Methods + /// + /// Copies the contents of the vector into the given array. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyTo(Single[] array) + { + CopyTo(array, 0); + } + + /// + /// Copies the contents of the vector into the given array, starting from index. + /// + /// If array is null. + /// If array is multidimensional. + /// If index is greater than end of the array or index is less than zero. + /// If number of elements in source vector is greater than those available in destination array. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void CopyTo(Single[] array, int index) + { + if (array == null) + { + // Match the JIT's exception type here. For perf, a NullReference is thrown instead of an ArgumentNull. + throw new NullReferenceException(); + } + if (index < 0 || index >= array.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + if ((array.Length - index) < 3) + { + throw new ArgumentException("", nameof(index)); + } + + array[index] = X; + array[index + 1] = Y; + array[index + 2] = Z; + } + + /// + /// Returns a boolean indicating whether the given Vector3 is equal to this Vector3 instance. + /// + /// The Vector3 to compare this instance to. + /// True if the other Vector3 is equal to this instance; False otherwise. + [JitIntrinsic] + public bool Equals(Vector3 other) + { + return X == other.X && + Y == other.Y && + Z == other.Z; + } + #endregion Public Instance Methods + + #region Public Static Methods + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float Dot(Vector3 vector1, Vector3 vector2) + { + return vector1.X * vector2.X + + vector1.Y * vector2.Y + + vector1.Z * vector2.Z; + } + + /// + /// Returns a vector whose elements are the minimum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The minimized vector. + [JitIntrinsic] + public static Vector3 Min(Vector3 value1, Vector3 value2) + { + return new Vector3( + (value1.X < value2.X) ? value1.X : value2.X, + (value1.Y < value2.Y) ? value1.Y : value2.Y, + (value1.Z < value2.Z) ? value1.Z : value2.Z); + } + + /// + /// Returns a vector whose elements are the maximum of each of the pairs of elements in the two source vectors. + /// + /// The first source vector. + /// The second source vector. + /// The maximized vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Max(Vector3 value1, Vector3 value2) + { + return new Vector3( + (value1.X > value2.X) ? value1.X : value2.X, + (value1.Y > value2.Y) ? value1.Y : value2.Y, + (value1.Z > value2.Z) ? value1.Z : value2.Z); + } + + /// + /// Returns a vector whose elements are the absolute values of each of the source vector's elements. + /// + /// The source vector. + /// The absolute value vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 Abs(Vector3 value) + { + return new Vector3(MathF.Abs(value.X), MathF.Abs(value.Y), MathF.Abs(value.Z)); + } + + /// + /// Returns a vector whose elements are the square root of each of the source vector's elements. + /// + /// The source vector. + /// The square root vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 SquareRoot(Vector3 value) + { + return new Vector3((Single)MathF.Sqrt(value.X), (Single)MathF.Sqrt(value.Y), (Single)MathF.Sqrt(value.Z)); + } + #endregion Public Static Methods + + #region Public Static Operators + /// + /// Adds two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The summed vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator +(Vector3 left, Vector3 right) + { + return new Vector3(left.X + right.X, left.Y + right.Y, left.Z + right.Z); + } + + /// + /// Subtracts the second vector from the first. + /// + /// The first source vector. + /// The second source vector. + /// The difference vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 left, Vector3 right) + { + return new Vector3(left.X - right.X, left.Y - right.Y, left.Z - right.Z); + } + + /// + /// Multiplies two vectors together. + /// + /// The first source vector. + /// The second source vector. + /// The product vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, Vector3 right) + { + return new Vector3(left.X * right.X, left.Y * right.Y, left.Z * right.Z); + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Vector3 left, Single right) + { + return left * new Vector3(right); + } + + /// + /// Multiplies a vector by the given scalar. + /// + /// The scalar value. + /// The source vector. + /// The scaled vector. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator *(Single left, Vector3 right) + { + return new Vector3(left) * right; + } + + /// + /// Divides the first vector by the second. + /// + /// The first source vector. + /// The second source vector. + /// The vector resulting from the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 left, Vector3 right) + { + return new Vector3(left.X / right.X, left.Y / right.Y, left.Z / right.Z); + } + + /// + /// Divides the vector by the given scalar. + /// + /// The source vector. + /// The scalar value. + /// The result of the division. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator /(Vector3 value1, float value2) + { + float invDiv = 1.0f / value2; + + return new Vector3( + value1.X * invDiv, + value1.Y * invDiv, + value1.Z * invDiv); + } + + /// + /// Negates a given vector. + /// + /// The source vector. + /// The negated vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector3 operator -(Vector3 value) + { + return Zero - value; + } + + /// + /// Returns a boolean indicating whether the two given vectors are equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are equal; False otherwise. + [JitIntrinsic] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(Vector3 left, Vector3 right) + { + return (left.X == right.X && + left.Y == right.Y && + left.Z == right.Z); + } + + /// + /// Returns a boolean indicating whether the two given vectors are not equal. + /// + /// The first vector to compare. + /// The second vector to compare. + /// True if the vectors are not equal; False if they are equal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(Vector3 left, Vector3 right) + { + return (left.X != right.X || + left.Y != right.Y || + left.Z != right.Z); + } + + /// + /// Converts the SharpEngine representation to a System.Numerics representation. + /// + /// The value to convert + public static explicit operator System.Numerics.Vector3(Vector3 value) + => new(value.X, value.Y, value.Z); + + /// + /// Converts the System.Numerics representation to a SharpEngine representation. + /// + /// The value to convert + public static explicit operator Vector3(System.Numerics.Vector3 value) + => new(value.X, value.Y, value.Z); + + #endregion Public Static Operators + } +} \ No newline at end of file diff --git a/Core/Primitives/Cube.cs b/Core/Primitives/Cube.cs index 6db45cc..7a82896 100644 --- a/Core/Primitives/Cube.cs +++ b/Core/Primitives/Cube.cs @@ -141,6 +141,32 @@ public static class Cube 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f + ], + Indices = + [ + // Front face + 0, 1, 2, + 2, 3, 0, + + // Back face + 4, 5, 6, + 6, 7, 4, + + // Left face + 4, 0, 3, + 3, 7, 4, + + // Right face + 1, 5, 6, + 6, 2, 1, + + // Top face + 3, 2, 6, + 6, 7, 3, + + // Bottom face + 4, 5, 1, + 1, 0, 4 ] }; } diff --git a/Core/Primitives/Plane.cs b/Core/Primitives/Plane.cs index 1468423..b7c6911 100644 --- a/Core/Primitives/Plane.cs +++ b/Core/Primitives/Plane.cs @@ -12,24 +12,29 @@ public static class Plane { Vertices = [ - 0.5f, 0.5f, 0.0f, // top right - 0.5f, -0.5f, 0.0f, // bottom right - -0.5f, -0.5f, 0.0f, // bottom left - -0.5f, 0.5f, 0.0f, // top left + 1f, 1f, 0.0f, // top right + 1f, -1f, 0.0f, // bottom right + -1f, -1f, 0.0f, // bottom left + -1f, 1f, 0.0f, // top left ], Normals = - [ - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - 1.0f, 0.0f, 0.0f, - ], - TextureCoordinates = [ 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, + ], + TextureCoordinates = + [ + 1.0f, 0.0f, + 1.0f, 0.0f, + 0.0f, 1.0f, + 0.0f, 1.0f, + ], + Indices = + [ + 0, 1, 3, // first triangle + 1, 2, 3, // second triangle ] }; } diff --git a/Core/Primitives/PrimitiveFactory.cs b/Core/Primitives/PrimitiveFactory.cs index 38ec50e..8dd935e 100644 --- a/Core/Primitives/PrimitiveFactory.cs +++ b/Core/Primitives/PrimitiveFactory.cs @@ -1,6 +1,7 @@ using SharpEngine.Core.Entities; using SharpEngine.Core.Entities.Properties; using SharpEngine.Core.Extensions; + using System; using System.Numerics; @@ -11,17 +12,13 @@ namespace SharpEngine.Core.Primitives; /// public static class PrimitiveFactory { - private static string DebugTexture => PathExtensions.GetAssemblyPath("Textures/DefaultTextures/debug.JPG"); - private static string DefaultVertexShader => PathExtensions.GetAssemblyPath("Shaders/shader.vert"); - private static string DefaultFragmentShader => PathExtensions.GetAssemblyPath("Shaders/lighting.frag"); - /// /// Creates a new by the given primitive type. /// /// The type of primitive to create. /// Where the game object should be placed. public static GameObject Create(PrimitiveType primitiveType, Vector3 position) - => Create(primitiveType, position, DebugTexture); + => Create(primitiveType, position, _Resources.Default.DebugTexture); /// /// The type of primitive to be created. @@ -33,10 +30,10 @@ public static GameObject Create(PrimitiveType primitiveType, Vector3 position) /// A new game object. /// Thrown when the specified primitive type does not exist. public static GameObject Create(PrimitiveType primitiveType, Vector3 position, string diffuseMapFile, string? specularMapFile = null, string? vertShaderFile = null, string? fragShaderFile = null) - => new(diffuseMapFile, specularMapFile ?? DebugTexture, vertShaderFile ?? DefaultVertexShader, fragShaderFile ?? DefaultFragmentShader) + => new(diffuseMapFile, specularMapFile, vertShaderFile ?? _Resources.Default.VertexShader, fragShaderFile ?? _Resources.Default.FragmentShader) { - Transform = new Transform { Position = position }, - Mesh = primitiveType switch + Transform = new Transform { Position = (SharpEngine.Core.Numerics.Vector3)position }, + Meshes = primitiveType switch { PrimitiveType.Cube => [Cube.Mesh], PrimitiveType.Plane => [Plane.Mesh], diff --git a/Core/Ray.cs b/Core/Ray.cs index 42bae6e..942346b 100644 --- a/Core/Ray.cs +++ b/Core/Ray.cs @@ -43,7 +43,7 @@ public bool IsGameObjectInView(Scene scene, out GameObject? intersectingObject, const float maxDistance = 100.0f; // Maximum distance to check for intersections const float stepSize = 0.1f; // Step size for ray marching - var sceneNodes = scene.GetAllGameObjects(); + var sceneNodes = scene.GetObjectsOfType(); if (allowedTypes.Length != 0) sceneNodes = [.. sceneNodes.Where(go => allowedTypes.Any(type => type.IsInstanceOfType(go)))]; diff --git a/Core/Renderers/RenderFlags.cs b/Core/Renderers/RenderFlags.cs index 68d5137..005999b 100644 --- a/Core/Renderers/RenderFlags.cs +++ b/Core/Renderers/RenderFlags.cs @@ -14,13 +14,12 @@ public enum RenderFlags /// All the vertices will for the mesh will be rendered. Renderer3D = 1, - /// - /// The front face of the mesh will be rendered. - /// + /// The front face of the mesh will be rendered. UIRenderer = 2, - /// - /// Enables all renderers. - /// - All = 3 + /// Text components will be rendered. + Text = 4, + + /// Enables all renderers. + All = 7, } diff --git a/Core/Renderers/Renderer.cs b/Core/Renderers/Renderer.cs index ae52bed..3768c84 100644 --- a/Core/Renderers/Renderer.cs +++ b/Core/Renderers/Renderer.cs @@ -4,7 +4,7 @@ using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; - +using SharpEngine.Core.Windowing; using Silk.NET.OpenGL; using System; @@ -23,6 +23,7 @@ public class Renderer : RendererBase private readonly CameraView _camera; private readonly Scene _scene; + private readonly Window _window; // Read only once, load into OpenGL buffer once. // TODO: Multiple meshes @@ -34,12 +35,14 @@ public class Renderer : RendererBase /// Initializes a new instance of . /// /// The game the renderer is being used for. + /// The window executing the renderer. /// The settings for the renderer. /// The game scene to be rendered. - public Renderer(CameraView camera, ISettings settings, Scene scene) : base(settings) + public Renderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) { _camera = camera; _scene = scene; + _window = window; // TODO: These should be refactored out. The minimum build shouldn't need to use these. _lightingShader = new LightingShader(); @@ -49,6 +52,7 @@ public Renderer(CameraView camera, ISettings settings, Scene scene) : base(setti /// public override Task Render() { + // return Task.CompletedTask; try { Window.GL.Enable(EnableCap.DepthTest); @@ -81,7 +85,7 @@ private Task RenderGameObject(SceneNode node) return Task.CompletedTask; // TODO: Skip blocks that are behind others relative to the camera - return gameObject.Render(); + return gameObject.Render(_camera, _window); } private static bool IsInViewFrustum(BoundingBox boundingBox, CameraView camera) diff --git a/Core/Renderers/RendererBase.cs b/Core/Renderers/RendererBase.cs index c36bd03..1491fab 100644 --- a/Core/Renderers/RendererBase.cs +++ b/Core/Renderers/RendererBase.cs @@ -1,3 +1,4 @@ +using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; using System; using System.Threading.Tasks; diff --git a/Core/Renderers/TextRenderer.cs b/Core/Renderers/TextRenderer.cs new file mode 100644 index 0000000..5787fc5 --- /dev/null +++ b/Core/Renderers/TextRenderer.cs @@ -0,0 +1,22 @@ +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Interfaces; +using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; + +using System.Threading.Tasks; + +namespace SharpEngine.Core.Renderers; +internal class TextRenderer : RendererBase +{ + public TextRenderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) + { + } + + public override RenderFlags RenderFlag => RenderFlags.Text; + + /// + public override Task Render() + { + return Task.CompletedTask; + } +} diff --git a/Core/Renderers/UIRenderer.cs b/Core/Renderers/UIRenderer.cs index 47a6e4c..6c0e29e 100644 --- a/Core/Renderers/UIRenderer.cs +++ b/Core/Renderers/UIRenderer.cs @@ -1,8 +1,10 @@ -using SharpEngine.Core.Entities; +using SharpEngine.Core.Entities.UI; using SharpEngine.Core.Entities.Views; using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; +using SharpEngine.Core.Windowing; + using Silk.NET.OpenGL; using System; @@ -17,6 +19,8 @@ public class UIRenderer : RendererBase { private readonly Scene _scene; private readonly UIShader _uiShader; + private readonly CameraView _camera; + private readonly Window _window; /// public override RenderFlags RenderFlag => RenderFlags.UIRenderer; @@ -24,53 +28,32 @@ public class UIRenderer : RendererBase /// /// Initializes a new instance of . /// - public UIRenderer(CameraView _, ISettings settings, Scene scene) : base(settings) + public UIRenderer(CameraView camera, Window window, ISettings settings, Scene scene) : base(settings) { _scene = scene; _uiShader = new UIShader(); - } - - /// - public override void Initialize() - { - if (_uiShader.Shader is null) - { - Console.WriteLine("The UI shader is null. This is broken mostly when the exe is started outside the solution. This needs to be fixed later."); - return; - } - - _uiShader.Shader.Use(); - - _scene.UIElements.Add(new UIElement("uiElement")); - - _scene.Iterate(_scene.UIElements, elem => elem.Initialize()); - - InitializeVertexArrays(); - } - - private void InitializeVertexArrays() - { - _uiShader.SetAttributes(); + _camera = camera; + _window = window; } /// public override Task Render() { - return Task.CompletedTask; + if (_uiShader.Shader is null) + return Task.CompletedTask; try { - Window.GL.Disable(EnableCap.DepthTest); + Window.GL.Enable(EnableCap.DepthTest); Window.GL.DepthFunc(DepthFunction.Less); - foreach (var item in _scene.UIElements) - { - item.Bind(); - } + // Disable face culling to render both sides of the quad + Window.GL.Disable(EnableCap.CullFace); - _uiShader.Shader.Use(); + // _camera.SetShaderUniforms(_uiShader.Shader!); + _uiShader.Shader?.Use(); - var uiElementRenderTasks = _scene.IterateAsync(_scene.UIElements, elem => elem.Render()); + var uiElementRenderTasks = _scene.IterateAsync(_scene.UIElements, elem => elem.Render(_camera, _window)); return Task.WhenAll(uiElementRenderTasks); } diff --git a/Core/Scenes/Scene.cs b/Core/Scenes/Scene.cs index 0370af4..6e4706f 100644 --- a/Core/Scenes/Scene.cs +++ b/Core/Scenes/Scene.cs @@ -1,5 +1,6 @@ using SharpEngine.Core.Entities; - +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.UI; using System; using System.Collections.Generic; using System.Linq; @@ -41,13 +42,13 @@ public Scene(string sceneFile) /// /// Gets or sets the root node of the scene. /// - public SceneNode Root { get; set; } = new SceneNode("Root"); + public SceneNode Root { get; set; } = new EmptyNode("Root"); /// Gets or sets the nodes in the scene. private List Nodes { get; set; } = []; /// Gets or sets the UI elements in the scene. - public List UIElements { get; private set; } = []; + public List UIElements { get; private set; } = []; /// Gets or sets the active element in the scene. /// Editor only. @@ -59,7 +60,7 @@ public Scene(string sceneFile) /// The name of the new empty node. public void AddNode(string name) { - var node = new SceneNode(name); + var node = new EmptyNode(name); Nodes.Add(node); Root.Children.Add(node); } @@ -104,26 +105,27 @@ public virtual void RemoveNode(SceneNode node) } /// - /// Gets all game objects in the scene. + /// Gets all the objects in the scene of type . /// + /// The type of the objects to be retrieved. /// All the game objects in the current scene. - public List GetAllGameObjects() + public List GetObjectsOfType(SceneNode? root = null) { // TODO: Can we make this async? - var gameObjects = new List(); - static void FindGameObjects(SceneNode node, List gameObjects) + var result = new List(); + static void FindsObjects(SceneNode node, List result) { - if (node is GameObject gameObject) - gameObjects.Add(gameObject); + if (node is T obj) + result.Add(obj); foreach (var child in node.Children) - FindGameObjects(child, gameObjects); + FindsObjects(child, result); } - foreach (var node in Root.Children) - FindGameObjects(node, gameObjects); + foreach (var node in root?.Children ?? Root.Children) + FindsObjects(node, result); - return gameObjects; + return result; } /// diff --git a/Core/Scenes/SceneNode.cs b/Core/Scenes/SceneNode.cs index 5644ac3..13b3526 100644 --- a/Core/Scenes/SceneNode.cs +++ b/Core/Scenes/SceneNode.cs @@ -1,13 +1,40 @@ -using System.Collections.Generic; +using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Views; +using SharpEngine.Core.Numerics; +using SharpEngine.Core.Windowing; + +using System.Collections.Generic; using System.Threading.Tasks; namespace SharpEngine.Core.Scenes; +/// +/// Represents a empty node in a scene. +/// +/// Specifies the type used for transformations. +/// Defines vector used by the transform. +public class EmptyNode : SceneNode where TTransform : ITransform, new() where TVector : IVector, new() +{ + /// + /// Initializes a new instance of . + /// + /// + public EmptyNode(string name) : base(name) { } + + /// + /// Gets or sets the transform of the node. + /// + public virtual TTransform Transform { get; set; } = new(); +} + /// /// Represents a node in the scene. /// -public class SceneNode +public abstract class SceneNode { + /// Gets a new empty node. + public static SceneNode Empty => new EmptyNode("Empty Node"); + /// /// Gets or sets the name of the node. /// @@ -16,7 +43,7 @@ public class SceneNode /// /// Gets or sets the children of the node. /// - public List Children { get; set; } = new(); + public List Children { get; set; } = []; /// /// Initializes a new empty . @@ -37,12 +64,20 @@ public SceneNode(string name) /// /// The name of the empty node to be added. /// The created node. - public SceneNode AddChild(string name) + public virtual SceneNode AddChild(string name) + => AddChild(name); + + /// + /// Adds an empty child node to this node by the given . + /// + /// The name of the empty node to be added. + /// The created node. + public virtual SceneNode AddChild(string name) where TTransform : ITransform, new() where TVector : IVector, new() { - var node = new SceneNode(name); - Children.Add(node); + var node = new EmptyNode(name) as SceneNode; + Children.Add(node!); - return node; + return node!; } /// @@ -71,5 +106,5 @@ public void RemoveChild(SceneNode node) /// Renders the current object to the screen. /// /// A representing an asynchronous operation. - public virtual Task Render() => Task.CompletedTask; + public virtual Task Render(CameraView camera, Window window) => Task.CompletedTask; } diff --git a/Core/Shaders/LampShader.cs b/Core/Shaders/LampShader.cs index aa63ddb..465ab1c 100644 --- a/Core/Shaders/LampShader.cs +++ b/Core/Shaders/LampShader.cs @@ -1,11 +1,15 @@ -using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Extensions; +using SharpEngine.Core.Windowing; using Silk.NET.OpenGL; namespace SharpEngine.Core.Shaders; internal class LampShader : ShaderBase { + /// + /// Initializes a new instance of . + /// public LampShader() { Shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/shader.frag"), "lamp"); diff --git a/Core/Shaders/LightingShader.cs b/Core/Shaders/LightingShader.cs index 8ddd2af..e7ee010 100644 --- a/Core/Shaders/LightingShader.cs +++ b/Core/Shaders/LightingShader.cs @@ -1,12 +1,15 @@ -using SharpEngine.Core.Entities.Properties; +using SharpEngine.Core.Entities.Properties.Meshes; using SharpEngine.Core.Extensions; +using SharpEngine.Core.Windowing; using Silk.NET.OpenGL; -using System; namespace SharpEngine.Core.Shaders; internal class LightingShader : ShaderBase { + /// + /// Initializes a new instance of . + /// public LightingShader() { Shader = ShaderService.Instance.LoadShader(PathExtensions.GetAssemblyPath("Shaders/shader.vert"), PathExtensions.GetAssemblyPath("Shaders/lighting.frag"), "lighting"); diff --git a/Core/Shaders/Shader.cs b/Core/Shaders/Shader.cs index 0ae4049..9b117c0 100644 --- a/Core/Shaders/Shader.cs +++ b/Core/Shaders/Shader.cs @@ -1,11 +1,11 @@ +using SharpEngine.Core.Windowing; +using Silk.NET.OpenGL; using System; -using System.IO; using System.Collections.Generic; - -using System.Text.RegularExpressions; +using System.IO; using System.Numerics; - -using Silk.NET.OpenGL; +using System.Text.RegularExpressions; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace SharpEngine.Core.Shaders; @@ -42,6 +42,12 @@ public Shader(string vertPath, string fragPath, string name) // The fragment shader is responsible for then converting the vertices to "fragments", which represent all the data OpenGL needs to draw a pixel. // The fragment shader is what we'll be using the most here. + if (!vertPath.EndsWith(".vert")) + Console.WriteLine("Vertex shaders should have the file extension '.vert' for easier manageability."); + + if (!fragPath.EndsWith(".frag")) + Console.WriteLine("Fragment shaders should have the file extension '.frag' for easier manageability."); + // Load and compile shader if (!LoadShader(ShaderType.VertexShader, vertPath, out uint vertexShader)) { @@ -81,7 +87,6 @@ public Shader(string vertPath, string fragPath, string name) // The shader is now ready to go, but first, we're going to cache all the shader uniform locations. // Querying this from the shader is very slow, so we do it once on initialization and reuse those values // later. - GetUniformLocations(); } @@ -212,6 +217,18 @@ public bool TryGetAttribLocation(string attribName, out int location) // 2. Get a handle to the location of the uniform with GL.GetUniformLocation. // 3. Use the appropriate GL.Uniform* function to set the uniform. + private bool TrySetUniform(string name, T data, Action setter) + { + if (!_uniformLocations.TryGetValue(name, out int uniform)) + { + Console.WriteLine($"Uniform '{name}' not found in shader program."); + return false; + } + + setter(uniform, data); + return true; + } + /// /// Set a uniform int on this shader. /// @@ -220,7 +237,7 @@ public bool TryGetAttribLocation(string attribName, out int location) public void SetInt(string name, int data) { Window.GL.UseProgram(Handle); - Window.GL.Uniform1(_uniformLocations[name], data); + TrySetUniform(name, data, Window.GL.Uniform1); } /// @@ -231,7 +248,7 @@ public void SetInt(string name, int data) public void SetFloat(string name, float data) { Window.GL.UseProgram(Handle); - Window.GL.Uniform1(_uniformLocations[name], data); + TrySetUniform(name, data, Window.GL.Uniform1); } /// @@ -248,7 +265,18 @@ public void SetFloat(string name, float data) public void SetMatrix4(string name, Matrix4x4 data, bool transpose = true) { Window.GL.UseProgram(Handle); - Window.GL.UniformMatrix4(_uniformLocations[name], transpose, data.ToSpan()); + TrySetUniform(name, data, (uniform, d) => Window.GL.UniformMatrix4(uniform, transpose, d.ToSpan())); + } + + /// + /// Set a uniform Vector2 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public void SetVector2(string name, Vector2 data) + { + Window.GL.UseProgram(Handle); + TrySetUniform(name, data, Window.GL.Uniform2); } /// @@ -259,6 +287,17 @@ public void SetMatrix4(string name, Matrix4x4 data, bool transpose = true) public void SetVector3(string name, Vector3 data) { Window.GL.UseProgram(Handle); - Window.GL.Uniform3(_uniformLocations[name], data); + TrySetUniform(name, data, Window.GL.Uniform3); + } + + /// + /// Set a uniform Vector3 on this shader. + /// + /// The name of the uniform. + /// The data to set. + public void SetVector4(string name, Vector4 data) + { + Window.GL.UseProgram(Handle); + TrySetUniform(name, data, Window.GL.Uniform4); } } diff --git a/Core/Shaders/ShaderService.cs b/Core/Shaders/ShaderService.cs index db685b3..6a27cb8 100644 --- a/Core/Shaders/ShaderService.cs +++ b/Core/Shaders/ShaderService.cs @@ -15,7 +15,7 @@ public class ShaderService public static ShaderService Instance { get; } = new ShaderService(); // TODO: Cache data is never set - private readonly Dictionary _shaderCache = new(); + private readonly Dictionary _shaderCache = []; /// /// Gets or sets whether there are shaders to load. @@ -34,7 +34,7 @@ private ShaderService() { } public List GetAll() { HasShadersToLoad = false; - return new(_shaderCache.Values); + return [.. _shaderCache.Values]; } /// diff --git a/Core/Shaders/UIShader.cs b/Core/Shaders/UIShader.cs index 126b2fe..4c5e675 100644 --- a/Core/Shaders/UIShader.cs +++ b/Core/Shaders/UIShader.cs @@ -1,13 +1,18 @@ -using SharpEngine.Core.Extensions; +using SharpEngine.Core.Entities.Properties.Meshes; +using SharpEngine.Core.Extensions; +using SharpEngine.Core.Windowing; using Silk.NET.OpenGL; namespace SharpEngine.Core.Shaders; internal class UIShader : ShaderBase { + /// + /// Initializes a new instance of . + /// public UIShader() { - Shader = new Shader(PathExtensions.GetAssemblyPath("Shaders/uiShader.vert"), PathExtensions.GetAssemblyPath("Shaders/uiShader.frag"), "ui"); + Shader = new Shader(PathExtensions.GetAssemblyPath("Shaders\\uiShader.vert"), PathExtensions.GetAssemblyPath("Shaders\\uiShader.frag"), "ui"); } /// @@ -16,12 +21,26 @@ public override bool SetAttributes() if (!base.SetAttributes()) return false; - if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int vertexLocation)) + if (!Shader!.TryGetAttribLocation(ShaderAttributes.Pos, out int positionLocation)) return false; - uint vertexLocationUInt = (uint)vertexLocation; - Window.GL.EnableVertexAttribArray(vertexLocationUInt); - Window.GL.VertexAttribPointer(vertexLocationUInt, 3, VertexAttribPointerType.Float, false, 6 * sizeof(float), 0); + var positionLocationUint = (uint)positionLocation; + Window.GL.EnableVertexAttribArray(positionLocationUint); + Window.GL.VertexAttribPointer(positionLocationUint, VertexData.VerticesSize, VertexAttribPointerType.Float, false, VertexData.Stride, 0); + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.Normal, out int normalLocation)) + return false; + + var normalLocationUint = (uint)normalLocation; + Window.GL.EnableVertexAttribArray(normalLocationUint); + Window.GL.VertexAttribPointer(normalLocationUint, VertexData.NormalsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.NormalsOffset); + + if (!Shader!.TryGetAttribLocation(ShaderAttributes.TexCoords, out int texCoordLocation)) + return false; + + var texCoordLocationUint = (uint)texCoordLocation; + Window.GL.EnableVertexAttribArray(texCoordLocationUint); + Window.GL.VertexAttribPointer(texCoordLocationUint, VertexData.TexCoordsSize, VertexAttribPointerType.Float, false, VertexData.Stride, VertexData.TexCoordsOffset); return true; } diff --git a/Core/Shaders/uiShader.frag b/Core/Shaders/uiShader.frag index fe3175f..1cc0a9e 100644 --- a/Core/Shaders/uiShader.frag +++ b/Core/Shaders/uiShader.frag @@ -1,7 +1,10 @@ #version 330 core + +uniform sampler2D texture1; out vec4 FragColor; +in vec2 TexCoords; void main() { - FragColor = vec4(1.0, 0.0, 0.0, 1.0); // set all 4 vector values to 1.0 + FragColor = texture(texture1, TexCoords); } \ No newline at end of file diff --git a/Core/Shaders/uiShader.vert b/Core/Shaders/uiShader.vert index aa9fdcc..2967764 100644 --- a/Core/Shaders/uiShader.vert +++ b/Core/Shaders/uiShader.vert @@ -1,15 +1,58 @@ #version 330 core layout (location = 0) in vec3 aPos; +layout (location = 1) in vec3 aNormal; +layout (location = 2) in vec2 aTexCoords; + +uniform float width; +uniform float height; + +uniform vec2 position; +uniform float rotation; +uniform vec2 screenSize; +uniform mat4 orthoMatrix; uniform mat4 model; +out vec3 Normal; out vec3 FragPos; +out vec2 TexCoords; void main() { - vec4 pos = vec4(aPos, 1.0); + float reciprScaleOnscreen = 0.005f; + + // Calculate the aspect ratio + float aspectRatio = screenSize.x / screenSize.y; + + // Create the rotation matrix for the z-axis + mat4 rotationMatrix = mat4( + cos(rotation), -sin(rotation), 0.0, 0.0, + sin(rotation), cos(rotation), 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); + + // Scale the vertex position using width and height + vec3 scaledPos = vec3( + aPos.x * width * reciprScaleOnscreen, + aPos.y * height * reciprScaleOnscreen, + aPos.z + ); + + // Calculate the model-view-projection matrix using the orthographic projection matrix + mat4 mvp = orthoMatrix * rotationMatrix * model; + + // Apply the MVP matrix to the scaled vertex position with aspect ratio + gl_Position = mvp * vec4( + (scaledPos.x + (position.x * reciprScaleOnscreen)) * aspectRatio, + scaledPos.y + (position.y * reciprScaleOnscreen), + scaledPos.z, + 1 + ); - gl_Position = model * pos; - FragPos = vec3(model * pos); + // Pass through other attributes + Normal = mat3(model) * aNormal; // Transform the normal + FragPos = vec3(model * vec4(aPos, 1.0)); // Transform the fragment position + TexCoords = aTexCoords; } \ No newline at end of file diff --git a/Core/SharpEngine.Core.csproj b/Core/SharpEngine.Core.csproj index 5ec1e72..2fda018 100644 --- a/Core/SharpEngine.Core.csproj +++ b/Core/SharpEngine.Core.csproj @@ -41,7 +41,7 @@ - + Always diff --git a/Core/Textures/Texture.cs b/Core/Textures/Texture.cs index 689b1a7..a947e3e 100644 --- a/Core/Textures/Texture.cs +++ b/Core/Textures/Texture.cs @@ -1,4 +1,5 @@ -using Silk.NET.OpenGL; +using SharpEngine.Core.Windowing; +using Silk.NET.OpenGL; namespace SharpEngine.Core.Textures; diff --git a/Core/Textures/TextureService.cs b/Core/Textures/TextureService.cs index 89b4bcf..05b1775 100644 --- a/Core/Textures/TextureService.cs +++ b/Core/Textures/TextureService.cs @@ -5,6 +5,7 @@ using System; using Silk.NET.OpenGL; +using SharpEngine.Core.Windowing; namespace SharpEngine.Core.Textures; diff --git a/Core/Windowing/Frame.cs b/Core/Windowing/Frame.cs new file mode 100644 index 0000000..9b43fbf --- /dev/null +++ b/Core/Windowing/Frame.cs @@ -0,0 +1,22 @@ +namespace SharpEngine.Core.Windowing; + +/// +/// Represents a frame with a specific duration in time. +/// +public class Frame +{ + /// + /// Initializes a new instance of . + /// + /// Represents the duration of the frame in seconds. + public Frame(double frameTime) + { + FrameTime = frameTime; + } + + /// Gets or sets the time taken to handle the previous frame. + public double FrameTime { get; set; } + + /// Gets the frame rate based on the frame time. + public float FrameRate => (float)(1 / FrameTime); +} diff --git a/Core/SilkWindow.cs b/Core/Windowing/SilkWindow.cs similarity index 70% rename from Core/SilkWindow.cs rename to Core/Windowing/SilkWindow.cs index bcc1936..3cae2f5 100644 --- a/Core/SilkWindow.cs +++ b/Core/Windowing/SilkWindow.cs @@ -1,4 +1,4 @@ -using Silk.NET.Core; +using Silk.NET.Core; using Silk.NET.Core.Contexts; using Silk.NET.Input; using Silk.NET.Maths; @@ -11,7 +11,7 @@ using System.IO; using System.Numerics; -namespace SharpEngine.Core; +namespace SharpEngine.Core.Windowing; /// /// Represents an abstraction for the interface. @@ -22,28 +22,48 @@ namespace SharpEngine.Core; public abstract class SilkWindow : IWindow { /// - public virtual IWindowHost? Parent { get; } + public virtual IWindowHost? Parent => CurrentWindow.Parent; /// - public IMonitor? Monitor { get; set; } + public IMonitor? Monitor + { + get => CurrentWindow.Monitor; + set => CurrentWindow.Monitor = value; + } /// - public bool IsClosing { get; set; } + public bool IsClosing + { + get => CurrentWindow.IsClosing; + set => CurrentWindow.IsClosing = value; + } /// Gets or sets the input context for the window. public IInputContext? Input { get; protected set; } /// - public virtual Rectangle BorderSize { get; } + public virtual Rectangle BorderSize => CurrentWindow.BorderSize; /// - public bool IsVisible { get; set; } + public bool IsVisible + { + get => CurrentWindow.IsVisible; + set => CurrentWindow.IsVisible = value; + } /// - public Vector2D Position { get; set; } + public Vector2D Position + { + get => CurrentWindow.Position; + set => CurrentWindow.Position = value; + } /// - public Vector2D Size { get; set; } + public Vector2D Size + { + get => CurrentWindow.Size; + set => CurrentWindow.Size = value; + } /// public string Title @@ -53,16 +73,28 @@ public string Title } /// - public WindowState WindowState { get; set; } + public WindowState WindowState + { + get => CurrentWindow.WindowState; + set => CurrentWindow.WindowState = value; + } /// - public WindowBorder WindowBorder { get; set; } + public WindowBorder WindowBorder + { + get => CurrentWindow.WindowBorder; + set => CurrentWindow.WindowBorder = value; + } /// - public bool TransparentFramebuffer { get; } + public bool TransparentFramebuffer => CurrentWindow.TransparentFramebuffer; /// - public bool TopMost { get; set; } + public bool TopMost + { + get => CurrentWindow.TopMost; + set => CurrentWindow.TopMost = value; + } /// public IGLContext? SharedContext => CurrentWindow.SharedContext; @@ -83,25 +115,49 @@ public string Title public bool IsInitialized => CurrentWindow.IsInitialized; /// - public bool ShouldSwapAutomatically { get; set; } + public bool ShouldSwapAutomatically + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } /// - public bool IsEventDriven { get; set; } - + public bool IsEventDriven + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } + /// - public bool IsContextControlDisabled { get; set; } + public bool IsContextControlDisabled + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } /// - public double FramesPerSecond { get; set; } - + public double FramesPerSecond + { + get => CurrentWindow.FramesPerSecond; + set => CurrentWindow.FramesPerSecond = value; + } + /// - public double UpdatesPerSecond { get; set; } + public double UpdatesPerSecond + { + get => CurrentWindow.UpdatesPerSecond; + set => CurrentWindow.UpdatesPerSecond = value; + } /// - public GraphicsAPI API { get; } + public GraphicsAPI API => CurrentWindow.API; /// - public bool VSync { get; set; } + public bool VSync + { + get => CurrentWindow.ShouldSwapAutomatically; + set => CurrentWindow.ShouldSwapAutomatically = value; + } /// public VideoMode VideoMode => CurrentWindow.VideoMode; @@ -159,7 +215,7 @@ public string Title public event Action? Render; /// Raised after the scene has been rendered. - public event Action? OnAfterRender; + public event Action? OnAfterRender; /// An event executed when the window has loaded. public event Action? OnLoaded; @@ -193,9 +249,9 @@ public IWindow CreateWindow(WindowOptions opts) // public static T Create(WindowOptions opts) where T : SilkWindow, new() // { // var instance = new T(); - // var winndow = instance.CreateWindow(opts); + // var window = instance.CreateWindow(opts); // - // instance.CurrentWindow = winndow; + // instance.CurrentWindow = window; // return instance; // } @@ -225,16 +281,16 @@ public void Dispose() public virtual void Initialize() => CurrentWindow.Initialize(); /// - public virtual object Invoke(Delegate d, params object[] args) => new(); + public virtual object Invoke(Delegate d, params object[] args) => CurrentWindow.Invoke(d, args); /// - public virtual Vector2D PointToClient(Vector2D point) => throw new NotImplementedException(); + public virtual Vector2D PointToClient(Vector2D point) => CurrentWindow.PointToClient(point); /// - public virtual Vector2D PointToFramebuffer(Vector2D point) => throw new NotImplementedException(); + public virtual Vector2D PointToFramebuffer(Vector2D point) => CurrentWindow.PointToFramebuffer(point); /// - public virtual Vector2D PointToScreen(Vector2D point) => throw new NotImplementedException(); + public virtual Vector2D PointToScreen(Vector2D point) => CurrentWindow.PointToScreen(point); /// public virtual void Reset() => CurrentWindow.Reset(); @@ -279,16 +335,15 @@ public virtual void OnLoad() /// /// Handles operations needed to be executed after the renderers are finished. /// - /// Time since the last frame. - protected virtual void AfterRender(double deltaTime) => OnAfterRender?.Invoke(deltaTime); + /// Contains information about the previous frame. + protected virtual void AfterRender(Frame frame) => OnAfterRender?.Invoke(frame); /// /// Handles operations needed to be executed before the renderers are executed. /// - /// Time since the last frame. - protected virtual void PreRender(double deltaTime) { } + /// Contains information about the previous frame. + protected virtual void PreRender(Frame frame) { } - // TODO: Scene unsaved changes warning. /// /// Handles closing the application. /// diff --git a/Core/Window.cs b/Core/Windowing/Window.cs similarity index 89% rename from Core/Window.cs rename to Core/Windowing/Window.cs index cb1d7e3..3d4f44f 100644 --- a/Core/Window.cs +++ b/Core/Windowing/Window.cs @@ -3,7 +3,6 @@ using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Enums; using SharpEngine.Core.Extensions; -using SharpEngine.Core.Interfaces; using SharpEngine.Core.Renderers; using SharpEngine.Core.Scenes; using SharpEngine.Core.Shaders; @@ -22,7 +21,7 @@ using System.Numerics; using System.Threading.Tasks; -namespace SharpEngine.Core; +namespace SharpEngine.Core.Windowing; /// /// Represents the game window. @@ -33,8 +32,14 @@ public class Window : SilkWindow private IEnumerable _renderers = []; private ImGuiController? _imGuiController; + /// + /// Gets or sets the view for the window. + /// public readonly CameraView Camera; + /// + /// Gets the settings for the current window. + /// public IViewSettings Settings; /// The event executed when mouse events are executed. @@ -67,8 +72,7 @@ public class Window : SilkWindow /// The OpenGL context for this window. public static GL GetGL() => GL; private static void SetGL(GL gl) => GL = gl; - public Window() { } - + /// /// Initializes a new instance of . /// @@ -80,7 +84,7 @@ public Window(CameraView camera, Scene scene, IViewSettings settings) Scene = scene; Settings = settings; Camera = camera; - Initialize(); + InitializeWindow(); } /// @@ -94,17 +98,16 @@ public Window(Scene scene, IViewSettings settings) Settings = settings; Camera = new(Vector3.One, settings); - Initialize(); + InitializeWindow(); } private bool _windowInitialized; - // public void Initialize() where T : SilkWindow, new() - private void Initialize() + private void InitializeWindow() { CurrentWindow = CreateWindow(Settings.WindowOptions); - CurrentWindow.Update += OnUpdateFrame; - CurrentWindow.Render += RenderFrame; + CurrentWindow.Update += deltaTime => OnUpdateFrame(new Frame(deltaTime)); + CurrentWindow.Render += deltaTime => RenderFrame(new Frame(deltaTime)); CurrentWindow.Resize += OnResize; CurrentWindow.Load += OnLoad; CurrentWindow.Closing += OnClosing; @@ -159,7 +162,7 @@ public override void OnLoad() { // Make sure the renderer has the correct constructor parameters! // TODO: The static reference to the context will not work when multiple windows are implemented, since the context will be different. - var requiredArguments = new object[] { Camera, Settings, Scene }; + var requiredArguments = new object[] { Camera, this, Settings, Scene }; var renderer = (RendererBase)Activator.CreateInstance(type, requiredArguments)!; _renderers = _renderers.Append(renderer); @@ -183,8 +186,8 @@ public override void OnLoad() /// /// Renders the current view and all specified renderers. /// - /// The time since the last frame. - protected void RenderFrame(double deltaTime) + /// Contains information about the previous frame. + protected void RenderFrame(Frame frame) { while (!_initialized) { @@ -193,9 +196,9 @@ protected void RenderFrame(double deltaTime) try { - PreRender(deltaTime); + PreRender(frame); - _imGuiController?.Update((float)deltaTime); + _imGuiController?.Update((float)frame.FrameTime); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); @@ -212,7 +215,7 @@ protected void RenderFrame(double deltaTime) // TODO: This call causes flickering in the new framework. Investigate why. // _window.SwapBuffers(); - AfterRender(deltaTime); + AfterRender(frame); _imGuiController?.Render(); } @@ -240,7 +243,7 @@ private void UseShaders() } /// - protected void OnUpdateFrame(double deltaTime) + protected void OnUpdateFrame(Frame frame) { while (!_initialized) { @@ -252,7 +255,7 @@ protected void OnUpdateFrame(double deltaTime) // return; if (Settings.PrintFrameRate) - Console.WriteLine($"FPS: {1f / deltaTime}"); + Console.WriteLine($"FPS: {frame.FrameRate}"); // TODO: Handle multiple mice? var mouse = Input?.Mice[0]; @@ -268,11 +271,11 @@ protected void OnUpdateFrame(double deltaTime) if (keyboard.IsKeyPressed(Key.Escape)) CurrentWindow.Close(); - OnHandleKeyboard?.Invoke(keyboard, deltaTime); + OnHandleKeyboard?.Invoke(keyboard, frame.FrameTime); } if (Input is not null) - OnUpdate?.Invoke(deltaTime, Input); + OnUpdate?.Invoke(frame.FrameTime, Input); } // TODO: #21 Input system @@ -320,7 +323,9 @@ protected void OnMouseWheel(IMouse mouse, ScrollWheel sw) protected void OnResize(Vector2D size) { GL.Viewport(size); - Camera.AspectRatio = size.X / size.Y; + + if (size != Vector2D.Zero) + Camera.AspectRatio = size.X / size.Y; } /// diff --git a/Core/_Resources/Default.cs b/Core/_Resources/Default.cs new file mode 100644 index 0000000..fb078fb --- /dev/null +++ b/Core/_Resources/Default.cs @@ -0,0 +1,18 @@ +using SharpEngine.Core.Extensions; + +namespace SharpEngine.Core._Resources; + +/// +/// Contains default resources used by the engine. +/// +public struct Default +{ + /// Gets the path to the debug texture. + public static string DebugTexture => PathExtensions.GetAssemblyPath("Textures\\DefaultTextures\\debug.JPG"); + + /// Gets the path to the default vertex shader. + public static string VertexShader => PathExtensions.GetAssemblyPath("Shaders\\shader.vert"); + + /// Gets the path to the lighting fragment shader. + public static string FragmentShader => PathExtensions.GetAssemblyPath("Shaders\\lighting.frag"); +} diff --git a/Editor/EditorWindow.cs b/Editor/EditorWindow.cs index d46c5b5..4746b74 100644 --- a/Editor/EditorWindow.cs +++ b/Editor/EditorWindow.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using Launcher.UI; using SharpEngine.Core; using SharpEngine.Core.Entities.Views.Settings; @@ -13,6 +12,8 @@ using System.Numerics; using System.Reflection; +using SharpEngine.Core.Windowing; +using SharpEngine.Shared.Dto; namespace SharpEngine.Editor; @@ -94,11 +95,45 @@ public override void OnLoad() } } + private bool showUnsavedChangesDialog = true; + /// - protected override void AfterRender(double deltaTime) + protected override void AfterRender(Frame frame) + { + base.AfterRender(frame); + + EnableDocking(); + RenderMenuBar(); + + ImGui.Begin("Unsaved changes", ref showUnsavedChangesDialog, ImGuiWindowFlags.NoResize | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoMove); + ImGui.Text("There are unsaved changes in the project. Are you sure you want to exist without saving?"); + + bool cancelled = false; + if (ImGui.Button("Cancel")) + cancelled = true; + + if (!cancelled && ImGui.Button("Save & close")) + { + Save(); + CurrentWindow.Close(); + } + + if (!cancelled && ImGui.Button("Exit without saving")) + CurrentWindow.Close(); + + ImGui.End(); + + foreach (var window in _windows) + window.RenderWindow(); + } + + private void Save() { - base.AfterRender(deltaTime); + // TODO: Save all changes. + } + private static void EnableDocking() + { // Enable docking ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable; @@ -123,9 +158,75 @@ protected override void AfterRender(double deltaTime) ImGui.DockSpace(dockSpaceId, Vector2.Zero, ImGuiDockNodeFlags.PassthruCentralNode); ImGui.End(); + } - foreach (var window in _windows) - window.RenderWindow(); + private void RenderMenuBar() + { + // Add a main menu bar + if (ImGui.BeginMainMenuBar()) + { + RenderFileMenu(); + + if (ImGui.BeginMenu("Edit")) + { + if (ImGui.MenuItem("Settings")) + { + // Handle New action + + // TODO: Select preferred editor + // Required fields: + // radio button, each defined editor (store as json) + // textbox,Path to executable + // textbox, cli argument to open a file + } + + ImGui.EndMenu(); + } + + if (ImGui.BeginMenu("Windows")) + { + foreach (var window in _windows) + { + if (ImGui.MenuItem(window.Name)) + window.Open(); + } + + ImGui.EndMenu(); + } + + ImGui.EndMainMenuBar(); + } + } + + private void RenderFileMenu() + { + if (ImGui.BeginMenu("File")) + { + if (ImGui.MenuItem("New")) + { + // TODO: Handle New action + } + + if (ImGui.MenuItem("Open")) + { + // TODO: Handle Open action + } + + if (ImGui.MenuItem("Save")) + Save(); + + if (ImGui.MenuItem("Exit")) + { + // TODO: Handle Exit action + } + + if (ImGui.MenuItem("Publish")) + { + // TODO: Handle Publish action + } + + ImGui.EndMenu(); + } } /// @@ -135,13 +236,15 @@ public override void OnMouseClick(IMouse mouse, MouseButton button, Vector2 vect if (button == Settings.PrimaryButton) { - if (ImGui.IsAnyItemHovered()) + // TODO: This probably isn't doable using ImGui. Figure out a new way or wait until UI is written using the Core 2D renderer. + if (ImGui.IsItemClicked((ImGuiMouseButton)button)) { - // TODO: Right-clicked on an ImGui component + // Clicked on an ImGui component + uint hoveredItemId = ImGui.GetID(""); + Debug.LogInformation($"Clicked on ImGui component with ID: {hoveredItemId}"); } else { - // TODO: Right-clicked outside of ImGui components Camera.IsInView(Scene, out GameObject? intersectingObject, out Vector3 hitPosition); _contextMenuWindow?.ShowContextMenu(intersectingObject); } @@ -149,9 +252,12 @@ public override void OnMouseClick(IMouse mouse, MouseButton button, Vector2 vect if (button == Settings.SecondaryButton) { - if (ImGui.IsAnyItemHovered()) + // TODO: Make this work. + if (ImGui.IsItemClicked((ImGuiMouseButton)button)) { - // TODO: Left-clicked on an ImGui component + // Clicked on an ImGui component + uint hoveredItemId = ImGui.GetID(""); + Debug.LogInformation($"Clicked on ImGui component with ID: {hoveredItemId}"); } else { diff --git a/Editor/Program.cs b/Editor/Program.cs index 1dd9670..e899d16 100644 --- a/Editor/Program.cs +++ b/Editor/Program.cs @@ -1,9 +1,9 @@ -using Launcher.UI; -using SharpEngine.Core; +using SharpEngine.Core; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Scenes; using SharpEngine.Editor.Windows; using SharpEngine.Shared; +using SharpEngine.Shared.Dto; using System.Reflection; namespace SharpEngine.Editor; diff --git a/Editor/Windows/ActionsMenuWindow.cs b/Editor/Windows/ActionsMenuWindow.cs index af5e223..a0e356e 100644 --- a/Editor/Windows/ActionsMenuWindow.cs +++ b/Editor/Windows/ActionsMenuWindow.cs @@ -1,9 +1,9 @@ using ImGuiNET; -using Launcher.UI; using SharpEngine.Core; using SharpEngine.Core.Audio; using SharpEngine.Core.Scenes; -using SharpEngine.Shared; +using SharpEngine.Shared.Extensions; +using System.Text; namespace SharpEngine.Editor.Windows { @@ -32,16 +32,24 @@ public override void Render() if (ImGui.Button("Play")) StartGame(); + const uint size = 256; + var buffer = new byte[size]; + + ImGui.InputText("Audio path", buffer, size); if (ImGui.Button("Test audio")) - PlayTestAudio(); + PlayTestAudio(Encoding.Default.GetString(buffer)); } - private void PlayTestAudio() + private void PlayTestAudio(string filePath) { - try + if (!File.Exists(filePath)) { - string filePath = ""; // TODO: Replace with absolute path to .wav file. + Debug.LogInformation($"Audio file '{filePath}' does not exist."); + return; + } + try + { var audioPlayer = new WavPlayer(); audioPlayer.AudioProperties.IsLooping = false; diff --git a/Editor/Windows/AssetsWindow.cs b/Editor/Windows/AssetsWindow.cs index b9b7051..9f6f531 100644 --- a/Editor/Windows/AssetsWindow.cs +++ b/Editor/Windows/AssetsWindow.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using Launcher.UI; namespace SharpEngine.Editor.Windows { @@ -33,6 +32,9 @@ private static void RenderDirectory(string path) foreach (var file in files) if (ImGui.Selectable(Path.GetFileName(file))) { + // TODO: Open the file in the IDE + // Process.Start(Settings.PreferredIDE.Name + Settings.PreferredIDE.CLIArguments + file); + // Scene.ActiveElement = file; // Set the file as the active element } } diff --git a/Editor/Windows/ContextMenuWindow.cs b/Editor/Windows/ContextMenuWindow.cs index 8b10d97..1233710 100644 --- a/Editor/Windows/ContextMenuWindow.cs +++ b/Editor/Windows/ContextMenuWindow.cs @@ -1,5 +1,4 @@ using ImGuiNET; -using Launcher.UI; using SharpEngine.Core.Entities; using SharpEngine.Core.Primitives; using SharpEngine.Core.Scenes; @@ -20,7 +19,11 @@ public class ContextMenuWindow : ImGuiWindowBase public override string Name => "Context Menu"; /// - public override ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.AlwaysAutoResize; + public override ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.NoTitleBar | + ImGuiWindowFlags.NoResize | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDocking; /// /// Shows the context menu. @@ -63,7 +66,7 @@ private void RenderIntersectedObjectActions() { if (ImGui.Button("Delete")) { - Scene.Root.RemoveChild(IntersectedObject); + Scene.Root.RemoveChild(IntersectedObject!); IntersectedObject = null; } } @@ -82,6 +85,12 @@ private void RenderSceneActions() Scene.Root.AddChild(cube); } } + + if (ImGui.Button("Empty")) + { + var element = Scene.ActiveElement ?? Scene.Root; + element.AddChild(SceneNode.Empty); + } } } } diff --git a/Editor/Windows/ImGuiWindowBase.cs b/Editor/Windows/ImGuiWindowBase.cs index a75b2b3..5fabc93 100644 --- a/Editor/Windows/ImGuiWindowBase.cs +++ b/Editor/Windows/ImGuiWindowBase.cs @@ -1,8 +1,8 @@ using ImGuiNET; -using Launcher.UI; using SharpEngine.Core.Entities.Views.Settings; using SharpEngine.Core.Scenes; +using SharpEngine.Shared.Dto; namespace SharpEngine.Editor.Windows { @@ -17,6 +17,15 @@ public abstract class ImGuiWindowBase /// Gets or sets the currently active scene. public Scene Scene { get; private set; } + /// Gets the name of the window. + public abstract string Name { get; } + + /// + public virtual ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.None; + + private readonly Dictionary _previousDockingStates = []; + private bool _isVisible = true; + /// /// Initializes a new instance of . /// @@ -26,14 +35,6 @@ public ImGuiWindowBase() Project = new Project(); } - private readonly Dictionary _previousDockingStates = []; - - /// Gets the name of the window. - public abstract string Name { get; } - - /// - public virtual ImGuiWindowFlags ImGuiWindowFlags => ImGuiWindowFlags.None; - /// /// Sets the current scene. /// @@ -46,6 +47,11 @@ public ImGuiWindowBase() /// The new project. public void SetProject(Project project) => Project = project; + /// + /// Makes the ImGui window visible. + /// + public void Open() => _isVisible = true; + /// /// Executes operations required before rendering the ImGui window. /// @@ -56,9 +62,12 @@ public virtual void PreRender() { } /// public void RenderWindow() { + if (!_isVisible) + return; + PreRender(); - ImGui.Begin(Name, ImGuiWindowFlags); + ImGui.Begin(Name, ref _isVisible, ImGuiWindowFlags); Render(); @@ -92,7 +101,7 @@ private void CreateSilkWindow() var thread = new Thread(() => { - var window = new Core.Window(new(), new DefaultViewSettings()); + var window = new Core.Windowing.Window(new(), new DefaultViewSettings()); window.Closing += () => _previousDockingStates[Name] = false; window.OnAfterRender += deltaTime => diff --git a/Examples/MinecraftClone/Block/BlockBase.cs b/Examples/MinecraftClone/Block/BlockBase.cs index 15c7a93..7f06016 100644 --- a/Examples/MinecraftClone/Block/BlockBase.cs +++ b/Examples/MinecraftClone/Block/BlockBase.cs @@ -57,6 +57,6 @@ private void AssignProperties(GameObject cube, string name) Name = name; Material = cube.Material; - Mesh = cube.Mesh; + Meshes = cube.Meshes; } } diff --git a/Examples/MinecraftClone/Block/Dirt.cs b/Examples/MinecraftClone/Block/Dirt.cs index 8aada39..edcb5bb 100644 --- a/Examples/MinecraftClone/Block/Dirt.cs +++ b/Examples/MinecraftClone/Block/Dirt.cs @@ -13,10 +13,10 @@ internal class Dirt : BlockBase /// /// The position where the block should be initialized. /// The name of the block to initialize. - public Dirt(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources/grass.jpg"), - PathExtensions.GetAssemblyPath("Resources/container2_specular.png"), - PathExtensions.GetAssemblyPath("Shaders/shader.vert"), - PathExtensions.GetAssemblyPath("Shaders/lighting.frag")) { } + public Dirt(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\grass.jpg"), + PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), + PathExtensions.GetAssemblyPath("Shaders\\shader.vert"), + PathExtensions.GetAssemblyPath("Shaders\\lighting.frag")) { } /// public override BlockType BlockType => BlockType.Dirt; diff --git a/Examples/MinecraftClone/Block/Stone.cs b/Examples/MinecraftClone/Block/Stone.cs index 08dc773..599c086 100644 --- a/Examples/MinecraftClone/Block/Stone.cs +++ b/Examples/MinecraftClone/Block/Stone.cs @@ -13,10 +13,10 @@ internal class Stone : BlockBase /// /// The position where the block is created. /// The name of the object in the scene. - public Stone(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources/container2.png"), - PathExtensions.GetAssemblyPath("Resources/container2_specular.png"), - PathExtensions.GetAssemblyPath("Shaders/shader.vert"), - PathExtensions.GetAssemblyPath("Shaders/lighting.frag")) { } + public Stone(Vector3 position, string name) : base(position, name, PathExtensions.GetAssemblyPath("Resources\\container2.png"), + PathExtensions.GetAssemblyPath("Resources\\container2_specular.png"), + PathExtensions.GetAssemblyPath("Shaders\\shader.vert"), + PathExtensions.GetAssemblyPath("Shaders\\lighting.frag")) { } /// public override BlockType BlockType => BlockType.Stone; diff --git a/Examples/MinecraftClone/Minecraft.cs b/Examples/MinecraftClone/Minecraft.cs index e1311bd..036d8c7 100644 --- a/Examples/MinecraftClone/Minecraft.cs +++ b/Examples/MinecraftClone/Minecraft.cs @@ -10,6 +10,13 @@ using System; using System.Numerics; +using SharpEngine.Core.Entities.UI; +using SharpEngine.Core.Windowing; +using SharpEngine.Core.Entities.UI.Layouts; +using SharpEngine.Core.Entities.Properties; +using ImGuiNET; +using SharpEngine.Core.Entities.Properties.Meshes; +using System.Linq; namespace Minecraft; @@ -28,7 +35,12 @@ public class Minecraft : Game private Input _input; private readonly Inventory _inventory; - private UIElement uiElem; + private UIElement _uiElem; + + /// + /// Gets the main window. + /// + public Window? Window { get; set; } /// /// Initializes a new instance of the . @@ -39,6 +51,7 @@ public Minecraft(Scene scene, ISettings settings) CoreSettings = settings; _inventory = new Inventory(); + } /// @@ -54,9 +67,21 @@ public override void Initialize() _lightsNode = _scene.Root.AddChild("lights"); _blocksNode = _scene.Root.AddChild("blocks"); + var gridLayout = new GridLayout(); + // TODO: Fix UI renderer - uiElem = new UIElement("uiElement"); - _scene.UIElements.Add(uiElem); + _uiElem = new UIElement("uiElement"); + _scene.UIElements.Add(_uiElem); + + //var uiElem2 = new UIElement("uiElement"); + //uiElem2.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(0.2f, 0.2f); + //uiElem2.Transform.Position = new Vector2(30, 0); + + // gridLayout.AddChild(_uiElem, uiElem2); + // _scene.UIElements.Add(_uiElem); + // _scene.UIElements.Add(uiElem2); + + //_scene.UIElements.Add(gridLayout); InitializeWorld(); } @@ -66,10 +91,64 @@ public override void Initialize() } } + /// + /// Handles rendering after the frame is drawn. + /// + /// Information about the frame. + public void OnAfterRender(Frame frame) + { + var x = _uiElem.Transform.Position.X; + var y = _uiElem.Transform.Position.Y; + + var sx = _uiElem.Transform.Scale.X; + var sy = _uiElem.Transform.Scale.Y; + + var rotation = _uiElem.Transform.Rotation.Angle; + + var height = _uiElem.Height; + var width = _uiElem.Width; + + ImGui.Begin("Debug"); + ImGui.Text($"FPS: {frame.FrameRate}"); + ImGui.Text($"Camera position: {Camera.Position}"); + ImGui.Text($"UI Element position: {_uiElem.Transform.Position}"); + + ImGui.SliderFloat("X", ref x, -2000, 2000); + ImGui.SliderFloat("Y", ref y, -2000, 2000); + + ImGui.Text($"UI Element scale: {_uiElem.Transform.Scale}"); + + ImGui.SliderFloat("sX", ref sx, 0, 1); + ImGui.SliderFloat("sY", ref sy, 0, 1); + + ImGui.Text($"UI Element scale: {_uiElem.Transform.Scale}"); + ImGui.SliderFloat("rotation", ref rotation, 0, 360); + + ImGui.Text($"UI Element width: {_uiElem.Width}"); + ImGui.SliderFloat("width", ref width, 0, 100); + + ImGui.Text($"UI Element height: {_uiElem.Height}"); + ImGui.SliderFloat("height", ref height, 0, 100); + + ImGui.End(); + + _uiElem.Transform.Position = new SharpEngine.Core.Numerics.Vector2(x, y); + _uiElem.Transform.Scale = new SharpEngine.Core.Numerics.Vector2(sx, sy); + _uiElem.Transform.Rotation.Angle = rotation; + + _uiElem.Height = height; + _uiElem.Width = width; + } + private void InitializeWorld() { InitializeLights(); InitializeChunks(); + + // TODO: Does not work yet. + // var torus = MeshService.Instance.LoadMesh("torus", @"C:\Users\antti\Documents\Untitled2.obj"); + // var go = new GameObject(); + // go.Meshes.Add(torus.First()); } private void InitializeLights() @@ -89,11 +168,6 @@ private void InitializeLights() Diffuse = new Vector3(1.0f, 1.0f, 1.0f), Specular = new Vector3(1.0f, 1.0f, 1.0f), }); - - // var _lightingShader = ShaderService.Instance.LoadShader("Shaders/shader.vert", "Shaders/lighting.frag", "lighting"); - // _lightingShader.SetInt("numDirLights", 1); - // _lightingShader.SetInt("numPointLights", 4); - // _lightingShader.SetInt("numSpotLights", 1); } private void InitializeChunks() @@ -136,12 +210,12 @@ private void GenerateChunk(int chunkSize, Vector3 chunkPos) /// public override void Update(double deltaTime, IInputContext input) { - UpdateUI(); + //UpdateUI(); _input.HandleKeyboard(input.Keyboards[0], (float)deltaTime); } private void UpdateUI() - => uiElem.Transform.Rotation += 0.01f; + => _uiElem.Transform.Rotation.Angle += 0.01f; // TODO: Input system to let users change change key bindings? /// @@ -237,7 +311,7 @@ private void PlaceBlock() private static Vector3 GetNewBlockPosition(Vector3 hitPosition, GameObject intersectingObject) { Vector3 normal = Ray.GetClosestFaceNormal(hitPosition, intersectingObject); - return intersectingObject.Transform.Position + (normal * intersectingObject.Transform.Scale); + return (System.Numerics.Vector3)intersectingObject.Transform.Position + (normal * (System.Numerics.Vector3)intersectingObject.Transform.Scale); } /// diff --git a/Examples/MinecraftClone/Program.cs b/Examples/MinecraftClone/Program.cs index 376ee7a..bf3e6a3 100644 --- a/Examples/MinecraftClone/Program.cs +++ b/Examples/MinecraftClone/Program.cs @@ -1,5 +1,6 @@ using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; +using SharpEngine.Core.Windowing; namespace Minecraft; @@ -18,13 +19,18 @@ private static void Main() Scene scene = new Scene(); Minecraft game = new Minecraft(scene, gameSettings); - using var window = new SharpEngine.Core.Window(game.Camera, scene, game.Camera.Settings); - window.OnLoaded += () => game.Initialize(); + using var window = new Window(game.Camera, scene, game.Camera.Settings); + window.OnLoaded += game.Initialize; window.OnHandleMouse += game.HandleMouse; window.OnUpdate += game.Update; window.OnHandleKeyboard += game.HandleKeyboard; window.OnButtonMouseDown += game.HandleMouseDown; window.HandleMouseWheel += game.HandleMouseWheel; + window.OnAfterRender += frame => game.OnAfterRender(frame); + + // TODO: This needs to be streamlined. + game.Window = window; + window.Run(); } } diff --git a/Examples/Minimal/Program.cs b/Examples/Minimal/Program.cs index b3c78f4..9751459 100644 --- a/Examples/Minimal/Program.cs +++ b/Examples/Minimal/Program.cs @@ -1,8 +1,6 @@ using SharpEngine.Core.Interfaces; using SharpEngine.Core.Scenes; - -using Silk.NET.Maths; -using Silk.NET.Windowing; +using SharpEngine.Core.Windowing; namespace Minimal; @@ -19,7 +17,7 @@ public static void Main(string[] _) var game = new Minimal(new DefaultSettings()); var scene = new Scene(); - using var window = new SharpEngine.Core.Window(game.Camera, scene, game.Camera.Settings); + using var window = new Window(game.Camera, scene, game.Camera.Settings); window.Run(); } } diff --git a/Examples/MultipleWindows/MultipleWindows.csproj b/Examples/MultipleWindows/MultipleWindows.csproj index f87a3eb..c40ef83 100644 --- a/Examples/MultipleWindows/MultipleWindows.csproj +++ b/Examples/MultipleWindows/MultipleWindows.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/Examples/MultipleWindows/Program.cs b/Examples/MultipleWindows/Program.cs index 0f3a748..83caac5 100644 --- a/Examples/MultipleWindows/Program.cs +++ b/Examples/MultipleWindows/Program.cs @@ -1,39 +1,113 @@ -using Silk.NET.Core.Native; -using Silk.NET.GLFW; -using Silk.NET.OpenGL; +using Silk.NET.Input; +using Silk.NET.Maths; using Silk.NET.Windowing; -internal class Program +using System.Collections.Concurrent; +using SharpEngine.Core.Entities.Views.Settings; + +// An example provided a lovely person in this thread: +// https://github.com/dotnet/Silk.NET/issues/2436#issuecomment-2752966073 +public static partial class Program { - static void Main() + private static readonly List _windows = []; + private static readonly List _inputContexts = []; + private static readonly ConcurrentQueue _windowQueue = []; + private static readonly CancellationTokenSource _cancellationTokenSource = new(); + + /// + /// The main entry point of the application. + /// + /// Arguments discarded. + public static void Main(string[] _) { - var options1 = WindowOptions.Default with { Title = "Window 1", WindowClass = "MyWindowClass1" }; - var options2 = WindowOptions.Default with { Title = "Window 2", WindowClass = "MyWindowClass2" }; + StartWindowQueueTask(); - var window1 = Window.Create(options1); - var window2 = Window.Create(options2); + while (!_cancellationTokenSource.IsCancellationRequested) + { + for (int i = 0; i < _windows.Count; i++) + UpdateWindow(ref i); - StartWindow(window1); - StartWindow(window2); + DequeueWindows(); + } } - static void StartWindow(IWindow window) + private static void UpdateWindow(ref int i) { - window.Load += () => + var window = _windows[i]; + if (window is null) + return; + + window.DoEvents(); + window.DoUpdate(); + window.DoRender(); + + if (window.IsClosing) { - Console.WriteLine($"Hello from window: " + window.Title); + window.Reset(); + window.Dispose(); + + _windows.RemoveAt(i); + i--; // Adjust the index to account for the removed item + + if (_windows.Count == 0) + _cancellationTokenSource.Cancel(); + } + } - var gl = GL.GetApi(window); + private static void DequeueWindows() + { + if (_cancellationTokenSource.IsCancellationRequested) + return; + + while (_windowQueue.TryDequeue(out var options)) + EnqueueWindow(options); + } - unsafe + private static void StartWindowQueueTask() + => Task.Run(async () => + { + _windowQueue.Enqueue(WindowOptions.Default); + while (!_cancellationTokenSource.IsCancellationRequested) { - var versionPtr = gl.GetString(StringName.Version); - string? version = SilkMarshal.PtrToString((nint)versionPtr); - Console.WriteLine($"OpenGL version: {version}"); + await Task.Delay(1000); + Console.WriteLine("Running loop on background thread..."); } + }); + private static SharpEngine.Core.Windowing.Window CreateWindow() + { + var options = new DefaultViewSettings() with + { + WindowOptions = WindowOptions.Default with + { + Title = "Window" + _windows.Count, + + // This is to make sure the windows don't overlap + Position = new Vector2D( + x: 500 + (50 * _windows.Count), + y: 400 + (50 * _windows.Count)) + } }; - window.Run(); + //var window = Window.Create(options); + var window = new SharpEngine.Core.Windowing.Window(new(), options); + window.Initialize(); + + return window; + } + + private static void EnqueueWindow(WindowOptions options) + { + var window = CreateWindow(); + foreach (var mouse in window!.Input!.Mice) + mouse.Click += Mouse_Click; + + _inputContexts.Add(window.Input); + _windows.Add(window); + } + + private static void Mouse_Click(IMouse args1, Silk.NET.Input.MouseButton arg2, System.Numerics.Vector2 arg3) + { + _windowQueue.Enqueue(WindowOptions.Default); } } diff --git a/Launcher/Attributes/FileAttribute.cs b/Launcher/Attributes/FileAttribute.cs index 0c748ea..fe43381 100644 --- a/Launcher/Attributes/FileAttribute.cs +++ b/Launcher/Attributes/FileAttribute.cs @@ -12,7 +12,7 @@ public class FileAttribute : ValidationAttribute public override bool IsValid(object? value) { if (value is string path) - return System.IO.File.Exists(path); + return File.Exists(path); return false; } diff --git a/Launcher/Enums/SvgIcon.cs b/Launcher/Enums/SvgIcon.cs new file mode 100644 index 0000000..650ac01 --- /dev/null +++ b/Launcher/Enums/SvgIcon.cs @@ -0,0 +1,31 @@ +namespace Launcher.Enums; + +/// +/// Contains the supported SVG icons. +/// +public enum SvgIcon +{ + /// Indicates no icon. + None, + + /// Indicates the home icon. + Home, + + /// Indicates the settings icon. + Cog, + + /// Indicates the plus icon. + Discard, + + /// Indicates the search lens icon. + Lens, + + /// Indicates the close 'x' icon. + Close, + + /// Indicates the chevron '<' down icon. + ChevronDown, + + /// Indicates the chevron '<' up icon. + ChevronUp, +} diff --git a/Launcher/Services/EditorService.cs b/Launcher/Services/EditorService.cs index 22142f6..0f8f10a 100644 --- a/Launcher/Services/EditorService.cs +++ b/Launcher/Services/EditorService.cs @@ -1,5 +1,6 @@ -using Launcher.UI; -using SharpEngine.Shared; +using SharpEngine.Shared; +using SharpEngine.Shared.Dto; +using SharpEngine.Shared.Extensions; using System.Diagnostics; using System.Runtime.InteropServices; @@ -159,7 +160,10 @@ public List LoadProjects(string projectsFile) var projects = System.Text.Json.JsonSerializer.Deserialize>(json); if (projects is not null) + { + // TOOD: Resolve last modified return projects; + } _notificationService.Show($"Unable to load projects.", false, json, projects?.ToArray()); return []; diff --git a/Launcher/TestData/TestData.cs b/Launcher/TestData/TestData.cs new file mode 100644 index 0000000..0643b85 --- /dev/null +++ b/Launcher/TestData/TestData.cs @@ -0,0 +1,22 @@ +using SharpEngine.Shared.Dto; + +namespace Launcher.TestData; + +/// +/// Contains test data for the launcher. +/// +public static class TestData +{ + /// Indicates whether test data should be loaded. + public const bool LoadTestData = true; + + /// Indicates whether to simulate loading. + public const bool SimulateLoading = false; + + /// A collection of test data projects. + public static IEnumerable Projects = Enumerable.Range(0, 25).Select(i => new Project() + { + Name = $"Project {i}", + Path = @"C:\test", + }); +} diff --git a/Launcher/UI/Components/ContextMenu.razor b/Launcher/UI/Components/ContextMenu.razor new file mode 100644 index 0000000..6f3d22f --- /dev/null +++ b/Launcher/UI/Components/ContextMenu.razor @@ -0,0 +1,33 @@ +@if (showMenu) +{ +
+ @ChildContent(_context!) +
+} + +@code { + [Inject] + private IJSRuntime JS { get; init; } = default!; + + [Parameter] + public RenderFragment ChildContent { get; set; } + + private object? _context; + private bool showMenu = false; + private int menuX, menuY; + + private string menuXpx => $"{menuX}px"; + private string menuYpx => $"{menuY}px"; + + public void Open(MouseEventArgs e, object context) + { + menuX = (int)e.ClientX; + menuY = (int)e.ClientY; + showMenu = true; + + _context = context; + + // Prevent the default browser context menu + JS.InvokeVoidAsync("preventContextMenu"); + } +} diff --git a/Launcher/UI/Components/ContextMenu.razor.css b/Launcher/UI/Components/ContextMenu.razor.css new file mode 100644 index 0000000..d0f597f --- /dev/null +++ b/Launcher/UI/Components/ContextMenu.razor.css @@ -0,0 +1,9 @@ +.context-menu { + position: absolute; + z-index: 1000; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 8px; +} diff --git a/Launcher/UI/Components/ContextMenu.razor.min.css b/Launcher/UI/Components/ContextMenu.razor.min.css new file mode 100644 index 0000000..c5c1a48 --- /dev/null +++ b/Launcher/UI/Components/ContextMenu.razor.min.css @@ -0,0 +1 @@ +.context-menu{position:absolute;z-index:1000;background-color:#fff;border:1px solid #ccc;border-radius:4px;box-shadow:0 2px 4px rgba(0,0,0,.1);padding:8px;} \ No newline at end of file diff --git a/Launcher/UI/Components/ContextMenu.razor.scss b/Launcher/UI/Components/ContextMenu.razor.scss new file mode 100644 index 0000000..580b5a7 --- /dev/null +++ b/Launcher/UI/Components/ContextMenu.razor.scss @@ -0,0 +1,9 @@ +.context-menu { + position: absolute; + z-index: 1000; + background-color: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 8px; +} diff --git a/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor b/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor index 52ed7ad..53bdec2 100644 --- a/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor +++ b/Launcher/UI/Components/Dialogs/ConfirmDeleteDialog.razor @@ -22,7 +22,7 @@ @code { private string _confirmDeleteVisibleClass => Visible ? "confirm-delete-visible" : "confirm-delete-hidden"; - /// Gets or sets the event exectued when the project deletion is confirmed. + /// Gets or sets the event executed when the project deletion is confirmed. [Parameter] public EventCallback OnDeleteProjectConfirmed { get; set; } diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor index 27d5313..5f3275d 100644 --- a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor +++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor @@ -10,7 +10,7 @@ -
+
@@ -18,7 +18,7 @@
-
+
diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs index ad2eaff..7d1b324 100644 --- a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs +++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Components; using Launcher.Services; using CommunityToolkit.Maui.Storage; +using SharpEngine.Shared.Dto; namespace Launcher.UI.Components.Dialogs { diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css new file mode 100644 index 0000000..a76d394 --- /dev/null +++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.css @@ -0,0 +1,9 @@ +.field-path-container { + display: flex; + align-items: center; + gap: 8px; +} + +.field-name-container { + margin-bottom: 8px; +} diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css new file mode 100644 index 0000000..0ee1ee3 --- /dev/null +++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.min.css @@ -0,0 +1 @@ +.field-path-container{display:flex;align-items:center;gap:8px;}.field-name-container{margin-bottom:8px;} \ No newline at end of file diff --git a/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss new file mode 100644 index 0000000..6457dd0 --- /dev/null +++ b/Launcher/UI/Components/Dialogs/CreateProjectDialog.razor.scss @@ -0,0 +1,9 @@ +.field-path-container { + display: flex; + align-items: center; + gap: 8px; +} + +.field-name-container { + margin-bottom: 8px; +} \ No newline at end of file diff --git a/Launcher/UI/Components/Dialogs/Notification.razor b/Launcher/UI/Components/Dialogs/Notification.razor deleted file mode 100644 index 8a40e57..0000000 --- a/Launcher/UI/Components/Dialogs/Notification.razor +++ /dev/null @@ -1,29 +0,0 @@ -@using Launcher.Services -@inherits DialogBase - -@inject INotificationService NotificationService - -
-
- @foreach (var notification in NotificationService.Notifications) - { - - } -
-
- -@code { - /// Gets or sets the type of the notification. - [Parameter] - public NotificationType Type { get; set; } = NotificationType.Warning; - - /// - protected override void OnInitialized() - { - NotificationService.SetInvokeAsync(InvokeAsync); - NotificationService.SetOnNotificationsChanged(StateHasChanged); - } -} diff --git a/Launcher/UI/Components/DiscardIcon.razor.css b/Launcher/UI/Components/DiscardIcon.razor.css deleted file mode 100644 index 301f29c..0000000 --- a/Launcher/UI/Components/DiscardIcon.razor.css +++ /dev/null @@ -1,6 +0,0 @@ -span { - cursor: pointer; -} -span:hover { - filter: brightness(0.8); -} diff --git a/Launcher/UI/Components/DiscardIcon.razor.min.css b/Launcher/UI/Components/DiscardIcon.razor.min.css deleted file mode 100644 index 6048a35..0000000 --- a/Launcher/UI/Components/DiscardIcon.razor.min.css +++ /dev/null @@ -1 +0,0 @@ -span{cursor:pointer;}span:hover{filter:brightness(.8);} \ No newline at end of file diff --git a/Launcher/UI/Components/DiscardIcon.razor.scss b/Launcher/UI/Components/DiscardIcon.razor.scss deleted file mode 100644 index f5cb62a..0000000 --- a/Launcher/UI/Components/DiscardIcon.razor.scss +++ /dev/null @@ -1,7 +0,0 @@ -span { - cursor: pointer; - - &:hover { - filter: brightness(0.8); - } -} diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor b/Launcher/UI/Components/Flyouts/Flyout.razor new file mode 100644 index 0000000..5fca179 --- /dev/null +++ b/Launcher/UI/Components/Flyouts/Flyout.razor @@ -0,0 +1,56 @@ +@using Launcher.Enums +
+
+
+ @if (Header is not null) + { + @Header + } + else + { +

@Title

+ } +
+ +
+
+ @Content +
+ +
+ +@code { + /// Gets or sets the header content. + [Parameter] + public RenderFragment Header { get; set; } + + /// Gets or sets the main content. + [Parameter] + public RenderFragment Content { get; set; } + + /// Gets or sets the footer content. + [Parameter] + public RenderFragment Footer { get; set; } + + /// Gets or sets the title. + [Parameter] + public string Title { get; set; } = default!; + + /// Gets or sets whether the flyout is visible. + [Parameter] + public bool IsVisible { get; set; } + + /// Gets or sets the event executed when the visibility changes. + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + private string _isVisibleStyle => IsVisible ? "grid" : "none"; + + private async Task Close() + { + IsVisible = false; + await IsVisibleChanged.InvokeAsync(IsVisible); + } +} diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor.css b/Launcher/UI/Components/Flyouts/Flyout.razor.css new file mode 100644 index 0000000..82882fa --- /dev/null +++ b/Launcher/UI/Components/Flyouts/Flyout.razor.css @@ -0,0 +1,38 @@ +.flyout { + display: none; + position: absolute; + top: 0; + width: 50%; + height: 100vh; + z-index: 9999; + background: white; + border-right: 1px solid black; + left: 50px; + grid-template-rows: 70px auto 70px; +} + +.flyout-header { + display: flex; + justify-content: space-between; +} +.flyout-header button { + border: none; + background: none; +} + +.flyout-content { + overflow-y: scroll; + padding-left: 1em; +} + +.flyout-footer { + border-top: 1px solid black; + padding: 5px; + display: flex; + justify-content: end; + gap: 8px; +} + +.flyout-footer, .flyout-header, .flyout-content { + padding: 1em; +} diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor.min.css b/Launcher/UI/Components/Flyouts/Flyout.razor.min.css new file mode 100644 index 0000000..f1bb45f --- /dev/null +++ b/Launcher/UI/Components/Flyouts/Flyout.razor.min.css @@ -0,0 +1 @@ +.flyout{display:none;position:absolute;top:0;width:50%;height:100vh;z-index:9999;background:#fff;border-right:1px solid #000;left:50px;grid-template-rows:70px auto 70px;}.flyout-header{display:flex;justify-content:space-between;}.flyout-header button{border:0;background:none;}.flyout-content{overflow-y:scroll;padding-left:1em;}.flyout-footer{border-top:1px solid #000;padding:5px;display:flex;justify-content:end;gap:8px;}.flyout-footer,.flyout-header,.flyout-content{padding:1em;} \ No newline at end of file diff --git a/Launcher/UI/Components/Flyouts/Flyout.razor.scss b/Launcher/UI/Components/Flyouts/Flyout.razor.scss new file mode 100644 index 0000000..decc8e0 --- /dev/null +++ b/Launcher/UI/Components/Flyouts/Flyout.razor.scss @@ -0,0 +1,39 @@ +.flyout { + display: none; + position: absolute; + top: 0; + width: 50%; + height: 100vh; + z-index: 9999; + background: white; + border-right: 1px solid black; + left: 50px; + grid-template-rows: 70px auto 70px; +} + +.flyout-header { + display: flex; + justify-content: space-between; + + button { + border: none; + background: none; + } +} + +.flyout-content { + overflow-y: scroll; + padding-left: 1em; +} + +.flyout-footer { + border-top: 1px solid black; + padding: 5px; + display: flex; + justify-content: end; + gap: 8px; +} + +.flyout-footer, .flyout-header, .flyout-content { + padding: 1em; +} \ No newline at end of file diff --git a/Launcher/UI/Components/Flyouts/FlyoutBase.cs b/Launcher/UI/Components/Flyouts/FlyoutBase.cs new file mode 100644 index 0000000..0d0d302 --- /dev/null +++ b/Launcher/UI/Components/Flyouts/FlyoutBase.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Components; + +namespace Launcher.UI.Components.Flyouts; + +/// +/// Represents a base class for flyout components. +/// +public class FlyoutBase : ComponentBase +{ + /// Gets or sets whether the flyout is visible. + [Parameter] + public bool IsVisible { get; set; } + + /// Gets or sets the event executed when the visibility changes. + [Parameter] + public EventCallback IsVisibleChanged { get; set; } + + /// Gets or sets the event executed when the flyout is closed. + [Parameter] + public EventCallback OnClose { get; set; } + + /// + /// Handles changes in visibility asynchronously and updates the visibility state. + /// + /// Indicates whether the element is currently visible or not. + /// + /// A representing an asynchronous operation. + /// + public virtual async Task OnIsVisibleChangedAsync(bool visibility) + { + IsVisible = visibility; + await IsVisibleChanged.InvokeAsync(visibility); + } + + /// + /// Closes the flyout. + /// + /// + /// A representing an asynchronous operation. + /// + public virtual async Task Close() + { + await OnClose.InvokeAsync(); + await OnIsVisibleChangedAsync(false); + } +} diff --git a/Launcher/UI/Components/Flyouts/SettingsFlyout.razor b/Launcher/UI/Components/Flyouts/SettingsFlyout.razor new file mode 100644 index 0000000..e8c7930 --- /dev/null +++ b/Launcher/UI/Components/Flyouts/SettingsFlyout.razor @@ -0,0 +1,19 @@ +@inherits FlyoutBase + + +
+ + +
+
+ +@code { + [Inject] + private INotificationService _notificationService { get; init; } = default!; + + private async Task SaveSettings() + { + _notificationService.Show("Settings saved."); + await Close(); + } +} diff --git a/Launcher/UI/Components/Grid.razor b/Launcher/UI/Components/Grid.razor deleted file mode 100644 index a65a5ef..0000000 --- a/Launcher/UI/Components/Grid.razor +++ /dev/null @@ -1,35 +0,0 @@ -@typeparam TItem - - - - - - - - - - - @foreach (var item in Items) - { - - - @Row(item) - - - } - -
NameLast ModifiedActions
- -@code { - /// - /// Gets or sets the items to be rendered onto the grid. - /// - [Parameter] - public List Items { get; set; } - - /// - /// Gets or sets a template by which each row should be rendered. - /// - [Parameter] - public RenderFragment Row { get; set; } -} \ No newline at end of file diff --git a/Launcher/UI/Components/Grid/Grid.razor b/Launcher/UI/Components/Grid/Grid.razor new file mode 100644 index 0000000..2b23f84 --- /dev/null +++ b/Launcher/UI/Components/Grid/Grid.razor @@ -0,0 +1,84 @@ +@using System.ComponentModel +@using SharpEngine.Shared.Attributes + +@typeparam TItem + +
+ + + + @foreach (var header in _headers) + { + + } + + +
+
+ + +
+ + + @foreach (var item in Items) + { + + + @GridRowTemplate(item) + + + } + +
+
+ +@code { + /// + /// Gets or sets the items to be rendered onto the grid. + /// + [Parameter] + public List Items { get; set; } = []; + + /// + /// Gets or sets a template by which each row should be rendered. + /// + [Parameter] + public RenderFragment GridRowTemplate { get; set; } + + [Parameter] + public EventCallback> FilterChanged { get; set; } + + private List _headers { get; set; } = []; + private Dictionary _filterModes { get; set; } = new(); + + /// + protected override Task OnInitializedAsync() + { + GetHeaders(); + return base.OnInitializedAsync(); + } + + private void GetHeaders() + { + var props = typeof(TItem).GetProperties(); + var headers = props.Select(prop => + { + var displayNameAttribute = (DisplayNameAttribute?)prop.GetCustomAttributes(typeof(DisplayNameAttribute), false).FirstOrDefault(); + var filterAttribute = (FilterAttribute?)prop.GetCustomAttributes(typeof(FilterAttribute), false).FirstOrDefault(); + + return new GridHeaderInfo(displayNameAttribute?.DisplayName ?? prop.Name, filterAttribute?.IsFilterable ?? true); + }); + + _headers = headers.ToList(); + } + + private async Task OnFilterChanged(FilterMode filterMode, string header) + { + if (_filterModes.TryGetValue(header, out var mode)) + _filterModes[header] = filterMode; + else + _filterModes.Add(header, filterMode); + + await FilterChanged.InvokeAsync(_filterModes); + } +} \ No newline at end of file diff --git a/Launcher/UI/Components/Grid/Grid.razor.css b/Launcher/UI/Components/Grid/Grid.razor.css new file mode 100644 index 0000000..96e0633 --- /dev/null +++ b/Launcher/UI/Components/Grid/Grid.razor.css @@ -0,0 +1,19 @@ +.auto-width-table { + table-layout: auto; + width: 100%; +} + +.auto-width-table th, .auto-width-table td { + white-space: nowrap; + padding: 8px; + text-align: left; +} + +.table-header { + border-bottom: 1px solid black; +} + +.table-content { + overflow-y: scroll; + height: 250px; +} diff --git a/Launcher/UI/Components/Grid/Grid.razor.min.css b/Launcher/UI/Components/Grid/Grid.razor.min.css new file mode 100644 index 0000000..74f740a --- /dev/null +++ b/Launcher/UI/Components/Grid/Grid.razor.min.css @@ -0,0 +1 @@ +.auto-width-table{table-layout:auto;width:100%;}.auto-width-table th,.auto-width-table td{white-space:nowrap;padding:8px;text-align:left;}.table-header{border-bottom:1px solid #000;}.table-content{overflow-y:scroll;height:250px;} \ No newline at end of file diff --git a/Launcher/UI/Components/Grid/Grid.razor.scss b/Launcher/UI/Components/Grid/Grid.razor.scss new file mode 100644 index 0000000..daf24c1 --- /dev/null +++ b/Launcher/UI/Components/Grid/Grid.razor.scss @@ -0,0 +1,19 @@ +.auto-width-table { + table-layout: auto; + width: 100%; +} + +.auto-width-table th, .auto-width-table td { + white-space: nowrap; + padding: 8px; + text-align: left; +} + +.table-header { + border-bottom: 1px solid black; +} + +.table-content { + overflow-y: scroll; + height: 250px; +} \ No newline at end of file diff --git a/Launcher/UI/Components/Grid/GridHeader.cs b/Launcher/UI/Components/Grid/GridHeader.cs new file mode 100644 index 0000000..91e07ec --- /dev/null +++ b/Launcher/UI/Components/Grid/GridHeader.cs @@ -0,0 +1,24 @@ +namespace Launcher.UI.Components.Grid; + +/// +/// Contains information about a grid header. +/// +public class GridHeaderInfo +{ + /// Gets or sets the title of the column. + public string Title { get; set; } + + /// Gets or sets whether the column allows filtering. + public bool AllowFiltering { get; set; } + + /// + /// Initializes a new instance of the . + /// + /// Specifies the header title for the grid. + /// Indicates whether filtering is permitted on the grid header. + public GridHeaderInfo(string title, bool allowFiltering) + { + Title = title; + AllowFiltering = allowFiltering; + } +} diff --git a/Launcher/UI/Components/Grid/GridHeader.razor b/Launcher/UI/Components/Grid/GridHeader.razor new file mode 100644 index 0000000..e5a8481 --- /dev/null +++ b/Launcher/UI/Components/Grid/GridHeader.razor @@ -0,0 +1,47 @@ + + + + +@code { + /// Gets or sets the contents of the grid row header. + [Parameter] + public string Title { get; set; } = default!; + + /// Gets or sets whether filtering is allowed for the header. + [Parameter] + public bool AllowFiltering { get; set; } + + /// Gets or sets the event executed when the filter mode changes. + [Parameter] + public EventCallback FilterChanged { get; set; } + + /// Gets or sets the filter mode of the header. + [Parameter] + public FilterMode FilterMode { get; set; } = FilterMode.None; + + private string _filterClass => AllowFiltering ? "filter-btn" : "filter-disabled"; + + private SvgIcon _icon => this.FilterMode switch + { + FilterMode.Ascending => SvgIcon.ChevronUp, + FilterMode.Descending => SvgIcon.ChevronDown, + _ => SvgIcon.None + }; + + private async Task OnFilterClicked() + { + FilterMode = FilterMode switch + { + FilterMode.None => FilterMode.Ascending, + FilterMode.Ascending => FilterMode.Descending, + FilterMode.Descending => FilterMode.None, + _ => FilterMode.None + }; + + if (AllowFiltering) + await FilterChanged.InvokeAsync(FilterMode); + } +} diff --git a/Launcher/UI/Components/Grid/GridHeader.razor.css b/Launcher/UI/Components/Grid/GridHeader.razor.css new file mode 100644 index 0000000..642257e --- /dev/null +++ b/Launcher/UI/Components/Grid/GridHeader.razor.css @@ -0,0 +1,15 @@ +.btn { + background: transparent; + border: none; + padding: 0; +} + +.filter-btn { + display: flex; + align-items: center; +} + +.filter-disabled { + cursor: none; + pointer-events: none; +} diff --git a/Launcher/UI/Components/Grid/GridHeader.razor.min.css b/Launcher/UI/Components/Grid/GridHeader.razor.min.css new file mode 100644 index 0000000..b5f165e --- /dev/null +++ b/Launcher/UI/Components/Grid/GridHeader.razor.min.css @@ -0,0 +1 @@ +.btn{background:transparent;border:0;padding:0;}.filter-btn{display:flex;align-items:center;}.filter-disabled{cursor:none;pointer-events:none;} \ No newline at end of file diff --git a/Launcher/UI/Components/Grid/GridHeader.razor.scss b/Launcher/UI/Components/Grid/GridHeader.razor.scss new file mode 100644 index 0000000..e0f25a0 --- /dev/null +++ b/Launcher/UI/Components/Grid/GridHeader.razor.scss @@ -0,0 +1,15 @@ +.btn { + background: transparent; + border: none; + padding: 0; +} + +.filter-btn { + display: flex; + align-items: center; +} + +.filter-disabled { + cursor: none; + pointer-events: none; +} \ No newline at end of file diff --git a/Launcher/UI/Components/GridRow.razor b/Launcher/UI/Components/Grid/GridRow.razor similarity index 100% rename from Launcher/UI/Components/GridRow.razor rename to Launcher/UI/Components/Grid/GridRow.razor diff --git a/Launcher/UI/Components/DiscardIcon.razor b/Launcher/UI/Components/Icons/DiscardIcon.razor similarity index 54% rename from Launcher/UI/Components/DiscardIcon.razor rename to Launcher/UI/Components/Icons/DiscardIcon.razor index 0b36549..ee38ca5 100644 --- a/Launcher/UI/Components/DiscardIcon.razor +++ b/Launcher/UI/Components/Icons/DiscardIcon.razor @@ -1,11 +1,10 @@ -✖ + + + @code { /// Gets or sets the event executed when the discard icon is clicked. [Parameter] public EventCallback DiscardClicked { get; set; } - - private async Task OnDiscardClicked() - => await DiscardClicked.InvokeAsync(); } diff --git a/Launcher/UI/Components/Icons/Icon.razor b/Launcher/UI/Components/Icons/Icon.razor new file mode 100644 index 0000000..4c2b415 --- /dev/null +++ b/Launcher/UI/Components/Icons/Icon.razor @@ -0,0 +1,73 @@ +@using Launcher.Enums + +@switch (IconType) +{ + case SvgIcon.Home: + + + + + + + + break; + case SvgIcon.Cog: + + + + + + + + break; + case SvgIcon.Lens: + + + + + + + break; + case SvgIcon.Discard: + ✖ + break; + case SvgIcon.Close: + + + + + + + + break; + case SvgIcon.ChevronDown: + + + + + + + break; + case SvgIcon.ChevronUp: + + + + + + + + break; + default: + break; +} + +@code { + + /// Gets or sets the type of the icon. + [Parameter] + public SvgIcon IconType { get; set; } + + /// Gets or sets the size of the icon in pixels. + [Parameter] + public int SizePx { get; set; } = 16; +} diff --git a/Launcher/UI/Components/Layout.razor b/Launcher/UI/Components/Layout.razor index 21adf40..507dd2d 100644 --- a/Launcher/UI/Components/Layout.razor +++ b/Launcher/UI/Components/Layout.razor @@ -1,5 +1,23 @@ @inherits LayoutComponentBase
-@Body -
\ No newline at end of file + + + + + + + +
+ @Body +
+
+ + + +@code { + private bool _settingsVisible; + + private void OpenSettingsFlyout() + => _settingsVisible = true; +} \ No newline at end of file diff --git a/Launcher/UI/Components/Layout.razor.css b/Launcher/UI/Components/Layout.razor.css new file mode 100644 index 0000000..cb2bbbb --- /dev/null +++ b/Launcher/UI/Components/Layout.razor.css @@ -0,0 +1,6 @@ +.content { + margin: 0 1em; + padding: 0; + display: grid; + grid-template-rows: 100px auto; +} diff --git a/Launcher/UI/Components/Layout.razor.min.css b/Launcher/UI/Components/Layout.razor.min.css new file mode 100644 index 0000000..aee90ef --- /dev/null +++ b/Launcher/UI/Components/Layout.razor.min.css @@ -0,0 +1 @@ +.content{margin:0 1em;padding:0;display:grid;grid-template-rows:100px auto;} \ No newline at end of file diff --git a/Launcher/UI/Components/Layout.razor.scss b/Launcher/UI/Components/Layout.razor.scss new file mode 100644 index 0000000..6a00208 --- /dev/null +++ b/Launcher/UI/Components/Layout.razor.scss @@ -0,0 +1,6 @@ +.content { + margin: 0 1em; + padding: 0; + display: grid; + grid-template-rows: 100px auto; +} diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor b/Launcher/UI/Components/Navigation/SideBarItem.razor new file mode 100644 index 0000000..fe8bc2b --- /dev/null +++ b/Launcher/UI/Components/Navigation/SideBarItem.razor @@ -0,0 +1,14 @@ + + +@code { + /// Gets or set the icon for the sidebar item. + [Parameter] + public SvgIcon Icon { get; set; } + + /// Gets or sets the event executed when the sidebar item is clicked. + [Parameter] + public EventCallback OnClick { get; set; } + + private async Task OnItemClicked() + => await OnClick.InvokeAsync(); +} diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor.css b/Launcher/UI/Components/Navigation/SideBarItem.razor.css new file mode 100644 index 0000000..63ddf80 --- /dev/null +++ b/Launcher/UI/Components/Navigation/SideBarItem.razor.css @@ -0,0 +1,5 @@ +.sidebar-item { + cursor: pointer; + list-style: none; + padding: 0.5rem 1em; +} diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css b/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css new file mode 100644 index 0000000..38c8234 --- /dev/null +++ b/Launcher/UI/Components/Navigation/SideBarItem.razor.min.css @@ -0,0 +1 @@ +.sidebar-item{cursor:pointer;list-style:none;padding:.5rem 1em;} \ No newline at end of file diff --git a/Launcher/UI/Components/Navigation/SideBarItem.razor.scss b/Launcher/UI/Components/Navigation/SideBarItem.razor.scss new file mode 100644 index 0000000..73c8089 --- /dev/null +++ b/Launcher/UI/Components/Navigation/SideBarItem.razor.scss @@ -0,0 +1,5 @@ +.sidebar-item { + cursor: pointer; + list-style: none; + padding: 0.5rem 1em; +} diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor b/Launcher/UI/Components/Navigation/Sidebar.razor new file mode 100644 index 0000000..bc3ca2c --- /dev/null +++ b/Launcher/UI/Components/Navigation/Sidebar.razor @@ -0,0 +1,11 @@ + + +@code { + /// Gets or sets the sidebar items to be rendered within the sidebar. + [Parameter, EditorRequired] + public RenderFragment ChildContent { get; set; } +} diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor.css b/Launcher/UI/Components/Navigation/Sidebar.razor.css new file mode 100644 index 0000000..8d28b22 --- /dev/null +++ b/Launcher/UI/Components/Navigation/Sidebar.razor.css @@ -0,0 +1,6 @@ +.sidebar { + border-right: 1px solid black; +} +.sidebar ul { + padding: 0; +} diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor.min.css b/Launcher/UI/Components/Navigation/Sidebar.razor.min.css new file mode 100644 index 0000000..d701475 --- /dev/null +++ b/Launcher/UI/Components/Navigation/Sidebar.razor.min.css @@ -0,0 +1 @@ +.sidebar{border-right:1px solid #000;}.sidebar ul{padding:0;} \ No newline at end of file diff --git a/Launcher/UI/Components/Navigation/Sidebar.razor.scss b/Launcher/UI/Components/Navigation/Sidebar.razor.scss new file mode 100644 index 0000000..6a71104 --- /dev/null +++ b/Launcher/UI/Components/Navigation/Sidebar.razor.scss @@ -0,0 +1,7 @@ +.sidebar { + border-right: 1px solid black; + + ul { + padding: 0; + } +} diff --git a/Launcher/UI/Components/Notification/Notification.razor b/Launcher/UI/Components/Notification/Notification.razor new file mode 100644 index 0000000..a46aee5 --- /dev/null +++ b/Launcher/UI/Components/Notification/Notification.razor @@ -0,0 +1,30 @@ +@inherits DialogBase + +@inject INotificationService NotificationService + +
+
+ + @foreach (var notification in NotificationService.Notifications) + { + + } + +
+
+ +@code { + /// Gets or sets the type of the notification. + [Parameter] + public NotificationType Type { get; set; } = NotificationType.Warning; + + /// + protected override void OnInitialized() + { + NotificationService.SetInvokeAsync(InvokeAsync); + NotificationService.SetOnNotificationsChanged(StateHasChanged); + } +} diff --git a/Launcher/UI/Components/Notification/Notification.razor.css b/Launcher/UI/Components/Notification/Notification.razor.css new file mode 100644 index 0000000..e12e310 --- /dev/null +++ b/Launcher/UI/Components/Notification/Notification.razor.css @@ -0,0 +1,18 @@ +.notification-container { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; +} + +.notification { + position: relative; + display: flex; + top: 50px; + flex-direction: column; + align-items: end; +} + +.notification-content { + pointer-events: auto; +} diff --git a/Launcher/UI/Components/Notification/Notification.razor.min.css b/Launcher/UI/Components/Notification/Notification.razor.min.css new file mode 100644 index 0000000..c478afd --- /dev/null +++ b/Launcher/UI/Components/Notification/Notification.razor.min.css @@ -0,0 +1 @@ +.notification-container{width:100%;height:100%;position:absolute;pointer-events:none;}.notification{position:relative;display:flex;top:50px;flex-direction:column;align-items:end;}.notification-content{pointer-events:auto;} \ No newline at end of file diff --git a/Launcher/UI/Components/Notification/Notification.razor.scss b/Launcher/UI/Components/Notification/Notification.razor.scss new file mode 100644 index 0000000..f44c2d1 --- /dev/null +++ b/Launcher/UI/Components/Notification/Notification.razor.scss @@ -0,0 +1,18 @@ +.notification-container { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; +} + +.notification { + position: relative; + display: flex; + top: 50px; + flex-direction: column; + align-items: end; +} + +.notification-content { + pointer-events: auto; +} \ No newline at end of file diff --git a/Launcher/UI/Components/Notification/NotificationType.cs b/Launcher/UI/Components/Notification/NotificationType.cs new file mode 100644 index 0000000..a5fd255 --- /dev/null +++ b/Launcher/UI/Components/Notification/NotificationType.cs @@ -0,0 +1,32 @@ +namespace Launcher.UI.Components.Notification +{ + /// + /// Represents the type of notification. + /// + public enum NotificationType + { + /// Primary notification type. + Primary, + + /// Secondary notification type. + Secondary, + + /// Success notification type. + Success, + + /// Danger notification type. + Danger, + + /// Warning notification type. + Warning, + + /// Info notification type. + Info, + + /// Light notification type. + Light, + + /// Dark notification type. + Dark, + } +} diff --git a/Launcher/UI/Components/Dialogs/NotificationType.cs b/Launcher/UI/Components/Notification/NotificationTypeExtensions.cs similarity index 59% rename from Launcher/UI/Components/Dialogs/NotificationType.cs rename to Launcher/UI/Components/Notification/NotificationTypeExtensions.cs index 9f34c0a..7762cf4 100644 --- a/Launcher/UI/Components/Dialogs/NotificationType.cs +++ b/Launcher/UI/Components/Notification/NotificationTypeExtensions.cs @@ -1,35 +1,5 @@ -namespace Launcher.UI.Components +namespace Launcher.UI.Components.Notification { - /// - /// Represents the type of notification. - /// - public enum NotificationType - { - /// Primary notification type. - Primary, - - /// Secondary notification type. - Secondary, - - /// Success notification type. - Success, - - /// Danger notification type. - Danger, - - /// Warning notification type. - Warning, - - /// Info notification type. - Info, - - /// Light notification type. - Light, - - /// Dark notification type. - Dark, - } - /// /// Extension methods for . /// diff --git a/Launcher/UI/Components/Searchbox.razor b/Launcher/UI/Components/Searchbox.razor new file mode 100644 index 0000000..2417a3e --- /dev/null +++ b/Launcher/UI/Components/Searchbox.razor @@ -0,0 +1,35 @@ + + +@code { + /// Gets or sets the value of the search box. + [Parameter] + public string Value { get; set; } = default!; + + /// Gets or sets the event executed when the value of the search box changes. + [Parameter] + public EventCallback ValueChanged { get; set; } + + /// Gets or sets the delay in milliseconds before the value is updated after the user stops typing. + [Parameter] + public int DebounceDelayMilliseconds { get; set; } = 1000; + + /// Gets or sets any extra CSS classes to apply to the search box. + [Parameter] + public string? CssClass { get; set; } + + private CancellationTokenSource _tokenSource { get; set; } = new(); + + private async Task OnChangeAsync(ChangeEventArgs e) + { + _tokenSource = new CancellationTokenSource(); + var token = _tokenSource.Token; + + await Task.Delay(DebounceDelayMilliseconds, token); + + Value = e.Value?.ToString() ?? string.Empty; + await ValueChanged.InvokeAsync(Value); + } +} diff --git a/Launcher/UI/Components/Searchbox.razor.css b/Launcher/UI/Components/Searchbox.razor.css new file mode 100644 index 0000000..9f2bc34 --- /dev/null +++ b/Launcher/UI/Components/Searchbox.razor.css @@ -0,0 +1,14 @@ +.searchbox { + border: 1px solid black; + width: 40%; + justify-self: end; +} + +.searchbox-btn { + border: none; + background: none; +} + +.searchbox-input { + border: none; +} diff --git a/Launcher/UI/Components/Searchbox.razor.min.css b/Launcher/UI/Components/Searchbox.razor.min.css new file mode 100644 index 0000000..2641fc8 --- /dev/null +++ b/Launcher/UI/Components/Searchbox.razor.min.css @@ -0,0 +1 @@ +.searchbox{border:1px solid #000;width:40%;justify-self:end;}.searchbox-btn{border:0;background:none;}.searchbox-input{border:0;} \ No newline at end of file diff --git a/Launcher/UI/Components/Searchbox.razor.scss b/Launcher/UI/Components/Searchbox.razor.scss new file mode 100644 index 0000000..4dd8607 --- /dev/null +++ b/Launcher/UI/Components/Searchbox.razor.scss @@ -0,0 +1,14 @@ +.searchbox { + border: 1px solid black; + width: 40%; + justify-self: end; +} + +.searchbox-btn { + border: none; + background: none; +} + +.searchbox-input { + border: none; +} \ No newline at end of file diff --git a/Launcher/UI/Home.razor b/Launcher/UI/Home.razor index 35fb873..0eae1e4 100644 --- a/Launcher/UI/Home.razor +++ b/Launcher/UI/Home.razor @@ -1,10 +1,5 @@ @page "/" -@using Launcher.UI.Components -@using Launcher.UI.Components.Dialogs -@using System.Diagnostics - - @@ -17,15 +12,30 @@
+
- - - @context.Name + + + + +

@context.Name

+

@context.Path

+ @context.LastModified - + -
+
-
\ No newline at end of file +
+ + + @{ + var project = (Project)context; + + + + + } + \ No newline at end of file diff --git a/Launcher/UI/Home.razor.cs b/Launcher/UI/Home.razor.cs index 71c52fb..73ff595 100644 --- a/Launcher/UI/Home.razor.cs +++ b/Launcher/UI/Home.razor.cs @@ -1,7 +1,11 @@ using Launcher.Services; +using Launcher.UI.Components; using Microsoft.AspNetCore.Components; +using SharpEngine.Shared.Dto; +using System.Diagnostics; using System.Text.Json; -using System.Text.Json.Serialization; + +using FilterMode = SharpEngine.Shared.Enums.FilterMode; namespace Launcher.UI { @@ -17,12 +21,26 @@ public partial class Home : ComponentBase private bool _showConfirmDelete; private bool _showCreateProjectDialog; - private List Projects { get; set; } = []; + private ContextMenu? _contextMenu; + + private List Projects + { + get => _allProjects; + set + { + _allProjects = value; + _filteredProjects = value; + } + } + + private List _allProjects = []; + private List _filteredProjects = []; + private Project _selectedProject = new(); private const string _projectsFile = "projects.json"; private static readonly string[] _sharpProjectFilePickerExtension = [".sharpproject"]; -#if DEBUG +#if !DEBUG private readonly string _projectsFilePath = Path.Join(Path.GetTempPath(), _projectsFile); #else private readonly string _projectsFilePath = Path.Join(AppContext.BaseDirectory, _projectsFile); @@ -35,14 +53,32 @@ protected override async Task OnInitializedAsync() // TODO: Get current launcher version // TODO: Check if GitHub has a new version released. +#if DEBUG + if (TestData.TestData.SimulateLoading) + { + _notificationService.Show("Simulating loading projects."); + await Task.Delay(10000); + } + + if (TestData.TestData.LoadTestData) + { + _notificationService.Show("Using test data."); + Projects = [.. TestData.TestData.Projects]; + return; + } +#endif + var projects = _editorService.LoadProjects(_projectsFilePath); - + if (projects is not null) Projects.AddRange(projects); else _notificationService.Show($"Unable to load project file.", false, projects); } + private void SearchProjects(string searchValue) + => _filteredProjects = [.. Projects.Where(p => p.Name!.Contains(searchValue, StringComparison.OrdinalIgnoreCase))]; + private void OnCreateProjectClicked() => _showCreateProjectDialog = true; @@ -113,5 +149,26 @@ private void SetProjects(Project project) string json = JsonSerializer.Serialize(Projects); File.WriteAllText(_projectsFilePath, json); } + + private static void OpenInExplorer(string projectPath) + => Process.Start("explorer.exe", projectPath); + + private void RemoveFromList(Project project) + { + var foundProject = Projects.Find(p => p.Id == project.Id); + if (foundProject is null) + { + _notificationService.Show($"Unable to remove project '{project.Name}', project not found.", false, project); + return; + } + + Projects.Remove(foundProject); + } + + private void OnFiltersChanged(Dictionary filters) + { + _filteredProjects = [.. Projects.FilterBy(filters)]; + StateHasChanged(); + } } } diff --git a/Launcher/UI/Home.razor.css b/Launcher/UI/Home.razor.css index ffb5a06..dfc3ab4 100644 --- a/Launcher/UI/Home.razor.css +++ b/Launcher/UI/Home.razor.css @@ -34,7 +34,7 @@ display: flex; justify-content: space-between; align-items: center; - margin: 25px; + margin: 25px 0; } .alert-header { diff --git a/Launcher/UI/Home.razor.min.css b/Launcher/UI/Home.razor.min.css index 384ee6f..cb60f75 100644 --- a/Launcher/UI/Home.razor.min.css +++ b/Launcher/UI/Home.razor.min.css @@ -1 +1 @@ -.toast{right:0;transition:transform .5s ease-in-out;}.dialog{left:0;right:0;margin-left:auto;margin-right:auto;width:auto;max-width:90%;}.toast,.dialog{position:absolute;top:50px;}.notification-visible{transform:translateX(0);}.notification-hidden{transform:translateX(100%);}.create-project-dialog-hidden,.confirm-delete-hidden{display:none;}.header{display:flex;justify-content:space-between;align-items:center;margin:25px;}.alert-header{display:flex;justify-content:space-between;}.alert-header h4{padding:0;}.flex-end{justify-content:end;display:flex;gap:8px;}.custom-file-upload{display:inline-block;padding:8px 12px;background-color:#007bff;color:#fff;border-radius:4px;cursor:pointer;}.hidden-input{position:absolute;width:0;height:0;opacity:0;} \ No newline at end of file +.toast{right:0;transition:transform .5s ease-in-out;}.dialog{left:0;right:0;margin-left:auto;margin-right:auto;width:auto;max-width:90%;}.toast,.dialog{position:absolute;top:50px;}.notification-visible{transform:translateX(0);}.notification-hidden{transform:translateX(100%);}.create-project-dialog-hidden,.confirm-delete-hidden{display:none;}.header{display:flex;justify-content:space-between;align-items:center;margin:25px 0;}.alert-header{display:flex;justify-content:space-between;}.alert-header h4{padding:0;}.flex-end{justify-content:end;display:flex;gap:8px;}.custom-file-upload{display:inline-block;padding:8px 12px;background-color:#007bff;color:#fff;border-radius:4px;cursor:pointer;}.hidden-input{position:absolute;width:0;height:0;opacity:0;} \ No newline at end of file diff --git a/Launcher/UI/Home.razor.scss b/Launcher/UI/Home.razor.scss index be215ee..bbdf065 100644 --- a/Launcher/UI/Home.razor.scss +++ b/Launcher/UI/Home.razor.scss @@ -34,7 +34,7 @@ display: flex; justify-content: space-between; align-items: center; - margin: 25px; + margin: 25px 0 ; } .alert-header { diff --git a/Launcher/UI/_Imports.razor b/Launcher/UI/_Imports.razor index 464f868..ea37d20 100644 --- a/Launcher/UI/_Imports.razor +++ b/Launcher/UI/_Imports.razor @@ -1,5 +1,6 @@ @using System.Net.Http @using System.Net.Http.Json +@using System.Diagnostics @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @@ -8,5 +9,17 @@ @using Microsoft.JSInterop @using Launcher +@using Launcher.Services +@using Launcher.Enums + @using Launcher.UI -@using Launcher.Services \ No newline at end of file +@using Launcher.UI.Components +@using Launcher.UI.Components.Grid +@using Launcher.UI.Components.Notification +@using Launcher.UI.Components.Icons +@using Launcher.UI.Components.Dialogs +@using Launcher.UI.Components.Flyouts +@using Launcher.UI.Components.Navigation + +@using SharpEngine.Shared.Dto +@using SharpEngine.Shared.Enums \ No newline at end of file diff --git a/Launcher/compilerconfig.json b/Launcher/compilerconfig.json index 9fe060d..fa73a0f 100644 --- a/Launcher/compilerconfig.json +++ b/Launcher/compilerconfig.json @@ -7,10 +7,6 @@ "outputFile": "UI/Home.razor.css", "inputFile": "UI/Home.razor.scss" }, - { - "outputFile": "UI/Components/DiscardIcon.razor.css", - "inputFile": "UI/Components/DiscardIcon.razor.scss" - }, { "outputFile": "wwwroot/css/additional.razor.css", "inputFile": "wwwroot/css/additional.razor.scss" @@ -22,5 +18,45 @@ { "outputFile": "UI/Components/Dialogs/ConfirmDeleteDialog.razor.css", "inputFile": "UI/Components/Dialogs/ConfirmDeleteDialog.razor.scss" + }, + { + "outputFile": "UI/Components/Navigation/SideBarItem.razor.css", + "inputFile": "UI/Components/Navigation/SideBarItem.razor.scss" + }, + { + "outputFile": "UI/Components/Flyouts/Flyout.razor.css", + "inputFile": "UI/Components/Flyouts/Flyout.razor.scss" + }, + { + "outputFile": "UI/Components/Grid.razor.css", + "inputFile": "UI/Components/Grid.razor.scss" + }, + { + "outputFile": "UI/Components/Grid/GridHeader.razor.css", + "inputFile": "UI/Components/Grid/GridHeader.razor.scss" + }, + { + "outputFile": "UI/Components/Dialogs/CreateProjectDialog.razor.css", + "inputFile": "UI/Components/Dialogs/CreateProjectDialog.razor.scss" + }, + { + "outputFile": "UI/Components/Navigation/Sidebar.razor.css", + "inputFile": "UI/Components/Navigation/Sidebar.razor.scss" + }, + { + "outputFile": "UI/Components/Notification/Notification.razor.css", + "inputFile": "UI/Components/Notification/Notification.razor.scss" + }, + { + "outputFile": "UI/Components/ContextMenu.razor.css", + "inputFile": "UI/Components/ContextMenu.razor.scss" + }, + { + "outputFile": "UI/Components/Layout.razor.css", + "inputFile": "UI/Components/Layout.razor.scss" + }, + { + "outputFile": "UI/Components/Searchbox.razor.css", + "inputFile": "UI/Components/Searchbox.razor.scss" } ] \ No newline at end of file diff --git a/Launcher/wwwroot/css/app.css b/Launcher/wwwroot/css/app.css index aad5a54..2196041 100644 --- a/Launcher/wwwroot/css/app.css +++ b/Launcher/wwwroot/css/app.css @@ -100,5 +100,11 @@ p, h1, h4, a, td, tr, div, .prevent-select { .page { height: 100%; - display: flow-root; + display: grid; + grid-template-rows: 1fr; + grid-template-columns: 50px auto; +} + +p { + margin: 0; } \ No newline at end of file diff --git a/SharpEngine.Shared/Attributes/FilterAttribute.cs b/SharpEngine.Shared/Attributes/FilterAttribute.cs new file mode 100644 index 0000000..f37af87 --- /dev/null +++ b/SharpEngine.Shared/Attributes/FilterAttribute.cs @@ -0,0 +1,20 @@ +namespace SharpEngine.Shared.Attributes; + +/// +/// Represents a attribute that indicates if a property is filterable. +/// +[AttributeUsage(AttributeTargets.Property)] +public class FilterAttribute : Attribute +{ + /// Gets or sets whether the property is filterable in collections. + public bool IsFilterable { get; set; } + + /// + /// Initializes a new instance of . + /// + /// Determines whether the target property is filterable. + public FilterAttribute(bool isFilterable = true) + { + IsFilterable = isFilterable; + } +} diff --git a/SharpEngine.Shared/Project.cs b/SharpEngine.Shared/Dto/Project.cs similarity index 85% rename from SharpEngine.Shared/Project.cs rename to SharpEngine.Shared/Dto/Project.cs index 5cac973..cf6b427 100644 --- a/SharpEngine.Shared/Project.cs +++ b/SharpEngine.Shared/Dto/Project.cs @@ -1,8 +1,10 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Text.Json; using System.Text.Json.Serialization; +using SharpEngine.Shared.Attributes; -namespace Launcher.UI; +namespace SharpEngine.Shared.Dto; /// /// Represents a SharpEngine project. @@ -30,6 +32,8 @@ public class Project /// Gets or sets when the project was last modified. /// [JsonIgnore] + [DisplayName("Last Modified")] + [Filter(false)] public DateTime LastModified { get; set; } = DateTime.Now; /// diff --git a/SharpEngine.Shared/Dto/UserProfile.cs b/SharpEngine.Shared/Dto/UserProfile.cs new file mode 100644 index 0000000..6235570 --- /dev/null +++ b/SharpEngine.Shared/Dto/UserProfile.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SharpEngine.Shared.Dto; + +public class UserProfile +{ + public string Username { get; set; } = default!; + public string Email { get; set; } = default!; + public string Password + { + get => _password; + set => SetPassword(value); + } + + private string _password; + + public List LinkedSocialMedia { get; set; } = []; + + public List Projects { get; set; } = []; + + public List Achievements { get; set; } = []; + + private void SetPassword(string pwd) + { + // TODO: Figure out a good way to handle encryption + _password = pwd; + } +} + +public class SocialMedia +{ + public string Name { get; set; } + public string Url { get; set; } +} + +public class Achievement +{ + public string Name { get; set; } + public string Description { get; set; } + public DateTime? DateAchieved { get; set; } + + public bool IsAchieved => DateAchieved.HasValue; +} diff --git a/SharpEngine.Shared/Enums/FilterMode.cs b/SharpEngine.Shared/Enums/FilterMode.cs new file mode 100644 index 0000000..71d1da1 --- /dev/null +++ b/SharpEngine.Shared/Enums/FilterMode.cs @@ -0,0 +1,16 @@ +namespace SharpEngine.Shared.Enums; + +/// +/// Contains the possible ways to filter a collection. +/// +public enum FilterMode +{ + /// No filter applied. + None, + + /// Filter in ascending order. + Ascending, + + /// Filter in descending order. + Descending +} diff --git a/SharpEngine.Shared/Extensions/OrderedEnumerableExtensions.cs b/SharpEngine.Shared/Extensions/OrderedEnumerableExtensions.cs new file mode 100644 index 0000000..d6f93d6 --- /dev/null +++ b/SharpEngine.Shared/Extensions/OrderedEnumerableExtensions.cs @@ -0,0 +1,39 @@ +using Launcher.UI; +using SharpEngine.Shared.Dto; +using SharpEngine.Shared.Enums; +using SharpEngine.Shared.Extensions; + +/// +/// Handles ordering collections. +/// +public static class OrderedEnumerableExtensions +{ + /// + /// Filters and orders a collection of projects based on specified criteria. + /// + /// The collection of projects to be filtered and ordered. + /// The names of properties and their corresponding filter modes. + /// A filtered collection of projects based on the applied filters. + public static IEnumerable FilterBy(this IEnumerable toFilter, Dictionary filters) + { + var stringComparer = new NaturalStringComparer(); + IOrderedEnumerable? ordered = null; + + foreach (var filter in filters) + { + string keySelector(Project p) => p.GetPropertyValue(filter.Key)?.ToString() ?? string.Empty; + + if (ordered == null) + ordered = filter.Value == FilterMode.Ascending + ? toFilter.OrderBy(keySelector, stringComparer) + : toFilter.OrderByDescending(keySelector, stringComparer); + + else + ordered = filter.Value == FilterMode.Ascending + ? ordered.ThenBy(keySelector, stringComparer) + : ordered.ThenByDescending(keySelector, stringComparer); + } + + return ordered ?? toFilter; + } +} diff --git a/SharpEngine.Shared/ProcessExtensions.cs b/SharpEngine.Shared/Extensions/ProcessExtensions.cs similarity index 97% rename from SharpEngine.Shared/ProcessExtensions.cs rename to SharpEngine.Shared/Extensions/ProcessExtensions.cs index 00cb0bc..f8d2a36 100644 --- a/SharpEngine.Shared/ProcessExtensions.cs +++ b/SharpEngine.Shared/Extensions/ProcessExtensions.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -namespace SharpEngine.Shared; +namespace SharpEngine.Shared.Extensions; /// /// Contains extensions for handling processes. @@ -68,7 +68,7 @@ void SendMessage(string? msg) } }); - Thread thread = new Thread(start); + var thread = new Thread(start); thread.Start(); SendMessage("Editor launched."); diff --git a/SharpEngine.Shared/Extensions/ReflectionExtensions.cs b/SharpEngine.Shared/Extensions/ReflectionExtensions.cs new file mode 100644 index 0000000..a663bf7 --- /dev/null +++ b/SharpEngine.Shared/Extensions/ReflectionExtensions.cs @@ -0,0 +1,23 @@ +namespace SharpEngine.Shared.Extensions; + +/// +/// Contains extensions for reflection. +/// +public static class ReflectionExtensions +{ + /// + /// Gets the value of a property from the given . + /// + /// The type of the object. + /// The object whose property value is being retrieved. + /// The name of the property whose values is being retrieved. + /// The value of the property, if not found. + public static object? GetPropertyValue(this T obj, string propertyName) + { + var property = typeof(T).GetProperty(propertyName); + if (property == null) + return null; + + return property.GetValue(obj); + } +} diff --git a/SharpEngine.Shared/NaturalStringComparer.cs b/SharpEngine.Shared/NaturalStringComparer.cs new file mode 100644 index 0000000..50b10b7 --- /dev/null +++ b/SharpEngine.Shared/NaturalStringComparer.cs @@ -0,0 +1,73 @@ +using System.Globalization; + +namespace Launcher.UI; + +/// +/// Checks if two strings are equal in a natural way. +/// +public class NaturalStringComparer : IComparer +{ + /// + public int Compare(string? s1, string? s2) + { + if (s1 == s2) + return 0; + if (s1 == null) + return -1; + if (s2 == null) + return 1; + + int i = 0, j = 0; + int len1 = s1.Length, len2 = s2.Length; + + while (i < len1 && j < len2) + { + int result = CompareCharacters(s1, s2, ref i, ref j); + if (result != 0) + return result; + } + + // If one string has remaining characters, it is greater + return (len1 - i).CompareTo(len2 - j); + } + + private static int CompareCharacters(string s1, string s2, ref int i, ref int j) + { + char c1 = s1[i]; + char c2 = s2[j]; + + // If both characters are digits, compare as numbers + if (char.IsDigit(c1) && char.IsDigit(c2)) + { + int start1 = i; + while (i < s1.Length && char.IsDigit(s1[i])) + i++; + + int start2 = j; + while (j < s2.Length && char.IsDigit(s2[j])) + j++; + + string numStr1 = s1.Substring(start1, i - start1); + string numStr2 = s2.Substring(start2, j - start2); + + // Parse numbers ignoring leading zeros + int num1 = int.Parse(numStr1, CultureInfo.InvariantCulture); + int num2 = int.Parse(numStr2, CultureInfo.InvariantCulture); + + if (num1 != num2) + return num1.CompareTo(num2); + + return 0; + } + + // Compare characters case-insensitively + int result = char.ToUpperInvariant(c1).CompareTo(char.ToUpperInvariant(c2)); + if (result != 0) + return result; + + i++; + j++; + + return 0; + } +}