// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2024 Shai Seger * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include "GuiDisplay.h" #include "OpenGlWrapper.h" #include "MillSimulation.h" #include #include #include #include using namespace MillSim; // clang-format off // NOLINTBEGIN(*-magic-numbers) GuiItem guiItems[] = { {.name=eGuiItemSlider, .vbo=0, .vao=0, .sx=28, .sy=-80, .actionKey=0, .hidden=false, .flags=0}, {.name=eGuiItemThumb, .vbo=0, .vao=0, .sx=328, .sy=-94, .actionKey=1, .hidden=false, .flags=0}, {.name=eGuiItemPause, .vbo=0, .vao=0, .sx=28, .sy=-50, .actionKey='P', .hidden=true, .flags=0}, {.name=eGuiItemPlay, .vbo=0, .vao=0, .sx=28, .sy=-50, .actionKey='S', .hidden=false, .flags=0}, {.name=eGuiItemSingleStep, .vbo=0, .vao=0, .sx=68, .sy=-50, .actionKey='T', .hidden=false, .flags=0}, {.name=eGuiItemSlower, .vbo=0, .vao=0, .sx=113, .sy=-50, .actionKey=' ', .hidden=false, .flags=0}, {.name=eGuiItemFaster, .vbo=0, .vao=0, .sx=158, .sy=-50, .actionKey='F', .hidden=false, .flags=0}, {.name=eGuiItemX, .vbo=0, .vao=0, .sx=208, .sy=-45, .actionKey=0, .hidden=false, .flags=0}, {.name=eGuiItem1, .vbo=0, .vao=0, .sx=230, .sy=-50, .actionKey=0, .hidden=false, .flags=0}, {.name=eGuiItem5, .vbo=0, .vao=0, .sx=230, .sy=-50, .actionKey=0, .hidden=true, .flags=0}, {.name=eGuiItem10, .vbo=0, .vao=0, .sx=230, .sy=-50, .actionKey=0, .hidden=true, .flags=0}, {.name=eGuiItem25, .vbo=0, .vao=0, .sx=230, .sy=-50, .actionKey=0, .hidden=true, .flags=0}, {.name=eGuiItem50, .vbo=0, .vao=0, .sx=230, .sy=-50, .actionKey=0, .hidden=true, .flags=0}, {.name=eGuiItemRotate, .vbo=0, .vao=0, .sx=-140, .sy=-50, .actionKey=' ', .hidden=false, .flags=GUIITEM_CHECKABLE}, {.name=eGuiItemPath, .vbo=0, .vao=0, .sx=-100, .sy=-50, .actionKey='L', .hidden=false, .flags=GUIITEM_CHECKABLE}, {.name=eGuiItemAmbientOclusion, .vbo=0, .vao=0, .sx=-60, .sy=-50, .actionKey='A', .hidden=false, .flags=GUIITEM_CHECKABLE}, {.name=eGuiItemView, .vbo=0, .vao=0, .sx=-180, .sy=-50, .actionKey='V', .hidden=false, .flags=0}, {.name=eGuiItemHome, .vbo=0, .vao=0, .sx=-220, .sy=-50, .actionKey='H', .hidden=false, .flags=0}, }; // NOLINTEND(*-magic-numbers) // clang-format on #define NUM_GUI_ITEMS (sizeof(guiItems) / sizeof(GuiItem)) #define TEX_SIZE 256 std::vector guiFileNames = { "Slider.png", "Thumb.png", "Pause.png", "Play.png", "SingleStep.png", "Slower.png", "Faster.png", "x.png", "1.png", "5.png", "10.png", "25.png", "50.png", "Rotate.png", "Path.png", "AmbientOclusion.png", "View.png", "Home.png" }; void GuiDisplay::UpdateProjection() { mat4x4 projmat; // mat4x4 viewmat; mat4x4_ortho(projmat, 0, gWindowSizeW, gWindowSizeH, 0, -1, 1); mShader.Activate(); mShader.UpdateProjectionMat(projmat); mThumbMaxMotion = guiItems[eGuiItemAmbientOclusion].posx() + guiItems[eGuiItemAmbientOclusion].texItem.w - guiItems[eGuiItemSlider].posx(); // - guiItems[eGuiItemThumb].texItem.w; HStretchGlItem(&(guiItems[eGuiItemSlider]), mThumbMaxMotion, 10.0f); } bool GuiDisplay::GenerateGlItem(GuiItem* guiItem) { Vertex2D verts[4]; int x = guiItem->texItem.tx; int y = guiItem->texItem.ty; int w = guiItem->texItem.w; int h = guiItem->texItem.h; verts[0] = {0, (float)h, mTexture.getTexX(x), mTexture.getTexY(y + h)}; verts[1] = {(float)w, (float)h, mTexture.getTexX(x + w), mTexture.getTexY(y + h)}; verts[2] = {0, 0, mTexture.getTexX(x), mTexture.getTexY(y)}; verts[3] = {(float)w, 0, mTexture.getTexX(x + w), mTexture.getTexY(y)}; // vertex buffer glGenBuffers(1, &(guiItem->vbo)); glBindBuffer(GL_ARRAY_BUFFER, guiItem->vbo); glBufferData(GL_ARRAY_BUFFER, 4 * sizeof(Vertex2D), verts, GL_STATIC_DRAW); // glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_SHORT, nullptr); // vertex array glGenVertexArrays(1, &(guiItem->vao)); glBindVertexArray(guiItem->vao); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (void*)offsetof(Vertex2D, x)); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (void*)offsetof(Vertex2D, tx)); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIbo); glBindVertexArray(0); return true; } bool MillSim::GuiDisplay::HStretchGlItem(GuiItem* guiItem, float newWidth, float edgeWidth) { if (guiItem->vbo == 0) { return false; } DestroyGlItem(guiItem); Vertex2D verts[12]; int x = guiItem->texItem.tx; int y = guiItem->texItem.ty; int h = guiItem->texItem.h; int w = guiItem->texItem.w; // left edge verts[0] = {0, (float)h, mTexture.getTexX(x), mTexture.getTexY(y + h)}; verts[1] = {edgeWidth, (float)h, mTexture.getTexX(x + edgeWidth), mTexture.getTexY(y + h)}; verts[2] = {0, 0, mTexture.getTexX(x), mTexture.getTexY(y)}; verts[3] = {edgeWidth, 0, mTexture.getTexX(x + edgeWidth), mTexture.getTexY(y)}; // right edge float px = newWidth - edgeWidth; verts[4] = {px, (float)h, mTexture.getTexX(x + w - edgeWidth), mTexture.getTexY(y + h)}; verts[5] = {newWidth, (float)h, mTexture.getTexX(x + w), mTexture.getTexY(y + h)}; verts[6] = {px, 0, mTexture.getTexX(x + w - edgeWidth), mTexture.getTexY(y)}; verts[7] = {newWidth, 0, mTexture.getTexX(x + w), mTexture.getTexY(y)}; // center verts[8] = {edgeWidth, (float)h, mTexture.getTexX(x + edgeWidth), mTexture.getTexY(y + h)}; verts[9] = {px, (float)h, mTexture.getTexX(x + w - edgeWidth), mTexture.getTexY(y + h)}; verts[10] = {edgeWidth, 0, mTexture.getTexX(x + edgeWidth), mTexture.getTexY(y)}; verts[11] = {px, 0, mTexture.getTexX(x + w - edgeWidth), mTexture.getTexY(y)}; guiItem->flags |= GUIITEM_STRETCHED; // vertex buffer glGenBuffers(1, &(guiItem->vbo)); glBindBuffer(GL_ARRAY_BUFFER, guiItem->vbo); glBufferData(GL_ARRAY_BUFFER, 12 * sizeof(Vertex2D), verts, GL_STATIC_DRAW); glGenVertexArrays(1, &(guiItem->vao)); glBindVertexArray(guiItem->vao); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (void*)offsetof(Vertex2D, x)); glEnableVertexAttribArray(1); glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (void*)offsetof(Vertex2D, tx)); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIbo); glBindVertexArray(0); return true; } void GuiDisplay::DestroyGlItem(GuiItem* guiItem) { GLDELETE_BUFFER((guiItem->vbo)); GLDELETE_VERTEXARRAY((guiItem->vao)); } bool GuiDisplay::InitGui() { if (guiInitiated) { return true; } // index buffer SetupTooltips(); glGenBuffers(1, &mIbo); GLshort indices[18] = {0, 2, 3, 0, 3, 1, 4, 6, 7, 4, 7, 5, 8, 10, 11, 8, 11, 9}; glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIbo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, 18 * sizeof(GLushort), indices, GL_STATIC_DRAW); TextureLoader tLoader(":/gl_simulator/", guiFileNames, TEX_SIZE); unsigned int* buffer = tLoader.GetRawData(); if (buffer == nullptr) { return false; } mTexture.LoadImage(buffer, TEX_SIZE, TEX_SIZE); for (unsigned int i = 0; i < NUM_GUI_ITEMS; i++) { guiItems[i].texItem = *tLoader.GetTextureItem(i); GenerateGlItem(&(guiItems[i])); } mThumbStartX = guiItems[eGuiItemSlider].posx() - guiItems[eGuiItemThumb].texItem.w / 2; mThumbMaxMotion = (float)guiItems[eGuiItemSlider].texItem.w; // init shader mShader.CompileShader("GuiDisplay", (char*)VertShader2DTex, (char*)FragShader2dTex); mShader.UpdateTextureSlot(0); UpdateSimSpeed(1); UpdateProjection(); guiInitiated = true; return true; } void GuiDisplay::ResetGui() { mShader.Destroy(); for (unsigned int i = 0; i < NUM_GUI_ITEMS; i++) { DestroyGlItem(&(guiItems[i])); } mTexture.DestroyTexture(); GLDELETE_BUFFER(mIbo); guiInitiated = false; } void GuiDisplay::RenderItem(int itemId) { GuiItem* item = &(guiItems[itemId]); if (item->hidden) { return; } mat4x4 model; mat4x4_translate(model, (float)item->posx(), (float)item->posy(), 0); mShader.UpdateModelMat(model, nullptr); if (item == mPressedItem) { mShader.UpdateObjColor(mPressedColor); } else if (item->mouseOver) { mShader.UpdateObjColor(mHighlightColor); } else if (itemId > 1 && item->actionKey == 0) { mShader.UpdateObjColor(mTextColor); } else if (item->flags & GUIITEM_CHECKED) { mShader.UpdateObjColor(mToggleColor); } else { mShader.UpdateObjColor(mStdColor); } glBindVertexArray(item->vao); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIbo); int nTriangles = (item->flags & GUIITEM_STRETCHED) == 0 ? 6 : 18; glDrawElements(GL_TRIANGLES, nTriangles, GL_UNSIGNED_SHORT, nullptr); } void MillSim::GuiDisplay::SetupTooltips() { guiItems[eGuiItemPause].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Pause simulation", nullptr); guiItems[eGuiItemPlay].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Play simulation", nullptr); guiItems[eGuiItemSingleStep].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Single step simulation", nullptr); guiItems[eGuiItemSlower].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Decrease simulation speed", nullptr); guiItems[eGuiItemFaster].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Increase simulation speed", nullptr); guiItems[eGuiItemPath].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Show/Hide tool path", nullptr); guiItems[eGuiItemRotate].toolTip = QCoreApplication::translate( "CAM:Simulator:Tooltips", "Toggle turn table animation", nullptr ); guiItems[eGuiItemAmbientOclusion].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Toggle ambient occlusion", nullptr); guiItems[eGuiItemView].toolTip = QCoreApplication::translate( "CAM:Simulator:Tooltips", "Toggle view simulation/model", nullptr ); guiItems[eGuiItemHome].toolTip = QCoreApplication::translate("CAM:Simulator:Tooltips", "Reset camera", nullptr); } void GuiDisplay::MouseCursorPos(int x, int y) { GuiItem* prevMouseOver = mMouseOverItem; mMouseOverItem = nullptr; for (unsigned int i = 0; i < NUM_GUI_ITEMS; i++) { GuiItem* g = &(guiItems[i]); if (g->actionKey == 0) { continue; } bool mouseCursorContained = x > g->posx() && x < (g->posx() + g->texItem.w) && y > g->posy() && y < (g->posy() + g->texItem.h); g->mouseOver = !g->hidden && mouseCursorContained; if (g->mouseOver) { mMouseOverItem = g; } } if (mMouseOverItem != prevMouseOver) { if (mMouseOverItem != nullptr && !mMouseOverItem->toolTip.isEmpty()) { QPoint pos(x, y); QPoint globPos = CAMSimulator::DlgCAMSimulator::GetInstance()->mapToGlobal(pos); QToolTip::showText(globPos, mMouseOverItem->toolTip); } else { QToolTip::hideText(); } } } void MillSim::GuiDisplay::HandleActionItem(GuiItem* guiItem) { if (guiItem->actionKey >= ' ') { if (guiItem->flags & GUIITEM_CHECKABLE) { guiItem->flags ^= GUIITEM_CHECKED; } bool isChecked = (guiItem->flags & GUIITEM_CHECKED) != 0; mMillSim->HandleGuiAction(guiItem->name, isChecked); } } void GuiDisplay::MousePressed(int button, bool isPressed, bool isSimRunning) { if (button == MS_MOUSE_LEFT) { if (isPressed) { if (mMouseOverItem != nullptr) { mPressedItem = mMouseOverItem; HandleActionItem(mPressedItem); } } else // button released { UpdatePlayState(isSimRunning); if (mPressedItem != nullptr) { MouseCursorPos(mPressedItem->posx() + 1, mPressedItem->posy() + 1); mPressedItem = nullptr; } } } } void GuiDisplay::MouseDrag(int /* buttons */, int dx, int /* dy */) { if (mPressedItem == nullptr) { return; } if (mPressedItem->name == eGuiItemThumb) { int newx = mPressedItem->posx() + dx; if (newx < mThumbStartX) { newx = mThumbStartX; } if (newx > ((int)mThumbMaxMotion + mThumbStartX)) { newx = (int)mThumbMaxMotion + mThumbStartX; } if (newx != mPressedItem->posx()) { mMillSim->SetSimulationStage((float)(newx - mThumbStartX) / mThumbMaxMotion); mPressedItem->setPosx(newx); } } } void GuiDisplay::UpdatePlayState(bool isRunning) { guiItems[eGuiItemPause].hidden = !isRunning; guiItems[eGuiItemPlay].hidden = isRunning; } void MillSim::GuiDisplay::UpdateSimSpeed(int speed) { guiItems[eGuiItem1].hidden = speed != 1; guiItems[eGuiItem5].hidden = speed != 5; guiItems[eGuiItem10].hidden = speed != 10; guiItems[eGuiItem25].hidden = speed != 25; guiItems[eGuiItem50].hidden = speed != 50; } void MillSim::GuiDisplay::HandleKeyPress(int key) { for (unsigned int i = 0; i < NUM_GUI_ITEMS; i++) { GuiItem* g = &(guiItems[i]); if (g->actionKey == key) { HandleActionItem(g); } } } bool MillSim::GuiDisplay::IsChecked(eGuiItems item) { return (guiItems[item].flags & GUIITEM_CHECKED) != 0; } void MillSim::GuiDisplay::UpdateWindowScale() { UpdateProjection(); } void GuiDisplay::Render(float progress) { if (mPressedItem == nullptr || mPressedItem->name != eGuiItemThumb) { guiItems[eGuiItemThumb].setPosx((int)(mThumbMaxMotion * progress) + mThumbStartX); } glDisable(GL_CULL_FACE); glDisable(GL_DEPTH_TEST); mTexture.Activate(); mShader.Activate(); mShader.UpdateTextureSlot(0); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); for (int i = 0; i < (int)NUM_GUI_ITEMS; i++) { RenderItem(i); } }