Files
parkan-playground/NResUI/ImGuiUI/ViewportPanel.cs
T

300 lines
9.2 KiB
C#
Raw Normal View History

2026-06-05 20:33:21 +03:00
using System.Numerics;
using ImGuiNET;
using NResUI.Abstractions;
2026-06-09 01:46:57 +03:00
using NResUI.Models;
using NResUI.Rendering.Import.Msh;
using NResUI.Rendering.Materials;
2026-06-05 20:33:21 +03:00
using NResUI.Rendering.Viewport;
2026-06-05 21:12:25 +03:00
using NativeFileDialogSharp;
2026-06-05 20:33:21 +03:00
using NResUI.Rendering.Viewport.Meshes;
using Silk.NET.OpenGL;
using Silk.NET.Windowing;
namespace NResUI.ImGuiUI;
public sealed class ViewportPanel : IImGuiPanel
{
private readonly ViewportRenderer _renderer;
private readonly ViewportScene _scene;
private readonly ViewportCamera _camera = new();
private readonly ViewportInputController _inputController = new();
2026-06-07 00:41:16 +03:00
private readonly IConfigProvider _configProvider;
2026-06-09 01:46:57 +03:00
private readonly MeshViewportViewModel _viewModel;
2026-06-05 20:33:21 +03:00
2026-06-09 01:46:57 +03:00
private ViewportMaterialSet _materialSet = ViewportMaterialSet.Empty;
2026-06-05 21:12:25 +03:00
2026-06-09 01:46:57 +03:00
public ViewportPanel(GL gl, IWindow window, IConfigProvider configProvider, MeshViewportViewModel viewModel)
2026-06-05 20:33:21 +03:00
{
2026-06-07 00:41:16 +03:00
_configProvider = configProvider;
2026-06-09 01:46:57 +03:00
_viewModel = viewModel;
2026-06-05 20:33:21 +03:00
var cubeMesh = PrimitiveMeshes.CreateCube(gl);
var gridMesh = PrimitiveMeshes.CreateWorldGrid(gl);
_scene = ViewportScene.CreateDefaultCubeScene(cubeMesh, gridMesh);
_renderer = new ViewportRenderer(gl, window);
}
public void OnImGuiRender()
{
if (!ImGui.Begin("Viewport"))
{
ImGui.End();
return;
}
2026-06-05 21:12:25 +03:00
DrawModelControls();
2026-06-05 20:33:21 +03:00
DrawSelectionStatus();
2026-06-05 20:47:57 +03:00
DrawViewportToolbar();
DrawDebugControls();
2026-06-09 01:46:57 +03:00
RebuildSceneIfNeeded();
SyncSelectionFromViewModel();
2026-06-05 20:33:21 +03:00
var imageSize = ImGui.GetContentRegionAvail();
if (imageSize.X < 32 || imageSize.Y < 32)
{
ImGui.TextDisabled("Viewport is too small.");
ImGui.End();
return;
}
var textureId = _renderer.Render(
width: Math.Max(1, (int)imageSize.X),
height: Math.Max(1, (int)imageSize.Y),
scene: _scene,
camera: _camera);
ImGui.Image(
(IntPtr)textureId,
imageSize,
new Vector2(0, 1),
new Vector2(1, 0));
var viewportHovered = ImGui.IsItemHovered();
2026-06-05 20:47:57 +03:00
var viewportFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
2026-06-05 20:33:21 +03:00
var imageMin = ImGui.GetItemRectMin();
DrawViewportHelpOverlay();
_inputController.Handle(
2026-06-10 02:01:41 +03:00
_renderer,
2026-06-05 20:33:21 +03:00
_scene,
_camera,
viewportHovered,
2026-06-05 20:47:57 +03:00
viewportFocused,
2026-06-05 20:33:21 +03:00
imageMin,
imageSize);
2026-06-09 01:46:57 +03:00
if (_viewModel.RenderState.SelectedPieceId != _scene.SelectedPieceId)
_viewModel.RenderState.SelectedPieceId = _scene.SelectedPieceId;
2026-06-05 20:33:21 +03:00
ImGui.End();
}
2026-06-05 21:12:25 +03:00
private void DrawModelControls()
{
if (ImGui.Button("Open MSH in viewport"))
{
var result = Dialog.FileOpen("msh");
if (result.IsOk)
LoadMsh(result.Path);
}
ImGui.SameLine();
if (ImGui.Button("Reset cube"))
{
var cubeMesh = PrimitiveMeshes.CreateCube(_renderer.Gl);
2026-06-07 00:41:16 +03:00
_scene.ReplacePieces([ViewportPiece.CreateUnitCube(0, "Cube", cubeMesh)]);
2026-06-09 01:46:57 +03:00
_viewModel.ClearDocument();
2026-06-05 21:12:25 +03:00
_camera.Reset();
}
2026-06-09 01:46:57 +03:00
if (_viewModel.Document != null)
ImGui.TextDisabled($"Model: {Path.GetFileName(_viewModel.Document.SourcePath)}");
2026-06-05 21:12:25 +03:00
2026-06-09 01:46:57 +03:00
if (_viewModel.LoadError != null)
ImGui.TextColored(new Vector4(1.0f, 0.35f, 0.25f, 1.0f), $"MSH load failed: {_viewModel.LoadError}");
2026-06-05 21:12:25 +03:00
}
private void LoadMsh(string path)
{
2026-06-09 01:46:57 +03:00
// Сначала строим CPU-документ для inspector, затем отдельно резолвим материалы и OpenGL textures.
var loadResult = MshMeshDocumentImporter.LoadFromFile(path);
2026-06-05 21:12:25 +03:00
if (!loadResult.IsSuccess)
{
2026-06-09 01:46:57 +03:00
_viewModel.SetError(loadResult.Error ?? "Unknown error.");
2026-06-05 21:12:25 +03:00
return;
}
2026-06-09 01:46:57 +03:00
_viewModel.SetDocument(loadResult.Document!);
_materialSet = ViewportMaterialResolver.LoadForMsh(
_renderer.Gl,
path,
_configProvider,
loadResult.Document!.Warnings is ICollection<string> mutableWarnings ? mutableWarnings : null);
RebuildSceneIfNeeded();
2026-06-05 21:12:25 +03:00
if (_scene.TryGetSceneWorldBounds(out var bounds))
_camera.FrameBounds(bounds.Min, bounds.Max);
else
_camera.Reset();
}
2026-06-05 20:33:21 +03:00
private void DrawSelectionStatus()
{
var selectedPiece = _scene.SelectedPiece;
if (selectedPiece != null)
2026-06-05 21:12:25 +03:00
{
2026-06-05 20:33:21 +03:00
ImGui.Text($"Selected: {selectedPiece.Name}");
2026-06-05 21:12:25 +03:00
var debugInfo = selectedPiece.DebugInfo;
if (debugInfo != null)
{
ImGui.TextDisabled(
$"{debugInfo.SourceKind} | parent {debugInfo.SourceParentIndex} | slot {debugInfo.GeometrySlotIndex} | " +
$"flags 0x{debugInfo.Msh01Flags:X4} | batches {debugInfo.BatchCount} | tris {debugInfo.TriangleCount}");
}
}
2026-06-05 20:33:21 +03:00
else
2026-06-05 21:12:25 +03:00
{
2026-06-05 20:33:21 +03:00
ImGui.TextDisabled("Selected: none");
2026-06-05 21:12:25 +03:00
}
2026-06-05 20:33:21 +03:00
}
2026-06-09 01:46:57 +03:00
private void RebuildSceneIfNeeded()
{
if (!_viewModel.NeedsSceneRebuild)
return;
var document = _viewModel.Document;
if (document == null)
{
_viewModel.MarkSceneRebuilt();
return;
}
// Перестройка нужна при смене state/LOD, hide/isolate и batch filter: все это меняет состав GPU-мешей.
var pieces = MshViewportMeshFactory.BuildViewportPieces(
_renderer.Gl,
document,
_viewModel.RenderState,
_materialSet);
_scene.ReplacePieces(pieces);
_scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId;
_viewModel.MarkSceneRebuilt();
}
private void SyncSelectionFromViewModel()
{
if (_viewModel.Document == null)
return;
// Inspector может выбрать piece без клика по viewport; renderer должен подсветить тот же id.
_scene.SelectedPieceId = _viewModel.RenderState.SelectedPieceId;
}
2026-06-05 20:47:57 +03:00
private void DrawViewportToolbar()
{
if (ImGui.Button("Reset view"))
_camera.Reset();
ImGui.SameLine();
if (ImGui.Button("Frame selected"))
{
if (_scene.TryGetSelectedPieceWorldBounds(out var selectedBounds))
_camera.FrameBounds(selectedBounds.Min, selectedBounds.Max);
}
ImGui.SameLine();
if (ImGui.Button("Frame all"))
{
if (_scene.TryGetSceneWorldBounds(out var sceneBounds))
_camera.FrameBounds(sceneBounds.Min, sceneBounds.Max);
else
_camera.Reset();
}
}
private void DrawDebugControls()
2026-06-05 20:33:21 +03:00
{
var grid = _scene.Grid;
2026-06-05 20:47:57 +03:00
if (grid != null)
{
var isVisible = grid.IsVisible;
if (ImGui.Checkbox("Grid", ref isVisible))
grid.IsVisible = isVisible;
}
ImGui.SameLine();
var showOriginAxes = _scene.Debug.ShowOriginAxes;
if (ImGui.Checkbox("Origin axes", ref showOriginAxes))
_scene.Debug.ShowOriginAxes = showOriginAxes;
ImGui.SameLine();
var showPieceOrigins = _scene.Debug.ShowPieceOrigins;
if (ImGui.Checkbox("Piece axes", ref showPieceOrigins))
_scene.Debug.ShowPieceOrigins = showPieceOrigins;
ImGui.SameLine();
var showSelectedBounds = _scene.Debug.ShowSelectedBounds;
if (ImGui.Checkbox("Selected bounds", ref showSelectedBounds))
_scene.Debug.ShowSelectedBounds = showSelectedBounds;
ImGui.SameLine();
var showSceneBounds = _scene.Debug.ShowSceneBounds;
if (ImGui.Checkbox("Scene bounds", ref showSceneBounds))
_scene.Debug.ShowSceneBounds = showSceneBounds;
ImGui.SameLine();
2026-06-05 20:33:21 +03:00
2026-06-05 20:47:57 +03:00
var wireframe = _scene.Debug.Wireframe;
if (ImGui.Checkbox("Wireframe", ref wireframe))
_scene.Debug.Wireframe = wireframe;
2026-06-05 20:33:21 +03:00
}
private static void DrawViewportHelpOverlay()
{
var drawList = ImGui.GetWindowDrawList();
var imageMin = ImGui.GetItemRectMin();
var imageMax = ImGui.GetItemRectMax();
2026-06-05 20:47:57 +03:00
const string helpText =
"LMB click: select / deselect\n" +
"MMB drag: rotate\n" +
"RMB drag or Alt+MMB: pan\n" +
"Mouse wheel: zoom\n" +
"F: frame selected Home: frame all";
2026-06-05 20:33:21 +03:00
var textSize = ImGui.CalcTextSize(helpText);
var padding = new Vector2(8.0f, 6.0f);
var boxMin = new Vector2(
imageMin.X + 8.0f,
imageMax.Y - textSize.Y - padding.Y * 2.0f - 8.0f);
var boxMax = new Vector2(
boxMin.X + textSize.X + padding.X * 2.0f,
boxMin.Y + textSize.Y + padding.Y * 2.0f);
drawList.AddRectFilled(
boxMin,
boxMax,
ImGui.GetColorU32(new Vector4(0.0f, 0.0f, 0.0f, 0.45f)),
4.0f);
drawList.AddText(
boxMin + padding,
ImGui.GetColorU32(new Vector4(1.0f, 1.0f, 1.0f, 0.9f)),
helpText);
}
}