/* =========================================================================== 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" #include "../../models/Model_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_RedrawControl(HWND hWnd) { if (!hWnd) { return; } ::RedrawWindow(hWnd, NULL, NULL, RDW_INVALIDATE | RDW_ERASE | RDW_FRAME); } 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_BindAlbedoForMaterial(const idMaterial* shader) { if (!shader) { MV_BindNullTexture(); return false; } const shaderStage_t* stage = MV_FindAlbedoStage(shader); idImage* image = MV_GetImageFromStage(stage); if (!image) { MV_BindNullTexture(); return false; } GL_SelectTexture(0); glEnable(GL_TEXTURE_2D); image->Bind(); return true; } static idImage* MV_GetPreviewImageForMaterial(const idMaterial* shader) { if (!shader) { return NULL; } const shaderStage_t* stage = MV_FindAlbedoStage(shader); idImage* image = MV_GetImageFromStage(stage); if (image) { return image; } return shader->GetEditorImage(); } //============================================================================= // ModelView decl creation helpers //============================================================================= #define MV_TEXTPROMPT_CLASS "IceTechModelViewTextPrompt" #define MV_TEXTPROMPT_EDIT 1001 #define MV_MATERIALPICKER_CLASS "IceTechModelViewMaterialPicker" #define MV_MATERIALPREVIEW_CLASS "IceTechModelViewMaterialPreview" #define MV_MATERIALPICKER_FILTER 1101 #define MV_MATERIALPICKER_LIST 1102 #define MV_MATERIALPICKER_PREVIEW 1103 #define MV_MATERIALPICKER_NAME 1104 struct modelViewTextPrompt_t { CString title; CString label; CString text; bool accepted; HWND hEdit; HWND hOwner; }; struct modelViewMaterialPicker_t { CString title; CString filter; CString selectedName; bool accepted; HWND hOwner; HWND hWnd; HWND hFilter; HWND hList; HWND hPreview; HWND hName; HDC hPreviewDC; HGLRC hPreviewRC; const idMaterial* selectedMaterial; }; 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_FormatFloat(float value) { CString text; text.Format("%.6g", value); 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 const idMaterial* MV_MaterialPickerMaterialFromList(modelViewMaterialPicker_t* picker, int sel) { if (!picker || !picker->hList || sel == LB_ERR || !declManager) { return NULL; } const int declIndex = (int)::SendMessage(picker->hList, LB_GETITEMDATA, sel, 0); if (declIndex < 0) { return NULL; } return static_cast(declManager->DeclByIndex(DECL_MATERIAL, declIndex, true)); } static void MV_DrawMaterialPreview(modelViewMaterialPicker_t* picker) { if (!picker || !picker->hPreview) { return; } RECT rc; ::GetClientRect(picker->hPreview, &rc); const int w = rc.right - rc.left; const int h = rc.bottom - rc.top; if (w <= 0 || h <= 0) { return; } if (!picker->hPreviewDC || !picker->hPreviewRC) { HDC hDC = ::GetDC(picker->hPreview); MV_FillRect(hDC, rc, MV_DARK_INPUT); MV_FrameRect(hDC, rc, MV_DARK_BORDER); ::ReleaseDC(picker->hPreview, hDC); return; } HDC oldDC = wglGetCurrentDC(); HGLRC oldRC = (HGLRC)wglGetCurrentContext(); wglMakeCurrent(picker->hPreviewDC, picker->hPreviewRC); glViewport(0, 0, w, h); glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0.0, (GLdouble)w, (GLdouble)h, 0.0, -1.0, 1.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glClearColor(14.0f / 255.0f, 16.0f / 255.0f, 20.0f / 255.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); idImage* image = MV_GetPreviewImageForMaterial(picker->selectedMaterial); if (image) { GL_SelectTexture(0); glEnable(GL_TEXTURE_2D); image->Bind(); glColor3f(1.0f, 1.0f, 1.0f); const float iw = (image->uploadWidth > 0) ? (float)image->uploadWidth : 128.0f; const float ih = (image->uploadHeight > 0) ? (float)image->uploadHeight : 128.0f; const float availW = (float)MV_MaxInt(1, w - 24); const float availH = (float)MV_MaxInt(1, h - 24); const float scaleX = availW / iw; const float scaleY = availH / ih; const float scale = (scaleX < scaleY) ? scaleX : scaleY; float drawW = iw * scale; float drawH = ih * scale; if (drawW < 1.0f || drawH < 1.0f) { drawW = availW; drawH = availH; } const float x0 = ((float)w - drawW) * 0.5f; const float y0 = ((float)h - drawH) * 0.5f; const float x1 = x0 + drawW; const float y1 = y0 + drawH; glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); glVertex2f(x0, y0); glTexCoord2f(1.0f, 0.0f); glVertex2f(x1, y0); glTexCoord2f(1.0f, 1.0f); glVertex2f(x1, y1); glTexCoord2f(0.0f, 1.0f); glVertex2f(x0, y1); glEnd(); } else { glDisable(GL_TEXTURE_2D); glColor3f(45.0f / 255.0f, 50.0f / 255.0f, 58.0f / 255.0f); glBegin(GL_QUADS); glVertex2f(12.0f, 12.0f); glVertex2f((float)w - 12.0f, 12.0f); glVertex2f((float)w - 12.0f, (float)h - 12.0f); glVertex2f(12.0f, (float)h - 12.0f); glEnd(); } glDisable(GL_TEXTURE_2D); glColor3f(58.0f / 255.0f, 64.0f / 255.0f, 72.0f / 255.0f); glBegin(GL_LINE_LOOP); glVertex2f(0.5f, 0.5f); glVertex2f((float)w - 0.5f, 0.5f); glVertex2f((float)w - 0.5f, (float)h - 0.5f); glVertex2f(0.5f, (float)h - 0.5f); glEnd(); ::SwapBuffers(picker->hPreviewDC); if (oldRC) { wglMakeCurrent(oldDC, oldRC); } else { wglMakeCurrent(NULL, NULL); } } static LRESULT CALLBACK MVMaterialPreviewWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { modelViewMaterialPicker_t* picker = (modelViewMaterialPicker_t*)::GetWindowLongPtr(hWnd, GWLP_USERDATA); switch (uMsg) { case WM_CREATE: { CREATESTRUCT* cs = (CREATESTRUCT*)lParam; picker = (modelViewMaterialPicker_t*)cs->lpCreateParams; ::SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)picker); if (picker) { picker->hPreviewDC = ::GetDC(hWnd); if (picker->hPreviewDC) { QEW_SetupPixelFormat(picker->hPreviewDC, false); picker->hPreviewRC = (HGLRC)wglCreateContext(picker->hPreviewDC); } } return 0; } case WM_PAINT: { PAINTSTRUCT ps; ::BeginPaint(hWnd, &ps); MV_DrawMaterialPreview(picker); ::EndPaint(hWnd, &ps); return 0; } case WM_ERASEBKGND: return 1; case WM_DESTROY: if (picker) { if (picker->hPreviewRC) { wglMakeCurrent(NULL, NULL); wglDeleteContext(picker->hPreviewRC); picker->hPreviewRC = NULL; } if (picker->hPreviewDC) { ::ReleaseDC(hWnd, picker->hPreviewDC); picker->hPreviewDC = NULL; } } return 0; } return ::DefWindowProc(hWnd, uMsg, wParam, lParam); } static void MV_UpdateMaterialPickerSelection(modelViewMaterialPicker_t* picker) { if (!picker || !picker->hList) { return; } const int sel = (int)::SendMessage(picker->hList, LB_GETCURSEL, 0, 0); picker->selectedMaterial = MV_MaterialPickerMaterialFromList(picker, sel); picker->selectedName = picker->selectedMaterial ? picker->selectedMaterial->GetName() : ""; if (picker->hName) { CString label = picker->selectedName; if (label.IsEmpty()) { label = "No material selected"; } ::SetWindowTextA(picker->hName, (LPCSTR)label); } if (picker->hPreview) { ::InvalidateRect(picker->hPreview, NULL, FALSE); } } static void MV_PopulateMaterialPickerList(modelViewMaterialPicker_t* picker) { if (!picker || !picker->hList || !declManager) { return; } char filterBuffer[256]; filterBuffer[0] = '\0'; if (picker->hFilter) { ::GetWindowTextA(picker->hFilter, filterBuffer, sizeof(filterBuffer)); filterBuffer[sizeof(filterBuffer) - 1] = '\0'; } CString filter = MV_Trimmed(filterBuffer); filter.MakeLower(); ::SendMessage(picker->hList, LB_RESETCONTENT, 0, 0); int exactSel = LB_ERR; const int count = declManager->GetNumDecls(DECL_MATERIAL); for (int i = 0; i < count; i++) { const idDecl* decl = declManager->DeclByIndex(DECL_MATERIAL, i, false); if (!decl || !decl->GetName() || !decl->GetName()[0]) { continue; } CString name = decl->GetName(); CString lower = name; lower.MakeLower(); if (!filter.IsEmpty() && lower.Find(filter) < 0) { continue; } const int item = (int)::SendMessageA(picker->hList, LB_ADDSTRING, 0, (LPARAM)(LPCSTR)name); if (item >= 0) { ::SendMessage(picker->hList, LB_SETITEMDATA, item, i); if (!picker->selectedName.IsEmpty() && name.CompareNoCase(picker->selectedName) == 0) { exactSel = item; } } } const int listCount = (int)::SendMessage(picker->hList, LB_GETCOUNT, 0, 0); if (listCount > 0) { ::SendMessage(picker->hList, LB_SETCURSEL, exactSel != LB_ERR ? exactSel : 0, 0); } MV_UpdateMaterialPickerSelection(picker); } static void MV_AcceptMaterialPicker(modelViewMaterialPicker_t* picker) { MV_UpdateMaterialPickerSelection(picker); if (!picker || !picker->selectedMaterial) { return; } picker->accepted = true; ::DestroyWindow(picker->hWnd); } static LRESULT CALLBACK MVMaterialPickerWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { modelViewMaterialPicker_t* picker = (modelViewMaterialPicker_t*)::GetWindowLongPtr(hWnd, GWLP_USERDATA); switch (uMsg) { case WM_CREATE: { CREATESTRUCT* cs = (CREATESTRUCT*)lParam; picker = (modelViewMaterialPicker_t*)cs->lpCreateParams; ::SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)picker); if (picker) { picker->hWnd = hWnd; } HWND hFilterLabel = ::CreateWindowExA(0, "STATIC", "Filter", WS_CHILD | WS_VISIBLE | SS_LEFT, 12, 12, 332, 18, hWnd, NULL, AfxGetInstanceHandle(), NULL); picker->hFilter = ::CreateWindowExA(WS_EX_CLIENTEDGE, "EDIT", "", WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL, 12, 32, 332, 22, hWnd, (HMENU)MV_MATERIALPICKER_FILTER, AfxGetInstanceHandle(), NULL); picker->hList = ::CreateWindowExA(WS_EX_CLIENTEDGE, "LISTBOX", "", WS_CHILD | WS_VISIBLE | WS_VSCROLL | LBS_NOTIFY | LBS_HASSTRINGS, 12, 62, 332, 360, hWnd, (HMENU)MV_MATERIALPICKER_LIST, AfxGetInstanceHandle(), NULL); HWND hPreviewLabel = ::CreateWindowExA(0, "STATIC", "Preview", WS_CHILD | WS_VISIBLE | SS_LEFT, 358, 12, 332, 18, hWnd, NULL, AfxGetInstanceHandle(), NULL); picker->hPreview = ::CreateWindowExA(WS_EX_CLIENTEDGE, MV_MATERIALPREVIEW_CLASS, "", WS_CHILD | WS_VISIBLE, 358, 32, 332, 332, hWnd, (HMENU)MV_MATERIALPICKER_PREVIEW, AfxGetInstanceHandle(), picker); picker->hName = ::CreateWindowExA(0, "STATIC", "", WS_CHILD | WS_VISIBLE | SS_LEFT, 358, 374, 332, 40, hWnd, (HMENU)MV_MATERIALPICKER_NAME, AfxGetInstanceHandle(), NULL); HWND hOK = ::CreateWindowExA(0, "BUTTON", "OK", WS_CHILD | WS_VISIBLE | BS_DEFPUSHBUTTON, 524, 432, 78, 24, hWnd, (HMENU)IDOK, AfxGetInstanceHandle(), NULL); HWND hCancel = ::CreateWindowExA(0, "BUTTON", "Cancel", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 612, 432, 78, 24, hWnd, (HMENU)IDCANCEL, AfxGetInstanceHandle(), NULL); MV_SetDefaultGuiFont(hFilterLabel); MV_SetDefaultGuiFont(picker->hFilter); MV_SetDefaultGuiFont(picker->hList); MV_SetDefaultGuiFont(hPreviewLabel); MV_SetDefaultGuiFont(picker->hName); MV_SetDefaultGuiFont(hOK); MV_SetDefaultGuiFont(hCancel); MV_PopulateMaterialPickerList(picker); ::SetFocus(picker->hFilter); return 0; } case WM_COMMAND: if (LOWORD(wParam) == MV_MATERIALPICKER_FILTER && HIWORD(wParam) == EN_CHANGE) { MV_PopulateMaterialPickerList(picker); return 0; } if (LOWORD(wParam) == MV_MATERIALPICKER_LIST && HIWORD(wParam) == LBN_SELCHANGE) { MV_UpdateMaterialPickerSelection(picker); return 0; } if (LOWORD(wParam) == MV_MATERIALPICKER_LIST && HIWORD(wParam) == LBN_DBLCLK) { MV_AcceptMaterialPicker(picker); return 0; } if (LOWORD(wParam) == IDOK) { MV_AcceptMaterialPicker(picker); 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_RegisterMaterialPickerClasses() { static ATOM pickerAtom = 0; static ATOM previewAtom = 0; if (pickerAtom && previewAtom) { return pickerAtom; } if (!previewAtom) { WNDCLASSA wcPreview; memset(&wcPreview, 0, sizeof(wcPreview)); wcPreview.style = CS_OWNDC; wcPreview.lpfnWndProc = MVMaterialPreviewWndProc; wcPreview.hInstance = AfxGetInstanceHandle(); wcPreview.hCursor = ::LoadCursor(NULL, IDC_ARROW); wcPreview.hbrBackground = ModelViewInputBrush(); wcPreview.lpszClassName = MV_MATERIALPREVIEW_CLASS; previewAtom = ::RegisterClassA(&wcPreview); } if (!pickerAtom) { WNDCLASSA wcPicker; memset(&wcPicker, 0, sizeof(wcPicker)); wcPicker.lpfnWndProc = MVMaterialPickerWndProc; wcPicker.hInstance = AfxGetInstanceHandle(); wcPicker.hCursor = ::LoadCursor(NULL, IDC_ARROW); wcPicker.hbrBackground = ModelViewPanelBrush(); wcPicker.lpszClassName = MV_MATERIALPICKER_CLASS; pickerAtom = ::RegisterClassA(&wcPicker); } return (pickerAtom && previewAtom) ? pickerAtom : 0; } static bool MV_PickMaterial(CWnd* parent, CString& materialName) { if (!declManager || !MV_RegisterMaterialPickerClasses()) { return false; } modelViewMaterialPicker_t picker; picker.title = "Select Surface Material"; picker.filter.Empty(); picker.selectedName = materialName; picker.accepted = false; picker.hOwner = parent ? parent->GetSafeHwnd() : NULL; picker.hWnd = NULL; picker.hFilter = NULL; picker.hList = NULL; picker.hPreview = NULL; picker.hName = NULL; picker.hPreviewDC = NULL; picker.hPreviewRC = NULL; picker.selectedMaterial = NULL; HWND hWnd = ::CreateWindowExA( WS_EX_DLGMODALFRAME, MV_MATERIALPICKER_CLASS, (LPCSTR)picker.title, WS_POPUP | WS_CAPTION | WS_SYSMENU, CW_USEDEFAULT, CW_USEDEFAULT, 720, 500, picker.hOwner, NULL, AfxGetInstanceHandle(), &picker ); if (!hWnd) { return false; } if (picker.hOwner) { RECT ownerRect; RECT wndRect; ::GetWindowRect(picker.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(picker.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 (picker.hOwner) { ::EnableWindow(picker.hOwner, TRUE); ::SetActiveWindow(picker.hOwner); } if (!picker.accepted || picker.selectedName.IsEmpty()) { return false; } materialName = picker.selectedName; return true; } 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 importStaticMeshItem; modelViewMenuItemData_t exportMD5StaticItem; 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->importStaticMeshItem, ID_MODELVIEW_FILE_IMPORTSTATICMESH, "Import Static Mesh..."); MV_InitMenuItemData(data->exportMD5StaticItem, ID_MODELVIEW_FILE_EXPORTMD5STATIC, "Export MD5Static..."); 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_IMPORTSTATICMESH, (LPCSTR)&data->importStaticMeshItem); ::AppendMenuA(data->hFileMenu, MF_OWNERDRAW | MF_ENABLED, ID_MODELVIEW_FILE_EXPORTMD5STATIC, (LPCSTR)&data->exportMD5StaticItem); ::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_playerModelDecl = NULL; m_playerModel = NULL; m_cachedPlayerModel = NULL; m_joints = NULL; m_playerJoints = NULL; m_numJoints = 0; m_numPlayerJoints = 0; m_haveAnimFrame = FALSE; m_havePlayerFrame = FALSE; m_anim = NULL; m_standaloneDisplayName.Empty(); m_animLengthMS = 0; m_animFrames = 0; m_animTimeMS = 0; m_lastTick = 0; m_showSkeleton = FALSE; m_showPlayerMesh = 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; ResetStaticTransform(); } CModelRenderWnd::~CModelRenderWnd() { ShutdownGL(); ResetCachedDynamicModel(); ResetCachedPlayerModel(); FreeJointBuffer(); FreePlayerJointBuffer(); } 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::FreePlayerJointBuffer() { delete[] m_playerJoints; m_playerJoints = NULL; m_numPlayerJoints = 0; m_havePlayerFrame = 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::ResetCachedPlayerModel() { if (m_cachedPlayerModel && renderModelManager) { renderModelManager->FreeModel(m_cachedPlayerModel); } m_cachedPlayerModel = 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::ResolvePlayerReferenceModel() { if (m_playerModelDecl && m_playerModel) { return; } m_playerModelDecl = NULL; m_playerModel = NULL; if (!declManager) { return; } const idDecl* decl = declManager->FindType(DECL_MODELDEF, "model_sp_marine", true); if (!decl) { decl = declManager->FindType(DECL_MODELDEF, "player_doommarine", true); } if (!decl) { return; } m_playerModelDecl = (const idDeclModelDefInterface*)decl; const char* modelName = m_playerModelDecl->GetModelName(); if (gameEdit && modelName && modelName[0]) { m_playerModel = gameEdit->ANIM_GetModelFromName(modelName); } if (!m_playerModel) { m_playerModel = m_playerModelDecl->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::UpdatePlayerReferenceFrame(BOOL forceResetCachedModel) { m_havePlayerFrame = FALSE; if (forceResetCachedModel) { ResetCachedPlayerModel(); } ResolvePlayerReferenceModel(); if (!m_playerModelDecl || !m_playerModel) { FreePlayerJointBuffer(); return; } const int numJoints = m_playerModel->NumJoints(); if (numJoints <= 0 || !m_playerModel->GetDefaultPose()) { FreePlayerJointBuffer(); return; } if (!m_playerJoints || m_numPlayerJoints != numJoints) { FreePlayerJointBuffer(); m_numPlayerJoints = numJoints; m_playerJoints = new idJointMat[m_numPlayerJoints]; } const idJointQuat* pose = m_playerModel->GetDefaultPose(); SIMDProcessor->ConvertJointQuatsToJointMats(m_playerJoints, pose, m_numPlayerJoints); m_playerJoints[0].SetTranslation(pose[0].t + m_playerModelDecl->GetVisualOffset()); const int* parents = m_playerModelDecl->JointParents(); if (parents && m_numPlayerJoints > 1) { SIMDProcessor->TransformJoints(m_playerJoints, parents, 1, m_numPlayerJoints - 1); } m_havePlayerFrame = TRUE; } void CModelRenderWnd::SetModelDecl(const idDeclModelDef* modelDecl) { m_modelDecl = (idDeclModelDefInterface*)modelDecl; m_model = NULL; m_anim = NULL; m_animDisplayName.Empty(); m_standaloneDisplayName.Empty(); ClearSurfaceMaterialOverrides(); ResetStaticTransform(); m_animLengthMS = 0; m_animFrames = 0; m_animTimeMS = 0; m_playing = FALSE; m_lastTick = ::GetTickCount(); ResetCachedDynamicModel(); ResetCachedPlayerModel(); ResolveModelFromGameEdit(); if (m_showPlayerMesh) { UpdatePlayerReferenceFrame(TRUE); } AllocateJointBuffer(); UpdateAnimationFrame(FALSE); FitCameraToModel(); Invalidate(FALSE); NotifyAnimationTimeChanged(); } void CModelRenderWnd::SetStandaloneModel(idRenderModel* model, const char* displayName) { m_modelDecl = NULL; m_model = model; m_anim = NULL; m_animDisplayName.Empty(); m_standaloneDisplayName = displayName ? displayName : ""; ClearSurfaceMaterialOverrides(); ResetStaticTransform(); m_animLengthMS = 0; m_animFrames = 0; m_animTimeMS = 0; m_playing = FALSE; m_lastTick = ::GetTickCount(); ResetCachedDynamicModel(); ResetCachedPlayerModel(); if (m_showPlayerMesh) { UpdatePlayerReferenceFrame(TRUE); } FreeJointBuffer(); 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; } idRenderModel* CModelRenderWnd::CurrentModel() const { return m_model; } const idMaterial* CModelRenderWnd::SurfaceMaterialForSurface(int surfaceNum, const idMaterial* fallback) const { const idMaterial* material = SurfaceMaterialOverride(surfaceNum); return material ? material : fallback; } const idMaterial* CModelRenderWnd::SurfaceMaterialOverride(int surfaceNum) const { if (surfaceNum < 0 || surfaceNum >= m_surfaceMaterialOverrides.Num()) { return NULL; } return m_surfaceMaterialOverrides[surfaceNum]; } void CModelRenderWnd::SetSurfaceMaterialOverride(int surfaceNum, const idMaterial* material) { if (surfaceNum < 0 || !material) { return; } if (surfaceNum >= m_surfaceMaterialOverrides.Num()) { const int oldNum = m_surfaceMaterialOverrides.Num(); m_surfaceMaterialOverrides.SetNum(surfaceNum + 1); for (int i = oldNum; i < m_surfaceMaterialOverrides.Num(); i++) { m_surfaceMaterialOverrides[i] = NULL; } } m_surfaceMaterialOverrides[surfaceNum] = material; Invalidate(FALSE); } void CModelRenderWnd::ClearSurfaceMaterialOverride(int surfaceNum) { if (surfaceNum < 0 || surfaceNum >= m_surfaceMaterialOverrides.Num()) { return; } m_surfaceMaterialOverrides[surfaceNum] = NULL; Invalidate(FALSE); } void CModelRenderWnd::ClearSurfaceMaterialOverrides() { m_surfaceMaterialOverrides.Clear(); Invalidate(FALSE); } void CModelRenderWnd::SetStaticTransform(const idVec3& translate, const idVec3& rotate, const idVec3& scale) { m_staticTranslate = translate; m_staticRotate = rotate; m_staticScale = scale; Invalidate(FALSE); } void CModelRenderWnd::ResetStaticTransform() { m_staticTranslate.Zero(); m_staticRotate.Zero(); m_staticScale.Set(1.0f, 1.0f, 1.0f); } bool CModelRenderWnd::InvertCurrentModelNormals() { if (!m_model || m_model->IsDynamicModel() != DM_STATIC) { return false; } HDC oldDC = wglGetCurrentDC(); HGLRC oldRC = (HGLRC)wglGetCurrentContext(); const bool madeCurrent = (m_hDC && m_hGLRC && wglMakeCurrent(m_hDC, m_hGLRC)) ? true : false; bool inverted = false; const int surfaceCount = m_model->NumSurfaces(); for (int s = 0; s < surfaceCount; s++) { const modelSurface_t* surface = m_model->Surface(s); if (!surface || !surface->geometry) { continue; } srfTriangles_t* tri = surface->geometry; if (!tri->verts || !tri->indexes || tri->numVerts <= 0 || tri->numIndexes < 3 || (tri->numIndexes % 3) != 0) { continue; } if (madeCurrent) { R_FreeStaticTriSurfVertexCaches(tri); } R_ReverseTriangles(tri); if (tri->facePlanes) { R_DeriveFacePlanes(tri); } inverted = true; } if (oldRC) { wglMakeCurrent(oldDC, oldRC); } else { wglMakeCurrent(NULL, NULL); } if (inverted) { Invalidate(FALSE); } return inverted; } bool CModelRenderWnd::WriteCurrentMD5Static(const char* fileName) const { idRenderModelStatic* staticModel = dynamic_cast(m_model); if (!staticModel) { return false; } return staticModel->WriteMD5Static(fileName, &m_surfaceMaterialOverrides, &m_staticTranslate, &m_staticRotate, &m_staticScale); } 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::SetShowPlayerMesh(BOOL showPlayerMesh) { m_showPlayerMesh = showPlayerMesh; if (m_showPlayerMesh) { UpdatePlayerReferenceFrame(TRUE); } else { ResetCachedPlayerModel(); } FitCameraToModel(); 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; } } if (m_showPlayerMesh) { ResolvePlayerReferenceModel(); if (m_playerModel) { idBounds bounds = model ? model->Bounds(NULL) : idBounds(idVec3(-16.0f, -16.0f, 0.0f), idVec3(16.0f, 16.0f, 72.0f)); idBounds playerBounds = m_playerModel->Bounds(NULL); playerBounds.TranslateSelf(PlayerReferenceOffset(model)); bounds.AddBounds(playerBounds); 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; } } idVec3 CModelRenderWnd::PlayerReferenceOffset(idRenderModel* drawModel) const { idBounds modelBounds = drawModel ? drawModel->Bounds(NULL) : idBounds(idVec3(-16.0f, -16.0f, 0.0f), idVec3(16.0f, 16.0f, 72.0f)); idBounds playerBounds = m_playerModel ? m_playerModel->Bounds(NULL) : idBounds(idVec3(-16.0f, -16.0f, 0.0f), idVec3(16.0f, 16.0f, 72.0f)); const float spacing = 32.0f; idVec3 offset; offset.x = modelBounds[1].x - playerBounds[0].x + spacing; offset.y = 0.0f; offset.z = modelBounds[0].z - playerBounds[0].z; return offset; } 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, bool useMaterialOverrides, bool applyStaticTransform) { if (!model) { return; } const bool transformStaticModel = applyStaticTransform && (model->IsDynamicModel() == DM_STATIC); if (transformStaticModel) { glPushMatrix(); glTranslatef(m_staticTranslate.x, m_staticTranslate.y, m_staticTranslate.z); glRotatef(m_staticRotate.z, 0.0f, 0.0f, 1.0f); glRotatef(m_staticRotate.y, 0.0f, 1.0f, 0.0f); glRotatef(m_staticRotate.x, 1.0f, 0.0f, 0.0f); glScalef(m_staticScale.x, m_staticScale.y, m_staticScale.z); } 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; const idMaterial* shader = useMaterialOverrides ? SurfaceMaterialForSurface(s, surface->shader) : surface->shader; if (!wireOnly) { textured = MV_BindAlbedoForMaterial(shader); 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()); } glNormal3fv(tri->verts[index].normal.ToFloatPtr()); glVertex3fv(tri->verts[index].xyz.ToFloatPtr()); } glEnd(); } MV_BindNullTexture(); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); if (transformStaticModel) { glPopMatrix(); } } void CModelRenderWnd::DrawPlayerReferenceModel(idRenderModel* drawModel) { if (!m_showPlayerMesh) { return; } UpdatePlayerReferenceFrame(FALSE); if (!m_playerModel || !m_havePlayerFrame || !m_playerJoints || m_numPlayerJoints <= 0) { return; } renderEntity_t renderEntity; memset(&renderEntity, 0, sizeof(renderEntity)); renderEntity.hModel = m_playerModel; renderEntity.axis.Identity(); renderEntity.origin.Zero(); renderEntity.customSkin = m_playerModelDecl ? m_playerModelDecl->GetSkin() : NULL; renderEntity.numJoints = m_numPlayerJoints; renderEntity.joints = m_playerJoints; idRenderModel* dynamicModel = m_playerModel->InstantiateDynamicModel(&renderEntity, NULL, m_cachedPlayerModel); if (dynamicModel) { m_cachedPlayerModel = dynamicModel; } idRenderModel* playerDrawModel = dynamicModel ? dynamicModel : m_playerModel; glPushMatrix(); idVec3 offset = PlayerReferenceOffset(drawModel); glTranslatef(offset.x, offset.y, offset.z); glColor3f(0.78f, 0.88f, 1.0f); DrawRenderModel(playerDrawModel, false, false, false); glPopMatrix(); } 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); DrawPlayerReferenceModel(drawModel); //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 if (!m_standaloneDisplayName.IsEmpty()) { text = m_standaloneDisplayName; } 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_LBN_SELCHANGE(ID_MODELVIEW_SURFACEMATS, OnSurfaceMaterialChanged) ON_LBN_DBLCLK(ID_MODELVIEW_SURFACEMATS, OnSetSurfaceMaterial) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_TX, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_TY, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_TZ, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_RX, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_RY, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_RZ, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_SX, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_SY, OnStaticTransformChanged) ON_EN_CHANGE(ID_MODELVIEW_TRANSFORM_SZ, OnStaticTransformChanged) ON_BN_CLICKED(ID_MODELVIEW_REFRESH, OnRefresh) ON_BN_CLICKED(ID_MODELVIEW_REIMPORT, OnReimportModel) ON_BN_CLICKED(ID_MODELVIEW_SETMATERIAL, OnSetSurfaceMaterial) ON_BN_CLICKED(ID_MODELVIEW_CLEARMATERIAL, OnClearSurfaceMaterial) ON_COMMAND(ID_MODELVIEW_FILE_NEWMD5DECL, OnNewMD5Decl) ON_COMMAND(ID_MODELVIEW_FILE_IMPORTSTATICMESH, OnImportStaticMesh) ON_COMMAND(ID_MODELVIEW_FILE_EXPORTMD5STATIC, OnExportMD5Static) ON_COMMAND(ID_MODELVIEW_FILE_ADDANIM, OnAddAnimationFile) ON_BN_CLICKED(ID_MODELVIEW_SHOWSKELETON, OnShowSkeleton) ON_BN_CLICKED(ID_MODELVIEW_SHOWPLAYERMESH, OnShowPlayerMesh) ON_BN_CLICKED(ID_MODELVIEW_INVERTNORMALS, OnInvertNormals) 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_importedModel = NULL; m_importedIsOBJ = false; m_importedIsMD5Static = false; m_active = FALSE; m_inLayout = false; m_updatingTimeline = false; m_updatingStaticTransform = false; m_timelineLengthMS = -1; } CModelViewDockWnd::~CModelViewDockWnd() { ClearImportedModel(); 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_chkPlayerMesh.Create("Show player mesh", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX, CRect(0, 0, 0, 0), this, ID_MODELVIEW_SHOWPLAYERMESH); m_btnInvertNormals.Create("Invert normals", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_INVERTNORMALS); 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_lblStaticTransform.Create("Static transform", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_lblTransformCols[0].Create("X", WS_CHILD | WS_VISIBLE | SS_CENTER, CRect(0, 0, 0, 0), this); m_lblTransformCols[1].Create("Y", WS_CHILD | WS_VISIBLE | SS_CENTER, CRect(0, 0, 0, 0), this); m_lblTransformCols[2].Create("Z", WS_CHILD | WS_VISIBLE | SS_CENTER, CRect(0, 0, 0, 0), this); m_lblTransformRows[0].Create("Loc", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_lblTransformRows[1].Create("Rot", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_lblTransformRows[2].Create("Scl", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { const UINT id = ID_MODELVIEW_TRANSFORM_TX + row * 3 + col; m_editTransform[row][col].Create(WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL, CRect(0, 0, 0, 0), this, id); } } m_lblSurfaceMaterials.Create("Surface materials", WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(0, 0, 0, 0), this); m_listSurfaceMaterials.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_VSCROLL | LBS_NOTIFY, CRect(0, 0, 0, 0), this, ID_MODELVIEW_SURFACEMATS); m_btnSetSurfaceMaterial.Create("Set", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_SETMATERIAL); m_btnClearSurfaceMaterial.Create("Clear", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, CRect(0, 0, 0, 0), this, ID_MODELVIEW_CLEARMATERIAL); 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); ResetStaticTransformControls(); UpdateStaticTransformControls(); UpdateTransportControls(); ApplyDarkTheme(); PopulateModelDecls(); return 0; } void CModelViewDockWnd::ApplyDarkTheme() { MV_SubclassStatic(m_lblModel); MV_SubclassStatic(m_lblMesh); MV_SubclassStatic(m_lblStaticTransform); for (int i = 0; i < 3; i++) { MV_SubclassStatic(m_lblTransformCols[i]); MV_SubclassStatic(m_lblTransformRows[i]); } MV_SubclassStatic(m_lblSurfaceMaterials); 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_chkPlayerMesh); MV_SubclassButton(m_btnInvertNormals); MV_SubclassButton(m_btnSetSurfaceMaterial); MV_SubclassButton(m_btnClearSurfaceMaterial); 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()); for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { MV_ApplyNativeTheme(m_editTransform[row][col].GetSafeHwnd()); } } MV_ApplyNativeTheme(m_listSurfaceMaterials.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_chkPlayerMesh.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnInvertNormals.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnSetSurfaceMaterial.GetSafeHwnd()); MV_ApplyNativeTheme(m_btnClearSurfaceMaterial.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::RedrawDarkControls() { RedrawWindow(NULL, NULL, RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN); MV_RedrawControl(m_lblModel.GetSafeHwnd()); MV_RedrawControl(m_comboDecls.GetSafeHwnd()); MV_RedrawControl(m_btnRefresh.GetSafeHwnd()); MV_RedrawControl(m_btnReimport.GetSafeHwnd()); MV_RedrawControl(m_chkSkeleton.GetSafeHwnd()); MV_RedrawControl(m_chkPlayerMesh.GetSafeHwnd()); MV_RedrawControl(m_btnInvertNormals.GetSafeHwnd()); MV_RedrawControl(m_lblMesh.GetSafeHwnd()); MV_RedrawControl(m_treeMesh.GetSafeHwnd()); MV_RedrawControl(m_lblStaticTransform.GetSafeHwnd()); for (int i = 0; i < 3; i++) { MV_RedrawControl(m_lblTransformCols[i].GetSafeHwnd()); MV_RedrawControl(m_lblTransformRows[i].GetSafeHwnd()); } for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { MV_RedrawControl(m_editTransform[row][col].GetSafeHwnd()); } } MV_RedrawControl(m_lblSurfaceMaterials.GetSafeHwnd()); MV_RedrawControl(m_listSurfaceMaterials.GetSafeHwnd()); MV_RedrawControl(m_btnSetSurfaceMaterial.GetSafeHwnd()); MV_RedrawControl(m_btnClearSurfaceMaterial.GetSafeHwnd()); MV_RedrawControl(m_lblAnim.GetSafeHwnd()); MV_RedrawControl(m_listAnims.GetSafeHwnd()); MV_RedrawControl(m_transportPanel.GetSafeHwnd()); MV_RedrawControl(m_btnPlay.GetSafeHwnd()); MV_RedrawControl(m_btnPause.GetSafeHwnd()); MV_RedrawControl(m_btnStop.GetSafeHwnd()); MV_RedrawControl(m_sliderTimeline.GetSafeHwnd()); MV_RedrawControl(m_lblTime.GetSafeHwnd()); } 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); if (active) { ApplyDarkTheme(); RedrawDarkControls(); Invalidate(FALSE); } } 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() { ClearImportedModel(); 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)); } void CModelViewDockWnd::ClearImportedModel() { if (m_importedModel) { if (m_wndRender.GetSafeHwnd() && m_wndRender.CurrentModel() == m_importedModel) { m_wndRender.SetStandaloneModel(NULL, ""); } delete m_importedModel; m_importedModel = NULL; } m_importedModelPath.Empty(); m_importedIsOBJ = false; m_importedIsMD5Static = false; } 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); ResetStaticTransformControls(); UpdateStaticTransformControls(); PopulateMeshTree(); PopulateSurfaceMaterialList(); PopulateAnimationList(); if (m_listAnims.GetCount() > 0) { m_listAnims.SetCurSel(0); SelectAnimationItem(0); } else { m_wndRender.SetAnimation(NULL, 0, 0, ""); UpdateTransportControls(); } } void CModelViewDockWnd::PopulateStandaloneModelTree(idRenderModel* model, const char* displayName) { m_treeMesh.DeleteAllItems(); m_listAnims.ResetContent(); ClearAnimationItems(); CString rootText = displayName ? displayName : "Imported static model"; HTREEITEM root = m_treeMesh.InsertItem(rootText); if (!model) { return; } CString modelText; modelText.Format("static mesh: %s", model->Name()); m_treeMesh.InsertItem(modelText, root); CString surfaceCountText; surfaceCountText.Format("surfaces: %d", model->NumSurfaces()); HTREEITEM surfacesRoot = m_treeMesh.InsertItem(surfaceCountText, root); for (int i = 0; i < model->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(surfacesRoot, TVE_EXPAND); ResetStaticTransformControls(); UpdateStaticTransformControls(); PopulateSurfaceMaterialList(); 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); } bool CModelViewDockWnd::CurrentPreviewModelIsStatic() const { idRenderModel* model = CurrentPreviewModel(); return model && model->IsDynamicModel() == DM_STATIC; } void CModelViewDockWnd::ResetStaticTransformControls() { m_updatingStaticTransform = true; const float defaults[3][3] = { { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }, { 1.0f, 1.0f, 1.0f } }; for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { if (m_editTransform[row][col].GetSafeHwnd()) { m_editTransform[row][col].SetWindowText(MV_FormatFloat(defaults[row][col])); } } } m_wndRender.ResetStaticTransform(); m_updatingStaticTransform = false; } static float MV_ReadEditFloat(CEdit& edit, float fallback) { CString text; edit.GetWindowText(text); text = MV_Trimmed(text); if (text.IsEmpty()) { return fallback; } return (float)atof((LPCTSTR)text); } void CModelViewDockWnd::ReadStaticTransformControls() { if (!CurrentPreviewModelIsStatic()) { m_wndRender.ResetStaticTransform(); return; } idVec3 translate; idVec3 rotate; idVec3 scale; translate.x = MV_ReadEditFloat(m_editTransform[0][0], 0.0f); translate.y = MV_ReadEditFloat(m_editTransform[0][1], 0.0f); translate.z = MV_ReadEditFloat(m_editTransform[0][2], 0.0f); rotate.x = MV_ReadEditFloat(m_editTransform[1][0], 0.0f); rotate.y = MV_ReadEditFloat(m_editTransform[1][1], 0.0f); rotate.z = MV_ReadEditFloat(m_editTransform[1][2], 0.0f); scale.x = MV_ReadEditFloat(m_editTransform[2][0], 1.0f); scale.y = MV_ReadEditFloat(m_editTransform[2][1], 1.0f); scale.z = MV_ReadEditFloat(m_editTransform[2][2], 1.0f); m_wndRender.SetStaticTransform(translate, rotate, scale); } void CModelViewDockWnd::UpdateStaticTransformControls() { const BOOL show = CurrentPreviewModelIsStatic() ? TRUE : FALSE; if (m_lblStaticTransform.GetSafeHwnd()) { m_lblStaticTransform.ShowWindow(show ? SW_SHOW : SW_HIDE); } for (int i = 0; i < 3; i++) { if (m_lblTransformCols[i].GetSafeHwnd()) { m_lblTransformCols[i].ShowWindow(show ? SW_SHOW : SW_HIDE); } if (m_lblTransformRows[i].GetSafeHwnd()) { m_lblTransformRows[i].ShowWindow(show ? SW_SHOW : SW_HIDE); } } for (int row = 0; row < 3; row++) { for (int col = 0; col < 3; col++) { if (m_editTransform[row][col].GetSafeHwnd()) { m_editTransform[row][col].ShowWindow(show ? SW_SHOW : SW_HIDE); m_editTransform[row][col].EnableWindow(show); } } } if (!show) { m_wndRender.ResetStaticTransform(); } if (m_btnInvertNormals.GetSafeHwnd()) { m_btnInvertNormals.EnableWindow(show); } if (!m_inLayout) { LayoutChildren(); } } idRenderModel* CModelViewDockWnd::CurrentPreviewModel() const { if (m_importedModel) { return m_importedModel; } if (m_modelDecl) { 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(); } return model; } return m_wndRender.CurrentModel(); } void CModelViewDockWnd::PopulateSurfaceMaterialList() { if (!m_listSurfaceMaterials.GetSafeHwnd()) { return; } m_listSurfaceMaterials.ResetContent(); idRenderModel* model = CurrentPreviewModel(); if (!model) { m_btnSetSurfaceMaterial.EnableWindow(FALSE); m_btnClearSurfaceMaterial.EnableWindow(FALSE); return; } for (int i = 0; i < model->NumSurfaces(); i++) { const modelSurface_t* surface = model->Surface(i); const idMaterial* original = (surface && surface->shader) ? surface->shader : NULL; const idMaterial* overrideMaterial = m_wndRender.SurfaceMaterialOverride(i); CString text; if (overrideMaterial && original && idStr::Icmp(overrideMaterial->GetName(), original->GetName()) != 0) { text.Format("%03d %s -> %s", i, original->GetName(), overrideMaterial->GetName()); } else { const char* materialName = original ? original->GetName() : "_default"; text.Format("%03d %s", i, materialName); } const int item = m_listSurfaceMaterials.AddString(text); m_listSurfaceMaterials.SetItemData(item, (DWORD_PTR)i); } const BOOL hasSurfaces = m_listSurfaceMaterials.GetCount() > 0; m_btnSetSurfaceMaterial.EnableWindow(hasSurfaces); m_btnClearSurfaceMaterial.EnableWindow(hasSurfaces); if (hasSurfaces && m_listSurfaceMaterials.GetCurSel() == LB_ERR) { m_listSurfaceMaterials.SetCurSel(0); } } 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::OnSurfaceMaterialChanged() { const int sel = m_listSurfaceMaterials.GetCurSel(); const BOOL hasSurface = (sel != LB_ERR); m_btnSetSurfaceMaterial.EnableWindow(hasSurface); m_btnClearSurfaceMaterial.EnableWindow(hasSurface); } void CModelViewDockWnd::OnStaticTransformChanged() { if (m_updatingStaticTransform) { return; } ReadStaticTransformControls(); } void CModelViewDockWnd::OnSetSurfaceMaterial() { const int sel = m_listSurfaceMaterials.GetCurSel(); if (sel == LB_ERR || !declManager) { return; } const int surfaceNum = (int)m_listSurfaceMaterials.GetItemData(sel); idRenderModel* model = CurrentPreviewModel(); if (!model || surfaceNum < 0 || surfaceNum >= model->NumSurfaces()) { return; } const modelSurface_t* surface = model->Surface(surfaceNum); const idMaterial* current = m_wndRender.SurfaceMaterialForSurface(surfaceNum, surface ? surface->shader : NULL); CString materialName = current ? current->GetName() : "_default"; if (!MV_PickMaterial(this, materialName)) { return; } const idMaterial* material = declManager->FindMaterial((LPCTSTR)materialName, false); if (!material) { CString message; message.Format("Could not find material:\n%s", (LPCTSTR)materialName); AfxMessageBox(message, MB_ICONERROR | MB_OK); return; } m_wndRender.SetSurfaceMaterialOverride(surfaceNum, material); PopulateSurfaceMaterialList(); m_listSurfaceMaterials.SetCurSel(sel); OnSurfaceMaterialChanged(); } void CModelViewDockWnd::OnClearSurfaceMaterial() { const int sel = m_listSurfaceMaterials.GetCurSel(); if (sel == LB_ERR) { return; } const int surfaceNum = (int)m_listSurfaceMaterials.GetItemData(sel); m_wndRender.ClearSurfaceMaterialOverride(surfaceNum); PopulateSurfaceMaterialList(); m_listSurfaceMaterials.SetCurSel(sel); OnSurfaceMaterialChanged(); } 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::OnImportStaticMesh() { CFileDialog dlg(TRUE, "obj", NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST, "Static Meshes (*.obj;*.md5static)|*.obj;*.md5static|Wavefront OBJ Files (*.obj)|*.obj|MD5 Static Meshes (*.md5static)|*.md5static|All Files (*.*)|*.*||", this); dlg.m_ofn.lpstrTitle = "Import Static Mesh For Preview"; if (dlg.DoModal() != IDOK) { return; } idStr meshPath = (LPCTSTR)dlg.GetPathName(); idStr extension; meshPath.ExtractFileExtension(extension); const bool isOBJ = (extension.Icmp("obj") == 0); const bool isMD5Static = (extension.Icmp(MD5_STATIC_EXT) == 0); if (!isOBJ && !isMD5Static) { AfxMessageBox("Static mesh import supports OBJ and md5static files.", MB_ICONEXCLAMATION | MB_OK); return; } idRenderModelStatic* model = new idRenderModelStatic; if (isOBJ) { const int axisChoice = AfxMessageBox( "Convert this OBJ from Y-up to Doom/Radiant axes?\n\n" "Yes: convert common Y-up OBJ exports.\n" "No: keep the OBJ coordinates as-is.", MB_ICONQUESTION | MB_YESNOCANCEL ); if (axisChoice == IDCANCEL) { delete model; return; } model->InitEmpty(dlg.GetPathName()); if (!model->LoadOBJ(dlg.GetPathName(), axisChoice == IDYES)) { delete model; AfxMessageBox("Could not import that OBJ.", MB_ICONERROR | MB_OK); return; } model->FinishSurfaces(); } else { model->InitFromFile(dlg.GetPathName()); if (model->IsDefaultModel()) { delete model; AfxMessageBox("Could not import that md5static mesh.", MB_ICONERROR | MB_OK); return; } } ClearImportedModel(); m_importedModel = model; m_importedModelPath = dlg.GetPathName(); m_importedIsOBJ = isOBJ; m_importedIsMD5Static = isMD5Static; m_modelDecl = NULL; m_comboDecls.SetCurSel(-1); m_wndRender.SetStandaloneModel(m_importedModel, (LPCTSTR)m_importedModelPath); PopulateStandaloneModelTree(m_importedModel, (LPCTSTR)m_importedModelPath); } void CModelViewDockWnd::OnExportMD5Static() { idRenderModel* model = NULL; bool exportable = false; CString sourceName; if (m_importedModel && (m_importedIsOBJ || m_importedIsMD5Static)) { model = m_importedModel; exportable = true; sourceName = m_importedModelPath; } else if (m_modelDecl && m_modelDecl->GetModelName()) { idStr modelName = m_modelDecl->GetModelName(); idStr extension; modelName.ExtractFileExtension(extension); if (extension.Icmp(MD5_STATIC_EXT) == 0) { model = m_modelDecl->ModelHandle(); exportable = true; sourceName = m_modelDecl->GetModelName(); } } if (!exportable || !model) { AfxMessageBox("Export is only available for an imported static mesh or the current md5static mesh.", MB_ICONEXCLAMATION | MB_OK); return; } if (model->IsDynamicModel() != DM_STATIC) { AfxMessageBox("Skeletal and dynamic models cannot be exported as md5static.", MB_ICONEXCLAMATION | MB_OK); return; } if (!dynamic_cast(model)) { AfxMessageBox("The current model is not a static render model.", MB_ICONEXCLAMATION | MB_OK); return; } ReadStaticTransformControls(); CString defaultName = MV_GetFileBaseName(sourceName, true); if (defaultName.IsEmpty()) { defaultName = "static_mesh"; } defaultName += "."; defaultName += MD5_STATIC_EXT; CFileDialog dlg(FALSE, MD5_STATIC_EXT, defaultName, OFN_HIDEREADONLY | OFN_OVERWRITEPROMPT, "MD5 Static Meshes (*.md5static)|*.md5static|All Files (*.*)|*.*||", this); dlg.m_ofn.lpstrTitle = "Export MD5Static"; if (dlg.DoModal() != IDOK) { return; } CString outPath = MV_EnsureExtension(dlg.GetPathName(), ".md5static"); if (!m_wndRender.WriteCurrentMD5Static(outPath)) { AfxMessageBox("Could not write the md5static file.", MB_ICONERROR | MB_OK); return; } CString message; message.Format("Exported md5static mesh:\n%s", (LPCTSTR)outPath); 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::OnShowPlayerMesh() { m_wndRender.SetShowPlayerMesh(m_chkPlayerMesh.GetCheck() == BST_CHECKED); } void CModelViewDockWnd::OnInvertNormals() { if (!CurrentPreviewModelIsStatic()) { AfxMessageBox("Invert normals is only available for static preview meshes.", MB_ICONEXCLAMATION | MB_OK); return; } if (!m_wndRender.InvertCurrentModelNormals()) { AfxMessageBox("Could not invert normals for the current mesh.", MB_ICONERROR | MB_OK); } } 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); const int surfaceButtonH = 24; 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 = 124; const int playerMeshW = 154; const int invertNormalsW = 102; const int reserveW = reimportW + gap + refreshW + gap + skeletonW + gap + playerMeshW + gap + invertNormalsW; 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, 124, 24, TRUE); x += 124 + gap; } if (m_chkPlayerMesh.GetSafeHwnd()) { m_chkPlayerMesh.MoveWindow(x, topRect.top + 2, 154, 24, TRUE); x += 154 + gap; } if (m_btnInvertNormals.GetSafeHwnd()) { m_btnInvertNormals.MoveWindow(x, topRect.top + 2, 102, 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 BOOL showStaticTransform = CurrentPreviewModelIsStatic() ? TRUE : FALSE; const int surfaceH = showStaticTransform ? MV_MaxInt(64, MV_MinInt(120, left.Height() / 6)) : MV_MaxInt(96, MV_MinInt(150, left.Height() / 5)); const int transformRowH = 22; const int transformColH = 16; const int transformH = showStaticTransform ? (labelH + transformColH + transformRowH * 3 + gap * 3) : 0; 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); const int animTop = left.bottom - animH - labelH; const int surfaceBlockBottom = animTop - gap; const int surfaceButtonsTop = surfaceBlockBottom - surfaceButtonH; const int surfaceListTop = MV_MaxInt(left.top + labelH + 40, surfaceButtonsTop - surfaceH); const int surfaceLabelTop = surfaceListTop - labelH; const int treeTop = left.top + labelH; const int transformBottom = surfaceLabelTop - gap; const int transformTop = showStaticTransform ? MV_MaxInt(left.top + labelH + 40, transformBottom - transformH) : transformBottom; const int treeBottom = MV_MaxInt(treeTop + 40, (showStaticTransform ? transformTop : surfaceLabelTop) - gap); if (m_lblMesh.GetSafeHwnd()) { m_lblMesh.MoveWindow(left.left, left.top, left.Width(), labelH, TRUE); } if (m_treeMesh.GetSafeHwnd()) { m_treeMesh.MoveWindow(left.left, treeTop, left.Width(), treeBottom - treeTop, TRUE); } if (showStaticTransform) { if (m_lblStaticTransform.GetSafeHwnd()) { m_lblStaticTransform.MoveWindow(left.left, transformTop, left.Width(), labelH, TRUE); } const int rowLabelW = 34; const int editGap = 4; const int editW = MV_MaxInt(36, (left.Width() - rowLabelW - editGap * 4) / 3); const int colsTop = transformTop + labelH; for (int col = 0; col < 3; col++) { const int editLeft = left.left + rowLabelW + editGap + col * (editW + editGap); if (m_lblTransformCols[col].GetSafeHwnd()) { m_lblTransformCols[col].MoveWindow(editLeft, colsTop, editW, transformColH, TRUE); } } for (int row = 0; row < 3; row++) { const int rowTop = colsTop + transformColH + gap + row * (transformRowH + editGap); if (m_lblTransformRows[row].GetSafeHwnd()) { m_lblTransformRows[row].MoveWindow(left.left, rowTop, rowLabelW, transformRowH, TRUE); } for (int col = 0; col < 3; col++) { const int editLeft = left.left + rowLabelW + editGap + col * (editW + editGap); if (m_editTransform[row][col].GetSafeHwnd()) { m_editTransform[row][col].MoveWindow(editLeft, rowTop, editW, transformRowH, TRUE); } } } } if (m_lblSurfaceMaterials.GetSafeHwnd()) { m_lblSurfaceMaterials.MoveWindow(left.left, surfaceLabelTop, left.Width(), labelH, TRUE); } if (m_listSurfaceMaterials.GetSafeHwnd()) { m_listSurfaceMaterials.MoveWindow(left.left, surfaceListTop, left.Width(), MV_MaxInt(24, surfaceButtonsTop - surfaceListTop - gap), TRUE); } const int materialButtonW = (left.Width() - gap) / 2; if (m_btnSetSurfaceMaterial.GetSafeHwnd()) { m_btnSetSurfaceMaterial.MoveWindow(left.left, surfaceButtonsTop, materialButtonW, surfaceButtonH, TRUE); } if (m_btnClearSurfaceMaterial.GetSafeHwnd()) { m_btnClearSurfaceMaterial.MoveWindow(left.left + materialButtonW + gap, surfaceButtonsTop, left.Width() - materialButtonW - gap, surfaceButtonH, TRUE); } if (m_lblAnim.GetSafeHwnd()) { m_lblAnim.MoveWindow(left.left, animTop, 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(); RedrawDarkControls(); Invalidate(FALSE); } 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_listSurfaceMaterials.GetSafeHwnd()) { m_listSurfaceMaterials.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; }