#include "ViewportPanel.h" #include "../utils/Logger.h" #include #include #include namespace UsdLayerManager { // --------------------------------------------------------------------------- // Construction // --------------------------------------------------------------------------- ViewportPanel::ViewportPanel() { EnsureTileCount(1); // start with a single tile } ViewportPanel::~ViewportPanel() = default; // --------------------------------------------------------------------------- // EnsureTileCount / WireCallbacks // --------------------------------------------------------------------------- void ViewportPanel::EnsureTileCount(int count) { // Grow while (static_cast(m_tiles.size()) < count) { int idx = static_cast(m_tiles.size()); m_tiles.push_back(std::make_unique()); if (m_stage) m_tiles.back()->SetStage(m_stage); if (m_iconManager) m_tiles.back()->SetIconManager(m_iconManager); if (m_commandHistory) m_tiles.back()->SetCommandHistory(m_commandHistory); m_tiles.back()->SetTimeCodes(m_displayTime, m_editTime); m_tiles.back()->SetSelectedPaths(m_selectedSdfPaths, m_selectedPrimPath); WireCallbacks(idx); } // Shrink while (static_cast(m_tiles.size()) > count) m_tiles.pop_back(); // Clamp indices m_focusedTileIndex = std::min(m_focusedTileIndex, std::max(0, static_cast(m_tiles.size()) - 1)); if (m_maximizedTileIndex >= static_cast(m_tiles.size())) m_maximizedTileIndex = -1; } void ViewportPanel::WireCallbacks(int i) { m_tiles[i]->OnPrimPicked = [this](const std::string& path) { m_selectedPrimPath = path; m_selectedSdfPaths.clear(); if (!path.empty()) m_selectedSdfPaths.push_back(pxr::SdfPath(path)); BroadcastSelection(); if (OnPrimPicked) OnPrimPicked(path); }; m_tiles[i]->OnPrimsPickedRect = [this](const std::vector& paths) { m_selectedSdfPaths.clear(); for (const auto& p : paths) m_selectedSdfPaths.push_back(pxr::SdfPath(p)); m_selectedPrimPath = m_selectedSdfPaths.empty() ? "" : m_selectedSdfPaths.front().GetString(); BroadcastSelection(); if (OnPrimsPickedRect) OnPrimsPickedRect(paths); }; } // --------------------------------------------------------------------------- // BroadcastSelection / UpdateFocusTile // --------------------------------------------------------------------------- void ViewportPanel::BroadcastSelection() { for (auto& t : m_tiles) t->SetSelectedPaths(m_selectedSdfPaths, m_selectedPrimPath); pxr::SdfPath primary = m_selectedSdfPaths.empty() ? pxr::SdfPath() : m_selectedSdfPaths.front(); m_manipulator.SetSelectedPrim(primary); } void ViewportPanel::UpdateFocusTile(int idx) { if (idx < 0 || idx >= static_cast(m_tiles.size())) return; m_focusedTileIndex = idx; } // --------------------------------------------------------------------------- // Public setup // --------------------------------------------------------------------------- void ViewportPanel::SetStage(pxr::UsdStageRefPtr stage) { m_stage = stage; m_manipulator.SetStage(stage); for (auto& t : m_tiles) t->SetStage(stage); m_selectedSdfPaths.clear(); m_selectedPrimPath.clear(); BroadcastSelection(); } void ViewportPanel::FrameScene() { for (auto& t : m_tiles) t->FrameScene(); } void ViewportPanel::SetCommandHistory(CommandHistory* history) { m_commandHistory = history; m_manipulator.SetCommandHistory(history); for (auto& t : m_tiles) t->SetCommandHistory(history); } void ViewportPanel::SetIconManager(IconManager* icons) { m_iconManager = icons; for (auto& t : m_tiles) t->SetIconManager(icons); } void ViewportPanel::SetTimeCodes(pxr::UsdTimeCode displayTime, pxr::UsdTimeCode editTime) { m_displayTime = displayTime; m_editTime = editTime; for (auto& t : m_tiles) t->SetTimeCodes(displayTime, editTime); m_manipulator.SetTimeCodes(displayTime, editTime); } void ViewportPanel::SetSelectedPrimPath(const std::string& path) { m_selectedPrimPath = path; m_selectedSdfPaths.clear(); if (!path.empty()) m_selectedSdfPaths.push_back(pxr::SdfPath(path)); BroadcastSelection(); } // --------------------------------------------------------------------------- // Forwarding accessors // --------------------------------------------------------------------------- ViewportCamera& ViewportPanel::GetCamera() { return m_tiles[static_cast(m_focusedTileIndex)]->GetCamera(); } UsdSceneRenderer& ViewportPanel::GetRenderer() { return m_tiles[static_cast(m_focusedTileIndex)]->GetRenderer(); } // --------------------------------------------------------------------------- // SetLayout // --------------------------------------------------------------------------- void ViewportPanel::SetLayout(LayoutMode mode) { m_layout = mode; m_maximizedTileIndex = -1; switch (mode) { case LayoutMode::Single: EnsureTileCount(1); break; case LayoutMode::HSplit: EnsureTileCount(2); break; case LayoutMode::VSplit: EnsureTileCount(2); break; case LayoutMode::Quad: EnsureTileCount(4); break; } } // --------------------------------------------------------------------------- // ComputeTileRects // --------------------------------------------------------------------------- std::vector ViewportPanel::ComputeTileRects(ImVec2 origin, ImVec2 total) const { std::vector rects; switch (m_layout) { case LayoutMode::Single: rects.push_back({ origin, total }); break; case LayoutMode::HSplit: { float leftW = total.x * m_splitH; float rightW = total.x - leftW; rects.push_back({ origin, ImVec2(leftW, total.y) }); rects.push_back({ ImVec2(origin.x + leftW, origin.y), ImVec2(rightW, total.y) }); break; } case LayoutMode::VSplit: { float topH = total.y * m_splitV; float bottomH = total.y - topH; rects.push_back({ origin, ImVec2(total.x, topH) }); rects.push_back({ ImVec2(origin.x, origin.y + topH), ImVec2(total.x, bottomH) }); break; } case LayoutMode::Quad: { float leftW = total.x * m_splitH; float rightW = total.x - leftW; float topH = total.y * m_splitV; float bottomH = total.y - topH; rects.push_back({ origin, ImVec2(leftW, topH) }); rects.push_back({ ImVec2(origin.x + leftW, origin.y), ImVec2(rightW, topH) }); rects.push_back({ ImVec2(origin.x, origin.y + topH), ImVec2(leftW, bottomH) }); rects.push_back({ ImVec2(origin.x + leftW, origin.y + topH), ImVec2(rightW, bottomH) }); break; } } // In multi-tile layouts inset every tile by 2px on all sides. // Adjacent tiles then have a 4px gap (2px inset from each side) so both // the focused border (2px) and the hovered border (1px) are fully visible. if (m_layout != LayoutMode::Single) { for (auto& r : rects) { // r.pos.x += 2.f; // r.pos.y += 2.f; // r.size.x -= 4.f; // r.size.y -= 4.f; r.pos.x += 2.f; r.pos.y += 2.f; r.size.x -= 2.f; r.size.y -= 2.f; } } return rects; } // --------------------------------------------------------------------------- bool ViewportPanel::IsMouseOverDivider(ImVec2 origin, ImVec2 total) const { if (m_layout == LayoutMode::Single) return false; if (m_maximizedTileIndex >= 0) return false; if (m_draggingDivH || m_draggingDivV) return true; const float kDivHalf = 3.0f; ImVec2 mouse = ImGui::GetMousePos(); if (m_layout == LayoutMode::HSplit || m_layout == LayoutMode::Quad) { float divX = origin.x + total.x * m_splitH; if (mouse.x >= divX - kDivHalf && mouse.x <= divX + kDivHalf && mouse.y >= origin.y && mouse.y <= origin.y + total.y) return true; } if (m_layout == LayoutMode::VSplit || m_layout == LayoutMode::Quad) { float divY = origin.y + total.y * m_splitV; if (mouse.y >= divY - kDivHalf && mouse.y <= divY + kDivHalf && mouse.x >= origin.x && mouse.x <= origin.x + total.x) return true; } return false; } // --------------------------------------------------------------------------- // DrawDividers // --------------------------------------------------------------------------- void ViewportPanel::DrawDividers(ImVec2 origin, ImVec2 total) { const float kDivThick = 6.0f; const float kDivVisual = 2.0f; const float kMinFrac = 0.1f; const float kMaxFrac = 0.9f; ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 mousePos = ImGui::GetMousePos(); // Use IsMouseClicked (edge-triggered) instead of IsMouseDown so that a // divider drag only starts on a fresh press. If LMB is already held // (e.g. the user is mid-rect-select in a tile) the divider is never // accidentally triggered when the mouse drifts over the hit-zone. bool lmbClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left); bool lmbReleased = ImGui::IsMouseReleased(ImGuiMouseButton_Left); // Vertical divider (HSplit / Quad) if (m_layout == LayoutMode::HSplit || m_layout == LayoutMode::Quad) { float divX = origin.x + total.x * m_splitH; ImVec2 hMin(divX - kDivThick * 0.5f, origin.y); ImVec2 hMax(divX + kDivThick * 0.5f, origin.y + total.y); bool hovering = !m_draggingDivV && mousePos.x >= hMin.x && mousePos.x <= hMax.x && mousePos.y >= hMin.y && mousePos.y <= hMax.y; if ((hovering || m_draggingDivH) && !m_draggingDivV) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeEW); if (hovering && lmbClicked && !m_draggingDivH && !m_draggingDivV) m_draggingDivH = true; if (m_draggingDivH) { float f = (mousePos.x - origin.x) / total.x; m_splitH = std::max(kMinFrac, std::min(kMaxFrac, f)); if (lmbReleased) m_draggingDivH = false; } // ImU32 col = (hovering || m_draggingDivH) ? IM_COL32(66,150,250,200) : IM_COL32(80,80,80,180); ImU32 col = (hovering || m_draggingDivH) ? IM_COL32(250,150,66,200) : IM_COL32(80,80,80,180); dl->AddLine(ImVec2(divX, origin.y), ImVec2(divX, origin.y + total.y), col, kDivVisual); } // Horizontal divider (VSplit / Quad) if (m_layout == LayoutMode::VSplit || m_layout == LayoutMode::Quad) { float divY = origin.y + total.y * m_splitV; ImVec2 hMin(origin.x, divY - kDivThick * 0.5f); ImVec2 hMax(origin.x + total.x, divY + kDivThick * 0.5f); bool hovering = !m_draggingDivH && mousePos.x >= hMin.x && mousePos.x <= hMax.x && mousePos.y >= hMin.y && mousePos.y <= hMax.y; if ((hovering || m_draggingDivV) && !m_draggingDivH) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); if (hovering && lmbClicked && !m_draggingDivH && !m_draggingDivV) m_draggingDivV = true; if (m_draggingDivV) { float f = (mousePos.y - origin.y) / total.y; m_splitV = std::max(kMinFrac, std::min(kMaxFrac, f)); if (lmbReleased) m_draggingDivV = false; } // ImU32 col = (hovering || m_draggingDivV) ? IM_COL32(66,150,250,200) : IM_COL32(80,80,80,180); ImU32 col = (hovering || m_draggingDivV) ? IM_COL32(250,150,66,200) : IM_COL32(80,80,80,180); dl->AddLine(ImVec2(origin.x, divY), ImVec2(origin.x + total.x, divY), col, kDivVisual); } } // --------------------------------------------------------------------------- // HandleMaximizeInput // --------------------------------------------------------------------------- void ViewportPanel::HandleMaximizeInput(int hoveredTileIndex) { ImGuiIO& io = ImGui::GetIO(); if (io.WantTextInput) return; if (ImGui::IsKeyPressed(ImGuiKey_Space)) { if (m_maximizedTileIndex >= 0) { m_maximizedTileIndex = -1; m_layout = m_layoutBeforeMaximize; m_splitH = m_splitHBefore; m_splitV = m_splitVBefore; switch (m_layout) { case LayoutMode::Single: EnsureTileCount(1); break; case LayoutMode::HSplit: EnsureTileCount(2); break; case LayoutMode::VSplit: EnsureTileCount(2); break; case LayoutMode::Quad: EnsureTileCount(4); break; } } else if (m_layout != LayoutMode::Single && hoveredTileIndex >= 0) { m_layoutBeforeMaximize = m_layout; m_splitHBefore = m_splitH; m_splitVBefore = m_splitV; m_maximizedTileIndex = hoveredTileIndex; UpdateFocusTile(hoveredTileIndex); } } if (m_maximizedTileIndex >= 0 && ImGui::IsKeyPressed(ImGuiKey_Escape)) { m_maximizedTileIndex = -1; m_layout = m_layoutBeforeMaximize; m_splitH = m_splitHBefore; m_splitV = m_splitVBefore; switch (m_layout) { case LayoutMode::Single: EnsureTileCount(1); break; case LayoutMode::HSplit: EnsureTileCount(2); break; case LayoutMode::VSplit: EnsureTileCount(2); break; case LayoutMode::Quad: EnsureTileCount(4); break; } } } // --------------------------------------------------------------------------- // RenderGlobalLeftToolbar // --------------------------------------------------------------------------- // Draws a single vertical icon-button toolbar on the left edge of the // viewport content area. Sections (top to bottom): // [1][H][V][4] — layout mode // ───────────── // [Q][W][E][R] — manipulator tool (global, like Maya) // ───────────── // [W|O] — transform space toggle // --------------------------------------------------------------------------- void ViewportPanel::RenderGlobalLeftToolbar(ImVec2 contentPos, ImVec2 /*contentSize*/) { const float kBtnSize = 32.0f; const float kIconPad = 5.0f; const float kRounding = 4.0f; const float kSpacing = 3.0f; const float kPadX = 9.0f; // left padding inside the strip const float kPadY = 10.0f; // top padding const float kSepH = 1.0f; // separator line height const float kSepGap = 6.0f; // space around separator const ImVec2 kBtnSz(kBtnSize, kBtnSize); ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 mouse = ImGui::GetMousePos(); bool lmbClk = ImGui::IsMouseClicked(ImGuiMouseButton_Left); // Current Y cursor float x = contentPos.x + kPadX; float y = contentPos.y + kPadY; // Helper: draw one square icon button, return true if clicked. // `active` tints the button blue. Falls back to centred text if no icon. auto DrawBtn = [&](const char* id, Icon iconEnum, const char* fallbackLabel, bool active, const char* tooltip) -> bool { ImVec2 bMin(x, y); ImVec2 bMax(x + kBtnSize, y + kBtnSize); bool hov = mouse.x >= bMin.x && mouse.x <= bMax.x && mouse.y >= bMin.y && mouse.y <= bMax.y; bool clicked = hov && lmbClk; ImU32 bg = active ? IM_COL32( 66, 150, 250, 230) : hov ? IM_COL32( 70, 70, 70, 220) : IM_COL32( 32, 32, 32, 178); dl->AddRectFilled(bMin, bMax, bg, kRounding); if (active) dl->AddRect(bMin, bMax, IM_COL32(100, 180, 255, 200), kRounding, 0, 1.5f); if (m_iconManager) { ImTextureID tex = m_iconManager->Get(iconEnum); dl->AddImage(ImTextureRef(tex), ImVec2(bMin.x + kIconPad, bMin.y + kIconPad), ImVec2(bMax.x - kIconPad, bMax.y - kIconPad)); } else { ImVec2 ts = ImGui::CalcTextSize(fallbackLabel); dl->AddText( ImVec2(bMin.x + (kBtnSize - ts.x) * 0.5f, bMin.y + (kBtnSize - ts.y) * 0.5f), IM_COL32(255, 255, 255, 255), fallbackLabel); } if (hov) ImGui::SetTooltip("%s", tooltip); y += kBtnSize + kSpacing; (void)id; return clicked; }; // Helper: thin horizontal separator auto DrawSep = [&]() { y += kSepGap; dl->AddLine(ImVec2(x - 2.f, y), ImVec2(x + kBtnSize + 2.f, y), IM_COL32(80, 80, 80, 160), kSepH); y += kSepH + kSepGap; }; // ── Section 1: Layout mode ─────────────────────────────────────────────── // Use Layout icons if available, otherwise render small Unicode glyphs. // We don't currently have dedicated layout icons in IconManager so we use // the fallback text path with descriptive single-character labels. struct LayoutEntry { LayoutMode mode; Icon icon; const char* label; // fallback text when no icon manager const char* tooltip; }; static const LayoutEntry kLayouts[] = { { LayoutMode::Single, Icon::LayoutSingle, "1", "Single viewport [1]" }, { LayoutMode::HSplit, Icon::LayoutHSplit, "H", "Split left|right [H]" }, { LayoutMode::VSplit, Icon::LayoutVSplit, "V", "Split top/bottom [V]" }, { LayoutMode::Quad, Icon::LayoutQuad, "4", "4-quadrant grid [4]" }, }; for (const auto& lk : kLayouts) { bool active = (m_layout == lk.mode); ImVec2 bMin(x, y); ImVec2 bMax(x + kBtnSize, y + kBtnSize); bool hov = mouse.x >= bMin.x && mouse.x <= bMax.x && mouse.y >= bMin.y && mouse.y <= bMax.y; bool clicked = hov && lmbClk; ImU32 bg = active ? IM_COL32( 66, 150, 250, 230) : hov ? IM_COL32( 70, 70, 70, 220) : IM_COL32( 32, 32, 32, 178); dl->AddRectFilled(bMin, bMax, bg, kRounding); if (active) dl->AddRect(bMin, bMax, IM_COL32(100, 180, 255, 200), kRounding, 0, 1.5f); if (m_iconManager) { ImTextureID tex = m_iconManager->Get(lk.icon); dl->AddImage(ImTextureRef(tex), ImVec2(bMin.x + kIconPad, bMin.y + kIconPad), ImVec2(bMax.x - kIconPad, bMax.y - kIconPad)); } else { ImVec2 ts = ImGui::CalcTextSize(lk.label); dl->AddText( ImVec2(bMin.x + (kBtnSize - ts.x) * 0.5f, bMin.y + (kBtnSize - ts.y) * 0.5f), IM_COL32(255, 255, 255, 255), lk.label); } if (hov) ImGui::SetTooltip("%s", lk.tooltip); if (clicked) SetLayout(lk.mode); y += kBtnSize + kSpacing; } DrawSep(); // ── Section 2: Manipulator tool mode (global, Q/W/E/R) ────────────────── struct ToolEntry { ManipulatorMode mode; Icon icon; const char* label; const char* tooltip; }; static const ToolEntry kTools[] = { { ManipulatorMode::Select, Icon::ToolSelect, "Q", "Select (Q)" }, { ManipulatorMode::Move, Icon::ToolMove, "W", "Move (W)" }, { ManipulatorMode::Rotate, Icon::ToolRotate, "E", "Rotate (E)" }, { ManipulatorMode::Scale, Icon::ToolScale, "R", "Scale (R)" }, }; ManipulatorMode curMode = m_manipulator.GetMode(); for (const auto& tk : kTools) { if (DrawBtn(tk.label, tk.icon, tk.label, curMode == tk.mode, tk.tooltip)) m_manipulator.SetMode(tk.mode); } DrawSep(); // ── Section 3: Transform space toggle ─────────────────────────────────── bool isWorld = (m_manipulator.GetTransformSpace() == TransformSpace::World); if (DrawBtn("WO", isWorld ? Icon::WorldSpace : Icon::LocalSpace, isWorld ? "W" : "O", isWorld, isWorld ? "World space (click → Object)" : "Object space (click → World)")) { m_manipulator.SetTransformSpace(isWorld ? TransformSpace::Object : TransformSpace::World); } } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- void ViewportPanel::Render() { ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); ImGui::Begin("Viewport", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); // Full content area (no top toolbar — layout buttons are now in the left toolbar) ImVec2 contentPos = ImGui::GetCursorScreenPos(); ImVec2 contentSize = ImGui::GetContentRegionAvail(); // Reserve kToolbarW pixels on the left for the global toolbar. // Tiles occupy the remaining area to the right. ImVec2 tilesPos (contentPos.x + kToolbarW, contentPos.y); ImVec2 tilesSize(contentSize.x - kToolbarW, contentSize.y); // ── Global keyboard shortcuts (Q/W/E/R — no hover gate, truly global) ──── { ImGuiIO& io = ImGui::GetIO(); if (!io.WantTextInput) { if (ImGui::IsKeyPressed(ImGuiKey_Q)) m_manipulator.SetMode(ManipulatorMode::Select); if (ImGui::IsKeyPressed(ImGuiKey_W)) m_manipulator.SetMode(ManipulatorMode::Move); if (ImGui::IsKeyPressed(ImGuiKey_E)) m_manipulator.SetMode(ManipulatorMode::Rotate); if (ImGui::IsKeyPressed(ImGuiKey_R)) m_manipulator.SetMode(ManipulatorMode::Scale); } } // ── Render tiles ────────────────────────────────────────────────────────── int hoveredTileIndex = -1; if (m_maximizedTileIndex >= 0 && m_maximizedTileIndex < static_cast(m_tiles.size())) { // Maximised: tile fills the tile area (not the toolbar strip) int i = m_maximizedTileIndex; m_tiles[i]->Render(i, tilesPos, tilesSize, /*isFocused=*/true, m_manipulator, /*dividerActive=*/false); if (m_tiles[i]->WasClickedThisFrame()) UpdateFocusTile(i); if (m_tiles[i]->IsHoveredThisFrame()) hoveredTileIndex = i; ImDrawList* dl = ImGui::GetWindowDrawList(); dl->AddText( ImVec2(tilesPos.x + tilesSize.x - 200.f, tilesPos.y + 5.f), IM_COL32(255, 200, 0, 160), "Maximized [Space / Esc] restore"); } else { bool dividerActive = IsMouseOverDivider(tilesPos, tilesSize); auto rects = ComputeTileRects(tilesPos, tilesSize); for (int i = 0; i < static_cast(m_tiles.size()); ++i) { bool focused = (i == m_focusedTileIndex); m_tiles[i]->Render(i, rects[i].pos, rects[i].size, focused, m_manipulator, dividerActive); if (m_tiles[i]->WasClickedThisFrame()) UpdateFocusTile(i); if (m_tiles[i]->IsHoveredThisFrame()) hoveredTileIndex = i; } if (m_layout != LayoutMode::Single) DrawDividers(tilesPos, tilesSize); } // ── Global left toolbar (layout + Q/W/E/R + space) ──────────────────────── // Drawn after tiles so it renders on top; uses raw screen-pos hit-testing // so it is not inside any tile's BeginChild scope. RenderGlobalLeftToolbar(contentPos, contentSize); // ── Space / Escape maximize ─────────────────────────────────────────────── HandleMaximizeInput(hoveredTileIndex); ImGui::End(); ImGui::PopStyleVar(); // outer WindowPadding } } // namespace UsdLayerManager