diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index b3fff2e403..d6b7d6850d 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -938,6 +938,7 @@ SET(Navigation_CPP_SRCS Navigation/CADNavigationStyle.cpp Navigation/RevitNavigationStyle.cpp Navigation/BlenderNavigationStyle.cpp + Navigation/SiemensNXStyle.cpp Navigation/SolidWorksNavigationStyle.cpp Navigation/MayaGestureNavigationStyle.cpp Navigation/OpenCascadeNavigationStyle.cpp diff --git a/src/Gui/Navigation/NavigationStyle.h b/src/Gui/Navigation/NavigationStyle.h index be54296471..e606b179b9 100644 --- a/src/Gui/Navigation/NavigationStyle.h +++ b/src/Gui/Navigation/NavigationStyle.h @@ -51,6 +51,7 @@ class SoCamera; class SoSensor; class SbSphereSheetProjector; +// NOLINTBEGIN(cppcoreguidelines-avoid*, readability-avoid-const-params-in-decls) namespace Gui { class View3DInventorViewer; @@ -489,7 +490,38 @@ protected: SbBool processSoEvent(const SoEvent * const ev) override; }; +class GuiExport SiemensNXStyle : public UserNavigationStyle { + using inherited = UserNavigationStyle; + + TYPESYSTEM_HEADER_WITH_OVERRIDE(); + +public: + SiemensNXStyle(); + ~SiemensNXStyle() override; + const char* mouseButtons(ViewerMode mode) override; + std::string userFriendlyName() const override; + +protected: + SbBool processSoEvent(const SoEvent * const ev) override; + SbBool processKeyboardEvent(const SoKeyboardEvent * const event) override; + +private: + struct Event; + struct NaviMachine; + struct IdleState; + struct AwaitingReleaseState; + struct AwaitingMoveState; + struct InteractState; + struct RotateState; + struct PanState; + struct ZoomState; + struct SelectionState; + + std::unique_ptr naviMachine; +}; + } // namespace Gui +// NOLINTEND(cppcoreguidelines-avoid*, readability-avoid-const-params-in-decls) Q_DECLARE_OPERATORS_FOR_FLAGS(Gui::NavigationStyle::RotationCenterModes) diff --git a/src/Gui/Navigation/SiemensNXStyle.cpp b/src/Gui/Navigation/SiemensNXStyle.cpp new file mode 100644 index 0000000000..4420321e26 --- /dev/null +++ b/src/Gui/Navigation/SiemensNXStyle.cpp @@ -0,0 +1,639 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2025 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#endif + +#include +#include +#include + +#include "Camera.h" +#include "NavigationStyle.h" +#include "View3DInventorViewer.h" + +// NOLINTBEGIN(cppcoreguidelines-pro-type-static-cast-downcast, +// cppcoreguidelines-avoid*, +// readability-avoid-const-params-in-decls) +using namespace Gui; +namespace sc = boost::statechart; +using NS = SiemensNXStyle; + +struct NS::Event: public sc::event +{ + Event() : flags(new Flags){} + using Button = SoMouseButtonEvent::Button; + using Key = SoKeyboardEvent::Key; + bool isMouseButtonEvent() const { + return this->inventor_event->isOfType(SoMouseButtonEvent::getClassTypeId()); + } + const SoMouseButtonEvent* asMouseButtonEvent() const { + return static_cast(this->inventor_event); + } + bool isPress(Button button) const { + return SoMouseButtonEvent::isButtonPressEvent(this->inventor_event, button); + } + bool isRelease(Button button) const { + return SoMouseButtonEvent::isButtonReleaseEvent(this->inventor_event, button); + } + bool isKeyPress(Key key) const { + return SoKeyboardEvent::isKeyPressEvent(this->inventor_event, key); + } + bool isKeyRelease(Key key) const { + return SoKeyboardEvent::isKeyReleaseEvent(this->inventor_event, key); + } + bool isKeyboardEvent() const { + return this->inventor_event->isOfType(SoKeyboardEvent::getClassTypeId()); + } + const SoKeyboardEvent* asKeyboardEvent() const { + return static_cast(this->inventor_event); + } + bool isLocation2Event() const { + return this->inventor_event->isOfType(SoLocation2Event::getClassTypeId()); + } + const SoLocation2Event* asLocation2Event() const { + return static_cast(this->inventor_event); + } + bool isMotion3Event() const { + return this->inventor_event->isOfType(SoMotion3Event::getClassTypeId()); + } + const SoMotion3Event* asMotion3Event() const { + return static_cast(this->inventor_event); + } + bool isDownButton(unsigned int state) const { + return mbstate() == state; + } + bool isDownNoButton() const { + return mbstate() == 0; + } + bool isDownButton1() const { + return (mbstate() & BUTTON1DOWN) == BUTTON1DOWN; + } + bool isDownButton2() const { + return (mbstate() & BUTTON2DOWN) == BUTTON2DOWN; + } + bool isDownButton3() const { + return (mbstate() & BUTTON3DOWN) == BUTTON3DOWN; + } + bool isDownControl() const { + return (kbstate() & CTRLDOWN) == CTRLDOWN; + } + bool isDownShift() const { + return (kbstate() & SHIFTDOWN) == SHIFTDOWN; + } + bool isDownAlt() const { + return (kbstate() & ALTDOWN) == ALTDOWN; + } + + enum { + // bits: 0-shift-ctrl-alt-0-lmb-mmb-rmb + BUTTON1DOWN = 0x00000100, + BUTTON2DOWN = 0x00000001, + BUTTON3DOWN = 0x00000010, + CTRLDOWN = 0x00100000, + SHIFTDOWN = 0x01000000, + ALTDOWN = 0x00010000, + MASKBUTTONS = BUTTON1DOWN | BUTTON2DOWN | BUTTON3DOWN, + MASKMODIFIERS = CTRLDOWN | SHIFTDOWN | ALTDOWN + }; + + const SoEvent* inventor_event{nullptr}; + unsigned int modifiers{0}; + unsigned int mbstate() const {return modifiers & MASKBUTTONS;} + unsigned int kbstate() const {return modifiers & MASKMODIFIERS;} + + struct Flags{ + bool processed = false; + bool propagated = false; + }; + std::shared_ptr flags; +}; + +struct NS::NaviMachine: public sc::state_machine +{ + using superclass = sc::state_machine; + explicit NaviMachine(NS& ns) : ns(ns) {} + NS& ns; +}; + +struct NS::IdleState: public sc::state +{ + using reactions = sc::custom_reaction; + explicit IdleState(my_context ctx) : my_base(ctx) + { + auto &ns = this->outermost_context().ns; + ns.setViewingMode(NavigationStyle::IDLE); + } + sc::result react(const NS::Event& ev) + { + auto &ns = this->outermost_context().ns; + switch (ns.getViewingMode()) { + case NavigationStyle::SEEK_WAIT_MODE: + { + if (ev.isPress(SoMouseButtonEvent::BUTTON1)) { + ns.seekToPoint(ev.inventor_event->getPosition()); + ns.setViewingMode(NavigationStyle::SEEK_MODE); + ev.flags->processed = true; + return transit(); + } + break; + } + case NavigationStyle::SPINNING: + case NavigationStyle::SEEK_MODE: + { + if (!ev.flags->processed) { + if (ev.isMouseButtonEvent()) { + ev.flags->processed = true; + return transit(); + } + else if (ev.isKeyboardEvent() || ev.isMotion3Event()) { + ns.setViewingMode(NavigationStyle::IDLE); + } + } + + break; + } + case NavigationStyle::BOXZOOM: + return forward_event(); + } + + // right-click + if (ev.isRelease(SoMouseButtonEvent::BUTTON2) + && ev.mbstate() == 0 + && !ns.viewer->isEditing() + && ns.isPopupMenuEnabled()){ + ns.openPopupMenu(ev.inventor_event->getPosition()); + } + + if (ev.isPress(SoMouseButtonEvent::BUTTON3)) { + if (ev.isDownShift()) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isDownButton(NS::Event::BUTTON3DOWN)) { + ev.flags->processed = true; + return transit(); + } + } + + // Use processClickEvent() + + // Implement selection callback + //if (ev.isLocation2Event() && ev.isDownButton1()) { + // ev.flags->processed = true; + // return transit(); + //} + + return forward_event(); + } +}; + +struct NS::AwaitingReleaseState : public sc::state +{ + using reactions = sc::custom_reaction; + explicit AwaitingReleaseState(my_context ctx) : my_base(ctx) + {} + + sc::result react(const NS::Event& /*ev*/) + { + return forward_event(); + } +}; + +struct NS::InteractState: public sc::state +{ + using reactions = sc::custom_reaction; + explicit InteractState(my_context ctx):my_base(ctx) + { + auto &ns = this->outermost_context().ns; + ns.setViewingMode(NavigationStyle::INTERACT); + } + + sc::result react(const NS::Event& /*ev*/) { + return forward_event(); + } +}; + +struct NS::AwaitingMoveState: public sc::state +{ + using reactions = sc::custom_reaction; + +private: + SbVec2s base_pos; + SbTime since; + +public: + explicit AwaitingMoveState(my_context ctx):my_base(ctx) + { + auto &ns = this->outermost_context().ns; + ns.setViewingMode(NavigationStyle::DRAGGING); + this->base_pos = static_cast(this->triggering_event())->inventor_event->getPosition(); + this->since = static_cast(this->triggering_event())->inventor_event->getTime(); + } + sc::result react(const NS::Event& ev){ + //this state consumes all mouse events. + ev.flags->processed = ev.isMouseButtonEvent() || ev.isLocation2Event(); + + if (ev.isLocation2Event()) { + return transit(); + } + + // right-click + if (ev.isPress(SoMouseButtonEvent::BUTTON2) && ev.isDownButton3()) { + return transit(); + } + + if (ev.isKeyPress(SoKeyboardEvent::LEFT_SHIFT)) { + ev.flags->processed = true; + return transit(); + } + + // left-click + if (ev.isPress(SoMouseButtonEvent::BUTTON1) && ev.isDownButton3()) { + return transit(); + } + + if (ev.isKeyPress(SoKeyboardEvent::LEFT_CONTROL)) { + ev.flags->processed = true; + return transit(); + } + + // middle-click + if (ev.isRelease(SoMouseButtonEvent::BUTTON3) && ev.isDownNoButton()) { + auto &ns = this->outermost_context().ns; + SbTime tmp = (ev.inventor_event->getTime() - this->since); + double dci = QApplication::doubleClickInterval() / 1000.0; + + // is this a simple middle click? + if (tmp.getValue() < dci) { + ev.flags->processed = true; + SbVec2s pos = ev.inventor_event->getPosition(); + ns.lookAtPoint(pos); + } + return transit(); + } + + return forward_event(); + } +}; + +struct NS::RotateState: public sc::state +{ + using reactions = sc::custom_reaction; + explicit RotateState(my_context ctx) : my_base(ctx) + { + auto &ns = this->outermost_context().ns; + const auto inventorEvent = static_cast(this->triggering_event())->inventor_event; + ns.saveCursorPosition(inventorEvent); + ns.setViewingMode(NavigationStyle::DRAGGING); + this->base_pos = inventorEvent->getPosition(); + } + + sc::result react(const NS::Event& ev) + { + if (ev.isLocation2Event()) { + auto &ns = this->outermost_context().ns; + ns.addToLog(ev.inventor_event->getPosition(), ev.inventor_event->getTime()); + const SbVec2s pos = ev.inventor_event->getPosition(); + const SbVec2f posn = ns.normalizePixelPos(pos); + ns.spin(posn); + ns.moveCursorPosition(); + ev.flags->processed = true; + } + + // right-click + if (ev.isPress(SoMouseButtonEvent::BUTTON2) && ev.isDownButton3()) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isKeyPress(SoKeyboardEvent::LEFT_SHIFT)) { + ev.flags->processed = true; + return transit(); + } + + // left-click + if (ev.isPress(SoMouseButtonEvent::BUTTON1) && ev.isDownButton3()) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isKeyPress(SoKeyboardEvent::LEFT_CONTROL)) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isRelease(SoMouseButtonEvent::BUTTON3) && ev.isDownNoButton()) { + ev.flags->processed = true; + return transit(); + } + return forward_event(); + } + +private: + SbVec2s base_pos; +}; + +struct NS::PanState: public sc::state +{ + using reactions = sc::custom_reaction; + explicit PanState(my_context ctx):my_base(ctx) + { + auto &ns = this->outermost_context().ns; + const NS::Event* ev = static_cast(this->triggering_event()); + ns.setViewingMode(NavigationStyle::PANNING); + this->base_pos = ev->inventor_event->getPosition(); + this->ratio = ns.viewer->getSoRenderManager()->getViewportRegion().getViewportAspectRatio(); + ns.centerTime = ev->inventor_event->getTime(); + ns.setupPanningPlane(ns.getCamera()); + } + sc::result react(const NS::Event& ev) + { + if (ev.isLocation2Event()){ + ev.flags->processed = true; + SbVec2s pos = ev.inventor_event->getPosition(); + auto &ns = this->outermost_context().ns; + ns.panCamera(ns.viewer->getSoRenderManager()->getCamera(), + this->ratio, + ns.panningplane, + ns.normalizePixelPos(pos), + ns.normalizePixelPos(this->base_pos)); + this->base_pos = pos; + } + + if (ev.isRelease(SoMouseButtonEvent::BUTTON2) && ev.isDownButton3()) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isKeyRelease(SoKeyboardEvent::LEFT_SHIFT) && ev.isDownButton3()) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isRelease(SoMouseButtonEvent::BUTTON3)) { + ev.flags->processed = true; + return transit(); + } + + return forward_event(); + } + +private: + SbVec2s base_pos; + float ratio {1.0F}; +}; + +struct NS::ZoomState: public sc::state +{ + using reactions = sc::custom_reaction; + explicit ZoomState(my_context ctx) : my_base(ctx) + { + auto &ns = this->outermost_context().ns; + const NS::Event* ev = static_cast(this->triggering_event()); + ns.setViewingMode(NavigationStyle::ZOOMING); + this->base_pos = ev->inventor_event->getPosition(); + } + + sc::result react(const NS::Event& ev) + { + if (ev.isLocation2Event()){ + ev.flags->processed = true; + SbVec2s pos = ev.inventor_event->getPosition(); + auto &ns = this->outermost_context().ns; + ns.zoomByCursor(ns.normalizePixelPos(pos), + ns.normalizePixelPos(this->base_pos)); + this->base_pos = pos; + } + + if (ev.isRelease(SoMouseButtonEvent::BUTTON1) && ev.isDownButton3()) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isKeyRelease(SoKeyboardEvent::LEFT_CONTROL) && ev.isDownButton3()) { + ev.flags->processed = true; + return transit(); + } + + if (ev.isRelease(SoMouseButtonEvent::BUTTON3)) { + ev.flags->processed = true; + return transit(); + } + + return forward_event(); + } + +private: + SbVec2s base_pos; +}; + +struct NS::SelectionState: public sc::state +{ + using reactions = sc::custom_reaction; + explicit SelectionState(my_context ctx) : my_base(ctx) + { + auto &ns = this->outermost_context().ns; + const NS::Event* ev = static_cast(this->triggering_event()); + + ns.setViewingMode(NavigationStyle::BOXZOOM); + ns.startSelection(NavigationStyle::Rubberband); + fakeLeftButtonDown(ev->inventor_event->getPosition()); + } + + void fakeLeftButtonDown(const SbVec2s& pos) + { + SoMouseButtonEvent mbe; + mbe.setButton(SoMouseButtonEvent::BUTTON1); + mbe.setState(SoMouseButtonEvent::DOWN); + mbe.setPosition(pos); + + auto &ns = this->outermost_context().ns; + ns.processEvent(&mbe); + } + + sc::result react(const NS::Event& /*ev*/) + { + // This isn't called while selection mode is active + return transit(); + } +}; + +// ---------------------------------------------------------------------------------- + +/* TRANSLATOR Gui::SiemensNXStyle */ + +TYPESYSTEM_SOURCE(Gui::SiemensNXStyle, Gui::UserNavigationStyle) + +SiemensNXStyle::SiemensNXStyle() + : naviMachine(new NS::NaviMachine(*this)) +{ + naviMachine->initiate(); +} + +SiemensNXStyle::~SiemensNXStyle() +{ + naviMachine.reset(); +} + +const char* SiemensNXStyle::mouseButtons(ViewerMode mode) +{ + switch (mode) { + case NavigationStyle::SELECTION: + return QT_TR_NOOP("Press left mouse button"); + case NavigationStyle::PANNING: + return QT_TR_NOOP("Press middle+right click"); + case NavigationStyle::DRAGGING: + return QT_TR_NOOP("Press middle mouse button"); + case NavigationStyle::ZOOMING: + return QT_TR_NOOP("Scroll mouse wheel"); + default: + return "No description"; + } +} + +std::string SiemensNXStyle::userFriendlyName() const +{ + return {"Siemens NX"}; +} + +SbBool SiemensNXStyle::processKeyboardEvent(const SoKeyboardEvent * const event) +{ + // See https://forum.freecad.org/viewtopic.php?t=96459 + // Isometric view: Home key button + // Trimetric view: End key button + // Fit all: CTRL+F + // Normal view: F8 + switch (event->getKey()) { + case SoKeyboardEvent::F: + if (event->wasCtrlDown()) { + viewer->viewAll(); + return true; + } + break; + case SoKeyboardEvent::HOME: + { + viewer->setCameraOrientation(Camera::rotation(Camera::Isometric)); + return true; + } + case SoKeyboardEvent::END: + { + viewer->setCameraOrientation(Camera::rotation(Camera::Trimetric)); + return true; + } + case SoKeyboardEvent::F8: + { + viewer->setCameraOrientation(Camera::rotation(Camera::Top)); + return true; + } + default: + break; + } + + return inherited::processKeyboardEvent(event); +} + +SbBool SiemensNXStyle::processSoEvent(const SoEvent * const ev) +{ + // Events when in "ready-to-seek" mode are ignored, except those + // which influence the seek mode itself -- these are handled further + // up the inheritance hierarchy. + if (this->isSeekMode()) { + return inherited::processSoEvent(ev); + } + + // Switch off viewing mode (Bug #0000911) + if (!this->isSeekMode() && !this->isAnimating() && this->isViewing()) { + this->setViewing(false); // by default disable viewing mode to render the scene + } + + // Mismatches in state of the modifier keys happens if the user + // presses or releases them outside the viewer window. + syncModifierKeys(ev); + + // give the nodes in the foreground root the chance to handle events (e.g color bar) + if (!viewer->isEditing()) { + if (handleEventInForeground(ev)) { + return true; + } + } + + NS::Event smev; + smev.inventor_event = ev; + + // Spaceball/joystick handling + if (ev->isOfType(SoMotion3Event::getClassTypeId())){ + smev.flags->processed = true; + this->processMotionEvent(static_cast(ev)); + return true; + } + + // Keyboard handling + if (ev->isOfType(SoKeyboardEvent::getClassTypeId())) { + const auto event = static_cast(ev); + smev.flags->processed = processKeyboardEvent(event); + } + + if (smev.isMouseButtonEvent()) { + const auto button = smev.asMouseButtonEvent()->getButton(); + const SbBool press = smev.asMouseButtonEvent()->getState() == SoButtonEvent::DOWN; + switch (button) { + case SoMouseButtonEvent::BUTTON1: + this->button1down = press; + break; + case SoMouseButtonEvent::BUTTON2: + this->button2down = press; + break; + case SoMouseButtonEvent::BUTTON3: + this->button3down = press; + break; + default: + break; + } + } + + smev.modifiers = + (this->button1down ? NS::Event::BUTTON1DOWN : 0) | + (this->button2down ? NS::Event::BUTTON2DOWN : 0) | + (this->button3down ? NS::Event::BUTTON3DOWN : 0) | + (this->ctrldown ? NS::Event::CTRLDOWN : 0) | + (this->shiftdown ? NS::Event::SHIFTDOWN : 0) | + (this->altdown ? NS::Event::ALTDOWN : 0); + + if (!smev.flags->processed) { + this->naviMachine->process_event(smev); + } + + if (!smev.flags->propagated && !smev.flags->processed) { + return inherited::processSoEvent(ev); + } + + return smev.flags->processed; +} +// NOLINTEND(cppcoreguidelines-pro-type-static-cast-downcast, +// cppcoreguidelines-avoid*, +// readability-avoid-const-params-in-decls) diff --git a/src/Gui/SoFCDB.cpp b/src/Gui/SoFCDB.cpp index 29e80fbe31..11e1a76212 100644 --- a/src/Gui/SoFCDB.cpp +++ b/src/Gui/SoFCDB.cpp @@ -199,6 +199,7 @@ void Gui::SoFCDB::init() OpenCascadeNavigationStyle ::init(); OpenSCADNavigationStyle ::init(); TinkerCADNavigationStyle ::init(); + SiemensNXStyle ::init(); GLGraphicsItem ::init(); GLFlagWindow ::init();