/*************************************************************************** * Copyright (c) 2017 Kustaa Nyholm * * * * 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 #include #include #ifdef FC_OS_WIN32 # include #endif #ifdef FC_OS_MACOSX # include #else # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "NaviCube.h" #include "Application.h" #include "Command.h" #include "Action.h" #include "MainWindow.h" #include "Navigation/NavigationAnimation.h" #include "View3DInventorViewer.h" #include "View3DInventor.h" #include "ViewParams.h" using namespace Eigen; using namespace std; using namespace Gui; class NaviCubeImplementation { public: explicit NaviCubeImplementation(Gui::View3DInventorViewer*); ~NaviCubeImplementation(); void drawNaviCube(); void createContextMenu(const std::vector& cmd); void createCubeFaceTextures(); void moveToCorner(NaviCube::Corner c); void setLabels(const std::vector& labels); bool processSoEvent(const SoEvent* ev); void setSize(int size); private: enum class PickId { None, Front, Top, Right, Rear, Bottom, Left, FrontTop, FrontBottom, FrontRight, FrontLeft, RearTop, RearBottom, RearRight, RearLeft, TopRight, TopLeft, BottomRight, BottomLeft, FrontTopRight, FrontTopLeft, FrontBottomRight, FrontBottomLeft, RearTopRight, RearTopLeft, RearBottomRight, RearBottomLeft, ArrowNorth, ArrowSouth, ArrowEast, ArrowWest, ArrowRight, ArrowLeft, DotBackside, ViewMenu }; enum class DirId { Custom, Up, Right, Out }; enum class ShapeId { None, Main, Edge, Corner, Button }; struct Face { ShapeId type; vector vertexArray; // The rotation is the standard orientation for the faces of the cube // For the flat buttons the rotation contains the direction of the rotation // The standard orientation is the desired camera orientation when a face is selected and // rotate to nearest is disabled SbRotation rotation; }; struct LabelTexture { vector vertexArray; qreal fontSize; QOpenGLTexture* texture = nullptr; string label; }; bool mousePressed(short x, short y); bool mouseReleased(short x, short y); bool mouseMoved(short x, short y); PickId pickFace(short x, short y); bool inDragZone(short x, short y); void prepare(); void handleResize(); void handleMenu(); void setHilite(PickId); void addCubeFace(const Vector3f&, const Vector3f&, ShapeId, PickId, float rotZ = 0.0); void addButtonFace(PickId, const SbVec3f& direction = SbVec3f(0, 0, 0)); QString str(const char* str); QMenu* createNaviCubeMenu(); void drawNaviCube(bool picking, float opacity); bool isFramebufferValid() const; void ensureFramebufferValid(); SbRotation getNearestOrientation(PickId pickId); qreal getPhysicalCubeWidgetSize(); public: static int m_CubeWidgetSize; QColor m_BaseColor; QColor m_EmphaseColor; QColor m_HiliteColor; bool m_ShowCS = true; PickId m_HiliteId = PickId::None; double m_BorderWidth = 1.1; bool m_RotateToNearest = true; int m_NaviStepByTurn = 8; float m_FontZoom = 0.3F; float m_Chamfer = 0.12F; std::string m_TextFont; int m_FontWeight = 0; int m_FontStretch = 0; float m_InactiveOpacity = 0.5; SbVec2s m_PosOffset = SbVec2s(0, 0); Base::Color m_xColor; Base::Color m_yColor; Base::Color m_zColor; bool m_Prepared = false; static vector m_commands; bool m_Draggable = false; SbVec2s m_ViewSize = SbVec2s(0, 0); private: bool m_MouseDown = false; bool m_Dragging = false; bool m_MightDrag = false; bool m_Hovering = false; SbVec2f m_RelPos = SbVec2f(1.0f, 1.0f); SbVec2s m_PosAreaBase = SbVec2s(0, 0); SbVec2s m_PosAreaSize = SbVec2s(0, 0); qreal m_DevicePixelRatio = 1.0; QOpenGLFramebufferObject* m_PickingFramebuffer; Gui::View3DInventorViewer* m_View3DInventorViewer; map m_Faces; map m_LabelTextures; QMenu* m_Menu; std::shared_ptr m_flatButtonAnimation; SbRotation m_flatButtonTargetOrientation; }; int NaviCubeImplementation::m_CubeWidgetSize = 132; int NaviCube::getNaviCubeSize() { return NaviCubeImplementation::m_CubeWidgetSize; } NaviCube::NaviCube(Gui::View3DInventorViewer* viewer) { m_NaviCubeImplementation = new NaviCubeImplementation(viewer); updateColors(); } NaviCube::~NaviCube() { delete m_NaviCubeImplementation; } void NaviCube::drawNaviCube() { m_NaviCubeImplementation->drawNaviCube(); } void NaviCube::createContextMenu(const std::vector& cmd) { m_NaviCubeImplementation->createContextMenu(cmd); } bool NaviCube::processSoEvent(const SoEvent* ev) { return m_NaviCubeImplementation->processSoEvent(ev); } vector NaviCubeImplementation::m_commands; void NaviCube::setCorner(Corner c) { m_NaviCubeImplementation->moveToCorner(c); } void NaviCube::setOffset(int x, int y) { m_NaviCubeImplementation->m_PosOffset = SbVec2s(x, y); m_NaviCubeImplementation->m_ViewSize = SbVec2s(0, 0); } bool NaviCube::isDraggable() { return m_NaviCubeImplementation->m_Draggable; } void NaviCube::setDraggable(bool draggable) { m_NaviCubeImplementation->m_Draggable = draggable; } void NaviCube::setSize(int size) { m_NaviCubeImplementation->setSize(size); } void NaviCube::setChamfer(float chamfer) { m_NaviCubeImplementation->m_Chamfer = min(max(0.05f, chamfer), 0.18f); m_NaviCubeImplementation->m_Prepared = false; } void NaviCube::setNaviRotateToNearest(bool toNearest) { m_NaviCubeImplementation->m_RotateToNearest = toNearest; } void NaviCube::setNaviStepByTurn(int steps) { m_NaviCubeImplementation->m_NaviStepByTurn = steps; } void NaviCube::setFont(std::string font) { m_NaviCubeImplementation->m_TextFont = font; m_NaviCubeImplementation->m_Prepared = false; } void NaviCube::setFontWeight(int weight) { m_NaviCubeImplementation->m_FontWeight = weight; m_NaviCubeImplementation->m_Prepared = false; } void NaviCube::setFontStretch(int stretch) { m_NaviCubeImplementation->m_FontStretch = stretch; m_NaviCubeImplementation->m_Prepared = false; } void NaviCube::setFontZoom(float zoom) { m_NaviCubeImplementation->m_FontZoom = zoom; m_NaviCubeImplementation->m_Prepared = false; } void NaviCube::setBaseColor(QColor bColor) { m_NaviCubeImplementation->m_BaseColor = bColor; } void NaviCube::setEmphaseColor(QColor eColor) { m_NaviCubeImplementation->m_EmphaseColor = eColor; m_NaviCubeImplementation->m_Prepared = false; } void NaviCube::setHiliteColor(QColor HiliteColor) { m_NaviCubeImplementation->m_HiliteColor = HiliteColor; } void NaviCube::setBorderWidth(double BorderWidth) { m_NaviCubeImplementation->m_BorderWidth = BorderWidth; } void NaviCube::setShowCS(bool showCS) { m_NaviCubeImplementation->m_ShowCS = showCS; } void NaviCube::setNaviCubeLabels(const std::vector& labels) { m_NaviCubeImplementation->setLabels(labels); } void NaviCube::setInactiveOpacity(float opacity) { m_NaviCubeImplementation->m_InactiveOpacity = opacity; } qreal NaviCubeImplementation::getPhysicalCubeWidgetSize() { return m_CubeWidgetSize * m_DevicePixelRatio; } void NaviCubeImplementation::setLabels(const std::vector& labels) { m_LabelTextures[PickId::Front].label = labels[0]; m_LabelTextures[PickId::Top].label = labels[1]; m_LabelTextures[PickId::Right].label = labels[2]; m_LabelTextures[PickId::Rear].label = labels[3]; m_LabelTextures[PickId::Bottom].label = labels[4]; m_LabelTextures[PickId::Left].label = labels[5]; m_Prepared = false; } NaviCubeImplementation::NaviCubeImplementation(Gui::View3DInventorViewer* viewer) : m_BaseColor {226, 232, 239} , m_HiliteColor {170, 226, 255} { m_View3DInventorViewer = viewer; m_PickingFramebuffer = nullptr; m_Menu = createNaviCubeMenu(); } NaviCubeImplementation::~NaviCubeImplementation() { delete m_Menu; if (m_PickingFramebuffer) { delete m_PickingFramebuffer; } for (auto tex : m_LabelTextures) { delete tex.second.texture; } } void NaviCubeImplementation::moveToCorner(NaviCube::Corner c) { if (c == NaviCube::TopLeftCorner) { m_RelPos = SbVec2f(0.0f, 1.0f); } else if (c == NaviCube::TopRightCorner) { m_RelPos = SbVec2f(1.0f, 1.0f); } else if (c == NaviCube::BottomLeftCorner) { m_RelPos = SbVec2f(0.0f, 0.0f); } else if (c == NaviCube::BottomRightCorner) { m_RelPos = SbVec2f(1.0f, 0.0f); } } auto convertWeights = [](int weight) -> QFont::Weight { if (weight >= 87) { return QFont::Black; } if (weight >= 81) { return QFont::ExtraBold; } if (weight >= 75) { return QFont::Bold; } if (weight >= 63) { return QFont::DemiBold; } if (weight >= 57) { return QFont::Medium; } if (weight >= 50) { return QFont::Normal; } if (weight >= 25) { return QFont::Light; } if (weight >= 12) { return QFont::ExtraLight; } return QFont::Thin; }; int imageVerticalBalance(QImage p, int sizeHint) { if (sizeHint < 0) { return 0; } int h = p.height(); int startRow = (h - sizeHint) / 2; bool done = false; int x, bottom, top; for (top = startRow; top < h; top++) { for (x = 0; x < p.width(); x++) { if (qAlpha(p.pixel(x, top))) { done = true; break; } } if (done) { break; } } for (bottom = startRow; bottom < h; bottom++) { for (x = 0; x < p.width(); x++) { if (qAlpha(p.pixel(x, h - 1 - bottom))) { return (bottom - top) / 2; } } } return 0; } void NaviCubeImplementation::createCubeFaceTextures() { int texSize = 192; // Works well for the max cube size 1024 QFont font; if (m_TextFont.empty()) { font.fromString(QStringLiteral("Arial")); } else { font.fromString(QString::fromStdString(m_TextFont)); } font.setStyleHint(QFont::SansSerif); if (m_FontWeight > 0) { font.setWeight(convertWeights(m_FontWeight)); } if (m_FontStretch > 0) { font.setStretch(m_FontStretch); } font.setPointSizeF(texSize); QFontMetrics fm(font); qreal minFontSize = texSize; qreal maxFontSize = 0.; vector mains = {PickId::Front, PickId::Top, PickId::Right, PickId::Rear, PickId::Bottom, PickId::Left}; for (PickId pickId : mains) { auto t = QString::fromUtf8(m_LabelTextures[pickId].label.c_str()); QRect br = fm.boundingRect(t); float scale = (float)texSize / max(br.width(), br.height()); m_LabelTextures[pickId].fontSize = texSize * scale; minFontSize = std::min(minFontSize, m_LabelTextures[pickId].fontSize); maxFontSize = std::max(maxFontSize, m_LabelTextures[pickId].fontSize); } if (m_FontZoom > 0.0) { maxFontSize = minFontSize + (maxFontSize - minFontSize) * m_FontZoom; } else { maxFontSize = minFontSize * std::pow(2.0, m_FontZoom); } for (PickId pickId : mains) { QImage image(texSize, texSize, QImage::Format_ARGB32); image.fill(qRgba(255, 255, 255, 0)); if (m_LabelTextures[pickId].fontSize > 0.5) { // 5% margin looks nice and prevents some artifacts font.setPointSizeF(std::min(m_LabelTextures[pickId].fontSize, maxFontSize) * 0.9); QPainter paint; paint.begin(&image); paint.setRenderHints( QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::SmoothPixmapTransform ); paint.setPen(Qt::white); QString text = QString::fromUtf8(m_LabelTextures[pickId].label.c_str()); paint.setFont(font); paint.drawText(QRect(0, 0, texSize, texSize), Qt::AlignCenter, text); int offset = imageVerticalBalance(image, font.pointSize()); image.fill(qRgba(255, 255, 255, 0)); paint.drawText(QRect(0, offset, texSize, texSize), Qt::AlignCenter, text); paint.end(); } if (m_LabelTextures[pickId].texture) { delete m_LabelTextures[pickId].texture; } m_LabelTextures[pickId].texture = new QOpenGLTexture(image.mirrored()); m_LabelTextures[pickId].texture->setMaximumAnisotropy(4.0); m_LabelTextures[pickId].texture->setMinificationFilter(QOpenGLTexture::LinearMipMapLinear); m_LabelTextures[pickId].texture->setMagnificationFilter(QOpenGLTexture::Linear); m_LabelTextures[pickId].texture->generateMipMaps(); } } void NaviCubeImplementation::addButtonFace(PickId pickId, const SbVec3f& direction) { if (m_Faces[pickId].vertexArray.size()) { m_Faces[pickId].vertexArray.clear(); } float scale = 0.005F; float offx = 0.5F; float offy = 0.5F; vector pointData; switch (pickId) { default: break; case PickId::ArrowRight: case PickId::ArrowLeft: { pointData = {66.6F, -66.6F, // outer curve 58.3F, -74.0F, 49.2F, -80.3F, 39.4F, -85.5F, 29.0F, -89.5F, 25.3F, -78.1F, // inner curve 34.3F, -74.3F, 42.8F, -69.9F, 50.8F, -64.4F, 58.1F, -58.1F, 53.8F, -53.8F, // arrowhead 74.7F, -46.8F, 70.7F, -70.4F}; break; } case PickId::ArrowWest: case PickId::ArrowNorth: case PickId::ArrowSouth: case PickId::ArrowEast: { pointData = {100., 0., 80., -18., 80., 18.}; break; } case PickId::ViewMenu: { offx = 0.84F; offy = 0.84F; pointData = {0., 0., // top rhombus 15., -6., 0., -12., -15., -6., 0., 0., // left rhombus -15., -6., -15., 12., 0., 18., 0., 0., // right rhombus 0., 18., 15., 12., 15., -6.}; break; } case PickId::DotBackside: { int steps = 16; for (int i = 0; i < steps; i++) { float angle = 2.0f * std::numbers::pi_v * ((float)i + 0.5) / (float)steps; pointData.emplace_back(10. * cos(angle) + 87.); pointData.emplace_back(10. * sin(angle) - 87.); } break; } } int count = static_cast(pointData.size()) / 2; m_Faces[pickId].vertexArray.reserve(count); for (int i = 0; i < count; i++) { float x = pointData[i * 2] * scale + offx; float y = pointData[i * 2 + 1] * scale + offy; if (pickId == PickId::ArrowNorth || pickId == PickId::ArrowWest || pickId == PickId::ArrowLeft) { x = 1.0 - x; } if (pickId == PickId::ArrowSouth || pickId == PickId::ArrowNorth) { m_Faces[pickId].vertexArray.emplace_back(Vector3f(y, x, 0.0)); } else { m_Faces[pickId].vertexArray.emplace_back(Vector3f(x, y, 0.0)); } } m_Faces[pickId].type = ShapeId::Button; m_Faces[pickId].rotation = SbRotation(direction, 1).inverse(); } void NaviCubeImplementation::addCubeFace( const Vector3f& x, const Vector3f& z, ShapeId shapeType, PickId pickId, float rotZ ) { m_Faces[pickId].vertexArray.clear(); m_Faces[pickId].type = shapeType; Vector3f y = x.cross(-z); // Determine the standard orientations based on vector x and vector z // Rotate by an additional rotZ if vector x and vector z are not already the standard orientation // Create normalized vectors for x, y and z SbVec3f xN(x.x(), x.y(), x.z()); SbVec3f yN(y.x(), y.y(), y.z()); SbVec3f zN(z.x(), z.y(), z.z()); xN.normalize(); yN.normalize(); zN.normalize(); // Create a rotation matrix SbMatrix R(xN[0], yN[0], zN[0], 0, xN[1], yN[1], zN[1], 0, xN[2], yN[2], zN[2], 0, 0, 0, 0, 1); // Store the standard orientation m_Faces[pickId].rotation = (SbRotation(R) * SbRotation(SbVec3f(0, 0, 1), rotZ)).inverse(); if (shapeType == ShapeId::Corner) { auto xC = x * m_Chamfer; auto yC = y * m_Chamfer; auto zC = (1 - 2 * m_Chamfer) * z; m_Faces[pickId].vertexArray.reserve(6); m_Faces[pickId].vertexArray.emplace_back(zC - 2 * xC); m_Faces[pickId].vertexArray.emplace_back(zC - xC - yC); m_Faces[pickId].vertexArray.emplace_back(zC + xC - yC); m_Faces[pickId].vertexArray.emplace_back(zC + 2 * xC); m_Faces[pickId].vertexArray.emplace_back(zC + xC + yC); m_Faces[pickId].vertexArray.emplace_back(zC - xC + yC); } else if (shapeType == ShapeId::Edge) { auto x4 = x * (1 - m_Chamfer * 4); auto yE = y * m_Chamfer; auto zE = z * (1 - m_Chamfer); m_Faces[pickId].vertexArray.reserve(4); m_Faces[pickId].vertexArray.emplace_back(zE - x4 - yE); m_Faces[pickId].vertexArray.emplace_back(zE + x4 - yE); m_Faces[pickId].vertexArray.emplace_back(zE + x4 + yE); m_Faces[pickId].vertexArray.emplace_back(zE - x4 + yE); } else if (shapeType == ShapeId::Main) { auto x2 = x * (1 - m_Chamfer * 2); auto y2 = y * (1 - m_Chamfer * 2); auto x4 = x * (1 - m_Chamfer * 4); auto y4 = y * (1 - m_Chamfer * 4); m_Faces[pickId].vertexArray.reserve(8); m_Faces[pickId].vertexArray.emplace_back(z - x2 - y4); m_Faces[pickId].vertexArray.emplace_back(z - x4 - y2); m_Faces[pickId].vertexArray.emplace_back(z + x4 - y2); m_Faces[pickId].vertexArray.emplace_back(z + x2 - y4); m_Faces[pickId].vertexArray.emplace_back(z + x2 + y4); m_Faces[pickId].vertexArray.emplace_back(z + x4 + y2); m_Faces[pickId].vertexArray.emplace_back(z - x4 + y2); m_Faces[pickId].vertexArray.emplace_back(z - x2 + y4); m_LabelTextures[pickId].vertexArray.clear(); m_LabelTextures[pickId].vertexArray.emplace_back(z - x2 - y2); m_LabelTextures[pickId].vertexArray.emplace_back(z + x2 - y2); m_LabelTextures[pickId].vertexArray.emplace_back(z + x2 + y2); m_LabelTextures[pickId].vertexArray.emplace_back(z - x2 + y2); } } void NaviCubeImplementation::setSize(int size) { m_CubeWidgetSize = size; m_ViewSize = SbVec2s(0, 0); m_Prepared = false; } void NaviCubeImplementation::prepare() { constexpr float pi = std::numbers::pi_v; constexpr float pi1_2 = pi / 2; createCubeFaceTextures(); Vector3f x(1, 0, 0); Vector3f y(0, 1, 0); Vector3f z(0, 0, 1); // create the main faces addCubeFace(x, z, ShapeId::Main, PickId::Top); addCubeFace(x, -y, ShapeId::Main, PickId::Front); addCubeFace(-y, -x, ShapeId::Main, PickId::Left); addCubeFace(-x, y, ShapeId::Main, PickId::Rear); addCubeFace(y, x, ShapeId::Main, PickId::Right); addCubeFace(x, -z, ShapeId::Main, PickId::Bottom); // create corner faces addCubeFace(-x - y, x - y + z, ShapeId::Corner, PickId::FrontTopRight, pi); addCubeFace(-x + y, -x - y + z, ShapeId::Corner, PickId::FrontTopLeft, pi); addCubeFace(x + y, x - y - z, ShapeId::Corner, PickId::FrontBottomRight); addCubeFace(x - y, -x - y - z, ShapeId::Corner, PickId::FrontBottomLeft); addCubeFace(x - y, x + y + z, ShapeId::Corner, PickId::RearTopRight, pi); addCubeFace(x + y, -x + y + z, ShapeId::Corner, PickId::RearTopLeft, pi); addCubeFace(-x + y, x + y - z, ShapeId::Corner, PickId::RearBottomRight); addCubeFace(-x - y, -x + y - z, ShapeId::Corner, PickId::RearBottomLeft); // create edge faces addCubeFace(x, z - y, ShapeId::Edge, PickId::FrontTop); addCubeFace(x, -z - y, ShapeId::Edge, PickId::FrontBottom); addCubeFace(x, y - z, ShapeId::Edge, PickId::RearBottom, pi); addCubeFace(x, y + z, ShapeId::Edge, PickId::RearTop, pi); addCubeFace(z, x + y, ShapeId::Edge, PickId::RearRight, pi1_2); addCubeFace(z, x - y, ShapeId::Edge, PickId::FrontRight, pi1_2); addCubeFace(z, -x - y, ShapeId::Edge, PickId::FrontLeft, pi1_2); addCubeFace(z, y - x, ShapeId::Edge, PickId::RearLeft, pi1_2); addCubeFace(y, z - x, ShapeId::Edge, PickId::TopLeft, pi); addCubeFace(y, x + z, ShapeId::Edge, PickId::TopRight); addCubeFace(y, x - z, ShapeId::Edge, PickId::BottomRight); addCubeFace(y, -z - x, ShapeId::Edge, PickId::BottomLeft, pi); // create the flat buttons addButtonFace(PickId::ArrowNorth, SbVec3f(-1, 0, 0)); addButtonFace(PickId::ArrowSouth, SbVec3f(1, 0, 0)); addButtonFace(PickId::ArrowEast, SbVec3f(0, 1, 0)); addButtonFace(PickId::ArrowWest, SbVec3f(0, -1, 0)); addButtonFace(PickId::ArrowLeft, SbVec3f(0, 0, 1)); addButtonFace(PickId::ArrowRight, SbVec3f(0, 0, -1)); addButtonFace(PickId::DotBackside, SbVec3f(0, 1, 0)); addButtonFace(PickId::ViewMenu); if (m_PickingFramebuffer) { delete m_PickingFramebuffer; m_PickingFramebuffer = nullptr; } ensureFramebufferValid(); m_View3DInventorViewer->getSoRenderManager()->scheduleRedraw(); } void NaviCubeImplementation::drawNaviCube() { ensureFramebufferValid(); handleResize(); qreal physicalCubeWidgetSize = getPhysicalCubeWidgetSize(); int posX = (int)(m_RelPos[0] * m_PosAreaSize[0]) + m_PosAreaBase[0] - physicalCubeWidgetSize / 2; int posY = (int)(m_RelPos[1] * m_PosAreaSize[1]) + m_PosAreaBase[1] - physicalCubeWidgetSize / 2; glViewport(posX, posY, physicalCubeWidgetSize, physicalCubeWidgetSize); drawNaviCube(false, m_Hovering ? 1.f : m_InactiveOpacity); } void NaviCubeImplementation::createContextMenu(const std::vector& cmd) { CommandManager& rcCmdMgr = Application::Instance->commandManager(); m_Menu->clear(); for (const auto& i : cmd) { Command* cmd = rcCmdMgr.getCommandByName(i.c_str()); if (cmd) { cmd->addTo(m_Menu); } } } void NaviCubeImplementation::handleResize() { qreal devicePixelRatio = m_View3DInventorViewer->devicePixelRatio(); SbVec2s viewSize = m_View3DInventorViewer->getSoRenderManager()->getSize(); if (viewSize != m_ViewSize || devicePixelRatio != m_DevicePixelRatio) { m_DevicePixelRatio = devicePixelRatio; qreal physicalCubeWidgetSize = getPhysicalCubeWidgetSize(); m_PosAreaBase[0] = std::min((int)(m_PosOffset[0] + physicalCubeWidgetSize * 0.55), viewSize[0] / 2); m_PosAreaBase[1] = std::min((int)(m_PosOffset[1] + physicalCubeWidgetSize * 0.55), viewSize[1] / 2); m_PosAreaSize[0] = viewSize[0] - 2 * m_PosAreaBase[0]; m_PosAreaSize[1] = viewSize[1] - 2 * m_PosAreaBase[1]; m_ViewSize = viewSize; } } void NaviCubeImplementation::drawNaviCube(bool pickMode, float opacity) { if (!m_Prepared) { if (!m_View3DInventorViewer->viewport()) { return; } prepare(); m_Prepared = true; m_View3DInventorViewer->getSoRenderManager()->scheduleRedraw(); return; } ensureFramebufferValid(); SoCamera* cam = m_View3DInventorViewer->getSoRenderManager()->getCamera(); if (!cam) { return; } // Store GL state. glPushAttrib(GL_ALL_ATTRIB_BITS); // configure glEnable(GL_DEPTH_TEST); glDepthMask(GL_TRUE); glDepthRange(0.f, 1.f); glClearDepth(1.f); glClear(GL_DEPTH_BUFFER_BIT); glDepthFunc(GL_LEQUAL); glDisable(GL_LIGHTING); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); glFrontFace(GL_CCW); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); if (pickMode) { glDisable(GL_BLEND); glShadeModel(GL_FLAT); glDisable(GL_DITHER); glDisable(GL_POLYGON_SMOOTH); glClearColor(0, 0, 0, 1); glClear(GL_COLOR_BUFFER_BIT); } else { glEnable(GL_POLYGON_OFFSET_FILL); glPolygonOffset(1.0f, 1.0f); glEnable(GL_BLEND); glShadeModel(GL_SMOOTH); } // mimic 3d view projection glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); const float NEARVAL = 0.1f; const float FARVAL = 10.1f; if (cam->getTypeId().isDerivedFrom(SoOrthographicCamera::getClassTypeId())) { glOrtho(-2.1, 2.1, -2.1, 2.1, NEARVAL, FARVAL); } else { const float dim = NEARVAL * float(tan(std::numbers::pi / 8.0)) * 1.1; glFrustum(-dim, dim, -dim, dim, NEARVAL, FARVAL); } glMatrixMode(GL_MODELVIEW); glPushMatrix(); SbMatrix mx; mx = cam->orientation.getValue(); mx = mx.inverse(); mx[3][2] = -5.1F; glLoadMatrixf((float*)mx); glEnableClientState(GL_VERTEX_ARRAY); QColor& cb = m_EmphaseColor; // Draw coordinate system if (!pickMode && m_ShowCS) { glLineWidth(m_BorderWidth * 2.f); glPointSize(m_BorderWidth * 2.f); float a = -1.1f; float b = -1.05f; float c = 0.5f; float pointData[] = { b, a, a, // X1 c, a, a, // X2 a, b, a, // Y1 a, c, a, // Y2 a, a, b, // Z1 a, a, c, // Z2 a, a, a // 0 }; glVertexPointer(3, GL_FLOAT, 0, pointData); glColor4f(m_xColor.r, m_xColor.g, m_xColor.b, opacity); glDrawArrays(GL_LINES, 0, 2); glDrawArrays(GL_POINTS, 0, 2); glColor4f(m_yColor.r, m_yColor.g, m_yColor.b, opacity); glDrawArrays(GL_LINES, 2, 2); glDrawArrays(GL_POINTS, 2, 2); glColor4f(m_zColor.r, m_zColor.g, m_zColor.b, opacity); glDrawArrays(GL_LINES, 4, 2); glDrawArrays(GL_POINTS, 4, 2); } // cube faces for (const auto& pair : m_Faces) { auto f = pair.second; if (f.type == ShapeId::Button) { continue; } auto pickId = pair.first; if (pickMode) { glColor3ub(static_cast(pickId), 0, 0); } else { QColor& c = m_HiliteId == pickId ? m_HiliteColor : m_BaseColor; glColor4f(c.redF(), c.greenF(), c.blueF(), c.alphaF() * opacity); } glVertexPointer(3, GL_FLOAT, 0, f.vertexArray.data()); glDrawArrays(GL_TRIANGLE_FAN, 0, f.vertexArray.size()); } if (!pickMode) { // cube borders glLineWidth(m_BorderWidth); for (const auto& pair : m_Faces) { auto f = pair.second; if (f.type == ShapeId::Button) { continue; } glColor4f(cb.redF(), cb.greenF(), cb.blueF(), cb.alphaF() * opacity); glVertexPointer(3, GL_FLOAT, 0, f.vertexArray.data()); glDrawArrays(GL_LINES, 0, f.vertexArray.size()); } // Label textures glDisable(GL_POLYGON_OFFSET_FILL); // make sure labels are on top glEnable(GL_TEXTURE_2D); glEnableClientState(GL_TEXTURE_COORD_ARRAY); float texCoords[] = {0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f, 1.f}; glTexCoordPointer(2, GL_FLOAT, 0, texCoords); QColor& c = m_EmphaseColor; glColor4f(c.redF(), c.greenF(), c.blueF(), c.alphaF() * opacity); for (const auto& pair : m_LabelTextures) { auto f = pair.second; PickId pickId = pair.first; glVertexPointer(3, GL_FLOAT, 0, m_LabelTextures[pickId].vertexArray.data()); glBindTexture(GL_TEXTURE_2D, f.texture->textureId()); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisable(GL_TEXTURE_2D); glEnable(GL_POLYGON_OFFSET_FILL); } // Draw the flat buttons glDisable(GL_CULL_FACE); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0.0, 1.0, 1.0, 0.0, 0.0, 1.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); for (const auto& pair : m_Faces) { auto f = pair.second; if (f.type != ShapeId::Button) { continue; } PickId pickId = pair.first; if (pickMode) { glColor3ub(static_cast(pickId), 0, 0); } else { QColor& c = m_HiliteId == pickId ? m_HiliteColor : m_BaseColor; glColor4f(c.redF(), c.greenF(), c.blueF(), c.alphaF() * opacity); } glVertexPointer(3, GL_FLOAT, 0, f.vertexArray.data()); glDrawArrays(GL_TRIANGLE_FAN, 0, f.vertexArray.size()); if (!pickMode) { glColor4f(cb.redF(), cb.greenF(), cb.blueF(), cb.alphaF() * opacity); glDrawArrays(GL_LINE_LOOP, 0, f.vertexArray.size()); } } // Restore original state. glPopMatrix(); glMatrixMode(GL_PROJECTION); glPopMatrix(); glPopAttrib(); } bool NaviCubeImplementation::isFramebufferValid() const { return m_PickingFramebuffer && m_PickingFramebuffer->isValid(); } void NaviCubeImplementation::ensureFramebufferValid() { if (!isFramebufferValid()) { if (m_PickingFramebuffer) { if (!m_PickingFramebuffer->isValid()) { Base::Console().developerWarning( "NaviCube", "The frame buffer has become invalid, a new frame buffer will be created\n" ); } delete m_PickingFramebuffer; m_PickingFramebuffer = nullptr; } qreal physicalCubeWidgetSize = getPhysicalCubeWidgetSize(); m_PickingFramebuffer = new QOpenGLFramebufferObject( 2 * physicalCubeWidgetSize, 2 * physicalCubeWidgetSize, QOpenGLFramebufferObject::CombinedDepthStencil ); } } NaviCubeImplementation::PickId NaviCubeImplementation::pickFace(short x, short y) { qreal physicalCubeWidgetSize = getPhysicalCubeWidgetSize(); GLubyte pixels[4] = {0}; ensureFramebufferValid(); if (m_PickingFramebuffer && std::abs(x) <= physicalCubeWidgetSize / 2 && std::abs(y) <= physicalCubeWidgetSize / 2) { static_cast(m_View3DInventorViewer->viewport())->makeCurrent(); m_PickingFramebuffer->bind(); glViewport(0, 0, physicalCubeWidgetSize * 2, physicalCubeWidgetSize * 2); drawNaviCube(true, 1.f); glFinish(); glReadPixels( 2 * x + physicalCubeWidgetSize, 2 * y + physicalCubeWidgetSize, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixels ); m_PickingFramebuffer->release(); static_cast(m_View3DInventorViewer->viewport())->doneCurrent(); } return pixels[3] == 255 ? static_cast(pixels[0]) : PickId::None; } bool NaviCubeImplementation::mousePressed(short x, short y) { m_MouseDown = true; m_MightDrag = inDragZone(x, y); PickId pick = pickFace(x, y); setHilite(pick); return pick != PickId::None; } void NaviCubeImplementation::handleMenu() { m_Menu->exec(QCursor::pos()); } SbRotation NaviCubeImplementation::getNearestOrientation(PickId pickId) { SbRotation cameraOrientation = m_View3DInventorViewer->getCameraOrientation(); SbRotation standardOrientation = m_Faces[pickId].rotation; SbVec3f cameraZ; cameraOrientation.multVec(SbVec3f(0, 0, 1), cameraZ); SbVec3f standardZ; standardOrientation.multVec(SbVec3f(0, 0, 1), standardZ); // Cleanup near zero values for (int i = 0; i < 3; i++) { if (abs(standardZ[i]) < 1e-6) { standardZ[i] = 0.0F; } } standardZ.normalize(); // Rotate the camera to the selected face by the smallest angle to align the z-axis SbRotation intermediateOrientation = cameraOrientation * SbRotation(cameraZ, standardZ); // Find an axis and angle to go from the intermediateOrientation to the standardOrientation SbVec3f axis; float angle; SbRotation rotation = intermediateOrientation.inverse() * standardOrientation; rotation.getValue(axis, angle); // Make sure the found axis aligns with the standardZ axis if (standardZ.dot(axis) < 0) { axis.negate(); angle *= -1; } constexpr float pi = std::numbers::pi_v; // Make angle positive if (angle < 0) { angle += 2 * pi; } // f is a small value used to control orientation priority when the camera is almost exactly // between two orientations (e.g. +45 and -45 degrees). The standard orientation is preferred // compared to +90 and -90 degree orientations and the +90 and -90 degree orientations are // preferred compared to an upside down standard orientation float f = 0.00001F; // Find the angle to rotate to the nearest orientation if (m_Faces[pickId].type == ShapeId::Corner) { // 6 possible orientations for the corners if (angle <= (pi / 6 + f)) { angle = 0; } else if (angle <= (pi / 2 + f)) { angle = pi / 3; } else if (angle < (5 * pi / 6 - f)) { angle = 2 * pi / 3; } else if (angle <= (pi + pi / 6 + f)) { angle = pi; } else if (angle < (pi + pi / 2 - f)) { angle = pi + pi / 3; } else if (angle < (pi + 5 * pi / 6 - f)) { angle = pi + 2 * pi / 3; } else { angle = 0; } } else { // 4 possible orientations for the main and edge faces if (angle <= (pi / 4 + f)) { angle = 0; } else if (angle <= (3 * pi / 4 + f)) { angle = pi / 2; } else if (angle < (pi + pi / 4 - f)) { angle = pi; } else if (angle < (pi + 3 * pi / 4 - f)) { angle = pi + pi / 2; } else { angle = 0; } } // Set the rotation to go from the standard orientation to the nearest orientation rotation.setValue(standardZ, angle); return standardOrientation * rotation.inverse(); } bool NaviCubeImplementation::mouseReleased(short x, short y) { static const float pi = boost::math::constants::pi(); setHilite(PickId::None); m_MouseDown = false; if (m_Dragging) { m_Dragging = false; } else { PickId pickId = pickFace(x, y); long step = Base::clamp(long(m_NaviStepByTurn), 4L, 36L); float rotStepAngle = (2 * std::numbers::pi) / step; if (m_Faces[pickId].type == ShapeId::Main || m_Faces[pickId].type == ShapeId::Edge || m_Faces[pickId].type == ShapeId::Corner) { // Handle the cube faces SbRotation orientation; if (m_RotateToNearest) { orientation = getNearestOrientation(pickId); } else { orientation = m_Faces[pickId].rotation; } m_View3DInventorViewer->setCameraOrientation(orientation); } else if (m_Faces[pickId].type == ShapeId::Button) { // Handle the menu if (pickId == PickId::ViewMenu) { handleMenu(); return true; } // Handle the flat buttons SbRotation rotation = m_Faces[pickId].rotation; if (pickId == PickId::DotBackside) { rotation.scaleAngle(pi); } else { rotation.scaleAngle(rotStepAngle); } // If the previous flat button animation is still active then apply the rotation to the // previous target orientation, otherwise apply the rotation to the current camera // orientation if (m_flatButtonAnimation != nullptr && m_flatButtonAnimation->state() == QAbstractAnimation::Running) { m_flatButtonTargetOrientation = rotation * m_flatButtonTargetOrientation; } else { m_flatButtonTargetOrientation = rotation * m_View3DInventorViewer->getCameraOrientation(); } m_flatButtonAnimation = m_View3DInventorViewer->setCameraOrientation( m_flatButtonTargetOrientation ); } else { return false; } } return true; } void NaviCubeImplementation::setHilite(PickId hilite) { if (hilite != m_HiliteId) { m_HiliteId = hilite; m_View3DInventorViewer->getSoRenderManager()->scheduleRedraw(); } } bool NaviCubeImplementation::inDragZone(short x, short y) { qreal physicalCubeWidgetSize = getPhysicalCubeWidgetSize(); int limit = physicalCubeWidgetSize / 4; return std::abs(x) < limit && std::abs(y) < limit; } bool NaviCubeImplementation::mouseMoved(short x, short y) { qreal physicalCubeWidgetSize = getPhysicalCubeWidgetSize(); bool hovering = std::abs(x) <= physicalCubeWidgetSize / 2 && std::abs(y) <= physicalCubeWidgetSize / 2; if (hovering != m_Hovering) { m_Hovering = hovering; m_View3DInventorViewer->getSoRenderManager()->scheduleRedraw(); } if (!m_Dragging) { setHilite(pickFace(x, y)); } if (m_MouseDown && m_Draggable) { if (m_MightDrag && !m_Dragging) { m_Dragging = true; setHilite(PickId::None); } if (m_Dragging && (std::abs(x) || std::abs(y))) { float newX = m_RelPos[0] + (float)(x) / m_PosAreaSize[0]; float newY = m_RelPos[1] + (float)(y) / m_PosAreaSize[1]; m_RelPos[0] = std::min(std::max(newX, 0.0f), 1.0f); m_RelPos[1] = std::min(std::max(newY, 0.0f), 1.0f); m_View3DInventorViewer->getSoRenderManager()->scheduleRedraw(); return true; } } return false; } bool NaviCubeImplementation::processSoEvent(const SoEvent* ev) { short x, y; ev->getPosition().getValue(x, y); // translate to internal cube center based coordinates short rx = x - (short)(m_PosAreaSize[0] * m_RelPos[0]) - m_PosAreaBase[0]; short ry = y - (short)(m_PosAreaSize[1] * m_RelPos[1]) - m_PosAreaBase[1]; if (ev->getTypeId().isDerivedFrom(SoMouseButtonEvent::getClassTypeId())) { const auto mbev = static_cast(ev); if (mbev->isButtonPressEvent(mbev, SoMouseButtonEvent::BUTTON1)) { return mousePressed(rx, ry); } if (mbev->isButtonReleaseEvent(mbev, SoMouseButtonEvent::BUTTON1)) { return mouseReleased(rx, ry); } } if (ev->getTypeId().isDerivedFrom(SoLocation2Event::getClassTypeId())) { return mouseMoved(rx, ry); } return false; } QString NaviCubeImplementation::str(const char* str) { return QString::fromLatin1(str); } void NaviCube::updateColors() { unsigned long colorLong; colorLong = Gui::ViewParams::instance()->getAxisXColor(); m_NaviCubeImplementation->m_xColor = Base::Color(static_cast(colorLong)); colorLong = Gui::ViewParams::instance()->getAxisYColor(); m_NaviCubeImplementation->m_yColor = Base::Color(static_cast(colorLong)); colorLong = Gui::ViewParams::instance()->getAxisZColor(); m_NaviCubeImplementation->m_zColor = Base::Color(static_cast(colorLong)); } void NaviCube::setNaviCubeCommands(const std::vector& cmd) { NaviCubeImplementation::m_commands = cmd; } DEF_STD_CMD_AC(NaviCubeDraggableCmd) NaviCubeDraggableCmd::NaviCubeDraggableCmd() : Command("NaviCubeDraggableCmd") { sGroup = ""; sMenuText = QT_TR_NOOP("Movable Navigation Cube"); sToolTipText = QT_TR_NOOP("Drag and place NaviCube"); sWhatsThis = ""; sStatusTip = sToolTipText; eType = Alter3DView; } void NaviCubeDraggableCmd::activated(int iMsg) { auto view = qobject_cast(getMainWindow()->activeWindow()); view->getViewer()->getNaviCube()->setDraggable(iMsg == 1 ? true : false); } bool NaviCubeDraggableCmd::isActive() { Gui::MDIView* view = Gui::getMainWindow()->activeWindow(); if (view && view->isDerivedFrom()) { bool check = _pcAction->isChecked(); auto view = qobject_cast(getMainWindow()->activeWindow()); bool mode = view->getViewer()->getNaviCube()->isDraggable(); if (mode != check) { _pcAction->setChecked(mode); } return true; } return false; } Gui::Action* NaviCubeDraggableCmd::createAction() { Gui::Action* pcAction = Command::createAction(); pcAction->setCheckable(true); return pcAction; } QMenu* NaviCubeImplementation::createNaviCubeMenu() { auto menu = new QMenu(getMainWindow()); menu->setObjectName(str("NaviCube_Menu")); CommandManager& rcCmdMgr = Application::Instance->commandManager(); static bool init = true; if (init) { init = false; rcCmdMgr.addCommand(new NaviCubeDraggableCmd); } vector commands = NaviCubeImplementation::m_commands; if (commands.empty()) { commands.emplace_back("Std_OrthographicCamera"); commands.emplace_back("Std_PerspectiveCamera"); commands.emplace_back("Std_ViewIsometric"); commands.emplace_back("Separator"); commands.emplace_back("Std_ViewFitAll"); commands.emplace_back("Std_ViewFitSelection"); commands.emplace_back("Std_AlignToSelection"); commands.emplace_back("Separator"); commands.emplace_back("NaviCubeDraggableCmd"); } for (const auto& command : commands) { if (command == "Separator") { menu->addSeparator(); } else { Command* cmd = rcCmdMgr.getCommandByName(command.c_str()); if (cmd) { cmd->addTo(menu); } } } return menu; }