using System.Numerics; using ImGuiNET; using NResUI.Abstractions; using NResUI.Models; using NResUI.Rendering.Import.Msh; using NResUI.Rendering.Materials; using NResUI.Rendering.Viewport; using NativeFileDialogSharp; 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(); private readonly IConfigProvider _configProvider; private readonly MeshViewportViewModel _viewModel; private ViewportMaterialSet _materialSet = ViewportMaterialSet.Empty; public ViewportPanel(GL gl, IWindow window, IConfigProvider configProvider, MeshViewportViewModel viewModel) { _configProvider = configProvider; _viewModel = viewModel; 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; } DrawModelControls(); DrawSelectionStatus(); DrawViewportToolbar(); DrawDebugControls(); RebuildSceneIfNeeded(); SyncSelectionFromViewModel(); 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(); var viewportFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows); var imageMin = ImGui.GetItemRectMin(); DrawViewportHelpOverlay(); _inputController.Handle( _scene, _camera, viewportHovered, viewportFocused, imageMin, imageSize); if (_viewModel.RenderState.SelectedPieceId != _scene.SelectedPieceId) _viewModel.RenderState.SelectedPieceId = _scene.SelectedPieceId; ImGui.End(); } 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); _scene.ReplacePieces([ViewportPiece.CreateUnitCube(0, "Cube", cubeMesh)]); _viewModel.ClearDocument(); _camera.Reset(); } if (_viewModel.Document != null) ImGui.TextDisabled($"Model: {Path.GetFileName(_viewModel.Document.SourcePath)}"); if (_viewModel.LoadError != null) ImGui.TextColored(new Vector4(1.0f, 0.35f, 0.25f, 1.0f), $"MSH load failed: {_viewModel.LoadError}"); } private void LoadMsh(string path) { // Сначала строим CPU-документ для inspector, затем отдельно резолвим материалы и OpenGL textures. var loadResult = MshMeshDocumentImporter.LoadFromFile(path); if (!loadResult.IsSuccess) { _viewModel.SetError(loadResult.Error ?? "Unknown error."); return; } _viewModel.SetDocument(loadResult.Document!); _materialSet = ViewportMaterialResolver.LoadForMsh( _renderer.Gl, path, _configProvider, loadResult.Document!.Warnings is ICollection mutableWarnings ? mutableWarnings : null); RebuildSceneIfNeeded(); if (_scene.TryGetSceneWorldBounds(out var bounds)) _camera.FrameBounds(bounds.Min, bounds.Max); else _camera.Reset(); } private void DrawSelectionStatus() { var selectedPiece = _scene.SelectedPiece; if (selectedPiece != null) { ImGui.Text($"Selected: {selectedPiece.Name}"); 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}"); } } else { ImGui.TextDisabled("Selected: none"); } } 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; } 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() { var grid = _scene.Grid; 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(); var wireframe = _scene.Debug.Wireframe; if (ImGui.Checkbox("Wireframe", ref wireframe)) _scene.Debug.Wireframe = wireframe; } private static void DrawViewportHelpOverlay() { var drawList = ImGui.GetWindowDrawList(); var imageMin = ImGui.GetItemRectMin(); var imageMax = ImGui.GetItemRectMax(); 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"; 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); } }