feat: Dear ImGui editor, GLTF materials, scene serialization

- ImGuiLayer: entity inspector (Transform/Material/Light/Camera editing),
  hierarchy panel, debug overlay with FPS graph (rlImgui-cs + ImGui.NET)
- GltfLoader.LoadWithMaterials: extracts PBR albedo, roughness, metallic,
  base color texture from glTF files
- SceneSerializer: save/load ECS world to/from JSON with custom
  Vector3/Quaternion converters, 6 tests
- ImGui disabled in tour/test mode to keep screenshots clean
- 66/66 tests passing
This commit is contained in:
emil28092005
2026-06-17 14:12:02 +03:00
parent fb6e26a268
commit 2d30dec01c
9 changed files with 903 additions and 39 deletions
+121
View File
@@ -0,0 +1,121 @@
using System.IO;
using System.Numerics;
using Engine.Core.Components;
using Engine.Graphics;
using Flecs.NET.Core;
namespace Engine.Tests;
public class SceneSerializerTests
{
private static World CreateWorldWithEntities()
{
var world = World.Create();
world.Entity("CubeA")
.Set(new Transform(new Vector3(1, 2, 3), Quaternion.Identity, Vector3.One))
.Set(new Material(new Vector3(0.9f, 0.2f, 0.2f), 0.4f, 0.1f));
world.Entity("CubeB")
.Set(new Transform(new Vector3(-1, 0, 5), Quaternion.Identity, new Vector3(2, 2, 2)))
.Set(new Material(new Vector3(0.2f, 0.8f, 0.3f), 0.7f, 0.0f));
return world;
}
[Fact]
public void SaveToString_Produces_NonEmpty_Json()
{
using var world = CreateWorldWithEntities();
var json = SceneSerializer.SaveToString(world);
Assert.False(string.IsNullOrEmpty(json));
Assert.Contains("CubeA", json);
Assert.Contains("CubeB", json);
}
[Fact]
public void SaveToFile_Creates_File()
{
using var world = CreateWorldWithEntities();
var path = Path.Combine(Path.GetTempPath(), $"scene_{Guid.NewGuid():N}.json");
try
{
SceneSerializer.SaveToFile(world, path);
Assert.True(File.Exists(path));
var content = File.ReadAllText(path);
Assert.Contains("CubeA", content);
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}
[Fact]
public void LoadFromString_Adds_Entities()
{
using var sourceWorld = CreateWorldWithEntities();
var json = SceneSerializer.SaveToString(sourceWorld);
using var targetWorld = World.Create();
var loaded = SceneSerializer.LoadFromString(targetWorld, json);
Assert.True(loaded > 0);
var entity = targetWorld.Lookup("CubeA");
Assert.True((ulong)entity.Id != 0);
}
[Fact]
public void LoadFromFile_Restores_Entities()
{
using var sourceWorld = CreateWorldWithEntities();
var path = Path.Combine(Path.GetTempPath(), $"scene_{Guid.NewGuid():N}.json");
try
{
SceneSerializer.SaveToFile(sourceWorld, path);
using var targetWorld = World.Create();
SceneSerializer.LoadFromFile(targetWorld, path);
var entity = targetWorld.Lookup("CubeB");
Assert.True((ulong)entity.Id != 0);
}
finally
{
if (File.Exists(path)) File.Delete(path);
}
}
[Fact]
public void LoadFromFile_Throws_For_Missing_File()
{
using var world = World.Create();
Assert.Throws<FileNotFoundException>(() =>
SceneSerializer.LoadFromFile(world, "/nonexistent/scene.json"));
}
[Fact]
public void Roundtrip_Preserves_Transform_Position()
{
using var sourceWorld = World.Create();
sourceWorld.Entity("TestEntity")
.Set(new Transform(new Vector3(5, 10, 15), Quaternion.Identity, Vector3.One));
var json = SceneSerializer.SaveToString(sourceWorld);
using var targetWorld = World.Create();
SceneSerializer.LoadFromString(targetWorld, json);
var entity = targetWorld.Lookup("TestEntity");
Assert.True((ulong)entity.Id != 0);
var transform = entity.Get<Transform>();
Assert.Equal(5f, transform.Position.X, 0.001f);
Assert.Equal(10f, transform.Position.Y, 0.001f);
Assert.Equal(15f, transform.Position.Z, 0.001f);
}
}