FreeCAD
1/***************************************************************************
2* Copyright (c) 2015 Victor Titov (DeepSOIC) <vv.titov@gmail.com> *
3* *
4* This file is part of the FreeCAD CAx development system. *
5* *
6* This library is free software; you can redistribute it and/or *
7* modify it under the terms of the GNU Library General Public *
8* License as published by the Free Software Foundation; either *
9* version 2 of the License, or (at your option) any later version. *
10* *
11* This library is distributed in the hope that it will be useful, *
12* but WITHOUT ANY WARRANTY; without even the implied warranty of *
13* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14* GNU Library General Public License for more details. *
15* *
16* You should have received a copy of the GNU Library General Public *
17* License along with this library; see the file COPYING.LIB. If not, *
18* write to the Free Software Foundation, Inc., 59 Temple Place, *
19* Suite 330, Boston, MA 02111-1307, USA *
20* *
21***************************************************************************
22* *
23* Minor modifications made by Pablo Gil (pablogil) in order to create *
24* a Maya or Unity 3D mouse navigation style: *
25* ALT + left mouse button = orbit *
26* ALT + right mouse button = zoom *
27* ALT + middle mouse button = pan *
28* *
29* Thanks Victor for your help! *
30* *
31***************************************************************************/
32
33/*
34*A few notes on this style. (by DeepSOIC)
35*
36* In this style, LMB serves dual purpose. It is selecting objects, as well as
37* spinning the view. The trick that enables it is as follows: The mousedown
38* event is consumed an saved, but otherwise remains unprocessed. If a drag is
39* detected while the button is down, the event is finally consumed (the saved
40* one is discarded), and spinning starts. If there is no drag detected before
41* the button is released, the saved mousedown is propagated to inherited,
42* followed by the mouseup. The same trick is used for RMB, so up to two
43* mousedown can be postponed.
44*
45* This navigation style does not exactly follow the structure of other
46* navigation styles, it does not fill many of the global variables defined in
47* NavigationStyle.
48*
49* This mode does not support locking cursor position on screen when
50* navigating, since with absolute pointing devices like pen and touch it makes
51* no sense (this style was specifically crafted for such devices).
52*
53* In this style, setViewing is not used (because I could not figure out how to
54* use it properly, and it seems to just work without it).
55*
56* This style wasn't tested with space during development (I don't have one).
57*/
58
59#include "PreCompiled.h"60#ifndef _PreComp_61# include <QApplication>62#endif63
64#include <Base/Console.h>65
66#include "NavigationStyle.h"67#include "SoTouchEvents.h"68#include "View3DInventorViewer.h"69
70
71using namespace Gui;72
73// ----------------------------------------------------------------------------------
74
75/* TRANSLATOR Gui::MayaGestureNavigationStyle */
76
77TYPESYSTEM_SOURCE(Gui::MayaGestureNavigationStyle, Gui::UserNavigationStyle)78
79MayaGestureNavigationStyle::MayaGestureNavigationStyle()80{
81mouseMoveThreshold = QApplication::startDragDistance();82mouseMoveThresholdBroken = false;83mousedownConsumedCount = 0;84thisClickIsComplex = false;85inGesture = false;86}
87
88MayaGestureNavigationStyle::~MayaGestureNavigationStyle() = default;89
90const char* MayaGestureNavigationStyle::mouseButtons(ViewerMode mode)91{
92switch (mode) {93case NavigationStyle::SELECTION:94return QT_TR_NOOP("Tap OR click left mouse button.");95case NavigationStyle::PANNING:96return QT_TR_NOOP("Drag screen with two fingers OR press ALT + middle mouse button.");97case NavigationStyle::DRAGGING:98return QT_TR_NOOP("Drag screen with one finger OR press ALT + left mouse button. In Sketcher and other edit modes, hold Alt in addition.");99case NavigationStyle::ZOOMING:100return QT_TR_NOOP("Pinch (place two fingers on the screen and drag them apart from or towards each other) OR scroll middle mouse button OR press ALT + right mouse button OR PgUp/PgDown on keyboard.");101default:102return "No description";103}104}
105
106/*!
107* \brief MayaGestureNavigationStyle::testMoveThreshold tests if the mouse has moved far enough to constder it a drag.
108* \param currentPos current position of mouse cursor, in local pixel coordinates.
109* \return true if the mouse was moved far enough. False if it's within the boundary. Ignores MayaGestureNavigationStyle::mouseMoveThresholdBroken flag.
110*/
111bool MayaGestureNavigationStyle::testMoveThreshold(const SbVec2s currentPos) const {112SbVec2s movedBy = currentPos - this->mousedownPos;113return SbVec2f(movedBy).length() >= this->mouseMoveThreshold;114}
115
116SbBool MayaGestureNavigationStyle::processSoEvent(const SoEvent * const ev)117{
118// Events when in "ready-to-seek" mode are ignored, except those119// which influence the seek mode itself -- these are handled further120// up the inheritance hierarchy.121if (this->isSeekMode()) {122return inherited::processSoEvent(ev);123}124// Switch off viewing mode (Bug #0000911)125if (!this->isSeekMode()&& !this->isAnimating() && this->isViewing() )126this->setViewing(false); // by default disable viewing mode to render the scene127//setViewing() is never used in this style, so the previous if is very unlikely to be hit.128
129const SoType type(ev->getTypeId());130//define some shortcuts...131bool evIsButton = type.isDerivedFrom(SoMouseButtonEvent::getClassTypeId());132bool evIsKeyboard = type.isDerivedFrom(SoKeyboardEvent::getClassTypeId());133bool evIsLoc2 = type.isDerivedFrom(SoLocation2Event::getClassTypeId());//mouse movement134bool evIsLoc3 = type.isDerivedFrom(SoMotion3Event::getClassTypeId());//spaceball/joystick movement135bool evIsGesture = type.isDerivedFrom(SoGestureEvent::getClassTypeId());//touchscreen gesture136
137const SbVec2f prevnormalized = this->lastmouseposition;138const SbVec2s pos(ev->getPosition());//not valid for gestures139const SbVec2f posn = this->normalizePixelPos(pos);140//pos: local coordinates of event, in pixels141//posn: normalized local coordinates of event ((0,0) = lower left corner, (1,1) = upper right corner)142float ratio = viewer->getSoRenderManager()->getViewportRegion().getViewportAspectRatio();143
144if (evIsButton || evIsLoc2){145this->lastmouseposition = posn;146}147
148const ViewerMode curmode = this->currentmode;149//ViewerMode newmode = curmode;150
151//make a unified mouse+modifiers state value (combo)152enum {153BUTTON1DOWN = 1 << 0,154BUTTON2DOWN = 1 << 1,155BUTTON3DOWN = 1 << 2,156CTRLDOWN = 1 << 3,157SHIFTDOWN = 1 << 4,158ALTDOWN = 1 << 5,159MASKBUTTONS = BUTTON1DOWN | BUTTON2DOWN | BUTTON3DOWN,160MASKMODIFIERS = CTRLDOWN | SHIFTDOWN | ALTDOWN161};162unsigned int comboBefore = //before = state before this event163(this->button1down ? BUTTON1DOWN : 0) |164(this->button2down ? BUTTON2DOWN : 0) |165(this->button3down ? BUTTON3DOWN : 0) |166(this->ctrldown ? CTRLDOWN : 0) |167(this->shiftdown ? SHIFTDOWN : 0) |168(this->altdown ? ALTDOWN : 0);169
170//test for complex clicks171int cntMBBefore = (comboBefore & BUTTON1DOWN ? 1 : 0 ) //cntMBBefore = how many buttons were down when this event arrived?172+(comboBefore & BUTTON2DOWN ? 1 : 0 )173+(comboBefore & BUTTON3DOWN ? 1 : 0 );174if (cntMBBefore>=2) this->thisClickIsComplex = true;175if (cntMBBefore==0) {//a good chance to reset some click-related stuff176this->thisClickIsComplex = false;177this->mousedownConsumedCount = 0;//shouldn't be necessary, just a fail-safe.178}179
180// Mismatches in state of the modifier keys happens if the user181// presses or releases them outside the viewer window.182syncModifierKeys(ev);183//before this block, mouse button states in NavigationStyle::buttonXdown reflected those before current event arrived.184//track mouse button states185if (evIsButton) {186auto const event = (const SoMouseButtonEvent *) ev;187const int button = event->getButton();188const SbBool press //the button was pressed (if false -> released)189= event->getState() == SoButtonEvent::DOWN ? true : false;190switch (button) {191case SoMouseButtonEvent::BUTTON1:192this->button1down = press;193break;194case SoMouseButtonEvent::BUTTON2:195this->button2down = press;196break;197case SoMouseButtonEvent::BUTTON3:198this->button3down = press;199break;200//whatever else, we don't track201}202}203//after this block, the new states of the buttons are already in.204
205unsigned int comboAfter = //after = state after this event (current, essentially)206(this->button1down ? BUTTON1DOWN : 0) |207(this->button2down ? BUTTON2DOWN : 0) |208(this->button3down ? BUTTON3DOWN : 0) |209(this->ctrldown ? CTRLDOWN : 0) |210(this->shiftdown ? SHIFTDOWN : 0) |211(this->altdown ? ALTDOWN : 0);212
213//test for complex clicks (again)214int cntMBAfter = (comboAfter & BUTTON1DOWN ? 1 : 0 ) //cntMBAfter = how many buttons were down when this event arrived?215+(comboAfter & BUTTON2DOWN ? 1 : 0 )216+(comboAfter & BUTTON3DOWN ? 1 : 0 );217if (cntMBAfter>=2) this->thisClickIsComplex = true;218//if (cntMBAfter==0) this->thisClickIsComplex = false;//don't reset the flag now, we need to know that this mouseUp was an end of a click that was complex. The flag will reset by the before-check in the next event.219
220//test for move detection221if (evIsLoc2 || evIsButton){222this->mouseMoveThresholdBroken |= this->testMoveThreshold(pos);223}224
225//track gestures226if (evIsGesture) {227auto gesture = static_cast<const SoGestureEvent*>(ev);228switch(gesture->state) {229case SoGestureEvent::SbGSStart:230//assert(!inGesture);//start of another gesture before the first finished? Happens all the time for Pan gesture... No idea why! --DeepSOIC231inGesture = true;232break;233case SoGestureEvent::SbGSUpdate:234assert(inGesture);//gesture update without start?235inGesture = true;236break;237case SoGestureEvent::SbGSEnd:238assert(inGesture);//gesture ended without starting?239inGesture = false;240break;241case SoGestureEvent::SbGsCanceled:242assert(inGesture);//gesture canceled without starting?243inGesture=false;244break;245default:246assert(0);//shouldn't happen247inGesture = false;248}249}250if (evIsButton) {251if(inGesture){252inGesture = false;//reset the flag when mouse clicks are received, to ensure enabling mouse navigation back.253setViewingMode(NavigationStyle::SELECTION);//exit navigation asap, to proceed with regular processing of the click254}255}256
257bool suppressLMBDrag = false;258if(viewer->isEditing()){259//in edit mode, disable lmb dragging (spinning). Holding Alt enables it.260suppressLMBDrag = !(comboAfter & ALTDOWN);261}262
263//----------all this were preparations. Now comes the event handling! ----------264
265SbBool processed = false;//a return value for the BlahblahblahNavigationStyle::processSoEvent266bool propagated = false;//an internal flag indicating that the event has been already passed to inherited, to suppress the automatic doing of this at the end.267//goto finalize = return processed. Might be important to do something before done (none now).268
269// give the nodes in the foreground root the chance to handle events (e.g color bar)270if (!viewer->isEditing()) {271processed = handleEventInForeground(ev);272}273if (processed)274goto finalize;275
276// Mode-independent keyboard handling277if (evIsKeyboard) {278auto const event = (const SoKeyboardEvent *) ev;279const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;280switch (event->getKey()) {281case SoKeyboardEvent::H:282processed = true;283if (!press) {284setupPanningPlane(viewer->getCamera());285lookAtPoint(event->getPosition());286}287break;288default:289break;290}291}292if (processed)293goto finalize;294
295//mode-independent spaceball/joystick handling296if (evIsLoc3) {297auto const event = static_cast<const SoMotion3Event *>(ev);298if (event)299this->processMotionEvent(event);300processed = true;301}302if (processed)303goto finalize;304
305//all mode-dependent stuff is within this switch.306switch(curmode){307case NavigationStyle::SELECTION:308// Prevent interrupting rubber-band selection in sketcher309if (viewer->isEditing()) {310if (evIsButton) {311auto const event = (const SoMouseButtonEvent*)ev;312const SbBool press = event->getState() == SoButtonEvent::DOWN;313const int button = event->getButton();314
315if (!press && button == SoMouseButtonEvent::BUTTON1) {316setViewingMode(NavigationStyle::IDLE);317break;318}319}320
321if (this->button1down) {322break;323}324}325[[fallthrough]];326case NavigationStyle::IDLE:327// Prevent interrupting rubber-band selection in sketcher328if (viewer->isEditing()) {329if (evIsButton) {330auto const event = (const SoMouseButtonEvent*)ev;331const SbBool press = event->getState() == SoButtonEvent::DOWN;332const int button = event->getButton();333
334if (press && button == SoMouseButtonEvent::BUTTON1 && !this->altdown) {335setViewingMode(NavigationStyle::SELECTION);336break;337}338}339}340[[fallthrough]];341case NavigationStyle::INTERACT: {342//idle and interaction343
344//keyboard345if (evIsKeyboard) {346auto const event = (const SoKeyboardEvent *) ev;347const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;348
349switch(event->getKey()){350case SoKeyboardEvent::S:351case SoKeyboardEvent::HOME:352case SoKeyboardEvent::LEFT_ARROW:353case SoKeyboardEvent::UP_ARROW:354case SoKeyboardEvent::RIGHT_ARROW:355case SoKeyboardEvent::DOWN_ARROW:356processed = inherited::processSoEvent(ev);357propagated = true;358break;359case SoKeyboardEvent::PAGE_UP:360if(press){361doZoom(viewer->getSoRenderManager()->getCamera(), getDelta(), posn);362}363processed = true;364break;365case SoKeyboardEvent::PAGE_DOWN:366if(press){367doZoom(viewer->getSoRenderManager()->getCamera(), -getDelta(), posn);368}369processed = true;370break;371default:372break;373}//switch key374}375if (processed)376goto finalize;377
378
379// Mouse Button / Spaceball Button handling380if (evIsButton) {381auto const event = (const SoMouseButtonEvent *) ev;382const int button = event->getButton();383const SbBool press //the button was pressed (if false -> released)384= event->getState() == SoButtonEvent::DOWN ? true : false;385switch(button){386case SoMouseButtonEvent::BUTTON1:387case SoMouseButtonEvent::BUTTON2:388if(press){389if(this->thisClickIsComplex && this->mouseMoveThresholdBroken){390//this should prevent re-attempts to enter navigation when doing more clicks after a move.391} else {392//on LMB-down or RMB-down, we don't know yet if we should propagate it or process it. Save the event to be refired later, when it becomes clear.393//reset/start move detection machine394this->mousedownPos = pos;395this->mouseMoveThresholdBroken = false;396setupPanningPlane(viewer->getSoRenderManager()->getCamera());//set up panningplane397int &cnt = this->mousedownConsumedCount;398this->mousedownConsumedEvents[cnt] = *event;//hopefully, a shallow copy is enough. There are no pointers stored in events, apparently. Will lose a subclass, though.399cnt++;400assert(cnt<=2);401if(cnt>static_cast<int>(sizeof(mousedownConsumedEvents))){402cnt=sizeof(mousedownConsumedEvents);//we are in trouble403}404processed = true;//just consume this event, and wait for the move threshold to be broken to start dragging/panning405}406} else {//release407if (button == SoMouseButtonEvent::BUTTON2 && !this->thisClickIsComplex) {408if (!viewer->isEditing() && this->isPopupMenuEnabled()) {409processed=true;410this->openPopupMenu(event->getPosition());411}412}413if(! processed) {414//re-synthesize all previously-consumed mouseDowns, if any. They might have been re-synthesized already when threshold was broken.415for( int i=0; i < this->mousedownConsumedCount; i++ ){416inherited::processSoEvent(& (this->mousedownConsumedEvents[i]));//simulate the previously-comsumed mousedown.417}418this->mousedownConsumedCount = 0;419processed = inherited::processSoEvent(ev);//explicitly, just for clarity that we are sending a full click sequence.420propagated = true;421}422}423break;424case SoMouseButtonEvent::BUTTON3://press the wheel425// starts PANNING mode426if(press & this->altdown){427setViewingMode(NavigationStyle::PANNING);428} else if(press){429// if not PANNING then look at point430setupPanningPlane(viewer->getCamera());431lookAtPoint(event->getPosition());432}433processed = true;434break;435}436}437
438//mouse moves - test for move threshold breaking439if (evIsLoc2) {440if (this->mouseMoveThresholdBroken && (this->button1down || this->button2down) && mousedownConsumedCount > 0) {441//mousemovethreshold has JUST been broken442
443//test if we should enter navigation444if ((this->button1down && !suppressLMBDrag && this->altdown) || (this->button2down && this->altdown)) {445//yes, we are entering navigation.446//throw away consumed mousedowns.447this->mousedownConsumedCount = 0;448
449// start DRAGGING mode (orbit)450// if not pressing left mouse button then it assumes is right mouse button and starts ZOOMING mode451saveCursorPosition(ev);452setViewingMode(this->button1down ? NavigationStyle::DRAGGING : NavigationStyle::ZOOMING);453processed = true;454} else {455//no, we are not entering navigation.456//re-synthesize all previously-consumed mouseDowns, if any, and propagate this mousemove.457for( int i=0; i < this->mousedownConsumedCount; i++ ){458inherited::processSoEvent(& (this->mousedownConsumedEvents[i]));//simulate the previously-comsumed mousedown.459}460this->mousedownConsumedCount = 0;461processed = inherited::processSoEvent(ev);//explicitly, just for clarity that we are sending a full click sequence.462propagated = true;463}464}465if (mousedownConsumedCount > 0)466processed = true;//if we are still deciding if it's a drag or not, consume mouseMoves.467}468
469//gesture start470if (evIsGesture && /*!this->button1down &&*/ !this->button2down){//ignore gestures when mouse buttons are down. Button1down check was disabled because of wrong state after doubleclick on sketcher constraint to edit datum471auto gesture = static_cast<const SoGestureEvent*>(ev);472if (gesture->state == SoGestureEvent::SbGSStart473|| gesture->state == SoGestureEvent::SbGSUpdate) {//even if we didn't get a start, assume the first update is a start (sort-of fail-safe).474if (type.isDerivedFrom(SoGesturePanEvent::getClassTypeId())) {475setupPanningPlane(viewer->getSoRenderManager()->getCamera());//set up panning plane476setViewingMode(NavigationStyle::PANNING);477processed = true;478} else if (type.isDerivedFrom(SoGesturePinchEvent::getClassTypeId())) {479setupPanningPlane(viewer->getSoRenderManager()->getCamera());//set up panning plane480saveCursorPosition(ev);481setViewingMode(NavigationStyle::DRAGGING);482processed = true;483} //all other gestures - ignore!484}485}486
487//loc2 (mousemove) - ignore.488
489} break;//end of idle and interaction490case NavigationStyle::DRAGGING:491case NavigationStyle::ZOOMING:492case NavigationStyle::PANNING:{493//actual navigation494
495//no keyboard.496
497// Mouse Button / Spaceball Button handling498if (evIsButton) {499auto const event = (const SoMouseButtonEvent *) ev;500const int button = event->getButton();501switch(button){502case SoMouseButtonEvent::BUTTON1:503case SoMouseButtonEvent::BUTTON2:504case SoMouseButtonEvent::BUTTON3: // allows to release button3 into SELECTION mode505if(comboAfter & BUTTON1DOWN || comboAfter & BUTTON2DOWN) {506//don't leave navigation till all buttons have been released507if (comboAfter & BUTTON1DOWN && comboAfter & BUTTON2DOWN) {508setRotationCenter(getFocalPoint());509}510else {511saveCursorPosition(ev);512}513setViewingMode((comboAfter & BUTTON1DOWN) ? NavigationStyle::DRAGGING : NavigationStyle::PANNING);514processed = true;515} else { //all buttons are released516//end of dragging/panning/whatever517setViewingMode(NavigationStyle::SELECTION);518processed = true;519} //end of else (some buttons down)520break;521} //switch(button)522} //if(evIsButton)523
524//the essence part 1!525//mouse movement into camera motion. Suppress if in gesture. Ignore until threshold is surpassed.526if (evIsLoc2 && ! this->inGesture && this->mouseMoveThresholdBroken) {527if (curmode == NavigationStyle::ZOOMING) {//doesn't happen528this->zoomByCursor(posn, prevnormalized);529processed = true;530} else if (curmode == NavigationStyle::PANNING) {531panCamera(viewer->getSoRenderManager()->getCamera(), ratio, this->panningplane, posn, prevnormalized);532processed = true;533} else if (curmode == NavigationStyle::DRAGGING) {534if (comboAfter & BUTTON1DOWN && comboAfter & BUTTON2DOWN) {535//two mouse buttons down - tilting!536NavigationStyle::doRotate(viewer->getSoRenderManager()->getCamera(),537(posn - prevnormalized)[0]*(-2),538SbVec2f(0.5,0.5));539processed = true;540} else {//one mouse button - normal spinning541//this will also handle the single-finger drag (there's no gesture used, pseudomouse is enough)542//this->addToLog(event->getPosition(), event->getTime());543this->spin_simplified(viewer->getSoRenderManager()->getCamera(),544posn, prevnormalized);545processed = true;546}547}548}549
550//the essence part 2!551//gesture into camera motion552if (evIsGesture){553auto gesture = static_cast<const SoGestureEvent*>(ev);554assert(gesture);555if (gesture->state == SoGestureEvent::SbGSEnd) {556setViewingMode(NavigationStyle::SELECTION);557processed=true;558} else if (gesture->state == SoGestureEvent::SbGSUpdate){559if(type.isDerivedFrom(SoGesturePinchEvent::getClassTypeId())){560auto const event = static_cast<const SoGesturePinchEvent*>(ev);561if (this->zoomAtCursor){562//this is just dealing with the pan part of pinch gesture. Taking care of zooming to pos is done in doZoom.563SbVec2f panDist = this->normalizePixelPos(event->deltaCenter.getValue());564NavigationStyle::panCamera(viewer->getSoRenderManager()->getCamera(), ratio, this->panningplane, panDist, SbVec2f(0,0));565}566NavigationStyle::doZoom(viewer->getSoRenderManager()->getCamera(),-logf(event->deltaZoom),this->normalizePixelPos(event->curCenter));567if (event->deltaAngle != 0)568NavigationStyle::doRotate(viewer->getSoRenderManager()->getCamera(),event->deltaAngle,this->normalizePixelPos(event->curCenter));569processed = true;570}571if(type.isDerivedFrom(SoGesturePanEvent::getClassTypeId())){572auto const event = static_cast<const SoGesturePanEvent*>(ev);573//this is just dealing with the pan part of pinch gesture. Taking care of zooming to pos is done in doZoom.574SbVec2f panDist = this->normalizePixelPos(event->deltaOffset);575NavigationStyle::panCamera(viewer->getSoRenderManager()->getCamera(), ratio, this->panningplane, panDist, SbVec2f(0,0));576processed = true;577}578} else {579//shouldn't happen. Gestures are not expected to start in the middle of navigation.580//we'll consume it, without reacting.581processed=true;582}583}584
585} break;//end of actual navigation586case NavigationStyle::SEEK_WAIT_MODE:{587if (evIsButton) {588auto const event = (const SoMouseButtonEvent *) ev;589const int button = event->getButton();590const SbBool press = event->getState() == SoButtonEvent::DOWN ? true : false;591if (button == SoMouseButtonEvent::BUTTON1 && press) {592this->seekToPoint(pos); // implicitly calls interactiveCountInc()593this->setViewingMode(NavigationStyle::SEEK_MODE);594processed = true;595}596}597} ; //not end of SEEK_WAIT_MODE. Fall through by design!!!598/* FALLTHRU */599case NavigationStyle::SPINNING:600case NavigationStyle::SEEK_MODE: {601//animation modes602if (!processed) {603if (evIsButton || evIsGesture || evIsKeyboard || evIsLoc3)604setViewingMode(NavigationStyle::SELECTION);605}606} break; //end of animation modes607case NavigationStyle::BOXZOOM:608default:609//all the rest - will be pass on to inherited, later.610break;611}612
613if (! processed && ! propagated) {614processed = inherited::processSoEvent(ev);615propagated = true;616}617
618//-----------------------end of event handling---------------------619finalize:620return processed;621}
622