/* =========================================================================== IceTech GPL Source Code Copyright (C) 2026 Justin Marshall This file is part of the IceTech GPL Source Code. =========================================================================== */ #include "precompiled.h" #pragma hdrstop #include "qe3.h" #include "Radiant.h" #include "ModelViewDock.h" #include "../../renderer/tr_local.h" #ifndef GL_CLAMP_TO_EDGE #define GL_CLAMP_TO_EDGE 0x812F #endif #ifndef TVM_SETLINECOLOR #define TVM_SETLINECOLOR (TV_FIRST + 40) #endif #ifndef BS_TYPEMASK #define BS_TYPEMASK 0x0000000F #endif static const UINT MODELVIEW_TIMER_ID = 0x7A90; static const UINT MODELVIEW_TIMER_MS = 16; static const COLORREF MV_DARK_BG = RGB(17, 19, 23); static const COLORREF MV_DARK_PANEL = RGB(23, 26, 31); static const COLORREF MV_DARK_INPUT = RGB(14, 16, 20); static const COLORREF MV_DARK_BUTTON = RGB(31, 35, 41); static const COLORREF MV_DARK_BUTTON_D = RGB(22, 25, 30); static const COLORREF MV_DARK_MENU = RGB(28, 31, 36); static const COLORREF MV_DARK_MENU_HOT = RGB(45, 50, 58); static const COLORREF MV_DARK_BORDER = RGB(58, 64, 72); static const COLORREF MV_DARK_TEXT = RGB(226, 232, 240); static const COLORREF MV_DARK_MUTED = RGB(148, 163, 184); static const COLORREF MV_DARK_ACCENT = RGB(87, 166, 255); static const char* MV_DARK_BUTTON_OLDPROC = "IceTech.ModelView.DarkButtonOldProc"; static const char* MV_DARK_STATIC_OLDPROC = "IceTech.ModelView.DarkStaticOldProc"; static const char* MV_DARK_COMBO_OLDPROC = "IceTech.ModelView.DarkComboOldProc"; struct modelViewAnimItem_t { int declAnimIndex; int md5AnimIndex; const idAnimInterface* declAnim; const idMD5AnimInterface* md5Anim; int lengthMS; int numFrames; CString displayText; }; static HBRUSH ModelViewBgBrush() { static HBRUSH brush = ::CreateSolidBrush(MV_DARK_BG); return brush; } static HBRUSH ModelViewPanelBrush() { static HBRUSH brush = ::CreateSolidBrush(MV_DARK_PANEL); return brush; } static HBRUSH ModelViewInputBrush() { static HBRUSH brush = ::CreateSolidBrush(MV_DARK_INPUT); return brush; } static HBRUSH ModelViewMenuBrush() { static HBRUSH brush = ::CreateSolidBrush(MV_DARK_MENU); return brush; } static void MV_FillRect(HDC hDC, const RECT& rc, COLORREF color) { HBRUSH brush = ::CreateSolidBrush(color); ::FillRect(hDC, &rc, brush); ::DeleteObject(brush); } static void MV_FrameRect(HDC hDC, const RECT& rc, COLORREF color) { HBRUSH brush = ::CreateSolidBrush(color); ::FrameRect(hDC, &rc, brush); ::DeleteObject(brush); } static void MV_FrameRect(CDC* dc, const CRect& rc, COLORREF color) { if (!dc) { return; } MV_FrameRect(dc->m_hDC, rc, color); } static int MV_MaxInt(int a, int b) { return (a > b) ? a : b; } static int MV_MinInt(int a, int b) { return (a < b) ? a : b; } static float MV_ClampFloat(float value, float lo, float hi) { if (value < lo) { return lo; } if (value > hi) { return hi; } return value; } static int MV_ClampInt(int value, int lo, int hi) { if (value < lo) { return lo; } if (value > hi) { return hi; } return value; } static CString MV_FormatAnimTime(int timeMS) { if (timeMS < 0) { timeMS = 0; } const int minutes = timeMS / 60000; const int seconds = (timeMS / 1000) % 60; const int millis = timeMS % 1000; CString text; text.Format("%d:%02d.%03d", minutes, seconds, millis); return text; } static void MV_ApplyNativeTheme(HWND hWnd) { if (!hWnd) { return; } typedef HRESULT(WINAPI* SetWindowThemeProc)(HWND, LPCWSTR, LPCWSTR); typedef BOOL(WINAPI* AllowDarkModeForWindowProc)(HWND, BOOL); static HMODULE hUxTheme = ::LoadLibraryA("uxtheme.dll"); static SetWindowThemeProc pSetWindowTheme = hUxTheme ? (SetWindowThemeProc)::GetProcAddress(hUxTheme, "SetWindowTheme") : NULL; static AllowDarkModeForWindowProc pAllowDarkModeForWindow = hUxTheme ? (AllowDarkModeForWindowProc)::GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)) : NULL; if (pAllowDarkModeForWindow) { pAllowDarkModeForWindow(hWnd, TRUE); } if (pSetWindowTheme) { pSetWindowTheme(hWnd, L"DarkMode_Explorer", NULL); } ::SendMessage(hWnd, WM_THEMECHANGED, 0, 0); } static void MV_DrawButton(HWND hWnd, HDC hDC) { RECT rc; ::GetClientRect(hWnd, &rc); const UINT style = (UINT)::GetWindowLong(hWnd, GWL_STYLE); const UINT type = style & BS_TYPEMASK; const bool isCheck = (type == BS_CHECKBOX || type == BS_AUTOCHECKBOX || type == BS_3STATE || type == BS_AUTO3STATE); const bool enabled = ::IsWindowEnabled(hWnd) ? true : false; const bool pushed = (::SendMessage(hWnd, BM_GETSTATE, 0, 0) & BST_PUSHED) != 0; const bool checked = (::SendMessage(hWnd, BM_GETCHECK, 0, 0) == BST_CHECKED); char text[256]; text[0] = '\0'; ::GetWindowTextA(hWnd, text, sizeof(text)); HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); ::SetTextColor(hDC, enabled ? MV_DARK_TEXT : RGB(101, 110, 126)); if (isCheck) { MV_FillRect(hDC, rc, MV_DARK_PANEL); RECT box = rc; box.left += 3; box.right = box.left + 14; box.top += MV_MaxInt(0, (rc.bottom - rc.top - 14) / 2); box.bottom = box.top + 14; MV_FillRect(hDC, box, MV_DARK_INPUT); MV_FrameRect(hDC, box, checked ? MV_DARK_ACCENT : MV_DARK_BORDER); if (checked) { HPEN pen = ::CreatePen(PS_SOLID, 2, MV_DARK_ACCENT); HPEN oldPen = (HPEN)::SelectObject(hDC, pen); ::MoveToEx(hDC, box.left + 3, box.top + 7, NULL); ::LineTo(hDC, box.left + 6, box.top + 10); ::LineTo(hDC, box.right - 3, box.top + 3); ::SelectObject(hDC, oldPen); ::DeleteObject(pen); } RECT textRect = rc; textRect.left = box.right + 7; ::DrawTextA(hDC, text, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); } else { MV_FillRect(hDC, rc, pushed ? MV_DARK_BUTTON_D : MV_DARK_BUTTON); MV_FrameRect(hDC, rc, enabled ? MV_DARK_BORDER : RGB(39, 44, 52)); RECT textRect = rc; if (pushed) { ::OffsetRect(&textRect, 1, 1); } ::DrawTextA(hDC, text, -1, &textRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); } if (oldFont) { ::SelectObject(hDC, oldFont); } } static LRESULT CALLBACK MVDarkButtonProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, MV_DARK_BUTTON_OLDPROC); if (!oldProc) { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } switch (uMsg) { case WM_ERASEBKGND: return 1; case WM_PAINT: { PAINTSTRUCT ps; HDC hDC = ::BeginPaint(hWnd, &ps); MV_DrawButton(hWnd, hDC); ::EndPaint(hWnd, &ps); return 0; } case WM_PRINTCLIENT: MV_DrawButton(hWnd, (HDC)wParam); return 0; case WM_MOUSEMOVE: case WM_LBUTTONDOWN: case WM_LBUTTONUP: case WM_ENABLE: case WM_SETTEXT: case BM_SETCHECK: case BM_SETSTATE: { LRESULT result = ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); ::InvalidateRect(hWnd, NULL, FALSE); return result; } case WM_NCDESTROY: { ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); ::RemovePropA(hWnd, MV_DARK_BUTTON_OLDPROC); return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } } return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } static void MV_SubclassButton(CWnd& wnd) { HWND hWnd = wnd.GetSafeHwnd(); if (!hWnd || ::GetPropA(hWnd, MV_DARK_BUTTON_OLDPROC)) { return; } WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); ::SetPropA(hWnd, MV_DARK_BUTTON_OLDPROC, (HANDLE)oldProc); ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)MVDarkButtonProc); ::InvalidateRect(hWnd, NULL, TRUE); } static void MV_DrawStatic(HWND hWnd, HDC hDC) { RECT rc; ::GetClientRect(hWnd, &rc); MV_FillRect(hDC, rc, MV_DARK_PANEL); char text[256]; text[0] = '\0'; ::GetWindowTextA(hWnd, text, sizeof(text)); HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); ::SetTextColor(hDC, MV_DARK_TEXT); UINT format = DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS; const UINT staticType = ((UINT)::GetWindowLong(hWnd, GWL_STYLE)) & 0x0000000F; if (staticType == SS_CENTER) { format |= DT_CENTER; } else if (staticType == SS_RIGHT) { format |= DT_RIGHT; } else { format |= DT_LEFT; } ::DrawTextA(hDC, text, -1, &rc, format); if (oldFont) { ::SelectObject(hDC, oldFont); } } static LRESULT CALLBACK MVDarkStaticProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, MV_DARK_STATIC_OLDPROC); if (!oldProc) { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } if (uMsg == WM_ERASEBKGND) { return 1; } if (uMsg == WM_PAINT) { PAINTSTRUCT ps; HDC hDC = ::BeginPaint(hWnd, &ps); MV_DrawStatic(hWnd, hDC); ::EndPaint(hWnd, &ps); return 0; } if (uMsg == WM_NCDESTROY) { ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); ::RemovePropA(hWnd, MV_DARK_STATIC_OLDPROC); return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } static void MV_SubclassStatic(CWnd& wnd) { HWND hWnd = wnd.GetSafeHwnd(); if (!hWnd || ::GetPropA(hWnd, MV_DARK_STATIC_OLDPROC)) { return; } WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); ::SetPropA(hWnd, MV_DARK_STATIC_OLDPROC, (HANDLE)oldProc); ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)MVDarkStaticProc); ::InvalidateRect(hWnd, NULL, TRUE); } static void MV_DrawCombo(HWND hWnd, HDC hDC) { RECT rc; ::GetClientRect(hWnd, &rc); const bool enabled = ::IsWindowEnabled(hWnd) ? true : false; const bool focused = (::GetFocus() == hWnd); MV_FillRect(hDC, rc, MV_DARK_INPUT); MV_FrameRect(hDC, rc, focused ? MV_DARK_ACCENT : MV_DARK_BORDER); RECT arrowRect = rc; arrowRect.left = MV_MaxInt(rc.left, rc.right - 22); MV_FillRect(hDC, arrowRect, MV_DARK_BUTTON); MV_FrameRect(hDC, arrowRect, focused ? MV_DARK_ACCENT : MV_DARK_BORDER); POINT arrow[3]; const int midX = (arrowRect.left + arrowRect.right) / 2; const int midY = (arrowRect.top + arrowRect.bottom) / 2; arrow[0].x = midX - 4; arrow[0].y = midY - 2; arrow[1].x = midX + 4; arrow[1].y = midY - 2; arrow[2].x = midX; arrow[2].y = midY + 4; HBRUSH arrowBrush = ::CreateSolidBrush(enabled ? MV_DARK_TEXT : RGB(101, 110, 126)); HBRUSH oldBrush = (HBRUSH)::SelectObject(hDC, arrowBrush); HPEN pen = ::CreatePen(PS_SOLID, 1, enabled ? MV_DARK_TEXT : RGB(101, 110, 126)); HPEN oldPen = (HPEN)::SelectObject(hDC, pen); ::Polygon(hDC, arrow, 3); ::SelectObject(hDC, oldPen); ::DeleteObject(pen); ::SelectObject(hDC, oldBrush); ::DeleteObject(arrowBrush); char text[1024]; text[0] = '\0'; const int sel = (int)::SendMessage(hWnd, CB_GETCURSEL, 0, 0); if (sel != CB_ERR) { const int len = (int)::SendMessage(hWnd, CB_GETLBTEXTLEN, sel, 0); if (len >= 0 && len < (int)sizeof(text)) { ::SendMessageA(hWnd, CB_GETLBTEXT, sel, (LPARAM)text); text[sizeof(text) - 1] = '\0'; } } if (!text[0]) { ::GetWindowTextA(hWnd, text, sizeof(text)); text[sizeof(text) - 1] = '\0'; } RECT textRect = rc; textRect.left += 8; textRect.right = MV_MaxInt(textRect.left, arrowRect.left - 6); textRect.top += 1; textRect.bottom -= 1; HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); ::SetTextColor(hDC, enabled ? MV_DARK_TEXT : RGB(101, 110, 126)); ::DrawTextA(hDC, text, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); if (oldFont) { ::SelectObject(hDC, oldFont); } } static LRESULT CALLBACK MVDarkComboProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, MV_DARK_COMBO_OLDPROC); if (!oldProc) { return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } switch (uMsg) { case WM_ERASEBKGND: return 1; case WM_PAINT: { PAINTSTRUCT ps; HDC hDC = ::BeginPaint(hWnd, &ps); MV_DrawCombo(hWnd, hDC); ::EndPaint(hWnd, &ps); return 0; } case WM_PRINTCLIENT: MV_DrawCombo(hWnd, (HDC)wParam); return 0; case WM_SETFOCUS: case WM_KILLFOCUS: case WM_ENABLE: case WM_SETTEXT: case CB_SETCURSEL: case CB_ADDSTRING: case CB_INSERTSTRING: case CB_DELETESTRING: case CB_RESETCONTENT: { LRESULT result = ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); ::InvalidateRect(hWnd, NULL, FALSE); return result; } case WM_NCDESTROY: { ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc); ::RemovePropA(hWnd, MV_DARK_COMBO_OLDPROC); return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } } return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam); } static void MV_SubclassCombo(CWnd& wnd) { HWND hWnd = wnd.GetSafeHwnd(); if (!hWnd || ::GetPropA(hWnd, MV_DARK_COMBO_OLDPROC)) { return; } WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hWnd, GWLP_WNDPROC); ::SetPropA(hWnd, MV_DARK_COMBO_OLDPROC, (HANDLE)oldProc); ::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)MVDarkComboProc); ::InvalidateRect(hWnd, NULL, TRUE); } static void MV_BindNullTexture() { if (globalImages) { globalImages->BindNull(); } else { glBindTexture(GL_TEXTURE_2D, 0); } glDisable(GL_TEXTURE_2D); } static idImage* MV_GetImageFromStage(const shaderStage_t* stage) { if (!stage) { return NULL; } if (stage->texture.image) { return stage->texture.image; } return NULL; } static const shaderStage_t* MV_FindAlbedoStage(const idMaterial* shader) { if (!shader) { return NULL; } const shaderStage_t* firstUsableImageStage = NULL; const int numStages = shader->GetNumStages(); for (int i = 0; i < numStages; i++) { const shaderStage_t* stage = shader->GetStage(i); if (!stage || !MV_GetImageFromStage(stage)) { continue; } if (stage->lighting == SL_DIFFUSE) { return stage; } if (!firstUsableImageStage && stage->lighting != SL_BUMP && stage->lighting != SL_SPECULAR) { firstUsableImageStage = stage; } } return firstUsableImageStage; } void GL_SelectTexture(int unit); static bool MV_BindAlbedoForSurface(const modelSurface_t* surface) { if (!surface || !surface->shader) { MV_BindNullTexture(); return false; } const shaderStage_t* stage = MV_FindAlbedoStage(surface->shader); idImage* image = MV_GetImageFromStage(stage); if (!image) { MV_BindNullTexture(); return false; } GL_SelectTexture(0); glEnable(GL_TEXTURE_2D); image->Bind(); return true; } //============================================================================= // ModelView decl creation helpers //============================================================================= #define MV_TEXTPROMPT_CLASS "IceTechModelViewTextPrompt" #define MV_TEXTPROMPT_EDIT 1001 struct modelViewTextPrompt_t { CString title; CString label; CString text; bool accepted; HWND hEdit; HWND hOwner; }; static CString MV_NormalizeSlashes(const CString& path) { CString out = path; out.Replace('\\', '/'); return out; } static CString MV_Trimmed(CString text) { text.TrimLeft(); text.TrimRight(); return text; } static CString MV_GetFileBaseName(const CString& path, bool stripExtension) { CString normalized = MV_NormalizeSlashes(path); int slash = normalized.ReverseFind('/'); CString name = (slash >= 0) ? normalized.Mid(slash + 1) : normalized; if (stripExtension) { int dot = name.ReverseFind('.'); if (dot > 0) { name = name.Left(dot); } } return name; } static CString MV_EnsureExtension(const CString& path, const char* extension) { CString normalized = MV_NormalizeSlashes(path); CString lower = normalized; lower.MakeLower(); CString ext = extension ? extension : ""; ext.MakeLower(); if (!ext.IsEmpty() && lower.Right(ext.GetLength()) != ext) { normalized += ext; } return normalized; } static CString MV_StripToGameRoot(const CString& path, const char** roots, int numRoots, const char* fallbackRoot) { CString normalized = MV_NormalizeSlashes(path); CString lower = normalized; lower.MakeLower(); for (int i = 0; i < numRoots; i++) { CString root = roots[i]; root.MakeLower(); if (lower.Left(root.GetLength()) == root) { return normalized; } CString slashRoot = "/"; slashRoot += root; int pos = lower.Find(slashRoot); if (pos >= 0) { return normalized.Mid(pos + 1); } } CString baseToken = "/base/"; int basePos = lower.Find(baseToken); if (basePos >= 0) { return normalized.Mid(basePos + baseToken.GetLength()); } CString fallback = fallbackRoot ? fallbackRoot : ""; if (!fallback.IsEmpty()) { if (fallback[fallback.GetLength() - 1] != '/') { fallback += "/"; } return fallback + MV_GetFileBaseName(normalized, false); } return normalized; } static CString MV_NormalizeMayaAssetPath(const CString& path) { static const char* roots[] = { "models/", "model_export/", "animations/", "anim/", "art/" }; return MV_StripToGameRoot(path, roots, sizeof(roots) / sizeof(roots[0]), NULL); } static CString MV_NormalizeDeclFilePath(const CString& path) { static const char* roots[] = { "def/", "defs/", "decls/" }; CString declPath = MV_StripToGameRoot(path, roots, sizeof(roots) / sizeof(roots[0]), "def"); return MV_EnsureExtension(declPath, ".def"); } static bool MV_IsTrimNameChar(char c) { return c == '/' || c == '_' || c == '-'; } static CString MV_TrimNameTokenChars(CString name) { while (!name.IsEmpty() && MV_IsTrimNameChar(name[0])) { name = name.Mid(1); } while (!name.IsEmpty() && MV_IsTrimNameChar(name[name.GetLength() - 1])) { name = name.Left(name.GetLength() - 1); } return name; } static CString MV_BuildSafeNameFromAssetPath(const CString& assetPath) { CString name = MV_NormalizeSlashes(assetPath); CString lower = name; lower.MakeLower(); if (lower.Left(7) == "models/") { name = name.Mid(7); } else if (lower.Left(13) == "model_export/") { name = name.Mid(13); } else if (lower.Left(11) == "animations/") { name = name.Mid(11); } int dot = name.ReverseFind('.'); if (dot > 0) { name = name.Left(dot); } for (int i = 0; i < name.GetLength(); i++) { char c = name[i]; const bool valid = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '/'; if (!valid) { name.SetAt(i, '_'); } } name = MV_TrimNameTokenChars(name); if (name.IsEmpty()) { name = MV_GetFileBaseName(assetPath, true); } name.Replace('/', '_'); return name; } static CString MV_EscapeDeclPath(const CString& path) { CString escaped = MV_NormalizeSlashes(path); escaped.Replace("\\", "\\\\"); escaped.Replace("\"", "\\\""); return escaped; } static CString MV_BuildModelDefText(const char* declName, const char* meshPath) { const char* typeName = "modelDef"; if (declManager) { typeName = declManager->GetDeclNameFromType(DECL_MODELDEF); } CString text; text.Format("%s %s\r\n{\r\n\tmesh \"%s\"\r\n}\r\n", typeName, declName ? declName : "unnamed_model", (LPCTSTR)MV_EscapeDeclPath(meshPath ? meshPath : "")); return text; } static int MV_FindFinalDeclBrace(const CString& text) { for (int i = text.GetLength() - 1; i >= 0; i--) { char c = text[i]; if (c == '}') { return i; } if (c != ' ' && c != '\t' && c != '\r' && c != '\n') { break; } } return text.ReverseFind('}'); } static CString MV_InsertAnimIntoModelDefText(const CString& declText, const char* animName, const char* animPath) { CString animLine; animLine.Format("\tanim %s \"%s\"\r\n", animName ? animName : "new_anim", (LPCTSTR)MV_EscapeDeclPath(animPath ? animPath : "")); int brace = MV_FindFinalDeclBrace(declText); if (brace < 0) { CString text = declText; if (!text.IsEmpty() && text[text.GetLength() - 1] != '\n') { text += "\r\n"; } text += animLine; text += "}\r\n"; return text; } CString prefix = declText.Left(brace); CString suffix = declText.Mid(brace); if (!prefix.IsEmpty() && prefix[prefix.GetLength() - 1] != '\n') { prefix += "\r\n"; } return prefix + animLine + suffix; } static CString MV_ReadDeclText(const idDecl* decl) { CString text; if (!decl) { return text; } const int length = decl->GetTextLength(); if (length <= 0) { return text; } char* buffer = new char[length + 1]; decl->GetText(buffer); buffer[length] = '\0'; text = buffer; delete[] buffer; return text; } static void MV_SetDefaultGuiFont(HWND hWnd) { HFONT font = (HFONT)::GetStockObject(DEFAULT_GUI_FONT); if (font && hWnd) { ::SendMessage(hWnd, WM_SETFONT, (WPARAM)font, TRUE); } } static LRESULT CALLBACK MVTextPromptWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { modelViewTextPrompt_t* prompt = (modelViewTextPrompt_t*)::GetWindowLongPtr(hWnd, GWLP_USERDATA); switch (uMsg) { case WM_CREATE: { CREATESTRUCT* cs = (CREATESTRUCT*)lParam; prompt = (modelViewTextPrompt_t*)cs->lpCreateParams; ::SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)prompt); HWND hLabel = ::CreateWindowExA(0, "STATIC", prompt->label, WS_CHILD | WS_VISIBLE | SS_LEFT, 12, 12, 356, 18, hWnd, NULL, AfxGetInstanceHandle(), NULL); prompt->hEdit = ::CreateWindowExA(WS_EX_CLIENTEDGE, "EDIT", prompt->text, WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, 12, 36, 356, 22, hWnd, (HMENU)MV_TEXTPROMPT_EDIT, AfxGetInstanceHandle(), NULL); HWND hOK = ::CreateWindowExA(0, "BUTTON", "OK", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 206, 72, 78, 24, hWnd, (HMENU)IDOK, AfxGetInstanceHandle(), NULL); HWND hCancel = ::CreateWindowExA(0, "BUTTON", "Cancel", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 290, 72, 78, 24, hWnd, (HMENU)IDCANCEL, AfxGetInstanceHandle(), NULL); MV_SetDefaultGuiFont(hLabel); MV_SetDefaultGuiFont(prompt->hEdit); MV_SetDefaultGuiFont(hOK); MV_SetDefaultGuiFont(hCancel); ::SendMessage(prompt->hEdit, EM_SETSEL, 0, -1); ::SetFocus(prompt->hEdit); return 0; } case WM_COMMAND: if (LOWORD(wParam) == IDOK) { char buffer[MAX_STRING_CHARS]; buffer[0] = '\0'; if (prompt && prompt->hEdit) { ::GetWindowTextA(prompt->hEdit, buffer, sizeof(buffer)); } if (prompt) { prompt->text = buffer; prompt->accepted = true; } ::DestroyWindow(hWnd); return 0; } if (LOWORD(wParam) == IDCANCEL) { ::DestroyWindow(hWnd); return 0; } break; case WM_CLOSE: ::DestroyWindow(hWnd); return 0; } return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } static ATOM MV_RegisterTextPromptClass() { static ATOM atom = 0; if (atom) { return atom; } WNDCLASSA wc; memset(&wc, 0, sizeof(wc)); wc.lpfnWndProc = MVTextPromptWndProc; wc.hInstance = AfxGetInstanceHandle(); wc.hCursor = ::LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1); wc.lpszClassName = MV_TEXTPROMPT_CLASS; atom = ::RegisterClassA(&wc); return atom; } static bool MV_PromptForText(CWnd* parent, const char* title, const char* label, CString& value) { if (!MV_RegisterTextPromptClass()) { return false; } modelViewTextPrompt_t prompt; prompt.title = title ? title : "Text"; prompt.label = label ? label : "Value:"; prompt.text = value; prompt.accepted = false; prompt.hEdit = NULL; prompt.hOwner = parent ? parent->GetSafeHwnd() : NULL; HWND hWnd = ::CreateWindowExA( WS_EX_DLGMODALFRAME, MV_TEXTPROMPT_CLASS, prompt.title, WS_POPUP | WS_CAPTION | WS_SYSMENU, CW_USEDEFAULT, CW_USEDEFAULT, 390, 136, prompt.hOwner, NULL, AfxGetInstanceHandle(), &prompt ); if (!hWnd) { return false; } if (prompt.hOwner) { RECT ownerRect; RECT wndRect; ::GetWindowRect(prompt.hOwner, &ownerRect); ::GetWindowRect(hWnd, &wndRect); const int w = wndRect.right - wndRect.left; const int h = wndRect.bottom - wndRect.top; const int x = ownerRect.left + ((ownerRect.right - ownerRect.left) - w) / 2; const int y = ownerRect.top + ((ownerRect.bottom - ownerRect.top) - h) / 2; ::SetWindowPos(hWnd, NULL, x, y, 0, 0, SWP_NOZORDER | SWP_NOSIZE); ::EnableWindow(prompt.hOwner, FALSE); } ::ShowWindow(hWnd, SW_SHOW); ::UpdateWindow(hWnd); MSG msg; while (::IsWindow(hWnd) && ::GetMessage(&msg, NULL, 0, 0) > 0) { if (!::IsDialogMessage(hWnd, &msg)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } } if (prompt.hOwner) { ::EnableWindow(prompt.hOwner, TRUE); ::SetActiveWindow(prompt.hOwner); } if (!prompt.accepted) { return false; } value = MV_Trimmed(prompt.text); return !value.IsEmpty(); } static bool MV_ChooseMayaFile(CWnd* parent, const char* title, CString& assetPath) { CFileDialog dlg(TRUE, "ma", NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, "Maya ASCII Files (*.ma)|*.ma|All Files (*.*)|*.*||", parent); dlg.m_ofn.lpstrTitle = title ? title : "Select Maya ASCII File"; if (dlg.DoModal() != IDOK) { return false; } assetPath = MV_NormalizeMayaAssetPath(dlg.GetPathName()); return !assetPath.IsEmpty(); } static bool MV_ChooseDeclFile(CWnd* parent, CString& declFile, bool& createdNew) { int choice = AfxMessageBox( "Save this modelDef in an existing decl file?\n\nYes = choose an existing decl file\nNo = create a new decl file\nCancel = abort", MB_ICONQUESTION | MB_YESNOCANCEL); if (choice == IDCANCEL) { return false; } const bool existing = (choice == IDYES); createdNew = !existing; CFileDialog dlg(existing ? TRUE : FALSE, "def", NULL, existing ? (OFN_HIDEREADONLY | OFN_FILEMUSTEXIST) : (OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT), "Decl Files (*.def)|*.def|All Files (*.*)|*.*||", parent); dlg.m_ofn.lpstrTitle = existing ? "Choose Existing Decl File" : "Create New Decl File"; if (dlg.DoModal() != IDOK) { return false; } declFile = MV_NormalizeDeclFilePath(dlg.GetPathName()); return !declFile.IsEmpty(); } static CString MV_GetExportModelsNameFromDeclFile(const char* declFileName) { CString exportName = MV_GetFileBaseName(declFileName ? declFileName : "", true); exportName = MV_Trimmed(exportName); exportName.Replace("\r", ""); exportName.Replace("\n", ""); return exportName; } static void MV_ExecuteExportModelsCommand(const char* declFileName, const char* reason) { if (!cmdSystem) { common->Warning("ModelView: cmdSystem is not available; cannot run exportModels."); return; } CString exportName = MV_GetExportModelsNameFromDeclFile(declFileName); if (exportName.IsEmpty()) { common->Warning("ModelView: no containing decl file available; cannot run exportModels."); return; } CString commandText; commandText.Format("exportModels %s", (LPCTSTR)exportName); if (reason && reason[0]) { common->Printf("ModelView: running %s after %s.\n", (LPCTSTR)commandText, reason); } else { common->Printf("ModelView: running %s.\n", (LPCTSTR)commandText); } commandText += "\n"; cmdSystem->BufferCommandText(CMD_EXEC_NOW, (LPCTSTR)commandText); } static int MV_MenuBarPreferredHeight() { int h = ::GetSystemMetrics(SM_CYMENU) + 4; return (h < 22) ? 22 : h; } #define MV_MENU_OWNERDRAW_MAGIC 0x4D564F44 struct modelViewMenuItemData_t { DWORD magic; UINT id; char text[64]; }; struct modelViewMenuBarData_t { HMENU hTopMenu; HMENU hFileMenu; RECT itemRects[8]; int numItemRects; int hotItem; modelViewMenuItemData_t newMD5DeclItem; modelViewMenuItemData_t addAnimItem; }; static modelViewMenuBarData_t* MV_MenuBarData(HWND hWnd) { return (modelViewMenuBarData_t*)::GetWindowLongPtr(hWnd, GWLP_USERDATA); } static void MV_InitMenuItemData(modelViewMenuItemData_t& item, UINT id, const char* text) { memset(&item, 0, sizeof(item)); item.magic = MV_MENU_OWNERDRAW_MAGIC; item.id = id; if (text) { strncpy(item.text, text, sizeof(item.text) - 1); item.text[sizeof(item.text) - 1] = '\0'; } } static void MV_MenuBarRebuildRects(HWND hWnd, HDC hDC, modelViewMenuBarData_t* data) { if (!data || !data->hTopMenu) { return; } data->numItemRects = 0; int x = 4; const int y = 1; const int h = MV_MenuBarPreferredHeight() - 2; const int count = ::GetMenuItemCount(data->hTopMenu); HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; for (int i = 0; i < count && data->numItemRects < 8; i++) { char text[128]; text[0] = '\0'; ::GetMenuStringA(data->hTopMenu, i, text, sizeof(text), MF_BYPOSITION); SIZE size = { 40, h }; if (text[0]) { ::GetTextExtentPoint32A(hDC, text, lstrlenA(text), &size); } RECT item; item.left = x; item.top = y; item.right = x + size.cx + 20; item.bottom = y + h; data->itemRects[data->numItemRects++] = item; x = item.right + 1; } if (oldFont) { ::SelectObject(hDC, oldFont); } } static int MV_MenuBarHitTest(modelViewMenuBarData_t* data, const POINT& point) { if (!data) { return -1; } for (int i = 0; i < data->numItemRects; i++) { if (::PtInRect(&data->itemRects[i], point)) { return i; } } return -1; } static void MV_DrawOwnerMenuItem(const DRAWITEMSTRUCT* dis) { if (!dis || dis->CtlType != ODT_MENU) { return; } const modelViewMenuItemData_t* item = (const modelViewMenuItemData_t*)dis->itemData; if (!item || item->magic != MV_MENU_OWNERDRAW_MAGIC) { return; } const bool selected = (dis->itemState & ODS_SELECTED) != 0; const bool disabled = (dis->itemState & ODS_DISABLED) != 0; RECT rc = dis->rcItem; MV_FillRect(dis->hDC, rc, selected ? MV_DARK_MENU_HOT : MV_DARK_MENU); RECT accent = rc; accent.right = accent.left + 3; MV_FillRect(dis->hDC, accent, selected ? MV_DARK_ACCENT : MV_DARK_MENU); RECT textRect = rc; textRect.left += 22; textRect.right -= 8; HFONT font = (HFONT)::GetStockObject(DEFAULT_GUI_FONT); HFONT oldFont = font ? (HFONT)::SelectObject(dis->hDC, font) : NULL; ::SetBkMode(dis->hDC, TRANSPARENT); ::SetTextColor(dis->hDC, disabled ? RGB(101, 110, 126) : MV_DARK_TEXT); ::DrawTextA(dis->hDC, item->text, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); if (oldFont) { ::SelectObject(dis->hDC, oldFont); } } static void MV_MenuBarPaint(HWND hWnd) { PAINTSTRUCT ps; HDC hDC = ::BeginPaint(hWnd, &ps); RECT client; ::GetClientRect(hWnd, &client); MV_FillRect(hDC, client, MV_DARK_MENU); modelViewMenuBarData_t* data = MV_MenuBarData(hWnd); if (data) { MV_MenuBarRebuildRects(hWnd, hDC, data); HFONT font = (HFONT)::SendMessage(hWnd, WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); const int count = ::GetMenuItemCount(data->hTopMenu); for (int i = 0; i < count && i < data->numItemRects; i++) { RECT itemRect = data->itemRects[i]; if (i == data->hotItem) { MV_FillRect(hDC, itemRect, MV_DARK_MENU_HOT); MV_FrameRect(hDC, itemRect, MV_DARK_ACCENT); ::InflateRect(&itemRect, -1, -1); } char text[128]; text[0] = '\0'; ::GetMenuStringA(data->hTopMenu, i, text, sizeof(text), MF_BYPOSITION); ::SetTextColor(hDC, MV_DARK_TEXT); ::DrawTextA(hDC, text, -1, &itemRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); } if (oldFont) { ::SelectObject(hDC, oldFont); } } RECT line = client; line.top = client.bottom - 1; MV_FillRect(hDC, line, MV_DARK_BORDER); ::EndPaint(hWnd, &ps); } static void MV_MenuBarTrackTopLevel(HWND hWnd, int index) { modelViewMenuBarData_t* data = MV_MenuBarData(hWnd); if (!data || !data->hTopMenu || index < 0 || index >= ::GetMenuItemCount(data->hTopMenu)) { return; } HMENU popup = ::GetSubMenu(data->hTopMenu, index); if (!popup) { return; } RECT rect; if (index < data->numItemRects) { rect = data->itemRects[index]; } else { ::GetClientRect(hWnd, &rect); } POINT pt; pt.x = rect.left; pt.y = rect.bottom; ::ClientToScreen(hWnd, &pt); data->hotItem = index; ::InvalidateRect(hWnd, NULL, FALSE); ::UpdateWindow(hWnd); HWND hOwner = ::GetAncestor(hWnd, GA_ROOT); if (hOwner) { ::SetForegroundWindow(hOwner); } const UINT command = ::TrackPopupMenu( popup, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON | TPM_RETURNCMD, pt.x, pt.y, 0, hWnd, NULL ); if (command != 0) { HWND hParent = ::GetParent(hWnd); if (hParent) { ::SendMessage(hParent, WM_COMMAND, MAKEWPARAM(command, 0), 0); } } data->hotItem = -1; ::InvalidateRect(hWnd, NULL, FALSE); ::PostMessage(hWnd, WM_NULL, 0, 0); } static LRESULT CALLBACK MVMenuBarWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { modelViewMenuBarData_t* data = MV_MenuBarData(hWnd); switch (uMsg) { case WM_NCCREATE: { CREATESTRUCT* cs = (CREATESTRUCT*)lParam; ::SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)cs->lpCreateParams); return TRUE; } case WM_NCDESTROY: if (data) { if (data->hTopMenu) { ::DestroyMenu(data->hTopMenu); data->hTopMenu = NULL; } data->hFileMenu = NULL; delete data; ::SetWindowLongPtr(hWnd, GWLP_USERDATA, 0); } return ::DefWindowProc(hWnd, uMsg, wParam, lParam); case WM_ERASEBKGND: return 1; case WM_PAINT: MV_MenuBarPaint(hWnd); return 0; case WM_PRINTCLIENT: { HDC hDC = (HDC)wParam; RECT client; ::GetClientRect(hWnd, &client); MV_FillRect(hDC, client, MV_DARK_MENU); if (data) { MV_MenuBarRebuildRects(hWnd, hDC, data); } return 0; } case WM_MOUSEMOVE: if (data) { POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; int hot = MV_MenuBarHitTest(data, pt); if (hot != data->hotItem) { data->hotItem = hot; ::InvalidateRect(hWnd, NULL, FALSE); } } return 0; case WM_LBUTTONDOWN: if (data) { HDC hDC = ::GetDC(hWnd); MV_MenuBarRebuildRects(hWnd, hDC, data); ::ReleaseDC(hWnd, hDC); POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; MV_MenuBarTrackTopLevel(hWnd, MV_MenuBarHitTest(data, pt)); } return 0; case WM_MEASUREITEM: { MEASUREITEMSTRUCT* mis = (MEASUREITEMSTRUCT*)lParam; if (mis && mis->CtlType == ODT_MENU) { const modelViewMenuItemData_t* item = (const modelViewMenuItemData_t*)mis->itemData; if (item && item->magic == MV_MENU_OWNERDRAW_MAGIC) { mis->itemWidth = 128; mis->itemHeight = 24; return TRUE; } } break; } case WM_DRAWITEM: { DRAWITEMSTRUCT* dis = (DRAWITEMSTRUCT*)lParam; if (dis && dis->CtlType == ODT_MENU) { const modelViewMenuItemData_t* item = (const modelViewMenuItemData_t*)dis->itemData; if (item && item->magic == MV_MENU_OWNERDRAW_MAGIC) { MV_DrawOwnerMenuItem(dis); return TRUE; } } break; } case WM_SYSKEYDOWN: case WM_KEYDOWN: if (wParam == VK_F10 || wParam == 'F') { MV_MenuBarTrackTopLevel(hWnd, 0); return 0; } break; } return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } static ATOM MV_RegisterMenuBarClass() { static ATOM atom = 0; if (atom) { return atom; } WNDCLASSA wc; memset(&wc, 0, sizeof(wc)); wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = MVMenuBarWndProc; wc.hInstance = AfxGetInstanceHandle(); wc.hCursor = ::LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = ModelViewMenuBrush(); wc.lpszClassName = "IceTechModelViewMenuBar"; atom = ::RegisterClassA(&wc); return atom; } static HWND MV_CreateModelViewMenuBar(HWND hParent) { if (!hParent || !MV_RegisterMenuBarClass()) { return NULL; } modelViewMenuBarData_t* data = new modelViewMenuBarData_t; memset(data, 0, sizeof(*data)); data->hotItem = -1; MV_InitMenuItemData(data->newMD5DeclItem, ID_MODELVIEW_FILE_NEWMD5DECL, "New MD5 Decl..."); MV_InitMenuItemData(data->addAnimItem, ID_MODELVIEW_FILE_ADDANIM, "Add Animation..."); data->hTopMenu = ::CreateMenu(); data->hFileMenu = ::CreatePopupMenu(); if (!data->hTopMenu || !data->hFileMenu) { if (data->hTopMenu) { ::DestroyMenu(data->hTopMenu); } if (data->hFileMenu) { ::DestroyMenu(data->hFileMenu); } delete data; return NULL; } ::AppendMenuA(data->hFileMenu, MF_OWNERDRAW | MF_ENABLED, ID_MODELVIEW_FILE_NEWMD5DECL, (LPCSTR)&data->newMD5DeclItem); ::AppendMenuA(data->hFileMenu, MF_OWNERDRAW | MF_ENABLED, ID_MODELVIEW_FILE_ADDANIM, (LPCSTR)&data->addAnimItem); ::AppendMenuA(data->hTopMenu, MF_POPUP | MF_ENABLED, (UINT_PTR)data->hFileMenu, "&File"); HWND hWnd = ::CreateWindowExA( 0, "IceTechModelViewMenuBar", "ModelViewMenuBar", WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, 0, 0, 0, 0, hParent, (HMENU)ID_MODELVIEW_MENUBAR, AfxGetInstanceHandle(), data ); if (!hWnd) { if (data->hTopMenu) { ::DestroyMenu(data->hTopMenu); } delete data; return NULL; } HFONT font = (HFONT)::GetStockObject(DEFAULT_GUI_FONT); if (font) { ::SendMessage(hWnd, WM_SETFONT, (WPARAM)font, FALSE); } return hWnd; } //============================================================================= // CModelRenderWnd //============================================================================= IMPLEMENT_DYNAMIC(CModelRenderWnd, CWnd) BEGIN_MESSAGE_MAP(CModelRenderWnd, CWnd) ON_WM_CREATE() ON_WM_DESTROY() ON_WM_PAINT() ON_WM_SIZE() ON_WM_ERASEBKGND() ON_WM_TIMER() ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() ON_WM_MOUSEMOVE() ON_WM_MOUSEWHEEL() END_MESSAGE_MAP() CModelRenderWnd::CModelRenderWnd() { m_hDC = NULL; m_hGLRC = NULL; m_modelDecl = NULL; m_model = NULL; m_cachedDynamicModel = NULL; m_joints = NULL; m_numJoints = 0; m_haveAnimFrame = FALSE; m_anim = NULL; m_animLengthMS = 0; m_animFrames = 0; m_animTimeMS = 0; m_lastTick = 0; m_showSkeleton = FALSE; m_active = FALSE; m_playing = FALSE; m_dragging = FALSE; m_yaw = -35.0f; m_pitch = 18.0f; m_distance = 192.0f; m_center[0] = m_center[1] = m_center[2] = 0.0f; m_radius = 64.0f; } CModelRenderWnd::~CModelRenderWnd() { ShutdownGL(); ResetCachedDynamicModel(); FreeJointBuffer(); } BOOL CModelRenderWnd::Create(CWnd* pParent, UINT nID) { CString className = AfxRegisterWndClass( CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS | CS_OWNDC, ::LoadCursor(NULL, IDC_ARROW), (HBRUSH)::GetStockObject(BLACK_BRUSH), NULL ); return CWnd::CreateEx( WS_EX_CLIENTEDGE, className, "RadiantModelRenderWnd", WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 0, 0), pParent, nID ); } int CModelRenderWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) { return -1; } InitGL(); m_lastTick = ::GetTickCount(); SetTimer(MODELVIEW_TIMER_ID, MODELVIEW_TIMER_MS, NULL); return 0; } void CModelRenderWnd::OnDestroy() { KillTimer(MODELVIEW_TIMER_ID); ShutdownGL(); CWnd::OnDestroy(); } void CModelRenderWnd::InitGL() { if (m_hGLRC) { return; } m_hDC = ::GetDC(GetSafeHwnd()); if (!m_hDC) { return; } QEW_SetupPixelFormat(m_hDC, false); m_hGLRC = (HGLRC)wglCreateContext(m_hDC); } void CModelRenderWnd::ShutdownGL() { if (m_hGLRC) { wglMakeCurrent(NULL, NULL); wglDeleteContext(m_hGLRC); m_hGLRC = NULL; } if (m_hDC && GetSafeHwnd()) { ::ReleaseDC(GetSafeHwnd(), m_hDC); m_hDC = NULL; } } void CModelRenderWnd::FreeJointBuffer() { delete[] m_joints; m_joints = NULL; m_numJoints = 0; m_haveAnimFrame = FALSE; } void CModelRenderWnd::AllocateJointBuffer() { FreeJointBuffer(); if (!m_modelDecl) { return; } m_numJoints = m_modelDecl->NumJoints(); if (m_numJoints > 0) { m_joints = new idJointMat[m_numJoints]; memset(m_joints, 0, sizeof(idJointMat) * m_numJoints); } } void CModelRenderWnd::ResetCachedDynamicModel() { if (m_cachedDynamicModel && renderModelManager) { renderModelManager->FreeModel(m_cachedDynamicModel); } m_cachedDynamicModel = NULL; } void CModelRenderWnd::ResolveModelFromGameEdit() { m_model = NULL; if (!m_modelDecl) { return; } const char* modelName = m_modelDecl->GetModelName(); if (gameEdit && modelName && modelName[0]) { m_model = gameEdit->ANIM_GetModelFromName(modelName); } // Keep this as a non-animator fallback for editor builds that have decls loaded // before gameEdit is initialized. if (!m_model) { m_model = m_modelDecl->ModelHandle(); } } void CModelRenderWnd::UpdateAnimationFrame(BOOL forceResetCachedModel) { m_haveAnimFrame = FALSE; if (forceResetCachedModel) { ResetCachedDynamicModel(); } if (!m_model || !m_anim || !m_joints || m_numJoints <= 0 || !gameEdit) { return; } int frameTime = m_animTimeMS; if (m_animLengthMS > 0) { frameTime = MV_ClampInt(frameTime, 0, m_animLengthMS - 1); } gameEdit->ANIM_CreateAnimFrame( m_model, m_anim, m_numJoints, m_joints, frameTime, m_modelDecl ? m_modelDecl->GetVisualOffset() : vec3_origin, true ); m_haveAnimFrame = TRUE; } void CModelRenderWnd::SetModelDecl(const idDeclModelDef* modelDecl) { m_modelDecl = (idDeclModelDefInterface*)modelDecl; m_model = NULL; m_anim = NULL; m_animDisplayName.Empty(); m_animLengthMS = 0; m_animFrames = 0; m_animTimeMS = 0; m_playing = FALSE; m_lastTick = ::GetTickCount(); ResetCachedDynamicModel(); ResolveModelFromGameEdit(); AllocateJointBuffer(); UpdateAnimationFrame(FALSE); FitCameraToModel(); Invalidate(FALSE); NotifyAnimationTimeChanged(); } void CModelRenderWnd::SetAnimation(const idMD5Anim* anim, int animLengthMS, int numFrames, const char* displayName) { m_anim = anim; m_animLengthMS = MV_MaxInt(0, animLengthMS); m_animFrames = MV_MaxInt(0, numFrames); m_animDisplayName = displayName ? displayName : ""; m_animTimeMS = 0; m_playing = FALSE; m_lastTick = ::GetTickCount(); UpdateAnimationFrame(TRUE); Invalidate(FALSE); NotifyAnimationTimeChanged(); } void CModelRenderWnd::SetAnimationTimeMS(int timeMS) { if (!m_anim) { m_animTimeMS = 0; m_lastTick = ::GetTickCount(); NotifyAnimationTimeChanged(); return; } if (m_animLengthMS > 0) { m_animTimeMS = MV_ClampInt(timeMS, 0, m_animLengthMS - 1); } else { m_animTimeMS = 0; } m_lastTick = ::GetTickCount(); UpdateAnimationFrame(TRUE); Invalidate(FALSE); NotifyAnimationTimeChanged(); } void CModelRenderWnd::PlayAnimation() { if (!m_anim || m_animLengthMS <= 0) { m_playing = FALSE; NotifyAnimationTimeChanged(); return; } m_playing = TRUE; m_lastTick = ::GetTickCount(); NotifyAnimationTimeChanged(); } void CModelRenderWnd::PauseAnimation() { m_playing = FALSE; m_lastTick = ::GetTickCount(); NotifyAnimationTimeChanged(); } void CModelRenderWnd::StopAnimation() { m_playing = FALSE; m_animTimeMS = 0; m_lastTick = ::GetTickCount(); UpdateAnimationFrame(TRUE); Invalidate(FALSE); NotifyAnimationTimeChanged(); } int CModelRenderWnd::GetAnimationTimeMS() const { return m_animTimeMS; } int CModelRenderWnd::GetAnimationLengthMS() const { return m_animLengthMS; } int CModelRenderWnd::GetAnimationFrames() const { return m_animFrames; } BOOL CModelRenderWnd::HasAnimation() const { return m_anim != NULL; } BOOL CModelRenderWnd::IsPlaying() const { return m_playing; } void CModelRenderWnd::NotifyAnimationTimeChanged() { CWnd* parent = GetParent(); if (parent && parent->GetSafeHwnd()) { parent->SendMessage(WM_MODELVIEW_ANIMTIME_CHANGED, (WPARAM)m_animTimeMS, (LPARAM)m_animLengthMS); } } void CModelRenderWnd::SetShowSkeleton(BOOL showSkeleton) { m_showSkeleton = showSkeleton; Invalidate(FALSE); } void CModelRenderWnd::SetActive(BOOL active) { m_active = active; if (m_active) { m_lastTick = ::GetTickCount(); Invalidate(FALSE); } } void CModelRenderWnd::FocusRenderWindow() { if (GetSafeHwnd()) { SetFocus(); } } void CModelRenderWnd::FitCameraToModel() { m_center[0] = m_center[1] = m_center[2] = 0.0f; m_radius = 64.0f; idRenderModel* model = m_model; if (!model && m_modelDecl) { model = m_modelDecl->ModelHandle(); } if (model) { idBounds bounds = model->Bounds(NULL); idVec3 center = (bounds[0] + bounds[1]) * 0.5f; idVec3 extents = bounds[1] - bounds[0]; m_center[0] = center.x; m_center[1] = center.y; m_center[2] = center.z; m_radius = extents.Length() * 0.5f; if (m_radius < 32.0f) { m_radius = 32.0f; } } m_distance = m_radius * 3.0f; if (m_distance < 96.0f) { m_distance = 96.0f; } } void CModelRenderWnd::StepAnimation() { if (!m_active || !m_playing || !m_anim || m_animLengthMS <= 0) { m_lastTick = ::GetTickCount(); return; } DWORD now = ::GetTickCount(); DWORD delta = now - m_lastTick; m_lastTick = now; if (delta > 250) { delta = 250; } m_animTimeMS += static_cast(delta); if (m_animLengthMS > 0) { m_animTimeMS %= m_animLengthMS; } UpdateAnimationFrame(FALSE); Invalidate(FALSE); NotifyAnimationTimeChanged(); } void CModelRenderWnd::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == MODELVIEW_TIMER_ID) { StepAnimation(); return; } CWnd::OnTimer(nIDEvent); } void CModelRenderWnd::OnPaint() { CPaintDC dc(this); if (!m_hGLRC || !m_hDC) { return; } idGraphicsDeviceContextHelper context(dc.m_hDC, m_hGLRC, false); DrawScene(); } void CModelRenderWnd::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); Invalidate(FALSE); } BOOL CModelRenderWnd::OnEraseBkgnd(CDC* pDC) { return TRUE; } void CModelRenderWnd::SetupCamera(const CRect& client) { const int width = MV_MaxInt(1, client.Width()); const int height = MV_MaxInt(1, client.Height()); const float aspect = static_cast(width) / static_cast(height); const float zNear = 1.0f; const float zFar = MV_MaxInt(4096, static_cast(m_distance + m_radius * 8.0f)); const float fovY = 60.0f; const float ymax = zNear * idMath::Tan(fovY * idMath::M_DEG2RAD * 0.5f); const float xmax = ymax * aspect; glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(-xmax, xmax, -ymax, ymax, zNear, zFar); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glTranslatef(0.0f, 0.0f, -m_distance); glRotatef(m_pitch, 1.0f, 0.0f, 0.0f); glRotatef(m_yaw, 0.0f, 0.0f, 1.0f); glTranslatef(-m_center[0], -m_center[1], -m_center[2]); } void CModelRenderWnd::DrawScene() { CRect client; GetClientRect(client); const int width = MV_MaxInt(1, client.Width()); const int height = MV_MaxInt(1, client.Height()); glViewport(0, 0, width, height); glScissor(0, 0, width, height); glEnable(GL_SCISSOR_TEST); glClearColor(0.045f, 0.052f, 0.065f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDisable(GL_CULL_FACE); glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); SetupCamera(client); DrawGrid(); DrawModel(); if (m_showSkeleton) { DrawSkeleton(); } DrawOverlayText(); glFlush(); } void CModelRenderWnd::DrawGrid() { const float size = MV_MaxInt(128, static_cast(m_radius * 2.0f)); const float step = 16.0f; glDisable(GL_DEPTH_TEST); glLineWidth(1.0f); glColor3f(0.18f, 0.20f, 0.24f); glBegin(GL_LINES); for (float x = -size; x <= size; x += step) { glVertex3f(x, -size, 0.0f); glVertex3f(x, size, 0.0f); } for (float y = -size; y <= size; y += step) { glVertex3f(-size, y, 0.0f); glVertex3f(size, y, 0.0f); } glEnd(); glLineWidth(2.0f); glBegin(GL_LINES); glColor3f(0.80f, 0.20f, 0.20f); glVertex3f(0.0f, 0.0f, 0.0f); glVertex3f(size, 0.0f, 0.0f); glColor3f(0.20f, 0.80f, 0.20f); glVertex3f(0.0f, 0.0f, 0.0f); glVertex3f(0.0f, size, 0.0f); glColor3f(0.20f, 0.35f, 1.0f); glVertex3f(0.0f, 0.0f, 0.0f); glVertex3f(0.0f, 0.0f, size * 0.5f); glEnd(); glLineWidth(1.0f); glEnable(GL_DEPTH_TEST); } void CModelRenderWnd::DrawRenderModel(idRenderModel* model, bool wireOnly) { if (!model) { return; } glPolygonMode(GL_FRONT_AND_BACK, wireOnly ? GL_LINE : GL_FILL); if (wireOnly) { MV_BindNullTexture(); glColor3f(0.08f, 0.10f, 0.12f); } else { glColor3f(1.0f, 1.0f, 1.0f); } const int surfaceCount = model->NumSurfaces(); for (int s = 0; s < surfaceCount; s++) { const modelSurface_t* surface = model->Surface(s); if (!surface || !surface->geometry || !surface->geometry->verts || !surface->geometry->indexes) { continue; } const srfTriangles_t* tri = surface->geometry; bool textured = false; if (!wireOnly) { textured = MV_BindAlbedoForSurface(surface); if (textured) { glColor3f(1.0f, 1.0f, 1.0f); } else { glColor3f(0.62f, 0.68f, 0.78f); } } glBegin(GL_TRIANGLES); for (int i = 0; i < tri->numIndexes; i++) { const int index = tri->indexes[i]; if (index < 0 || index >= tri->numVerts) { continue; } if (textured) { glTexCoord2fv(tri->verts[index].st.ToFloatPtr()); } glVertex3fv(tri->verts[index].xyz.ToFloatPtr()); } glEnd(); } MV_BindNullTexture(); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); } void CModelRenderWnd::DrawModel() { if (!m_model) { return; } idRenderModel* drawModel = m_model; if (m_model->IsDynamicModel() != DM_STATIC && m_haveAnimFrame && m_joints && m_numJoints > 0) { renderEntity_t renderEntity; memset(&renderEntity, 0, sizeof(renderEntity)); renderEntity.hModel = m_model; renderEntity.axis.Identity(); renderEntity.origin.Zero(); renderEntity.customSkin = m_modelDecl ? m_modelDecl->GetSkin() : NULL; renderEntity.numJoints = m_numJoints; renderEntity.joints = m_joints; idRenderModel* dynamicModel = m_model->InstantiateDynamicModel(&renderEntity, NULL, m_cachedDynamicModel); if (dynamicModel) { m_cachedDynamicModel = dynamicModel; drawModel = dynamicModel; } } glEnable(GL_DEPTH_TEST); DrawRenderModel(drawModel, false); //DrawRenderModel(drawModel, true); } void CModelRenderWnd::DrawSkeleton() { if (!m_modelDecl || !m_joints || !m_haveAnimFrame || m_numJoints <= 0) { return; } const int* parents = m_modelDecl->JointParents(); glDisable(GL_DEPTH_TEST); glLineWidth(2.0f); glColor3f(0.10f, 0.80f, 1.00f); glBegin(GL_LINES); for (int i = 0; i < m_numJoints; i++) { const int parent = parents ? parents[i] : -1; if (parent < 0 || parent >= m_numJoints) { continue; } const idVec3 a = m_joints[i].ToVec3(); const idVec3 b = m_joints[parent].ToVec3(); glVertex3fv(b.ToFloatPtr()); glVertex3fv(a.ToFloatPtr()); } glEnd(); glPointSize(4.0f); glBegin(GL_POINTS); for (int i = 0; i < m_numJoints; i++) { const idVec3 p = m_joints[i].ToVec3(); glVertex3fv(p.ToFloatPtr()); } glEnd(); glPointSize(1.0f); glLineWidth(1.0f); glEnable(GL_DEPTH_TEST); } void CModelRenderWnd::DrawOverlayText() { CRect client; GetClientRect(client); if (client.Width() <= 0 || client.Height() <= 0) { return; } glDisable(GL_DEPTH_TEST); glDisable(GL_TEXTURE_2D); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, client.Width(), client.Height(), 0, -1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); glColor3f(0.72f, 0.78f, 0.86f); glRasterPos2f(8.0f, 18.0f); CString text; if (m_modelDecl) { text.Format("%s", m_modelDecl->GetName()); } else { text = "No model decl selected"; } const char* textPtr = text.GetBuffer(0); glCallLists(text.GetLength(), GL_UNSIGNED_BYTE, textPtr); text.ReleaseBuffer(); if (!m_animDisplayName.IsEmpty()) { glRasterPos2f(8.0f, 36.0f); const char* animTextPtr = m_animDisplayName.GetBuffer(0); glCallLists(m_animDisplayName.GetLength(), GL_UNSIGNED_BYTE, animTextPtr); m_animDisplayName.ReleaseBuffer(); } glMatrixMode(GL_MODELVIEW); glPopMatrix(); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); } void CModelRenderWnd::OnLButtonDown(UINT nFlags, CPoint point) { m_dragging = TRUE; m_lastMouse = point; SetCapture(); SetFocus(); } void CModelRenderWnd::OnLButtonUp(UINT nFlags, CPoint point) { m_dragging = FALSE; if (GetCapture() == this) { ReleaseCapture(); } } void CModelRenderWnd::OnMouseMove(UINT nFlags, CPoint point) { if (m_dragging) { const int dx = point.x - m_lastMouse.x; const int dy = point.y - m_lastMouse.y; m_yaw += dx * 0.35f; m_pitch += dy * 0.35f; m_pitch = MV_ClampFloat(m_pitch, -89.0f, 89.0f); m_lastMouse = point; Invalidate(FALSE); } CWnd::OnMouseMove(nFlags, point); } BOOL CModelRenderWnd::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { const float scale = (zDelta > 0) ? 0.90f : 1.10f; m_distance *= scale; if (m_distance < m_radius * 0.50f) { m_distance = m_radius * 0.50f; } if (m_distance > m_radius * 20.0f) { m_distance = m_radius * 20.0f; } Invalidate(FALSE); return TRUE; } //============================================================================= // CModelViewDockWnd //============================================================================= IMPLEMENT_DYNAMIC(CModelViewDockWnd, CWnd) BEGIN_MESSAGE_MAP(CModelViewDockWnd, CWnd) ON_WM_CREATE() ON_WM_SIZE() ON_WM_ERASEBKGND() ON_WM_CTLCOLOR() ON_WM_DRAWITEM() ON_WM_MEASUREITEM() ON_WM_HSCROLL() ON_MESSAGE(WM_MODELVIEW_ANIMTIME_CHANGED, OnAnimTimeChanged) ON_CBN_SELCHANGE(ID_MODELVIEW_DECLCOMBO, OnDeclChanged) ON_LBN_SELCHANGE(ID_MODELVIEW_ANIMLIST, OnAnimChanged) ON_BN_CLICKED(ID_MODELVIEW_REFRESH, OnRefresh) ON_BN_CLICKED(ID_MODELVIEW_REIMPORT, OnReimportModel) ON_COMMAND(ID_MODELVIEW_FILE_NEWMD5DECL, OnNewMD5Decl) ON_COMMAND(ID_MODELVIEW_FILE_ADDANIM, OnAddAnimationFile) ON_BN_CLICKED(ID_MODELVIEW_SHOWSKELETON, OnShowSkeleton) ON_BN_CLICKED(ID_MODELVIEW_PLAY, OnPlay) ON_BN_CLICKED(ID_MODELVIEW_PAUSE, OnPause) ON_BN_CLICKED(ID_MODELVIEW_STOP, OnStop) END_MESSAGE_MAP() CModelViewDockWnd::CModelViewDockWnd() { m_hMenuBar = NULL; m_modelDecl = NULL; m_active = FALSE; m_inLayout = false; m_updatingTimeline = false; m_timelineLengthMS = -1; } CModelViewDockWnd::~CModelViewDockWnd() { ClearAnimationItems(); } BOOL CModelViewDockWnd::Create(CWnd* pParent, UINT nID) { CString className = AfxRegisterWndClass( CS_HREDRAW | CS_VREDRAW | CS_DBLCLKS, ::LoadCursor(NULL, IDC_ARROW), ModelViewPanelBrush(), NULL ); return CWnd::CreateEx( 0, className, "RadiantModelViewDockWnd", WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS, CRect(0, 0, 0, 0), pParent, nID ); } int CModelViewDockWnd::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) { return -1; } m_hMenuBar = MV_CreateModelViewMenuBar(GetSafeHwnd()); m_lblModel.Create("Model decl:", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_comboDecls.Create(WS_CHILD | WS_VISIBLE | WS_VSCROLL | CBS_DROPDOWNLIST | CBS_SORT | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS, CRect(0, 0, 0, 240), this, ID_MODELVIEW_DECLCOMBO); m_comboDecls.SetItemHeight(-1, 22); m_comboDecls.SetItemHeight(0, 22); m_btnRefresh.Create("Refresh", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_REFRESH); m_btnReimport.Create("Reimport", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_REIMPORT); m_chkSkeleton.Create("Show skeleton", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, CRect(0, 0, 0, 0), this, ID_MODELVIEW_SHOWSKELETON); m_lblMesh.Create("Mesh hierarchy", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_treeMesh.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS, CRect(0, 0, 0, 0), this, ID_MODELVIEW_MESHTREE); m_lblAnim.Create("Animations", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_listAnims.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | LBS_NOTIFY, CRect(0, 0, 0, 0), this, ID_MODELVIEW_ANIMLIST); if (!m_wndRender.Create(this, ID_MODELVIEW_RENDER)) { return -1; } m_transportPanel.Create("", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this, ID_MODELVIEW_TRANSPORT); m_btnPlay.Create("Play", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_PLAY); m_btnPause.Create("Pause", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_PAUSE); m_btnStop.Create("Stop", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_STOP); m_sliderTimeline.Create(WS_CHILD | WS_VISIBLE | TBS_HORZ | TBS_NOTICKS, CRect(0, 0, 0, 0), this, ID_MODELVIEW_TIMELINE); m_lblTime.Create("0:00.000 / 0:00.000", WS_CHILD | WS_VISIBLE | SS_RIGHT | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this, ID_MODELVIEW_TIMETEXT); SetTimelineRange(0); UpdateTransportControls(); ApplyDarkTheme(); PopulateModelDecls(); return 0; } void CModelViewDockWnd::ApplyDarkTheme() { MV_SubclassStatic(m_lblModel); MV_SubclassStatic(m_lblMesh); MV_SubclassStatic(m_lblAnim); MV_SubclassStatic(m_transportPanel); MV_SubclassStatic(m_lblTime); MV_SubclassButton(m_btnRefresh); MV_SubclassButton(m_btnReimport); MV_SubclassButton(m_chkSkeleton); MV_SubclassButton(m_btnPlay); MV_SubclassButton(m_btnPause); MV_SubclassButton(m_btnStop); MV_SubclassCombo(m_comboDecls); MV_ApplyNativeTheme(m_comboDecls.GetSafeHwnd()); MV_ApplyNativeTheme(m_treeMesh.GetSafeHwnd()); MV_ApplyNativeTheme(m_listAnims.GetSafeHwnd()); MV_ApplyNativeTheme(m_sliderTimeline.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnRefresh.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnReimport.GetSafeHwnd()); MV_ApplyNativeTheme(m_chkSkeleton.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnPlay.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnPause.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnStop.GetSafeHwnd()); if (m_treeMesh.GetSafeHwnd()) { m_treeMesh.SetBkColor(MV_DARK_INPUT); m_treeMesh.SetTextColor(MV_DARK_TEXT); ::SendMessage(m_treeMesh.GetSafeHwnd(), TVM_SETLINECOLOR, 0, (LPARAM)MV_DARK_BORDER); } } void CModelViewDockWnd::ClearAnimationItems() { for (int i = 0; i < m_animItems.GetSize(); i++) { modelViewAnimItem_t* item = reinterpret_cast(m_animItems[i]); delete item; } m_animItems.RemoveAll(); } void CModelViewDockWnd::SetActive(BOOL active) { m_active = active; m_wndRender.SetActive(active); } void CModelViewDockWnd::FocusModelView() { if (m_wndRender.GetSafeHwnd()) { m_wndRender.FocusRenderWindow(); } } void CModelViewDockWnd::PopulateModelDecls() { m_comboDecls.ResetContent(); if (!declManager) { return; } const int numDecls = declManager->GetNumDecls(DECL_MODELDEF); for (int i = 0; i < numDecls; i++) { const idDecl* decl = declManager->DeclByIndex(DECL_MODELDEF, i, false); if (!decl) { continue; } const int item = m_comboDecls.AddString(decl->GetName()); m_comboDecls.SetItemData(item, i); } if (m_comboDecls.GetCount() > 0) { m_comboDecls.SetCurSel(0); LoadSelectedModelDecl(); } } void CModelViewDockWnd::LoadSelectedModelDecl() { int sel = m_comboDecls.GetCurSel(); if (sel == CB_ERR || !declManager) { SetModelDecl(NULL); return; } CString declName; m_comboDecls.GetLBText(sel, declName); const idDecl* decl = declManager->FindType(DECL_MODELDEF, (LPCTSTR)declName, false); SetModelDecl((const idDeclModelDef*)(decl)); } CString CModelViewDockWnd::GetSelectedModelDeclFileName() { if (!declManager) { return ""; } CString declName; const int sel = m_comboDecls.GetCurSel(); if (sel != CB_ERR) { m_comboDecls.GetLBText(sel, declName); } else if (m_modelDecl && m_modelDecl->GetName()) { declName.Format("%s", m_modelDecl->GetName()); } declName = MV_Trimmed(declName); if (declName.IsEmpty()) { return ""; } const idDecl* decl = declManager->FindType(DECL_MODELDEF, (LPCTSTR)declName, false); if (!decl || decl->IsImplicit() || !decl->GetFileName() || !decl->GetFileName()[0]) { return ""; } CString declFileName; declFileName.Format("%s", decl->GetFileName()); return MV_Trimmed(declFileName); } void CModelViewDockWnd::SetModelDecl(const idDeclModelDef* modelDecl) { m_modelDecl = (idDeclModelDefInterface*)modelDecl; m_wndRender.SetModelDecl(modelDecl); PopulateMeshTree(); PopulateAnimationList(); if (m_listAnims.GetCount() > 0) { m_listAnims.SetCurSel(0); SelectAnimationItem(0); } else { m_wndRender.SetAnimation(NULL, 0, 0, ""); UpdateTransportControls(); } } void CModelViewDockWnd::PopulateMeshTree() { m_treeMesh.DeleteAllItems(); if (!m_modelDecl) { return; } CString rootText; rootText.Format("%s", m_modelDecl->GetName()); HTREEITEM root = m_treeMesh.InsertItem(rootText); CString modelText; modelText.Format("md5 mesh: %s", m_modelDecl->GetModelName()); m_treeMesh.InsertItem(modelText, root); CString jointCountText; jointCountText.Format("joints: %d", m_modelDecl->NumJoints()); HTREEITEM jointsRoot = m_treeMesh.InsertItem(jointCountText, root); const int numJoints = m_modelDecl->NumJoints(); const int* parents = m_modelDecl->JointParents(); HTREEITEM* jointItems = NULL; if (numJoints > 0) { jointItems = new HTREEITEM[numJoints]; memset(jointItems, 0, sizeof(HTREEITEM) * numJoints); } for (int i = 0; i < numJoints; i++) { CString jointText; jointText.Format("%03d %s", i, m_modelDecl->GetJointName(i)); HTREEITEM parentItem = jointsRoot; if (parents && parents[i] >= 0 && parents[i] < i && jointItems[parents[i]]) { parentItem = jointItems[parents[i]]; } jointItems[i] = m_treeMesh.InsertItem(jointText, parentItem); } delete[] jointItems; HTREEITEM surfacesRoot = m_treeMesh.InsertItem("surfaces", root); idRenderModel* model = NULL; if (gameEdit && m_modelDecl->GetModelName() && m_modelDecl->GetModelName()[0]) { model = gameEdit->ANIM_GetModelFromName(m_modelDecl->GetModelName()); } if (!model) { model = m_modelDecl->ModelHandle(); } if (model) { const int numSurfaces = model->NumSurfaces(); for (int i = 0; i < numSurfaces; i++) { const modelSurface_t* surface = model->Surface(i); CString surfaceText; if (surface && surface->shader) { surfaceText.Format("surface %03d %s", i, surface->shader->GetName()); } else { surfaceText.Format("surface %03d", i); } m_treeMesh.InsertItem(surfaceText, surfacesRoot); } } m_treeMesh.Expand(root, TVE_EXPAND); m_treeMesh.Expand(jointsRoot, TVE_EXPAND); m_treeMesh.Expand(surfacesRoot, TVE_EXPAND); } void CModelViewDockWnd::PopulateAnimationList() { m_listAnims.ResetContent(); ClearAnimationItems(); if (!m_modelDecl) { return; } const int numAnims = m_modelDecl->NumAnims(); for (int i = 0; i < numAnims; i++) { const idAnimInterface* declAnim = m_modelDecl->GetAnim(i); if (!declAnim) { continue; } const int syncedAnims = MV_MaxInt(1, declAnim->NumAnims()); for (int md5Index = 0; md5Index < syncedAnims; md5Index++) { const idMD5AnimInterface* md5Anim = (const idMD5AnimInterface*)declAnim->MD5Anim(md5Index); if (!md5Anim) { continue; } if (gameEdit && md5Anim->Name() && md5Anim->Name()[0]) { const idMD5AnimInterface* interfaceAnim = (const idMD5AnimInterface*)gameEdit->ANIM_GetAnim(md5Anim->Name()); if (interfaceAnim) { md5Anim = interfaceAnim; } } modelViewAnimItem_t* animItem = new modelViewAnimItem_t; animItem->declAnimIndex = i; animItem->md5AnimIndex = md5Index; animItem->declAnim = declAnim; animItem->md5Anim = md5Anim; animItem->lengthMS = gameEdit ? gameEdit->ANIM_GetLength((const idMD5Anim*)md5Anim) : 0; animItem->numFrames = gameEdit ? gameEdit->ANIM_GetNumFrames((const idMD5Anim*)md5Anim) : 0; if (syncedAnims > 1) { animItem->displayText.Format("%s [%d] (%d frames, %d ms)", declAnim->Name(), md5Index, animItem->numFrames, animItem->lengthMS); } else { animItem->displayText.Format("%s (%d frames, %d ms)", declAnim->Name(), animItem->numFrames, animItem->lengthMS); } m_animItems.Add(animItem); const int listItem = m_listAnims.AddString(animItem->displayText); m_listAnims.SetItemData(listItem, reinterpret_cast(animItem)); } } } void CModelViewDockWnd::SelectAnimationItem(int itemIndex) { if (itemIndex < 0 || itemIndex >= m_listAnims.GetCount()) { m_wndRender.SetAnimation(NULL, 0, 0, ""); UpdateTransportControls(); return; } modelViewAnimItem_t* animItem = reinterpret_cast(m_listAnims.GetItemData(itemIndex)); if (!animItem) { m_wndRender.SetAnimation(NULL, 0, 0, ""); UpdateTransportControls(); return; } m_wndRender.SetAnimation((const idMD5Anim*)animItem->md5Anim, animItem->lengthMS, animItem->numFrames, animItem->displayText); UpdateTransportControls(); } void CModelViewDockWnd::SelectModelDeclByName(const char* declName) { CString wanted = declName ? declName : ""; PopulateModelDecls(); if (wanted.IsEmpty()) { return; } int sel = m_comboDecls.FindStringExact(-1, wanted); if (sel != CB_ERR) { m_comboDecls.SetCurSel(sel); LoadSelectedModelDecl(); return; } if (declManager) { const idDecl* decl = declManager->FindType(DECL_MODELDEF, (LPCTSTR)wanted, false); if (decl) { SetModelDecl((const idDeclModelDef*)decl); } } } void CModelViewDockWnd::SelectAnimationByName(const char* animName) { if (!animName || !animName[0]) { return; } for (int i = 0; i < m_listAnims.GetCount(); i++) { modelViewAnimItem_t* animItem = reinterpret_cast(m_listAnims.GetItemData(i)); if (animItem && animItem->declAnim && animItem->declAnim->Name()) { if (idStr::Icmp(animItem->declAnim->Name(), animName) == 0) { m_listAnims.SetCurSel(i); SelectAnimationItem(i); return; } } CString display; m_listAnims.GetText(i, display); if (display.Find(animName) >= 0) { m_listAnims.SetCurSel(i); SelectAnimationItem(i); return; } } } void CModelViewDockWnd::SetTimelineRange(int animLengthMS) { m_timelineLengthMS = animLengthMS; const int maxPos = (animLengthMS > 0) ? MV_MaxInt(0, animLengthMS - 1) : 0; HWND hSlider = m_sliderTimeline.GetSafeHwnd(); if (!hSlider) { return; } ::SendMessage(hSlider, TBM_SETRANGEMIN, FALSE, 0); ::SendMessage(hSlider, TBM_SETRANGEMAX, TRUE, maxPos); ::SendMessage(hSlider, TBM_SETPAGESIZE, 0, MV_MaxInt(1, animLengthMS / 20)); ::SendMessage(hSlider, TBM_SETTICFREQ, MV_MaxInt(1, animLengthMS / 10), 0); } void CModelViewDockWnd::UpdateTimelinePosition(int animTimeMS, int animLengthMS) { HWND hSlider = m_sliderTimeline.GetSafeHwnd(); if (!hSlider) { return; } if (animLengthMS != m_timelineLengthMS) { SetTimelineRange(animLengthMS); } const int maxPos = (animLengthMS > 0) ? MV_MaxInt(0, animLengthMS - 1) : 0; const int pos = MV_ClampInt(animTimeMS, 0, maxPos); m_updatingTimeline = true; ::SendMessage(hSlider, TBM_SETPOS, TRUE, pos); m_updatingTimeline = false; } void CModelViewDockWnd::UpdateTimeLabel(int animTimeMS, int animLengthMS) { if (!m_lblTime.GetSafeHwnd()) { return; } CString current = MV_FormatAnimTime(animTimeMS); CString total = MV_FormatAnimTime(animLengthMS); CString text; text.Format("%s / %s", (LPCTSTR)current, (LPCTSTR)total); m_lblTime.SetWindowText(text); } void CModelViewDockWnd::UpdateTransportControls() { const BOOL hasAnim = m_wndRender.HasAnimation(); const BOOL canScrub = (hasAnim && m_wndRender.GetAnimationLengthMS() > 0) ? TRUE : FALSE; const BOOL playing = m_wndRender.IsPlaying(); const int animTimeMS = m_wndRender.GetAnimationTimeMS(); const int animLengthMS = m_wndRender.GetAnimationLengthMS(); if (m_btnPlay.GetSafeHwnd()) { m_btnPlay.EnableWindow(canScrub && !playing); } if (m_btnPause.GetSafeHwnd()) { m_btnPause.EnableWindow(canScrub && playing); } if (m_btnStop.GetSafeHwnd()) { m_btnStop.EnableWindow(canScrub && (playing || animTimeMS > 0)); } if (m_sliderTimeline.GetSafeHwnd()) { m_sliderTimeline.EnableWindow(canScrub); } UpdateTimelinePosition(animTimeMS, animLengthMS); UpdateTimeLabel(animTimeMS, animLengthMS); } void CModelViewDockWnd::OnMeasureItem(int nIDCtl, LPMEASUREITEMSTRUCT lpMeasureItemStruct) { if (nIDCtl == ID_MODELVIEW_DECLCOMBO && lpMeasureItemStruct) { lpMeasureItemStruct->itemHeight = 22; return; } CWnd::OnMeasureItem(nIDCtl, lpMeasureItemStruct); } void CModelViewDockWnd::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct) { if (nIDCtl == ID_MODELVIEW_DECLCOMBO && lpDrawItemStruct) { HDC hDC = lpDrawItemStruct->hDC; RECT rc = lpDrawItemStruct->rcItem; const bool selected = (lpDrawItemStruct->itemState & ODS_SELECTED) != 0; const bool disabled = (lpDrawItemStruct->itemState & ODS_DISABLED) != 0; const bool focus = (lpDrawItemStruct->itemState & ODS_FOCUS) != 0; MV_FillRect(hDC, rc, selected ? MV_DARK_MENU_HOT : MV_DARK_INPUT); if (focus) { MV_FrameRect(hDC, rc, MV_DARK_ACCENT); } char text[1024]; text[0] = '\0'; if (lpDrawItemStruct->itemID != (UINT)-1) { const int len = (int)m_comboDecls.SendMessage(CB_GETLBTEXTLEN, lpDrawItemStruct->itemID, 0); if (len >= 0 && len < (int)sizeof(text)) { m_comboDecls.SendMessage(CB_GETLBTEXT, lpDrawItemStruct->itemID, (LPARAM)text); text[sizeof(text) - 1] = '\0'; } } else if (m_comboDecls.GetSafeHwnd()) { m_comboDecls.GetWindowText(text, sizeof(text)); } RECT textRect = rc; textRect.left += 8; textRect.right -= 6; HFONT font = (HFONT)m_comboDecls.SendMessage(WM_GETFONT, 0, 0); HFONT oldFont = font ? (HFONT)::SelectObject(hDC, font) : NULL; ::SetBkMode(hDC, TRANSPARENT); ::SetTextColor(hDC, disabled ? RGB(101, 110, 126) : MV_DARK_TEXT); ::DrawTextA(hDC, text, -1, &textRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); if (oldFont) { ::SelectObject(hDC, oldFont); } return; } CWnd::OnDrawItem(nIDCtl, lpDrawItemStruct); } void CModelViewDockWnd::OnDeclChanged() { LoadSelectedModelDecl(); } void CModelViewDockWnd::OnAnimChanged() { const int sel = m_listAnims.GetCurSel(); if (sel == LB_ERR) { m_wndRender.SetAnimation(NULL, 0, 0, ""); UpdateTransportControls(); return; } SelectAnimationItem(sel); } void CModelViewDockWnd::OnRefresh() { CString previous; int oldSel = m_comboDecls.GetCurSel(); if (oldSel != CB_ERR) { m_comboDecls.GetLBText(oldSel, previous); } PopulateModelDecls(); if (!previous.IsEmpty()) { int newSel = m_comboDecls.FindStringExact(-1, previous); if (newSel != CB_ERR) { m_comboDecls.SetCurSel(newSel); LoadSelectedModelDecl(); } } } void CModelViewDockWnd::OnReimportModel() { CString declFileName = GetSelectedModelDeclFileName(); if (declFileName.IsEmpty()) { AfxMessageBox("Select a modelDef with a real decl file before reimporting.", MB_ICONEXCLAMATION | MB_OK); return; } MV_ExecuteExportModelsCommand((LPCTSTR)declFileName, "reimporting the selected modelDef"); } void CModelViewDockWnd::OnNewMD5Decl() { if (!declManager) { AfxMessageBox("The decl manager is not available.", MB_ICONERROR | MB_OK); return; } CString meshPath; if (!MV_ChooseMayaFile(this, "Select Maya Mesh For New MD5 Decl", meshPath)) { return; } CString declName = MV_BuildSafeNameFromAssetPath(meshPath); bool replaceExisting = false; CString existingFile; while (true) { if (!MV_PromptForText(this, "New MD5 Decl", "modelDef name:", declName)) { return; } const idDecl* existing = declManager->FindType(DECL_MODELDEF, (LPCTSTR)declName, false); if (!existing) { break; } CString message; message.Format("modelDef '%s' already exists in:\n%s\n\nReplace that decl with the selected Maya mesh?", (LPCTSTR)declName, existing->GetFileName()); const int choice = AfxMessageBox(message, MB_ICONQUESTION | MB_YESNOCANCEL); if (choice == IDYES) { replaceExisting = true; existingFile.Format("%s", existing->GetFileName()); break; } if (choice == IDCANCEL) { return; } } CString declFile = existingFile; bool createdNewFile = false; if (!replaceExisting) { if (!MV_ChooseDeclFile(this, declFile, createdNewFile)) { return; } if (createdNewFile && !declManager->CreateDeclFile((LPCTSTR)declFile, DECL_MODELDEF)) { CString message; message.Format("Could not create decl file:\n%s", (LPCTSTR)declFile); AfxMessageBox(message, MB_ICONERROR | MB_OK); return; } idDecl* newDecl = declManager->CreateNewDecl(DECL_MODELDEF, (LPCTSTR)declName, (LPCTSTR)declFile, true); if (!newDecl) { CString message; message.Format("Could not create modelDef '%s' in:\n%s", (LPCTSTR)declName, (LPCTSTR)declFile); AfxMessageBox(message, MB_ICONERROR | MB_OK); return; } } CString declText = MV_BuildModelDefText((LPCTSTR)declName, (LPCTSTR)meshPath); if (!declManager->SetDeclText(DECL_MODELDEF, (LPCTSTR)declName, (LPCTSTR)declText, true, true)) { CString message; message.Format("Could not save modelDef '%s'.", (LPCTSTR)declName); AfxMessageBox(message, MB_ICONERROR | MB_OK); return; } if (!declFile.IsEmpty()) { declManager->ReloadFile((LPCTSTR)declFile, true); } declManager->FindType(DECL_MODELDEF, (LPCTSTR)declName, true); MV_ExecuteExportModelsCommand((LPCTSTR)declFile, "creating a new MD5 modelDef"); SelectModelDeclByName((LPCTSTR)declName); CString message; message.Format("Created modelDef '%s' using mesh:\n%s", (LPCTSTR)declName, (LPCTSTR)meshPath); AfxMessageBox(message, MB_ICONINFORMATION | MB_OK); } void CModelViewDockWnd::OnAddAnimationFile() { if (!declManager) { AfxMessageBox("The decl manager is not available.", MB_ICONERROR | MB_OK); return; } if (!m_modelDecl) { AfxMessageBox("Select or create a modelDef before adding an animation.", MB_ICONEXCLAMATION | MB_OK); return; } CString animPath; if (!MV_ChooseMayaFile(this, "Select Maya Animation", animPath)) { return; } CString animName = MV_GetFileBaseName(animPath, true); for (int i = 0; i < animName.GetLength(); i++) { char c = animName[i]; const bool valid = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; if (!valid) { animName.SetAt(i, '_'); } } animName = MV_TrimNameTokenChars(animName); if (animName.IsEmpty()) { animName = "new_anim"; } if (!MV_PromptForText(this, "Add Animation", "animation name:", animName)) { return; } CString modelName; modelName.Format("%s", m_modelDecl->GetName()); const idDecl* decl = declManager->FindType(DECL_MODELDEF, (LPCTSTR)modelName, false); CString declFile; if (!decl || decl->IsImplicit()) { bool createdNewFile = false; if (!MV_ChooseDeclFile(this, declFile, createdNewFile)) { return; } if (createdNewFile && !declManager->CreateDeclFile((LPCTSTR)declFile, DECL_MODELDEF)) { CString message; message.Format("Could not create decl file:\n%s", (LPCTSTR)declFile); AfxMessageBox(message, MB_ICONERROR | MB_OK); return; } idDecl* newDecl = declManager->CreateNewDecl(DECL_MODELDEF, (LPCTSTR)modelName, (LPCTSTR)declFile, true); if (!newDecl) { CString message; message.Format("Could not create modelDef '%s'.", (LPCTSTR)modelName); AfxMessageBox(message, MB_ICONERROR | MB_OK); return; } decl = newDecl; } else { declFile.Format("%s", decl->GetFileName()); } CString declText = MV_ReadDeclText(decl); if (declText.IsEmpty()) { declText = MV_BuildModelDefText((LPCTSTR)modelName, m_modelDecl->GetModelName()); } CString newText = MV_InsertAnimIntoModelDefText(declText, (LPCTSTR)animName, (LPCTSTR)animPath); if (!declManager->SetDeclText(DECL_MODELDEF, (LPCTSTR)modelName, (LPCTSTR)newText, true, true)) { CString message; message.Format("Could not save animation '%s' into modelDef '%s'.", (LPCTSTR)animName, (LPCTSTR)modelName); AfxMessageBox(message, MB_ICONERROR | MB_OK); return; } if (!declFile.IsEmpty()) { declManager->ReloadFile((LPCTSTR)declFile, true); } declManager->FindType(DECL_MODELDEF, (LPCTSTR)modelName, true); MV_ExecuteExportModelsCommand((LPCTSTR)declFile, "adding a Maya animation to a modelDef"); SelectModelDeclByName((LPCTSTR)modelName); SelectAnimationByName((LPCTSTR)animName); CString message; message.Format("Added animation '%s' to modelDef '%s':\n%s", (LPCTSTR)animName, (LPCTSTR)modelName, (LPCTSTR)animPath); AfxMessageBox(message, MB_ICONINFORMATION | MB_OK); } void CModelViewDockWnd::OnShowSkeleton() { m_wndRender.SetShowSkeleton(m_chkSkeleton.GetCheck() == BST_CHECKED); } void CModelViewDockWnd::OnPlay() { m_wndRender.PlayAnimation(); UpdateTransportControls(); } void CModelViewDockWnd::OnPause() { m_wndRender.PauseAnimation(); UpdateTransportControls(); } void CModelViewDockWnd::OnStop() { m_wndRender.StopAnimation(); UpdateTransportControls(); } LRESULT CModelViewDockWnd::OnAnimTimeChanged(WPARAM wParam, LPARAM lParam) { UpdateTransportControls(); return 0; } void CModelViewDockWnd::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { if (pScrollBar && pScrollBar->GetSafeHwnd() == m_sliderTimeline.GetSafeHwnd()) { if (!m_updatingTimeline && m_wndRender.HasAnimation()) { const int pos = (int)::SendMessage(m_sliderTimeline.GetSafeHwnd(), TBM_GETPOS, 0, 0); m_wndRender.SetAnimationTimeMS(pos); UpdateTransportControls(); } return; } CWnd::OnHScroll(nSBCode, nPos, pScrollBar); } void CModelViewDockWnd::LayoutChildren() { if (!GetSafeHwnd() || m_inLayout) { return; } m_inLayout = true; CRect client; GetClientRect(client); if (client.Width() < 1 || client.Height() < 1) { m_inLayout = false; return; } const int margin = 6; const int gap = 6; const int menuH = MV_MenuBarPreferredHeight(); const int topH = 30; const int leftW = MV_MaxInt(260, MV_MinInt(380, client.Width() / 4)); const int labelH = 20; const int animH = MV_MaxInt(120, client.Height() / 4); if (m_hMenuBar && ::IsWindow(m_hMenuBar)) { ::MoveWindow(m_hMenuBar, client.left, client.top, client.Width(), menuH, TRUE); } CRect topRect(client.left + margin, client.top + menuH + margin, client.right - margin, client.top + menuH + margin + topH); int x = topRect.left; if (m_lblModel.GetSafeHwnd()) { m_lblModel.MoveWindow(x, topRect.top, 80, topH, TRUE); x += 84; } if (m_comboDecls.GetSafeHwnd()) { const int reimportW = 80; const int refreshW = 72; const int skeletonW = 140; const int reserveW = reimportW + gap + refreshW + gap + skeletonW; const int comboW = MV_MinInt(460, MV_MaxInt(120, topRect.right - x - reserveW)); m_comboDecls.MoveWindow(x, topRect.top + 2, comboW, 240, TRUE); x += comboW + gap; } if (m_btnReimport.GetSafeHwnd()) { m_btnReimport.MoveWindow(x, topRect.top + 2, 80, 24, TRUE); x += 80 + gap; } if (m_btnRefresh.GetSafeHwnd()) { m_btnRefresh.MoveWindow(x, topRect.top + 2, 72, 24, TRUE); x += 72 + gap; } if (m_chkSkeleton.GetSafeHwnd()) { m_chkSkeleton.MoveWindow(x, topRect.top + 2, 140, 24, TRUE); } CRect content(client.left + margin, topRect.bottom + margin, client.right - margin, client.bottom - margin); CRect left(content.left, content.top, content.left + leftW, content.bottom); CRect render(content.left + leftW + gap, content.top, content.right, content.bottom); const int transportH = (render.Height() > 96) ? 58 : MV_MaxInt(36, render.Height() / 3); CRect preview(render.left, render.top, render.right, MV_MaxInt(render.top, render.bottom - transportH - gap)); CRect transport(render.left, preview.bottom + gap, render.right, render.bottom); if (m_lblMesh.GetSafeHwnd()) { m_lblMesh.MoveWindow(left.left, left.top, left.Width(), labelH, TRUE); } if (m_treeMesh.GetSafeHwnd()) { m_treeMesh.MoveWindow(left.left, left.top + labelH, left.Width(), MV_MaxInt(40, left.Height() - animH - labelH * 2 - gap), TRUE); } if (m_lblAnim.GetSafeHwnd()) { m_lblAnim.MoveWindow(left.left, left.bottom - animH - labelH, left.Width(), labelH, TRUE); } if (m_listAnims.GetSafeHwnd()) { m_listAnims.MoveWindow(left.left, left.bottom - animH, left.Width(), animH, TRUE); } if (m_wndRender.GetSafeHwnd()) { m_wndRender.MoveWindow(preview, TRUE); } if (m_transportPanel.GetSafeHwnd()) { m_transportPanel.MoveWindow(transport, TRUE); } CRect transportInner(transport); transportInner.DeflateRect(8, 8); const int btnW = 52; const int btnH = 24; const int btnY = transportInner.top + MV_MaxInt(0, (transportInner.Height() - btnH) / 2); int tx = transportInner.left; if (m_btnPlay.GetSafeHwnd()) { m_btnPlay.MoveWindow(tx, btnY, btnW, btnH, TRUE); tx += btnW + gap; } if (m_btnPause.GetSafeHwnd()) { m_btnPause.MoveWindow(tx, btnY, btnW, btnH, TRUE); tx += btnW + gap; } if (m_btnStop.GetSafeHwnd()) { m_btnStop.MoveWindow(tx, btnY, btnW, btnH, TRUE); tx += btnW + gap + 2; } const int timeW = (transportInner.Width() < 480) ? 108 : 128; const int sliderLeft = tx; const int sliderRight = MV_MaxInt(sliderLeft, transportInner.right - timeW - gap); const int sliderW = MV_MaxInt(20, sliderRight - sliderLeft); if (m_sliderTimeline.GetSafeHwnd()) { m_sliderTimeline.MoveWindow(sliderLeft, btnY + 1, sliderW, btnH, TRUE); } if (m_lblTime.GetSafeHwnd()) { m_lblTime.MoveWindow(sliderRight + gap, btnY, timeW, btnH, TRUE); } m_inLayout = false; } void CModelViewDockWnd::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy); LayoutChildren(); } BOOL CModelViewDockWnd::OnEraseBkgnd(CDC* pDC) { CRect client; GetClientRect(client); pDC->FillSolidRect(client, MV_DARK_BG); CRect rc; if (m_treeMesh.GetSafeHwnd()) { m_treeMesh.GetWindowRect(rc); ScreenToClient(rc); rc.InflateRect(1, 1); MV_FrameRect(pDC, rc, MV_DARK_BORDER); } if (m_listAnims.GetSafeHwnd()) { m_listAnims.GetWindowRect(rc); ScreenToClient(rc); rc.InflateRect(1, 1); MV_FrameRect(pDC, rc, MV_DARK_BORDER); } if (m_comboDecls.GetSafeHwnd()) { m_comboDecls.GetWindowRect(rc); ScreenToClient(rc); rc.InflateRect(1, 1); MV_FrameRect(pDC, rc, MV_DARK_BORDER); } if (m_transportPanel.GetSafeHwnd()) { m_transportPanel.GetWindowRect(rc); ScreenToClient(rc); rc.InflateRect(1, 1); MV_FrameRect(pDC, rc, MV_DARK_BORDER); } return TRUE; } HBRUSH CModelViewDockWnd::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { HBRUSH brush = CWnd::OnCtlColor(pDC, pWnd, nCtlColor); if (!pDC) { return brush; } switch (nCtlColor) { case CTLCOLOR_STATIC: pDC->SetBkColor(MV_DARK_PANEL); pDC->SetTextColor(MV_DARK_TEXT); return ModelViewPanelBrush(); case CTLCOLOR_LISTBOX: case CTLCOLOR_EDIT: pDC->SetBkColor(MV_DARK_INPUT); pDC->SetTextColor(MV_DARK_TEXT); return ModelViewInputBrush(); case CTLCOLOR_BTN: pDC->SetBkColor(MV_DARK_PANEL); pDC->SetTextColor(MV_DARK_TEXT); return ModelViewPanelBrush(); default: break; } return brush; }