From 0977688583f87d1f35734c6771d088394fa2e834 Mon Sep 17 00:00:00 2001 From: Riku Avelar Date: Fri, 28 Oct 2016 16:36:45 -0400 Subject: [PATCH] Lab 1 End Version --- .gitignore | 43 + QGLViewer/ImageInterface.ui | 297 +++ QGLViewer/VRenderInterface.ui | 217 ++ QGLViewer/camera.cpp | 2073 +++++++++++++++++ QGLViewer/camera.h | 505 ++++ QGLViewer/config.h | 97 + QGLViewer/constraint.cpp | 291 +++ QGLViewer/constraint.h | 338 +++ QGLViewer/domUtils.h | 161 ++ QGLViewer/frame.cpp | 1144 ++++++++++ QGLViewer/frame.h | 415 ++++ QGLViewer/keyFrameInterpolator.cpp | 549 +++++ QGLViewer/keyFrameInterpolator.h | 351 +++ QGLViewer/manipulatedCameraFrame.cpp | 469 ++++ QGLViewer/manipulatedCameraFrame.h | 233 ++ QGLViewer/manipulatedFrame.cpp | 550 +++++ QGLViewer/manipulatedFrame.h | 335 +++ QGLViewer/mouseGrabber.cpp | 76 + QGLViewer/mouseGrabber.h | 264 +++ QGLViewer/qglviewer-icon.xpm | 359 +++ QGLViewer/qglviewer.cpp | 3171 ++++++++++++++++++++++++++ QGLViewer/qglviewer.h | 1278 +++++++++++ QGLViewer/qglviewer.icns | Bin 0 -> 185404 bytes QGLViewer/qglviewer_fr.qm | Bin 0 -> 14815 bytes QGLViewer/qglviewer_fr.ts | 608 +++++ QGLViewer/quaternion.cpp | 552 +++++ QGLViewer/quaternion.h | 311 +++ QGLViewer/saveSnapshot.cpp | 494 ++++ QGLViewer/vec.cpp | 164 ++ QGLViewer/vec.h | 390 ++++ log750-lab.pro | 73 + mainwindow.ui | 176 ++ src/glnodes/glnode.cpp | 17 + src/glnodes/glnode.h | 14 + src/glnodes/scenegroup.cpp | 42 + src/glnodes/scenegroup.h | 26 + src/glnodes/shapes.cpp | 44 + src/glnodes/shapes.h | 41 + src/interfaces/ivisitable.h | 10 + src/interfaces/visitor.h | 15 + src/main.cpp | 29 + src/shaders/basicShader.frag | 9 + src/shaders/basicShader.vert | 14 + src/viewer/simpleViewer.cpp | 444 ++++ src/viewer/simpleViewer.h | 95 + src/window/mainwindow.cpp | 35 + src/window/mainwindow.h | 30 + 47 files changed, 16849 insertions(+) create mode 100644 .gitignore create mode 100644 QGLViewer/ImageInterface.ui create mode 100644 QGLViewer/VRenderInterface.ui create mode 100644 QGLViewer/camera.cpp create mode 100644 QGLViewer/camera.h create mode 100644 QGLViewer/config.h create mode 100644 QGLViewer/constraint.cpp create mode 100644 QGLViewer/constraint.h create mode 100644 QGLViewer/domUtils.h create mode 100644 QGLViewer/frame.cpp create mode 100644 QGLViewer/frame.h create mode 100644 QGLViewer/keyFrameInterpolator.cpp create mode 100644 QGLViewer/keyFrameInterpolator.h create mode 100644 QGLViewer/manipulatedCameraFrame.cpp create mode 100644 QGLViewer/manipulatedCameraFrame.h create mode 100644 QGLViewer/manipulatedFrame.cpp create mode 100644 QGLViewer/manipulatedFrame.h create mode 100644 QGLViewer/mouseGrabber.cpp create mode 100644 QGLViewer/mouseGrabber.h create mode 100644 QGLViewer/qglviewer-icon.xpm create mode 100644 QGLViewer/qglviewer.cpp create mode 100644 QGLViewer/qglviewer.h create mode 100644 QGLViewer/qglviewer.icns create mode 100644 QGLViewer/qglviewer_fr.qm create mode 100644 QGLViewer/qglviewer_fr.ts create mode 100644 QGLViewer/quaternion.cpp create mode 100644 QGLViewer/quaternion.h create mode 100644 QGLViewer/saveSnapshot.cpp create mode 100644 QGLViewer/vec.cpp create mode 100644 QGLViewer/vec.h create mode 100644 log750-lab.pro create mode 100644 mainwindow.ui create mode 100644 src/glnodes/glnode.cpp create mode 100644 src/glnodes/glnode.h create mode 100644 src/glnodes/scenegroup.cpp create mode 100644 src/glnodes/scenegroup.h create mode 100644 src/glnodes/shapes.cpp create mode 100644 src/glnodes/shapes.h create mode 100644 src/interfaces/ivisitable.h create mode 100644 src/interfaces/visitor.h create mode 100644 src/main.cpp create mode 100644 src/shaders/basicShader.frag create mode 100644 src/shaders/basicShader.vert create mode 100644 src/viewer/simpleViewer.cpp create mode 100644 src/viewer/simpleViewer.h create mode 100644 src/window/mainwindow.cpp create mode 100644 src/window/mainwindow.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be80295 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Generated Bin and debug +debug/ +object_script.*.Debug +object_script.*.Release + +# C++ objects and libs + +*.slo +*.lo +*.o +*.a +*.la +*.lai +*.so +*.dll +*.dylib + +# Qt-es + +/.qmake.cache +/.qmake.stash +*.pro.user +*.pro.user.* +*.qbs.user +*.qbs.user.* +*.moc +moc_*.cpp +qrc_*.cpp +ui_*.h +Makefile* +*build-* + +# QtCreator + +*.autosave + +# QtCtreator Qml +*.qmlproject.user +*.qmlproject.user.* + +# QtCtreator CMake +CMakeLists.txt.user* + diff --git a/QGLViewer/ImageInterface.ui b/QGLViewer/ImageInterface.ui new file mode 100644 index 0000000..db85b69 --- /dev/null +++ b/QGLViewer/ImageInterface.ui @@ -0,0 +1,297 @@ + + + ImageInterface + + + + 0 + 0 + 298 + 204 + + + + Image settings + + + + 6 + + + 9 + + + + + 6 + + + 0 + + + + + Width + + + + + + + Width of the image (in pixels) + + + px + + + 1 + + + 32000 + + + + + + + Qt::Horizontal + + + + 20 + 22 + + + + + + + + Height + + + + + + + Height of the image (in pixels) + + + px + + + 1 + + + 32000 + + + + + + + + + 6 + + + 0 + + + + + Image quality + + + + + + + Between 0 (smallest files) and 100 (highest quality) + + + 0 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 6 + + + 0 + + + + + Oversampling + + + + + + + Antialiases image (when larger then 1.0) + + + x + + + 1 + + + 0.100000000000000 + + + 64.000000000000000 + + + 1.000000000000000 + + + 1.000000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Use white as background color + + + Use white background + + + + + + + When image aspect ratio differs from viewer's one, expand frustum as needed. Fits inside current frustum otherwise. + + + Expand frustum if needed + + + + + + + Qt::Vertical + + + + 20 + 16 + + + + + + + + 6 + + + 0 + + + + + Qt::Horizontal + + + + 131 + 31 + + + + + + + + OK + + + + + + + Cancel + + + + + + + + + + + okButton + clicked() + ImageInterface + accept() + + + 135 + 184 + + + 96 + 254 + + + + + cancelButton + clicked() + ImageInterface + reject() + + + 226 + 184 + + + 179 + 282 + + + + + diff --git a/QGLViewer/VRenderInterface.ui b/QGLViewer/VRenderInterface.ui new file mode 100644 index 0000000..e6e1a60 --- /dev/null +++ b/QGLViewer/VRenderInterface.ui @@ -0,0 +1,217 @@ + + + VRenderInterface + + + + 0 + 0 + 309 + 211 + + + + Vectorial rendering options + + + + 5 + + + 5 + + + 5 + + + 5 + + + + + Hidden polygons are also included in the output (usually twice bigger) + + + Include hidden parts + + + + + + + Back faces (non clockwise point ordering) are removed from the output + + + Cull back faces + + + + + + + Black and white rendering + + + Black and white + + + + + + + Use current background color instead of white + + + Color background + + + + + + + Fit output bounding box to current display + + + Tighten bounding box + + + + + + + 11 + + + 11 + + + 11 + + + 11 + + + + + Polygon depth sorting method + + + Sort method: + + + + + + + Polygon depth sorting method + + + 3 + + + + No sorting + + + + + BSP + + + + + Topological + + + + + Advanced topological + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 31 + 41 + + + + + + + + + + Save + + + + + + + Cancel + + + + + + + + + + SaveButton + CancelButton + includeHidden + cullBackFaces + blackAndWhite + colorBackground + tightenBBox + sortMethod + + + + + SaveButton + released() + VRenderInterface + accept() + + + 16 + 210 + + + 35 + 176 + + + + + CancelButton + released() + VRenderInterface + reject() + + + 225 + 198 + + + 211 + 180 + + + + + diff --git a/QGLViewer/camera.cpp b/QGLViewer/camera.cpp new file mode 100644 index 0000000..62474f4 --- /dev/null +++ b/QGLViewer/camera.cpp @@ -0,0 +1,2073 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "domUtils.h" +#include "camera.h" +#include "qglviewer.h" +#include "manipulatedCameraFrame.h" +#include + +using namespace std; +using namespace qglviewer; + +namespace { + + // Replacement for gluUnProject that uses QVector3D::unproject() + Vec unProject(const Vec& src, + const GLdouble matProjection[], + const GLdouble matModelView[], + const GLint viewport[]) + { + // Convert matrices to QMatrix4x4 + QMatrix4x4 projectionMatrix; + QMatrix4x4 modelViewMatrix; + for (int row=0; row<4; ++row) + { + for (int col=0; col<4; ++col) + { + int index = row + col*4; + projectionMatrix(row, col) = matProjection[index]; + modelViewMatrix(row, col) = matModelView[index]; + } + } + + // Unproject point + QVector3D point(src.x, src.y, src.z); + QRect vp(viewport[0], viewport[1], viewport[2], viewport[3]); + QVector3D result = point.unproject(modelViewMatrix, projectionMatrix, vp); + + return Vec(result.x(), result.y(), result.z()); + } + + // Replacement for gluProject that uses QVector3D::project() + Vec project(const Vec& src, + const GLdouble matProjection[], + const GLdouble matModelView[], + const GLint viewport[]) + { + // Convert matrices to QMatrix4x4 + QMatrix4x4 projectionMatrix; + QMatrix4x4 modelViewMatrix; + for (int row=0; row<4; ++row) + { + for (int col=0; col<4; ++col) + { + int index = row + col*4; + projectionMatrix(row, col) = matProjection[index]; + modelViewMatrix(row, col) = matModelView[index]; + } + } + + // Unproject point + QVector3D point(src.x, src.y, src.z); + QRect vp(viewport[0], viewport[1], viewport[2], viewport[3]); + QVector3D result = point.project(modelViewMatrix, projectionMatrix, vp); + + return Vec(result.x(), result.y(), result.z()); + } +} + +/*! Default constructor. + + sceneCenter() is set to (0,0,0) and sceneRadius() is set to 1.0. type() is Camera::PERSPECTIVE, + with a \c M_PI/4 fieldOfView(). + + See IODistance(), physicalDistanceToScreen(), physicalScreenWidth() and focusDistance() + documentations for default stereo parameter values. */ +Camera::Camera() + : frame_(NULL), fieldOfView_(M_PI/4.0), modelViewMatrixIsUpToDate_(false), projectionMatrixIsUpToDate_(false) +{ + // #CONNECTION# Camera copy constructor + interpolationKfi_ = new KeyFrameInterpolator; + // Requires the interpolationKfi_ + setFrame(new ManipulatedCameraFrame()); + + // #CONNECTION# All these default values identical in initFromDOMElement. + + // Requires fieldOfView() to define focusDistance() + setSceneRadius(1.0); + + // Initial value (only scaled after this) + orthoCoef_ = tan(fieldOfView()/2.0); + + // Also defines the pivotPoint(), which changes orthoCoef_. Requires a frame(). + setSceneCenter(Vec(0.0, 0.0, 0.0)); + + // Requires fieldOfView() when called with ORTHOGRAPHIC. Attention to projectionMatrix_ below. + setType(PERSPECTIVE); + + // #CONNECTION# initFromDOMElement default values + setZNearCoefficient(0.005); + setZClippingCoefficient(sqrt(3.0)); + + // Dummy values + setScreenWidthAndHeight(600, 400); + + // Stereo parameters + setIODistance(0.062); + setPhysicalScreenWidth(0.5); + // focusDistance is set from setFieldOfView() + + // #CONNECTION# Camera copy constructor + for (unsigned short j=0; j<16; ++j) + { + modelViewMatrix_[j] = ((j%5 == 0) ? 1.0 : 0.0); + // #CONNECTION# computeProjectionMatrix() is lazy and assumes 0.0 almost everywhere. + projectionMatrix_[j] = 0.0; + } + computeProjectionMatrix(); +} + +/*! Virtual destructor. + + The frame() is deleted, but the different keyFrameInterpolator() are \e not deleted (in case they + are shared). */ +Camera::~Camera() +{ + delete frame_; + delete interpolationKfi_; +} + + +/*! Copy constructor. Performs a deep copy using operator=(). */ +Camera::Camera(const Camera& camera) + : QObject(), frame_(NULL) +{ + // #CONNECTION# Camera constructor + interpolationKfi_ = new KeyFrameInterpolator; + // Requires the interpolationKfi_ + setFrame(new ManipulatedCameraFrame(*camera.frame())); + + for (unsigned short j=0; j<16; ++j) + { + modelViewMatrix_[j] = ((j%5 == 0) ? 1.0 : 0.0); + // #CONNECTION# computeProjectionMatrix() is lazy and assumes 0.0 almost everywhere. + projectionMatrix_[j] = 0.0; + } + + (*this)=camera; +} + +/*! Equal operator. + + All the parameters of \p camera are copied. The frame() pointer is not modified, but its + Frame::position() and Frame::orientation() are set to those of \p camera. + + \attention The Camera screenWidth() and screenHeight() are set to those of \p camera. If your + Camera is associated with a QGLViewer, you should update these value after the call to this method: + \code + *(camera()) = otherCamera; + camera()->setScreenWidthAndHeight(width(), height()); + \endcode + The same applies to sceneCenter() and sceneRadius(), if needed. */ +Camera& Camera::operator=(const Camera& camera) +{ + setScreenWidthAndHeight(camera.screenWidth(), camera.screenHeight()); + setFieldOfView(camera.fieldOfView()); + setSceneRadius(camera.sceneRadius()); + setSceneCenter(camera.sceneCenter()); + setZNearCoefficient(camera.zNearCoefficient()); + setZClippingCoefficient(camera.zClippingCoefficient()); + setType(camera.type()); + + // Stereo parameters + setIODistance(camera.IODistance()); + setFocusDistance(camera.focusDistance()); + setPhysicalScreenWidth(camera.physicalScreenWidth()); + + orthoCoef_ = camera.orthoCoef_; + projectionMatrixIsUpToDate_ = false; + + // frame_ and interpolationKfi_ pointers are not shared. + frame_->setReferenceFrame(NULL); + frame_->setPosition(camera.position()); + frame_->setOrientation(camera.orientation()); + + interpolationKfi_->resetInterpolation(); + + kfi_ = camera.kfi_; + + computeProjectionMatrix(); + computeModelViewMatrix(); + + return *this; +} + +/*! Sets Camera screenWidth() and screenHeight() (expressed in pixels). + +You should not call this method when the Camera is associated with a QGLViewer, since the +latter automatically updates these values when it is resized (hence overwritting your values). + +Non-positive dimension are silently replaced by a 1 pixel value to ensure frustrum coherence. + +If your Camera is used without a QGLViewer (offscreen rendering, shadow maps), use setAspectRatio() +instead to define the projection matrix. */ +void Camera::setScreenWidthAndHeight(int width, int height) +{ + // Prevent negative and zero dimensions that would cause divisions by zero. + screenWidth_ = width > 0 ? width : 1; + screenHeight_ = height > 0 ? height : 1; + projectionMatrixIsUpToDate_ = false; +} + +/*! Returns the near clipping plane distance used by the Camera projection matrix. + + The clipping planes' positions depend on the sceneRadius() and sceneCenter() rather than being fixed + small-enough and large-enough values. A good scene dimension approximation will hence result in an + optimal precision of the z-buffer. + + The near clipping plane is positioned at a distance equal to zClippingCoefficient() * sceneRadius() + in front of the sceneCenter(): + \code + zNear = distanceToSceneCenter() - zClippingCoefficient()*sceneRadius(); + \endcode + + In order to prevent negative or too small zNear() values (which would degrade the z precision), + zNearCoefficient() is used when the Camera is inside the sceneRadius() sphere: + \code + const qreal zMin = zNearCoefficient() * zClippingCoefficient() * sceneRadius(); + if (zNear < zMin) + zNear = zMin; + // With an ORTHOGRAPHIC type, the value is simply clamped to 0.0 + \endcode + + See also the zFar(), zClippingCoefficient() and zNearCoefficient() documentations. + + If you need a completely different zNear computation, overload the zNear() and zFar() methods in a + new class that publicly inherits from Camera and use QGLViewer::setCamera(): + \code + class myCamera :: public qglviewer::Camera + { + virtual qreal Camera::zNear() const { return 0.001; }; + virtual qreal Camera::zFar() const { return 100.0; }; + } + \endcode + + See the standardCamera example for an application. + + \attention The value is always positive although the clipping plane is positioned at a negative z + value in the Camera coordinate system. This follows the \c gluPerspective standard. */ +qreal Camera::zNear() const +{ + const qreal zNearScene = zClippingCoefficient() * sceneRadius(); + qreal z = distanceToSceneCenter() - zNearScene; + + // Prevents negative or null zNear values. + const qreal zMin = zNearCoefficient() * zNearScene; + if (z < zMin) + switch (type()) + { + case Camera::PERSPECTIVE : z = zMin; break; + case Camera::ORTHOGRAPHIC : z = 0.0; break; + } + return z; +} + +/*! Returns the far clipping plane distance used by the Camera projection matrix. + +The far clipping plane is positioned at a distance equal to zClippingCoefficient() * sceneRadius() +behind the sceneCenter(): +\code +zFar = distanceToSceneCenter() + zClippingCoefficient()*sceneRadius(); +\endcode + +See the zNear() documentation for details. */ +qreal Camera::zFar() const +{ + return distanceToSceneCenter() + zClippingCoefficient() * sceneRadius(); +} + + +/*! Sets the vertical fieldOfView() of the Camera (in radians). + +Note that focusDistance() is set to sceneRadius() / tan(fieldOfView()/2) by this method. */ +void Camera::setFieldOfView(qreal fov) { + fieldOfView_ = fov; + setFocusDistance(sceneRadius() / tan(fov/2.0)); + projectionMatrixIsUpToDate_ = false; +} + +/*! Defines the Camera type(). + +Changing the camera Type alters the viewport and the objects' sizes can be changed. +This method garantees that the two frustum match in a plane normal to viewDirection(), passing through the pivotPoint(). + +Prefix the type with \c Camera if needed, as in: +\code +camera()->setType(Camera::ORTHOGRAPHIC); +// or even qglviewer::Camera::ORTHOGRAPHIC if you do not use namespace +\endcode */ +void Camera::setType(Type type) +{ + // make ORTHOGRAPHIC frustum fit PERSPECTIVE (at least in plane normal to viewDirection(), passing + // through RAP). Done only when CHANGING type since orthoCoef_ may have been changed with a + // setPivotPoint() in the meantime. + if ( (type == Camera::ORTHOGRAPHIC) && (type_ == Camera::PERSPECTIVE) ) + orthoCoef_ = tan(fieldOfView()/2.0); + type_ = type; + projectionMatrixIsUpToDate_ = false; +} + +/*! Sets the Camera frame(). + +If you want to move the Camera, use setPosition() and setOrientation() or one of the Camera +positioning methods (lookAt(), fitSphere(), showEntireScene()...) instead. + +If you want to save the Camera position(), there's no need to call this method either. Use +addKeyFrameToPath() and playPath() instead. + +This method is actually mainly useful if you derive the ManipulatedCameraFrame class and want to +use an instance of your new class to move the Camera. + +A \c NULL \p mcf pointer will silently be ignored. The calling method is responsible for +deleting the previous frame() pointer if needed in order to prevent memory leaks. */ +void Camera::setFrame(ManipulatedCameraFrame* const mcf) +{ + if (!mcf) + return; + + if (frame_) { + disconnect(frame_, SIGNAL(modified()), this, SLOT(onFrameModified())); + } + + frame_ = mcf; + interpolationKfi_->setFrame(frame()); + + connect(frame_, SIGNAL(modified()), this, SLOT(onFrameModified())); + onFrameModified(); +} + +/*! Returns the distance from the Camera center to sceneCenter(), projected along the Camera Z axis. + Used by zNear() and zFar() to optimize the Z range. */ +qreal Camera::distanceToSceneCenter() const +{ + return fabs((frame()->coordinatesOf(sceneCenter())).z); +} + + +/*! Returns the \p halfWidth and \p halfHeight of the Camera orthographic frustum. + + These values are only valid and used when the Camera is of type() Camera::ORTHOGRAPHIC. They are + expressed in OpenGL units and are used by loadProjectionMatrix() to define the projection matrix + using: + \code + glOrtho( -halfWidth, halfWidth, -halfHeight, halfHeight, zNear(), zFar() ) + \endcode + + These values are proportional to the Camera (z projected) distance to the pivotPoint(). + When zooming on the object, the Camera is translated forward \e and its frustum is narrowed, making + the object appear bigger on screen, as intuitively expected. + + Overload this method to change this behavior if desired, as is done in the + standardCamera example. */ +void Camera::getOrthoWidthHeight(GLdouble& halfWidth, GLdouble& halfHeight) const +{ + const qreal dist = orthoCoef_ * fabs(cameraCoordinatesOf(pivotPoint()).z); + //#CONNECTION# fitScreenRegion + halfWidth = dist * ((aspectRatio() < 1.0) ? 1.0 : aspectRatio()); + halfHeight = dist * ((aspectRatio() < 1.0) ? 1.0/aspectRatio() : 1.0); +} + + +/*! Computes the projection matrix associated with the Camera. + + If type() is Camera::PERSPECTIVE, defines a \c GL_PROJECTION matrix similar to what would \c + gluPerspective() do using the fieldOfView(), window aspectRatio(), zNear() and zFar() parameters. + + If type() is Camera::ORTHOGRAPHIC, the projection matrix is as what \c glOrtho() would do. + Frustum's width and height are set using getOrthoWidthHeight(). + + Both types use zNear() and zFar() to place clipping planes. These values are determined from + sceneRadius() and sceneCenter() so that they best fit the scene size. + + Use getProjectionMatrix() to retrieve this matrix. Overload loadProjectionMatrix() if you want your + Camera to use an exotic projection matrix. + + \note You must call this method if your Camera is not associated with a QGLViewer and is used for + offscreen computations (using (un)projectedCoordinatesOf() for instance). loadProjectionMatrix() + does it otherwise. */ +void Camera::computeProjectionMatrix() const +{ + if (projectionMatrixIsUpToDate_) return; + + const qreal ZNear = zNear(); + const qreal ZFar = zFar(); + + switch (type()) + { + case Camera::PERSPECTIVE: + { + // #CONNECTION# all non null coefficients were set to 0.0 in constructor. + const qreal f = 1.0/tan(fieldOfView()/2.0); + projectionMatrix_[0] = f/aspectRatio(); + projectionMatrix_[5] = f; + projectionMatrix_[10] = (ZNear + ZFar) / (ZNear - ZFar); + projectionMatrix_[11] = -1.0; + projectionMatrix_[14] = 2.0 * ZNear * ZFar / (ZNear - ZFar); + projectionMatrix_[15] = 0.0; + // same as gluPerspective( 180.0*fieldOfView()/M_PI, aspectRatio(), zNear(), zFar() ); + break; + } + case Camera::ORTHOGRAPHIC: + { + GLdouble w, h; + getOrthoWidthHeight(w,h); + projectionMatrix_[0] = 1.0/w; + projectionMatrix_[5] = 1.0/h; + projectionMatrix_[10] = -2.0/(ZFar - ZNear); + projectionMatrix_[11] = 0.0; + projectionMatrix_[14] = -(ZFar + ZNear)/(ZFar - ZNear); + projectionMatrix_[15] = 1.0; + // same as glOrtho( -w, w, -h, h, zNear(), zFar() ); + break; + } + } + + projectionMatrixIsUpToDate_ = true; +} + +/*! Computes the modelView matrix associated with the Camera's position() and orientation(). + + This matrix converts from the world coordinates system to the Camera coordinates system, so that + coordinates can then be projected on screen using the projection matrix (see computeProjectionMatrix()). + + Use getModelViewMatrix() to retrieve this matrix. + + \note You must call this method if your Camera is not associated with a QGLViewer and is used for + offscreen computations (using (un)projectedCoordinatesOf() for instance). loadModelViewMatrix() + does it otherwise. */ +void Camera::computeModelViewMatrix() const +{ + if (modelViewMatrixIsUpToDate_) return; + + const Quaternion q = frame()->orientation(); + + const qreal q00 = 2.0 * q[0] * q[0]; + const qreal q11 = 2.0 * q[1] * q[1]; + const qreal q22 = 2.0 * q[2] * q[2]; + + const qreal q01 = 2.0 * q[0] * q[1]; + const qreal q02 = 2.0 * q[0] * q[2]; + const qreal q03 = 2.0 * q[0] * q[3]; + + const qreal q12 = 2.0 * q[1] * q[2]; + const qreal q13 = 2.0 * q[1] * q[3]; + + const qreal q23 = 2.0 * q[2] * q[3]; + + modelViewMatrix_[0] = 1.0 - q11 - q22; + modelViewMatrix_[1] = q01 - q23; + modelViewMatrix_[2] = q02 + q13; + modelViewMatrix_[3] = 0.0; + + modelViewMatrix_[4] = q01 + q23; + modelViewMatrix_[5] = 1.0 - q22 - q00; + modelViewMatrix_[6] = q12 - q03; + modelViewMatrix_[7] = 0.0; + + modelViewMatrix_[8] = q02 - q13; + modelViewMatrix_[9] = q12 + q03; + modelViewMatrix_[10] = 1.0 - q11 - q00; + modelViewMatrix_[11] = 0.0; + + const Vec t = q.inverseRotate(frame()->position()); + + modelViewMatrix_[12] = -t.x; + modelViewMatrix_[13] = -t.y; + modelViewMatrix_[14] = -t.z; + modelViewMatrix_[15] = 1.0; + + modelViewMatrixIsUpToDate_ = true; +} + + +/*! Loads the OpenGL \c GL_PROJECTION matrix with the Camera projection matrix. + + The Camera projection matrix is computed using computeProjectionMatrix(). + + When \p reset is \c true (default), the method clears the previous projection matrix by calling \c + glLoadIdentity before setting the matrix. Setting \p reset to \c false is useful for \c GL_SELECT + mode, to combine the pushed matrix with a picking matrix. See QGLViewer::beginSelection() for details. + + This method is used by QGLViewer::preDraw() (called before user's QGLViewer::draw() method) to + set the \c GL_PROJECTION matrix according to the viewer's QGLViewer::camera() settings. + + Use getProjectionMatrix() to retrieve this matrix. Overload this method if you want your Camera to + use an exotic projection matrix. See also loadModelViewMatrix(). + + \attention \c glMatrixMode is set to \c GL_PROJECTION. + + \attention If you use several OpenGL contexts and bypass the Qt main refresh loop, you should call + QGLWidget::makeCurrent() before this method in order to activate the right OpenGL context. [TODO Update with QOpenGLWidget] */ +void Camera::loadProjectionMatrix(bool /*reset*/) const +{ + computeProjectionMatrix(); +} + +/*! Loads the OpenGL \c GL_MODELVIEW matrix with the modelView matrix corresponding to the Camera. + + Calls computeModelViewMatrix() to compute the Camera's modelView matrix. + + This method is used by QGLViewer::preDraw() (called before user's QGLViewer::draw() method) to + set the \c GL_MODELVIEW matrix according to the viewer's QGLViewer::camera() position() and + orientation(). + + As a result, the vertices used in QGLViewer::draw() can be defined in the so called world + coordinate system. They are multiplied by this matrix to get converted to the Camera coordinate + system, before getting projected using the \c GL_PROJECTION matrix (see loadProjectionMatrix()). + + When \p reset is \c true (default), the method loads (overwrites) the \c GL_MODELVIEW matrix. Setting + \p reset to \c false simply calls \c glMultMatrixd (might be useful for some applications). + + Overload this method or simply call glLoadMatrixd() at the beginning of QGLViewer::draw() if you + want your Camera to use an exotic modelView matrix. See also loadProjectionMatrix(). + + getModelViewMatrix() returns the 4x4 modelView matrix. + + \attention glMatrixMode is set to \c GL_MODELVIEW + + \attention If you use several OpenGL contexts and bypass the Qt main refresh loop, you should call + QGLWidget::makeCurrent() before this method in order to activate the right OpenGL context. [TODO Update with QOpenGLWidget] */ +void Camera::loadModelViewMatrix(bool /*reset*/) const +{ + // WARNING: makeCurrent must be called by every calling method + computeModelViewMatrix(); +} + +/*! Same as loadModelViewMatrix() but for a stereo setup. + + Only the Camera::PERSPECTIVE type() is supported for stereo mode. See + QGLViewer::setStereoDisplay(). + + The modelView matrix is almost identical to the mono-vision one. It is simply translated along its + horizontal axis by a value that depends on stereo parameters (see focusDistance(), + IODistance(), and physicalScreenWidth()). + + When \p leftBuffer is \c true, computes the modelView matrix associated to the left eye (right eye + otherwise). + + loadProjectionMatrixStereo() explains how to retrieve to resulting matrix. + + See the stereoViewer and the anaglyph examples for an illustration. + + \attention glMatrixMode is set to \c GL_MODELVIEW. */ +void Camera::loadModelViewMatrixStereo(bool leftBuffer) const +{ + // WARNING: makeCurrent must be called by every calling method + + qreal halfWidth = focusDistance() * tan(horizontalFieldOfView() / 2.0); + qreal shift = halfWidth * IODistance() / physicalScreenWidth(); // * current window width / full screen width + + computeModelViewMatrix(); + if (leftBuffer) + modelViewMatrix_[12] -= shift; + else + modelViewMatrix_[12] += shift; +} + +/*! Fills \p m with the Camera projection matrix values. + + Based on computeProjectionMatrix() to make sure the Camera projection matrix is up to date. + + This matrix only reflects the Camera's internal parameters and it may differ from the \c + GL_PROJECTION matrix retrieved using \c glGetDoublev(GL_PROJECTION_MATRIX, m). It actually + represents the state of the \c GL_PROJECTION after QGLViewer::preDraw(), at the beginning of + QGLViewer::draw(). If you modified the \c GL_PROJECTION matrix (for instance using + QGLViewer::startScreenCoordinatesSystem()), the two results differ. + + The result is an OpenGL 4x4 matrix, which is given in \e column-major order (see \c glMultMatrix + man page for details). + + See also getModelViewMatrix() and setFromProjectionMatrix(). */ +void Camera::getProjectionMatrix(GLdouble m[16]) const +{ + computeProjectionMatrix(); + for (unsigned short i=0; i<16; ++i) + m[i] = projectionMatrix_[i]; +} + +/*! Overloaded getProjectionMatrix(GLdouble m[16]) method using a \c GLfloat array instead. */ +void Camera::getProjectionMatrix(GLfloat m[16]) const +{ + static GLdouble mat[16]; + getProjectionMatrix(mat); + for (unsigned short i=0; i<16; ++i) + m[i] = float(mat[i]); +} + +/*! Overloaded getProjectionMatrix(GLdouble m[16]) method using a \c QMatrix4x4 instead. */ +void Camera::getProjectionMatrix(QMatrix4x4& m) const +{ + computeProjectionMatrix(); + for (int row=0; row<4; ++row) + for (int col=0; col<4; ++col) + m(row, col) = projectionMatrix_[row + col*4]; +} + +/*! Fills \p m with the Camera modelView matrix values. + + First calls computeModelViewMatrix() to define the Camera modelView matrix. + + Note that this matrix may \e not be the one you would get from a \c + glGetDoublev(GL_MODELVIEW_MATRIX, m). It actually represents the state of the \c + GL_MODELVIEW after QGLViewer::preDraw(), at the \e beginning of QGLViewer::draw(). It converts from + the world to the Camera coordinate system. As soon as you modify the \c GL_MODELVIEW in your + QGLViewer::draw() method (using glTranslate, glRotate... or similar methods), the two matrices differ. + + The result is an OpenGL 4x4 matrix, which is given in \e column-major order (see \c glMultMatrix + man page for details). + + See also getProjectionMatrix() and setFromModelViewMatrix(). */ +void Camera::getModelViewMatrix(GLdouble m[16]) const +{ + // May not be needed, but easier like this. + // Prevents from retrieving matrix in stereo mode -> overwrites shifted value. + computeModelViewMatrix(); + for (unsigned short i=0; i<16; ++i) + m[i] = modelViewMatrix_[i]; +} + + +/*! Overloaded getModelViewMatrix(GLdouble m[16]) method using a \c GLfloat array instead. */ +void Camera::getModelViewMatrix(GLfloat m[16]) const +{ + static GLdouble mat[16]; + getModelViewMatrix(mat); + for (unsigned short i=0; i<16; ++i) + m[i] = float(mat[i]); +} + +/*! Overloaded getModelViewMatrix(GLdouble m[16]) method using a \c QMatrix4x4 instead. */ +void Camera::getModelViewMatrix(QMatrix4x4& m) const +{ + computeModelViewMatrix(); + for (int row=0; row<4; ++row) + for (int col=0; col<4; ++col) + m(row, col) = modelViewMatrix_[row + col*4]; +} + +/*! Fills \p m with the product of the ModelView and Projection matrices. + + Calls getModelViewMatrix() and getProjectionMatrix() and then fills \p m with the product of these two matrices. */ +void Camera::getModelViewProjectionMatrix(GLdouble m[16]) const +{ + GLdouble mv[16]; + GLdouble proj[16]; + getModelViewMatrix(mv); + getProjectionMatrix(proj); + + for (unsigned short i=0; i<4; ++i) + { + for (unsigned short j=0; j<4; ++j) + { + qreal sum = 0.0; + for (unsigned short k=0; k<4; ++k) + sum += proj[i+4*k]*mv[k+4*j]; + m[i+4*j] = sum; + } + } +} + +/*! Overloaded getModelViewProjectionMatrix(GLdouble m[16]) method using a \c GLfloat array instead. */ +void Camera::getModelViewProjectionMatrix(GLfloat m[16]) const +{ + static GLdouble mat[16]; + getModelViewProjectionMatrix(mat); + for (unsigned short i=0; i<16; ++i) + m[i] = float(mat[i]); +} + +/*! Overloaded getModelViewProjectionMatrix(GLdouble m[16]) method using a \c QMatrix4x4 instead. */ +void Camera::getModelViewProjectionMatrix(QMatrix4x4& m) const +{ + static GLdouble mat[16]; + getModelViewProjectionMatrix(mat); + for (int row=0; row<4; ++row) + for (int col=0; col<4; ++col) + m(row, col) = mat[row + col*4]; +} + +/*! Sets the sceneRadius() value. Negative values are ignored. + +\attention This methods also sets focusDistance() to sceneRadius() / tan(fieldOfView()/2) and +flySpeed() to 1% of sceneRadius(). */ +void Camera::setSceneRadius(qreal radius) +{ + if (radius <= 0.0) + { + qWarning("Scene radius must be positive - Ignoring value"); + return; + } + + sceneRadius_ = radius; + projectionMatrixIsUpToDate_ = false; + + setFocusDistance(sceneRadius() / tan(fieldOfView()/2.0)); + + frame()->setFlySpeed(0.01*sceneRadius()); +} + +/*! Similar to setSceneRadius() and setSceneCenter(), but the scene limits are defined by a (world + axis aligned) bounding box. */ +void Camera::setSceneBoundingBox(const Vec& min, const Vec& max) +{ + setSceneCenter((min+max)/2.0); + setSceneRadius(0.5*(max-min).norm()); +} + + +/*! Sets the sceneCenter(). + + \attention This method also sets the pivotPoint() to sceneCenter(). */ +void Camera::setSceneCenter(const Vec& center) +{ + sceneCenter_ = center; + setPivotPoint(sceneCenter()); + projectionMatrixIsUpToDate_ = false; +} + +/*! setSceneCenter() to the result of pointUnderPixel(\p pixel). + + Returns \c true if a pointUnderPixel() was found and sceneCenter() was actually changed. + + See also setPivotPointFromPixel(). See the pointUnderPixel() documentation. */ +bool Camera::setSceneCenterFromPixel(const QPoint& pixel) +{ + bool found; + Vec point = pointUnderPixel(pixel, found); + if (found) + setSceneCenter(point); + return found; +} + +#ifndef DOXYGEN +void Camera::setRevolveAroundPoint(const Vec& point) { + qWarning("setRevolveAroundPoint() is deprecated, use setPivotPoint() instead"); + setPivotPoint(point); +} +bool Camera::setRevolveAroundPointFromPixel(const QPoint& pixel) { + qWarning("setRevolveAroundPointFromPixel() is deprecated, use setPivotPointFromPixel() instead"); + return setPivotPointFromPixel(pixel); +} +Vec Camera::revolveAroundPoint() const { + qWarning("revolveAroundPoint() is deprecated, use pivotPoint() instead"); + return pivotPoint(); +} +#endif + +/*! Changes the pivotPoint() to \p point (defined in the world coordinate system). */ +void Camera::setPivotPoint(const Vec& point) +{ + const qreal prevDist = fabs(cameraCoordinatesOf(pivotPoint()).z); + + // If frame's RAP is set directly, projectionMatrixIsUpToDate_ should also be + // set to false to ensure proper recomputation of the ORTHO projection matrix. + frame()->setPivotPoint(point); + + // orthoCoef_ is used to compensate for changes of the pivotPoint, so that the image does + // not change when the pivotPoint is changed in ORTHOGRAPHIC mode. + const qreal newDist = fabs(cameraCoordinatesOf(pivotPoint()).z); + // Prevents division by zero when rap is set to camera position + if ((prevDist > 1E-9) && (newDist > 1E-9)) + orthoCoef_ *= prevDist / newDist; + projectionMatrixIsUpToDate_ = false; +} + +/*! The pivotPoint() is set to the point located under \p pixel on screen. + +Returns \c true if a pointUnderPixel() was found. If no point was found under \p pixel, the +pivotPoint() is left unchanged. + +\p pixel is expressed in Qt format (origin in the upper left corner of the window). See +pointUnderPixel(). + +See also setSceneCenterFromPixel(). */ +bool Camera::setPivotPointFromPixel(const QPoint& pixel) +{ + bool found; + Vec point = pointUnderPixel(pixel, found); + if (found) + setPivotPoint(point); + return found; +} + +/*! Returns the ratio between pixel and OpenGL units at \p position. + + A line of \c n * pixelGLRatio() OpenGL units, located at \p position in the world coordinates + system, will be projected with a length of \c n pixels on screen. + + Use this method to scale objects so that they have a constant pixel size on screen. The following + code will draw a 20 pixel line, starting at sceneCenter() and always directed along the screen + vertical direction: + \code + glBegin(GL_LINES); + glVertex3fv(sceneCenter()); + glVertex3fv(sceneCenter() + 20 * pixelGLRatio(sceneCenter()) * camera()->upVector()); + glEnd(); + \endcode */ +qreal Camera::pixelGLRatio(const Vec& position) const +{ + switch (type()) + { + case Camera::PERSPECTIVE : + return 2.0 * fabs((frame()->coordinatesOf(position)).z) * tan(fieldOfView()/2.0) / screenHeight(); + case Camera::ORTHOGRAPHIC : + { + GLdouble w, h; + getOrthoWidthHeight(w,h); + return 2.0 * h / screenHeight(); + } + } + // Bad compilers complain + return 1.0; +} + +/*! Changes the Camera fieldOfView() so that the entire scene (defined by QGLViewer::sceneCenter() + and QGLViewer::sceneRadius()) is visible from the Camera position(). + + The position() and orientation() of the Camera are not modified and you first have to orientate the + Camera in order to actually see the scene (see lookAt(), showEntireScene() or fitSphere()). + + This method is especially useful for \e shadow \e maps computation. Use the Camera positioning + tools (setPosition(), lookAt()) to position a Camera at the light position. Then use this method to + define the fieldOfView() so that the shadow map resolution is optimally used: + \code + // The light camera needs size hints in order to optimize its fieldOfView + lightCamera->setSceneRadius(sceneRadius()); + lightCamera->setSceneCenter(sceneCenter()); + + // Place the light camera. + lightCamera->setPosition(lightFrame->position()); + lightCamera->lookAt(sceneCenter()); + lightCamera->setFOVToFitScene(); + \endcode + + See the (soon available) shadowMap contribution example for a practical implementation. + + \attention The fieldOfView() is clamped to M_PI/2.0. This happens when the Camera is at a distance + lower than sqrt(2.0) * sceneRadius() from the sceneCenter(). It optimizes the shadow map + resolution, although it may miss some parts of the scene. */ +void Camera::setFOVToFitScene() +{ + if (distanceToSceneCenter() > sqrt(2.0)*sceneRadius()) + setFieldOfView(2.0 * asin(sceneRadius() / distanceToSceneCenter())); + else + setFieldOfView(M_PI / 2.0); +} + +/*! Makes the Camera smoothly zoom on the pointUnderPixel() \p pixel. + + Nothing happens if no pointUnderPixel() is found. Otherwise a KeyFrameInterpolator is created that + animates the Camera on a one second path that brings the Camera closer to the point under \p pixel. + + See also interpolateToFitScene(). */ +void Camera::interpolateToZoomOnPixel(const QPoint& pixel) +{ + const qreal coef = 0.1; + + bool found; + Vec target = pointUnderPixel(pixel, found); + + if (!found) + return; + + if (interpolationKfi_->interpolationIsStarted()) + interpolationKfi_->stopInterpolation(); + + interpolationKfi_->deletePath(); + interpolationKfi_->addKeyFrame(*(frame())); + + interpolationKfi_->addKeyFrame(Frame(0.3*frame()->position() + 0.7*target, frame()->orientation()), 0.4); + + // Small hack: attach a temporary frame to take advantage of lookAt without modifying frame + static ManipulatedCameraFrame* tempFrame = new ManipulatedCameraFrame(); + ManipulatedCameraFrame* const originalFrame = frame(); + tempFrame->setPosition(coef*frame()->position() + (1.0-coef)*target); + tempFrame->setOrientation(frame()->orientation()); + setFrame(tempFrame); + lookAt(target); + setFrame(originalFrame); + + interpolationKfi_->addKeyFrame(*(tempFrame), 1.0); + + interpolationKfi_->startInterpolation(); +} + +/*! Interpolates the Camera on a one second KeyFrameInterpolator path so that the entire scene fits + the screen at the end. + + The scene is defined by its sceneCenter() and its sceneRadius(). See showEntireScene(). + + The orientation() of the Camera is not modified. See also interpolateToZoomOnPixel(). */ +void Camera::interpolateToFitScene() +{ + if (interpolationKfi_->interpolationIsStarted()) + interpolationKfi_->stopInterpolation(); + + interpolationKfi_->deletePath(); + interpolationKfi_->addKeyFrame(*(frame())); + + // Small hack: attach a temporary frame to take advantage of lookAt without modifying frame + static ManipulatedCameraFrame* tempFrame = new ManipulatedCameraFrame(); + ManipulatedCameraFrame* const originalFrame = frame(); + tempFrame->setPosition(frame()->position()); + tempFrame->setOrientation(frame()->orientation()); + setFrame(tempFrame); + showEntireScene(); + setFrame(originalFrame); + + interpolationKfi_->addKeyFrame(*(tempFrame)); + + interpolationKfi_->startInterpolation(); +} + + +/*! Smoothly interpolates the Camera on a KeyFrameInterpolator path so that it goes to \p fr. + + \p fr is expressed in world coordinates. \p duration tunes the interpolation speed (default is + 1 second). + + See also interpolateToFitScene() and interpolateToZoomOnPixel(). */ +void Camera::interpolateTo(const Frame& fr, qreal duration) +{ + if (interpolationKfi_->interpolationIsStarted()) + interpolationKfi_->stopInterpolation(); + + interpolationKfi_->deletePath(); + interpolationKfi_->addKeyFrame(*(frame())); + interpolationKfi_->addKeyFrame(fr, duration); + + interpolationKfi_->startInterpolation(); +} + + +/*! Returns the coordinates of the 3D point located at pixel (x,y) on screen. + + Calls a \c glReadPixel to get the pixel depth and applies an unprojectedCoordinatesOf() to the + result. \p found indicates whether a point was found or not (i.e. background pixel, result's depth + is zFar() in that case). + + \p x and \p y are expressed in pixel units with an origin in the upper left corner. Use + screenHeight() - y to convert to OpenGL standard. + + \attention This method assumes that a GL context is available, and that its content was drawn using + the Camera (i.e. using its projection and modelview matrices). This method hence cannot be used for + offscreen Camera computations. Use cameraCoordinatesOf() and worldCoordinatesOf() to perform + similar operations in that case. + + \note The precision of the z-Buffer highly depends on how the zNear() and zFar() values are fitted + to your scene. Loose boundaries will result in imprecision along the viewing direction. */ +Vec Camera::pointUnderPixel(const QPoint& pixel, bool& found) const +{ + float depth; + // Qt uses upper corner for its origin while GL uses the lower corner. + glReadPixels(pixel.x(), screenHeight()-1-pixel.y(), 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, &depth); + found = depth < 1.0; + Vec point(pixel.x(), pixel.y(), depth); + point = unprojectedCoordinatesOf(point); + return point; +} + +/*! Moves the Camera so that the entire scene is visible. + + Simply calls fitSphere() on a sphere defined by sceneCenter() and sceneRadius(). + + You will typically use this method in QGLViewer::init() after you defined a new sceneRadius(). */ +void Camera::showEntireScene() +{ + fitSphere(sceneCenter(), sceneRadius()); +} + +/*! Moves the Camera so that its sceneCenter() is projected on the center of the window. The + orientation() and fieldOfView() are unchanged. + + Simply projects the current position on a line passing through sceneCenter(). See also + showEntireScene().*/ +void Camera::centerScene() +{ + frame()->projectOnLine(sceneCenter(), viewDirection()); +} + +/*! Sets the Camera orientation(), so that it looks at point \p target (defined in the world + coordinate system). + + The Camera position() is not modified. Simply setViewDirection(). + + See also setUpVector(), setOrientation(), showEntireScene(), fitSphere() and fitBoundingBox(). */ +void Camera::lookAt(const Vec& target) +{ + setViewDirection(target - position()); +} + +/*! Moves the Camera so that the sphere defined by (\p center, \p radius) is visible and fits in the frustum. + + The Camera is simply translated to center the sphere in the screen and make it fit the frustum. Its + orientation() and its fieldOfView() are unchanged. + + You should therefore orientate the Camera before you call this method. See lookAt(), + setOrientation() and setUpVector(). */ +void Camera::fitSphere(const Vec& center, qreal radius) +{ + qreal distance = 0.0; + switch (type()) + { + case Camera::PERSPECTIVE : + { + const qreal yview = radius / sin(fieldOfView() / 2.0); + const qreal xview = radius / sin(horizontalFieldOfView() / 2.0); + distance = qMax(xview, yview); + break; + } + case Camera::ORTHOGRAPHIC : + { + distance = ((center-pivotPoint()) * viewDirection()) + (radius / orthoCoef_); + break; + } + } + Vec newPos(center - distance * viewDirection()); + frame()->setPositionWithConstraint(newPos); +} + +/*! Moves the Camera so that the (world axis aligned) bounding box (\p min, \p max) is entirely + visible, using fitSphere(). */ +void Camera::fitBoundingBox(const Vec& min, const Vec& max) +{ + qreal diameter = qMax(fabs(max[1]-min[1]), fabs(max[0]-min[0])); + diameter = qMax(fabs(max[2]-min[2]), diameter); + fitSphere(0.5*(min+max), 0.5*diameter); +} + +/*! Moves the Camera so that the rectangular screen region defined by \p rectangle (pixel units, + with origin in the upper left corner) fits the screen. + + The Camera is translated (its orientation() is unchanged) so that \p rectangle is entirely + visible. Since the pixel coordinates only define a \e frustum in 3D, it's the intersection of this + frustum with a plane (orthogonal to the viewDirection() and passing through the sceneCenter()) + that is used to define the 3D rectangle that is eventually fitted. */ +void Camera::fitScreenRegion(const QRect& rectangle) +{ + const Vec vd = viewDirection(); + const qreal distToPlane = distanceToSceneCenter(); + const QPoint center = rectangle.center(); + + Vec orig, dir; + convertClickToLine( center, orig, dir ); + Vec newCenter = orig + distToPlane / (dir*vd) * dir; + + convertClickToLine( QPoint(rectangle.x(), center.y()), orig, dir ); + const Vec pointX = orig + distToPlane / (dir*vd) * dir; + + convertClickToLine( QPoint(center.x(), rectangle.y()), orig, dir ); + const Vec pointY = orig + distToPlane / (dir*vd) * dir; + + qreal distance = 0.0; + switch (type()) + { + case Camera::PERSPECTIVE : + { + const qreal distX = (pointX-newCenter).norm() / sin(horizontalFieldOfView()/2.0); + const qreal distY = (pointY-newCenter).norm() / sin(fieldOfView()/2.0); + distance = qMax(distX, distY); + break; + } + case Camera::ORTHOGRAPHIC : + { + const qreal dist = ((newCenter-pivotPoint()) * vd); + //#CONNECTION# getOrthoWidthHeight + const qreal distX = (pointX-newCenter).norm() / orthoCoef_ / ((aspectRatio() < 1.0) ? 1.0 : aspectRatio()); + const qreal distY = (pointY-newCenter).norm() / orthoCoef_ / ((aspectRatio() < 1.0) ? 1.0/aspectRatio() : 1.0); + distance = dist + qMax(distX, distY); + break; + } + } + + Vec newPos(newCenter - distance * vd); + frame()->setPositionWithConstraint(newPos); +} + +/*! Rotates the Camera so that its upVector() becomes \p up (defined in the world coordinate + system). + + The Camera is rotated around an axis orthogonal to \p up and to the current upVector() direction. + Use this method in order to define the Camera horizontal plane. + + When \p noMove is set to \c false, the orientation modification is compensated by a translation, so + that the pivotPoint() stays projected at the same position on screen. This is especially + useful when the Camera is used as an observer of the scene (default mouse binding). + + When \p noMove is \c true (default), the Camera position() is left unchanged, which is an intuitive + behavior when the Camera is in a walkthrough fly mode (see the QGLViewer::MOVE_FORWARD and + QGLViewer::MOVE_BACKWARD QGLViewer::MouseAction). + + The frame()'s ManipulatedCameraFrame::sceneUpVector() is set accordingly. + + See also setViewDirection(), lookAt() and setOrientation(). */ +void Camera::setUpVector(const Vec& up, bool noMove) +{ + Quaternion q(Vec(0.0, 1.0, 0.0), frame()->transformOf(up)); + + if (!noMove) + frame()->setPosition(pivotPoint() - (frame()->orientation()*q).rotate(frame()->coordinatesOf(pivotPoint()))); + + frame()->rotate(q); + + // Useful in fly mode to keep the horizontal direction. + frame()->updateSceneUpVector(); +} + +/*! Sets the orientation() of the Camera using polar coordinates. + + \p theta rotates the Camera around its Y axis, and \e then \p phi rotates it around its X axis. + The polar coordinates are defined in the world coordinates system: \p theta = \p phi = 0 means + that the Camera is directed towards the world Z axis. Both angles are expressed in radians. + + See also setUpVector(). The position() of the Camera is unchanged, you may want to call showEntireScene() + after this method to move the Camera. + + This method can be useful to create Quicktime VR panoramic sequences, see the + QGLViewer::saveSnapshot() documentation for details. */ +void Camera::setOrientation(qreal theta, qreal phi) +{ + Vec axis(0.0, 1.0, 0.0); + const Quaternion rot1(axis, theta); + axis = Vec(-cos(theta), 0.0, sin(theta)); + const Quaternion rot2(axis, phi); + setOrientation(rot1 * rot2); +} + +/*! Sets the Camera orientation(), defined in the world coordinate system. */ +void Camera::setOrientation(const Quaternion& q) +{ + frame()->setOrientation(q); + frame()->updateSceneUpVector(); +} + +/*! Rotates the Camera so that its viewDirection() is \p direction (defined in the world coordinate + system). + + The Camera position() is not modified. The Camera is rotated so that the horizon (defined by its + upVector()) is preserved. See also lookAt() and setUpVector(). */ +void Camera::setViewDirection(const Vec& direction) +{ + if (direction.squaredNorm() < 1E-10) + return; + + Vec xAxis = direction ^ upVector(); + if (xAxis.squaredNorm() < 1E-10) + { + // target is aligned with upVector, this means a rotation around X axis + // X axis is then unchanged, let's keep it ! + xAxis = frame()->inverseTransformOf(Vec(1.0, 0.0, 0.0)); + } + + Quaternion q; + q.setFromRotatedBasis(xAxis, xAxis^direction, -direction); + frame()->setOrientationWithConstraint(q); +} + +// Compute a 3 by 3 determinant. +static qreal det(qreal m00,qreal m01,qreal m02, + qreal m10,qreal m11,qreal m12, + qreal m20,qreal m21,qreal m22) +{ + return m00*m11*m22 + m01*m12*m20 + m02*m10*m21 - m20*m11*m02 - m10*m01*m22 - m00*m21*m12; +} + +// Computes the index of element [i][j] in a \c qreal matrix[3][4]. +static inline unsigned int ind(unsigned int i, unsigned int j) +{ + return (i*4+j); +} + +/*! Returns the Camera position (the eye), defined in the world coordinate system. + +Use setPosition() to set the Camera position. Other convenient methods are showEntireScene() or +fitSphere(). Actually returns \c frame()->position(). + +This position corresponds to the projection center of a Camera::PERSPECTIVE Camera. It is not +located in the image plane, which is at a zNear() distance ahead. */ +Vec Camera::position() const { return frame()->position(); } + +/*! Returns the normalized up vector of the Camera, defined in the world coordinate system. + +Set using setUpVector() or setOrientation(). It is orthogonal to viewDirection() and to +rightVector(). + +It corresponds to the Y axis of the associated frame() (actually returns +frame()->inverseTransformOf(Vec(0.0, 1.0, 0.0)) ). */ +Vec Camera::upVector() const +{ + return frame()->inverseTransformOf(Vec(0.0, 1.0, 0.0)); +} +/*! Returns the normalized view direction of the Camera, defined in the world coordinate system. + +Change this value using setViewDirection(), lookAt() or setOrientation(). It is orthogonal to +upVector() and to rightVector(). + +This corresponds to the negative Z axis of the frame() ( frame()->inverseTransformOf(Vec(0.0, +0.0, -1.0)) ). */ +Vec Camera::viewDirection() const { return frame()->inverseTransformOf(Vec(0.0, 0.0, -1.0)); } + +/*! Returns the normalized right vector of the Camera, defined in the world coordinate system. + +This vector lies in the Camera horizontal plane, directed along the X axis (orthogonal to +upVector() and to viewDirection()). Set using setUpVector(), lookAt() or setOrientation(). + +Simply returns frame()->inverseTransformOf(Vec(1.0, 0.0, 0.0)). */ +Vec Camera::rightVector() const +{ + return frame()->inverseTransformOf(Vec(1.0, 0.0, 0.0)); +} + +/*! Returns the Camera orientation, defined in the world coordinate system. + +Actually returns \c frame()->orientation(). Use setOrientation(), setUpVector() or lookAt() to +set the Camera orientation. */ +Quaternion Camera::orientation() const { return frame()->orientation(); } + +/*! Sets the Camera position() (the eye), defined in the world coordinate system. */ +void Camera::setPosition(const Vec& pos) { frame()->setPosition(pos); } + +/*! Returns the Camera frame coordinates of a point \p src defined in world coordinates. + +worldCoordinatesOf() performs the inverse transformation. + +Note that the point coordinates are simply converted in a different coordinate system. They are +not projected on screen. Use projectedCoordinatesOf() for that. */ +Vec Camera::cameraCoordinatesOf(const Vec& src) const { return frame()->coordinatesOf(src); } + +/*! Returns the world coordinates of the point whose position \p src is defined in the Camera +coordinate system. + +cameraCoordinatesOf() performs the inverse transformation. */ +Vec Camera::worldCoordinatesOf(const Vec& src) const { return frame()->inverseCoordinatesOf(src); } + +/*! Returns the fly speed of the Camera. + +Simply returns frame()->flySpeed(). See the ManipulatedCameraFrame::flySpeed() documentation. +This value is only meaningful when the MouseAction bindings is QGLViewer::MOVE_FORWARD or +QGLViewer::MOVE_BACKWARD. + +Set to 1% of the sceneRadius() by setSceneRadius(). See also setFlySpeed(). */ +qreal Camera::flySpeed() const { return frame()->flySpeed(); } + +/*! Sets the Camera flySpeed(). + +\attention This value is modified by setSceneRadius(). */ +void Camera::setFlySpeed(qreal speed) { frame()->setFlySpeed(speed); } + +/*! The point the Camera pivots around with the QGLViewer::ROTATE mouse binding. Defined in world coordinate system. + +Default value is the sceneCenter(). + +\attention setSceneCenter() changes this value. */ +Vec Camera::pivotPoint() const { return frame()->pivotPoint(); } + +/*! Sets the Camera's position() and orientation() from an OpenGL ModelView matrix. + +This enables a Camera initialisation from an other OpenGL application. \p modelView is a 16 GLdouble +vector representing a valid OpenGL ModelView matrix, such as one can get using: +\code +GLdouble mvm[16]; +glGetDoublev(GL_MODELVIEW_MATRIX, mvm); +myCamera->setFromModelViewMatrix(mvm); +\endcode + +After this method has been called, getModelViewMatrix() returns a matrix equivalent to \p +modelView. + +Only the orientation() and position() of the Camera are modified. + +\note If you defined your matrix as \c GLdouble \c mvm[4][4], pass \c &(mvm[0][0]) as a +parameter. */ +void Camera::setFromModelViewMatrix(const GLdouble* const modelViewMatrix) +{ + // Get upper left (rotation) matrix + qreal upperLeft[3][3]; + for (int i=0; i<3; ++i) + for (int j=0; j<3; ++j) + upperLeft[i][j] = modelViewMatrix[i*4+j]; + + // Transform upperLeft into the associated Quaternion + Quaternion q; + q.setFromRotationMatrix(upperLeft); + + setOrientation(q); + setPosition(-q.rotate(Vec(modelViewMatrix[12], modelViewMatrix[13], modelViewMatrix[14]))); +} + +/*! Defines the Camera position(), orientation() and fieldOfView() from a projection matrix. + + \p matrix has to be given in the format used by vision algorithm. It has 3 lines and 4 columns. It + transforms a point from the world homogeneous coordinate system (4 coordinates: \c sx, \c sy, \c sz + and \c s) into a point in the screen homogeneous coordinate system (3 coordinates: \c sx, \c sy, + and \c s, where \c x and \c y are the pixel coordinates on the screen). + + Its three lines correspond to the homogeneous coordinates of the normals to the planes x=0, y=0 and + z=0, defined in the Camera coordinate system. + + The elements of the matrix are ordered in line major order: you can call \c + setFromProjectionMatrix(&(matrix[0][0])) if you defined your matrix as a \c qreal \c matrix[3][4]. + + \attention Passing the result of getProjectionMatrix() or getModelViewMatrix() to this method is + not possible (purposefully incompatible matrix dimensions). \p matrix is more likely to be the + product of these two matrices, without the last line. + + Use setFromModelViewMatrix() to set position() and orientation() from a \c GL_MODELVIEW matrix. + fieldOfView() can also be retrieved from a \e perspective \c GL_PROJECTION matrix using 2.0 * + atan(1.0/projectionMatrix[5]). + + This code was written by Sylvain Paris. */ +void Camera::setFromProjectionMatrix(const qreal matrix[12]) +{ + // The 3 lines of the matrix are the normals to the planes x=0, y=0, z=0 + // in the camera CS. As we normalize them, we do not need the 4th coordinate. + Vec line_0(matrix[ind(0,0)],matrix[ind(0,1)],matrix[ind(0,2)]); + Vec line_1(matrix[ind(1,0)],matrix[ind(1,1)],matrix[ind(1,2)]); + Vec line_2(matrix[ind(2,0)],matrix[ind(2,1)],matrix[ind(2,2)]); + + line_0.normalize(); + line_1.normalize(); + line_2.normalize(); + + // The camera position is at (0,0,0) in the camera CS so it is the + // intersection of the 3 planes. It can be seen as the kernel + // of the 3x4 projection matrix. We calculate it through 4 dimensional + // vectorial product. We go directly into 3D that is to say we directly + // divide the first 3 coordinates by the 4th one. + + // We derive the 4 dimensional vectorial product formula from the + // computation of a 4x4 determinant that is developped according to + // its 4th column. This implies some 3x3 determinants. + const Vec cam_pos = Vec(det(matrix[ind(0,1)],matrix[ind(0,2)],matrix[ind(0,3)], + matrix[ind(1,1)],matrix[ind(1,2)],matrix[ind(1,3)], + matrix[ind(2,1)],matrix[ind(2,2)],matrix[ind(2,3)]), + + -det(matrix[ind(0,0)],matrix[ind(0,2)],matrix[ind(0,3)], + matrix[ind(1,0)],matrix[ind(1,2)],matrix[ind(1,3)], + matrix[ind(2,0)],matrix[ind(2,2)],matrix[ind(2,3)]), + + det(matrix[ind(0,0)],matrix[ind(0,1)],matrix[ind(0,3)], + matrix[ind(1,0)],matrix[ind(1,1)],matrix[ind(1,3)], + matrix[ind(2,0)],matrix[ind(2,1)],matrix[ind(2,3)])) / + + (-det(matrix[ind(0,0)],matrix[ind(0,1)],matrix[ind(0,2)], + matrix[ind(1,0)],matrix[ind(1,1)],matrix[ind(1,2)], + matrix[ind(2,0)],matrix[ind(2,1)],matrix[ind(2,2)])); + + // We compute the rotation matrix column by column. + + // GL Z axis is front facing. + Vec column_2 = -line_2; + + // X-axis is almost like line_0 but should be orthogonal to the Z axis. + Vec column_0 = ((column_2^line_0)^column_2); + column_0.normalize(); + + // Y-axis is almost like line_1 but should be orthogonal to the Z axis. + // Moreover line_1 is downward oriented as the screen CS. + Vec column_1 = -((column_2^line_1)^column_2); + column_1.normalize(); + + qreal rot[3][3]; + rot[0][0] = column_0[0]; + rot[1][0] = column_0[1]; + rot[2][0] = column_0[2]; + + rot[0][1] = column_1[0]; + rot[1][1] = column_1[1]; + rot[2][1] = column_1[2]; + + rot[0][2] = column_2[0]; + rot[1][2] = column_2[1]; + rot[2][2] = column_2[2]; + + // We compute the field of view + + // line_1^column_0 -> vector of intersection line between + // y_screen=0 and x_camera=0 plane. + // column_2*(...) -> cos of the angle between Z vector et y_screen=0 plane + // * 2 -> field of view = 2 * half angle + + // We need some intermediate values. + Vec dummy = line_1^column_0; + dummy.normalize(); + qreal fov = acos(column_2*dummy) * 2.0; + + // We set the camera. + Quaternion q; + q.setFromRotationMatrix(rot); + setOrientation(q); + setPosition(cam_pos); + setFieldOfView(fov); +} + + +/* + // persp : projectionMatrix_[0] = f/aspectRatio(); +void Camera::setFromProjectionMatrix(const GLdouble* projectionMatrix) +{ + QString message; + if ((fabs(projectionMatrix[1]) > 1E-3) || + (fabs(projectionMatrix[2]) > 1E-3) || + (fabs(projectionMatrix[3]) > 1E-3) || + (fabs(projectionMatrix[4]) > 1E-3) || + (fabs(projectionMatrix[6]) > 1E-3) || + (fabs(projectionMatrix[7]) > 1E-3) || + (fabs(projectionMatrix[8]) > 1E-3) || + (fabs(projectionMatrix[9]) > 1E-3)) + message = "Non null coefficient in projection matrix - Aborting"; + else + if ((fabs(projectionMatrix[11]+1.0) < 1E-5) && (fabs(projectionMatrix[15]) < 1E-5)) + { + if (projectionMatrix[5] < 1E-4) + message="Negative field of view in Camera::setFromProjectionMatrix"; + else + setType(Camera::PERSPECTIVE); + } + else + if ((fabs(projectionMatrix[11]) < 1E-5) && (fabs(projectionMatrix[15]-1.0) < 1E-5)) + setType(Camera::ORTHOGRAPHIC); + else + message = "Unable to determine camera type in setFromProjectionMatrix - Aborting"; + + if (!message.isEmpty()) + { + qWarning(message); + return; + } + + switch (type()) + { + case Camera::PERSPECTIVE: + { + setFieldOfView(2.0 * atan(1.0/projectionMatrix[5])); + const qreal far = projectionMatrix[14] / (2.0 * (1.0 + projectionMatrix[10])); + const qreal near = (projectionMatrix[10]+1.0) / (projectionMatrix[10]-1.0) * far; + setSceneRadius((far-near)/2.0); + setSceneCenter(position() + (near + sceneRadius())*viewDirection()); + break; + } + case Camera::ORTHOGRAPHIC: + { + GLdouble w, h; + getOrthoWidthHeight(w,h); + projectionMatrix_[0] = 1.0/w; + projectionMatrix_[5] = 1.0/h; + projectionMatrix_[10] = -2.0/(ZFar - ZNear); + projectionMatrix_[11] = 0.0; + projectionMatrix_[14] = -(ZFar + ZNear)/(ZFar - ZNear); + projectionMatrix_[15] = 1.0; + // same as glOrtho( -w, w, -h, h, zNear(), zFar() ); + break; + } + } +} +*/ + +///////////////////////// Camera to world transform /////////////////////// + +/*! Same as cameraCoordinatesOf(), but with \c qreal[3] parameters (\p src and \p res may be identical pointers). */ +void Camera::getCameraCoordinatesOf(const qreal src[3], qreal res[3]) const +{ + Vec r = cameraCoordinatesOf(Vec(src)); + for (int i=0; i<3; ++i) + res[i] = r[i]; +} + +/*! Same as worldCoordinatesOf(), but with \c qreal[3] parameters (\p src and \p res may be identical pointers). */ +void Camera::getWorldCoordinatesOf(const qreal src[3], qreal res[3]) const +{ + Vec r = worldCoordinatesOf(Vec(src)); + for (int i=0; i<3; ++i) + res[i] = r[i]; +} + +/*! Fills \p viewport with the Camera OpenGL viewport. + +This method is mainly used in conjunction with \c gluProject, which requires such a viewport. +Returned values are (0, screenHeight(), screenWidth(), - screenHeight()), so that the origin is +located in the \e upper left corner of the window (Qt style coordinate system). */ +void Camera::getViewport(GLint viewport[4]) const +{ + viewport[0] = 0; + viewport[1] = screenHeight(); + viewport[2] = screenWidth(); + viewport[3] = -screenHeight(); +} + +/*! Returns the screen projected coordinates of a point \p src defined in the \p frame coordinate + system. + + When \p frame in \c NULL (default), \p src is expressed in the world coordinate system. + + The x and y coordinates of the returned Vec are expressed in pixel, (0,0) being the \e upper left + corner of the window. The z coordinate ranges between 0.0 (near plane) and 1.0 (excluded, far + plane). See the \c gluProject man page for details. + + unprojectedCoordinatesOf() performs the inverse transformation. + + See the screenCoordSystem example. + + This method only uses the intrinsic Camera parameters (see getModelViewMatrix(), + getProjectionMatrix() and getViewport()) and is completely independent of the OpenGL \c + GL_MODELVIEW, \c GL_PROJECTION and viewport matrices. You can hence define a virtual Camera and use + this method to compute projections out of a classical rendering context. + + \attention However, if your Camera is not attached to a QGLViewer (used for offscreen computations + for instance), make sure the Camera matrices are updated before calling this method. Call + computeModelViewMatrix() and computeProjectionMatrix() to do so. + + If you call this method several times with no change in the matrices, consider precomputing the + projection times modelview matrix to save computation time if required (\c P x \c M in the \c + gluProject man page). + + Here is the code corresponding to what this method does (kindly submitted by Robert W. Kuhn) : + \code + Vec project(Vec point) + { + GLint Viewport[4]; + GLdouble Projection[16], Modelview[16]; + GLdouble matrix[16]; + + // Precomputation begin + glGetIntegerv(GL_VIEWPORT , Viewport); + glGetDoublev (GL_MODELVIEW_MATRIX , Modelview); + glGetDoublev (GL_PROJECTION_MATRIX, Projection); + + for (unsigned short m=0; m<4; ++m) + { + for (unsigned short l=0; l<4; ++l) + { + qreal sum = 0.0; + for (unsigned short k=0; k<4; ++k) + sum += Projection[l+4*k]*Modelview[k+4*m]; + matrix[l+4*m] = sum; + } + } + // Precomputation end + + GLdouble v[4], vs[4]; + v[0]=point[0]; v[1]=point[1]; v[2]=point[2]; v[3]=1.0; + + vs[0]=matrix[0 ]*v[0] + matrix[4 ]*v[1] + matrix[8 ]*v[2] + matrix[12 ]*v[3]; + vs[1]=matrix[1 ]*v[0] + matrix[5 ]*v[1] + matrix[9 ]*v[2] + matrix[13 ]*v[3]; + vs[2]=matrix[2 ]*v[0] + matrix[6 ]*v[1] + matrix[10]*v[2] + matrix[14 ]*v[3]; + vs[3]=matrix[3 ]*v[0] + matrix[7 ]*v[1] + matrix[11]*v[2] + matrix[15 ]*v[3]; + + vs[0] /= vs[3]; + vs[1] /= vs[3]; + vs[2] /= vs[3]; + + vs[0] = vs[0] * 0.5 + 0.5; + vs[1] = vs[1] * 0.5 + 0.5; + vs[2] = vs[2] * 0.5 + 0.5; + + vs[0] = vs[0] * Viewport[2] + Viewport[0]; + vs[1] = vs[1] * Viewport[3] + Viewport[1]; + + return Vec(vs[0], Viewport[3]-vs[1], vs[2]); + } + \endcode + */ +Vec Camera::projectedCoordinatesOf(const Vec& src, const Frame* frame) const +{ + static GLint viewport[4]; + getViewport(viewport); + + Vec result; + if (frame) + { + const Vec tmp = frame->inverseCoordinatesOf(src); + result = project(tmp, projectionMatrix_, modelViewMatrix_, viewport); + } + else + result = project(src, projectionMatrix_, modelViewMatrix_, viewport); + + return result; +} + +/*! Returns the world unprojected coordinates of a point \p src defined in the screen coordinate + system. + + The \p src.x and \p src.y input values are expressed in pixels, (0,0) being the \e upper left corner + of the window. \p src.z is a depth value ranging in [0..1[ (respectively corresponding to the near + and far planes). Note that src.z is \e not a linear interpolation between zNear and zFar. + /code + src.z = zFar() / (zFar() - zNear()) * (1.0 - zNear() / z); + /endcode + Where z is the distance from the point you project to the camera, along the viewDirection(). + See the \c gluUnProject man page for details. + + The result is expressed in the \p frame coordinate system. When \p frame is \c NULL (default), the + result is expressed in the world coordinates system. The possible \p frame Frame::referenceFrame() + are taken into account. + + projectedCoordinatesOf() performs the inverse transformation. + + This method only uses the intrinsic Camera parameters (see getModelViewMatrix(), + getProjectionMatrix() and getViewport()) and is completely independent of the OpenGL \c + GL_MODELVIEW, \c GL_PROJECTION and viewport matrices. You can hence define a virtual Camera and use + this method to compute un-projections out of a classical rendering context. + + \attention However, if your Camera is not attached to a QGLViewer (used for offscreen computations + for instance), make sure the Camera matrices are updated before calling this method (use + computeModelViewMatrix(), computeProjectionMatrix()). See also setScreenWidthAndHeight(). + + This method is not computationally optimized. If you call it several times with no change in the + matrices, you should buffer the entire inverse projection matrix (modelview, projection and then + viewport) to speed-up the queries. See the \c gluUnProject man page for details. */ +Vec Camera::unprojectedCoordinatesOf(const Vec& src, const Frame* frame) const +{ + static GLint viewport[4]; + getViewport(viewport); + Vec point = unProject(src, projectionMatrix_, modelViewMatrix_, viewport); + if (frame) + return frame->coordinatesOf(point); + else + return point; +} + +/*! Same as projectedCoordinatesOf(), but with \c qreal parameters (\p src and \p res can be identical pointers). */ +void Camera::getProjectedCoordinatesOf(const qreal src[3], qreal res[3], const Frame* frame) const +{ + Vec r = projectedCoordinatesOf(Vec(src), frame); + for (int i=0; i<3; ++i) + res[i] = r[i]; +} + +/*! Same as unprojectedCoordinatesOf(), but with \c qreal parameters (\p src and \p res can be identical pointers). */ +void Camera::getUnprojectedCoordinatesOf(const qreal src[3], qreal res[3], const Frame* frame) const +{ + Vec r = unprojectedCoordinatesOf(Vec(src), frame); + for (int i=0; i<3; ++i) + res[i] = r[i]; +} + +///////////////////////////////////// KFI ///////////////////////////////////////// + +/*! Returns the KeyFrameInterpolator that defines the Camera path number \p i. + +If path \p i is not defined for this index, the method returns a \c NULL pointer. */ +KeyFrameInterpolator* Camera::keyFrameInterpolator(unsigned int i) const +{ + if (kfi_.contains(i)) + return kfi_[i]; + else + return NULL; +} + +/*! Sets the KeyFrameInterpolator that defines the Camera path of index \p i. + + The previous keyFrameInterpolator() is lost and should be deleted by the calling method if + needed. + + The KeyFrameInterpolator::interpolated() signal of \p kfi probably needs to be connected to the + Camera's associated QGLViewer::update() slot, so that when the Camera position is interpolated + using \p kfi, every interpolation step updates the display: + \code + myViewer.camera()->deletePath(3); + myViewer.camera()->setKeyFrameInterpolator(3, myKeyFrameInterpolator); + connect(myKeyFrameInterpolator, SIGNAL(interpolated()), myViewer, SLOT(update()); + \endcode + + \note These connections are done automatically when a Camera is attached to a QGLViewer, or when a + new KeyFrameInterpolator is defined using the QGLViewer::addKeyFrameKeyboardModifiers() and + QGLViewer::pathKey() (default is Alt+F[1-12]). See the keyboard page + for details. */ +void Camera::setKeyFrameInterpolator(unsigned int i, KeyFrameInterpolator* const kfi) +{ + if (kfi) + kfi_[i] = kfi; + else + kfi_.remove(i); +} + +/*! Adds the current Camera position() and orientation() as a keyFrame to the path number \p i. + +This method can also be used if you simply want to save a Camera point of view (a path made of a +single keyFrame). Use playPath() to make the Camera play the keyFrame path (resp. restore +the point of view). Use deletePath() to clear the path. + +The default keyboard shortcut for this method is Alt+F[1-12]. Set QGLViewer::pathKey() and +QGLViewer::addKeyFrameKeyboardModifiers(). + +If you use directly this method and the keyFrameInterpolator(i) does not exist, a new one is +created. Its KeyFrameInterpolator::interpolated() signal should then be connected to the +QGLViewer::update() slot (see setKeyFrameInterpolator()). */ +void Camera::addKeyFrameToPath(unsigned int i) +{ + if (!kfi_.contains(i)) + setKeyFrameInterpolator(i, new KeyFrameInterpolator(frame())); + + kfi_[i]->addKeyFrame(*(frame())); +} + +/*! Makes the Camera follow the path of keyFrameInterpolator() number \p i. + + If the interpolation is started, it stops it instead. + + This method silently ignores undefined (empty) paths (see keyFrameInterpolator()). + + The default keyboard shortcut for this method is F[1-12]. Set QGLViewer::pathKey() and + QGLViewer::playPathKeyboardModifiers(). */ +void Camera::playPath(unsigned int i) +{ + if (kfi_.contains(i)) { + if (kfi_[i]->interpolationIsStarted()) + kfi_[i]->stopInterpolation(); + else + kfi_[i]->startInterpolation(); + } +} + +/*! Resets the path of the keyFrameInterpolator() number \p i. + +If this path is \e not being played (see playPath() and +KeyFrameInterpolator::interpolationIsStarted()), resets it to its starting position (see +KeyFrameInterpolator::resetInterpolation()). If the path is played, simply stops interpolation. */ +void Camera::resetPath(unsigned int i) +{ + if (kfi_.contains(i)) { + if ((kfi_[i]->interpolationIsStarted())) + kfi_[i]->stopInterpolation(); + else + { + kfi_[i]->resetInterpolation(); + kfi_[i]->interpolateAtTime(kfi_[i]->interpolationTime()); + } + } +} + +/*! Deletes the keyFrameInterpolator() of index \p i. + +Disconnect the keyFrameInterpolator() KeyFrameInterpolator::interpolated() signal before deleting the +keyFrameInterpolator() if needed: +\code +disconnect(camera()->keyFrameInterpolator(i), SIGNAL(interpolated()), this, SLOT(update())); +camera()->deletePath(i); +\endcode */ +void Camera::deletePath(unsigned int i) +{ + if (kfi_.contains(i)) + { + kfi_[i]->stopInterpolation(); + delete kfi_[i]; + kfi_.remove(i); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +/*! Returns an XML \c QDomElement that represents the Camera. + + \p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create + QDomElement. + + Concatenates the Camera parameters, the ManipulatedCameraFrame::domElement() and the paths' + KeyFrameInterpolator::domElement(). + + Use initFromDOMElement() to restore the Camera state from the resulting \c QDomElement. + + If you want to save the Camera state in a file, use: + \code + QDomDocument document("myCamera"); + doc.appendChild( myCamera->domElement("Camera", document) ); + + QFile f("myCamera.xml"); + if (f.open(IO_WriteOnly)) + { + QTextStream out(&f); + document.save(out, 2); + } + \endcode + + Note that the QGLViewer::camera() is automatically saved by QGLViewer::saveStateToFile() when a + QGLViewer is closed. Use QGLViewer::restoreStateFromFile() to restore it back. */ +QDomElement Camera::domElement(const QString& name, QDomDocument& document) const +{ + QDomElement de = document.createElement(name); + QDomElement paramNode = document.createElement("Parameters"); + paramNode.setAttribute("fieldOfView", QString::number(fieldOfView())); + paramNode.setAttribute("zNearCoefficient", QString::number(zNearCoefficient())); + paramNode.setAttribute("zClippingCoefficient", QString::number(zClippingCoefficient())); + paramNode.setAttribute("orthoCoef", QString::number(orthoCoef_)); + paramNode.setAttribute("sceneRadius", QString::number(sceneRadius())); + paramNode.appendChild(sceneCenter().domElement("SceneCenter", document)); + + switch (type()) + { + case Camera::PERSPECTIVE : paramNode.setAttribute("Type", "PERSPECTIVE"); break; + case Camera::ORTHOGRAPHIC : paramNode.setAttribute("Type", "ORTHOGRAPHIC"); break; + } + de.appendChild(paramNode); + + QDomElement stereoNode = document.createElement("Stereo"); + stereoNode.setAttribute("IODist", QString::number(IODistance())); + stereoNode.setAttribute("focusDistance", QString::number(focusDistance())); + stereoNode.setAttribute("physScreenWidth", QString::number(physicalScreenWidth())); + de.appendChild(stereoNode); + + de.appendChild(frame()->domElement("ManipulatedCameraFrame", document)); + + // KeyFrame paths + for (QMap::ConstIterator it = kfi_.begin(), end=kfi_.end(); it != end; ++it) + { + QDomElement kfNode = (it.value())->domElement("KeyFrameInterpolator", document); + kfNode.setAttribute("index", QString::number(it.key())); + de.appendChild(kfNode); + } + + return de; +} + +/*! Restores the Camera state from a \c QDomElement created by domElement(). + + Use the following code to retrieve a Camera state from a file created using domElement(): + \code + // Load DOM from file + QDomDocument document; + QFile f("myCamera.xml"); + if (f.open(IO_ReadOnly)) + { + document.setContent(&f); + f.close(); + } + + // Parse the DOM tree + QDomElement main = document.documentElement(); + myCamera->initFromDOMElement(main); + \endcode + + The frame() pointer is not modified by this method. The frame() state is however modified. + + \attention The original keyFrameInterpolator() are deleted and should be copied first if they are shared. */ +void Camera::initFromDOMElement(const QDomElement& element) +{ + QDomElement child=element.firstChild().toElement(); + + QMutableMapIterator it(kfi_); + while (it.hasNext()) { + it.next(); + deletePath(it.key()); + } + + while (!child.isNull()) + { + if (child.tagName() == "Parameters") + { + // #CONNECTION# Default values set in constructor + setFieldOfView(DomUtils::qrealFromDom(child, "fieldOfView", M_PI/4.0)); + setZNearCoefficient(DomUtils::qrealFromDom(child, "zNearCoefficient", 0.005)); + setZClippingCoefficient(DomUtils::qrealFromDom(child, "zClippingCoefficient", sqrt(3.0))); + orthoCoef_ = DomUtils::qrealFromDom(child, "orthoCoef", tan(fieldOfView()/2.0)); + setSceneRadius(DomUtils::qrealFromDom(child, "sceneRadius", sceneRadius())); + + setType(PERSPECTIVE); + QString type = child.attribute("Type", "PERSPECTIVE"); + if (type == "PERSPECTIVE") setType(Camera::PERSPECTIVE); + if (type == "ORTHOGRAPHIC") setType(Camera::ORTHOGRAPHIC); + + QDomElement child2=child.firstChild().toElement(); + while (!child2.isNull()) + { + /* Although the scene does not change when a camera is loaded, restore the saved center and radius values. + Mainly useful when a the viewer is restored on startup, with possible additional cameras. */ + if (child2.tagName() == "SceneCenter") + setSceneCenter(Vec(child2)); + + child2 = child2.nextSibling().toElement(); + } + } + + if (child.tagName() == "ManipulatedCameraFrame") + frame()->initFromDOMElement(child); + + if (child.tagName() == "Stereo") + { + setIODistance(DomUtils::qrealFromDom(child, "IODist", 0.062)); + setFocusDistance(DomUtils::qrealFromDom(child, "focusDistance", focusDistance())); + setPhysicalScreenWidth(DomUtils::qrealFromDom(child, "physScreenWidth", 0.5)); + } + + if (child.tagName() == "KeyFrameInterpolator") + { + unsigned int index = DomUtils::uintFromDom(child, "index", 0); + setKeyFrameInterpolator(index, new KeyFrameInterpolator(frame())); + if (keyFrameInterpolator(index)) + keyFrameInterpolator(index)->initFromDOMElement(child); + } + + child = child.nextSibling().toElement(); + } +} + +/*! Gives the coefficients of a 3D half-line passing through the Camera eye and pixel (x,y). + + The origin of the half line (eye position) is stored in \p orig, while \p dir contains the properly + oriented and normalized direction of the half line. + + \p x and \p y are expressed in Qt format (origin in the upper left corner). Use screenHeight() - y + to convert to OpenGL units. + + This method is useful for analytical intersection in a selection method. + + See the select example for an illustration. */ +void Camera::convertClickToLine(const QPoint& pixel, Vec& orig, Vec& dir) const +{ + switch (type()) + { + case Camera::PERSPECTIVE: + orig = position(); + dir = Vec( ((2.0 * pixel.x() / screenWidth()) - 1.0) * tan(fieldOfView()/2.0) * aspectRatio(), + ((2.0 * (screenHeight()-pixel.y()) / screenHeight()) - 1.0) * tan(fieldOfView()/2.0), + -1.0 ); + dir = worldCoordinatesOf(dir) - orig; + dir.normalize(); + break; + + case Camera::ORTHOGRAPHIC: + { + GLdouble w,h; + getOrthoWidthHeight(w,h); + orig = Vec((2.0 * pixel.x() / screenWidth() - 1.0)*w, -(2.0 * pixel.y() / screenHeight() - 1.0)*h, 0.0); + orig = worldCoordinatesOf(orig); + dir = viewDirection(); + break; + } + } +} + + +/*! Returns the 6 plane equations of the Camera frustum. + +The six 4-component vectors of \p coef respectively correspond to the left, right, near, far, top +and bottom Camera frustum planes. Each vector holds a plane equation of the form: +\code +a*x + b*y + c*z + d = 0 +\endcode +where \c a, \c b, \c c and \c d are the 4 components of each vector, in that order. + +See the frustumCulling example for an application. + +This format is compatible with the \c glClipPlane() function. One camera frustum plane can hence be +applied in an other viewer to visualize the culling results: +\code + // Retrieve plane equations + GLdouble coef[6][4]; + mainViewer->camera()->getFrustumPlanesCoefficients(coef); + + // These two additional clipping planes (which must have been enabled) + // will reproduce the mainViewer's near and far clipping. + glClipPlane(GL_CLIP_PLANE0, coef[2]); + glClipPlane(GL_CLIP_PLANE1, coef[3]); +\endcode */ +void Camera::getFrustumPlanesCoefficients(GLdouble coef[6][4]) const +{ + // Computed once and for all + const Vec pos = position(); + const Vec viewDir = viewDirection(); + const Vec up = upVector(); + const Vec right = rightVector(); + const qreal posViewDir = pos * viewDir; + + static Vec normal[6]; + static GLdouble dist[6]; + + switch (type()) + { + case Camera::PERSPECTIVE : + { + const qreal hhfov = horizontalFieldOfView() / 2.0; + const qreal chhfov = cos(hhfov); + const qreal shhfov = sin(hhfov); + normal[0] = - shhfov * viewDir; + normal[1] = normal[0] + chhfov * right; + normal[0] = normal[0] - chhfov * right; + + normal[2] = -viewDir; + normal[3] = viewDir; + + const qreal hfov = fieldOfView() / 2.0; + const qreal chfov = cos(hfov); + const qreal shfov = sin(hfov); + normal[4] = - shfov * viewDir; + normal[5] = normal[4] - chfov * up; + normal[4] = normal[4] + chfov * up; + + for (int i=0; i<2; ++i) + dist[i] = pos * normal[i]; + for (int j=4; j<6; ++j) + dist[j] = pos * normal[j]; + + // Natural equations are: + // dist[0,1,4,5] = pos * normal[0,1,4,5]; + // dist[2] = (pos + zNear() * viewDir) * normal[2]; + // dist[3] = (pos + zFar() * viewDir) * normal[3]; + + // 2 times less computations using expanded/merged equations. Dir vectors are normalized. + const qreal posRightCosHH = chhfov * pos * right; + dist[0] = -shhfov * posViewDir; + dist[1] = dist[0] + posRightCosHH; + dist[0] = dist[0] - posRightCosHH; + const qreal posUpCosH = chfov * pos * up; + dist[4] = - shfov * posViewDir; + dist[5] = dist[4] - posUpCosH; + dist[4] = dist[4] + posUpCosH; + + break; + } + case Camera::ORTHOGRAPHIC : + normal[0] = -right; + normal[1] = right; + normal[4] = up; + normal[5] = -up; + + GLdouble hw, hh; + getOrthoWidthHeight(hw, hh); + dist[0] = (pos - hw * right) * normal[0]; + dist[1] = (pos + hw * right) * normal[1]; + dist[4] = (pos + hh * up) * normal[4]; + dist[5] = (pos - hh * up) * normal[5]; + break; + } + + // Front and far planes are identical for both camera types. + normal[2] = -viewDir; + normal[3] = viewDir; + dist[2] = -posViewDir - zNear(); + dist[3] = posViewDir + zFar(); + + for (int i=0; i<6; ++i) + { + coef[i][0] = GLdouble(normal[i].x); + coef[i][1] = GLdouble(normal[i].y); + coef[i][2] = GLdouble(normal[i].z); + coef[i][3] = dist[i]; + } +} + +void Camera::onFrameModified() { + projectionMatrixIsUpToDate_ = false; + modelViewMatrixIsUpToDate_ = false; +} diff --git a/QGLViewer/camera.h b/QGLViewer/camera.h new file mode 100644 index 0000000..f5d5ae1 --- /dev/null +++ b/QGLViewer/camera.h @@ -0,0 +1,505 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_CAMERA_H +#define QGLVIEWER_CAMERA_H + +#include +#include "keyFrameInterpolator.h" +class QGLViewer; + +namespace qglviewer { + +class ManipulatedCameraFrame; + +/*! \brief A perspective or orthographic camera. + \class Camera camera.h QGLViewer/camera.h + + A Camera defines some intrinsic parameters (fieldOfView(), position(), viewDirection(), + upVector()...) and useful positioning tools that ease its placement (showEntireScene(), + fitSphere(), lookAt()...). It exports its associated OpenGL projection and modelview matrices and + can interactively be modified using the mouse. + +

Mouse manipulation

+ + The position() and orientation() of the Camera are defined by a ManipulatedCameraFrame (retrieved + using frame()). These methods are just convenient wrappers to the equivalent Frame methods. This + also means that the Camera frame() can be attached to a Frame::referenceFrame() which enables + complex Camera setups. + + Different displacements can be performed using the mouse. The list of possible actions is defined + by the QGLViewer::MouseAction enum. Use QGLViewer::setMouseBinding() to attach a specific action + to an arbitrary mouse button-state key binding. These actions are detailed in the mouse page. + + The default button binding are: QGLViewer::ROTATE (left), QGLViewer::ZOOM (middle) and + QGLViewer::TRANSLATE (right). With this configuration, the Camera \e observes a scene and rotates + around its pivotPoint(). You can switch between this mode and a fly mode using the + QGLViewer::CAMERA_MODE (see QGLViewer::toggleCameraMode()) keyboard shortcut (default is 'Space'). + +

Other functionalities

+ + The type() of the Camera can be Camera::ORTHOGRAPHIC or Camera::PERSPECTIVE (see Type()). + fieldOfView() is meaningless with Camera::ORTHOGRAPHIC. + + The near and far planes of the Camera are fitted to the scene and determined from + QGLViewer::sceneRadius(), QGLViewer::sceneCenter() and zClippingCoefficient() by the zNear() and + zFar() methods. Reasonable values on the scene extends hence have to be provided to the QGLViewer + in order for the Camera to correctly display the scene. High level positioning methods also use + this information (showEntireScene(), centerScene()...). + + A Camera holds KeyFrameInterpolator that can be used to save Camera positions and paths. You can + interactively addKeyFrameToPath() to a given path using the default \c Alt+F[1-12] shortcuts. Use + playPath() to make the Camera follow the path (default shortcut is F[1-12]). See the keyboard page for details on key customization. + + Use cameraCoordinatesOf() and worldCoordinatesOf() to convert to and from the Camera frame() + coordinate system. projectedCoordinatesOf() and unprojectedCoordinatesOf() will convert from + screen to 3D coordinates. convertClickToLine() is very useful for analytical object selection. + + Stereo display is possible on machines with quad buffer capabilities (with Camera::PERSPECTIVE + type() only). Test the stereoViewer example to check. + + A Camera can also be used outside of a QGLViewer or even without OpenGL for its coordinate system + conversion capabilities. Note however that some of them explicitly rely on the presence of a + Z-buffer. \nosubgrouping */ +class QGLVIEWER_EXPORT Camera : public QObject +{ +#ifndef DOXYGEN + friend class ::QGLViewer; +#endif + + Q_OBJECT + +public: + Camera(); + virtual ~Camera(); + + Camera(const Camera& camera); + Camera& operator=(const Camera& camera); + + + /*! Enumerates the two possible types of Camera. + + See type() and setType(). This type mainly defines different Camera projection matrix (see + loadProjectionMatrix()). Many other methods (pointUnderPixel(), convertClickToLine(), + projectedCoordinatesOf(), pixelGLRatio()...) are affected by this Type. */ + enum Type { PERSPECTIVE, ORTHOGRAPHIC }; + + /*! @name Position and orientation */ + //@{ +public: + Vec position() const; + Vec upVector() const; + Vec viewDirection() const; + Vec rightVector() const; + Quaternion orientation() const; + + void setFromModelViewMatrix(const GLdouble* const modelViewMatrix); + void setFromProjectionMatrix(const qreal matrix[12]); + +public Q_SLOTS: + void setPosition(const Vec& pos); + void setOrientation(const Quaternion& q); + void setOrientation(qreal theta, qreal phi); + void setUpVector(const Vec& up, bool noMove=true); + void setViewDirection(const Vec& direction); + //@} + + + /*! @name Positioning tools */ + //@{ +public Q_SLOTS: + void lookAt(const Vec& target); + void showEntireScene(); + void fitSphere(const Vec& center, qreal radius); + void fitBoundingBox(const Vec& min, const Vec& max); + void fitScreenRegion(const QRect& rectangle); + void centerScene(); + void interpolateToZoomOnPixel(const QPoint& pixel); + void interpolateToFitScene(); + void interpolateTo(const Frame& fr, qreal duration); + //@} + + + /*! @name Frustum */ + //@{ +public: + /*! Returns the Camera::Type of the Camera. + + Set by setType(). Mainly used by loadProjectionMatrix(). + + A Camera::PERSPECTIVE Camera uses a classical projection mainly defined by its fieldOfView(). + + With a Camera::ORTHOGRAPHIC type(), the fieldOfView() is meaningless and the width and height of + the Camera frustum are inferred from the distance to the pivotPoint() using + getOrthoWidthHeight(). + + Both types use zNear() and zFar() (to define their clipping planes) and aspectRatio() (for + frustum shape). */ + Type type() const { return type_; } + + /*! Returns the vertical field of view of the Camera (in radians). + + Value is set using setFieldOfView(). Default value is pi/4 radians. This value is meaningless if + the Camera type() is Camera::ORTHOGRAPHIC. + + The field of view corresponds the one used in \c gluPerspective (see manual). It sets the Y + (vertical) aperture of the Camera. The X (horizontal) angle is inferred from the window aspect + ratio (see aspectRatio() and horizontalFieldOfView()). + + Use setFOVToFitScene() to adapt the fieldOfView() to a given scene. */ + qreal fieldOfView() const { return fieldOfView_; } + + /*! Returns the horizontal field of view of the Camera (in radians). + + Value is set using setHorizontalFieldOfView() or setFieldOfView(). These values + are always linked by: + \code + horizontalFieldOfView() = 2.0 * atan ( tan(fieldOfView()/2.0) * aspectRatio() ). + \endcode */ + qreal horizontalFieldOfView() const { return 2.0 * atan ( tan(fieldOfView()/2.0) * aspectRatio() ); } + + /*! Returns the Camera aspect ratio defined by screenWidth() / screenHeight(). + + When the Camera is attached to a QGLViewer, these values and hence the aspectRatio() are + automatically fitted to the viewer's window aspect ratio using setScreenWidthAndHeight(). */ + qreal aspectRatio() const { return screenWidth_ / static_cast(screenHeight_); } + /*! Returns the width (in pixels) of the Camera screen. + + Set using setScreenWidthAndHeight(). This value is automatically fitted to the QGLViewer's + window dimensions when the Camera is attached to a QGLViewer. See also QGLWidget::width() [TODO Update with QOpenGLWidget] */ + int screenWidth() const { return screenWidth_; } + /*! Returns the height (in pixels) of the Camera screen. + + Set using setScreenWidthAndHeight(). This value is automatically fitted to the QGLViewer's + window dimensions when the Camera is attached to a QGLViewer. See also QGLWidget::height() [TODO Update with QOpenGLWidget] */ + int screenHeight() const { return screenHeight_; } + void getViewport(GLint viewport[4]) const; + qreal pixelGLRatio(const Vec& position) const; + + /*! Returns the coefficient which is used to set zNear() when the Camera is inside the sphere + defined by sceneCenter() and zClippingCoefficient() * sceneRadius(). + + In that case, the zNear() value is set to zNearCoefficient() * zClippingCoefficient() * + sceneRadius(). See the zNear() documentation for details. + + Default value is 0.005, which is appropriate for most applications. In case you need a high + dynamic ZBuffer precision, you can increase this value (~0.1). A lower value will prevent + clipping of very close objects at the expense of a worst Z precision. + + Only meaningful when Camera type is Camera::PERSPECTIVE. */ + qreal zNearCoefficient() const { return zNearCoef_; } + /*! Returns the coefficient used to position the near and far clipping planes. + + The near (resp. far) clipping plane is positioned at a distance equal to zClippingCoefficient() * + sceneRadius() in front of (resp. behind) the sceneCenter(). This garantees an optimal use of + the z-buffer range and minimizes aliasing. See the zNear() and zFar() documentations. + + Default value is square root of 3.0 (so that a cube of size sceneRadius() is not clipped). + + However, since the sceneRadius() is used for other purposes (see showEntireScene(), flySpeed(), + ...) and you may want to change this value to define more precisely the location of the clipping + planes. See also zNearCoefficient(). + + For a total control on clipping planes' positions, an other option is to overload the zNear() + and zFar() methods. See the standardCamera example. + + \attention When QGLViewer::cameraPathAreEdited(), this value is set to 5.0 so that the Camera + paths are not clipped. The previous zClippingCoefficient() value is restored back when you leave + this mode. */ + qreal zClippingCoefficient() const { return zClippingCoef_; } + + virtual qreal zNear() const; + virtual qreal zFar() const; + virtual void getOrthoWidthHeight(GLdouble& halfWidth, GLdouble& halfHeight) const; + void getFrustumPlanesCoefficients(GLdouble coef[6][4]) const; + +public Q_SLOTS: + void setType(Type type); + + void setFieldOfView(qreal fov); + + /*! Sets the horizontalFieldOfView() of the Camera (in radians). + + horizontalFieldOfView() and fieldOfView() are linked by the aspectRatio(). This method actually + calls setFieldOfView(( 2.0 * atan (tan(hfov / 2.0) / aspectRatio()) )) so that a call to + horizontalFieldOfView() returns the expected value. */ + void setHorizontalFieldOfView(qreal hfov) { setFieldOfView( 2.0 * atan (tan(hfov / 2.0) / aspectRatio()) ); } + + void setFOVToFitScene(); + + /*! Defines the Camera aspectRatio(). + + This value is actually inferred from the screenWidth() / screenHeight() ratio. You should use + setScreenWidthAndHeight() instead. + + This method might however be convenient when the Camera is not associated with a QGLViewer. It + actually sets the screenHeight() to 100 and the screenWidth() accordingly. See also + setFOVToFitScene(). + + \note If you absolutely need an aspectRatio() that does not correspond to your viewer's window + dimensions, overload loadProjectionMatrix() or multiply the created GL_PROJECTION matrix by a + scaled diagonal matrix in your QGLViewer::draw() method. */ + void setAspectRatio(qreal aspect) { setScreenWidthAndHeight(int(100.0*aspect), 100); } + + void setScreenWidthAndHeight(int width, int height); + /*! Sets the zNearCoefficient() value. */ + void setZNearCoefficient(qreal coef) { zNearCoef_ = coef; projectionMatrixIsUpToDate_ = false; } + /*! Sets the zClippingCoefficient() value. */ + void setZClippingCoefficient(qreal coef) { zClippingCoef_ = coef; projectionMatrixIsUpToDate_ = false; } + //@} + + + /*! @name Scene radius and center */ + //@{ +public: + /*! Returns the radius of the scene observed by the Camera. + + You need to provide such an approximation of the scene dimensions so that the Camera can adapt + its zNear() and zFar() values. See the sceneCenter() documentation. + + See also setSceneBoundingBox(). + + Note that QGLViewer::sceneRadius() (resp. QGLViewer::setSceneRadius()) simply call this method + (resp. setSceneRadius()) on its associated QGLViewer::camera(). */ + qreal sceneRadius() const { return sceneRadius_; } + + /*! Returns the position of the scene center, defined in the world coordinate system. + + The scene observed by the Camera should be roughly centered on this position, and included in a + sceneRadius() sphere. This approximate description of the scene permits a zNear() and zFar() + clipping planes definition, and allows convenient positioning methods such as showEntireScene(). + + Default value is (0,0,0) (world origin). Use setSceneCenter() to change it. See also + setSceneBoundingBox(). + + Note that QGLViewer::sceneCenter() (resp. QGLViewer::setSceneCenter()) simply calls this method + (resp. setSceneCenter()) on its associated QGLViewer::camera(). */ + Vec sceneCenter() const { return sceneCenter_; } + qreal distanceToSceneCenter() const; + +public Q_SLOTS: + void setSceneRadius(qreal radius); + void setSceneCenter(const Vec& center); + bool setSceneCenterFromPixel(const QPoint& pixel); + void setSceneBoundingBox(const Vec& min, const Vec& max); + //@} + + + /*! @name Pivot Point */ + //@{ +public Q_SLOTS: + void setPivotPoint(const Vec& point); + bool setPivotPointFromPixel(const QPoint& pixel); + +public: + Vec pivotPoint() const; + +#ifndef DOXYGEN +public Q_SLOTS: + void setRevolveAroundPoint(const Vec& point); + bool setRevolveAroundPointFromPixel(const QPoint& pixel); +public: + Vec revolveAroundPoint() const; +#endif + //@} + + + /*! @name Associated frame */ + //@{ +public: + /*! Returns the ManipulatedCameraFrame attached to the Camera. + + This ManipulatedCameraFrame defines its position() and orientation() and can translate mouse + events into Camera displacement. Set using setFrame(). */ + ManipulatedCameraFrame* frame() const { return frame_; } +public Q_SLOTS: + void setFrame(ManipulatedCameraFrame* const mcf); + //@} + + + /*! @name KeyFramed paths */ + //@{ +public: + KeyFrameInterpolator* keyFrameInterpolator(unsigned int i) const; + +public Q_SLOTS: + void setKeyFrameInterpolator(unsigned int i, KeyFrameInterpolator* const kfi); + + virtual void addKeyFrameToPath(unsigned int i); + virtual void playPath(unsigned int i); + virtual void deletePath(unsigned int i); + virtual void resetPath(unsigned int i); + //@} + + + /*! @name OpenGL matrices */ + //@{ +public: + virtual void loadProjectionMatrix(bool reset=true) const; + virtual void loadModelViewMatrix(bool reset=true) const; + void computeProjectionMatrix() const; + void computeModelViewMatrix() const; + + virtual void loadModelViewMatrixStereo(bool leftBuffer=true) const; + + void getProjectionMatrix(GLfloat m[16]) const; + void getProjectionMatrix(GLdouble m[16]) const; + void getProjectionMatrix(QMatrix4x4& m) const; + + void getModelViewMatrix(GLfloat m[16]) const; + void getModelViewMatrix(GLdouble m[16]) const; + void getModelViewMatrix(QMatrix4x4& m) const; + + void getModelViewProjectionMatrix(GLfloat m[16]) const; + void getModelViewProjectionMatrix(GLdouble m[16]) const; + void getModelViewProjectionMatrix(QMatrix4x4& m) const; + //@} + + + /*! @name World to Camera coordinate systems conversions */ + //@{ +public: + Vec cameraCoordinatesOf(const Vec& src) const; + Vec worldCoordinatesOf(const Vec& src) const; + void getCameraCoordinatesOf(const qreal src[3], qreal res[3]) const; + void getWorldCoordinatesOf(const qreal src[3], qreal res[3]) const; + //@} + + + /*! @name 2D screen to 3D world coordinate systems conversions */ + //@{ +public: + Vec projectedCoordinatesOf(const Vec& src, const Frame* frame=NULL) const; + Vec unprojectedCoordinatesOf(const Vec& src, const Frame* frame=NULL) const; + void getProjectedCoordinatesOf(const qreal src[3], qreal res[3], const Frame* frame=NULL) const; + void getUnprojectedCoordinatesOf(const qreal src[3], qreal res[3], const Frame* frame=NULL) const; + void convertClickToLine(const QPoint& pixel, Vec& orig, Vec& dir) const; + Vec pointUnderPixel(const QPoint& pixel, bool& found) const; + //@} + + + /*! @name Fly speed */ + //@{ +public: + qreal flySpeed() const; +public Q_SLOTS: + void setFlySpeed(qreal speed); + //@} + + + /*! @name Stereo parameters */ + //@{ +public: + /*! Returns the user's inter-ocular distance (in meters). Default value is 0.062m, which fits most people. + + loadProjectionMatrixStereo() uses this value to define the Camera offset and frustum. See + setIODistance(). */ + qreal IODistance() const { return IODistance_; } + + /*! Returns the physical distance between the user's eyes and the screen (in meters). + + physicalDistanceToScreen() and focusDistance() represent the same distance. The former is + expressed in physical real world units, while the latter is expressed in OpenGL virtual world + units. + + This is a helper function. It simply returns physicalScreenWidth() / 2.0 / tan(horizontalFieldOfView() / 2.0); */ + qreal physicalDistanceToScreen() const { return physicalScreenWidth() / 2.0 / tan(horizontalFieldOfView() / 2.0); } + + /*! Returns the physical screen width, in meters. Default value is 0.5m (average monitor width). + + Used for stereo display only (see loadModelViewMatrixStereo() and loadProjectionMatrixStereo()). + Set using setPhysicalScreenWidth(). */ + qreal physicalScreenWidth() const { return physicalScreenWidth_; } + + /*! Returns the focus distance used by stereo display, expressed in OpenGL units. + + This is the distance in the virtual world between the Camera and the plane where the horizontal + stereo parallax is null (the stereo left and right cameras' lines of sigth cross at this distance). + + This distance is the virtual world equivalent of the real-world physicalDistanceToScreen(). + + \attention This value is modified by QGLViewer::setSceneRadius(), setSceneRadius() and + setFieldOfView(). When one of these values is modified, focusDistance() is set to sceneRadius() + / tan(fieldOfView()/2), which provides good results. */ + qreal focusDistance() const { return focusDistance_; } +public Q_SLOTS: + /*! Sets the IODistance(). */ + void setIODistance(qreal distance) { IODistance_ = distance; } + +#ifndef DOXYGEN + /*! This method is deprecated. Use setPhysicalScreenWidth() instead. */ + void setPhysicalDistanceToScreen(qreal distance) { Q_UNUSED(distance); qWarning("setPhysicalDistanceToScreen is deprecated, use setPhysicalScreenWidth instead"); } +#endif + + /*! Sets the physical screen (monitor or projected wall) width (in meters). */ + void setPhysicalScreenWidth(qreal width) { physicalScreenWidth_ = width; } + + /*! Sets the focusDistance(), in OpenGL scene units. */ + void setFocusDistance(qreal distance) { focusDistance_ = distance; } + //@} + + + /*! @name XML representation */ + //@{ +public: + virtual QDomElement domElement(const QString& name, QDomDocument& document) const; +public Q_SLOTS: + virtual void initFromDOMElement(const QDomElement& element); + //@} + + +private Q_SLOTS: + void onFrameModified(); + +private: + // F r a m e + ManipulatedCameraFrame* frame_; + + // C a m e r a p a r a m e t e r s + int screenWidth_, screenHeight_; // size of the window, in pixels + qreal fieldOfView_; // in radians + Vec sceneCenter_; + qreal sceneRadius_; // OpenGL units + qreal zNearCoef_; + qreal zClippingCoef_; + qreal orthoCoef_; + Type type_; // PERSPECTIVE or ORTHOGRAPHIC + mutable GLdouble modelViewMatrix_[16]; // Buffered model view matrix. + mutable bool modelViewMatrixIsUpToDate_; + mutable GLdouble projectionMatrix_[16]; // Buffered projection matrix. + mutable bool projectionMatrixIsUpToDate_; + + // S t e r e o p a r a m e t e r s + qreal IODistance_; // inter-ocular distance, in meters + qreal focusDistance_; // in scene units + qreal physicalScreenWidth_; // in meters + + // P o i n t s o f V i e w s a n d K e y F r a m e s + QMap kfi_; + KeyFrameInterpolator* interpolationKfi_; +}; + +} // namespace qglviewer + +#endif // QGLVIEWER_CAMERA_H diff --git a/QGLViewer/config.h b/QGLViewer/config.h new file mode 100644 index 0000000..c288242 --- /dev/null +++ b/QGLViewer/config.h @@ -0,0 +1,97 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +/////////////////////////////////////////////////////////////////// +// libQGLViewer configuration file // +// Modify these settings according to your local configuration // +/////////////////////////////////////////////////////////////////// + +#ifndef QGLVIEWER_CONFIG_H +#define QGLVIEWER_CONFIG_H + +#define QGLVIEWER_VERSION 0x020603 + +// Needed for Qt < 4 (?) +#ifndef QT_CLEAN_NAMESPACE +# define QT_CLEAN_NAMESPACE +#endif + +// Get QT_VERSION and other Qt flags +#include + +#if QT_VERSION < 0x040000 +Error : libQGLViewer requires a minimum Qt version of 4.0 +#endif + +// Win 32 DLL export macros +#ifdef Q_OS_WIN32 +# ifndef M_PI +# define M_PI 3.14159265358979323846 +# endif +# ifndef QGLVIEWER_STATIC +# ifdef CREATE_QGLVIEWER_DLL +# if QT_VERSION >= 0x040500 +# define QGLVIEWER_EXPORT Q_DECL_EXPORT +# else +# define QGLVIEWER_EXPORT __declspec(dllexport) +# endif +# else +# if QT_VERSION >= 0x040500 +# define QGLVIEWER_EXPORT Q_DECL_IMPORT +# else +# define QGLVIEWER_EXPORT __declspec(dllimport) +# endif +# endif +# endif +# ifndef __MINGW32__ +# pragma warning( disable : 4251 ) // DLL interface, needed with Visual 6 +# pragma warning( disable : 4786 ) // identifier truncated to 255 in browser information (Visual 6). +# endif +#endif // Q_OS_WIN32 + +// For other architectures, this macro is empty +#ifndef QGLVIEWER_EXPORT +# define QGLVIEWER_EXPORT +#endif + +// OpenGL includes - Included here and hence shared by all the files that need OpenGL headers. +# include + +// Container classes interfaces changed a lot in Qt. +// Compatibility patches are all grouped here. +#include +#include + +// For deprecated methods +// #define __WHERE__ "In file "<<__FILE__<<", line "<<__LINE__<<": " +// #define orientationAxisAngle(x,y,z,a) { std::cout << __WHERE__ << "getOrientationAxisAngle()." << std::endl; exit(0); } + +// Patch for gcc version <= 2.95. Seems to no longer be needed with recent Qt versions. +// Uncomment these lines if you have error message dealing with operator << on QStrings +// #if defined(__GNUC__) && defined(__GNUC_MINOR__) && (__GNUC__ < 3) && (__GNUC_MINOR__ < 96) +// # include +// # include +// std::ostream& operator<<(std::ostream& out, const QString& str) +// { out << str.latin1(); return out; } +// #endif + +#endif // QGLVIEWER_CONFIG_H diff --git a/QGLViewer/constraint.cpp b/QGLViewer/constraint.cpp new file mode 100644 index 0000000..fbb9a6c --- /dev/null +++ b/QGLViewer/constraint.cpp @@ -0,0 +1,291 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "constraint.h" +#include "frame.h" +#include "camera.h" +#include "manipulatedCameraFrame.h" + +using namespace qglviewer; +using namespace std; + +//////////////////////////////////////////////////////////////////////////////// +// Constraint // +//////////////////////////////////////////////////////////////////////////////// + +/*! Default constructor. + +translationConstraintType() and rotationConstraintType() are set to AxisPlaneConstraint::FREE. +translationConstraintDirection() and rotationConstraintDirection() are set to (0,0,0). */ +AxisPlaneConstraint::AxisPlaneConstraint() + : translationConstraintType_(FREE), rotationConstraintType_(FREE) +{ + // Do not use set since setRotationConstraintType needs a read. +} + +/*! Simply calls setTranslationConstraintType() and setTranslationConstraintDirection(). */ +void AxisPlaneConstraint::setTranslationConstraint(Type type, const Vec& direction) +{ + setTranslationConstraintType(type); + setTranslationConstraintDirection(direction); +} + +/*! Defines the translationConstraintDirection(). The coordinate system where \p direction is expressed depends on your class implementation. */ +void AxisPlaneConstraint::setTranslationConstraintDirection(const Vec& direction) +{ + if ((translationConstraintType()!=AxisPlaneConstraint::FREE) && (translationConstraintType()!=AxisPlaneConstraint::FORBIDDEN)) + { + const qreal norm = direction.norm(); + if (norm < 1E-8) + { + qWarning("AxisPlaneConstraint::setTranslationConstraintDir: null vector for translation constraint"); + translationConstraintType_ = AxisPlaneConstraint::FREE; + } + else + translationConstraintDir_ = direction/norm; + } +} + +/*! Simply calls setRotationConstraintType() and setRotationConstraintDirection(). */ +void AxisPlaneConstraint::setRotationConstraint(Type type, const Vec& direction) +{ + setRotationConstraintType(type); + setRotationConstraintDirection(direction); +} + +/*! Defines the rotationConstraintDirection(). The coordinate system where \p direction is expressed depends on your class implementation. */ +void AxisPlaneConstraint::setRotationConstraintDirection(const Vec& direction) +{ + if ((rotationConstraintType()!=AxisPlaneConstraint::FREE) && (rotationConstraintType()!=AxisPlaneConstraint::FORBIDDEN)) + { + const qreal norm = direction.norm(); + if (norm < 1E-8) + { + qWarning("AxisPlaneConstraint::setRotationConstraintDir: null vector for rotation constraint"); + rotationConstraintType_ = AxisPlaneConstraint::FREE; + } + else + rotationConstraintDir_ = direction/norm; + } +} + +/*! Set the Type() of the rotationConstraintType(). Default is AxisPlaneConstraint::FREE. + + Depending on this value, the Frame will freely rotate (AxisPlaneConstraint::FREE), will only be able + to rotate around an axis (AxisPlaneConstraint::AXIS), or will not able to rotate at all + (AxisPlaneConstraint::FORBIDDEN). + + Use Frame::setOrientation() to define the orientation of the constrained Frame before it gets + constrained. + + \attention An AxisPlaneConstraint::PLANE Type() is not meaningful for rotational constraints and + will be ignored. */ +void AxisPlaneConstraint::setRotationConstraintType(Type type) +{ + if (rotationConstraintType() == AxisPlaneConstraint::PLANE) + { + qWarning("AxisPlaneConstraint::setRotationConstraintType: the PLANE type cannot be used for a rotation constraints"); + return; + } + + rotationConstraintType_ = type; +} + + +//////////////////////////////////////////////////////////////////////////////// +// LocalConstraint // +//////////////////////////////////////////////////////////////////////////////// + +/*! Depending on translationConstraintType(), constrain \p translation to be along an axis or + limited to a plane defined in the Frame local coordinate system by + translationConstraintDirection(). */ +void LocalConstraint::constrainTranslation(Vec& translation, Frame* const frame) +{ + Vec proj; + switch (translationConstraintType()) + { + case AxisPlaneConstraint::FREE: + break; + case AxisPlaneConstraint::PLANE: + proj = frame->rotation().rotate(translationConstraintDirection()); + translation.projectOnPlane(proj); + break; + case AxisPlaneConstraint::AXIS: + proj = frame->rotation().rotate(translationConstraintDirection()); + translation.projectOnAxis(proj); + break; + case AxisPlaneConstraint::FORBIDDEN: + translation = Vec(0.0, 0.0, 0.0); + break; + } +} + +/*! When rotationConstraintType() is AxisPlaneConstraint::AXIS, constrain \p rotation to be a rotation + around an axis whose direction is defined in the Frame local coordinate system by + rotationConstraintDirection(). */ +void LocalConstraint::constrainRotation(Quaternion& rotation, Frame* const) +{ + switch (rotationConstraintType()) + { + case AxisPlaneConstraint::FREE: + break; + case AxisPlaneConstraint::PLANE: + break; + case AxisPlaneConstraint::AXIS: + { + Vec axis = rotationConstraintDirection(); + Vec quat = Vec(rotation[0], rotation[1], rotation[2]); + quat.projectOnAxis(axis); + rotation = Quaternion(quat, 2.0*acos(rotation[3])); + } + break; + case AxisPlaneConstraint::FORBIDDEN: + rotation = Quaternion(); // identity + break; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// WorldConstraint // +//////////////////////////////////////////////////////////////////////////////// + +/*! Depending on translationConstraintType(), constrain \p translation to be along an axis or + limited to a plane defined in the world coordinate system by + translationConstraintDirection(). */ +void WorldConstraint::constrainTranslation(Vec& translation, Frame* const frame) +{ + Vec proj; + switch (translationConstraintType()) + { + case AxisPlaneConstraint::FREE: + break; + case AxisPlaneConstraint::PLANE: + if (frame->referenceFrame()) + { + proj = frame->referenceFrame()->transformOf(translationConstraintDirection()); + translation.projectOnPlane(proj); + } + else + translation.projectOnPlane(translationConstraintDirection()); + break; + case AxisPlaneConstraint::AXIS: + if (frame->referenceFrame()) + { + proj = frame->referenceFrame()->transformOf(translationConstraintDirection()); + translation.projectOnAxis(proj); + } + else + translation.projectOnAxis(translationConstraintDirection()); + break; + case AxisPlaneConstraint::FORBIDDEN: + translation = Vec(0.0, 0.0, 0.0); + break; + } +} + +/*! When rotationConstraintType() is AxisPlaneConstraint::AXIS, constrain \p rotation to be a rotation + around an axis whose direction is defined in the world coordinate system by + rotationConstraintDirection(). */ +void WorldConstraint::constrainRotation(Quaternion& rotation, Frame* const frame) +{ + switch (rotationConstraintType()) + { + case AxisPlaneConstraint::FREE: + break; + case AxisPlaneConstraint::PLANE: + break; + case AxisPlaneConstraint::AXIS: + { + Vec quat(rotation[0], rotation[1], rotation[2]); + Vec axis = frame->transformOf(rotationConstraintDirection()); + quat.projectOnAxis(axis); + rotation = Quaternion(quat, 2.0*acos(rotation[3])); + break; + } + case AxisPlaneConstraint::FORBIDDEN: + rotation = Quaternion(); // identity + break; + } +} + +//////////////////////////////////////////////////////////////////////////////// +// CameraConstraint // +//////////////////////////////////////////////////////////////////////////////// + +/*! Creates a CameraConstraint, whose constrained directions are defined in the \p camera coordinate + system. */ +CameraConstraint::CameraConstraint(const Camera* const camera) + : AxisPlaneConstraint(), camera_(camera) +{} + +/*! Depending on translationConstraintType(), constrain \p translation to be along an axis or + limited to a plane defined in the camera() coordinate system by + translationConstraintDirection(). */ +void CameraConstraint::constrainTranslation(Vec& translation, Frame* const frame) +{ + Vec proj; + switch (translationConstraintType()) + { + case AxisPlaneConstraint::FREE: + break; + case AxisPlaneConstraint::PLANE: + proj = camera()->frame()->inverseTransformOf(translationConstraintDirection()); + if (frame->referenceFrame()) + proj = frame->referenceFrame()->transformOf(proj); + translation.projectOnPlane(proj); + break; + case AxisPlaneConstraint::AXIS: + proj = camera()->frame()->inverseTransformOf(translationConstraintDirection()); + if (frame->referenceFrame()) + proj = frame->referenceFrame()->transformOf(proj); + translation.projectOnAxis(proj); + break; + case AxisPlaneConstraint::FORBIDDEN: + translation = Vec(0.0, 0.0, 0.0); + break; + } +} + +/*! When rotationConstraintType() is AxisPlaneConstraint::AXIS, constrain \p rotation to be a rotation + around an axis whose direction is defined in the camera() coordinate system by + rotationConstraintDirection(). */ +void CameraConstraint::constrainRotation(Quaternion& rotation, Frame* const frame) +{ + switch (rotationConstraintType()) + { + case AxisPlaneConstraint::FREE: + break; + case AxisPlaneConstraint::PLANE: + break; + case AxisPlaneConstraint::AXIS: + { + Vec axis = frame->transformOf(camera()->frame()->inverseTransformOf(rotationConstraintDirection())); + Vec quat = Vec(rotation[0], rotation[1], rotation[2]); + quat.projectOnAxis(axis); + rotation = Quaternion(quat, 2.0*acos(rotation[3])); + } + break; + case AxisPlaneConstraint::FORBIDDEN: + rotation = Quaternion(); // identity + break; + } +} diff --git a/QGLViewer/constraint.h b/QGLViewer/constraint.h new file mode 100644 index 0000000..d90d820 --- /dev/null +++ b/QGLViewer/constraint.h @@ -0,0 +1,338 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_CONSTRAINT_H +#define QGLVIEWER_CONSTRAINT_H + +#include "vec.h" +#include "quaternion.h" + +namespace qglviewer { +class Frame; +class Camera; + +/*! \brief An interface class for Frame constraints. + \class Constraint constraint.h QGLViewer/constraint.h + + This class defines the interface for the Constraints that can be applied to a Frame to limit its + motion. Use Frame::setConstraint() to associate a Constraint to a Frame (default is a \c NULL + Frame::constraint()). + +

How does it work ?

+ + The Constraint acts as a filter on the translation and rotation Frame increments. + constrainTranslation() and constrainRotation() should be overloaded to specify the constraint + behavior: the desired displacement is given as a parameter that can optionally be modified. + + Here is how the Frame::translate() and Frame::rotate() methods use the Constraint: + \code + Frame::translate(Vec& T) + { + if (constraint()) + constraint()->constrainTranslation(T, this); + t += T; + } + + Frame::rotate(Quaternion& Q) + { + if (constraint()) + constraint()->constrainRotation(Q, this); + q *= Q; + } + \endcode + + The default behavior of constrainTranslation() and constrainRotation() is empty (meaning no + filtering). + + The Frame which uses the Constraint is passed as a parameter to the constrainTranslation() and + constrainRotation() methods, so that they can have access to its current state (mainly + Frame::position() and Frame::orientation()). It is not \c const for versatility reasons, but + directly modifying it should be avoided. + + \attention Frame::setTranslation(), Frame::setRotation() and similar methods will actually indeed + set the frame position and orientation, without taking the constraint into account. Use the \e + WithConstraint versions of these methods to enforce the Constraint. + +

Implemented Constraints

+ + Classical axial and plane Constraints are provided for convenience: see the LocalConstraint, + WorldConstraint and CameraConstraint classes' documentations. + + Try the constrainedFrame and constrainedCamera examples for an illustration. + +

Creating new Constraints

+ + The implementation of a new Constraint class simply consists in overloading the filtering methods: + \code + // This Constraint enforces that the Frame cannot have a negative z world coordinate. + class myConstraint : public Constraint + { + public: + virtual void constrainTranslation(Vec& t, Frame * const fr) + { + // Express t in the world coordinate system. + const Vec tWorld = fr->inverseTransformOf(t); + if (fr->position().z + tWorld.z < 0.0) // check the new fr z coordinate + t.z = fr->transformOf(-fr->position().z); // t.z is clamped so that next z position is 0.0 + } + }; + \endcode + + Note that the translation (resp. rotation) parameter passed to constrainTranslation() (resp. + constrainRotation()) is expressed in the \e local Frame coordinate system. Here, we use the + Frame::transformOf() and Frame::inverseTransformOf() method to convert it to and from the world + coordinate system. + + Combined constraints can easily be achieved by creating a new class that applies the different + constraint filters: + \code + myConstraint::constrainTranslation(Vec& v, Frame* const fr) + { + constraint1->constrainTranslation(v, fr); + constraint2->constrainTranslation(v, fr); + // and so on, with possible branches, tests, loops... + } + \endcode + */ +class QGLVIEWER_EXPORT Constraint +{ +public: + /*! Virtual destructor. Empty. */ + virtual ~Constraint() {} + + /*! Filters the translation applied to the \p frame. This default implementation is empty (no + filtering). + + Overload this method in your own Constraint class to define a new translation constraint. \p + frame is the Frame to which is applied the translation. It is not defined \c const, but you + should refrain from directly changing its value in the constraint. Use its Frame::position() and + update the \p translation accordingly instead. + + \p translation is expressed in local frame coordinate system. Use Frame::inverseTransformOf() to + express it in the world coordinate system if needed. */ + virtual void constrainTranslation(Vec& translation, Frame* const frame) { Q_UNUSED(translation); Q_UNUSED(frame); } + /*! Filters the rotation applied to the \p frame. This default implementation is empty (no + filtering). + + Overload this method in your own Constraint class to define a new rotation constraint. See + constrainTranslation() for details. + + Use Frame::inverseTransformOf() on the \p rotation Quaternion::axis() to express \p rotation in + the world coordinate system if needed. */ + virtual void constrainRotation(Quaternion& rotation, Frame* const frame) { Q_UNUSED(rotation); Q_UNUSED(frame); } +}; + +/*! + \brief An abstract class for Frame Constraints defined by an axis or a plane. + \class AxisPlaneConstraint constraint.h QGLViewer/constraint.h + + AxisPlaneConstraint is an interface for (translation and/or rotation) Constraint that are defined + by a direction. translationConstraintType() and rotationConstraintType() define how this + direction should be interpreted: as an axis (AxisPlaneConstraint::AXIS) or as a plane normal + (AxisPlaneConstraint::PLANE). See the Type() documentation for details. + + The three implementations of this class: LocalConstraint, WorldConstraint and CameraConstraint + differ by the coordinate system in which this direction is expressed. + + Different implementations of this class are illustrated in the + contrainedCamera and + constrainedFrame examples. + + \attention When applied, the rotational Constraint may not intuitively follow the mouse + displacement. A solution would be to directly measure the rotation angle in screen coordinates, + but that would imply to know the QGLViewer::camera(), so that we can compute the projected + coordinates of the rotation center (as is done with the QGLViewer::SCREEN_ROTATE binding). + However, adding an extra pointer to the QGLViewer::camera() in all the AxisPlaneConstraint + derived classes (which the user would have to update in a multi-viewer application) was judged as + an overkill. */ +class QGLVIEWER_EXPORT AxisPlaneConstraint : public Constraint +{ +public: + AxisPlaneConstraint(); + /*! Virtual destructor. Empty. */ + virtual ~AxisPlaneConstraint() {} + + /*! Type lists the different types of translation and rotation constraints that are available. + + It specifies the meaning of the constraint direction (see translationConstraintDirection() and + rotationConstraintDirection()): as an axis direction (AxisPlaneConstraint::AXIS) or a plane + normal (AxisPlaneConstraint::PLANE). AxisPlaneConstraint::FREE means no constraint while + AxisPlaneConstraint::FORBIDDEN completely forbids the translation and/or the rotation. + + See translationConstraintType() and rotationConstraintType(). + + \attention The AxisPlaneConstraint::PLANE Type is not valid for rotational constraint. + + New derived classes can use their own extended enum for specific constraints: + \code + class MyAxisPlaneConstraint : public AxisPlaneConstraint + { + public: + enum MyType { FREE, AXIS, PLANE, FORBIDDEN, CUSTOM }; + virtual void constrainTranslation(Vec &translation, Frame *const frame) + { + // translationConstraintType() is simply an int. CUSTOM Type is handled seamlessly. + switch (translationConstraintType()) + { + case MyAxisPlaneConstraint::FREE: ... break; + case MyAxisPlaneConstraint::CUSTOM: ... break; + } + }; + + MyAxisPlaneConstraint* c = new MyAxisPlaneConstraint(); + // Note the Type conversion + c->setTranslationConstraintType(AxisPlaneConstraint::Type(MyAxisPlaneConstraint::CUSTOM)); + }; + \endcode */ + enum Type { FREE, AXIS, PLANE, FORBIDDEN }; + + /*! @name Translation constraint */ + //@{ + /*! Overloading of Constraint::constrainTranslation(). Empty */ + virtual void constrainTranslation(Vec& translation, Frame* const frame) { Q_UNUSED(translation); Q_UNUSED(frame); }; + + void setTranslationConstraint(Type type, const Vec& direction); + /*! Sets the Type() of the translationConstraintType(). Default is AxisPlaneConstraint::FREE. */ + void setTranslationConstraintType(Type type) { translationConstraintType_ = type; }; + void setTranslationConstraintDirection(const Vec& direction); + + /*! Returns the translation constraint Type(). + + Depending on this value, the Frame will freely translate (AxisPlaneConstraint::FREE), will only + be able to translate along an axis direction (AxisPlaneConstraint::AXIS), will be forced to stay + into a plane (AxisPlaneConstraint::PLANE) or will not able to translate at all + (AxisPlaneConstraint::FORBIDDEN). + + Use Frame::setPosition() to define the position of the constrained Frame before it gets + constrained. */ + Type translationConstraintType() const { return translationConstraintType_; }; + /*! Returns the direction used by the translation constraint. + + It represents the axis direction (AxisPlaneConstraint::AXIS) or the plane normal + (AxisPlaneConstraint::PLANE) depending on the translationConstraintType(). It is undefined for + AxisPlaneConstraint::FREE or AxisPlaneConstraint::FORBIDDEN. + + The AxisPlaneConstraint derived classes express this direction in different coordinate system + (camera for CameraConstraint, local for LocalConstraint, and world for WorldConstraint). This + value can be modified with setTranslationConstraintDirection(). */ + Vec translationConstraintDirection() const { return translationConstraintDir_; }; + //@} + + /*! @name Rotation constraint */ + //@{ + /*! Overloading of Constraint::constrainRotation(). Empty. */ + virtual void constrainRotation(Quaternion& rotation, Frame* const frame) { Q_UNUSED(rotation); Q_UNUSED(frame); }; + + void setRotationConstraint(Type type, const Vec& direction); + void setRotationConstraintType(Type type); + void setRotationConstraintDirection(const Vec& direction); + + /*! Returns the rotation constraint Type(). */ + Type rotationConstraintType() const { return rotationConstraintType_; }; + /*! Returns the axis direction used by the rotation constraint. + + This direction is defined only when rotationConstraintType() is AxisPlaneConstraint::AXIS. + + The AxisPlaneConstraint derived classes express this direction in different coordinate system + (camera for CameraConstraint, local for LocalConstraint, and world for WorldConstraint). This + value can be modified with setRotationConstraintDirection(). */ + Vec rotationConstraintDirection() const { return rotationConstraintDir_; }; + //@} + +private: + // int and not Type to allow for overloading and new types definition. + Type translationConstraintType_; + Type rotationConstraintType_; + + Vec translationConstraintDir_; + Vec rotationConstraintDir_; +}; + + +/*! \brief An AxisPlaneConstraint defined in the Frame local coordinate system. + \class LocalConstraint constraint.h QGLViewer/constraint.h + + The translationConstraintDirection() and rotationConstraintDirection() are expressed in the Frame + local coordinate system (see Frame::referenceFrame()). + + See the constrainedFrame example for an illustration. */ +class QGLVIEWER_EXPORT LocalConstraint : public AxisPlaneConstraint +{ +public: + /*! Virtual destructor. Empty. */ + virtual ~LocalConstraint() {}; + + virtual void constrainTranslation(Vec& translation, Frame* const frame); + virtual void constrainRotation (Quaternion& rotation, Frame* const frame); +}; + + + +/*! \brief An AxisPlaneConstraint defined in the world coordinate system. + \class WorldConstraint constraint.h QGLViewer/constraint.h + + The translationConstraintDirection() and rotationConstraintDirection() are expressed in world + coordinate system. + + See the constrainedFrame and multiView examples for an illustration. */ +class QGLVIEWER_EXPORT WorldConstraint : public AxisPlaneConstraint +{ +public: + /*! Virtual destructor. Empty. */ + virtual ~WorldConstraint() {}; + + virtual void constrainTranslation(Vec& translation, Frame* const frame); + virtual void constrainRotation (Quaternion& rotation, Frame* const frame); +}; + + + +/*! \brief An AxisPlaneConstraint defined in the camera coordinate system. + \class CameraConstraint constraint.h QGLViewer/constraint.h + + The translationConstraintDirection() and rotationConstraintDirection() are expressed in the + associated camera() coordinate system. + + See the constrainedFrame and constrainedCamera examples for an illustration. */ +class QGLVIEWER_EXPORT CameraConstraint : public AxisPlaneConstraint +{ +public: + explicit CameraConstraint(const Camera* const camera); + /*! Virtual destructor. Empty. */ + virtual ~CameraConstraint() {}; + + virtual void constrainTranslation(Vec& translation, Frame* const frame); + virtual void constrainRotation (Quaternion& rotation, Frame* const frame); + + /*! Returns the associated Camera. Set using the CameraConstraint constructor. */ + const Camera* camera() const { return camera_; }; + +private: + const Camera* const camera_; +}; + +} // namespace qglviewer + +#endif // QGLVIEWER_CONSTRAINT_H diff --git a/QGLViewer/domUtils.h b/QGLViewer/domUtils.h new file mode 100644 index 0000000..3994468 --- /dev/null +++ b/QGLViewer/domUtils.h @@ -0,0 +1,161 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "config.h" + +#include +#include +#include +#include + +#include + +#ifndef DOXYGEN + +// QDomElement loading with syntax checking. +class DomUtils +{ +private: + static void warning(const QString& message) + { + qWarning("%s", message.toLatin1().constData()); + } + +public: + static qreal qrealFromDom(const QDomElement& e, const QString& attribute, qreal defValue) + { + qreal value = defValue; + if (e.hasAttribute(attribute)) { + const QString s = e.attribute(attribute); + bool ok; + value = s.toDouble(&ok); + if (!ok) { + warning(QString("'%1' is not a valid qreal syntax for attribute \"%2\" in initialization of \"%3\". Setting value to %4.") + .arg(s).arg(attribute).arg(e.tagName()).arg(QString::number(defValue))); + value = defValue; + } + } else { + warning(QString("\"%1\" attribute missing in initialization of \"%2\". Setting value to %3.") + .arg(attribute).arg(e.tagName()).arg(QString::number(value))); + } + +#if defined(isnan) + // The "isnan" method may not be available on all platforms. + // Find its equivalent or simply remove these two lines + if (isnan(value)) + warning(QString("Warning, attribute \"%1\" initialized to Not a Number in \"%2\"") + .arg(attribute).arg(e.tagName())); +#endif + + return value; + } + + static int intFromDom(const QDomElement& e, const QString& attribute, int defValue) + { + int value = defValue; + if (e.hasAttribute(attribute)) + { + const QString s = e.attribute(attribute); + bool ok; + value = s.toInt(&ok); + if (!ok) { + warning(QString("'%1' is not a valid integer syntax for attribute \"%2\" in initialization of \"%3\". Setting value to %4.") + .arg(s).arg(attribute).arg(e.tagName()).arg(QString::number(defValue))); + value = defValue; + } + } else { + warning(QString("\"%1\" attribute missing in initialization of \"%2\". Setting value to %3.") + .arg(attribute).arg(e.tagName()).arg(QString::number(value))); + } + + return value; + } + + static unsigned int uintFromDom(const QDomElement& e, const QString& attribute, unsigned int defValue) + { + unsigned int value = defValue; + if (e.hasAttribute(attribute)) + { + const QString s = e.attribute(attribute); + bool ok; + value = s.toUInt(&ok); + if (!ok) { + warning(QString("'%1' is not a valid unsigned integer syntax for attribute \"%2\" in initialization of \"%3\". Setting value to %4.") + .arg(s).arg(attribute).arg(e.tagName()).arg(QString::number(defValue))); + value = defValue; + } + } else { + warning(QString("\"%1\" attribute missing in initialization of \"%2\". Setting value to %3.") + .arg(attribute).arg(e.tagName()).arg(QString::number(value))); + } + + return value; + } + + static bool boolFromDom(const QDomElement& e, const QString& attribute, bool defValue) + { + bool value = defValue; + if (e.hasAttribute(attribute)) + { + const QString s = e.attribute(attribute); + if (s.toLower() == QString("true")) + value = true; + else if (s.toLower() == QString("false")) + value = false; + else + { + warning(QString("'%1' is not a valid boolean syntax for attribute \"%2\" in initialization of \"%3\". Setting value to %4.") + .arg(s).arg(attribute).arg(e.tagName()).arg(defValue?"true":"false")); + } + } else { + warning(QString("\"%1\" attribute missing in initialization of \"%2\". Setting value to %3.") + .arg(attribute).arg(e.tagName()).arg(value?"true":"false")); + } + + return value; + } + + static void setBoolAttribute(QDomElement& element, const QString& attribute, bool value) { + element.setAttribute(attribute, (value ? "true" : "false")); + } + + static QDomElement QColorDomElement(const QColor& color, const QString& name, QDomDocument& doc) + { + QDomElement de = doc.createElement(name); + de.setAttribute("red", QString::number(color.red())); + de.setAttribute("green", QString::number(color.green())); + de.setAttribute("blue", QString::number(color.blue())); + return de; + } + + static QColor QColorFromDom(const QDomElement& e) + { + int color[3]; + QStringList attribute; + attribute << "red" << "green" << "blue"; + for (int i=0; i + +using namespace qglviewer; +using namespace std; + + +/*! Creates a default Frame. + + Its position() is (0,0,0) and it has an identity orientation() Quaternion. The referenceFrame() + and the constraint() are \c NULL. */ +Frame::Frame() + : constraint_(NULL), referenceFrame_(NULL) +{} + +/*! Creates a Frame with a position() and an orientation(). + + See the Vec and Quaternion documentations for convenient constructors and methods. + + The Frame is defined in the world coordinate system (its referenceFrame() is \c NULL). It + has a \c NULL associated constraint(). */ +Frame::Frame(const Vec& position, const Quaternion& orientation) + : t_(position), q_(orientation), constraint_(NULL), referenceFrame_(NULL) +{} + +/*! Equal operator. + + The referenceFrame() and constraint() pointers are copied. + + \attention Signal and slot connections are not copied. */ +Frame& Frame::operator=(const Frame& frame) +{ + // Automatic compiler generated version would not emit the modified() signals as is done in + // setTranslationAndRotation. + setTranslationAndRotation(frame.translation(), frame.rotation()); + setConstraint(frame.constraint()); + setReferenceFrame(frame.referenceFrame()); + return *this; +} + +/*! Copy constructor. + + The translation() and rotation() as well as constraint() and referenceFrame() pointers are + copied. */ +Frame::Frame(const Frame& frame) + : QObject() +{ + (*this) = frame; +} + +/////////////////////////////// MATRICES ////////////////////////////////////// + +/*! Returns the 4x4 OpenGL transformation matrix represented by the Frame. + + This method should be used in conjunction with \c glMultMatrixd() to modify the OpenGL modelview + matrix from a Frame hierarchy. With this Frame hierarchy: + \code + Frame* body = new Frame(); + Frame* leftArm = new Frame(); + Frame* rightArm = new Frame(); + leftArm->setReferenceFrame(body); + rightArm->setReferenceFrame(body); + \endcode + + The associated OpenGL drawing code should look like: + \code + void Viewer::draw() + { + glPushMatrix(); + glMultMatrixd(body->matrix()); + drawBody(); + + glPushMatrix(); + glMultMatrixd(leftArm->matrix()); + drawArm(); + glPopMatrix(); + + glPushMatrix(); + glMultMatrixd(rightArm->matrix()); + drawArm(); + glPopMatrix(); + + glPopMatrix(); + } + \endcode + Note the use of nested \c glPushMatrix() and \c glPopMatrix() blocks to represent the frame hierarchy: \c + leftArm and \c rightArm are both correctly drawn with respect to the \c body coordinate system. + + This matrix only represents the local Frame transformation (i.e. with respect to the + referenceFrame()). Use worldMatrix() to get the full Frame transformation matrix (i.e. from the + world to the Frame coordinate system). These two match when the referenceFrame() is \c NULL. + + The result is only valid until the next call to matrix(), getMatrix(), worldMatrix() or + getWorldMatrix(). Use it immediately (as above) or use getMatrix() instead. + + \attention The OpenGL format of the result is the transpose of the actual mathematical European + representation (translation is on the last \e line instead of the last \e column). + + \note The scaling factor of the 4x4 matrix is 1.0. */ +const GLdouble* Frame::matrix() const +{ + static GLdouble m[4][4]; + getMatrix(m); + return (const GLdouble*)(m); +} + +/*! \c GLdouble[4][4] version of matrix(). See also getWorldMatrix() and matrix(). */ +void Frame::getMatrix(GLdouble m[4][4]) const +{ + q_.getMatrix(m); + + m[3][0] = t_[0]; + m[3][1] = t_[1]; + m[3][2] = t_[2]; +} + +/*! \c GLdouble[16] version of matrix(). See also getWorldMatrix() and matrix(). */ +void Frame::getMatrix(GLdouble m[16]) const +{ + q_.getMatrix(m); + + m[12] = t_[0]; + m[13] = t_[1]; + m[14] = t_[2]; +} + +/*! Returns a Frame representing the inverse of the Frame space transformation. + + The rotation() of the new Frame is the Quaternion::inverse() of the original rotation. + Its translation() is the negated inverse rotated image of the original translation. + + If a Frame is considered as a space rigid transformation (translation and rotation), the inverse() + Frame performs the inverse transformation. + + Only the local Frame transformation (i.e. defined with respect to the referenceFrame()) is inverted. + Use worldInverse() for a global inverse. + + The resulting Frame has the same referenceFrame() as the Frame and a \c NULL constraint(). + + \note The scaling factor of the 4x4 matrix is 1.0. */ +Frame Frame::inverse() const +{ + Frame fr(-(q_.inverseRotate(t_)), q_.inverse()); + fr.setReferenceFrame(referenceFrame()); + return fr; +} + +/*! Returns the 4x4 OpenGL transformation matrix represented by the Frame. + + This method should be used in conjunction with \c glMultMatrixd() to modify + the OpenGL modelview matrix from a Frame: + \code + // The modelview here corresponds to the world coordinate system. + Frame fr(pos, Quaternion(from, to)); + glPushMatrix(); + glMultMatrixd(fr.worldMatrix()); + // draw object in the fr coordinate system. + glPopMatrix(); + \endcode + + This matrix represents the global Frame transformation: the entire referenceFrame() hierarchy is + taken into account to define the Frame transformation from the world coordinate system. Use + matrix() to get the local Frame transformation matrix (i.e. defined with respect to the + referenceFrame()). These two match when the referenceFrame() is \c NULL. + + The OpenGL format of the result is the transpose of the actual mathematical European + representation (translation is on the last \e line instead of the last \e column). + + \attention The result is only valid until the next call to matrix(), getMatrix(), worldMatrix() or + getWorldMatrix(). Use it immediately (as above) or use getWorldMatrix() instead. + + \note The scaling factor of the 4x4 matrix is 1.0. */ +const GLdouble* Frame::worldMatrix() const +{ + // This test is done for efficiency reasons (creates lots of temp objects otherwise). + if (referenceFrame()) + { + static Frame fr; + fr.setTranslation(position()); + fr.setRotation(orientation()); + return fr.matrix(); + } + else + return matrix(); +} + +/*! qreal[4][4] parameter version of worldMatrix(). See also getMatrix() and matrix(). */ +void Frame::getWorldMatrix(GLdouble m[4][4]) const +{ + const GLdouble* mat = worldMatrix(); + for (int i=0; i<4; ++i) + for (int j=0; j<4; ++j) + m[i][j] = mat[i*4+j]; +} + +/*! qreal[16] parameter version of worldMatrix(). See also getMatrix() and matrix(). */ +void Frame::getWorldMatrix(GLdouble m[16]) const +{ + const GLdouble* mat = worldMatrix(); + for (int i=0; i<16; ++i) + m[i] = mat[i]; +} + +/*! This is an overloaded method provided for convenience. Same as setFromMatrix(). */ +void Frame::setFromMatrix(const GLdouble m[4][4]) +{ + if (fabs(m[3][3]) < 1E-8) + { + qWarning("Frame::setFromMatrix: Null homogeneous coefficient"); + return; + } + + qreal rot[3][3]; + for (int i=0; i<3; ++i) + { + t_[i] = m[3][i] / m[3][3]; + for (int j=0; j<3; ++j) + // Beware of the transposition (OpenGL to European math) + rot[i][j] = m[j][i] / m[3][3]; + } + q_.setFromRotationMatrix(rot); + Q_EMIT modified(); +} + +/*! Sets the Frame from an OpenGL matrix representation (rotation in the upper left 3x3 matrix and + translation on the last line). + + Hence, if a code fragment looks like: + \code + GLdouble m[16]={...}; + glMultMatrixd(m); + \endcode + It is equivalent to write: + \code + Frame fr; + fr.setFromMatrix(m); + glMultMatrixd(fr.matrix()); + \endcode + + Using this conversion, you can benefit from the powerful Frame transformation methods to translate + points and vectors to and from the Frame coordinate system to any other Frame coordinate system + (including the world coordinate system). See coordinatesOf() and transformOf(). + + Emits the modified() signal. See also matrix(), getMatrix() and + Quaternion::setFromRotationMatrix(). + + \attention A Frame does not contain a scale factor. The possible scaling in \p m will not be + converted into the Frame by this method. */ +void Frame::setFromMatrix(const GLdouble m[16]) +{ + GLdouble mat[4][4]; + for (int i=0; i<4; ++i) + for (int j=0; j<4; ++j) + mat[i][j] = m[i*4+j]; + setFromMatrix(mat); +} + +//////////////////// SET AND GET LOCAL TRANSLATION AND ROTATION /////////////////////////////// + + +/*! Same as setTranslation(), but with \p qreal parameters. */ +void Frame::setTranslation(qreal x, qreal y, qreal z) +{ + setTranslation(Vec(x, y, z)); +} + +/*! Fill \c x, \c y and \c z with the translation() of the Frame. */ +void Frame::getTranslation(qreal& x, qreal& y, qreal& z) const +{ + const Vec t = translation(); + x = t[0]; + y = t[1]; + z = t[2]; +} + +/*! Same as setRotation() but with \c qreal Quaternion parameters. */ +void Frame::setRotation(qreal q0, qreal q1, qreal q2, qreal q3) +{ + setRotation(Quaternion(q0, q1, q2, q3)); +} + +/*! The \p q are set to the rotation() of the Frame. + +See Quaternion::Quaternion(qreal, qreal, qreal, qreal) for details on \c q. */ +void Frame::getRotation(qreal& q0, qreal& q1, qreal& q2, qreal& q3) const +{ + const Quaternion q = rotation(); + q0 = q[0]; + q1 = q[1]; + q2 = q[2]; + q3 = q[3]; +} + +//////////////////////////////////////////////////////////////////////////////// + +/*! Translates the Frame of \p t (defined in the Frame coordinate system). + + The translation actually applied to the Frame may differ from \p t since it can be filtered by the + constraint(). Use translate(Vec&) or setTranslationWithConstraint() to retrieve the filtered + translation value. Use setTranslation() to directly translate the Frame without taking the + constraint() into account. + + See also rotate(const Quaternion&). Emits the modified() signal. */ +void Frame::translate(const Vec& t) +{ + Vec tbis = t; + translate(tbis); +} + +/*! Same as translate(const Vec&) but \p t may be modified to satisfy the translation constraint(). + Its new value corresponds to the translation that has actually been applied to the Frame. */ +void Frame::translate(Vec& t) +{ + if (constraint()) + constraint()->constrainTranslation(t, this); + t_ += t; + Q_EMIT modified(); +} + +/*! Same as translate(const Vec&) but with \c qreal parameters. */ +void Frame::translate(qreal x, qreal y, qreal z) +{ + Vec t(x,y,z); + translate(t); +} + +/*! Same as translate(Vec&) but with \c qreal parameters. */ +void Frame::translate(qreal& x, qreal& y, qreal& z) +{ + Vec t(x,y,z); + translate(t); + x = t[0]; + y = t[1]; + z = t[2]; +} + +/*! Rotates the Frame by \p q (defined in the Frame coordinate system): R = R*q. + + The rotation actually applied to the Frame may differ from \p q since it can be filtered by the + constraint(). Use rotate(Quaternion&) or setRotationWithConstraint() to retrieve the filtered + rotation value. Use setRotation() to directly rotate the Frame without taking the constraint() + into account. + + See also translate(const Vec&). Emits the modified() signal. */ +void Frame::rotate(const Quaternion& q) +{ + Quaternion qbis = q; + rotate(qbis); +} + +/*! Same as rotate(const Quaternion&) but \p q may be modified to satisfy the rotation constraint(). + Its new value corresponds to the rotation that has actually been applied to the Frame. */ +void Frame::rotate(Quaternion& q) +{ + if (constraint()) + constraint()->constrainRotation(q, this); + q_ *= q; + q_.normalize(); // Prevents numerical drift + Q_EMIT modified(); +} + +/*! Same as rotate(Quaternion&) but with \c qreal Quaternion parameters. */ +void Frame::rotate(qreal& q0, qreal& q1, qreal& q2, qreal& q3) +{ + Quaternion q(q0,q1,q2,q3); + rotate(q); + q0 = q[0]; + q1 = q[1]; + q2 = q[2]; + q3 = q[3]; +} + +/*! Same as rotate(const Quaternion&) but with \c qreal Quaternion parameters. */ +void Frame::rotate(qreal q0, qreal q1, qreal q2, qreal q3) +{ + Quaternion q(q0,q1,q2,q3); + rotate(q); +} + +/*! Makes the Frame rotate() by \p rotation around \p point. + + \p point is defined in the world coordinate system, while the \p rotation axis is defined in the + Frame coordinate system. + + If the Frame has a constraint(), \p rotation is first constrained using + Constraint::constrainRotation(). The translation which results from the filtered rotation around + \p point is then computed and filtered using Constraint::constrainTranslation(). The new \p + rotation value corresponds to the rotation that has actually been applied to the Frame. + + Emits the modified() signal. */ +void Frame::rotateAroundPoint(Quaternion& rotation, const Vec& point) +{ + if (constraint()) + constraint()->constrainRotation(rotation, this); + q_ *= rotation; + q_.normalize(); // Prevents numerical drift + Vec trans = point + Quaternion(inverseTransformOf(rotation.axis()), rotation.angle()).rotate(position()-point) - t_; + if (constraint()) + constraint()->constrainTranslation(trans, this); + t_ += trans; + Q_EMIT modified(); +} + +/*! Same as rotateAroundPoint(), but with a \c const \p rotation Quaternion. Note that the actual + rotation may differ since it can be filtered by the constraint(). */ +void Frame::rotateAroundPoint(const Quaternion& rotation, const Vec& point) +{ + Quaternion rot = rotation; + rotateAroundPoint(rot, point); +} + +//////////////////// SET AND GET WORLD POSITION AND ORIENTATION /////////////////////////////// + +/*! Sets the position() of the Frame, defined in the world coordinate system. Emits the modified() + signal. + +Use setTranslation() to define the \e local frame translation (with respect to the +referenceFrame()). The potential constraint() of the Frame is not taken into account, use +setPositionWithConstraint() instead. */ +void Frame::setPosition(const Vec& position) +{ + if (referenceFrame()) + setTranslation(referenceFrame()->coordinatesOf(position)); + else + setTranslation(position); +} + +/*! Same as setPosition(), but with \c qreal parameters. */ +void Frame::setPosition(qreal x, qreal y, qreal z) +{ + setPosition(Vec(x, y, z)); +} + +/*! Same as successive calls to setPosition() and then setOrientation(). + +Only one modified() signal is emitted, which is convenient if this signal is connected to a +QGLViewer::update() slot. See also setTranslationAndRotation() and +setPositionAndOrientationWithConstraint(). */ +void Frame::setPositionAndOrientation(const Vec& position, const Quaternion& orientation) +{ + if (referenceFrame()) + { + t_ = referenceFrame()->coordinatesOf(position); + q_ = referenceFrame()->orientation().inverse() * orientation; + } + else + { + t_ = position; + q_ = orientation; + } + Q_EMIT modified(); +} + + +/*! Same as successive calls to setTranslation() and then setRotation(). + +Only one modified() signal is emitted, which is convenient if this signal is connected to a +QGLViewer::update() slot. See also setPositionAndOrientation() and +setTranslationAndRotationWithConstraint(). */ +void Frame::setTranslationAndRotation(const Vec& translation, const Quaternion& rotation) +{ + t_ = translation; + q_ = rotation; + Q_EMIT modified(); +} + + +/*! \p x, \p y and \p z are set to the position() of the Frame. */ +void Frame::getPosition(qreal& x, qreal& y, qreal& z) const +{ + Vec p = position(); + x = p.x; + y = p.y; + z = p.z; +} + +/*! Sets the orientation() of the Frame, defined in the world coordinate system. Emits the modified() signal. + +Use setRotation() to define the \e local frame rotation (with respect to the referenceFrame()). The +potential constraint() of the Frame is not taken into account, use setOrientationWithConstraint() +instead. */ +void Frame::setOrientation(const Quaternion& orientation) +{ + if (referenceFrame()) + setRotation(referenceFrame()->orientation().inverse() * orientation); + else + setRotation(orientation); +} + +/*! Same as setOrientation(), but with \c qreal parameters. */ +void Frame::setOrientation(qreal q0, qreal q1, qreal q2, qreal q3) +{ + setOrientation(Quaternion(q0, q1, q2, q3)); +} + +/*! Get the current orientation of the frame (same as orientation()). + Parameters are the orientation Quaternion values. + See also setOrientation(). */ + +/*! The \p q are set to the orientation() of the Frame. + +See Quaternion::Quaternion(qreal, qreal, qreal, qreal) for details on \c q. */ +void Frame::getOrientation(qreal& q0, qreal& q1, qreal& q2, qreal& q3) const +{ + Quaternion o = orientation(); + q0 = o[0]; + q1 = o[1]; + q2 = o[2]; + q3 = o[3]; +} + +/*! Returns the position of the Frame, defined in the world coordinate system. See also + orientation(), setPosition() and translation(). */ +Vec Frame::position() const { + if (referenceFrame_) + return inverseCoordinatesOf(Vec(0.0,0.0,0.0)); + else + return t_; +} + +/*! Returns the orientation of the Frame, defined in the world coordinate system. See also + position(), setOrientation() and rotation(). */ +Quaternion Frame::orientation() const +{ + Quaternion res = rotation(); + const Frame* fr = referenceFrame(); + while (fr != NULL) + { + res = fr->rotation() * res; + fr = fr->referenceFrame(); + } + return res; +} + + +////////////////////// C o n s t r a i n t V e r s i o n s ////////////////////////// + +/*! Same as setTranslation(), but \p translation is modified so that the potential constraint() of the + Frame is satisfied. + + Emits the modified() signal. See also setRotationWithConstraint() and setPositionWithConstraint(). */ +void Frame::setTranslationWithConstraint(Vec& translation) +{ + Vec deltaT = translation - this->translation(); + if (constraint()) + constraint()->constrainTranslation(deltaT, this); + + setTranslation(this->translation() + deltaT); + translation = this->translation(); +} + +/*! Same as setRotation(), but \p rotation is modified so that the potential constraint() of the + Frame is satisfied. + + Emits the modified() signal. See also setTranslationWithConstraint() and setOrientationWithConstraint(). */ +void Frame::setRotationWithConstraint(Quaternion& rotation) +{ + Quaternion deltaQ = this->rotation().inverse() * rotation; + if (constraint()) + constraint()->constrainRotation(deltaQ, this); + + // Prevent numerical drift + deltaQ.normalize(); + + setRotation(this->rotation() * deltaQ); + q_.normalize(); + rotation = this->rotation(); +} + +/*! Same as setTranslationAndRotation(), but \p translation and \p orientation are modified to + satisfy the constraint(). Emits the modified() signal. */ +void Frame::setTranslationAndRotationWithConstraint(Vec& translation, Quaternion& rotation) +{ + Vec deltaT = translation - this->translation(); + Quaternion deltaQ = this->rotation().inverse() * rotation; + + if (constraint()) + { + constraint()->constrainTranslation(deltaT, this); + constraint()->constrainRotation(deltaQ, this); + } + + // Prevent numerical drift + deltaQ.normalize(); + + t_ += deltaT; + q_ *= deltaQ; + q_.normalize(); + + translation = this->translation(); + rotation = this->rotation(); + + Q_EMIT modified(); +} + +/*! Same as setPosition(), but \p position is modified so that the potential constraint() of the + Frame is satisfied. See also setOrientationWithConstraint() and setTranslationWithConstraint(). */ +void Frame::setPositionWithConstraint(Vec& position) +{ + if (referenceFrame()) + position = referenceFrame()->coordinatesOf(position); + + setTranslationWithConstraint(position); +} + +/*! Same as setOrientation(), but \p orientation is modified so that the potential constraint() of the Frame + is satisfied. See also setPositionWithConstraint() and setRotationWithConstraint(). */ +void Frame::setOrientationWithConstraint(Quaternion& orientation) +{ + if (referenceFrame()) + orientation = referenceFrame()->orientation().inverse() * orientation; + + setRotationWithConstraint(orientation); +} + +/*! Same as setPositionAndOrientation() but \p position and \p orientation are modified to satisfy +the constraint. Emits the modified() signal. */ +void Frame::setPositionAndOrientationWithConstraint(Vec& position, Quaternion& orientation) +{ + if (referenceFrame()) + { + position = referenceFrame()->coordinatesOf(position); + orientation = referenceFrame()->orientation().inverse() * orientation; + } + setTranslationAndRotationWithConstraint(position, orientation); +} + + +///////////////////////////// REFERENCE FRAMES /////////////////////////////////////// + +/*! Sets the referenceFrame() of the Frame. + +The Frame translation() and rotation() are then defined in the referenceFrame() coordinate system. +Use position() and orientation() to express these in the world coordinate system. + +Emits the modified() signal if \p refFrame differs from the current referenceFrame(). + +Using this method, you can create a hierarchy of Frames. This hierarchy needs to be a tree, which +root is the world coordinate system (i.e. a \c NULL referenceFrame()). A warning is printed and no +action is performed if setting \p refFrame as the referenceFrame() would create a loop in the Frame +hierarchy (see settingAsReferenceFrameWillCreateALoop()). */ +void Frame::setReferenceFrame(const Frame* const refFrame) +{ + if (settingAsReferenceFrameWillCreateALoop(refFrame)) + qWarning("Frame::setReferenceFrame would create a loop in Frame hierarchy"); + else + { + bool identical = (referenceFrame_ == refFrame); + referenceFrame_ = refFrame; + if (!identical) + Q_EMIT modified(); + } +} + +/*! Returns \c true if setting \p frame as the Frame's referenceFrame() would create a loop in the + Frame hierarchy. */ +bool Frame::settingAsReferenceFrameWillCreateALoop(const Frame* const frame) +{ + const Frame* f = frame; + while (f != NULL) + { + if (f == this) + return true; + f = f->referenceFrame(); + } + return false; +} + +///////////////////////// FRAME TRANSFORMATIONS OF 3D POINTS ////////////////////////////// + +/*! Returns the Frame coordinates of a point \p src defined in the world coordinate system (converts + from world to Frame). + + inverseCoordinatesOf() performs the inverse convertion. transformOf() converts 3D vectors instead + of 3D coordinates. + + See the frameTransform example for an + illustration. */ +Vec Frame::coordinatesOf(const Vec& src) const +{ + if (referenceFrame()) + return localCoordinatesOf(referenceFrame()->coordinatesOf(src)); + else + return localCoordinatesOf(src); +} + +/*! Returns the world coordinates of the point whose position in the Frame coordinate system is \p + src (converts from Frame to world). + + coordinatesOf() performs the inverse convertion. Use inverseTransformOf() to transform 3D vectors + instead of 3D coordinates. */ +Vec Frame::inverseCoordinatesOf(const Vec& src) const +{ + const Frame* fr = this; + Vec res = src; + while (fr != NULL) + { + res = fr->localInverseCoordinatesOf(res); + fr = fr->referenceFrame(); + } + return res; +} + +/*! Returns the Frame coordinates of a point \p src defined in the referenceFrame() coordinate + system (converts from referenceFrame() to Frame). + + localInverseCoordinatesOf() performs the inverse convertion. See also localTransformOf(). */ +Vec Frame::localCoordinatesOf(const Vec& src) const +{ + return rotation().inverseRotate(src - translation()); +} + +/*! Returns the referenceFrame() coordinates of a point \p src defined in the Frame coordinate + system (converts from Frame to referenceFrame()). + + localCoordinatesOf() performs the inverse convertion. See also localInverseTransformOf(). */ +Vec Frame::localInverseCoordinatesOf(const Vec& src) const +{ + return rotation().rotate(src) + translation(); +} + +/*! Returns the Frame coordinates of the point whose position in the \p from coordinate system is \p + src (converts from \p from to Frame). + + coordinatesOfIn() performs the inverse transformation. */ +Vec Frame::coordinatesOfFrom(const Vec& src, const Frame* const from) const +{ + if (this == from) + return src; + else + if (referenceFrame()) + return localCoordinatesOf(referenceFrame()->coordinatesOfFrom(src, from)); + else + return localCoordinatesOf(from->inverseCoordinatesOf(src)); +} + +/*! Returns the \p in coordinates of the point whose position in the Frame coordinate system is \p + src (converts from Frame to \p in). + + coordinatesOfFrom() performs the inverse transformation. */ +Vec Frame::coordinatesOfIn(const Vec& src, const Frame* const in) const +{ + const Frame* fr = this; + Vec res = src; + while ((fr != NULL) && (fr != in)) + { + res = fr->localInverseCoordinatesOf(res); + fr = fr->referenceFrame(); + } + + if (fr != in) + // in was not found in the branch of this, res is now expressed in the world + // coordinate system. Simply convert to in coordinate system. + res = in->coordinatesOf(res); + + return res; +} + +////// qreal[3] versions + +/*! Same as coordinatesOf(), but with \c qreal parameters. */ +void Frame::getCoordinatesOf(const qreal src[3], qreal res[3]) const +{ + const Vec r = coordinatesOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as inverseCoordinatesOf(), but with \c qreal parameters. */ +void Frame::getInverseCoordinatesOf(const qreal src[3], qreal res[3]) const +{ + const Vec r = inverseCoordinatesOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as localCoordinatesOf(), but with \c qreal parameters. */ +void Frame::getLocalCoordinatesOf(const qreal src[3], qreal res[3]) const +{ + const Vec r = localCoordinatesOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as localInverseCoordinatesOf(), but with \c qreal parameters. */ +void Frame::getLocalInverseCoordinatesOf(const qreal src[3], qreal res[3]) const +{ + const Vec r = localInverseCoordinatesOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as coordinatesOfIn(), but with \c qreal parameters. */ +void Frame::getCoordinatesOfIn(const qreal src[3], qreal res[3], const Frame* const in) const +{ + const Vec r = coordinatesOfIn(Vec(src), in); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as coordinatesOfFrom(), but with \c qreal parameters. */ +void Frame::getCoordinatesOfFrom(const qreal src[3], qreal res[3], const Frame* const from) const +{ + const Vec r = coordinatesOfFrom(Vec(src), from); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + + +///////////////////////// FRAME TRANSFORMATIONS OF VECTORS ////////////////////////////// + +/*! Returns the Frame transform of a vector \p src defined in the world coordinate system (converts + vectors from world to Frame). + + inverseTransformOf() performs the inverse transformation. coordinatesOf() converts 3D coordinates + instead of 3D vectors (here only the rotational part of the transformation is taken into account). + + See the frameTransform example for an + illustration. */ +Vec Frame::transformOf(const Vec& src) const +{ + if (referenceFrame()) + return localTransformOf(referenceFrame()->transformOf(src)); + else + return localTransformOf(src); +} + +/*! Returns the world transform of the vector whose coordinates in the Frame coordinate + system is \p src (converts vectors from Frame to world). + + transformOf() performs the inverse transformation. Use inverseCoordinatesOf() to transform 3D + coordinates instead of 3D vectors. */ +Vec Frame::inverseTransformOf(const Vec& src) const +{ + const Frame* fr = this; + Vec res = src; + while (fr != NULL) + { + res = fr->localInverseTransformOf(res); + fr = fr->referenceFrame(); + } + return res; +} + +/*! Returns the Frame transform of a vector \p src defined in the referenceFrame() coordinate system + (converts vectors from referenceFrame() to Frame). + + localInverseTransformOf() performs the inverse transformation. See also localCoordinatesOf(). */ +Vec Frame::localTransformOf(const Vec& src) const +{ + return rotation().inverseRotate(src); +} + +/*! Returns the referenceFrame() transform of a vector \p src defined in the Frame coordinate + system (converts vectors from Frame to referenceFrame()). + + localTransformOf() performs the inverse transformation. See also localInverseCoordinatesOf(). */ +Vec Frame::localInverseTransformOf(const Vec& src) const +{ + return rotation().rotate(src); +} + +/*! Returns the Frame transform of the vector whose coordinates in the \p from coordinate system is \p + src (converts vectors from \p from to Frame). + + transformOfIn() performs the inverse transformation. */ +Vec Frame::transformOfFrom(const Vec& src, const Frame* const from) const +{ + if (this == from) + return src; + else + if (referenceFrame()) + return localTransformOf(referenceFrame()->transformOfFrom(src, from)); + else + return localTransformOf(from->inverseTransformOf(src)); +} + +/*! Returns the \p in transform of the vector whose coordinates in the Frame coordinate system is \p + src (converts vectors from Frame to \p in). + + transformOfFrom() performs the inverse transformation. */ +Vec Frame::transformOfIn(const Vec& src, const Frame* const in) const +{ + const Frame* fr = this; + Vec res = src; + while ((fr != NULL) && (fr != in)) + { + res = fr->localInverseTransformOf(res); + fr = fr->referenceFrame(); + } + + if (fr != in) + // in was not found in the branch of this, res is now expressed in the world + // coordinate system. Simply convert to in coordinate system. + res = in->transformOf(res); + + return res; +} + +///////////////// qreal[3] versions ////////////////////// + +/*! Same as transformOf(), but with \c qreal parameters. */ +void Frame::getTransformOf(const qreal src[3], qreal res[3]) const +{ + Vec r = transformOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as inverseTransformOf(), but with \c qreal parameters. */ +void Frame::getInverseTransformOf(const qreal src[3], qreal res[3]) const +{ + Vec r = inverseTransformOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as localTransformOf(), but with \c qreal parameters. */ +void Frame::getLocalTransformOf(const qreal src[3], qreal res[3]) const +{ + Vec r = localTransformOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as localInverseTransformOf(), but with \c qreal parameters. */ +void Frame::getLocalInverseTransformOf(const qreal src[3], qreal res[3]) const +{ + Vec r = localInverseTransformOf(Vec(src)); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as transformOfIn(), but with \c qreal parameters. */ +void Frame::getTransformOfIn(const qreal src[3], qreal res[3], const Frame* const in) const +{ + Vec r = transformOfIn(Vec(src), in); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +/*! Same as transformOfFrom(), but with \c qreal parameters. */ +void Frame::getTransformOfFrom(const qreal src[3], qreal res[3], const Frame* const from) const +{ + Vec r = transformOfFrom(Vec(src), from); + for (int i=0; i<3 ; ++i) + res[i] = r[i]; +} + +//////////////////////////// STATE ////////////////////////////// + +/*! Returns an XML \c QDomElement that represents the Frame. + + \p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create + QDomElement. + + The resulting QDomElement looks like: + \code + + + + + \endcode + + Use initFromDOMElement() to restore the Frame state from the resulting \c QDomElement. + + See Vec::domElement() for a complete example. See also Quaternion::domElement(), + Camera::domElement()... + + \attention The constraint() and referenceFrame() are not saved in the QDomElement. */ +QDomElement Frame::domElement(const QString& name, QDomDocument& document) const +{ + // TODO: use translation and rotation instead when referenceFrame is coded... + QDomElement e = document.createElement(name); + e.appendChild(position().domElement("position", document)); + e.appendChild(orientation().domElement("orientation", document)); + return e; +} + +/*! Restores the Frame state from a \c QDomElement created by domElement(). + + See domElement() for the \c QDomElement syntax. See the Vec::initFromDOMElement() and + Quaternion::initFromDOMElement() documentations for details on default values if an argument is + missing. + + \attention The constraint() and referenceFrame() are not restored by this method and are left + unchanged. */ +void Frame::initFromDOMElement(const QDomElement& element) +{ + // TODO: use translation and rotation instead when referenceFrame is coded... + + // Reset default values. Attention: destroys constraint. + // *this = Frame(); + // This instead ? Better : what is not set is not changed. + // setPositionAndOrientation(Vec(), Quaternion()); + + QDomElement child=element.firstChild().toElement(); + while (!child.isNull()) + { + if (child.tagName() == "position") + setPosition(Vec(child)); + if (child.tagName() == "orientation") + setOrientation(Quaternion(child).normalized()); + + child = child.nextSibling().toElement(); + } +} + +///////////////////////////////// ALIGN ///////////////////////////////// + +/*! Aligns the Frame with \p frame, so that two of their axis are parallel. + +If one of the X, Y and Z axis of the Frame is almost parallel to any of the X, Y, or Z axis of \p +frame, the Frame is rotated so that these two axis actually become parallel. + +If, after this first rotation, two other axis are also almost parallel, a second alignment is +performed. The two frames then have identical orientations, up to 90 degrees rotations. + +\p threshold measures how close two axis must be to be considered parallel. It is compared with the +absolute values of the dot product of the normalized axis. As a result, useful range is sqrt(2)/2 +(systematic alignment) to 1 (no alignment). + +When \p move is set to \c true, the Frame position() is also affected by the alignment. The new +Frame's position() is such that the \p frame position (computed with coordinatesOf(), in the Frame +coordinates system) does not change. + +\p frame may be \c NULL and then represents the world coordinate system (same convention than for +the referenceFrame()). + +The rotation (and translation when \p move is \c true) applied to the Frame are filtered by the +possible constraint(). */ +void Frame::alignWithFrame(const Frame* const frame, bool move, qreal threshold) +{ + Vec directions[2][3]; + for (unsigned short d=0; d<3; ++d) + { + Vec dir((d==0)? 1.0 : 0.0, (d==1)? 1.0 : 0.0, (d==2)? 1.0 : 0.0); + if (frame) + directions[0][d] = frame->inverseTransformOf(dir); + else + directions[0][d] = dir; + directions[1][d] = inverseTransformOf(dir); + } + + qreal maxProj = 0.0; + qreal proj; + unsigned short index[2]; + index[0] = index[1] = 0; + for (unsigned short i=0; i<3; ++i) + for (unsigned short j=0; j<3; ++j) + if ( (proj=fabs(directions[0][i]*directions[1][j])) >= maxProj ) + { + index[0] = i; + index[1] = j; + maxProj = proj; + } + + Frame old; + old=*this; + + qreal coef = directions[0][index[0]] * directions[1][index[1]]; + if (fabs(coef) >= threshold) + { + const Vec axis = cross(directions[0][index[0]], directions[1][index[1]]); + qreal angle = asin(axis.norm()); + if (coef >= 0.0) + angle = -angle; + rotate(rotation().inverse() * Quaternion(axis, angle) * orientation()); + + // Try to align an other axis direction + unsigned short d = (index[1]+1) % 3; + Vec dir((d==0)? 1.0 : 0.0, (d==1)? 1.0 : 0.0, (d==2)? 1.0 : 0.0); + dir = inverseTransformOf(dir); + + qreal max = 0.0; + for (unsigned short i=0; i<3; ++i) + { + qreal proj = fabs(directions[0][i]*dir); + if (proj > max) + { + index[0] = i; + max = proj; + } + } + + if (max >= threshold) + { + const Vec axis = cross(directions[0][index[0]], dir); + qreal angle = asin(axis.norm()); + if (directions[0][index[0]] * dir >= 0.0) + angle = -angle; + rotate(rotation().inverse() * Quaternion(axis, angle) * orientation()); + } + } + + if (move) + { + Vec center; + if (frame) + center = frame->position(); + + translate(center - orientation().rotate(old.coordinatesOf(center)) - translation()); + } +} + +/*! Translates the Frame so that its position() lies on the line defined by \p origin and \p + direction (defined in the world coordinate system). + +Simply uses an orthogonal projection. \p direction does not need to be normalized. */ +void Frame::projectOnLine(const Vec& origin, const Vec& direction) +{ + // If you are trying to find a bug here, because of memory problems, you waste your time. + // This is a bug in the gcc 3.3 compiler. Compile the library in debug mode and test. + // Uncommenting this line also seems to solve the problem. Horrible. + // cout << "position = " << position() << endl; + // If you found a problem or are using a different compiler, please let me know. + const Vec shift = origin - position(); + Vec proj = shift; + proj.projectOnAxis(direction); + translate(shift-proj); +} diff --git a/QGLViewer/frame.h b/QGLViewer/frame.h new file mode 100644 index 0000000..1513283 --- /dev/null +++ b/QGLViewer/frame.h @@ -0,0 +1,415 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_FRAME_H +#define QGLVIEWER_FRAME_H + +#include +#include + +#include "constraint.h" +// #include "GL/gl.h" is now included in config.h for ease of configuration + +namespace qglviewer { +/*! \brief The Frame class represents a coordinate system, defined by a position and an + orientation. \class Frame frame.h QGLViewer/frame.h + + A Frame is a 3D coordinate system, represented by a position() and an orientation(). The order of + these transformations is important: the Frame is first translated \e and \e then rotated around + the new translated origin. + + A Frame is useful to define the position and orientation of a 3D rigid object, using its matrix() + method, as shown below: + \code + // Builds a Frame at position (0.5,0,0) and oriented such that its Y axis is along the (1,1,1) + // direction. One could also have used setPosition() and setOrientation(). + Frame fr(Vec(0.5,0,0), Quaternion(Vec(0,1,0), Vec(1,1,1))); + glPushMatrix(); + glMultMatrixd(fr.matrix()); + // Draw your object here, in the local fr coordinate system. + glPopMatrix(); + \endcode + + Many functions are provided to transform a 3D point from one coordinate system (Frame) to an + other: see coordinatesOf(), inverseCoordinatesOf(), coordinatesOfIn(), coordinatesOfFrom()... + + You may also want to transform a 3D vector (such as a normal), which corresponds to applying only + the rotational part of the frame transformation: see transformOf() and inverseTransformOf(). See + the frameTransform example for an illustration. + + The translation() and the rotation() that are encapsulated in a Frame can also be used to + represent a \e rigid \e transformation of space. Such a transformation can also be interpreted as + a change of coordinate system, and the coordinate system conversion functions actually allow you + to use a Frame as a rigid transformation. Use inverseCoordinatesOf() (resp. coordinatesOf()) to + apply the transformation (resp. its inverse). Note the inversion. + +

Hierarchy of Frames

+ + The position and the orientation of a Frame are actually defined with respect to a + referenceFrame(). The default referenceFrame() is the world coordinate system (represented by a \c + NULL referenceFrame()). If you setReferenceFrame() to a different Frame, you must then + differentiate: + + \arg the \e local translation() and rotation(), defined with respect to the referenceFrame(), + + \arg the \e global position() and orientation(), always defined with respect to the world + coordinate system. + + A Frame is actually defined by its translation() with respect to its referenceFrame(), and then by + a rotation() of the coordinate system around the new translated origin. + + This terminology for \e local (translation() and rotation()) and \e global (position() and + orientation()) definitions is used in all the methods' names and should be sufficient to prevent + ambiguities. These notions are obviously identical when the referenceFrame() is \c NULL, i.e. when + the Frame is defined in the world coordinate system (the one you are in at the beginning of the + QGLViewer::draw() method, see the introduction page). + + Frames can hence easily be organized in a tree hierarchy, which root is the world coordinate + system. A loop in the hierarchy would result in an inconsistent (multiple) Frame definition. + settingAsReferenceFrameWillCreateALoop() checks this and prevents setReferenceFrame() from + creating such a loop. + + This frame hierarchy is used in methods like coordinatesOfIn(), coordinatesOfFrom()... which allow + coordinates (or vector) conversions from a Frame to any other one (including the world coordinate + system). + + However, one must note that this hierarchical representation is internal to the Frame classes. + When the Frames represent OpenGL coordinates system, one should map this hierarchical + representation to the OpenGL GL_MODELVIEW matrix stack. See the matrix() documentation for + details. + +

Constraints

+ + An interesting feature of Frames is that their displacements can be constrained. When a Constraint + is attached to a Frame, it filters the input of translate() and rotate(), and only the resulting + filtered motion is applied to the Frame. The default constraint() is \c NULL resulting in no + filtering. Use setConstraint() to attach a Constraint to a frame. + + Constraints are especially usefull for the ManipulatedFrame instances, in order to forbid some + mouse motions. See the constrainedFrame, constrainedCamera and luxo examples for an illustration. + + Classical constraints are provided for convenience (see LocalConstraint, WorldConstraint and + CameraConstraint) and new constraints can very easily be implemented. + +

Derived classes

+ + The ManipulatedFrame class inherits Frame and implements a mouse motion convertion, so that a + Frame (and hence an object) can be manipulated in the scene with the mouse. + + \nosubgrouping */ +class QGLVIEWER_EXPORT Frame : public QObject +{ + Q_OBJECT + +public: + Frame(); + + /*! Virtual destructor. Empty. */ + virtual ~Frame() {} + + Frame(const Frame& frame); + Frame& operator=(const Frame& frame); + +Q_SIGNALS: + /*! This signal is emitted whenever the position() or the orientation() of the Frame is modified. + + Connect this signal to any object that must be notified: + \code + QObject::connect(myFrame, SIGNAL(modified()), myObject, SLOT(update())); + \endcode + Use the QGLViewer::QGLViewerPool() to connect the signal to all the viewers. + + \note If your Frame is part of a Frame hierarchy (see referenceFrame()), a modification of one + of the parents of this Frame will \e not emit this signal. Use code like this to change this + behavior (you can do this recursively for all the referenceFrame() until the \c NULL world root + frame is encountered): + \code + // Emits the Frame modified() signal when its referenceFrame() is modified(). + connect(myFrame->referenceFrame(), SIGNAL(modified()), myFrame, SIGNAL(modified())); + \endcode + + \attention Connecting this signal to a QGLWidget::update() slot (or a method that calls it) [TODO Update with QOpenGLWidget] + will prevent you from modifying the Frame \e inside your QGLViewer::draw() method as it would + result in an infinite loop. However, QGLViewer::draw() should not modify the scene. + + \note Note that this signal might be emitted even if the Frame is not actually modified, for + instance after a translate(Vec(0,0,0)) or a setPosition(position()). */ + void modified(); + + /*! This signal is emitted when the Frame is interpolated by a KeyFrameInterpolator. + + See the KeyFrameInterpolator documentation for details. + + If a KeyFrameInterpolator is used to successively interpolate several Frames in your scene, + connect the KeyFrameInterpolator::interpolated() signal instead (identical, but independent of + the interpolated Frame). */ + void interpolated(); + +public: + /*! @name World coordinates position and orientation */ + //@{ + Frame(const Vec& position, const Quaternion& orientation); + + void setPosition(const Vec& position); + void setPosition(qreal x, qreal y, qreal z); + void setPositionWithConstraint(Vec& position); + + void setOrientation(const Quaternion& orientation); + void setOrientation(qreal q0, qreal q1, qreal q2, qreal q3); + void setOrientationWithConstraint(Quaternion& orientation); + + void setPositionAndOrientation(const Vec& position, const Quaternion& orientation); + void setPositionAndOrientationWithConstraint(Vec& position, Quaternion& orientation); + + Vec position() const; + Quaternion orientation() const; + + void getPosition(qreal& x, qreal& y, qreal& z) const; + void getOrientation(qreal& q0, qreal& q1, qreal& q2, qreal& q3) const; + //@} + + +public: + /*! @name Local translation and rotation w/r reference Frame */ + //@{ + /*! Sets the translation() of the frame, locally defined with respect to the referenceFrame(). + Emits the modified() signal. + + Use setPosition() to define the world coordinates position(). Use + setTranslationWithConstraint() to take into account the potential constraint() of the Frame. */ + void setTranslation(const Vec& translation) { t_ = translation; Q_EMIT modified(); } + void setTranslation(qreal x, qreal y, qreal z); + void setTranslationWithConstraint(Vec& translation); + + /*! Set the current rotation Quaternion. See rotation() and the different Quaternion + constructors. Emits the modified() signal. See also setTranslation() and + setRotationWithConstraint(). */ + + /*! Sets the rotation() of the Frame, locally defined with respect to the referenceFrame(). + Emits the modified() signal. + + Use setOrientation() to define the world coordinates orientation(). The potential + constraint() of the Frame is not taken into account, use setRotationWithConstraint() + instead. */ + void setRotation(const Quaternion& rotation) { q_ = rotation; Q_EMIT modified(); } + void setRotation(qreal q0, qreal q1, qreal q2, qreal q3); + void setRotationWithConstraint(Quaternion& rotation); + + void setTranslationAndRotation(const Vec& translation, const Quaternion& rotation); + void setTranslationAndRotationWithConstraint(Vec& translation, Quaternion& rotation); + + /*! Returns the Frame translation, defined with respect to the referenceFrame(). + + Use position() to get the result in the world coordinates. These two values are identical + when the referenceFrame() is \c NULL (default). + + See also setTranslation() and setTranslationWithConstraint(). */ + Vec translation() const { return t_; } + /*! Returns the Frame rotation, defined with respect to the referenceFrame(). + + Use orientation() to get the result in the world coordinates. These two values are identical + when the referenceFrame() is \c NULL (default). + + See also setRotation() and setRotationWithConstraint(). */ + + /*! Returns the current Quaternion orientation. See setRotation(). */ + Quaternion rotation() const { return q_; } + + void getTranslation(qreal& x, qreal& y, qreal& z) const; + void getRotation(qreal& q0, qreal& q1, qreal& q2, qreal& q3) const; + //@} + +public: + /*! @name Frame hierarchy */ + //@{ + /*! Returns the reference Frame, in which coordinates system the Frame is defined. + + The translation() and rotation() of the Frame are defined with respect to the referenceFrame() + coordinate system. A \c NULL referenceFrame() (default value) means that the Frame is defined in + the world coordinate system. + + Use position() and orientation() to recursively convert values along the referenceFrame() chain + and to get values expressed in the world coordinate system. The values match when the + referenceFrame() is \c NULL. + + Use setReferenceFrame() to set this value and create a Frame hierarchy. Convenient functions + allow you to convert 3D coordinates from one Frame to an other: see coordinatesOf(), + localCoordinatesOf(), coordinatesOfIn() and their inverse functions. + + Vectors can also be converted using transformOf(), transformOfIn, localTransformOf() and their + inverse functions. */ + const Frame* referenceFrame() const { return referenceFrame_; } + void setReferenceFrame(const Frame* const refFrame); + bool settingAsReferenceFrameWillCreateALoop(const Frame* const frame); + //@} + + + /*! @name Frame modification */ + //@{ + void translate(Vec& t); + void translate(const Vec& t); + // Some compilers complain about "overloading cannot distinguish from previous declaration" + // Simply comment out the following method and its associated implementation + void translate(qreal x, qreal y, qreal z); + void translate(qreal& x, qreal& y, qreal& z); + + void rotate(Quaternion& q); + void rotate(const Quaternion& q); + // Some compilers complain about "overloading cannot distinguish from previous declaration" + // Simply comment out the following method and its associated implementation + void rotate(qreal q0, qreal q1, qreal q2, qreal q3); + void rotate(qreal& q0, qreal& q1, qreal& q2, qreal& q3); + + void rotateAroundPoint(Quaternion& rotation, const Vec& point); + void rotateAroundPoint(const Quaternion& rotation, const Vec& point); + + void alignWithFrame(const Frame* const frame, bool move=false, qreal threshold=0.0); + void projectOnLine(const Vec& origin, const Vec& direction); + //@} + + + /*! @name Coordinate system transformation of 3D coordinates */ + //@{ + Vec coordinatesOf(const Vec& src) const; + Vec inverseCoordinatesOf(const Vec& src) const; + Vec localCoordinatesOf(const Vec& src) const; + Vec localInverseCoordinatesOf(const Vec& src) const; + Vec coordinatesOfIn(const Vec& src, const Frame* const in) const; + Vec coordinatesOfFrom(const Vec& src, const Frame* const from) const; + + void getCoordinatesOf(const qreal src[3], qreal res[3]) const; + void getInverseCoordinatesOf(const qreal src[3], qreal res[3]) const; + void getLocalCoordinatesOf(const qreal src[3], qreal res[3]) const; + void getLocalInverseCoordinatesOf(const qreal src[3], qreal res[3]) const; + void getCoordinatesOfIn(const qreal src[3], qreal res[3], const Frame* const in) const; + void getCoordinatesOfFrom(const qreal src[3], qreal res[3], const Frame* const from) const; + //@} + + /*! @name Coordinate system transformation of vectors */ + // A frame is as a new coordinate system, defined with respect to a reference frame (the world + // coordinate system by default, see the "Composition of frame" section). + + // The transformOf() (resp. inverseTransformOf()) functions transform a 3D vector from (resp. + // to) the world coordinates system. This section defines the 3D vector transformation + // functions. See the Coordinate system transformation of 3D points above for the transformation + // of 3D points. The difference between the two sets of functions is simple: for vectors, only + // the rotational part of the transformations is taken into account, while translation is also + // considered for 3D points. + + // The length of the resulting transformed vector is identical to the one of the source vector + // for all the described functions. + + // When local is prepended to the names of the functions, the functions simply transform from + // (and to) the reference frame. + + // When In (resp. From) is appended to the names, the functions transform from (resp. To) the + // frame that is given as an argument. The frame does not need to be in the same branch or the + // hierarchical tree, and can be \c NULL (the world coordinates system). + + // Combining any of these functions with its inverse (in any order) leads to the identity. + //@{ + Vec transformOf(const Vec& src) const; + Vec inverseTransformOf(const Vec& src) const; + Vec localTransformOf(const Vec& src) const; + Vec localInverseTransformOf(const Vec& src) const; + Vec transformOfIn(const Vec& src, const Frame* const in) const; + Vec transformOfFrom(const Vec& src, const Frame* const from) const; + + void getTransformOf(const qreal src[3], qreal res[3]) const; + void getInverseTransformOf(const qreal src[3], qreal res[3]) const; + void getLocalTransformOf(const qreal src[3], qreal res[3]) const; + void getLocalInverseTransformOf(const qreal src[3], qreal res[3]) const; + void getTransformOfIn(const qreal src[3], qreal res[3], const Frame* const in) const; + void getTransformOfFrom(const qreal src[3], qreal res[3], const Frame* const from) const; + //@} + + + /*! @name Constraint on the displacement */ + //@{ + /*! Returns the current constraint applied to the Frame. + + A \c NULL value (default) means that no Constraint is used to filter Frame translation and + rotation. See the Constraint class documentation for details. + + You may have to use a \c dynamic_cast to convert the result to a Constraint derived class. */ + Constraint* constraint() const { return constraint_; } + /*! Sets the constraint() attached to the Frame. + + A \c NULL value means no constraint. The previous constraint() should be deleted by the calling + method if needed. */ + void setConstraint(Constraint* const constraint) { constraint_ = constraint; } + //@} + + /*! @name Associated matrices */ + //@{ +public: + const GLdouble* matrix() const; + void getMatrix(GLdouble m[4][4]) const; + void getMatrix(GLdouble m[16]) const; + + const GLdouble* worldMatrix() const; + void getWorldMatrix(GLdouble m[4][4]) const; + void getWorldMatrix(GLdouble m[16]) const; + + void setFromMatrix(const GLdouble m[4][4]); + void setFromMatrix(const GLdouble m[16]); + //@} + + /*! @name Inversion of the transformation */ + //@{ + Frame inverse() const; + /*! Returns the inverse() of the Frame world transformation. + + The orientation() of the new Frame is the Quaternion::inverse() of the original orientation. + Its position() is the negated and inverse rotated image of the original position. + + The result Frame has a \c NULL referenceFrame() and a \c NULL constraint(). + + Use inverse() for a local (i.e. with respect to referenceFrame()) transformation inverse. */ + Frame worldInverse() const { return Frame(-(orientation().inverseRotate(position())), orientation().inverse()); } + //@} + + /*! @name XML representation */ + //@{ +public: + virtual QDomElement domElement(const QString& name, QDomDocument& document) const; +public Q_SLOTS: + virtual void initFromDOMElement(const QDomElement& element); + //@} + +private: + // P o s i t i o n a n d o r i e n t a t i o n + Vec t_; + Quaternion q_; + + // C o n s t r a i n t s + Constraint* constraint_; + + // F r a m e c o m p o s i t i o n + const Frame* referenceFrame_; +}; + +} // namespace qglviewer + +#endif // QGLVIEWER_FRAME_H diff --git a/QGLViewer/keyFrameInterpolator.cpp b/QGLViewer/keyFrameInterpolator.cpp new file mode 100644 index 0000000..5b96c63 --- /dev/null +++ b/QGLViewer/keyFrameInterpolator.cpp @@ -0,0 +1,549 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "domUtils.h" +#include "qglviewer.h" // for QGLViewer::drawAxis and Camera::drawCamera + +using namespace qglviewer; +using namespace std; + +/*! Creates a KeyFrameInterpolator, with \p frame as associated frame(). + + The frame() can be set or changed using setFrame(). + + interpolationTime(), interpolationSpeed() and interpolationPeriod() are set to their default + values. */ +KeyFrameInterpolator::KeyFrameInterpolator(Frame* frame) + : frame_(NULL), period_(40), interpolationTime_(0.0), interpolationSpeed_(1.0), interpolationStarted_(false), + closedPath_(false), loopInterpolation_(false), pathIsValid_(false), valuesAreValid_(true), currentFrameValid_(false) + // #CONNECTION# Values cut pasted initFromDOMElement() +{ + setFrame(frame); + for (int i=0; i<4; ++i) + currentFrame_[i] = new QMutableListIterator(keyFrame_); + connect(&timer_, SIGNAL(timeout()), SLOT(update())); +} + +/*! Virtual destructor. Clears the keyFrame path. */ +KeyFrameInterpolator::~KeyFrameInterpolator() +{ + deletePath(); + for (int i=0; i<4; ++i) + delete currentFrame_[i]; +} + +/*! Sets the frame() associated to the KeyFrameInterpolator. */ +void KeyFrameInterpolator::setFrame(Frame* const frame) +{ + if (this->frame()) + disconnect(this, SIGNAL( interpolated() ), this->frame(), SIGNAL( interpolated() )); + + frame_ = frame; + + if (this->frame()) + connect(this, SIGNAL( interpolated() ), this->frame(), SIGNAL( interpolated() )); +} + +/*! Updates frame() state according to current interpolationTime(). Then adds + interpolationPeriod()*interpolationSpeed() to interpolationTime(). + + This internal method is called by a timer when interpolationIsStarted(). It can be used for + debugging purpose. stopInterpolation() is called when interpolationTime() reaches firstTime() or + lastTime(), unless loopInterpolation() is \c true. */ +void KeyFrameInterpolator::update() +{ + interpolateAtTime(interpolationTime()); + + interpolationTime_ += interpolationSpeed() * interpolationPeriod() / 1000.0; + + if (interpolationTime() > keyFrame_.last()->time()) + { + if (loopInterpolation()) + setInterpolationTime(keyFrame_.first()->time() + interpolationTime_ - keyFrame_.last()->time()); + else + { + // Make sure last KeyFrame is reached and displayed + interpolateAtTime(keyFrame_.last()->time()); + stopInterpolation(); + } + Q_EMIT endReached(); + } + else + if (interpolationTime() < keyFrame_.first()->time()) + { + if (loopInterpolation()) + setInterpolationTime(keyFrame_.last()->time() - keyFrame_.first()->time() + interpolationTime_); + else + { + // Make sure first KeyFrame is reached and displayed + interpolateAtTime(keyFrame_.first()->time()); + stopInterpolation(); + } + Q_EMIT endReached(); + } +} + + +/*! Starts the interpolation process. + + A timer is started with an interpolationPeriod() period that updates the frame()'s position and + orientation. interpolationIsStarted() will return \c true until stopInterpolation() or + toggleInterpolation() is called. + + If \p period is positive, it is set as the new interpolationPeriod(). The previous + interpolationPeriod() is used otherwise (default). + + If interpolationTime() is larger than lastTime(), interpolationTime() is reset to firstTime() + before interpolation starts (and inversely for negative interpolationSpeed()). + + Use setInterpolationTime() before calling this method to change the starting interpolationTime(). + + See the keyFrames example for an illustration. + + You may also be interested in QGLViewer::animate() and QGLViewer::startAnimation(). + + \attention The keyFrames must be defined (see addKeyFrame()) \e before you startInterpolation(), + or else the interpolation will naturally immediately stop. */ +void KeyFrameInterpolator::startInterpolation(int period) +{ + if (period >= 0) + setInterpolationPeriod(period); + + if (!keyFrame_.isEmpty()) + { + if ((interpolationSpeed() > 0.0) && (interpolationTime() >= keyFrame_.last()->time())) + setInterpolationTime(keyFrame_.first()->time()); + if ((interpolationSpeed() < 0.0) && (interpolationTime() <= keyFrame_.first()->time())) + setInterpolationTime(keyFrame_.last()->time()); + timer_.start(interpolationPeriod()); + interpolationStarted_ = true; + update(); + } +} + + +/*! Stops an interpolation started with startInterpolation(). See interpolationIsStarted() and toggleInterpolation(). */ +void KeyFrameInterpolator::stopInterpolation() +{ + timer_.stop(); + interpolationStarted_ = false; +} + + +/*! Stops the interpolation and resets interpolationTime() to the firstTime(). + +If desired, call interpolateAtTime() after this method to actually move the frame() to +firstTime(). */ +void KeyFrameInterpolator::resetInterpolation() +{ + stopInterpolation(); + setInterpolationTime(firstTime()); +} + +/*! Appends a new keyFrame to the path, with its associated \p time (in seconds). + + The keyFrame is given as a pointer to a Frame, which will be connected to the + KeyFrameInterpolator: when \p frame is modified, the KeyFrameInterpolator path is updated + accordingly. This allows for dynamic paths, where keyFrame can be edited, even during the + interpolation. See the keyFrames example for an + illustration. + + \c NULL \p frame pointers are silently ignored. The keyFrameTime() has to be monotonously + increasing over keyFrames. + + Use addKeyFrame(const Frame&, qreal) to add keyFrame by values. */ +void KeyFrameInterpolator::addKeyFrame(const Frame* const frame, qreal time) +{ + if (!frame) + return; + + if (keyFrame_.isEmpty()) + interpolationTime_ = time; + + if ( (!keyFrame_.isEmpty()) && (keyFrame_.last()->time() > time) ) + qWarning("Error in KeyFrameInterpolator::addKeyFrame: time is not monotone"); + else + keyFrame_.append(new KeyFrame(frame, time)); + connect(frame, SIGNAL(modified()), SLOT(invalidateValues())); + valuesAreValid_ = false; + pathIsValid_ = false; + currentFrameValid_ = false; + resetInterpolation(); +} + +/*! Appends a new keyFrame to the path, with its associated \p time (in seconds). + + The path will use the current \p frame state. If you want the path to change when \p frame is + modified, you need to pass a \e pointer to the Frame instead (see addKeyFrame(const Frame*, + qreal)). + + The keyFrameTime() have to be monotonously increasing over keyFrames. */ +void KeyFrameInterpolator::addKeyFrame(const Frame& frame, qreal time) +{ + if (keyFrame_.isEmpty()) + interpolationTime_ = time; + + if ( (!keyFrame_.isEmpty()) && (keyFrame_.last()->time() > time) ) + qWarning("Error in KeyFrameInterpolator::addKeyFrame: time is not monotone"); + else + keyFrame_.append(new KeyFrame(frame, time)); + + valuesAreValid_ = false; + pathIsValid_ = false; + currentFrameValid_ = false; + resetInterpolation(); +} + + +/*! Appends a new keyFrame to the path. + + Same as addKeyFrame(const Frame* frame, qreal), except that the keyFrameTime() is set to the + previous keyFrameTime() plus one second (or 0.0 if there is no previous keyFrame). */ +void KeyFrameInterpolator::addKeyFrame(const Frame* const frame) +{ + qreal time; + if (keyFrame_.isEmpty()) + time = 0.0; + else + time = lastTime() + 1.0; + + addKeyFrame(frame, time); +} + +/*! Appends a new keyFrame to the path. + + Same as addKeyFrame(const Frame& frame, qreal), except that the keyFrameTime() is automatically set + to previous keyFrameTime() plus one second (or 0.0 if there is no previous keyFrame). */ +void KeyFrameInterpolator::addKeyFrame(const Frame& frame) +{ + qreal time; + if (keyFrame_.isEmpty()) + time = 0.0; + else + time = keyFrame_.last()->time() + 1.0; + + addKeyFrame(frame, time); +} + +/*! Removes all keyFrames from the path. The numberOfKeyFrames() is set to 0. */ +void KeyFrameInterpolator::deletePath() +{ + stopInterpolation(); + qDeleteAll(keyFrame_); + keyFrame_.clear(); + pathIsValid_ = false; + valuesAreValid_ = false; + currentFrameValid_ = false; +} + +void KeyFrameInterpolator::updateModifiedFrameValues() +{ + Quaternion prevQ = keyFrame_.first()->orientation(); + KeyFrame* kf; + for (int i=0; iframe()) + kf->updateValuesFromPointer(); + kf->flipOrientationIfNeeded(prevQ); + prevQ = kf->orientation(); + } + + KeyFrame* prev = keyFrame_.first(); + kf = keyFrame_.first(); + int index = 1; + while (kf) + { + KeyFrame* next = (index < keyFrame_.size()) ? keyFrame_.at(index) : NULL; + index++; + if (next) + kf->computeTangent(prev, next); + else + kf->computeTangent(prev, kf); + prev = kf; + kf = next; + } + valuesAreValid_ = true; +} + +/*! Returns the Frame associated with the keyFrame at index \p index. + + See also keyFrameTime(). \p index has to be in the range 0..numberOfKeyFrames()-1. + + \note If this keyFrame was defined using a pointer to a Frame (see addKeyFrame(const Frame* + const)), the \e current pointed Frame state is returned. */ +Frame KeyFrameInterpolator::keyFrame(int index) const +{ + const KeyFrame* const kf = keyFrame_.at(index); + return Frame(kf->position(), kf->orientation()); +} + +/*! Returns the time corresponding to the \p index keyFrame. + + See also keyFrame(). \p index has to be in the range 0..numberOfKeyFrames()-1. */ +qreal KeyFrameInterpolator::keyFrameTime(int index) const +{ + return keyFrame_.at(index)->time(); +} + +/*! Returns the duration of the KeyFrameInterpolator path, expressed in seconds. + + Simply corresponds to lastTime() - firstTime(). Returns 0.0 if the path has less than 2 keyFrames. + See also keyFrameTime(). */ +qreal KeyFrameInterpolator::duration() const +{ + return lastTime() - firstTime(); +} + +/*! Returns the time corresponding to the first keyFrame, expressed in seconds. + +Returns 0.0 if the path is empty. See also lastTime(), duration() and keyFrameTime(). */ +qreal KeyFrameInterpolator::firstTime() const +{ + if (keyFrame_.isEmpty()) + return 0.0; + else + return keyFrame_.first()->time(); +} + +/*! Returns the time corresponding to the last keyFrame, expressed in seconds. + +Returns 0.0 if the path is empty. See also firstTime(), duration() and keyFrameTime(). */ +qreal KeyFrameInterpolator::lastTime() const +{ + if (keyFrame_.isEmpty()) + return 0.0; + else + return keyFrame_.last()->time(); +} + +void KeyFrameInterpolator::updateCurrentKeyFrameForTime(qreal time) +{ + // Assertion: times are sorted in monotone order. + // Assertion: keyFrame_ is not empty + + // TODO: Special case for loops when closed path is implemented !! + if (!currentFrameValid_) + // Recompute everything from scrach + currentFrame_[1]->toFront(); + + while (currentFrame_[1]->peekNext()->time() > time) + { + currentFrameValid_ = false; + if (!currentFrame_[1]->hasPrevious()) + break; + currentFrame_[1]->previous(); + } + + if (!currentFrameValid_) + *currentFrame_[2] = *currentFrame_[1]; + + while (currentFrame_[2]->peekNext()->time() < time) + { + currentFrameValid_ = false; + if (!currentFrame_[2]->hasNext()) + break; + currentFrame_[2]->next(); + } + + if (!currentFrameValid_) + { + *currentFrame_[1] = *currentFrame_[2]; + if ((currentFrame_[1]->hasPrevious()) && (time < currentFrame_[2]->peekNext()->time())) + currentFrame_[1]->previous(); + + *currentFrame_[0] = *currentFrame_[1]; + if (currentFrame_[0]->hasPrevious()) + currentFrame_[0]->previous(); + + *currentFrame_[3] = *currentFrame_[2]; + if (currentFrame_[3]->hasNext()) + currentFrame_[3]->next(); + + currentFrameValid_ = true; + splineCacheIsValid_ = false; + } + + // cout << "Time = " << time << " : " << currentFrame_[0]->peekNext()->time() << " , " << + // currentFrame_[1]->peekNext()->time() << " , " << currentFrame_[2]->peekNext()->time() << " , " << currentFrame_[3]->peekNext()->time() << endl; +} + +void KeyFrameInterpolator::updateSplineCache() +{ + Vec delta = currentFrame_[2]->peekNext()->position() - currentFrame_[1]->peekNext()->position(); + v1 = 3.0 * delta - 2.0 * currentFrame_[1]->peekNext()->tgP() - currentFrame_[2]->peekNext()->tgP(); + v2 = -2.0 * delta + currentFrame_[1]->peekNext()->tgP() + currentFrame_[2]->peekNext()->tgP(); + splineCacheIsValid_ = true; +} + +/*! Interpolate frame() at time \p time (expressed in seconds). interpolationTime() is set to \p + time and frame() is set accordingly. + + If you simply want to change interpolationTime() but not the frame() state, use + setInterpolationTime() instead. + + Emits the interpolated() signal and makes the frame() emit the Frame::interpolated() signal. */ +void KeyFrameInterpolator::interpolateAtTime(qreal time) +{ + setInterpolationTime(time); + + if ((keyFrame_.isEmpty()) || (!frame())) + return; + + if (!valuesAreValid_) + updateModifiedFrameValues(); + + updateCurrentKeyFrameForTime(time); + + if (!splineCacheIsValid_) + updateSplineCache(); + + qreal alpha; + qreal dt = currentFrame_[2]->peekNext()->time() - currentFrame_[1]->peekNext()->time(); + if (dt == 0.0) + alpha = 0.0; + else + alpha = (time - currentFrame_[1]->peekNext()->time()) / dt; + + // Linear interpolation - debug + // Vec pos = alpha*(currentFrame_[2]->peekNext()->position()) + (1.0-alpha)*(currentFrame_[1]->peekNext()->position()); + Vec pos = currentFrame_[1]->peekNext()->position() + alpha * (currentFrame_[1]->peekNext()->tgP() + alpha * (v1+alpha*v2)); + Quaternion q = Quaternion::squad(currentFrame_[1]->peekNext()->orientation(), currentFrame_[1]->peekNext()->tgQ(), + currentFrame_[2]->peekNext()->tgQ(), currentFrame_[2]->peekNext()->orientation(), alpha); + frame()->setPositionAndOrientationWithConstraint(pos, q); + + Q_EMIT interpolated(); +} + +/*! Returns an XML \c QDomElement that represents the KeyFrameInterpolator. + + The resulting QDomElement holds the KeyFrameInterpolator parameters as well as the path keyFrames + (if the keyFrame is defined by a pointer to a Frame, use its current value). + + \p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create + QDomElement. + + Use initFromDOMElement() to restore the ManipulatedFrame state from the resulting QDomElement. + + See Vec::domElement() for a complete example. See also Quaternion::domElement(), + Camera::domElement()... + + Note that the Camera::keyFrameInterpolator() are automatically saved by QGLViewer::saveStateToFile() + when a QGLViewer is closed. */ +QDomElement KeyFrameInterpolator::domElement(const QString& name, QDomDocument& document) const +{ + QDomElement de = document.createElement(name); + int count = 0; + Q_FOREACH (KeyFrame* kf, keyFrame_) + { + Frame fr(kf->position(), kf->orientation()); + QDomElement kfNode = fr.domElement("KeyFrame", document); + kfNode.setAttribute("index", QString::number(count)); + kfNode.setAttribute("time", QString::number(kf->time())); + de.appendChild(kfNode); + ++count; + } + de.setAttribute("nbKF", QString::number(keyFrame_.count())); + de.setAttribute("time", QString::number(interpolationTime())); + de.setAttribute("speed", QString::number(interpolationSpeed())); + de.setAttribute("period", QString::number(interpolationPeriod())); + DomUtils::setBoolAttribute(de, "closedPath", closedPath()); + DomUtils::setBoolAttribute(de, "loop", loopInterpolation()); + return de; +} + +/*! Restores the KeyFrameInterpolator state from a \c QDomElement created by domElement(). + + Note that the frame() pointer is not included in the domElement(): you need to setFrame() after + this method to attach a Frame to the KeyFrameInterpolator. + + See Vec::initFromDOMElement() for a complete code example. + + See also Camera::initFromDOMElement() and Frame::initFromDOMElement(). */ +void KeyFrameInterpolator::initFromDOMElement(const QDomElement& element) +{ + qDeleteAll(keyFrame_); + keyFrame_.clear(); + QDomElement child=element.firstChild().toElement(); + while (!child.isNull()) + { + if (child.tagName() == "KeyFrame") + { + Frame fr; + fr.initFromDOMElement(child); + qreal time = DomUtils::qrealFromDom(child, "time", 0.0); + addKeyFrame(fr, time); + } + + child = child.nextSibling().toElement(); + } + + // #CONNECTION# Values cut pasted from constructor + setInterpolationTime(DomUtils::qrealFromDom(element, "time", 0.0)); + setInterpolationSpeed(DomUtils::qrealFromDom(element, "speed", 1.0)); + setInterpolationPeriod(DomUtils::intFromDom(element, "period", 40)); + setClosedPath(DomUtils::boolFromDom(element, "closedPath", false)); + setLoopInterpolation(DomUtils::boolFromDom(element, "loop", false)); + + // setFrame(NULL); + pathIsValid_ = false; + valuesAreValid_ = false; + currentFrameValid_ = false; + + stopInterpolation(); +} + +#ifndef DOXYGEN + +//////////// KeyFrame private class implementation ///////// +KeyFrameInterpolator::KeyFrame::KeyFrame(const Frame& fr, qreal t) + : time_(t), frame_(NULL) +{ + p_ = fr.position(); + q_ = fr.orientation(); +} + +KeyFrameInterpolator::KeyFrame::KeyFrame(const Frame* fr, qreal t) + : time_(t), frame_(fr) +{ + updateValuesFromPointer(); +} + +void KeyFrameInterpolator::KeyFrame::updateValuesFromPointer() +{ + p_ = frame()->position(); + q_ = frame()->orientation(); +} + +void KeyFrameInterpolator::KeyFrame::computeTangent(const KeyFrame* const prev, const KeyFrame* const next) +{ + tgP_ = 0.5 * (next->position() - prev->position()); + tgQ_ = Quaternion::squadTangent(prev->orientation(), q_, next->orientation()); +} + +void KeyFrameInterpolator::KeyFrame::flipOrientationIfNeeded(const Quaternion& prev) +{ + if (Quaternion::dot(prev, q_) < 0.0) + q_.negate(); +} + +#endif //DOXYGEN diff --git a/QGLViewer/keyFrameInterpolator.h b/QGLViewer/keyFrameInterpolator.h new file mode 100644 index 0000000..1df8d45 --- /dev/null +++ b/QGLViewer/keyFrameInterpolator.h @@ -0,0 +1,351 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_KEY_FRAME_INTERPOLATOR_H +#define QGLVIEWER_KEY_FRAME_INTERPOLATOR_H + +#include +#include + +#include "quaternion.h" +// Not actually needed, but some bad compilers (Microsoft VS6) complain. +#include "frame.h" + +// If you compiler complains about incomplete type, uncomment the next line +// #include "frame.h" +// and comment "class Frame;" 3 lines below + +namespace qglviewer { +class Camera; +class Frame; +/*! \brief A keyFrame Catmull-Rom Frame interpolator. + \class KeyFrameInterpolator keyFrameInterpolator.h QGLViewer/keyFrameInterpolator.h + + A KeyFrameInterpolator holds keyFrames (that define a path) and a pointer to a Frame of your + application (which will be interpolated). When the user startInterpolation(), the + KeyFrameInterpolator regularly updates the frame() position and orientation along the path. + + Here is a typical utilization example (see also the keyFrames + example): + \code + + + init() + { + // The KeyFrameInterpolator kfi is given the Frame that it will drive over time. + kfi = new KeyFrameInterpolator( new Frame() ); + kfi->addKeyFrame( Frame( Vec(1,0,0), Quaternion() ) ); + kfi->addKeyFrame( new Frame( Vec(2,1,0), Quaternion() ) ); + // ...and so on for all the keyFrames. + + // Ask for a display update after each update of the KeyFrameInterpolator + connect(kfi, SIGNAL(interpolated()), SLOT(update())); + + kfi->startInterpolation(); + } + + draw() + { + glPushMatrix(); + glMultMatrixd( kfi->frame()->matrix() ); + // Draw your object here. Its position and orientation are interpolated. + glPopMatrix(); + } + \endcode + + The keyFrames are defined by a Frame and a time, expressed in seconds. The Frame can be provided + as a const reference or as a pointer to a Frame (see the addKeyFrame() methods). In the latter + case, the path will automatically be updated when the Frame is modified (using the + Frame::modified() signal). + + The time has to be monotonously increasing over keyFrames. When interpolationSpeed() equals 1.0 + (default value), these times correspond to actual user's seconds during interpolation (provided + that your main loop is fast enough). The interpolation is then real-time: the keyFrames will be + reached at their keyFrameTime(). + +

Interpolation details

+ + When the user startInterpolation(), a timer is started which will update the frame()'s position + and orientation every interpolationPeriod() milliseconds. This update increases the + interpolationTime() by interpolationPeriod() * interpolationSpeed() milliseconds. + + Note that this mechanism ensures that the number of interpolation steps is constant and equal to + the total path duration() divided by the interpolationPeriod() * interpolationSpeed(). This is + especially useful for benchmarking or movie creation (constant number of snapshots). + + During the interpolation, the KeyFrameInterpolator emits an interpolated() signal, which will + usually be connected to the QGLViewer::update() slot. The interpolation is stopped when + interpolationTime() is greater than the lastTime() (unless loopInterpolation() is \c true) and the + endReached() signal is then emitted. + + Note that a Camera has Camera::keyFrameInterpolator(), that can be used to drive the Camera along a + path, or to restore a saved position (a path made of a single keyFrame). Press Alt+Fx to define a + new keyFrame for path x. Pressing Fx plays/pauses path interpolation. See QGLViewer::pathKey() and + the keyboard page for details. + + \attention If a Constraint is attached to the frame() (see Frame::constraint()), it should be + deactivated before interpolationIsStarted(), otherwise the interpolated motion (computed as if + there was no constraint) will probably be erroneous. + +

Retrieving interpolated values

+ + This code defines a KeyFrameInterpolator, and displays the positions that will be followed by the + frame() along the path: + \code + KeyFrameInterpolator kfi( new Frame() ); + // calls to kfi.addKeyFrame() to define the path. + + const qreal deltaTime = 0.04; // output a position every deltaTime seconds + for (qreal time=kfi.firstTime(); time<=kfi.lastTime(); time += deltaTime) + { + kfi.interpolateAtTime(time); + cout << "t=" << time << "\tpos=" << kfi.frame()->position() << endl; + } + \endcode + You may want to temporally disconnect the \c kfi interpolated() signal from the + QGLViewer::update() slot before calling this code. \nosubgrouping */ +class QGLVIEWER_EXPORT KeyFrameInterpolator : public QObject +{ + // todo closedPath, insertKeyFrames, deleteKeyFrame, replaceKeyFrame + Q_OBJECT + +public: + KeyFrameInterpolator(Frame* fr=NULL); + virtual ~KeyFrameInterpolator(); + +Q_SIGNALS: + /*! This signal is emitted whenever the frame() state is interpolated. + + The emission of this signal triggers the synchronous emission of the frame() + Frame::interpolated() signal, which may also be useful. + + This signal should especially be connected to your QGLViewer::update() slot, so that the display + is updated after every update of the KeyFrameInterpolator frame(): + \code + connect(myKeyFrameInterpolator, SIGNAL(interpolated()), SLOT(update())); + \endcode + Use the QGLViewer::QGLViewerPool() to connect the signal to all the viewers. + + Note that the QGLViewer::camera() Camera::keyFrameInterpolator() created using QGLViewer::pathKey() + have their interpolated() signals automatically connected to the QGLViewer::update() slot. */ + void interpolated(); + + /*! This signal is emitted when the interpolation reaches the first (when interpolationSpeed() + is negative) or the last keyFrame. + + When loopInterpolation() is \c true, interpolationTime() is reset and the interpolation + continues. It otherwise stops. */ + void endReached(); + + /*! @name Path creation */ + //@{ +public Q_SLOTS: + void addKeyFrame(const Frame& frame); + void addKeyFrame(const Frame& frame, qreal time); + + void addKeyFrame(const Frame* const frame); + void addKeyFrame(const Frame* const frame, qreal time); + + void deletePath(); + //@} + + /*! @name Associated Frame */ + //@{ +public: + /*! Returns the associated Frame and that is interpolated by the KeyFrameInterpolator. + + When interpolationIsStarted(), this Frame's position and orientation will regularly be updated + by a timer, so that they follow the KeyFrameInterpolator path. + + Set using setFrame() or with the KeyFrameInterpolator constructor. */ + Frame* frame() const { return frame_; } + +public Q_SLOTS: + void setFrame(Frame* const frame); + //@} + + /*! @name Path parameters */ + //@{ +public: + Frame keyFrame(int index) const; + qreal keyFrameTime(int index) const; + /*! Returns the number of keyFrames used by the interpolation. Use addKeyFrame() to add new keyFrames. */ + int numberOfKeyFrames() const { return keyFrame_.count(); } + qreal duration() const; + qreal firstTime() const; + qreal lastTime() const; + //@} + + /*! @name Interpolation parameters */ + //@{ +public: + /*! Returns the current interpolation time (in seconds) along the KeyFrameInterpolator path. + + This time is regularly updated when interpolationIsStarted(). Can be set directly with + setInterpolationTime() or interpolateAtTime(). */ + qreal interpolationTime() const { return interpolationTime_; } + /*! Returns the current interpolation speed. + + Default value is 1.0, which means keyFrameTime() will be matched during the interpolation + (provided that your main loop is fast enough). + + A negative value will result in a reverse interpolation of the keyFrames. See also + interpolationPeriod(). */ + qreal interpolationSpeed() const { return interpolationSpeed_; } + /*! Returns the current interpolation period, expressed in milliseconds. + + The update of the frame() state will be done by a timer at this period when + interpolationIsStarted(). + + This period (multiplied by interpolationSpeed()) is added to the interpolationTime() at each + update, and the frame() state is modified accordingly (see interpolateAtTime()). Default value + is 40 milliseconds. */ + int interpolationPeriod() const { return period_; } + /*! Returns \c true when the interpolation is played in an infinite loop. + + When \c false (default), the interpolation stops when interpolationTime() reaches firstTime() + (with negative interpolationSpeed()) or lastTime(). + + interpolationTime() is otherwise reset to firstTime() (+ interpolationTime() - lastTime()) (and + inversely for negative interpolationSpeed()) and interpolation continues. + + In both cases, the endReached() signal is emitted. */ + bool loopInterpolation() const { return loopInterpolation_; } +#ifndef DOXYGEN + /*! Whether or not (default) the path defined by the keyFrames is a closed loop. When \c true, + the last and the first KeyFrame are linked by a new spline segment. + + Use setLoopInterpolation() to create a continuous animation over the entire path. + \attention The closed path feature is not yet implemented. */ + bool closedPath() const { return closedPath_; } +#endif +public Q_SLOTS: + /*! Sets the interpolationTime(). + + \attention The frame() state is not affected by this method. Use this function to define the + starting time of a future interpolation (see startInterpolation()). Use interpolateAtTime() to + actually interpolate at a given time. */ + void setInterpolationTime(qreal time) { interpolationTime_ = time; } + /*! Sets the interpolationSpeed(). Negative or null values are allowed. */ + void setInterpolationSpeed(qreal speed) { interpolationSpeed_ = speed; } + /*! Sets the interpolationPeriod(). */ + void setInterpolationPeriod(int period) { period_ = period; } + /*! Sets the loopInterpolation() value. */ + void setLoopInterpolation(bool loop=true) { loopInterpolation_ = loop; } +#ifndef DOXYGEN + /*! Sets the closedPath() value. \attention The closed path feature is not yet implemented. */ + void setClosedPath(bool closed=true) { closedPath_ = closed; } +#endif + //@} + + + /*! @name Interpolation */ + //@{ +public: + /*! Returns \c true when the interpolation is being performed. Use startInterpolation(), + stopInterpolation() or toggleInterpolation() to modify this state. */ + bool interpolationIsStarted() const { return interpolationStarted_; } +public Q_SLOTS: + void startInterpolation(int period = -1); + void stopInterpolation(); + void resetInterpolation(); + /*! Calls startInterpolation() or stopInterpolation(), depending on interpolationIsStarted(). */ + void toggleInterpolation() { if (interpolationIsStarted()) stopInterpolation(); else startInterpolation(); } + virtual void interpolateAtTime(qreal time); + //@} + + /*! @name XML representation */ + //@{ +public: + virtual QDomElement domElement(const QString& name, QDomDocument& document) const; + virtual void initFromDOMElement(const QDomElement& element); + //@} + +private Q_SLOTS: + virtual void update(); + virtual void invalidateValues() { valuesAreValid_ = false; pathIsValid_ = false; splineCacheIsValid_ = false; } + +private: + // Copy constructor and opertor= are declared private and undefined + // Prevents everyone from trying to use them + // KeyFrameInterpolator(const KeyFrameInterpolator& kfi); + // KeyFrameInterpolator& operator=(const KeyFrameInterpolator& kfi); + + void updateCurrentKeyFrameForTime(qreal time); + void updateModifiedFrameValues(); + void updateSplineCache(); + +#ifndef DOXYGEN + // Internal private KeyFrame representation + class KeyFrame + { + public: + KeyFrame(const Frame& fr, qreal t); + KeyFrame(const Frame* fr, qreal t); + + Vec position() const { return p_; } + Quaternion orientation() const { return q_; } + Vec tgP() const { return tgP_; } + Quaternion tgQ() const { return tgQ_; } + qreal time() const { return time_; } + const Frame* frame() const { return frame_; } + void updateValuesFromPointer(); + void flipOrientationIfNeeded(const Quaternion& prev); + void computeTangent(const KeyFrame* const prev, const KeyFrame* const next); + private: + Vec p_, tgP_; + Quaternion q_, tgQ_; + qreal time_; + const Frame* const frame_; + }; +#endif + + // K e y F r a m e s + mutable QList keyFrame_; + QMutableListIterator* currentFrame_[4]; + QList path_; + + // A s s o c i a t e d f r a m e + Frame* frame_; + + // R h y t h m + QTimer timer_; + int period_; + qreal interpolationTime_; + qreal interpolationSpeed_; + bool interpolationStarted_; + + // M i s c + bool closedPath_; + bool loopInterpolation_; + + // C a c h e d v a l u e s a n d f l a g s + bool pathIsValid_; + bool valuesAreValid_; + bool currentFrameValid_; + bool splineCacheIsValid_; + Vec v1, v2; +}; + +} // namespace qglviewer + +#endif // QGLVIEWER_KEY_FRAME_INTERPOLATOR_H diff --git a/QGLViewer/manipulatedCameraFrame.cpp b/QGLViewer/manipulatedCameraFrame.cpp new file mode 100644 index 0000000..723b0f7 --- /dev/null +++ b/QGLViewer/manipulatedCameraFrame.cpp @@ -0,0 +1,469 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "domUtils.h" +#include "manipulatedCameraFrame.h" +#include "qglviewer.h" + +#include + +using namespace qglviewer; +using namespace std; + +/*! Default constructor. + + flySpeed() is set to 0.0 and sceneUpVector() is (0,1,0). The pivotPoint() is set to (0,0,0). + + \attention Created object is removeFromMouseGrabberPool(). */ +ManipulatedCameraFrame::ManipulatedCameraFrame() + : driveSpeed_(0.0), sceneUpVector_(0.0, 1.0, 0.0), rotatesAroundUpVector_(false), zoomsOnPivotPoint_(false) +{ + setFlySpeed(0.0); + removeFromMouseGrabberPool(); + connect(&flyTimer_, SIGNAL(timeout()), SLOT(flyUpdate())); +} + +/*! Equal operator. Calls ManipulatedFrame::operator=() and then copy attributes. */ +ManipulatedCameraFrame& ManipulatedCameraFrame::operator=(const ManipulatedCameraFrame& mcf) +{ + ManipulatedFrame::operator=(mcf); + + setFlySpeed(mcf.flySpeed()); + setSceneUpVector(mcf.sceneUpVector()); + setRotatesAroundUpVector(mcf.rotatesAroundUpVector_); + setZoomsOnPivotPoint(mcf.zoomsOnPivotPoint_); + + return *this; +} + +/*! Copy constructor. Performs a deep copy of all members using operator=(). */ +ManipulatedCameraFrame::ManipulatedCameraFrame(const ManipulatedCameraFrame& mcf) + : ManipulatedFrame(mcf) +{ + removeFromMouseGrabberPool(); + connect(&flyTimer_, SIGNAL(timeout()), SLOT(flyUpdate())); + (*this)=(mcf); +} + +//////////////////////////////////////////////////////////////////////////////// + +/*! Overloading of ManipulatedFrame::spin(). + +Rotates the ManipulatedCameraFrame around its pivotPoint() instead of its origin. */ +void ManipulatedCameraFrame::spin() +{ + rotateAroundPoint(spinningQuaternion(), pivotPoint()); +} + +#ifndef DOXYGEN +/*! Called for continuous frame motion in fly mode (see QGLViewer::MOVE_FORWARD). Emits + manipulated(). */ +void ManipulatedCameraFrame::flyUpdate() +{ + static Vec flyDisp(0.0, 0.0, 0.0); + switch (action_) + { + case QGLViewer::MOVE_FORWARD: + flyDisp.z = -flySpeed(); + translate(localInverseTransformOf(flyDisp)); + break; + case QGLViewer::MOVE_BACKWARD: + flyDisp.z = flySpeed(); + translate(localInverseTransformOf(flyDisp)); + break; + case QGLViewer::DRIVE: + flyDisp.z = flySpeed() * driveSpeed_; + translate(localInverseTransformOf(flyDisp)); + break; + default: + break; + } + + // Needs to be out of the switch since ZOOM/fastDraw()/wheelEvent use this callback to trigger a final draw(). + // #CONNECTION# wheelEvent. + Q_EMIT manipulated(); +} + +Vec ManipulatedCameraFrame::flyUpVector() const { + qWarning("flyUpVector() is deprecated. Use sceneUpVector() instead."); + return sceneUpVector(); +} + +void ManipulatedCameraFrame::setFlyUpVector(const Vec& up) { + qWarning("setFlyUpVector() is deprecated. Use setSceneUpVector() instead."); + setSceneUpVector(up); +} + +#endif + +/*! This method will be called by the Camera when its orientation is changed, so that the +sceneUpVector (private) is changed accordingly. You should not need to call this method. */ +void ManipulatedCameraFrame::updateSceneUpVector() +{ + sceneUpVector_ = inverseTransformOf(Vec(0.0, 1.0, 0.0)); +} + +//////////////////////////////////////////////////////////////////////////////// +// S t a t e s a v i n g a n d r e s t o r i n g // +//////////////////////////////////////////////////////////////////////////////// + +/*! Returns an XML \c QDomElement that represents the ManipulatedCameraFrame. + + Adds to the ManipulatedFrame::domElement() the ManipulatedCameraFrame specific informations in a \c + ManipulatedCameraParameters child QDomElement. + + \p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create + QDomElement. + + Use initFromDOMElement() to restore the ManipulatedCameraFrame state from the resulting + \c QDomElement. + + See Vec::domElement() for a complete example. See also Quaternion::domElement(), + Frame::domElement(), Camera::domElement()... */ +QDomElement ManipulatedCameraFrame::domElement(const QString& name, QDomDocument& document) const +{ + QDomElement e = ManipulatedFrame::domElement(name, document); + QDomElement mcp = document.createElement("ManipulatedCameraParameters"); + mcp.setAttribute("flySpeed", QString::number(flySpeed())); + DomUtils::setBoolAttribute(mcp, "rotatesAroundUpVector", rotatesAroundUpVector()); + DomUtils::setBoolAttribute(mcp, "zoomsOnPivotPoint", zoomsOnPivotPoint()); + mcp.appendChild(sceneUpVector().domElement("sceneUpVector", document)); + e.appendChild(mcp); + return e; +} + +/*! Restores the ManipulatedCameraFrame state from a \c QDomElement created by domElement(). + +First calls ManipulatedFrame::initFromDOMElement() and then initializes ManipulatedCameraFrame +specific parameters. */ +void ManipulatedCameraFrame::initFromDOMElement(const QDomElement& element) +{ + // No need to initialize, since default sceneUpVector and flySpeed are not meaningful. + // It's better to keep current ones. And it would destroy constraint() and referenceFrame(). + // *this = ManipulatedCameraFrame(); + ManipulatedFrame::initFromDOMElement(element); + + QDomElement child=element.firstChild().toElement(); + while (!child.isNull()) + { + if (child.tagName() == "ManipulatedCameraParameters") + { + setFlySpeed(DomUtils::qrealFromDom(child, "flySpeed", flySpeed())); + setRotatesAroundUpVector(DomUtils::boolFromDom(child, "rotatesAroundUpVector", false)); + setZoomsOnPivotPoint(DomUtils::boolFromDom(child, "zoomsOnPivotPoint", false)); + + QDomElement schild=child.firstChild().toElement(); + while (!schild.isNull()) + { + if (schild.tagName() == "sceneUpVector") + setSceneUpVector(Vec(schild)); + + schild = schild.nextSibling().toElement(); + } + } + child = child.nextSibling().toElement(); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// M o u s e h a n d l i n g // +//////////////////////////////////////////////////////////////////////////////// + +#ifndef DOXYGEN +/*! Protected internal method used to handle mouse events. */ +void ManipulatedCameraFrame::startAction(int ma, bool withConstraint) +{ + ManipulatedFrame::startAction(ma, withConstraint); + + switch (action_) + { + case QGLViewer::MOVE_FORWARD: + case QGLViewer::MOVE_BACKWARD: + case QGLViewer::DRIVE: + flyTimer_.setSingleShot(false); + flyTimer_.start(10); + break; + case QGLViewer::ROTATE: + constrainedRotationIsReversed_ = transformOf(sceneUpVector_).y < 0.0; + break; + default: + break; + } +} + +void ManipulatedCameraFrame::zoom(qreal delta, const Camera * const camera) { + const qreal sceneRadius = camera->sceneRadius(); + if (zoomsOnPivotPoint_) { + Vec direction = position() - camera->pivotPoint(); + if (direction.norm() > 0.02 * sceneRadius || delta > 0.0) + translate(delta * direction); + } else { + const qreal coef = qMax(fabs((camera->frame()->coordinatesOf(camera->pivotPoint())).z), 0.2 * sceneRadius); + Vec trans(0.0, 0.0, -coef * delta); + translate(inverseTransformOf(trans)); + } +} + +#endif + +/*! Overloading of ManipulatedFrame::mouseMoveEvent(). + +Motion depends on mouse binding (see mouse page for details). The +resulting displacements are basically inverted from those of a ManipulatedFrame. */ +void ManipulatedCameraFrame::mouseMoveEvent(QMouseEvent* const event, Camera* const camera) +{ + // #CONNECTION# QGLViewer::mouseMoveEvent does the update(). + switch (action_) + { + case QGLViewer::TRANSLATE: + { + const QPoint delta = prevPos_ - event->pos(); + Vec trans(delta.x(), -delta.y(), 0.0); + // Scale to fit the screen mouse displacement + switch (camera->type()) + { + case Camera::PERSPECTIVE : + trans *= 2.0 * tan(camera->fieldOfView()/2.0) * + fabs((camera->frame()->coordinatesOf(pivotPoint())).z) / camera->screenHeight(); + break; + case Camera::ORTHOGRAPHIC : + { + GLdouble w,h; + camera->getOrthoWidthHeight(w, h); + trans[0] *= 2.0 * w / camera->screenWidth(); + trans[1] *= 2.0 * h / camera->screenHeight(); + break; + } + } + translate(inverseTransformOf(translationSensitivity()*trans)); + break; + } + + case QGLViewer::MOVE_FORWARD: + { + Quaternion rot = pitchYawQuaternion(event->x(), event->y(), camera); + rotate(rot); + //#CONNECTION# wheelEvent MOVE_FORWARD case + // actual translation is made in flyUpdate(). + //translate(inverseTransformOf(Vec(0.0, 0.0, -flySpeed()))); + break; + } + + case QGLViewer::MOVE_BACKWARD: + { + Quaternion rot = pitchYawQuaternion(event->x(), event->y(), camera); + rotate(rot); + // actual translation is made in flyUpdate(). + //translate(inverseTransformOf(Vec(0.0, 0.0, flySpeed()))); + break; + } + + case QGLViewer::DRIVE: + { + Quaternion rot = turnQuaternion(event->x(), camera); + rotate(rot); + // actual translation is made in flyUpdate(). + driveSpeed_ = 0.01 * (event->y() - pressPos_.y()); + break; + } + + case QGLViewer::ZOOM: + { + zoom(deltaWithPrevPos(event, camera), camera); + break; + } + + case QGLViewer::LOOK_AROUND: + { + Quaternion rot = pitchYawQuaternion(event->x(), event->y(), camera); + rotate(rot); + break; + } + + case QGLViewer::ROTATE: + { + Quaternion rot; + if (rotatesAroundUpVector_) { + // Multiply by 2.0 to get on average about the same speed as with the deformed ball + qreal dx = 2.0 * rotationSensitivity() * (prevPos_.x() - event->x()) / camera->screenWidth(); + qreal dy = 2.0 * rotationSensitivity() * (prevPos_.y() - event->y()) / camera->screenHeight(); + if (constrainedRotationIsReversed_) dx = -dx; + Vec verticalAxis = transformOf(sceneUpVector_); + rot = Quaternion(verticalAxis, dx) * Quaternion(Vec(1.0, 0.0, 0.0), dy); + } else { + Vec trans = camera->projectedCoordinatesOf(pivotPoint()); + rot = deformedBallQuaternion(event->x(), event->y(), trans[0], trans[1], camera); + } + //#CONNECTION# These two methods should go together (spinning detection and activation) + computeMouseSpeed(event); + setSpinningQuaternion(rot); + spin(); + break; + } + + case QGLViewer::SCREEN_ROTATE: + { + Vec trans = camera->projectedCoordinatesOf(pivotPoint()); + + const qreal angle = atan2(event->y() - trans[1], event->x() - trans[0]) - atan2(prevPos_.y()-trans[1], prevPos_.x()-trans[0]); + + Quaternion rot(Vec(0.0, 0.0, 1.0), angle); + //#CONNECTION# These two methods should go together (spinning detection and activation) + computeMouseSpeed(event); + setSpinningQuaternion(rot); + spin(); + updateSceneUpVector(); + break; + } + + case QGLViewer::ROLL: + { + const qreal angle = M_PI * (event->x() - prevPos_.x()) / camera->screenWidth(); + Quaternion rot(Vec(0.0, 0.0, 1.0), angle); + rotate(rot); + setSpinningQuaternion(rot); + updateSceneUpVector(); + break; + } + + case QGLViewer::SCREEN_TRANSLATE: + { + Vec trans; + int dir = mouseOriginalDirection(event); + if (dir == 1) + trans.setValue(prevPos_.x() - event->x(), 0.0, 0.0); + else if (dir == -1) + trans.setValue(0.0, event->y() - prevPos_.y(), 0.0); + + switch (camera->type()) + { + case Camera::PERSPECTIVE : + trans *= 2.0 * tan(camera->fieldOfView()/2.0) * + fabs((camera->frame()->coordinatesOf(pivotPoint())).z) / camera->screenHeight(); + break; + case Camera::ORTHOGRAPHIC : + { + GLdouble w,h; + camera->getOrthoWidthHeight(w, h); + trans[0] *= 2.0 * w / camera->screenWidth(); + trans[1] *= 2.0 * h / camera->screenHeight(); + break; + } + } + + translate(inverseTransformOf(translationSensitivity()*trans)); + break; + } + + case QGLViewer::ZOOM_ON_REGION: + case QGLViewer::NO_MOUSE_ACTION: + break; + } + + if (action_ != QGLViewer::NO_MOUSE_ACTION) + { + prevPos_ = event->pos(); + if (action_ != QGLViewer::ZOOM_ON_REGION) + // ZOOM_ON_REGION should not emit manipulated(). + // prevPos_ is used to draw rectangle feedback. + Q_EMIT manipulated(); + } +} + + +/*! This is an overload of ManipulatedFrame::mouseReleaseEvent(). The QGLViewer::MouseAction is + terminated. */ +void ManipulatedCameraFrame::mouseReleaseEvent(QMouseEvent* const event, Camera* const camera) +{ + if ((action_ == QGLViewer::MOVE_FORWARD) || (action_ == QGLViewer::MOVE_BACKWARD) || (action_ == QGLViewer::DRIVE)) + flyTimer_.stop(); + + if (action_ == QGLViewer::ZOOM_ON_REGION) + camera->fitScreenRegion(QRect(pressPos_, event->pos())); + + ManipulatedFrame::mouseReleaseEvent(event, camera); +} + +/*! This is an overload of ManipulatedFrame::wheelEvent(). + +The wheel behavior depends on the wheel binded action. Current possible actions are QGLViewer::ZOOM, +QGLViewer::MOVE_FORWARD, QGLViewer::MOVE_BACKWARD. QGLViewer::ZOOM speed depends on +wheelSensitivity() while QGLViewer::MOVE_FORWARD and QGLViewer::MOVE_BACKWARD depend on flySpeed(). +See QGLViewer::setWheelBinding() to customize the binding. */ +void ManipulatedCameraFrame::wheelEvent(QWheelEvent* const event, Camera* const camera) +{ + //#CONNECTION# QGLViewer::setWheelBinding, ManipulatedFrame::wheelEvent. + switch (action_) + { + case QGLViewer::ZOOM: + { + zoom(wheelDelta(event), camera); + Q_EMIT manipulated(); + break; + } + case QGLViewer::MOVE_FORWARD: + case QGLViewer::MOVE_BACKWARD: + //#CONNECTION# mouseMoveEvent() MOVE_FORWARD case + translate(inverseTransformOf(Vec(0.0, 0.0, 0.2*flySpeed()*event->delta()))); + Q_EMIT manipulated(); + break; + default: + break; + } + + // #CONNECTION# startAction should always be called before + if (previousConstraint_) + setConstraint(previousConstraint_); + + // The wheel triggers a fastDraw. A final update() is needed after the last wheel event to + // polish the rendering using draw(). Since the last wheel event does not say its name, we use + // the flyTimer_ to trigger flyUpdate(), which emits manipulated. Two wheel events + // separated by more than this delay milliseconds will trigger a draw(). + const int finalDrawAfterWheelEventDelay = 400; + + // Starts (or prolungates) the timer. + flyTimer_.setSingleShot(true); + flyTimer_.start(finalDrawAfterWheelEventDelay); + + // This could also be done *before* manipulated is emitted, so that isManipulated() returns false. + // But then fastDraw would not be used with wheel. + // Detecting the last wheel event and forcing a final draw() is done using the timer_. + action_ = QGLViewer::NO_MOUSE_ACTION; +} + +//////////////////////////////////////////////////////////////////////////////// + +/*! Returns a Quaternion that is a rotation around current camera Y, proportionnal to the horizontal mouse position. */ +Quaternion ManipulatedCameraFrame::turnQuaternion(int x, const Camera* const camera) +{ + return Quaternion(Vec(0.0, 1.0, 0.0), rotationSensitivity()*(prevPos_.x()-x)/camera->screenWidth()); +} + +/*! Returns a Quaternion that is the composition of two rotations, inferred from the + mouse pitch (X axis) and yaw (sceneUpVector() axis). */ +Quaternion ManipulatedCameraFrame::pitchYawQuaternion(int x, int y, const Camera* const camera) +{ + const Quaternion rotX(Vec(1.0, 0.0, 0.0), rotationSensitivity()*(prevPos_.y()-y)/camera->screenHeight()); + const Quaternion rotY(transformOf(sceneUpVector()), rotationSensitivity()*(prevPos_.x()-x)/camera->screenWidth()); + return rotY * rotX; +} diff --git a/QGLViewer/manipulatedCameraFrame.h b/QGLViewer/manipulatedCameraFrame.h new file mode 100644 index 0000000..1f500ad --- /dev/null +++ b/QGLViewer/manipulatedCameraFrame.h @@ -0,0 +1,233 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_MANIPULATED_CAMERA_FRAME_H +#define QGLVIEWER_MANIPULATED_CAMERA_FRAME_H + +#include "manipulatedFrame.h" + +namespace qglviewer { +/*! \brief The ManipulatedCameraFrame class represents a ManipulatedFrame with Camera specific mouse bindings. + \class ManipulatedCameraFrame manipulatedCameraFrame.h QGLViewer/manipulatedCameraFrame.h + + A ManipulatedCameraFrame is a specialization of a ManipulatedFrame, designed to be set as the + Camera::frame(). Mouse motions are basically interpreted in a negated way: when the mouse goes to + the right, the ManipulatedFrame translation goes to the right, while the ManipulatedCameraFrame + has to go to the \e left, so that the \e scene seems to move to the right. + + A ManipulatedCameraFrame rotates around its pivotPoint(), which corresponds to the + associated Camera::pivotPoint(). + + A ManipulatedCameraFrame can also "fly" in the scene. It basically moves forward, and turns + according to the mouse motion. See flySpeed(), sceneUpVector() and the QGLViewer::MOVE_FORWARD and + QGLViewer::MOVE_BACKWARD QGLViewer::MouseAction. + + See the mouse page for a description of the possible actions that can + be performed using the mouse and their bindings. + \nosubgrouping */ +class QGLVIEWER_EXPORT ManipulatedCameraFrame : public ManipulatedFrame +{ +#ifndef DOXYGEN + friend class Camera; + friend class ::QGLViewer; +#endif + + Q_OBJECT + +public: + ManipulatedCameraFrame(); + /*! Virtual destructor. Empty. */ + virtual ~ManipulatedCameraFrame() {} + + ManipulatedCameraFrame(const ManipulatedCameraFrame& mcf); + ManipulatedCameraFrame& operator=(const ManipulatedCameraFrame& mcf); + + /*! @name Pivot point */ + //@{ +public: + /*! Returns the point the ManipulatedCameraFrame pivot point, around which the camera rotates. + + It is defined in the world coordinate system. Default value is (0,0,0). + + When the ManipulatedCameraFrame is associated to a Camera, Camera::pivotPoint() also + returns this value. This point can interactively be changed using the mouse (see + Camera::setPivotPointFromPixel() and QGLViewer::RAP_FROM_PIXEL and QGLViewer::RAP_IS_CENTER + in the mouse page). */ + Vec pivotPoint() const { return pivotPoint_; } + /*! Sets the pivotPoint(), defined in the world coordinate system. */ + void setPivotPoint(const Vec& point) { pivotPoint_ = point; } + +#ifndef DOXYGEN + Vec revolveAroundPoint() const { qWarning("revolveAroundPoint() is deprecated, use pivotPoint() instead"); return pivotPoint(); } + void setRevolveArountPoint(const Vec& point) { qWarning("setRevolveAroundPoint() is deprecated, use setPivotPoint() instead"); setPivotPoint(point); } +#endif + //@} + + /*! @name Camera manipulation */ + //@{ +public: + /*! Returns \c true when the frame's rotation is constrained around the sceneUpVector(), + and \c false otherwise, when the rotation is completely free (default). + + In free mode, the associated camera can be arbitrarily rotated in the scene, along its + three axis, thus possibly leading to any arbitrary orientation. + + When you setRotatesAroundUpVector() to \c true, the sceneUpVector() defines a + 'vertical' direction around which the camera rotates. The camera can rotate left + or right, around this axis. It can also be moved up or down to show the 'top' and + 'bottom' views of the scene. As a result, the sceneUpVector() will always appear vertical + in the scene, and the horizon is preserved and stays projected along the camera's + horizontal axis. + + Note that setting this value to \c true when the sceneUpVector() is not already + vertically projected will break these invariants. It will also limit the possible movement + of the camera, possibly up to a lock when the sceneUpVector() is projected horizontally. + Use Camera::setUpVector() to define the sceneUpVector() and align the camera before calling + this method to ensure this does not happen. */ + bool rotatesAroundUpVector() const { return rotatesAroundUpVector_; } + /*! Sets the value of rotatesAroundUpVector(). + + Default value is false (free rotation). */ + void setRotatesAroundUpVector(bool constrained) { rotatesAroundUpVector_ = constrained; } + + /*! Returns whether or not the QGLViewer::ZOOM action zooms on the pivot point. + + When set to \c false (default), a zoom action will move the camera along its Camera::viewDirection(), + i.e. back and forth along a direction perpendicular to the projection screen. + + setZoomsOnPivotPoint() to \c true will move the camera along an axis defined by the + Camera::pivotPoint() and its current position instead. As a result, the projected position of the + pivot point on screen will stay the same during a zoom. */ + bool zoomsOnPivotPoint() const { return zoomsOnPivotPoint_; } + /*! Sets the value of zoomsOnPivotPoint(). + + Default value is false. */ + void setZoomsOnPivotPoint(bool enabled) { zoomsOnPivotPoint_ = enabled; } + +private: +#ifndef DOXYGEN + void zoom(qreal delta, const Camera * const camera); +#endif + //@} + + /*! @name Fly parameters */ + //@{ +public Q_SLOTS: + /*! Sets the flySpeed(), defined in OpenGL units. + + Default value is 0.0, but it is modified according to the QGLViewer::sceneRadius() when the + ManipulatedCameraFrame is set as the Camera::frame(). */ + void setFlySpeed(qreal speed) { flySpeed_ = speed; } + + /*! Sets the sceneUpVector(), defined in the world coordinate system. + + Default value is (0,1,0), but it is updated by the Camera when this object is set as its Camera::frame(). + Using Camera::setUpVector() instead is probably a better solution. */ + void setSceneUpVector(const Vec& up) { sceneUpVector_ = up; } + +public: + /*! Returns the fly speed, expressed in OpenGL units. + + It corresponds to the incremental displacement that is periodically applied to the + ManipulatedCameraFrame position when a QGLViewer::MOVE_FORWARD or QGLViewer::MOVE_BACKWARD + QGLViewer::MouseAction is proceeded. + + \attention When the ManipulatedCameraFrame is set as the Camera::frame(), this value is set + according to the QGLViewer::sceneRadius() by QGLViewer::setSceneRadius(). */ + qreal flySpeed() const { return flySpeed_; } + + /*! Returns the up vector of the scene, expressed in the world coordinate system. + + In 'fly mode' (corresponding to the QGLViewer::MOVE_FORWARD and QGLViewer::MOVE_BACKWARD + QGLViewer::MouseAction bindings), horizontal displacements of the mouse rotate + the ManipulatedCameraFrame around this vector. Vertical displacements rotate always around the + Camera \c X axis. + + This value is also used when setRotationIsConstrained() is set to \c true to define the up vector + (and incidentally the 'horizon' plane) around which the camera will rotate. + + Default value is (0,1,0), but it is updated by the Camera when this object is set as its Camera::frame(). + Camera::setOrientation() and Camera::setUpVector()) direclty modify this value and should be used + instead. */ + Vec sceneUpVector() const { return sceneUpVector_; } + +#ifndef DOXYGEN + Vec flyUpVector() const; + void setFlyUpVector(const Vec& up); +#endif + //@} + + /*! @name Mouse event handlers */ + //@{ +protected: + virtual void mouseReleaseEvent(QMouseEvent* const event, Camera* const camera); + virtual void mouseMoveEvent (QMouseEvent* const event, Camera* const camera); + virtual void wheelEvent (QWheelEvent* const event, Camera* const camera); + //@} + + /*! @name Spinning */ + //@{ +protected Q_SLOTS: + virtual void spin(); + //@} + + /*! @name XML representation */ + //@{ +public: + virtual QDomElement domElement(const QString& name, QDomDocument& document) const; +public Q_SLOTS: + virtual void initFromDOMElement(const QDomElement& element); + //@} + +#ifndef DOXYGEN +protected: + virtual void startAction(int ma, bool withConstraint=true); // int is really a QGLViewer::MouseAction +#endif + +private Q_SLOTS: + virtual void flyUpdate(); + +private: + void updateSceneUpVector(); + Quaternion turnQuaternion(int x, const Camera* const camera); + Quaternion pitchYawQuaternion(int x, int y, const Camera* const camera); + +private: + // Fly mode data + qreal flySpeed_; + qreal driveSpeed_; + Vec sceneUpVector_; + QTimer flyTimer_; + + bool rotatesAroundUpVector_; + // Inverse the direction of an horizontal mouse motion. Depends on the projected + // screen orientation of the vertical axis when the mouse button is pressed. + bool constrainedRotationIsReversed_; + + bool zoomsOnPivotPoint_; + + Vec pivotPoint_; +}; + +} // namespace qglviewer + +#endif // QGLVIEWER_MANIPULATED_CAMERA_FRAME_H diff --git a/QGLViewer/manipulatedFrame.cpp b/QGLViewer/manipulatedFrame.cpp new file mode 100644 index 0000000..b6b2d72 --- /dev/null +++ b/QGLViewer/manipulatedFrame.cpp @@ -0,0 +1,550 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "domUtils.h" +#include "manipulatedFrame.h" +#include "manipulatedCameraFrame.h" +#include "qglviewer.h" +#include "camera.h" + +#include + +#include + +using namespace qglviewer; +using namespace std; + +/*! Default constructor. + + The translation is set to (0,0,0), with an identity rotation (0,0,0,1) (see Frame constructor + for details). + + The different sensitivities are set to their default values (see rotationSensitivity(), + translationSensitivity(), spinningSensitivity() and wheelSensitivity()). */ +ManipulatedFrame::ManipulatedFrame() + : action_(QGLViewer::NO_MOUSE_ACTION), keepsGrabbingMouse_(false) +{ + // #CONNECTION# initFromDOMElement and accessor docs + setRotationSensitivity(1.0); + setTranslationSensitivity(1.0); + setSpinningSensitivity(0.3); + setWheelSensitivity(1.0); + setZoomSensitivity(1.0); + + isSpinning_ = false; + previousConstraint_ = NULL; + + connect(&spinningTimer_, SIGNAL(timeout()), SLOT(spinUpdate())); +} + +/*! Equal operator. Calls Frame::operator=() and then copy attributes. */ +ManipulatedFrame& ManipulatedFrame::operator=(const ManipulatedFrame& mf) +{ + Frame::operator=(mf); + + setRotationSensitivity(mf.rotationSensitivity()); + setTranslationSensitivity(mf.translationSensitivity()); + setSpinningSensitivity(mf.spinningSensitivity()); + setWheelSensitivity(mf.wheelSensitivity()); + setZoomSensitivity(mf.zoomSensitivity()); + + mouseSpeed_ = 0.0; + dirIsFixed_ = false; + keepsGrabbingMouse_ = false; + action_ = QGLViewer::NO_MOUSE_ACTION; + + return *this; +} + +/*! Copy constructor. Performs a deep copy of all attributes using operator=(). */ +ManipulatedFrame::ManipulatedFrame(const ManipulatedFrame& mf) + : Frame(mf), MouseGrabber() +{ + (*this)=mf; +} + +//////////////////////////////////////////////////////////////////////////////// + +/*! Implementation of the MouseGrabber main method. + +The ManipulatedFrame grabsMouse() when the mouse is within a 10 pixels region around its +Camera::projectedCoordinatesOf() position(). + +See the mouseGrabber example for an illustration. */ +void ManipulatedFrame::checkIfGrabsMouse(int x, int y, const Camera* const camera) +{ + const int thresold = 10; + const Vec proj = camera->projectedCoordinatesOf(position()); + setGrabsMouse(keepsGrabbingMouse_ || ((fabs(x-proj.x) < thresold) && (fabs(y-proj.y) < thresold))); +} + +//////////////////////////////////////////////////////////////////////////////// +// S t a t e s a v i n g a n d r e s t o r i n g // +//////////////////////////////////////////////////////////////////////////////// + +/*! Returns an XML \c QDomElement that represents the ManipulatedFrame. + + Adds to the Frame::domElement() the ManipulatedFrame specific informations in a \c + ManipulatedParameters child QDomElement. + + \p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create + QDomElement. + + Use initFromDOMElement() to restore the ManipulatedFrame state from the resulting \c QDomElement. + + See Vec::domElement() for a complete example. See also Quaternion::domElement(), + Camera::domElement()... */ +QDomElement ManipulatedFrame::domElement(const QString& name, QDomDocument& document) const +{ + QDomElement e = Frame::domElement(name, document); + QDomElement mp = document.createElement("ManipulatedParameters"); + mp.setAttribute("rotSens", QString::number(rotationSensitivity())); + mp.setAttribute("transSens", QString::number(translationSensitivity())); + mp.setAttribute("spinSens", QString::number(spinningSensitivity())); + mp.setAttribute("wheelSens", QString::number(wheelSensitivity())); + mp.setAttribute("zoomSens", QString::number(zoomSensitivity())); + e.appendChild(mp); + return e; +} + +/*! Restores the ManipulatedFrame state from a \c QDomElement created by domElement(). + +Fields that are not described in \p element are set to their default values (see +ManipulatedFrame()). + +First calls Frame::initFromDOMElement() and then initializes ManipulatedFrame specific parameters. +Note that constraint() and referenceFrame() are not restored and are left unchanged. + +See Vec::initFromDOMElement() for a complete code example. */ +void ManipulatedFrame::initFromDOMElement(const QDomElement& element) +{ + // Not called since it would set constraint() and referenceFrame() to NULL. + // *this = ManipulatedFrame(); + Frame::initFromDOMElement(element); + + stopSpinning(); + + QDomElement child=element.firstChild().toElement(); + while (!child.isNull()) + { + if (child.tagName() == "ManipulatedParameters") + { + // #CONNECTION# constructor default values and accessor docs + setRotationSensitivity (DomUtils::qrealFromDom(child, "rotSens", 1.0)); + setTranslationSensitivity(DomUtils::qrealFromDom(child, "transSens", 1.0)); + setSpinningSensitivity (DomUtils::qrealFromDom(child, "spinSens", 0.3)); + setWheelSensitivity (DomUtils::qrealFromDom(child, "wheelSens", 1.0)); + setZoomSensitivity (DomUtils::qrealFromDom(child, "zoomSens", 1.0)); + } + child = child.nextSibling().toElement(); + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// M o u s e h a n d l i n g // +//////////////////////////////////////////////////////////////////////////////// + +/*! Returns \c true when the ManipulatedFrame is being manipulated with the mouse. + + Can be used to change the display of the manipulated object during manipulation. + + When Camera::frame() of the QGLViewer::camera() isManipulated(), QGLViewer::fastDraw() is used in + place of QGLViewer::draw() for scene rendering. A simplified drawing will then allow for + interactive camera displacements. */ +bool ManipulatedFrame::isManipulated() const +{ + return action_ != QGLViewer::NO_MOUSE_ACTION; +} + +/*! Starts the spinning of the ManipulatedFrame. + +This method starts a timer that will call spin() every \p updateInterval milliseconds. The +ManipulatedFrame isSpinning() until you call stopSpinning(). */ +void ManipulatedFrame::startSpinning(int updateInterval) +{ + isSpinning_ = true; + spinningTimer_.start(updateInterval); +} + +/*! Rotates the ManipulatedFrame by its spinningQuaternion(). Called by a timer when the + ManipulatedFrame isSpinning(). */ +void ManipulatedFrame::spin() +{ + rotate(spinningQuaternion()); +} + +/* spin() and spinUpdate() differ since spin can be used by itself (for instance by + QGLViewer::SCREEN_ROTATE) without a spun emission. Much nicer to use the spinningQuaternion() and + hence spin() for these incremental updates. Nothing special to be done for continuous spinning + with this design. */ +void ManipulatedFrame::spinUpdate() +{ + spin(); + Q_EMIT spun(); +} + +#ifndef DOXYGEN +/*! Protected internal method used to handle mouse events. */ +void ManipulatedFrame::startAction(int ma, bool withConstraint) +{ + action_ = (QGLViewer::MouseAction)(ma); + + // #CONNECTION# manipulatedFrame::wheelEvent, manipulatedCameraFrame::wheelEvent and mouseReleaseEvent() + // restore previous constraint + if (withConstraint) + previousConstraint_ = NULL; + else + { + previousConstraint_ = constraint(); + setConstraint(NULL); + } + + switch (action_) + { + case QGLViewer::ROTATE: + case QGLViewer::SCREEN_ROTATE: + mouseSpeed_ = 0.0; + stopSpinning(); + break; + + case QGLViewer::SCREEN_TRANSLATE: + dirIsFixed_ = false; + break; + + default: + break; + } +} + +/*! Updates mouse speed, measured in pixels/milliseconds. Should be called by any method which wants to +use mouse speed. Currently used to trigger spinning in mouseReleaseEvent(). */ +void ManipulatedFrame::computeMouseSpeed(const QMouseEvent* const e) +{ + const QPoint delta = (e->pos() - prevPos_); + const qreal dist = sqrt(qreal(delta.x()*delta.x() + delta.y()*delta.y())); + delay_ = last_move_time.restart(); + if (delay_ == 0) + // Less than a millisecond: assume delay = 1ms + mouseSpeed_ = dist; + else + mouseSpeed_ = dist/delay_; +} + +/*! Return 1 if mouse motion was started horizontally and -1 if it was more vertical. Returns 0 if +this could not be determined yet (perfect diagonal motion, rare). */ +int ManipulatedFrame::mouseOriginalDirection(const QMouseEvent* const e) +{ + static bool horiz = true; // Two simultaneous manipulatedFrame require two mice ! + + if (!dirIsFixed_) + { + const QPoint delta = e->pos() - pressPos_; + dirIsFixed_ = abs(delta.x()) != abs(delta.y()); + horiz = abs(delta.x()) > abs(delta.y()); + } + + if (dirIsFixed_) + if (horiz) + return 1; + else + return -1; + else + return 0; +} + +qreal ManipulatedFrame::deltaWithPrevPos(QMouseEvent* const event, Camera* const camera) const { + qreal dx = qreal(event->x() - prevPos_.x()) / camera->screenWidth(); + qreal dy = qreal(event->y() - prevPos_.y()) / camera->screenHeight(); + + qreal value = fabs(dx) > fabs(dy) ? dx : dy; + return value * zoomSensitivity(); +} + +qreal ManipulatedFrame::wheelDelta(const QWheelEvent* event) const { + static const qreal WHEEL_SENSITIVITY_COEF = 8E-4; + return event->delta() * wheelSensitivity() * WHEEL_SENSITIVITY_COEF; +} + +void ManipulatedFrame::zoom(qreal delta, const Camera * const camera) { + Vec trans(0.0, 0.0, (camera->position() - position()).norm() * delta); + + trans = camera->frame()->orientation().rotate(trans); + if (referenceFrame()) + trans = referenceFrame()->transformOf(trans); + translate(trans); +} + +#endif // DOXYGEN + +/*! Initiates the ManipulatedFrame mouse manipulation. + +Overloading of MouseGrabber::mousePressEvent(). See also mouseMoveEvent() and mouseReleaseEvent(). + +The mouse behavior depends on which button is pressed. See the QGLViewer +mouse page for details. */ +void ManipulatedFrame::mousePressEvent(QMouseEvent* const event, Camera* const camera) +{ + Q_UNUSED(camera); + + if (grabsMouse()) + keepsGrabbingMouse_ = true; + + // #CONNECTION setMouseBinding + // action_ should no longer possibly be NO_MOUSE_ACTION since this value is not inserted in mouseBinding_ + //if (action_ == QGLViewer::NO_MOUSE_ACTION) + //event->ignore(); + + prevPos_ = pressPos_ = event->pos(); +} + +/*! Modifies the ManipulatedFrame according to the mouse motion. + +Actual behavior depends on mouse bindings. See the QGLViewer::MouseAction enum and the QGLViewer mouse page for details. + +The \p camera is used to fit the mouse motion with the display parameters (see +Camera::screenWidth(), Camera::screenHeight(), Camera::fieldOfView()). + +Emits the manipulated() signal. */ +void ManipulatedFrame::mouseMoveEvent(QMouseEvent* const event, Camera* const camera) +{ + switch (action_) + { + case QGLViewer::TRANSLATE: + { + const QPoint delta = event->pos() - prevPos_; + Vec trans(delta.x(), -delta.y(), 0.0); + // Scale to fit the screen mouse displacement + switch (camera->type()) + { + case Camera::PERSPECTIVE : + trans *= 2.0 * tan(camera->fieldOfView()/2.0) * fabs((camera->frame()->coordinatesOf(position())).z) / camera->screenHeight(); + break; + case Camera::ORTHOGRAPHIC : + { + GLdouble w,h; + camera->getOrthoWidthHeight(w, h); + trans[0] *= 2.0 * w / camera->screenWidth(); + trans[1] *= 2.0 * h / camera->screenHeight(); + break; + } + } + // Transform to world coordinate system. + trans = camera->frame()->orientation().rotate(translationSensitivity()*trans); + // And then down to frame + if (referenceFrame()) trans = referenceFrame()->transformOf(trans); + translate(trans); + break; + } + + case QGLViewer::ZOOM: + { + zoom(deltaWithPrevPos(event, camera), camera); + break; + } + + case QGLViewer::SCREEN_ROTATE: + { + Vec trans = camera->projectedCoordinatesOf(position()); + + const qreal prev_angle = atan2(prevPos_.y()-trans[1], prevPos_.x()-trans[0]); + const qreal angle = atan2(event->y()-trans[1], event->x()-trans[0]); + + const Vec axis = transformOf(camera->frame()->inverseTransformOf(Vec(0.0, 0.0, -1.0))); + Quaternion rot(axis, angle-prev_angle); + //#CONNECTION# These two methods should go together (spinning detection and activation) + computeMouseSpeed(event); + setSpinningQuaternion(rot); + spin(); + break; + } + + case QGLViewer::SCREEN_TRANSLATE: + { + Vec trans; + int dir = mouseOriginalDirection(event); + if (dir == 1) + trans.setValue(event->x() - prevPos_.x(), 0.0, 0.0); + else if (dir == -1) + trans.setValue(0.0, prevPos_.y() - event->y(), 0.0); + + switch (camera->type()) + { + case Camera::PERSPECTIVE : + trans *= 2.0 * tan(camera->fieldOfView()/2.0) * fabs((camera->frame()->coordinatesOf(position())).z) / camera->screenHeight(); + break; + case Camera::ORTHOGRAPHIC : + { + GLdouble w,h; + camera->getOrthoWidthHeight(w, h); + trans[0] *= 2.0 * w / camera->screenWidth(); + trans[1] *= 2.0 * h / camera->screenHeight(); + break; + } + } + // Transform to world coordinate system. + trans = camera->frame()->orientation().rotate(translationSensitivity()*trans); + // And then down to frame + if (referenceFrame()) + trans = referenceFrame()->transformOf(trans); + + translate(trans); + break; + } + + case QGLViewer::ROTATE: + { + Vec trans = camera->projectedCoordinatesOf(position()); + Quaternion rot = deformedBallQuaternion(event->x(), event->y(), trans[0], trans[1], camera); + trans = Vec(-rot[0], -rot[1], -rot[2]); + trans = camera->frame()->orientation().rotate(trans); + trans = transformOf(trans); + rot[0] = trans[0]; + rot[1] = trans[1]; + rot[2] = trans[2]; + //#CONNECTION# These two methods should go together (spinning detection and activation) + computeMouseSpeed(event); + setSpinningQuaternion(rot); + spin(); + break; + } + + case QGLViewer::MOVE_FORWARD: + case QGLViewer::MOVE_BACKWARD: + case QGLViewer::LOOK_AROUND: + case QGLViewer::ROLL: + case QGLViewer::DRIVE: + case QGLViewer::ZOOM_ON_REGION: + // These MouseAction values make no sense for a manipulatedFrame + break; + + case QGLViewer::NO_MOUSE_ACTION: + // Possible when the ManipulatedFrame is a MouseGrabber. This method is then called without startAction + // because of mouseTracking. + break; + } + + if (action_ != QGLViewer::NO_MOUSE_ACTION) + { + prevPos_ = event->pos(); + Q_EMIT manipulated(); + } +} + +/*! Stops the ManipulatedFrame mouse manipulation. + +Overloading of MouseGrabber::mouseReleaseEvent(). + +If the action was a QGLViewer::ROTATE QGLViewer::MouseAction, a continuous spinning is possible if +the speed of the mouse cursor is larger than spinningSensitivity() when the button is released. +Press the rotate button again to stop spinning. See startSpinning() and isSpinning(). */ +void ManipulatedFrame::mouseReleaseEvent(QMouseEvent* const event, Camera* const camera) +{ + Q_UNUSED(event); + Q_UNUSED(camera); + + keepsGrabbingMouse_ = false; + + if (previousConstraint_) + setConstraint(previousConstraint_); + + if (((action_ == QGLViewer::ROTATE) || (action_ == QGLViewer::SCREEN_ROTATE)) && (mouseSpeed_ >= spinningSensitivity())) + startSpinning(delay_); + + action_ = QGLViewer::NO_MOUSE_ACTION; +} + +/*! Overloading of MouseGrabber::mouseDoubleClickEvent(). + +Left button double click aligns the ManipulatedFrame with the \p camera axis (see alignWithFrame() + and QGLViewer::ALIGN_FRAME). Right button projects the ManipulatedFrame on the \p camera view + direction. */ +void ManipulatedFrame::mouseDoubleClickEvent(QMouseEvent* const event, Camera* const camera) +{ + if (event->modifiers() == Qt::NoModifier) + switch (event->button()) + { + case Qt::LeftButton: alignWithFrame(camera->frame()); break; + case Qt::RightButton: projectOnLine(camera->position(), camera->viewDirection()); break; + default: break; + } +} + +/*! Overloading of MouseGrabber::wheelEvent(). + +Using the wheel is equivalent to a QGLViewer::ZOOM QGLViewer::MouseAction. See + QGLViewer::setWheelBinding(), setWheelSensitivity(). */ +void ManipulatedFrame::wheelEvent(QWheelEvent* const event, Camera* const camera) +{ + //#CONNECTION# QGLViewer::setWheelBinding + if (action_ == QGLViewer::ZOOM) + { + zoom(wheelDelta(event), camera); + Q_EMIT manipulated(); + } + + // #CONNECTION# startAction should always be called before + if (previousConstraint_) + setConstraint(previousConstraint_); + + action_ = QGLViewer::NO_MOUSE_ACTION; +} + + +//////////////////////////////////////////////////////////////////////////////// + +/*! Returns "pseudo-distance" from (x,y) to ball of radius size. +\arg for a point inside the ball, it is proportional to the euclidean distance to the ball +\arg for a point outside the ball, it is proportional to the inverse of this distance (tends to +zero) on the ball, the function is continuous. */ +static qreal projectOnBall(qreal x, qreal y) +{ + // If you change the size value, change angle computation in deformedBallQuaternion(). + const qreal size = 1.0; + const qreal size2 = size*size; + const qreal size_limit = size2*0.5; + + const qreal d = x*x + y*y; + return d < size_limit ? sqrt(size2 - d) : size_limit/sqrt(d); +} + +#ifndef DOXYGEN +/*! Returns a quaternion computed according to the mouse motion. Mouse positions are projected on a +deformed ball, centered on (\p cx,\p cy). */ +Quaternion ManipulatedFrame::deformedBallQuaternion(int x, int y, qreal cx, qreal cy, const Camera* const camera) +{ + // Points on the deformed ball + qreal px = rotationSensitivity() * (prevPos_.x() - cx) / camera->screenWidth(); + qreal py = rotationSensitivity() * (cy - prevPos_.y()) / camera->screenHeight(); + qreal dx = rotationSensitivity() * (x - cx) / camera->screenWidth(); + qreal dy = rotationSensitivity() * (cy - y) / camera->screenHeight(); + + const Vec p1(px, py, projectOnBall(px, py)); + const Vec p2(dx, dy, projectOnBall(dx, dy)); + // Approximation of rotation angle + // Should be divided by the projectOnBall size, but it is 1.0 + const Vec axis = cross(p2,p1); + const qreal angle = 5.0 * asin(sqrt(axis.squaredNorm() / p1.squaredNorm() / p2.squaredNorm())); + return Quaternion(axis, angle); +} +#endif // DOXYGEN diff --git a/QGLViewer/manipulatedFrame.h b/QGLViewer/manipulatedFrame.h new file mode 100644 index 0000000..b27fb25 --- /dev/null +++ b/QGLViewer/manipulatedFrame.h @@ -0,0 +1,335 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_MANIPULATED_FRAME_H +#define QGLVIEWER_MANIPULATED_FRAME_H + +#include "frame.h" +#include "mouseGrabber.h" +#include "qglviewer.h" + +#include +#include +#include + +namespace qglviewer { +/*! \brief A ManipulatedFrame is a Frame that can be rotated and translated using the mouse. + \class ManipulatedFrame manipulatedFrame.h QGLViewer/manipulatedFrame.h + + It converts the mouse motion into a translation and an orientation updates. A ManipulatedFrame is + used to move an object in the scene. Combined with object selection, its MouseGrabber properties + and a dynamic update of the scene, the ManipulatedFrame introduces a great reactivity in your + applications. + + A ManipulatedFrame is attached to a QGLViewer using QGLViewer::setManipulatedFrame(): + \code + init() { setManipulatedFrame( new ManipulatedFrame() ); } + + draw() + { + glPushMatrix(); + glMultMatrixd(manipulatedFrame()->matrix()); + // draw the manipulated object here + glPopMatrix(); + } + \endcode + See the manipulatedFrame example for a complete + application. + + Mouse events are normally sent to the QGLViewer::camera(). You have to press the QGLViewer::FRAME + state key (default is \c Control) to move the QGLViewer::manipulatedFrame() instead. See the mouse page for a description of mouse button bindings. + +

Inherited functionalities

+ + A ManipulatedFrame is an overloaded instance of a Frame. The powerful coordinate system + transformation functions (Frame::coordinatesOf(), Frame::transformOf(), ...) can hence be applied + to a ManipulatedFrame. + + A ManipulatedFrame is also a MouseGrabber. If the mouse cursor gets within a distance of 10 pixels + from the projected position of the ManipulatedFrame, the ManipulatedFrame becomes the new + QGLViewer::mouseGrabber(). It can then be manipulated directly, without any specific state key, + object selection or GUI intervention. This is very convenient to directly move some objects in the + scene (typically a light). See the mouseGrabber + example as an illustration. Note that QWidget::setMouseTracking() needs to be enabled in order + to use this feature (see the MouseGrabber documentation). + +

Advanced functionalities

+ + A QGLViewer can handle at most one ManipulatedFrame at a time. If you want to move several objects + in the scene, you simply have to keep a list of the different ManipulatedFrames, and to activate + the right one (using QGLViewer::setManipulatedFrame()) when needed. This can for instance be done + according to an object selection: see the luxo example for an + illustration. + + When the ManipulatedFrame is being manipulated using the mouse (mouse pressed and not yet + released), isManipulated() returns \c true. This might be used to trigger a specific action or + display (as is done with QGLViewer::fastDraw()). + + The ManipulatedFrame also emits a manipulated() signal each time its state is modified by the + mouse. This signal is automatically connected to the QGLViewer::update() slot when the + ManipulatedFrame is attached to a viewer using QGLViewer::setManipulatedFrame(). + + You can make the ManipulatedFrame spin() if you release the rotation mouse button while moving the + mouse fast enough (see spinningSensitivity()). See also translationSensitivity() and + rotationSensitivity() for sensitivity tuning. \nosubgrouping */ +class QGLVIEWER_EXPORT ManipulatedFrame : public Frame, public MouseGrabber +{ +#ifndef DOXYGEN + friend class Camera; + friend class ::QGLViewer; +#endif + + Q_OBJECT + +public: + ManipulatedFrame(); + /*! Virtual destructor. Empty. */ + virtual ~ManipulatedFrame() {} + + ManipulatedFrame(const ManipulatedFrame& mf); + ManipulatedFrame& operator=(const ManipulatedFrame& mf); + +Q_SIGNALS: + /*! This signal is emitted when ever the ManipulatedFrame is manipulated (i.e. rotated or + translated) using the mouse. Connect this signal to any object that should be notified. + + Note that this signal is automatically connected to the QGLViewer::update() slot, when the + ManipulatedFrame is attached to a viewer using QGLViewer::setManipulatedFrame(), which is + probably all you need. + + Use the QGLViewer::QGLViewerPool() if you need to connect this signal to all the viewers. + + See also the spun(), modified(), interpolated() and KeyFrameInterpolator::interpolated() + signals' documentations. */ + void manipulated(); + + /*! This signal is emitted when the ManipulatedFrame isSpinning(). + + Note that for the QGLViewer::manipulatedFrame(), this signal is automatically connected to the + QGLViewer::update() slot. + + Connect this signal to any object that should be notified. Use the QGLViewer::QGLViewerPool() if + you need to connect this signal to all the viewers. + + See also the manipulated(), modified(), interpolated() and KeyFrameInterpolator::interpolated() + signals' documentations. */ + void spun(); + + /*! @name Manipulation sensitivity */ + //@{ +public Q_SLOTS: + /*! Defines the rotationSensitivity(). */ + void setRotationSensitivity(qreal sensitivity) { rotationSensitivity_ = sensitivity; } + /*! Defines the translationSensitivity(). */ + void setTranslationSensitivity(qreal sensitivity) { translationSensitivity_ = sensitivity; } + /*! Defines the spinningSensitivity(), in pixels per milliseconds. */ + void setSpinningSensitivity(qreal sensitivity) { spinningSensitivity_ = sensitivity; } + /*! Defines the wheelSensitivity(). */ + void setWheelSensitivity(qreal sensitivity) { wheelSensitivity_ = sensitivity; } + /*! Defines the zoomSensitivity(). */ + void setZoomSensitivity(qreal sensitivity) { zoomSensitivity_ = sensitivity; } + +public: + /*! Returns the influence of a mouse displacement on the ManipulatedFrame rotation. + + Default value is 1.0. With an identical mouse displacement, a higher value will generate a + larger rotation (and inversely for lower values). A 0.0 value will forbid ManipulatedFrame mouse + rotation (see also constraint()). + + See also setRotationSensitivity(), translationSensitivity(), spinningSensitivity() and + wheelSensitivity(). */ + qreal rotationSensitivity() const { return rotationSensitivity_; } + /*! Returns the influence of a mouse displacement on the ManipulatedFrame translation. + + Default value is 1.0. You should not have to modify this value, since with 1.0 the + ManipulatedFrame precisely stays under the mouse cursor. + + With an identical mouse displacement, a higher value will generate a larger translation (and + inversely for lower values). A 0.0 value will forbid ManipulatedFrame mouse translation (see + also constraint()). + + \note When the ManipulatedFrame is used to move a \e Camera (see the ManipulatedCameraFrame + class documentation), after zooming on a small region of your scene, the camera may translate + too fast. For a camera, it is the Camera::pivotPoint() that exactly matches the mouse + displacement. Hence, instead of changing the translationSensitivity(), solve the problem by + (temporarily) setting the Camera::pivotPoint() to a point on the zoomed region (see the + QGLViewer::RAP_FROM_PIXEL mouse binding in the mouse page). + + See also setTranslationSensitivity(), rotationSensitivity(), spinningSensitivity() and + wheelSensitivity(). */ + qreal translationSensitivity() const { return translationSensitivity_; } + /*! Returns the minimum mouse speed required (at button release) to make the ManipulatedFrame + spin(). + + See spin(), spinningQuaternion() and startSpinning() for details. + + Mouse speed is expressed in pixels per milliseconds. Default value is 0.3 (300 pixels per + second). Use setSpinningSensitivity() to tune this value. A higher value will make spinning more + difficult (a value of 100.0 forbids spinning in practice). + + See also setSpinningSensitivity(), translationSensitivity(), rotationSensitivity() and + wheelSensitivity(). */ + qreal spinningSensitivity() const { return spinningSensitivity_; } + + /*! Returns the zoom sensitivity. + + Default value is 1.0. A higher value will make the zoom faster. + Use a negative value to invert the zoom in and out directions. + + See also setZoomSensitivity(), translationSensitivity(), rotationSensitivity() wheelSensitivity() + and spinningSensitivity(). */ + qreal zoomSensitivity() const { return zoomSensitivity_; } + /*! Returns the mouse wheel sensitivity. + + Default value is 1.0. A higher value will make the wheel action more efficient (usually meaning + a faster zoom). Use a negative value to invert the zoom in and out directions. + + See also setWheelSensitivity(), translationSensitivity(), rotationSensitivity() zoomSensitivity() + and spinningSensitivity(). */ + qreal wheelSensitivity() const { return wheelSensitivity_; } + //@} + + + /*! @name Spinning */ + //@{ +public: + /*! Returns \c true when the ManipulatedFrame is spinning. + + During spinning, spin() rotates the ManipulatedFrame by its spinningQuaternion() at a frequency + defined when the ManipulatedFrame startSpinning(). + + Use startSpinning() and stopSpinning() to change this state. Default value is \c false. */ + bool isSpinning() const { return isSpinning_; } + /*! Returns the incremental rotation that is applied by spin() to the ManipulatedFrame + orientation when it isSpinning(). + + Default value is a null rotation (identity Quaternion). Use setSpinningQuaternion() to change + this value. + + The spinningQuaternion() axis is defined in the ManipulatedFrame coordinate system. You can use + Frame::transformOfFrom() to convert this axis from an other Frame coordinate system. */ + Quaternion spinningQuaternion() const { return spinningQuaternion_; } +public Q_SLOTS: + /*! Defines the spinningQuaternion(). Its axis is defined in the ManipulatedFrame coordinate + system. */ + void setSpinningQuaternion(const Quaternion& spinningQuaternion) { spinningQuaternion_ = spinningQuaternion; } + virtual void startSpinning(int updateInterval); + /*! Stops the spinning motion started using startSpinning(). isSpinning() will return \c false + after this call. */ + virtual void stopSpinning() { spinningTimer_.stop(); isSpinning_ = false; } +protected Q_SLOTS: + virtual void spin(); +private Q_SLOTS: + void spinUpdate(); + //@} + + /*! @name Mouse event handlers */ + //@{ +protected: + virtual void mousePressEvent (QMouseEvent* const event, Camera* const camera); + virtual void mouseMoveEvent (QMouseEvent* const event, Camera* const camera); + virtual void mouseReleaseEvent (QMouseEvent* const event, Camera* const camera); + virtual void mouseDoubleClickEvent(QMouseEvent* const event, Camera* const camera); + virtual void wheelEvent (QWheelEvent* const event, Camera* const camera); + //@} + +public: + /*! @name Current state */ + //@{ + bool isManipulated() const; + /*! Returns the \c MouseAction currently applied to this ManipulatedFrame. + + Will return QGLViewer::NO_MOUSE_ACTION unless a mouse button is being pressed + and has been bound to this QGLViewer::MouseHandler. + + The binding between mouse buttons and key modifiers and MouseAction is set using + QGLViewer::setMouseBinding(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton buttons, MouseHandler handler, MouseAction action, bool withConstraint). + */ + QGLViewer::MouseAction currentMouseAction() const { return action_; } + //@} + + /*! @name MouseGrabber implementation */ + //@{ +public: + virtual void checkIfGrabsMouse(int x, int y, const Camera* const camera); + //@} + + /*! @name XML representation */ + //@{ +public: + virtual QDomElement domElement(const QString& name, QDomDocument& document) const; +public Q_SLOTS: + virtual void initFromDOMElement(const QDomElement& element); + //@} + +#ifndef DOXYGEN +protected: + Quaternion deformedBallQuaternion(int x, int y, qreal cx, qreal cy, const Camera* const camera); + + QGLViewer::MouseAction action_; + Constraint* previousConstraint_; // When manipulation is without Contraint. + + virtual void startAction(int ma, bool withConstraint=true); // int is really a QGLViewer::MouseAction + void computeMouseSpeed(const QMouseEvent* const e); + int mouseOriginalDirection(const QMouseEvent* const e); + + /*! Returns a screen scaled delta from event's position to prevPos_, along the + X or Y direction, whichever has the largest magnitude. */ + qreal deltaWithPrevPos(QMouseEvent* const event, Camera* const camera) const; + /*! Returns a normalized wheel delta, proportionnal to wheelSensitivity(). */ + qreal wheelDelta(const QWheelEvent* event) const; + + // Previous mouse position (used for incremental updates) and mouse press position. + QPoint prevPos_, pressPos_; + +private: + void zoom(qreal delta, const Camera * const camera); + +#endif // DOXYGEN + +private: + // Sensitivity + qreal rotationSensitivity_; + qreal translationSensitivity_; + qreal spinningSensitivity_; + qreal wheelSensitivity_; + qreal zoomSensitivity_; + + // Mouse speed and spinning + QTime last_move_time; + qreal mouseSpeed_; + int delay_; + bool isSpinning_; + QTimer spinningTimer_; + Quaternion spinningQuaternion_; + + // Whether the SCREEN_TRANS direction (horizontal or vertical) is fixed or not. + bool dirIsFixed_; + + // MouseGrabber + bool keepsGrabbingMouse_; +}; + +} // namespace qglviewer + +#endif // QGLVIEWER_MANIPULATED_FRAME_H diff --git a/QGLViewer/mouseGrabber.cpp b/QGLViewer/mouseGrabber.cpp new file mode 100644 index 0000000..38cfdd0 --- /dev/null +++ b/QGLViewer/mouseGrabber.cpp @@ -0,0 +1,76 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "mouseGrabber.h" + +using namespace qglviewer; + +// Static private variable +QList MouseGrabber::MouseGrabberPool_; + +/*! Default constructor. + +Adds the created MouseGrabber in the MouseGrabberPool(). grabsMouse() is set to \c false. */ +MouseGrabber::MouseGrabber() + : grabsMouse_(false) +{ + addInMouseGrabberPool(); +} + +/*! Adds the MouseGrabber in the MouseGrabberPool(). + +All created MouseGrabber are automatically added in the MouseGrabberPool() by the constructor. +Trying to add a MouseGrabber that already isInMouseGrabberPool() has no effect. + +Use removeFromMouseGrabberPool() to remove the MouseGrabber from the list, so that it is no longer +tested with checkIfGrabsMouse() by the QGLViewer, and hence can no longer grab mouse focus. Use +isInMouseGrabberPool() to know the current state of the MouseGrabber. */ +void MouseGrabber::addInMouseGrabberPool() +{ + if (!isInMouseGrabberPool()) + MouseGrabber::MouseGrabberPool_.append(this); +} + +/*! Removes the MouseGrabber from the MouseGrabberPool(). + +See addInMouseGrabberPool() for details. Removing a MouseGrabber that is not in MouseGrabberPool() +has no effect. */ +void MouseGrabber::removeFromMouseGrabberPool() +{ + if (isInMouseGrabberPool()) + MouseGrabber::MouseGrabberPool_.removeAll(const_cast(this)); +} + +/*! Clears the MouseGrabberPool(). + + Use this method only if it is faster to clear the MouseGrabberPool() and then to add back a few + MouseGrabbers than to remove each one independently. Use QGLViewer::setMouseTracking(false) instead + if you want to disable mouse grabbing. + + When \p autoDelete is \c true, the MouseGrabbers of the MouseGrabberPool() are actually deleted + (use this only if you're sure of what you do). */ +void MouseGrabber::clearMouseGrabberPool(bool autoDelete) +{ + if (autoDelete) + qDeleteAll(MouseGrabber::MouseGrabberPool_); + MouseGrabber::MouseGrabberPool_.clear(); +} diff --git a/QGLViewer/mouseGrabber.h b/QGLViewer/mouseGrabber.h new file mode 100644 index 0000000..d704d1c --- /dev/null +++ b/QGLViewer/mouseGrabber.h @@ -0,0 +1,264 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_MOUSE_GRABBER_H +#define QGLVIEWER_MOUSE_GRABBER_H + +#include "config.h" + +#include + +class QGLViewer; + +namespace qglviewer { +class Camera; + +/*! \brief Abstract class for objects that grab mouse focus in a QGLViewer. + \class MouseGrabber mouseGrabber.h QGLViewer/mouseGrabber.h + + MouseGrabber are objects which react to the mouse cursor, usually when it hovers over them. This + abstract class only provides an interface for all these objects: their actual behavior has to be + defined in a derived class. + +

How does it work ?

+ + All the created MouseGrabber are grouped in a MouseGrabberPool(). The QGLViewers parse this pool, + calling all the MouseGrabbers' checkIfGrabsMouse() methods that setGrabsMouse() if desired. + + When a MouseGrabber grabsMouse(), it becomes the QGLViewer::mouseGrabber(). All the mouse events + (mousePressEvent(), mouseReleaseEvent(), mouseMoveEvent(), mouseDoubleClickEvent() and + wheelEvent()) are then transmitted to the QGLViewer::mouseGrabber() instead of being normally + processed. This continues while grabsMouse() (updated using checkIfGrabsMouse()) returns \c true. + + If you want to (temporarily) disable a specific MouseGrabbers, you can remove it from this pool + using removeFromMouseGrabberPool(). You can also disable a MouseGrabber in a specific QGLViewer + using QGLViewer::setMouseGrabberIsEnabled(). + +

Implementation details

+ + In order to make MouseGrabber react to mouse events, mouse tracking has to be activated in the + QGLViewer which wants to use MouseGrabbers: + \code + init() { setMouseTracking(true); } + \endcode + Call \c QGLWidget::hasMouseTracking() to get the current state of this flag. [TODO Update with QOpenGLWidget] + + The \p camera parameter of the different mouse event methods is a pointer to the + QGLViewer::camera() of the QGLViewer that uses the MouseGrabber. It can be used to compute 2D to + 3D coordinates conversion using Camera::projectedCoordinatesOf() and + Camera::unprojectedCoordinatesOf(). + + Very complex behaviors can be implemented using this framework: auto-selected objects (no need to + press a key to use them), automatic drop-down menus, 3D GUI, spinners using the wheelEvent(), and + whatever your imagination creates. See the mouseGrabber + example for an illustration. + + Note that ManipulatedFrame are MouseGrabber: see the keyFrame + example for an illustration. Every created ManipulatedFrame is hence present in the + MouseGrabberPool() (note however that ManipulatedCameraFrame are not inserted). + +

Example

+ + Here is for instance a draft version of a MovableObject class. Instances of these class can freely + be moved on screen using the mouse, as movable post-it-like notes: + \code + class MovableObject : public MouseGrabber + { + public: + MovableObject() : pos(0,0), moved(false) {} + + void checkIfGrabsMouse(int x, int y, const qglviewer::Camera* const) + { + // MovableObject is active in a region of 5 pixels around its pos. + // May depend on the actual shape of the object. Customize as desired. + // Once clicked (moved = true), it keeps grabbing mouse until button is released. + setGrabsMouse( moved || ((pos-QPoint(x,y)).manhattanLength() < 5) ); + } + + void mousePressEvent( QMouseEvent* const e, Camera* const) { prevPos = e->pos(); moved = true; } + + void mouseMoveEvent(QMouseEvent* const e, const Camera* const) + { + if (moved) + { + // Add position delta to current pos + pos += e->pos() - prevPos; + prevPos = e->pos(); + } + } + + void mouseReleaseEvent(QMouseEvent* const, Camera* const) { moved = false; } + + void draw() + { + // The object is drawn centered on its pos, with different possible aspects: + if (grabsMouse()) + if (moved) + // Object being moved, maybe a transparent display + else + // Object ready to be moved, maybe a highlighted visual feedback + else + // Normal display + } + + private: + QPoint pos, prevPos; + bool moved; + }; + \endcode + Note that the different event callback methods are called only once the MouseGrabber grabsMouse(). + \nosubgrouping */ +class QGLVIEWER_EXPORT MouseGrabber +{ +#ifndef DOXYGEN + friend class ::QGLViewer; +#endif + +public: + MouseGrabber(); + /*! Virtual destructor. Removes the MouseGrabber from the MouseGrabberPool(). */ + virtual ~MouseGrabber() { MouseGrabber::MouseGrabberPool_.removeAll(this); } + + /*! @name Mouse grabbing detection */ + //@{ +public: + /*! Pure virtual method, called by the QGLViewers before they test if the MouseGrabber + grabsMouse(). Should setGrabsMouse() according to the mouse position. + + This is the core method of the MouseGrabber. It has to be overloaded in your derived class. + Its goal is to update the grabsMouse() flag according to the mouse and MouseGrabber current + positions, using setGrabsMouse(). + + grabsMouse() is usually set to \c true when the mouse cursor is close enough to the MouseGrabber + position. It should also be set to \c false when the mouse cursor leaves this region in order to + release the mouse focus. + + \p x and \p y are the mouse cursor coordinates (Qt coordinate system: (0,0) corresponds to the upper + left corner). + + A typical implementation will look like: + \code + // (posX,posY) is the position of the MouseGrabber on screen. + // Here, distance to mouse must be less than 10 pixels to activate the MouseGrabber. + setGrabsMouse( sqrt((x-posX)*(x-posX) + (y-posY)*(y-posY)) < 10); + \endcode + + If the MouseGrabber position is defined in 3D, use the \p camera parameter, corresponding to + the calling QGLViewer Camera. Project on screen and then compare the projected coordinates: + \code + Vec proj = camera->projectedCoordinatesOf(myMouseGrabber->frame()->position()); + setGrabsMouse((fabs(x-proj.x) < 5) && (fabs(y-proj.y) < 2)); // Rectangular region + \endcode + + See examples in the detailed description section and in the mouseGrabber example. */ + virtual void checkIfGrabsMouse(int x, int y, const Camera* const camera) = 0; + + /*! Returns \c true when the MouseGrabber grabs the QGLViewer's mouse events. + + This flag is set with setGrabsMouse() by the checkIfGrabsMouse() method. */ + bool grabsMouse() const { return grabsMouse_; } + +protected: + /*! Sets the grabsMouse() flag. Normally used by checkIfGrabsMouse(). */ + void setGrabsMouse(bool grabs) { grabsMouse_ = grabs; } + //@} + + + /*! @name MouseGrabber pool */ + //@{ +public: + /*! Returns a list containing pointers to all the active MouseGrabbers. + + Used by the QGLViewer to parse all the MouseGrabbers and to check if any of them grabsMouse() + using checkIfGrabsMouse(). + + You should not have to directly use this list. Use removeFromMouseGrabberPool() and + addInMouseGrabberPool() to modify this list. + + \attention This method returns a \c QPtrList with Qt 3 and a \c QList with Qt 2. */ + static const QList& MouseGrabberPool() { return MouseGrabber::MouseGrabberPool_; } + + /*! Returns \c true if the MouseGrabber is currently in the MouseGrabberPool() list. + + Default value is \c true. When set to \c false using removeFromMouseGrabberPool(), the + QGLViewers no longer checkIfGrabsMouse() on this MouseGrabber. Use addInMouseGrabberPool() to + insert it back. */ + bool isInMouseGrabberPool() const { return MouseGrabber::MouseGrabberPool_.contains(const_cast(this)); } + void addInMouseGrabberPool(); + void removeFromMouseGrabberPool(); + void clearMouseGrabberPool(bool autoDelete=false); + //@} + + + /*! @name Mouse event handlers */ + //@{ +protected: + /*! Callback method called when the MouseGrabber grabsMouse() and a mouse button is pressed. + + + The MouseGrabber will typically start an action or change its state when a mouse button is + pressed. mouseMoveEvent() (called at each mouse displacement) will then update the MouseGrabber + accordingly and mouseReleaseEvent() (called when the mouse button is released) will terminate + this action. + + Use the \p event QMouseEvent::state() and QMouseEvent::button() to test the keyboard + and button state and possibly change the MouseGrabber behavior accordingly. + + See the detailed description section and the mouseGrabber example for examples. + + See the \c QGLWidget::mousePressEvent() and the \c QMouseEvent documentations for details. [TODO Update with QOpenGLWidget] */ + virtual void mousePressEvent(QMouseEvent* const event, Camera* const camera) { Q_UNUSED(event); Q_UNUSED(camera); } + /*! Callback method called when the MouseGrabber grabsMouse() and a mouse button is double clicked. + + See the \c QGLWidget::mouseDoubleClickEvent() and the \c QMouseEvent documentations for details. [TODO Update with QOpenGLWidget] */ + virtual void mouseDoubleClickEvent(QMouseEvent* const event, Camera* const camera) { Q_UNUSED(event); Q_UNUSED(camera); } + /*! Mouse release event callback method. See mousePressEvent(). */ + virtual void mouseReleaseEvent(QMouseEvent* const event, Camera* const camera) { Q_UNUSED(event); Q_UNUSED(camera); } + /*! Callback method called when the MouseGrabber grabsMouse() and the mouse is moved while a + button is pressed. + + This method will typically update the state of the MouseGrabber from the mouse displacement. See + the mousePressEvent() documentation for details. */ + virtual void mouseMoveEvent(QMouseEvent* const event, Camera* const camera) { Q_UNUSED(event); Q_UNUSED(camera); } + /*! Callback method called when the MouseGrabber grabsMouse() and the mouse wheel is used. + + See the \c QGLWidget::wheelEvent() and the \c QWheelEvent documentations for details. [TODO Update with QOpenGLWidget] */ + virtual void wheelEvent(QWheelEvent* const event, Camera* const camera) { Q_UNUSED(event); Q_UNUSED(camera); } + //@} + +private: + // Copy constructor and opertor= are declared private and undefined + // Prevents everyone from trying to use them + MouseGrabber(const MouseGrabber&); + MouseGrabber& operator=(const MouseGrabber&); + + bool grabsMouse_; + + // Q G L V i e w e r p o o l + static QList MouseGrabberPool_; +}; + +} // namespace qglviewer + +#endif // QGLVIEWER_MOUSE_GRABBER_H diff --git a/QGLViewer/qglviewer-icon.xpm b/QGLViewer/qglviewer-icon.xpm new file mode 100644 index 0000000..5c7e72a --- /dev/null +++ b/QGLViewer/qglviewer-icon.xpm @@ -0,0 +1,359 @@ +/* XPM */ +static const char * qglviewer_icon[] = { +"100 100 256 2", +" c None", +". c #0A0B27", +"+ c #090B2C", +"@ c #150C12", +"# c #080F34", +"$ c #1A0E1A", +"% c #220D0B", +"& c #260B0B", +"* c #230E1C", +"= c #12113E", +"- c #2C0D10", +"; c #2F0D09", +"> c #310D14", +", c #1A123A", +"' c #261025", +") c #17134B", +"! c #151453", +"~ c #1B1733", +"{ c #14135E", +"] c #281430", +"^ c #0E1867", +"/ c #3B1215", +"( c #32132A", +"_ c #1D1849", +": c #121C58", +"< c #431014", +"[ c #141974", +"} c #3A152E", +"| c #201A5E", +"1 c #1D204C", +"2 c #401431", +"3 c #261960", +"4 c #48171C", +"5 c #411B21", +"6 c #371B45", +"7 c #2A1C6B", +"8 c #0E229B", +"9 c #52171D", +"0 c #441A35", +"a c #1B2582", +"b c #65150F", +"c c #2D2076", +"d c #4D1B3A", +"e c #57182F", +"f c #3E1F54", +"g c #5E1924", +"h c #2E2567", +"i c #002BD1", +"j c #002AD9", +"k c #6D1A13", +"l c #36237A", +"m c #002FCD", +"n c #701A0D", +"o c #462060", +"p c #651C23", +"q c #322581", +"r c #552429", +"s c #581E41", +"t c #671E1B", +"u c #771911", +"v c #6C1B2B", +"w c #562341", +"x c #342A79", +"y c #3C2484", +"z c #062FE6", +"A c #68212B", +"B c #4D236B", +"C c #612045", +"D c #42267B", +"E c #811B10", +"F c #7C1E0E", +"G c #0033F1", +"H c #0032F9", +"I c #3A298E", +"J c #472872", +"K c #72202A", +"L c #4F2774", +"M c #652449", +"N c #442890", +"O c #1F37A8", +"P c #87200E", +"Q c #852014", +"R c #402B98", +"S c #6C244D", +"T c #7D2230", +"U c #8F1F12", +"V c #70254A", +"W c #542A7E", +"X c #212FF2", +"Y c #6E2E28", +"Z c #442E9B", +"` c #772834", +" . c #971E15", +".. c #552B86", +"+. c #693132", +"@. c #702A46", +"#. c #7B2645", +"$. c #862435", +"%. c #79264E", +"&. c #5A2B88", +"*. c #1B3AE9", +"=. c #9A2211", +"-. c #4E2EA0", +";. c #1E3BDE", +">. c #722F3D", +",. c #5E2C91", +"'. c #592F91", +"). c #7A322A", +"!. c #1D42D7", +"~. c #902539", +"{. c #7D2A52", +"]. c #862E26", +"^. c #822853", +"/. c #922B20", +"(. c #4E34AA", +"_. c #A62411", +":. c #5D309A", +"<. c #59387B", +"[. c #5631AB", +"}. c #5533A6", +"|. c #912840", +"1. c #8C2B3F", +"2. c #AE2215", +"3. c #862C57", +"4. c #5D32A8", +"5. c #882C53", +"6. c #5C34A2", +"7. c #6231A2", +"8. c #A32A18", +"9. c #5C398B", +"0. c #4D4480", +"a. c #982A3F", +"b. c #8D2B5A", +"c. c #962C44", +"d. c #902C56", +"e. c #7A3B43", +"f. c #3249C3", +"g. c #B42812", +"h. c #583E9B", +"i. c #BA2516", +"j. c #524297", +"k. c #55448B", +"l. c #5E3BA3", +"m. c #9E2C47", +"n. c #5A4676", +"o. c #873847", +"p. c #404E94", +"q. c #932F59", +"r. c #BE2810", +"s. c #992D5B", +"t. c #5941A4", +"u. c #8A3F36", +"v. c #9D2F58", +"w. c #78415D", +"x. c #A72D4D", +"y. c #A2304B", +"z. c #C72815", +"A. c #C12C13", +"B. c #654298", +"C. c #654780", +"D. c #8F385F", +"E. c #98345D", +"F. c #2F51E3", +"G. c #A93724", +"H. c #A0325B", +"I. c #CA2B10", +"J. c #A93054", +"K. c #A6305D", +"L. c #D42A00", +"M. c #D22914", +"N. c #89415D", +"O. c #6546A5", +"P. c #AB3255", +"Q. c #DC2806", +"R. c #A3355D", +"S. c #AA325A", +"T. c #A13F2F", +"U. c #6A4A8E", +"V. c #A53754", +"W. c #DE2A00", +"X. c #CF300A", +"Y. c #D62C0E", +"Z. c #A8385B", +"`. c #4B54C3", +" + c #964356", +".+ c #E82903", +"++ c #8F4757", +"@+ c #DF2C14", +"#+ c #D93011", +"$+ c #A23D62", +"%+ c #8C4E44", +"&+ c #E32F00", +"*+ c #6E4CA5", +"=+ c #6E4F9B", +"-+ c #A04259", +";+ c #9D4266", +">+ c #D03716", +",+ c #E3300D", +"'+ c #6A51A7", +")+ c #BA3F2F", +"!+ c #EC2E07", +"~+ c #A9405F", +"{+ c #EB2E13", +"]+ c #EF3100", +"^+ c #4A5CDC", +"/+ c #984A68", +"(+ c #B54536", +"_+ c #4563C9", +":+ c #F62E03", +"<+ c #EF310B", +"[+ c #E73411", +"}+ c #AA4B3E", +"|+ c #F62E10", +"1+ c #CE401F", +"2+ c #D33D23", +"3+ c #7455A7", +"4+ c #C54426", +"5+ c #A5496A", +"6+ c #F93106", +"7+ c #A45048", +"8+ c #CD412C", +"9+ c #F2350E", +"0+ c #AA4A64", +"a+ c #5266C7", +"b+ c #DE411E", +"c+ c #EF3C18", +"d+ c #755FAD", +"e+ c #C94C34", +"f+ c #C14F3F", +"g+ c #DB452E", +"h+ c #AB526E", +"i+ c #EB421F", +"j+ c #AA566A", +"k+ c #A55B6B", +"l+ c #AA5873", +"m+ c #E94824", +"n+ c #D94D35", +"o+ c #DB4F31", +"p+ c #5B71DE", +"q+ c #A85F76", +"r+ c #EC4C31", +"s+ c #E2522E", +"t+ c #E84F30", +"u+ c #CD5D42", +"v+ c #6976CD", +"w+ c #D9583F", +"x+ c #E55537", +"y+ c #E7573E", +"z+ c #D65F4C", +"A+ c #CA6457", +"B+ c #E35C3E", +"C+ c #D1634E", +"D+ c #E26447", +"E+ c #E1674E", +"F+ c #DE6F57", +"G+ c #DE705E", +" ", +" ", +" C+ ", +" ` >.o. 1+X.u+ ", +" e.A K K T $.T ++ 1+L.L.X. ", +" A A v ` T T T 1.~.++ X.L.L.L.W.1+ ", +" g A K K T T $.$.1.$.1. >+L.L.L.L.L.L. ", +" r g A v v T T $.1.$.$.|.|. + u+X.L.L.L.L.W.W.W.2+ ", +" f r g g K K v T T T |.|.1.1.c.-+ u+X.L.L.L.Q.L.Q.Q.W.W. ", +" 6 o B C. 4 9 p p v T T T 1.~.$.1.~.a.~.1. u+X.L.L.L.Q.L.W.L.W.W.W.o+ ", +" f o B C. 4 9 9 g v v T T $.1.1.~.|.c.c.c.m. 4+L.L.L.Q.L.Q.Q.W.&+W.W.,+b+ ", +" f o J L U. 9 9 A v K T $.1.1.$.|.|.c.c.c.a.a.q+ 4+L.L.L.L.L.Q.W.W.&+L.&+&+&+W. ", +" f o B W L <. < 9 g A e.` ` $.$.|.$.1.~.a.a.c.c.a.k+ 1+L.L.Q.Q.W.L.W.L.&+&+&+,+&+&+w+ ", +" 6 f B L L .. 4 4 9 o.$.|.$.c.a.c.c.m.m.y.c.q+ u+L.W.Q.L.W.&+&+W.&+,+&+&+&+.+s+ ", +" p. f o L W W .. / < r 1.|.|.c.c.c.c.y.m.y.a.k+ X.L.W.L.,+,+&+,+&+&+&+[+&+&+b+ ", +" 3 6 B L W W &.9. / o.|.|.c.a.m.y.a.a.y.y.q+ e+W.W.&+W.W.,+&+&+&+&+.+&+!+b+ ", +" a : f o L L ....&.B. / ++a.a.m.c.a.y.y.a.y.y.q+ u+W.W.W.&+.+&+{+&+[+&+!+&+]+&+ ", +" O ^ 1 f J W ....&.'.U. / 5 k+c.m.c.m.y.m.y.y.y.y. L.&+&+&+&+&+&+!+]+!+[+!+!+!+ ", +" f.[ ) $ <.L W ..&.'.'. / k+c.m.a.m.y.a.y.y.y.y. b+&+,+&+.+&+&+&+[+]+&+!+&+!+E+ ", +" `.8 ! = $ <.W ..&.'.'.B. - k+y.a.y.y.a.y.x.y.x.V. b+.+&+!+&+[+!+!+]+!+!+!+9+!+B+ ", +" a+m { = # $ ....'.'.'.,.=+ - k+m.y.y.y.V.y.x.y.x.V. w+!+&+!+]+&+[+&+!+[+<+!+]+&+B+ ", +" v+m 8 ) # $ U...'.'.'.,.:. - -+m.m.y.V.x.y.y.x.m.0+ s+&+!+[+&+!+]+<+!+]+]+[+<+9+B+ ", +" ;.m ) = + $ 9.&.'.l.l.,.*+ & -+y.y.x.x.J.x.y.x.y.0+ B+&+]+!+<+!+!+[+]+!+]+<+]+<+B+ ", +" ;.i [ = + . $ &.'.,.,.,.:.*+ & 0+y.y.x.y.y.y.J.V.x.l+ s+!+!+!+[+<+]+]+!+]+]+]+!+]+B+ ", +" ;.z ^+1 # + $ =+l.'.l.7.:.'. % y.x.y.x.x.P.y.x.P.y. B+.+[+]+!+[+]+]+<+]+]+]+]+<+B+ ", +" ;.z ;. + + . $ '.:.:.:.:.:.3+ & V.y.x.y.x.J.P.P.V.Z. x+<+]+]+]+:+]+]+]+9+<+9+9+]+ ", +" p+*.G G p+ + . @ U.:.:.7.:.6.7. % h+P.V.P.P.y.J.P.x.y.0+ s+]+9+]+]+9+9+]+]+]+]+]+9+<+ ", +" f. p+F.H H G F. . . . $ l.:.:.:.6.:.O. & 0+x.J.J.V.P.x.x.P.P.h+ x+<+]+<+]+]+]+9+]+]+]+9+]+<+ ", +" i G X X X G H ;. . . $ =+:.:.6.l.6.:.3+ % V.y.y.x.P.V.V.P.P.J. m+!+]+]+]+9+]+|+9+|+]+9+]+c+ ", +" m z G H G H G p+ . . * O.6.6.:.7.6.:. & V.P.P.P.x.P.P.V.V.V. i+]+]+<+|+:+]+]+:+]+:+:+]+m+ ", +" i z G G G G F. . . * 3+7.7.6.6.6.6.3+ % 0+x.P.x.V.x.x.x.x.P.0+ [+9+]+]+]+|+]+]+]+9+9+9+]+m+ ", +" i z G G H *. . . * :.6.6.7.6.7.:. & 0+P.P.P.P.P.P.P.P.V. :+:+]+]+9+]+9+9+|+:+:+:+]+B+ ", +" i z G G G p+ . * *+7.}.6.4.6.6.*+ % V.y.P.x.P.x.P.P.P.V. F+]+|+9+:+]+:+:+]+]+:+|+|+:+B+ ", +" i z G G ^+ . ' n.4.6.7.4.7.4.6. & 0+x.P.P.P.P.P.P.P.P.~+ s+9+]+:+]+|+6+]+]+|+9+]+]+9+ ", +" i z G F. . * O.4.6.6.6.6.4.*+ & ~+P.P.P.P.P.P.P.P.Z.l+ s+6+9+|+]+9+|+|+]+6+:+6+6+9+ ", +" j z !. . ] J 4.6.4.[.4.[.l. & P.P.P.P.P.P.P.P.P.V. c+9+6+]+|+6+9+]+6+9+|+9+|+i+ ", +" a+i ;. . ' l.[.(.6.6.6.4.*+ & h+P.P.S.P.S.P.P.P.P.0+ :+|+]+]+|+6+|+9+6+]+6+]+9+t+ ", +" a+!. . ] t.(.6.7.4.4.[.6. & S.P.P.P.P.P.S.S.S.S.q+ D+9+9+|+6+]+9+]+6+|+6+9+6+:+E+ ", +" . 3+7.[.6.(.6.6.6.*+ & h+S.S.P.S.~+P.P.P.P.~+ r+6+]+9+6+6+6+9+]+9+6+9+6+9+ ", +" . 6.(.6.4.4.4.(.6. & ~+P.P.P.P.P.P.P.P.P.h+ c+6+|+6+9+6+9+6+|+6+|+6+9+9+ ", +" . l.[.[.(.6.(.4.(.3+ % l+S.V.~+P.P.P.~+P.P.S. 9+]+|+6+9+6+|+9+9+]+|+6+9+t+ ", +" ~ *+(.6.4.4.4.6.4.l. % ~+P.S.P.S.P.S.P.S.S.0+ B+9+6+9+9+6+|+9+6+6+|+]+]+|+D+ ", +" ~ '+(.[.(.(.(.4.4.6.d+ & S.P.S.P.S.P.S.P.P.~+ i+9+6+6+9+6+6+:+9+:+6+|+6+c+ ", +" = '+(.[.(.(.[.4.[.(.O. % ~+~+S.Z.S.Z.S.P.Z.S.~+ 9+6+6+9+6+9+:+9+6+6+9+6+]+m+ ", +" _ '+Z }.}.4.6.(.(.[.4. & q+S.S.S.~+S.~+S.S.S.S.l+ x+]+9+6+9+6+9+6+9+9+6+9+|+6+B+ ", +" ) h h.Z -.-.}.(.(.[.[.(.'+ & R.Z.Z.S.S.S.S.~+Z.Z.R. r+|+6+|+6+9+6+|+6+6+9+6+9+]+F+ ", +" _ 3 x k.j.I Z -.Z Z }.4.(.(.4.t. % l+S.S.Z.Z.Z.S.S.S.S.Z.h+ F+]+9+6+9+6+9+6+9+9+6+9+6+6+c+ ", +" ) 3 7 q I I Z N }.}.}.(.}.}.(. & Z.Z.S.Z.Z.S.Z.Z.Z.Z.~+ B+6+6+9+6+|+6+9+6+6+|+6+6+9+y+ ", +" = ! 7 c q I N Z Z Z Z -.}.}.}.'+ % l+S.Z.S.S.Z.S.S.S.S.Z.h+ c+9+6+9+6+9+6+|+9+6+9+9+6+6+ ", +" _ ) 3 l q N N Z N -.}.Z -.(.O. & R.R.R.S.K.S.Z.Z.Z.V.$+ D+6+6+9+6+9+6+9+6+6+9+6+6+9+m+ ", +" _ 3 3 l q I N Z Z -.-.}.}.Z & $+S.S.Z.Z.Z.K.K.K.K.S.h+ i+9+6+9+6+9+6+9+9+6+9+9+6+9+B+ ", +" _ | 7 l y I I N N Z -.(.-.d+ % l+R.R.R.R.S.K.Z.S.S.S.~+ F+9+6+9+|+6+6+9+6+6+9+|+6+6+9+ ", +" _ 3 7 c q I R Z :.Z Z -.'+ & R.K.K.K.R.Z.S.Z.Z.Z.K.l+ r+9+6+6+9+6+6+6+9+6+6+9+9+6+t+ ", +" ) | 7 l y y I N R Z -.D & ;+R.Z.Z.S.K.K.R.Z.S.S.5+ G+6+6+9+9+6+9+9+9+6+6+9+6+6+]+E+ ", +" 3 x c I y I Z Z Z h.' & 5+K.K.R.R.S.K.S.K.R.R.S. t+9+6+|+9+6+6+6+6+9+6+9+|+|+i+ ", +" | c l q I R I Z h. $ & $+R.s.K.R.R.Z.R.K.K.K.5+ 9+6+9+6+6+6+c+6+9+6+9+|+6+6+x+ ", +" h 7 l q y N N j. * & H.H.R.s.K.H.H.K.Z.K.S.Z. r+9+6+9+6+6+6+6+6+6+6+9+9+9+c+ ", +" 0.l q y I y '+ ' ; ;+v.v.K.R.H.K.K.v.H.Z.R.5+ G+9+|+6+c+6+c+6+c+6+6+9+6+|+6+B+ ", +" 0.x D j. ' & ;+s.R.s.K.H.s.s.R.K.K.K.$+ m+9+9+|+9+6+9+6+6+c+6+6+9+9+6+ ", +" ( ( ; ;+v.$+s.R.v.H.R.K.K.R.s.R. E+9+|+9+6+6+c+6+c+6+6+9+9+6+6+t+ ", +" ( } & ; /+5.v.v.v.s.H.R.H.H.s.K.R.5+ i+9+|+9+9+6+6+6+c+6+6+6+9+6+9+ ", +" ( } - ; /+q.s.s.s.E.H.v.K.v.R.K.R.$+ y+|+|+9+9+|+9+c+6+6+c+9+9+|+9+x+ ", +" ( 2 0 - ; D.d.q.q.E.v.v.s.s.H.s.R.v.$+ 9+9+|+|+9+6+6+9+c+6+6+6+c+6+9+ ", +" } 0 0 ; ; 3.q.b.q.q.s.E.s.H.H.v.K.v.H.5+ r+9+|+9+9+6+c+9+6+6+c+9+9+|+|+x+ ", +" } 0 d d ; < N.5.3.q.q.q.q.s.E.E.E.s.v.s.H.5+ D+9+9+|+9+9+6+c+9+6+|+6+9+9+9+9+ ", +" } 2 s s s M w.w.e @.{.^.3.3.b.d.q.q.q.s.v.v.s.H.s.$+ i+9+|+9+|+c+6+|+6+9+9+9+9+6+|+y+ ", +" 0 0 d s C M V V V ^.^.5.5.5.3.d.q.q.q.q.s.v.E.R.E. r+9+c+|+9+|+9+c+c+|+|+9+6+9+6+i+ ", +" 2 2 s w C C V V %.{.{.5.b.5.q.3.d.q.q.s.E.E.v.s.q+ w+9+9+9+9+9+9+|+|+|+9+9+|+9+|+9+E+ ", +" 0 0 d w C C S V V {.%.#.3.5.d.d.d.q.q.s.s.E.E.q+ E+c+9+9+c+9+c+9+|+9+|+9+9+9+9+|+i+ ", +" } d s s C M V S %.%.^.3.3.3.3.3.d.d.q.d.q.s./+ i+!+c+9+9+9+|+9+9+|+9+|+9+|+|+9+G+ ", +" 0 d d e M M V S {.{.^.3.3.d.d.D.d.d.q.q.v./+ r+[+|+{+9+c+9+9+9+9+9+9+|+9+9+9+B+ ", +" d s s s V S V S %.{.{.5.3.3.d.d.q.q.q./+ o+[+!+c+9+|+9+c+9+c+9+c+9+9+|+9+m+ ", +" 0 s C s V V %.V {.^.{.5.5.3.3.D.d.q./+ n+{+[+{+[+c+c+|+9+9+9+|+9+c+9+|+|+G+ ", +" d w s M M M V %.%.{.3.{.3.d.d.d.q./+ w+[+[+{+&+{+{+{+9+c+9+c+9+c+|+9+c+B+ ", +" w s @.S S S %.%.{.5.5.^.3.b.D. G+@+[+[+[+c+[+<+[+9+c+|+c+9+9+c+|+m+ ", +" w s C M V V S %.^.{.{.3.5.N. C+@+[+{+&+{+&+<+[+{+{+{+9+9+9+c+9+c+G+ ", +" w M M S V {.V ^.5.5.{. w+,+,+@+[+[+{+[+{+c+9+<+[+9+9+9+9+{+D+ ", +" M @.V V {.{.#.++ w+@+@+@+[+{+[+[+{+[+&+[+[+{+c+{+{+{+x+ ", +" w.>.p t b ). 8+@+,+[+,+@+{+[+[+&+{+{+{+[+!+[+9+9+b+ ", +" +.b b n u %+ 2+#+,+@+Q.@+[+@+,+[+[+[+[+[+<+[+<+{+i+ ", +" b b k u u u+M.M.#+#+@+,+@+[+@+[+{+[+[+{+[+[+[+[+{+z+ ", +" b b n u u /. A+2+M.#+#+#+,+@+@+,+,+[+@+,+[+{+[+{+{+[+<+w+ ", +" t k k F u P /.7+ A+e+z.#+#+@+M.#+#+,+#+@+Q.[+@+@+,+{+&+[+{+[+B+ ", +" Y n u u E Q U U T.7+ f+4+I.M.>+M.M.Y.@+M.@+#+@+,+@+@+[+[+[+[+[+[+[+n+ ", +" k n F Q Q U .=._.8.G.(+(+)+g.i.z.r.I.M.I.>+I.#+#+#+#+#+#+,+,+Q.,+@+,+@+{+{+o+ ", +" k u u E U U U =.=._.2.g.i.i.i.A.r.z.z.I.M.Y.#+>+#+#+#+Q.@+@+@+@+,+[+@+[+,+s+ ", +" ).n E E P U =.=.=.8._._.g.g.i.i.A.A.z.I.z.M.Y.M.M.M.#+#+#+,+@+@+@+[+,+@+g+ ", +" u u Q U U U =._._.2.g.g.g.A.A.A.z.A.z.M.I.I.Y.#+#+#+#+@+#+,+,+,+,+@+o+ ", +" ).F Q U U =.=.=.8._.2.2.g.r.r.r.I.I.I.z.M.I.M.#+M.#+#+#+#+@+@+@+,+w+ ", +" Q Q P U U =._._.2._.g.g.g.i.A.A.A.A.I.I.M.X.M.#+@+#+@+@+@+,+#+z+ ", +" u.U /.U U 8._.8._.2.2.g.g.i.A.A.I.z.I.I.M.I.Y.>+@+#+#+#+#+#+A+ ", +" ].U U =.=.=.2.2._.g.i.i.A.i.A.A.I.z.I.>+Y.>+Y.Y.#+#+#+n+ ", +" /.U .=.=._._.2.2.g.i.A.A.z.A.I.I.M.I.M.M.M.M.Y.M.z+ ", +" /.=.8._._.2._.g.g.g.g.i.A.z.A.z.I.I.Y.I.X.#+8+ ", +" /.8.8._._.2.2.i.i.A.i.A.z.z.I.I.I.M.M.8+A+ ", +" }+8._.2.2.g.2.A.i.A.A.A.>+z.z.z.4+A+ ", +" T.)+_.g.2.A.r.A.A.r.A.4+z+ ", +" }+}+(+(+f+f+A+ ", +" ", +" "}; diff --git a/QGLViewer/qglviewer.cpp b/QGLViewer/qglviewer.cpp new file mode 100644 index 0000000..6452e52 --- /dev/null +++ b/QGLViewer/qglviewer.cpp @@ -0,0 +1,3171 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "domUtils.h" +#include "qglviewer.h" +#include "camera.h" +#include "keyFrameInterpolator.h" +#include "manipulatedCameraFrame.h" + +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#include +# include +# include +# include +# include + +using namespace std; +using namespace qglviewer; + +// Static private variable +QList QGLViewer::QGLViewerPool_; + + +/*! \mainpage + +libQGLViewer is a free C++ library based on Qt that enables the quick creation of OpenGL 3D viewers. +It features a powerful camera trackball and simple applications simply require an implementation of +the draw() method. This makes it a tool of choice for OpenGL beginners and +assignments. It provides screenshot saving, mouse manipulated frames, stereo display, interpolated +keyFrames, object selection, and much more. It is fully +customizable and easy to extend to create complex applications, with a possible Qt GUI. + +libQGLViewer is not a 3D viewer that can be used directly to view 3D scenes in various +formats. It is more likely to be the starting point for the coding of such a viewer. + +libQGLViewer is based on the Qt toolkit and hence compiles on any architecture (Unix-Linux, Mac, +Windows, ...). Full reference documentation and many examples are provided. + +See the project main page for details on the project and installation steps. */ + +void QGLViewer::defaultConstructor() +{ + // Test OpenGL context + // if (glGetString(GL_VERSION) == 0) + // qWarning("Unable to get OpenGL version, context may not be available - Check your configuration"); + + std::cout << "OpenGL version: " << format().majorVersion() + << "." << format().minorVersion() << std::endl; + + int poolIndex = QGLViewer::QGLViewerPool_.indexOf(NULL); + setFocusPolicy(Qt::StrongFocus); + + if (poolIndex >= 0) + QGLViewer::QGLViewerPool_.replace(poolIndex, this); + else + QGLViewer::QGLViewerPool_.append(this); + + camera_ = new Camera(); + setCamera(camera()); + + setDefaultShortcuts(); + setDefaultMouseBindings(); + + setSnapshotFileName(tr("snapshot", "Default snapshot file name")); + initializeSnapshotFormats(); + setSnapshotCounter(0); + setSnapshotQuality(95); + + fpsTime_.start(); + fpsCounter_ = 0; + f_p_s_ = 0.0; + fpsString_ = tr("%1Hz", "Frames per seconds, in Hertz").arg("?"); + visualHint_ = 0; + previousPathId_ = 0; + // prevPos_ is not initialized since pos() is not meaningful here. + // It will be set when setFullScreen(false) is called after setFullScreen(true) + + // #CONNECTION# default values in initFromDOMElement() + manipulatedFrame_ = NULL; + manipulatedFrameIsACamera_ = false; + mouseGrabberIsAManipulatedFrame_ = false; + mouseGrabberIsAManipulatedCameraFrame_ = false; + displayMessage_ = false; + connect(&messageTimer_, SIGNAL(timeout()), SLOT(hideMessage())); + messageTimer_.setSingleShot(true); + helpWidget_ = NULL; + setMouseGrabber(NULL); + + setSceneRadius(1.0); + showEntireScene(); + setStateFileName(".qglviewer.xml"); + + // #CONNECTION# default values in initFromDOMElement() + setAxisIsDrawn(false); + setGridIsDrawn(false); + setFPSIsDisplayed(false); + setCameraIsEdited(false); + setTextIsEnabled(true); + setStereoDisplay(false); + // Make sure move() is not called, which would call initializeGL() + fullScreen_ = false; + setFullScreen(false); + + animationTimerId_ = 0; + stopAnimation(); + setAnimationPeriod(40); // 25Hz + + selectBuffer_ = NULL; + + bufferTextureId_ = 0; + bufferTextureMaxU_ = 0.0; + bufferTextureMaxV_ = 0.0; + bufferTextureWidth_ = 0; + bufferTextureHeight_ = 0; + previousBufferTextureFormat_ = 0; + previousBufferTextureInternalFormat_ = 0; + currentlyPressedKey_ = Qt::Key(0); + + setAttribute(Qt::WA_NoSystemBackground); + + tileRegion_ = NULL; +} + +#if !defined QT3_SUPPORT +/*! Constructor. See \c QGLWidget documentation for details. + +All viewer parameters (display flags, scene parameters, associated objects...) are set to their default values. See +the associated documentation. + +If the \p shareWidget parameter points to a valid \c QGLWidget, the QGLViewer will share the OpenGL +context with \p shareWidget (see isSharing()). */ +QGLViewer::QGLViewer(QWidget* parent, Qt::WindowFlags flags) + : QOpenGLWidget(parent, flags) +{ defaultConstructor(); } +#endif // QT3_SUPPORT + +/*! Virtual destructor. + +The viewer is replaced by \c NULL in the QGLViewerPool() (in order to preserve other viewer's indexes) and allocated +memory is released. The camera() is deleted and should be copied before if it is shared by an other viewer. */ +QGLViewer::~QGLViewer() +{ + // See closeEvent comment. Destructor is called (and not closeEvent) only when the widget is embedded. + // Hence we saveToFile here. It is however a bad idea if virtual domElement() has been overloaded ! + // if (parent()) + // saveStateToFileForAllViewers(); + + QGLViewer::QGLViewerPool_.replace(QGLViewer::QGLViewerPool_.indexOf(this), NULL); + + delete camera(); + delete[] selectBuffer_; + if (helpWidget()) + { + // Needed for Qt 4 which has no main widget. + helpWidget()->close(); + delete helpWidget_; + } +} + + +static QString QGLViewerVersionString() +{ + return QString::number((QGLVIEWER_VERSION & 0xff0000) >> 16) + "." + + QString::number((QGLVIEWER_VERSION & 0x00ff00) >> 8) + "." + + QString::number(QGLVIEWER_VERSION & 0x0000ff); +} + +static Qt::KeyboardModifiers keyboardModifiersFromState(unsigned int state) { + // Convertion of keyboard modifiers and mouse buttons as an int is no longer supported : emulate + return Qt::KeyboardModifiers(int(state & 0xFF000000)); +} + + +static Qt::MouseButton mouseButtonFromState(unsigned int state) { + // Convertion of keyboard modifiers and mouse buttons as an int is no longer supported : emulate + return Qt::MouseButton(state & 0xFFFF); +} + +/*! Initializes the QGLViewer OpenGL context and then calls user-defined init(). + +This method is automatically called once, before the first call to paintGL(). + +Overload init() instead of this method to modify viewer specific OpenGL state or to create display +lists. + +To make beginners' life easier and to simplify the examples, this method slightly modifies the +standard OpenGL state: +\code +glEnable(GL_LIGHT0); +glEnable(GL_LIGHTING); +glEnable(GL_DEPTH_TEST); +glEnable(GL_COLOR_MATERIAL); +\endcode + +If you port an existing application to QGLViewer and your display changes, you probably want to +disable these flags in init() to get back to a standard OpenGL state. */ +void QGLViewer::initializeGL() +{ + glEnable(GL_DEPTH_TEST); + + // Default colors + setForegroundColor(QColor(180, 180, 180)); + setBackgroundColor(QColor(51, 51, 51)); + + // Clear the buffer where we're going to draw + if (format().stereo()) + { + glDrawBuffer(GL_BACK_RIGHT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glDrawBuffer(GL_BACK_LEFT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + else + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // Calls user defined method. Default emits a signal. + init(); + + // Give time to glInit to finish and then call setFullScreen(). + if (isFullScreen()) + QTimer::singleShot( 100, this, SLOT(delayedFullScreen()) ); +} + +/*! Main paint method, inherited from \c QGLWidget. + +Calls the following methods, in that order: +\arg preDraw() (or preDrawStereo() if viewer displaysInStereo()) : places the camera in the world coordinate system. +\arg draw() (or fastDraw() when the camera is manipulated) : main drawing method. Should be overloaded. +\arg postDraw() : display of visual hints (world axis, FPS...) */ +void QGLViewer::paintGL() +{ + if (displaysInStereo()) + { + for (int view=1; view>=0; --view) + { + // Clears screen, set model view matrix with shifted matrix for ith buffer + preDrawStereo(view); + // Used defined method. Default is empty + if (camera()->frame()->isManipulated()) + fastDraw(); + else + draw(); + postDraw(); + } + } + else + { + // Clears screen, set model view matrix... + preDraw(); + // Used defined method. Default calls draw() + if (camera()->frame()->isManipulated()) + fastDraw(); + else + draw(); + // Add visual hints: axis, camera, grid... + postDraw(); + } + Q_EMIT drawFinished(true); +} + +/*! Sets OpenGL state before draw(). + +Default behavior clears screen and sets the projection and modelView matrices: +\code +glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + +camera()->loadProjectionMatrix(); +camera()->loadModelViewMatrix(); +\endcode + +Emits the drawNeeded() signal once this is done (see the callback example). */ +void QGLViewer::preDraw() +{ + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + // GL_PROJECTION matrix + camera()->loadProjectionMatrix(); + // GL_MODELVIEW matrix + camera()->loadModelViewMatrix(); + + Q_EMIT drawNeeded(); +} + +/*! Called after draw() to draw viewer visual hints. + +Default implementation displays axis, grid, FPS... when the respective flags are sets. + +See the multiSelect and thumbnail examples for an overloading illustration. + +The GLContext (color, LIGHTING, BLEND...) is \e not modified by this method, so that in +draw(), the user can rely on the OpenGL context he defined. Respect this convention (by pushing/popping the +different attributes) if you overload this method. */ +void QGLViewer::postDraw() +{ + // Reset model view matrix to world coordinates origin + camera()->loadModelViewMatrix(); + // TODO restore model loadProjectionMatrixStereo + + // FPS computation + const unsigned int maxCounter = 20; + if (++fpsCounter_ == maxCounter) + { + f_p_s_ = 1000.0 * maxCounter / fpsTime_.restart(); + fpsString_ = tr("%1Hz", "Frames per seconds, in Hertz").arg(f_p_s_, 0, 'f', ((f_p_s_ < 10.0)?1:0)); + fpsCounter_ = 0; + } + + bool depthTestWasEnabled = glIsEnabled(GL_DEPTH_TEST); + glDisable(GL_DEPTH_TEST); + + // Restore GL state + if (depthTestWasEnabled) + glEnable(GL_DEPTH_TEST); +} + +/*! Called before draw() (instead of preDraw()) when viewer displaysInStereo(). + +Same as preDraw() except that the glDrawBuffer() is set to \c GL_BACK_LEFT or \c GL_BACK_RIGHT +depending on \p leftBuffer, and it uses qglviewer::Camera::loadProjectionMatrixStereo() and +qglviewer::Camera::loadModelViewMatrixStereo() instead. */ +void QGLViewer::preDrawStereo(bool leftBuffer) +{ + // Set buffer to draw in + // Seems that SGI and Crystal Eyes are not synchronized correctly ! + // That's why we don't draw in the appropriate buffer... + if (!leftBuffer) + glDrawBuffer(GL_BACK_LEFT); + else + glDrawBuffer(GL_BACK_RIGHT); + + // Clear the buffer where we're going to draw + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + // GL_PROJECTION matrix + //camera()->loadProjectionMatrixStereo(leftBuffer); + // GL_MODELVIEW matrix + camera()->loadModelViewMatrixStereo(leftBuffer); + + Q_EMIT drawNeeded(); +} + +/*! Draws a simplified version of the scene to guarantee interactive camera displacements. + +This method is called instead of draw() when the qglviewer::Camera::frame() is +qglviewer::ManipulatedCameraFrame::isManipulated(). Default implementation simply calls draw(). + +Overload this method if your scene is too complex to allow for interactive camera manipulation. See +the fastDraw example for an illustration. */ +void QGLViewer::fastDraw() +{ + draw(); +} + +/*! Starts (\p edit = \c true, default) or stops (\p edit=\c false) the edition of the camera(). + +Current implementation is limited to paths display. Get current state using cameraIsEdited(). + +\attention This method sets the qglviewer::Camera::zClippingCoefficient() to 5.0 when \p edit is \c +true, so that the Camera paths (see qglviewer::Camera::keyFrameInterpolator()) are not clipped. It +restores the previous value when \p edit is \c false. */ +void QGLViewer::setCameraIsEdited(bool edit) +{ + cameraIsEdited_ = edit; + if (edit) + { + previousCameraZClippingCoefficient_ = camera()->zClippingCoefficient(); + // #CONNECTION# 5.0 also used in domElement() and in initFromDOMElement(). + camera()->setZClippingCoefficient(5.0); + } + else + camera()->setZClippingCoefficient(previousCameraZClippingCoefficient_); + + Q_EMIT cameraIsEditedChanged(edit); + + update(); +} + +// Key bindings. 0 means not defined +void QGLViewer::setDefaultShortcuts() +{ + // D e f a u l t a c c e l e r a t o r s + setShortcut(DRAW_AXIS, Qt::Key_A); + setShortcut(DRAW_GRID, Qt::Key_G); + setShortcut(DISPLAY_FPS, Qt::Key_F); + setShortcut(ENABLE_TEXT, Qt::SHIFT+Qt::Key_Question); + setShortcut(EXIT_VIEWER, Qt::Key_Escape); + setShortcut(SAVE_SCREENSHOT, Qt::CTRL+Qt::Key_S); + setShortcut(CAMERA_MODE, Qt::Key_Space); + setShortcut(FULL_SCREEN, Qt::ALT+Qt::Key_Return); + setShortcut(STEREO, Qt::Key_S); + setShortcut(ANIMATION, Qt::Key_Return); + setShortcut(HELP, Qt::Key_H); + setShortcut(EDIT_CAMERA, Qt::Key_C); + setShortcut(MOVE_CAMERA_LEFT, Qt::Key_Left); + setShortcut(MOVE_CAMERA_RIGHT,Qt::Key_Right); + setShortcut(MOVE_CAMERA_UP, Qt::Key_Up); + setShortcut(MOVE_CAMERA_DOWN, Qt::Key_Down); + setShortcut(INCREASE_FLYSPEED, Qt::Key_Plus); + setShortcut(DECREASE_FLYSPEED, Qt::Key_Minus); + setShortcut(SNAPSHOT_TO_CLIPBOARD, Qt::CTRL+Qt::Key_C); + + keyboardActionDescription_[DISPLAY_FPS] = tr("Toggles the display of the FPS", "DISPLAY_FPS action description"); + keyboardActionDescription_[SAVE_SCREENSHOT] = tr("Saves a screenshot", "SAVE_SCREENSHOT action description"); + keyboardActionDescription_[FULL_SCREEN] = tr("Toggles full screen display", "FULL_SCREEN action description"); + keyboardActionDescription_[DRAW_AXIS] = tr("Toggles the display of the world axis", "DRAW_AXIS action description"); + keyboardActionDescription_[DRAW_GRID] = tr("Toggles the display of the XY grid", "DRAW_GRID action description"); + keyboardActionDescription_[CAMERA_MODE] = tr("Changes camera mode (observe or fly)", "CAMERA_MODE action description"); + keyboardActionDescription_[STEREO] = tr("Toggles stereo display", "STEREO action description"); + keyboardActionDescription_[HELP] = tr("Opens this help window", "HELP action description"); + keyboardActionDescription_[ANIMATION] = tr("Starts/stops the animation", "ANIMATION action description"); + keyboardActionDescription_[EDIT_CAMERA] = tr("Toggles camera paths display", "EDIT_CAMERA action description"); // TODO change + keyboardActionDescription_[ENABLE_TEXT] = tr("Toggles the display of the text", "ENABLE_TEXT action description"); + keyboardActionDescription_[EXIT_VIEWER] = tr("Exits program", "EXIT_VIEWER action description"); + keyboardActionDescription_[MOVE_CAMERA_LEFT] = tr("Moves camera left", "MOVE_CAMERA_LEFT action description"); + keyboardActionDescription_[MOVE_CAMERA_RIGHT] = tr("Moves camera right", "MOVE_CAMERA_RIGHT action description"); + keyboardActionDescription_[MOVE_CAMERA_UP] = tr("Moves camera up", "MOVE_CAMERA_UP action description"); + keyboardActionDescription_[MOVE_CAMERA_DOWN] = tr("Moves camera down", "MOVE_CAMERA_DOWN action description"); + keyboardActionDescription_[INCREASE_FLYSPEED] = tr("Increases fly speed", "INCREASE_FLYSPEED action description"); + keyboardActionDescription_[DECREASE_FLYSPEED] = tr("Decreases fly speed", "DECREASE_FLYSPEED action description"); + keyboardActionDescription_[SNAPSHOT_TO_CLIPBOARD] = tr("Copies a snapshot to clipboard", "SNAPSHOT_TO_CLIPBOARD action description"); + + // K e y f r a m e s s h o r t c u t k e y s + setPathKey(Qt::Key_F1, 1); + setPathKey(Qt::Key_F2, 2); + setPathKey(Qt::Key_F3, 3); + setPathKey(Qt::Key_F4, 4); + setPathKey(Qt::Key_F5, 5); + setPathKey(Qt::Key_F6, 6); + setPathKey(Qt::Key_F7, 7); + setPathKey(Qt::Key_F8, 8); + setPathKey(Qt::Key_F9, 9); + setPathKey(Qt::Key_F10, 10); + setPathKey(Qt::Key_F11, 11); + setPathKey(Qt::Key_F12, 12); + + setAddKeyFrameKeyboardModifiers(Qt::AltModifier); + setPlayPathKeyboardModifiers(Qt::NoModifier); +} + +// M o u s e b e h a v i o r +void QGLViewer::setDefaultMouseBindings() +{ + const Qt::KeyboardModifiers cameraKeyboardModifiers = Qt::NoModifier; + const Qt::KeyboardModifiers frameKeyboardModifiers = Qt::ControlModifier; + + //#CONNECTION# toggleCameraMode() + for (int handler=0; handler<2; ++handler) + { + MouseHandler mh = (MouseHandler)(handler); + Qt::KeyboardModifiers modifiers = (mh == FRAME) ? frameKeyboardModifiers : cameraKeyboardModifiers; + + setMouseBinding(modifiers, Qt::LeftButton, mh, ROTATE); + setMouseBinding(modifiers, Qt::MidButton, mh, ZOOM); + setMouseBinding(modifiers, Qt::RightButton, mh, TRANSLATE); + + setMouseBinding(Qt::Key_R, modifiers, Qt::LeftButton, mh, SCREEN_ROTATE); + + setWheelBinding(modifiers, mh, ZOOM); + } + + // Z o o m o n r e g i o n + setMouseBinding(Qt::ShiftModifier, Qt::MidButton, CAMERA, ZOOM_ON_REGION); + + // S e l e c t + setMouseBinding(Qt::ShiftModifier, Qt::LeftButton, SELECT); + + setMouseBinding(Qt::ShiftModifier, Qt::RightButton, RAP_FROM_PIXEL); + // D o u b l e c l i c k + setMouseBinding(Qt::NoModifier, Qt::LeftButton, ALIGN_CAMERA, true); + setMouseBinding(Qt::NoModifier, Qt::MidButton, SHOW_ENTIRE_SCENE, true); + setMouseBinding(Qt::NoModifier, Qt::RightButton, CENTER_SCENE, true); + + setMouseBinding(frameKeyboardModifiers, Qt::LeftButton, ALIGN_FRAME, true); + // middle double click makes no sense for manipulated frame + setMouseBinding(frameKeyboardModifiers, Qt::RightButton, CENTER_FRAME, true); + + // A c t i o n s w i t h k e y m o d i f i e r s + setMouseBinding(Qt::Key_Z, Qt::NoModifier, Qt::LeftButton, ZOOM_ON_PIXEL); + setMouseBinding(Qt::Key_Z, Qt::NoModifier, Qt::RightButton, ZOOM_TO_FIT); + +#ifdef Q_OS_MAC + // Specific Mac bindings for touchpads. Two fingers emulate a wheelEvent which zooms. + // There is no right button available : make Option key + left emulate the right button. + // A Control+Left indeed emulates a right click (OS X system configuration), but it does + // no seem to support dragging. + // Done at the end to override previous settings. + const Qt::KeyboardModifiers macKeyboardModifiers = Qt::AltModifier; + + setMouseBinding(macKeyboardModifiers, Qt::LeftButton, CAMERA, TRANSLATE); + setMouseBinding(macKeyboardModifiers, Qt::LeftButton, CENTER_SCENE, true); + setMouseBinding(frameKeyboardModifiers | macKeyboardModifiers, Qt::LeftButton, CENTER_FRAME, true); + setMouseBinding(frameKeyboardModifiers | macKeyboardModifiers, Qt::LeftButton, FRAME, TRANSLATE); +#endif +} + +/*! Associates a new qglviewer::Camera to the viewer. + +You should only use this method when you derive a new class from qglviewer::Camera and want to use +one of its instances instead of the original class. + +It you simply want to save and restore Camera positions, use qglviewer::Camera::addKeyFrameToPath() +and qglviewer::Camera::playPath() instead. + +This method silently ignores \c NULL \p camera pointers. The calling method is responsible for deleting +the previous camera pointer in order to prevent memory leaks if needed. + +The sceneRadius() and sceneCenter() of \p camera are set to the \e current QGLViewer values. + +All the \p camera qglviewer::Camera::keyFrameInterpolator() +qglviewer::KeyFrameInterpolator::interpolated() signals are connected to the viewer update() slot. +The connections with the previous viewer's camera are removed. */ +void QGLViewer::setCamera(Camera* const camera) +{ + if (!camera) + return; + + camera->setSceneRadius(sceneRadius()); + camera->setSceneCenter(sceneCenter()); + camera->setScreenWidthAndHeight(width(), height()); + + // Disconnect current camera from this viewer. + disconnect(this->camera()->frame(), SIGNAL(manipulated()), this, SLOT(update())); + disconnect(this->camera()->frame(), SIGNAL(spun()), this, SLOT(update())); + + // Connect camera frame to this viewer. + connect(camera->frame(), SIGNAL(manipulated()), SLOT(update())); + connect(camera->frame(), SIGNAL(spun()), SLOT(update())); + + connectAllCameraKFIInterpolatedSignals(false); + camera_ = camera; + connectAllCameraKFIInterpolatedSignals(); + + previousCameraZClippingCoefficient_ = this->camera()->zClippingCoefficient(); +} + +void QGLViewer::connectAllCameraKFIInterpolatedSignals(bool connection) +{ + for (QMap::ConstIterator it = camera()->kfi_.begin(), end=camera()->kfi_.end(); it != end; ++it) + { + if (connection) + connect(camera()->keyFrameInterpolator(it.key()), SIGNAL(interpolated()), SLOT(update())); + else + disconnect(camera()->keyFrameInterpolator(it.key()), SIGNAL(interpolated()), this, SLOT(update())); + } + + if (connection) + connect(camera()->interpolationKfi_, SIGNAL(interpolated()), SLOT(update())); + else + disconnect(camera()->interpolationKfi_, SIGNAL(interpolated()), this, SLOT(update())); +} + +/*! Briefly displays a message in the lower left corner of the widget. Convenient to provide +feedback to the user. + +\p message is displayed during \p delay milliseconds (default is 2 seconds) using drawText(). + +This method should not be called in draw(). If you want to display a text in each draw(), use +drawText() instead. + +If this method is called when a message is already displayed, the new message replaces the old one. +Use setTextIsEnabled() (default shortcut is '?') to enable or disable text (and hence messages) +display. */ +void QGLViewer::displayMessage(const QString& message, int delay) +{ + message_ = message; + displayMessage_ = true; + // Was set to single shot in defaultConstructor. + messageTimer_.start(delay); + if (textIsEnabled()) + update(); +} + +void QGLViewer::hideMessage() +{ + displayMessage_ = false; + if (textIsEnabled()) + update(); +} + + +/*! Overloading of the \c QObject method. + +If animationIsStarted(), calls animate() and draw(). */ +void QGLViewer::timerEvent(QTimerEvent *) +{ + if (animationIsStarted()) + { + animate(); + update(); + } +} + +/*! Starts the animation loop. See animationIsStarted(). */ +void QGLViewer::startAnimation() +{ + animationTimerId_ = startTimer(animationPeriod()); + animationStarted_ = true; +} + +/*! Stops animation. See animationIsStarted(). */ +void QGLViewer::stopAnimation() +{ + animationStarted_ = false; + if (animationTimerId_ != 0) + killTimer(animationTimerId_); +} + +/*! Overloading of the \c QWidget method. + +Saves the viewer state using saveStateToFile() and then calls QGLWidget::closeEvent(). */ +void QGLViewer::closeEvent(QCloseEvent *e) +{ + // When the user clicks on the window close (x) button: + // - If the viewer is a top level window, closeEvent is called and then saves to file. + // - Otherwise, nothing happen s:( + // When the user press the EXIT_VIEWER keyboard shortcut: + // - If the viewer is a top level window, saveStateToFile() is also called + // - Otherwise, closeEvent is NOT called and keyPressEvent does the job. + + /* After tests: + E : Embedded widget + N : Widget created with new + C : closeEvent called + D : destructor called + + E N C D + y y + y n y + n y y + n n y y + + closeEvent is called iif the widget is NOT embedded. + + Destructor is called iif the widget is created on the stack + or if widget (resp. parent if embedded) is created with WDestructiveClose flag. + + closeEvent always before destructor. + + Close using qApp->closeAllWindows or (x) is identical. + */ + + // #CONNECTION# Also done for EXIT_VIEWER in keyPressEvent(). + saveStateToFile(); + QOpenGLWidget::closeEvent(e); +} + +static QString mouseButtonsString(Qt::MouseButtons b) +{ + QString result(""); + bool addAmpersand = false; + if (b & Qt::LeftButton) { result += QGLViewer::tr("Left", "left mouse button"); addAmpersand=true; } + if (b & Qt::MidButton) { if (addAmpersand) result += " & "; result += QGLViewer::tr("Middle", "middle mouse button"); addAmpersand=true; } + if (b & Qt::RightButton) { if (addAmpersand) result += " & "; result += QGLViewer::tr("Right", "right mouse button"); } + return result; +} + +void QGLViewer::performClickAction(ClickAction ca, const QMouseEvent* const e) +{ + // Note: action that need it should call update(). + switch (ca) + { + // # CONNECTION setMouseBinding prevents adding NO_CLICK_ACTION in clickBinding_ + // This case should hence not be possible. Prevents unused case warning. + case NO_CLICK_ACTION : + break; + case ZOOM_ON_PIXEL : + camera()->interpolateToZoomOnPixel(e->pos()); + break; + case ZOOM_TO_FIT : + camera()->interpolateToFitScene(); + break; + case SELECT : + //select(e); DEPRECATED! + update(); + break; + case RAP_FROM_PIXEL : + if (! camera()->setPivotPointFromPixel(e->pos())) + camera()->setPivotPoint(sceneCenter()); + setVisualHintsMask(1); + update(); + break; + case RAP_IS_CENTER : + camera()->setPivotPoint(sceneCenter()); + setVisualHintsMask(1); + update(); + break; + case CENTER_FRAME : + if (manipulatedFrame()) + manipulatedFrame()->projectOnLine(camera()->position(), camera()->viewDirection()); + break; + case CENTER_SCENE : + camera()->centerScene(); + break; + case SHOW_ENTIRE_SCENE : + camera()->showEntireScene(); + break; + case ALIGN_FRAME : + if (manipulatedFrame()) + manipulatedFrame()->alignWithFrame(camera()->frame()); + break; + case ALIGN_CAMERA : + Frame * frame = new Frame(); + frame->setTranslation(camera()->pivotPoint()); + camera()->frame()->alignWithFrame(frame, true); + delete frame; + break; + } +} + +/*! Overloading of the \c QWidget method. + +When the user clicks on the mouse: +\arg if a mouseGrabber() is defined, qglviewer::MouseGrabber::mousePressEvent() is called, +\arg otherwise, the camera() or the manipulatedFrame() interprets the mouse displacements, +depending on mouse bindings. + +Mouse bindings customization can be achieved using setMouseBinding() and setWheelBinding(). See the +mouse page for a complete description of mouse bindings. + +See the mouseMoveEvent() documentation for an example of more complex mouse behavior customization +using overloading. + +\note When the mouseGrabber() is a manipulatedFrame(), the modifier keys are not taken into +account. This allows for a direct manipulation of the manipulatedFrame() when the mouse hovers, +which is probably what is expected. */ +void QGLViewer::mousePressEvent(QMouseEvent* e) +{ + //#CONNECTION# mouseDoubleClickEvent has the same structure + //#CONNECTION# mouseString() concatenates bindings description in inverse order. + ClickBindingPrivate cbp(e->modifiers(), e->button(), false, (Qt::MouseButtons)(e->buttons() & ~(e->button())), currentlyPressedKey_); + + if (clickBinding_.contains(cbp)) { + performClickAction(clickBinding_[cbp], e); + } else + if (mouseGrabber()) + { + if (mouseGrabberIsAManipulatedFrame_) + { + for (QMap::ConstIterator it=mouseBinding_.begin(), end=mouseBinding_.end(); it!=end; ++it) + if ((it.value().handler == FRAME) && (it.key().button == e->button())) + { + ManipulatedFrame* mf = dynamic_cast(mouseGrabber()); + if (mouseGrabberIsAManipulatedCameraFrame_) + { + mf->ManipulatedFrame::startAction(it.value().action, it.value().withConstraint); + mf->ManipulatedFrame::mousePressEvent(e, camera()); + } + else + { + mf->startAction(it.value().action, it.value().withConstraint); + mf->mousePressEvent(e, camera()); + } + break; + } + } + else + mouseGrabber()->mousePressEvent(e, camera()); + update(); + } + else + { + //#CONNECTION# wheelEvent has the same structure + const MouseBindingPrivate mbp(e->modifiers(), e->button(), currentlyPressedKey_); + + if (mouseBinding_.contains(mbp)) + { + MouseActionPrivate map = mouseBinding_[mbp]; + switch (map.handler) + { + case CAMERA : + camera()->frame()->startAction(map.action, map.withConstraint); + camera()->frame()->mousePressEvent(e, camera()); + break; + case FRAME : + if (manipulatedFrame()) + { + if (manipulatedFrameIsACamera_) + { + manipulatedFrame()->ManipulatedFrame::startAction(map.action, map.withConstraint); + manipulatedFrame()->ManipulatedFrame::mousePressEvent(e, camera()); + } + else + { + manipulatedFrame()->startAction(map.action, map.withConstraint); + manipulatedFrame()->mousePressEvent(e, camera()); + } + } + break; + } + if (map.action == SCREEN_ROTATE) + // Display visual hint line + update(); + } + else + e->ignore(); + } +} + +/*! Overloading of the \c QWidget method. + +Mouse move event is sent to the mouseGrabber() (if any) or to the camera() or the +manipulatedFrame(), depending on mouse bindings (see setMouseBinding()). + +If you want to define your own mouse behavior, do something like this: +\code +void Viewer::mousePressEvent(QMouseEvent* e) +{ + +if ((e->button() == myButton) && (e->modifiers() == myModifiers)) + myMouseBehavior = true; +else + QGLViewer::mousePressEvent(e); +} + +void Viewer::mouseMoveEvent(QMouseEvent *e) +{ +if (myMouseBehavior) + // Use e->x() and e->y() as you want... +else + QGLViewer::mouseMoveEvent(e); +} + +void Viewer::mouseReleaseEvent(QMouseEvent* e) +{ +if (myMouseBehavior) + myMouseBehavior = false; +else + QGLViewer::mouseReleaseEvent(e); +} +\endcode */ +void QGLViewer::mouseMoveEvent(QMouseEvent* e) +{ + if (mouseGrabber()) + { + mouseGrabber()->checkIfGrabsMouse(e->x(), e->y(), camera()); + if (mouseGrabber()->grabsMouse()) + if (mouseGrabberIsAManipulatedCameraFrame_) + (dynamic_cast(mouseGrabber()))->ManipulatedFrame::mouseMoveEvent(e, camera()); + else + mouseGrabber()->mouseMoveEvent(e, camera()); + else + setMouseGrabber(NULL); + update(); + } + + if (!mouseGrabber()) + { + //#CONNECTION# mouseReleaseEvent has the same structure + if (camera()->frame()->isManipulated()) + { + camera()->frame()->mouseMoveEvent(e, camera()); + // #CONNECTION# manipulatedCameraFrame::mouseMoveEvent specific if at the beginning + if (camera()->frame()->action_ == ZOOM_ON_REGION) + update(); + } + else // ! + if ((manipulatedFrame()) && (manipulatedFrame()->isManipulated())) + if (manipulatedFrameIsACamera_) + manipulatedFrame()->ManipulatedFrame::mouseMoveEvent(e, camera()); + else + manipulatedFrame()->mouseMoveEvent(e, camera()); + else + if (hasMouseTracking()) + { + Q_FOREACH (MouseGrabber* mg, MouseGrabber::MouseGrabberPool()) + { + mg->checkIfGrabsMouse(e->x(), e->y(), camera()); + if (mg->grabsMouse()) + { + setMouseGrabber(mg); + // Check that MouseGrabber is not disabled + if (mouseGrabber() == mg) + { + update(); + break; + } + } + } + } + } +} + +/*! Overloading of the \c QWidget method. + +Calls the mouseGrabber(), camera() or manipulatedFrame \c mouseReleaseEvent method. + +See the mouseMoveEvent() documentation for an example of mouse behavior customization. */ +void QGLViewer::mouseReleaseEvent(QMouseEvent* e) +{ + if (mouseGrabber()) + { + if (mouseGrabberIsAManipulatedCameraFrame_) + (dynamic_cast(mouseGrabber()))->ManipulatedFrame::mouseReleaseEvent(e, camera()); + else + mouseGrabber()->mouseReleaseEvent(e, camera()); + mouseGrabber()->checkIfGrabsMouse(e->x(), e->y(), camera()); + if (!(mouseGrabber()->grabsMouse())) + setMouseGrabber(NULL); + // update(); + } + else + //#CONNECTION# mouseMoveEvent has the same structure + if (camera()->frame()->isManipulated()) + { + camera()->frame()->mouseReleaseEvent(e, camera()); + } + else + if ((manipulatedFrame()) && (manipulatedFrame()->isManipulated())) + { + if (manipulatedFrameIsACamera_) + manipulatedFrame()->ManipulatedFrame::mouseReleaseEvent(e, camera()); + else + manipulatedFrame()->mouseReleaseEvent(e, camera()); + } + else + e->ignore(); + + // Not absolutely needed (see above commented code for the optimal version), but may reveal + // useful for specific applications. + update(); +} + +/*! Overloading of the \c QWidget method. + +If defined, the wheel event is sent to the mouseGrabber(). It is otherwise sent according to wheel +bindings (see setWheelBinding()). */ +void QGLViewer::wheelEvent(QWheelEvent* e) +{ + if (mouseGrabber()) + { + if (mouseGrabberIsAManipulatedFrame_) + { + for (QMap::ConstIterator it=wheelBinding_.begin(), end=wheelBinding_.end(); it!=end; ++it) + if (it.value().handler == FRAME) + { + ManipulatedFrame* mf = dynamic_cast(mouseGrabber()); + if (mouseGrabberIsAManipulatedCameraFrame_) + { + mf->ManipulatedFrame::startAction(it.value().action, it.value().withConstraint); + mf->ManipulatedFrame::wheelEvent(e, camera()); + } + else + { + mf->startAction(it.value().action, it.value().withConstraint); + mf->wheelEvent(e, camera()); + } + break; + } + } + else + mouseGrabber()->wheelEvent(e, camera()); + update(); + } + else + { + //#CONNECTION# mousePressEvent has the same structure + WheelBindingPrivate wbp(e->modifiers(), currentlyPressedKey_); + + if (wheelBinding_.contains(wbp)) + { + MouseActionPrivate map = wheelBinding_[wbp]; + switch (map.handler) + { + case CAMERA : + camera()->frame()->startAction(map.action, map.withConstraint); + camera()->frame()->wheelEvent(e, camera()); + break; + case FRAME : + if (manipulatedFrame()) { + if (manipulatedFrameIsACamera_) + { + manipulatedFrame()->ManipulatedFrame::startAction(map.action, map.withConstraint); + manipulatedFrame()->ManipulatedFrame::wheelEvent(e, camera()); + } + else + { + manipulatedFrame()->startAction(map.action, map.withConstraint); + manipulatedFrame()->wheelEvent(e, camera()); + } + } + break; + } + } + else + e->ignore(); + } +} + +/*! Overloading of the \c QWidget method. + +The behavior of the mouse double click depends on the mouse binding. See setMouseBinding() and the +mouse page. */ +void QGLViewer::mouseDoubleClickEvent(QMouseEvent* e) +{ + //#CONNECTION# mousePressEvent has the same structure + ClickBindingPrivate cbp(e->modifiers(), e->button(), true, (Qt::MouseButtons)(e->buttons() & ~(e->button())), currentlyPressedKey_); + if (clickBinding_.contains(cbp)) + performClickAction(clickBinding_[cbp], e); + else + if (mouseGrabber()) + mouseGrabber()->mouseDoubleClickEvent(e, camera()); + else + e->ignore(); +} + +/*! Sets the state of displaysInStereo(). See also toggleStereoDisplay(). + +First checks that the display is able to handle stereovision using QGLWidget::format(). Opens a +warning message box in case of failure. Emits the stereoChanged() signal otherwise. */ +void QGLViewer::setStereoDisplay(bool stereo) +{ + if (format().stereo()) + { + stereo_ = stereo; + if (!displaysInStereo()) + { + glDrawBuffer(GL_BACK_LEFT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glDrawBuffer(GL_BACK_RIGHT); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } + + Q_EMIT stereoChanged(stereo_); + + update(); + } + else + if (stereo) + QMessageBox::warning(this, tr("Stereo not supported", "Message box window title"), tr("Stereo is not supported on this display.")); + else + stereo_ = false; +} + +/*! Sets the isFullScreen() state. + +If the QGLViewer is embedded in an other QWidget (see QWidget::topLevelWidget()), this widget is +displayed in full screen instead. */ +void QGLViewer::setFullScreen(bool fullScreen) +{ + if (fullScreen_ == fullScreen) return; + + fullScreen_ = fullScreen; + + QWidget* tlw = topLevelWidget(); + + if (isFullScreen()) + { + prevPos_ = topLevelWidget()->pos(); + tlw->showFullScreen(); + tlw->move(0,0); + } + else + { + tlw->showNormal(); + tlw->move(prevPos_); + } +} + +/*! Directly defines the mouseGrabber(). + +You should not call this method directly as it bypasses the +qglviewer::MouseGrabber::checkIfGrabsMouse() test performed by mouseMoveEvent(). + +If the MouseGrabber is disabled (see mouseGrabberIsEnabled()), this method silently does nothing. */ +void QGLViewer::setMouseGrabber(MouseGrabber* mouseGrabber) +{ + if (!mouseGrabberIsEnabled(mouseGrabber)) + return; + + mouseGrabber_ = mouseGrabber; + + mouseGrabberIsAManipulatedFrame_ = (dynamic_cast(mouseGrabber) != NULL); + mouseGrabberIsAManipulatedCameraFrame_ = ((dynamic_cast(mouseGrabber) != NULL) && + (mouseGrabber != camera()->frame())); + Q_EMIT mouseGrabberChanged(mouseGrabber); +} + +/*! Sets the mouseGrabberIsEnabled() state. */ +void QGLViewer::setMouseGrabberIsEnabled(const qglviewer::MouseGrabber* const mouseGrabber, bool enabled) +{ + if (enabled) + disabledMouseGrabbers_.remove(reinterpret_cast(mouseGrabber)); + else + disabledMouseGrabbers_[reinterpret_cast(mouseGrabber)]; +} + +QString QGLViewer::mouseActionString(QGLViewer::MouseAction ma) +{ + switch (ma) + { + case QGLViewer::NO_MOUSE_ACTION : return QString::null; + case QGLViewer::ROTATE : return QGLViewer::tr("Rotates", "ROTATE mouse action"); + case QGLViewer::ZOOM : return QGLViewer::tr("Zooms", "ZOOM mouse action"); + case QGLViewer::TRANSLATE : return QGLViewer::tr("Translates", "TRANSLATE mouse action"); + case QGLViewer::MOVE_FORWARD : return QGLViewer::tr("Moves forward", "MOVE_FORWARD mouse action"); + case QGLViewer::LOOK_AROUND : return QGLViewer::tr("Looks around", "LOOK_AROUND mouse action"); + case QGLViewer::MOVE_BACKWARD : return QGLViewer::tr("Moves backward", "MOVE_BACKWARD mouse action"); + case QGLViewer::SCREEN_ROTATE : return QGLViewer::tr("Rotates in screen plane", "SCREEN_ROTATE mouse action"); + case QGLViewer::ROLL : return QGLViewer::tr("Rolls", "ROLL mouse action"); + case QGLViewer::DRIVE : return QGLViewer::tr("Drives", "DRIVE mouse action"); + case QGLViewer::SCREEN_TRANSLATE : return QGLViewer::tr("Horizontally/Vertically translates", "SCREEN_TRANSLATE mouse action"); + case QGLViewer::ZOOM_ON_REGION : return QGLViewer::tr("Zooms on region for", "ZOOM_ON_REGION mouse action"); + } + return QString::null; +} + +QString QGLViewer::clickActionString(QGLViewer::ClickAction ca) +{ + switch (ca) + { + case QGLViewer::NO_CLICK_ACTION : return QString::null; + case QGLViewer::ZOOM_ON_PIXEL : return QGLViewer::tr("Zooms on pixel", "ZOOM_ON_PIXEL click action"); + case QGLViewer::ZOOM_TO_FIT : return QGLViewer::tr("Zooms to fit scene", "ZOOM_TO_FIT click action"); + case QGLViewer::SELECT : return QGLViewer::tr("Selects", "SELECT click action"); + case QGLViewer::RAP_FROM_PIXEL : return QGLViewer::tr("Sets pivot point", "RAP_FROM_PIXEL click action"); + case QGLViewer::RAP_IS_CENTER : return QGLViewer::tr("Resets pivot point", "RAP_IS_CENTER click action"); + case QGLViewer::CENTER_FRAME : return QGLViewer::tr("Centers manipulated frame", "CENTER_FRAME click action"); + case QGLViewer::CENTER_SCENE : return QGLViewer::tr("Centers scene", "CENTER_SCENE click action"); + case QGLViewer::SHOW_ENTIRE_SCENE : return QGLViewer::tr("Shows entire scene", "SHOW_ENTIRE_SCENE click action"); + case QGLViewer::ALIGN_FRAME : return QGLViewer::tr("Aligns manipulated frame", "ALIGN_FRAME click action"); + case QGLViewer::ALIGN_CAMERA : return QGLViewer::tr("Aligns camera", "ALIGN_CAMERA click action"); + } + return QString::null; +} + +static QString keyString(unsigned int key) +{ +# if QT_VERSION >= 0x040100 + return QKeySequence(int(key)).toString(QKeySequence::NativeText); +# else + return QString(QKeySequence(key)); +# endif +} + +QString QGLViewer::formatClickActionPrivate(ClickBindingPrivate cbp) +{ + bool buttonsBefore = cbp.buttonsBefore != Qt::NoButton; + QString keyModifierString = keyString(cbp.modifiers + cbp.key); + if (!keyModifierString.isEmpty()) { +#ifdef Q_OS_MAC + // modifiers never has a '+' sign. Add one space to clearly separate modifiers (and possible key) from button + keyModifierString += " "; +#else + // modifiers might be of the form : 'S' or 'Ctrl+S' or 'Ctrl+'. For consistency, add an other '+' if needed, no spaces + if (!keyModifierString.endsWith('+')) + keyModifierString += "+"; +#endif + } + + return tr("%1%2%3%4%5%6", "Modifier / button or wheel / double click / with / button / pressed") + .arg(keyModifierString) + .arg(mouseButtonsString(cbp.button)+(cbp.button == Qt::NoButton ? tr("Wheel", "Mouse wheel") : "")) + .arg(cbp.doubleClick ? tr(" double click", "Suffix after mouse button") : "") + .arg(buttonsBefore ? tr(" with ", "As in : Left button with Ctrl pressed") : "") + .arg(buttonsBefore ? mouseButtonsString(cbp.buttonsBefore) : "") + .arg(buttonsBefore ? tr(" pressed", "As in : Left button with Ctrl pressed") : ""); +} + +bool QGLViewer::isValidShortcutKey(int key) { + return (key >= Qt::Key_Any && key < Qt::Key_Escape) || (key >= Qt::Key_F1 && key <= Qt::Key_F35); +} + +#ifndef DOXYGEN +/*! This method is deprecated since version 2.5.0 + + Use setMouseBindingDescription(Qt::KeyboardModifiers, Qt::MouseButtons, QString, bool, Qt::MouseButtons) instead. +*/ +void QGLViewer::setMouseBindingDescription(unsigned int state, QString description, bool doubleClick, Qt::MouseButtons buttonsBefore) { + qWarning("setMouseBindingDescription(int state,...) is deprecated. Use the modifier/button equivalent"); + setMouseBindingDescription(keyboardModifiersFromState(state), + mouseButtonFromState(state), + description, + doubleClick, + buttonsBefore); +} +#endif + +/*! Defines a custom mouse binding description, displayed in the help() window's Mouse tab. + + Same as calling setMouseBindingDescription(Qt::Key, Qt::KeyboardModifiers, Qt::MouseButton, QString, bool, Qt::MouseButtons), + with a key value of Qt::Key(0) (i.e. binding description when no regular key needs to be pressed). */ +void QGLViewer::setMouseBindingDescription(Qt::KeyboardModifiers modifiers, Qt::MouseButton button, QString description, bool doubleClick, Qt::MouseButtons buttonsBefore) +{ + setMouseBindingDescription(Qt::Key(0), modifiers, button, description, doubleClick, buttonsBefore); +} + +/*! Defines a custom mouse binding description, displayed in the help() window's Mouse tab. + +\p modifiers is a combination of Qt::KeyboardModifiers (\c Qt::ControlModifier, \c Qt::AltModifier, \c +Qt::ShiftModifier, \c Qt::MetaModifier). Possibly combined using the \c "|" operator. + +\p button is one of the Qt::MouseButtons (\c Qt::LeftButton, \c Qt::MidButton, +\c Qt::RightButton...). + +\p doubleClick indicates whether or not the user has to double click this button to perform the +described action. \p buttonsBefore lists the buttons that need to be pressed before the double click. + +Set an empty \p description to \e remove a mouse binding description. + +\code +// The R key combined with the Left mouse button rotates the camera in the screen plane. +setMouseBindingDescription(Qt::Key_R, Qt::NoModifier, Qt::LeftButton, "Rotates camera in screen plane"); + +// A left button double click toggles full screen +setMouseBindingDescription(Qt::NoModifier, Qt::LeftButton, "Toggles full screen mode", true); + +// Removes the description of Ctrl+Right button +setMouseBindingDescription(Qt::ControlModifier, Qt::RightButton, ""); +\endcode + +Overload mouseMoveEvent() and friends to implement your custom mouse behavior (see the +mouseMoveEvent() documentation for an example). See the keyboardAndMouse example for an illustration. + +Use setMouseBinding() and setWheelBinding() to change the standard mouse action bindings. */ +void QGLViewer::setMouseBindingDescription(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button, QString description, bool doubleClick, Qt::MouseButtons buttonsBefore) +{ + ClickBindingPrivate cbp(modifiers, button, doubleClick, buttonsBefore, key); + + if (description.isEmpty()) + mouseDescription_.remove(cbp); + else + mouseDescription_[cbp] = description; +} + +static QString tableLine(const QString& left, const QString& right) +{ + static bool even = false; + const QString tdtd(""); + const QString tdtr("\n"); + + QString res(""; + else + res += "#ffffff\">"; + res += "" + left + tdtd + right + tdtr; + even = !even; + + return res; +} + +/*! Returns a QString that describes the application mouse bindings, displayed in the help() window +\c Mouse tab. + +Result is a table that describes custom application mouse binding descriptions defined using +setMouseBindingDescription() as well as standard mouse bindings (defined using setMouseBinding() +and setWheelBinding()). See the mouse page for details on mouse +bindings. + +See also helpString() and keyboardString(). */ +QString QGLViewer::mouseString() const +{ + QString text("
\n"); + const QString trtd("\n"); + const QString tdtd("\n"). + arg(tr("Button(s)", "Buttons column header in help window mouse tab")).arg(tr("Description", "Description column header in help window mouse tab")); + + QMap mouseBinding; + + // User-defined mouse bindings come first. + for (QMap::ConstIterator itm=mouseDescription_.begin(), endm=mouseDescription_.end(); itm!=endm; ++itm) + mouseBinding[itm.key()] = itm.value(); + + for (QMap::ConstIterator it=mouseBinding.begin(), end=mouseBinding.end(); it != end; ++it) + { + // Should not be needed (see setMouseBindingDescription()) + if (it.value().isNull()) + continue; + + text += tableLine(formatClickActionPrivate(it.key()), it.value()); + } + + // Optional separator line + if (!mouseBinding.isEmpty()) + { + mouseBinding.clear(); + text += QString("\n").arg(tr("Standard mouse bindings", "In help window mouse tab")); + } + + // Then concatenates the descriptions of wheelBinding_, mouseBinding_ and clickBinding_. + // The order is significant and corresponds to the priorities set in mousePressEvent() (reverse priority order, last one overwrites previous) + // #CONNECTION# mousePressEvent() order + for (QMap::ConstIterator itmb=mouseBinding_.begin(), endmb=mouseBinding_.end(); + itmb != endmb; ++itmb) + { + ClickBindingPrivate cbp(itmb.key().modifiers, itmb.key().button, false, Qt::NoButton, itmb.key().key); + + QString text = mouseActionString(itmb.value().action); + + if (!text.isNull()) + { + switch (itmb.value().handler) + { + case CAMERA: text += " " + tr("camera", "Suffix after action"); break; + case FRAME: text += " " + tr("manipulated frame", "Suffix after action"); break; + } + if (!(itmb.value().withConstraint)) + text += "*"; + } + mouseBinding[cbp] = text; + } + + for (QMap::ConstIterator itw=wheelBinding_.begin(), endw=wheelBinding_.end(); itw != endw; ++itw) + { + ClickBindingPrivate cbp(itw.key().modifiers, Qt::NoButton, false, Qt::NoButton, itw.key().key); + + QString text = mouseActionString(itw.value().action); + + if (!text.isNull()) + { + switch (itw.value().handler) + { + case CAMERA: text += " " + tr("camera", "Suffix after action"); break; + case FRAME: text += " " + tr("manipulated frame", "Suffix after action"); break; + } + if (!(itw.value().withConstraint)) + text += "*"; + } + + mouseBinding[cbp] = text; + } + + for (QMap::ConstIterator itcb=clickBinding_.begin(), endcb=clickBinding_.end(); itcb!=endcb; ++itcb) + mouseBinding[itcb.key()] = clickActionString(itcb.value()); + + for (QMap::ConstIterator it2=mouseBinding.begin(), end2=mouseBinding.end(); it2 != end2; ++it2) + { + if (it2.value().isNull()) + continue; + + text += tableLine(formatClickActionPrivate(it2.key()), it2.value()); + } + + text += "
"); + const QString tdtr("
"); + + text += QString("
%1%2
%1
"; + + return text; +} + +/*! Defines a custom keyboard shortcut description, that will be displayed in the help() window \c +Keyboard tab. + +The \p key definition is given as an \c int using Qt enumerated values. Set an empty \p description +to remove a shortcut description: +\code +setKeyDescription(Qt::Key_W, "Toggles wireframe display"); +setKeyDescription(Qt::CTRL+Qt::Key_L, "Loads a new scene"); +// Removes a description +setKeyDescription(Qt::CTRL+Qt::Key_C, ""); +\endcode + +See the keyboardAndMouse example for illustration +and the keyboard page for details. */ +void QGLViewer::setKeyDescription(unsigned int key, QString description) +{ + if (description.isEmpty()) + keyDescription_.remove(key); + else + keyDescription_[key] = description; +} + +QString QGLViewer::cameraPathKeysString() const +{ + if (pathIndex_.isEmpty()) + return QString::null; + + QVector keys; + keys.reserve(pathIndex_.count()); + for (QMap::ConstIterator i = pathIndex_.begin(), endi=pathIndex_.end(); i != endi; ++i) + keys.push_back(i.key()); + qSort(keys); + + QVector::const_iterator it = keys.begin(), end = keys.end(); + QString res = keyString(*it); + + const int maxDisplayedKeys = 6; + int nbDisplayedKeys = 0; + Qt::Key previousKey = (*it); + int state = 0; + ++it; + while ((it != end) && (nbDisplayedKeys < maxDisplayedKeys-1)) + { + switch (state) + { + case 0 : + if ((*it) == previousKey + 1) + state++; + else + { + res += ", " + keyString(*it); + nbDisplayedKeys++; + } + break; + case 1 : + if ((*it) == previousKey + 1) + state++; + else + { + res += ", " + keyString(previousKey); + res += ", " + keyString(*it); + nbDisplayedKeys += 2; + state = 0; + } + break; + default : + if ((*it) != previousKey + 1) + { + res += ".." + keyString(previousKey); + res += ", " + keyString(*it); + nbDisplayedKeys += 2; + state = 0; + } + break; + } + previousKey = *it; + ++it; + } + + if (state == 1) + res += ", " + keyString(previousKey); + if (state == 2) + res += ".." + keyString(previousKey); + if (it != end) + res += "..."; + + return res; +} + +/*! Returns a QString that describes the application keyboard shortcut bindings, and that will be +displayed in the help() window \c Keyboard tab. + +Default value is a table that describes the custom shortcuts defined using setKeyDescription() as +well as the \e standard QGLViewer::KeyboardAction shortcuts (defined using setShortcut()). See the +keyboard page for details on key customization. + +See also helpString() and mouseString(). */ +QString QGLViewer::keyboardString() const +{ + QString text("
\n"); + text += QString("\n"). + arg(QGLViewer::tr("Key(s)", "Keys column header in help window mouse tab")).arg(QGLViewer::tr("Description", "Description column header in help window mouse tab")); + + QMap keyDescription; + + // 1 - User defined key descriptions + for (QMap::ConstIterator kd=keyDescription_.begin(), kdend=keyDescription_.end(); kd!=kdend; ++kd) + keyDescription[kd.key()] = kd.value(); + + // Add to text in sorted order + for (QMap::ConstIterator kb=keyDescription.begin(), endb=keyDescription.end(); kb!=endb; ++kb) + text += tableLine(keyString(kb.key()), kb.value()); + + + // 2 - Optional separator line + if (!keyDescription.isEmpty()) + { + keyDescription.clear(); + text += QString("\n").arg(QGLViewer::tr("Standard viewer keys", "In help window keys tab")); + } + + + // 3 - KeyboardAction bindings description + for (QMap::ConstIterator it=keyboardBinding_.begin(), end=keyboardBinding_.end(); it != end; ++it) + if ((it.value() != 0) && ((!cameraIsInRotateMode()) || ((it.key() != INCREASE_FLYSPEED) && (it.key() != DECREASE_FLYSPEED)))) + keyDescription[it.value()] = keyboardActionDescription_[it.key()]; + + // Add to text in sorted order + for (QMap::ConstIterator kb2=keyDescription.begin(), endb2=keyDescription.end(); kb2!=endb2; ++kb2) + text += tableLine(keyString(kb2.key()), kb2.value()); + + + // 4 - Camera paths keys description + const QString cpks = cameraPathKeysString(); + if (!cpks.isNull()) + { + text += "\n"; + text += tableLine(keyString(playPathKeyboardModifiers()) + "" + QGLViewer::tr("Fx", "Generic function key (F1..F12)") + "", + QGLViewer::tr("Plays path (or resets saved position)")); + text += tableLine(keyString(addKeyFrameKeyboardModifiers()) + "" + QGLViewer::tr("Fx", "Generic function key (F1..F12)") + "", + QGLViewer::tr("Adds a key frame to path (or defines a position)")); + text += tableLine(keyString(addKeyFrameKeyboardModifiers()) + "" + QGLViewer::tr("Fx", "Generic function key (F1..F12)") + "+" + QGLViewer::tr("Fx", "Generic function key (F1..F12)") + "", + QGLViewer::tr("Deletes path (or saved position)")); + } + text += "
%1%2
%1
\n"; + text += QGLViewer::tr("Camera paths are controlled using the %1 keys (noted Fx below):", "Help window key tab camera keys").arg(cpks) + "
"; + + return text; +} + +/*! Displays the help window "About" tab. See help() for details. */ +void QGLViewer::aboutQGLViewer() { + help(); + helpWidget()->setCurrentIndex(3); +} + + +/*! Opens a modal help window that includes four tabs, respectively filled with helpString(), +keyboardString(), mouseString() and about libQGLViewer. + +Rich html-like text can be used (see the QStyleSheet documentation). This method is called when the +user presses the QGLViewer::HELP key (default is 'H'). + +You can use helpWidget() to access to the help widget (to add/remove tabs, change layout...). + +The helpRequired() signal is emitted. */ +void QGLViewer::help() +{ + Q_EMIT helpRequired(); + + bool resize = false; + int width=600; + int height=400; + + static QString label[] = {tr("&Help", "Help window tab title"), tr("&Keyboard", "Help window tab title"), tr("&Mouse", "Help window tab title"), tr("&About", "Help window about title")}; + + if (!helpWidget()) + { + // Qt4 requires a NULL parent... + helpWidget_ = new QTabWidget(NULL); + helpWidget()->setWindowTitle(tr("Help", "Help window title")); + + resize = true; + for (int i=0; i<4; ++i) + { + QTextEdit* tab = new QTextEdit(NULL); + tab->setReadOnly(true); + + helpWidget()->insertTab(i, tab, label[i]); + if (i==3) { +# include "qglviewer-icon.xpm" + QPixmap pixmap(qglviewer_icon); + tab->document()->addResource(QTextDocument::ImageResource, + QUrl("mydata://qglviewer-icon.xpm"), QVariant(pixmap)); + } + } + } + + for (int i=0; i<4; ++i) + { + QString text; + switch (i) + { + case 0 : text = helpString(); break; + case 1 : text = keyboardString(); break; + case 2 : text = mouseString(); break; + case 3 : text = QString("

") + tr( + "

libQGLViewer

" + "

Version %1


" + "A versatile 3D viewer based on OpenGL and Qt
" + "Copyright 2002-%2 Gilles Debunne
" + "%3").arg(QGLViewerVersionString()).arg("2014").arg("http://www.libqglviewer.com") + + QString("
"); + break; + default : break; + } + + QTextEdit* textEdit = (QTextEdit*)(helpWidget()->widget(i)); + textEdit->setHtml(text); + textEdit->setText(text); + + if (resize && (textEdit->height() > height)) + height = textEdit->height(); + } + + if (resize) + helpWidget()->resize(width, height+40); // 40 pixels is ~ tabs' height + helpWidget()->show(); + helpWidget()->raise(); +} + +/*! Overloading of the \c QWidget method. + +Default keyboard shortcuts are defined using setShortcut(). Overload this method to implement a +specific keyboard binding. Call the original method if you do not catch the event to preserve the +viewer default key bindings: +\code +void Viewer::keyPressEvent(QKeyEvent *e) +{ + // Defines the Alt+R shortcut. + if ((e->key() == Qt::Key_R) && (e->modifiers() == Qt::AltModifier)) + { + myResetFunction(); + update(); // Refresh display + } + else + QGLViewer::keyPressEvent(e); +} + +// With Qt 2 or 3, you would retrieve modifiers keys using : +// const Qt::ButtonState modifiers = (Qt::ButtonState)(e->state() & Qt::KeyButtonMask); +\endcode +When you define a new keyboard shortcut, use setKeyDescription() to provide a short description +which is displayed in the help() window Keyboard tab. See the keyboardAndMouse example for an illustration. + +See also QGLWidget::keyReleaseEvent(). */ +void QGLViewer::keyPressEvent(QKeyEvent *e) +{ + if (e->key() == 0) + { + e->ignore(); + return; + } + + const Qt::Key key = Qt::Key(e->key()); + + const Qt::KeyboardModifiers modifiers = e->modifiers(); + + QMap::ConstIterator it=keyboardBinding_.begin(), end=keyboardBinding_.end(); + const unsigned int target = key | modifiers; + while ((it != end) && (it.value() != target)) + ++it; + + if (it != end) + handleKeyboardAction(it.key()); + else + if (pathIndex_.contains(Qt::Key(key))) + { + // Camera paths + unsigned int index = pathIndex_[Qt::Key(key)]; + + // not safe, but try to double press on two viewers at the same time ! + static QTime doublePress; + + if (modifiers == playPathKeyboardModifiers()) + { + int elapsed = doublePress.restart(); + if ((elapsed < 250) && (index==previousPathId_)) + camera()->resetPath(index); + else + { + // Stop previous interpolation before starting a new one. + if (index != previousPathId_) + { + KeyFrameInterpolator* previous = camera()->keyFrameInterpolator(previousPathId_); + if ((previous) && (previous->interpolationIsStarted())) + previous->resetInterpolation(); + } + camera()->playPath(index); + } + previousPathId_ = index; + } + else if (modifiers == addKeyFrameKeyboardModifiers()) + { + int elapsed = doublePress.restart(); + if ((elapsed < 250) && (index==previousPathId_)) + { + if (camera()->keyFrameInterpolator(index)) + { + disconnect(camera()->keyFrameInterpolator(index), SIGNAL(interpolated()), this, SLOT(update())); + if (camera()->keyFrameInterpolator(index)->numberOfKeyFrames() > 1) + displayMessage(tr("Path %1 deleted", "Feedback message").arg(index)); + else + displayMessage(tr("Position %1 deleted", "Feedback message").arg(index)); + camera()->deletePath(index); + } + } + else + { + bool nullBefore = (camera()->keyFrameInterpolator(index) == NULL); + camera()->addKeyFrameToPath(index); + if (nullBefore) + connect(camera()->keyFrameInterpolator(index), SIGNAL(interpolated()), SLOT(update())); + int nbKF = camera()->keyFrameInterpolator(index)->numberOfKeyFrames(); + if (nbKF > 1) + displayMessage(tr("Path %1, position %2 added", "Feedback message").arg(index).arg(nbKF)); + else + displayMessage(tr("Position %1 saved", "Feedback message").arg(index)); + } + previousPathId_ = index; + } + update(); + } else { + if (isValidShortcutKey(key)) currentlyPressedKey_ = key; + e->ignore(); + } +} + +void QGLViewer::keyReleaseEvent(QKeyEvent * e) { + if (isValidShortcutKey(e->key())) currentlyPressedKey_ = Qt::Key(0); +} + +void QGLViewer::handleKeyboardAction(KeyboardAction id) +{ + switch (id) + { + case DRAW_AXIS : toggleAxisIsDrawn(); break; + case DRAW_GRID : toggleGridIsDrawn(); break; + case DISPLAY_FPS : toggleFPSIsDisplayed(); break; + case ENABLE_TEXT : toggleTextIsEnabled(); break; + case EXIT_VIEWER : saveStateToFileForAllViewers(); qApp->closeAllWindows(); break; + case SAVE_SCREENSHOT : saveSnapshot(false, false); break; + case FULL_SCREEN : toggleFullScreen(); break; + case STEREO : toggleStereoDisplay(); break; + case ANIMATION : toggleAnimation(); break; + case HELP : help(); break; + case EDIT_CAMERA : toggleCameraIsEdited(); break; + case SNAPSHOT_TO_CLIPBOARD : snapshotToClipboard(); break; + case CAMERA_MODE : + toggleCameraMode(); + displayMessage(cameraIsInRotateMode()?tr("Camera in observer mode", "Feedback message"):tr("Camera in fly mode", "Feedback message")); + break; + + case MOVE_CAMERA_LEFT : + camera()->frame()->translate(camera()->frame()->inverseTransformOf(Vec(-10.0*camera()->flySpeed(), 0.0, 0.0))); + update(); + break; + case MOVE_CAMERA_RIGHT : + camera()->frame()->translate(camera()->frame()->inverseTransformOf(Vec( 10.0*camera()->flySpeed(), 0.0, 0.0))); + update(); + break; + case MOVE_CAMERA_UP : + camera()->frame()->translate(camera()->frame()->inverseTransformOf(Vec(0.0, 10.0*camera()->flySpeed(), 0.0))); + update(); + break; + case MOVE_CAMERA_DOWN : + camera()->frame()->translate(camera()->frame()->inverseTransformOf(Vec(0.0, -10.0*camera()->flySpeed(), 0.0))); + update(); + break; + + case INCREASE_FLYSPEED : camera()->setFlySpeed(camera()->flySpeed() * 1.5); break; + case DECREASE_FLYSPEED : camera()->setFlySpeed(camera()->flySpeed() / 1.5); break; + } +} + +/*! Callback method used when the widget size is modified. + +If you overload this method, first call the inherited method. Also called when the widget is +created, before its first display. */ +void QGLViewer::resizeGL(int width, int height) +{ + QOpenGLWidget::resizeGL(width, height); + glViewport( 0, 0, GLint(width), GLint(height) ); + camera()->setScreenWidthAndHeight(this->width(), this->height()); +} + +////////////////////////////////////////////////////////////////////////// +// K e y b o a r d s h o r t c u t s // +////////////////////////////////////////////////////////////////////////// + +/*! Defines the shortcut() that triggers a given QGLViewer::KeyboardAction. + +Here are some examples: +\code +// Press 'Q' to exit application +setShortcut(EXIT_VIEWER, Qt::Key_Q); + +// Alt+M toggles camera mode +setShortcut(CAMERA_MODE, Qt::ALT + Qt::Key_M); + +// The DISPLAY_FPS action is disabled +setShortcut(DISPLAY_FPS, 0); +\endcode + +Only one shortcut can be assigned to a given QGLViewer::KeyboardAction (new bindings replace +previous ones). If several KeyboardAction are binded to the same shortcut, only one of them is +active. */ +void QGLViewer::setShortcut(KeyboardAction action, unsigned int key) +{ + keyboardBinding_[action] = key; +} + +/*! Returns the keyboard shortcut associated to a given QGLViewer::KeyboardAction. + +Result is an \c unsigned \c int defined using Qt enumerated values, as in \c Qt::Key_Q or +\c Qt::CTRL + Qt::Key_X. Use Qt::MODIFIER_MASK to separate the key from the state keys. Returns \c 0 if +the KeyboardAction is disabled (not binded). Set using setShortcut(). + +If you want to define keyboard shortcuts for custom actions (say, open a scene file), overload +keyPressEvent() and then setKeyDescription(). + +These shortcuts and their descriptions are automatically included in the help() window \c Keyboard +tab. + +See the keyboard page for details and default values and the keyboardAndMouse example for a practical +illustration. */ +unsigned int QGLViewer::shortcut(KeyboardAction action) const +{ + if (keyboardBinding_.contains(action)) + return keyboardBinding_[action]; + else + return 0; +} + +#ifndef DOXYGEN +void QGLViewer::setKeyboardAccelerator(KeyboardAction action, unsigned int key) +{ + qWarning("setKeyboardAccelerator is deprecated. Use setShortcut instead."); + setShortcut(action, key); +} + +unsigned int QGLViewer::keyboardAccelerator(KeyboardAction action) const +{ + qWarning("keyboardAccelerator is deprecated. Use shortcut instead."); + return shortcut(action); +} +#endif + +/////// Key Frames associated keys /////// + +/*! Returns the keyboard key associated to camera Key Frame path \p index. + +Default values are F1..F12 for indexes 1..12. + +addKeyFrameKeyboardModifiers() (resp. playPathKeyboardModifiers()) define the state key(s) that +must be pressed with this key to add a KeyFrame to (resp. to play) the associated Key Frame path. +If you quickly press twice the pathKey(), the path is reset (resp. deleted). + +Use camera()->keyFrameInterpolator( \p index ) to retrieve the KeyFrameInterpolator that defines +the path. + +If several keys are binded to a given \p index (see setPathKey()), one of them is returned. +Returns \c 0 if no key is associated with this index. + +See also the keyboard page. */ +Qt::Key QGLViewer::pathKey(unsigned int index) const +{ + for (QMap::ConstIterator it = pathIndex_.begin(), end=pathIndex_.end(); it != end; ++it) + if (it.value() == index) + return it.key(); + return Qt::Key(0); +} + +/*! Sets the pathKey() associated with the camera Key Frame path \p index. + +Several keys can be binded to the same \p index. Use a negated \p key value to delete the binding +(the \p index value is then ignored): +\code +// Press 'space' to play/pause/add/delete camera path of index 0. +setPathKey(Qt::Key_Space, 0); + +// Remove this binding +setPathKey(-Qt::Key_Space); +\endcode */ +void QGLViewer::setPathKey(int key, unsigned int index) +{ + Qt::Key k = Qt::Key(abs(key)); + if (key < 0) + pathIndex_.remove(k); + else + pathIndex_[k] = index; +} + +/*! Sets the playPathKeyboardModifiers() value. */ +void QGLViewer::setPlayPathKeyboardModifiers(Qt::KeyboardModifiers modifiers) +{ + playPathKeyboardModifiers_ = modifiers; +} + +/*! Sets the addKeyFrameKeyboardModifiers() value. */ +void QGLViewer::setAddKeyFrameKeyboardModifiers(Qt::KeyboardModifiers modifiers) +{ + addKeyFrameKeyboardModifiers_ = modifiers; +} + +/*! Returns the keyboard modifiers that must be pressed with a pathKey() to add the current camera +position to a KeyFrame path. + +It can be \c Qt::NoModifier, \c Qt::ControlModifier, \c Qt::ShiftModifier, \c Qt::AltModifier, \c +Qt::MetaModifier or a combination of these (using the bitwise '|' operator). + +Default value is Qt::AltModifier. Defined using setAddKeyFrameKeyboardModifiers(). + +See also playPathKeyboardModifiers(). */ +Qt::KeyboardModifiers QGLViewer::addKeyFrameKeyboardModifiers() const +{ + return addKeyFrameKeyboardModifiers_; +} + +/*! Returns the keyboard modifiers that must be pressed with a pathKey() to play a camera KeyFrame path. + +It can be \c Qt::NoModifier, \c Qt::ControlModifier, \c Qt::ShiftModifier, \c Qt::AltModifier, \c +Qt::MetaModifier or a combination of these (using the bitwise '|' operator). + +Default value is Qt::NoModifier. Defined using setPlayPathKeyboardModifiers(). + +See also addKeyFrameKeyboardModifiers(). */ +Qt::KeyboardModifiers QGLViewer::playPathKeyboardModifiers() const +{ + return playPathKeyboardModifiers_; +} + +#ifndef DOXYGEN +// Deprecated methods +Qt::KeyboardModifiers QGLViewer::addKeyFrameStateKey() const +{ + qWarning("addKeyFrameStateKey has been renamed addKeyFrameKeyboardModifiers"); + return addKeyFrameKeyboardModifiers(); } + +Qt::KeyboardModifiers QGLViewer::playPathStateKey() const +{ + qWarning("playPathStateKey has been renamed playPathKeyboardModifiers"); + return playPathKeyboardModifiers(); +} + +void QGLViewer::setAddKeyFrameStateKey(unsigned int buttonState) +{ + qWarning("setAddKeyFrameStateKey has been renamed setAddKeyFrameKeyboardModifiers"); + setAddKeyFrameKeyboardModifiers(keyboardModifiersFromState(buttonState)); +} + +void QGLViewer::setPlayPathStateKey(unsigned int buttonState) +{ + qWarning("setPlayPathStateKey has been renamed setPlayPathKeyboardModifiers"); + setPlayPathKeyboardModifiers(keyboardModifiersFromState(buttonState)); +} + +Qt::Key QGLViewer::keyFrameKey(unsigned int index) const +{ + qWarning("keyFrameKey has been renamed pathKey."); + return pathKey(index); +} + +Qt::KeyboardModifiers QGLViewer::playKeyFramePathStateKey() const +{ + qWarning("playKeyFramePathStateKey has been renamed playPathKeyboardModifiers."); + return playPathKeyboardModifiers(); +} + +void QGLViewer::setKeyFrameKey(unsigned int index, int key) +{ + qWarning("setKeyFrameKey is deprecated, use setPathKey instead, with swapped parameters."); + setPathKey(key, index); +} + +void QGLViewer::setPlayKeyFramePathStateKey(unsigned int buttonState) +{ + qWarning("setPlayKeyFramePathStateKey has been renamed setPlayPathKeyboardModifiers."); + setPlayPathKeyboardModifiers(keyboardModifiersFromState(buttonState)); +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// M o u s e b e h a v i o r s t a t e k e y s // +//////////////////////////////////////////////////////////////////////////////// +#ifndef DOXYGEN +/*! This method has been deprecated since version 2.5.0 + +Associates keyboard modifiers to MouseHandler \p handler. + +The \p modifiers parameter is \c Qt::AltModifier, \c Qt::ShiftModifier, \c Qt::ControlModifier, \c +Qt::MetaModifier or a combination of these using the '|' bitwise operator. + +\e All the \p handler's associated bindings will then need the specified \p modifiers key(s) to be +activated. + +With this code, +\code +setHandlerKeyboardModifiers(QGLViewer::CAMERA, Qt::AltModifier); +setHandlerKeyboardModifiers(QGLViewer::FRAME, Qt::NoModifier); +\endcode +you will have to press the \c Alt key while pressing mouse buttons in order to move the camera(), +while no key will be needed to move the manipulatedFrame(). + +This method has a very basic implementation: every action binded to \p handler has its keyboard +modifier replaced by \p modifiers. If \p handler had some actions binded to different modifiers, +these settings will be lost. You should hence consider using setMouseBinding() for finer tuning. + +The default binding associates \c Qt::ControlModifier to all the QGLViewer::FRAME actions and \c +Qt::NoModifier to all QGLViewer::CAMERA actions. See mouse page for +details. + +\attention This method calls setMouseBinding(), which ensures that only one action is binded to a +given modifiers. If you want to \e swap the QGLViewer::CAMERA and QGLViewer::FRAME keyboard +modifiers, you have to use a temporary dummy modifier (as if you were swapping two variables) or +else the first call will overwrite the previous settings: +\code +// Associate FRAME with Alt (temporary value) +setHandlerKeyboardModifiers(QGLViewer::FRAME, Qt::AltModifier); +// Control is associated with CAMERA +setHandlerKeyboardModifiers(QGLViewer::CAMERA, Qt::ControlModifier); +// And finally, FRAME can be associated with NoModifier +setHandlerKeyboardModifiers(QGLViewer::FRAME, Qt::NoModifier); +\endcode */ +void QGLViewer::setHandlerKeyboardModifiers(MouseHandler handler, Qt::KeyboardModifiers modifiers) +{ + qWarning("setHandlerKeyboardModifiers is deprecated, call setMouseBinding() instead"); + + QMap newMouseBinding; + QMap newWheelBinding; + QMap newClickBinding_; + + QMap::Iterator mit; + QMap::Iterator wit; + + // First copy unchanged bindings. + for (mit = mouseBinding_.begin(); mit != mouseBinding_.end(); ++mit) + if ((mit.value().handler != handler) || (mit.value().action == ZOOM_ON_REGION)) + newMouseBinding[mit.key()] = mit.value(); + + for (wit = wheelBinding_.begin(); wit != wheelBinding_.end(); ++wit) + if (wit.value().handler != handler) + newWheelBinding[wit.key()] = wit.value(); + + // Then, add modified bindings, that can overwrite the previous ones. + for (mit = mouseBinding_.begin(); mit != mouseBinding_.end(); ++mit) + if ((mit.value().handler == handler) && (mit.value().action != ZOOM_ON_REGION)) + { + MouseBindingPrivate mbp(modifiers, mit.key().button, mit.key().key); + newMouseBinding[mbp] = mit.value(); + } + + for (wit = wheelBinding_.begin(); wit != wheelBinding_.end(); ++wit) + if (wit.value().handler == handler) + { + WheelBindingPrivate wbp(modifiers, wit.key().key); + newWheelBinding[wbp] = wit.value(); + } + + // Same for button bindings + for (QMap::ConstIterator cb=clickBinding_.begin(), end=clickBinding_.end(); cb != end; ++cb) + if (((handler==CAMERA) && ((cb.value() == CENTER_SCENE) || (cb.value() == ALIGN_CAMERA))) || + ((handler==FRAME) && ((cb.value() == CENTER_FRAME) || (cb.value() == ALIGN_FRAME)))) + { + ClickBindingPrivate cbp(modifiers, cb.key().button, cb.key().doubleClick, cb.key().buttonsBefore, cb.key().key); + newClickBinding_[cbp] = cb.value(); + } + else + newClickBinding_[cb.key()] = cb.value(); + + mouseBinding_ = newMouseBinding; + wheelBinding_ = newWheelBinding; + clickBinding_ = newClickBinding_; +} + +void QGLViewer::setHandlerStateKey(MouseHandler handler, unsigned int buttonState) +{ + qWarning("setHandlerStateKey has been renamed setHandlerKeyboardModifiers"); + setHandlerKeyboardModifiers(handler, keyboardModifiersFromState(buttonState)); +} + +void QGLViewer::setMouseStateKey(MouseHandler handler, unsigned int buttonState) +{ + qWarning("setMouseStateKey has been renamed setHandlerKeyboardModifiers."); + setHandlerKeyboardModifiers(handler, keyboardModifiersFromState(buttonState)); +} + +/*! This method is deprecated since version 2.5.0 + + Use setMouseBinding(Qt::KeyboardModifiers, Qt::MouseButtons, MouseHandler, MouseAction, bool) instead. +*/ +void QGLViewer::setMouseBinding(unsigned int state, MouseHandler handler, MouseAction action, bool withConstraint) +{ + qWarning("setMouseBinding(int state, MouseHandler...) is deprecated. Use the modifier/button equivalent"); + setMouseBinding(keyboardModifiersFromState(state), + mouseButtonFromState(state), + handler, + action, + withConstraint); +} +#endif + +/*! Defines a MouseAction binding. + + Same as calling setMouseBinding(Qt::Key, Qt::KeyboardModifiers, Qt::MouseButton, MouseHandler, MouseAction, bool), + with a key value of Qt::Key(0) (i.e. no regular extra key needs to be pressed to perform this action). */ +void QGLViewer::setMouseBinding(Qt::KeyboardModifiers modifiers, Qt::MouseButton button, MouseHandler handler, MouseAction action, bool withConstraint) { + setMouseBinding(Qt::Key(0), modifiers, button, handler, action, withConstraint); +} + +/*! Associates a MouseAction to any mouse \p button, while keyboard \p modifiers and \p key are pressed. +The receiver of the mouse events is a MouseHandler (QGLViewer::CAMERA or QGLViewer::FRAME). + +The parameters should read: when the mouse \p button is pressed, while the keyboard \p modifiers and \p key are down, +activate \p action on \p handler. Use Qt::NoModifier to indicate that no modifier +key is needed, and a \p key value of 0 if no regular key has to be pressed +(or simply use setMouseBinding(Qt::KeyboardModifiers, Qt::MouseButton, MouseHandler, MouseAction, bool)). + +Use the '|' operator to combine modifiers: +\code +// The R key combined with the Left mouse button rotates the camera in the screen plane. +setMouseBinding(Qt::Key_R, Qt::NoModifier, Qt::LeftButton, CAMERA, SCREEN_ROTATE); + +// Alt + Shift and Left button rotates the manipulatedFrame(). +setMouseBinding(Qt::AltModifier | Qt::ShiftModifier, Qt::LeftButton, FRAME, ROTATE); +\endcode + +If \p withConstraint is \c true (default), the possible +qglviewer::Frame::constraint() of the associated Frame will be enforced during motion. + +The list of all possible MouseAction, some binding examples and default bindings are provided in +the mouse page. + +See the keyboardAndMouse example for an illustration. + +If no mouse button is specified, the binding is ignored. If an action was previously +associated with this keyboard and button combination, it is silently overwritten (call mouseAction() +before to check). + +To remove a specific mouse binding, use \p NO_MOUSE_ACTION as the \p action. + +See also setMouseBinding(Qt::KeyboardModifiers, Qt::MouseButtons, ClickAction, bool, int), setWheelBinding() and clearMouseBindings(). */ +void QGLViewer::setMouseBinding(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button, MouseHandler handler, MouseAction action, bool withConstraint) +{ + if ((handler == FRAME) && ((action == MOVE_FORWARD) || (action == MOVE_BACKWARD) || + (action == ROLL) || (action == LOOK_AROUND) || + (action == ZOOM_ON_REGION))) { + qWarning("Cannot bind %s to FRAME", mouseActionString(action).toLatin1().constData()); + return; + } + + if (button == Qt::NoButton) { + qWarning("No mouse button specified in setMouseBinding"); + return; + } + + MouseActionPrivate map; + map.handler = handler; + map.action = action; + map.withConstraint = withConstraint; + + MouseBindingPrivate mbp(modifiers, button, key); + if (action == NO_MOUSE_ACTION) + mouseBinding_.remove(mbp); + else + mouseBinding_.insert(mbp, map); + + ClickBindingPrivate cbp(modifiers, button, false, Qt::NoButton, key); + clickBinding_.remove(cbp); +} + +#ifndef DOXYGEN +/*! This method is deprecated since version 2.5.0 + + Use setMouseBinding(Qt::KeyboardModifiers, Qt::MouseButtons, MouseHandler, MouseAction, bool) instead. +*/ +void QGLViewer::setMouseBinding(unsigned int state, ClickAction action, bool doubleClick, Qt::MouseButtons buttonsBefore) { + qWarning("setMouseBinding(int state, ClickAction...) is deprecated. Use the modifier/button equivalent"); + setMouseBinding(keyboardModifiersFromState(state), + mouseButtonFromState(state), + action, + doubleClick, + buttonsBefore); +} +#endif + +/*! Defines a ClickAction binding. + + Same as calling setMouseBinding(Qt::Key, Qt::KeyboardModifiers, Qt::MouseButton, ClickAction, bool, Qt::MouseButtons), + with a key value of Qt::Key(0) (i.e. no regular key needs to be pressed to activate this action). */ +void QGLViewer::setMouseBinding(Qt::KeyboardModifiers modifiers, Qt::MouseButton button, ClickAction action, bool doubleClick, Qt::MouseButtons buttonsBefore) +{ + setMouseBinding(Qt::Key(0), modifiers, button, action, doubleClick, buttonsBefore); +} + +/*! Associates a ClickAction to a button and keyboard key and modifier(s) combination. + +The parameters should read: when \p button is pressed, while the \p modifiers and \p key keys are down, +and possibly as a \p doubleClick, then perform \p action. Use Qt::NoModifier to indicate that no modifier +key is needed, and a \p key value of 0 if no regular key has to be pressed (or simply use +setMouseBinding(Qt::KeyboardModifiers, Qt::MouseButton, ClickAction, bool, Qt::MouseButtons)). + +If \p buttonsBefore is specified (valid only when \p doubleClick is \c true), then this (or these) other mouse +button(s) has (have) to be pressed \e before the double click occurs in order to execute \p action. + +The list of all possible ClickAction, some binding examples and default bindings are listed in the +mouse page. See also the setMouseBinding() documentation. + +See the keyboardAndMouse example for an +illustration. + +The binding is ignored if Qt::NoButton is specified as \p buttons. + +See also setMouseBinding(Qt::KeyboardModifiers, Qt::MouseButtons, MouseHandler, MouseAction, bool), setWheelBinding() and clearMouseBindings(). +*/ +void QGLViewer::setMouseBinding(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button, ClickAction action, bool doubleClick, Qt::MouseButtons buttonsBefore) +{ + if ((buttonsBefore != Qt::NoButton) && !doubleClick) { + qWarning("Buttons before is only meaningful when doubleClick is true in setMouseBinding()."); + return; + } + + if (button == Qt::NoButton) { + qWarning("No mouse button specified in setMouseBinding"); + return; + } + + ClickBindingPrivate cbp(modifiers, button, doubleClick, buttonsBefore, key); + + // #CONNECTION performClickAction comment on NO_CLICK_ACTION + if (action == NO_CLICK_ACTION) + clickBinding_.remove(cbp); + else + clickBinding_.insert(cbp, action); + + if ((!doubleClick) && (buttonsBefore == Qt::NoButton)) { + MouseBindingPrivate mbp(modifiers, button, key); + mouseBinding_.remove(mbp); + } +} + +/*! Defines a mouse wheel binding. + + Same as calling setWheelBinding(Qt::Key, Qt::KeyboardModifiers, MouseHandler, MouseAction, bool), + with a key value of Qt::Key(0) (i.e. no regular key needs to be pressed to activate this action). */ +void QGLViewer::setWheelBinding(Qt::KeyboardModifiers modifiers, MouseHandler handler, MouseAction action, bool withConstraint) { + setWheelBinding(Qt::Key(0), modifiers, handler, action, withConstraint); +} + +/*! Associates a MouseAction and a MouseHandler to a mouse wheel event. + +This method is very similar to setMouseBinding(), but specific to the wheel. + +In the current implementation only QGLViewer::ZOOM can be associated with QGLViewer::FRAME, while +QGLViewer::CAMERA can receive QGLViewer::ZOOM and QGLViewer::MOVE_FORWARD. + +The difference between QGLViewer::ZOOM and QGLViewer::MOVE_FORWARD is that QGLViewer::ZOOM speed +depends on the distance to the object, while QGLViewer::MOVE_FORWARD moves at a constant speed +defined by qglviewer::Camera::flySpeed(). */ +void QGLViewer::setWheelBinding(Qt::Key key, Qt::KeyboardModifiers modifiers, MouseHandler handler, MouseAction action, bool withConstraint) +{ + //#CONNECTION# ManipulatedFrame::wheelEvent and ManipulatedCameraFrame::wheelEvent switches + if ((action != ZOOM) && (action != MOVE_FORWARD) && (action != MOVE_BACKWARD) && (action != NO_MOUSE_ACTION)) { + qWarning("Cannot bind %s to wheel", mouseActionString(action).toLatin1().constData()); + return; + } + + if ((handler == FRAME) && (action != ZOOM) && (action != NO_MOUSE_ACTION)) { + qWarning("Cannot bind %s to FRAME wheel", mouseActionString(action).toLatin1().constData()); + return; + } + + MouseActionPrivate map; + map.handler = handler; + map.action = action; + map.withConstraint = withConstraint; + + WheelBindingPrivate wbp(modifiers, key); + if (action == NO_MOUSE_ACTION) + wheelBinding_.remove(wbp); + else + wheelBinding_[wbp] = map; +} + +/*! Clears all the default mouse bindings. + +After this call, you will have to use setMouseBinding() and setWheelBinding() to restore the mouse bindings you are interested in. +*/ +void QGLViewer::clearMouseBindings() { + mouseBinding_.clear(); + clickBinding_.clear(); + wheelBinding_.clear(); +} + +/*! Clears all the default keyboard shortcuts. + +After this call, you will have to use setShortcut() to define your own keyboard shortcuts. +*/ +void QGLViewer::clearShortcuts() { + keyboardBinding_.clear(); + pathIndex_.clear(); +} + +/*! This method is deprecated since version 2.5.0 + + Use mouseAction(Qt::Key, Qt::KeyboardModifiers, Qt::MouseButtons) instead. +*/ +QGLViewer::MouseAction QGLViewer::mouseAction(unsigned int state) const { + qWarning("mouseAction(int state,...) is deprecated. Use the modifier/button equivalent"); + return mouseAction(Qt::Key(0), keyboardModifiersFromState(state), mouseButtonFromState(state)); +} + +/*! Returns the MouseAction the will be triggered when the mouse \p button is pressed, +while the keyboard \p modifiers and \p key are pressed. + +Returns QGLViewer::NO_MOUSE_ACTION if no action is associated with this combination. Use 0 for \p key +to indicate that no regular key needs to be pressed. + +For instance, to know which motion corresponds to Alt+LeftButton, do: +\code +QGLViewer::MouseAction ma = mouseAction(0, Qt::AltModifier, Qt::LeftButton); +if (ma != QGLViewer::NO_MOUSE_ACTION) ... +\endcode + +Use mouseHandler() to know which object (QGLViewer::CAMERA or QGLViewer::FRAME) will execute this +action. */ +QGLViewer::MouseAction QGLViewer::mouseAction(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button) const +{ + MouseBindingPrivate mbp(modifiers, button, key); + if (mouseBinding_.contains(mbp)) + return mouseBinding_[mbp].action; + else + return NO_MOUSE_ACTION; +} + +/*! This method is deprecated since version 2.5.0 + + Use mouseHanler(Qt::Key, Qt::KeyboardModifiers, Qt::MouseButtons) instead. +*/ +int QGLViewer::mouseHandler(unsigned int state) const { + qWarning("mouseHandler(int state,...) is deprecated. Use the modifier/button equivalent"); + return mouseHandler(Qt::Key(0), keyboardModifiersFromState(state), mouseButtonFromState(state)); +} + +/*! Returns the MouseHandler which will be activated when the mouse \p button is pressed, while the \p modifiers and \p key are pressed. + +If no action is associated with this combination, returns \c -1. Use 0 for \p key and Qt::NoModifier for \p modifiers +to represent the lack of a key press. + +For instance, to know which handler receives the Alt+LeftButton, do: +\code +int mh = mouseHandler(0, Qt::AltModifier, Qt::LeftButton); +if (mh == QGLViewer::CAMERA) ... +\endcode + +Use mouseAction() to know which action (see the MouseAction enum) will be performed on this handler. */ +int QGLViewer::mouseHandler(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button) const +{ + MouseBindingPrivate mbp(modifiers, button, key); + if (mouseBinding_.contains(mbp)) + return mouseBinding_[mbp].handler; + else + return -1; +} + + +#ifndef DOXYGEN +/*! This method is deprecated since version 2.5.0 + + Use mouseButtons() and keyboardModifiers() instead. +*/ +int QGLViewer::mouseButtonState(MouseHandler handler, MouseAction action, bool withConstraint) const { + qWarning("mouseButtonState() is deprecated. Use mouseButtons() and keyboardModifiers() instead"); + for (QMap::ConstIterator it=mouseBinding_.begin(), end=mouseBinding_.end(); it != end; ++it) + if ( (it.value().handler == handler) && (it.value().action == action) && (it.value().withConstraint == withConstraint) ) + return (int) it.key().modifiers | (int) it.key().button; + + return Qt::NoButton; +} +#endif + +/*! Returns the keyboard state that triggers \p action on \p handler \p withConstraint using the mouse wheel. + +If such a binding exists, results are stored in the \p key and \p modifiers +parameters. If the MouseAction \p action is not bound, \p key is set to the illegal -1 value. +If several keyboard states trigger the MouseAction, one of them is returned. + +See also setMouseBinding(), getClickActionBinding() and getMouseActionBinding(). */ +void QGLViewer::getWheelActionBinding(MouseHandler handler, MouseAction action, bool withConstraint, + Qt::Key& key, Qt::KeyboardModifiers& modifiers) const +{ + for (QMap::ConstIterator it=wheelBinding_.begin(), end=wheelBinding_.end(); it != end; ++it) + if ( (it.value().handler == handler) && (it.value().action == action) && (it.value().withConstraint == withConstraint) ) { + key = it.key().key; + modifiers = it.key().modifiers; + return; + } + + key = Qt::Key(-1); + modifiers = Qt::NoModifier; +} + +/*! Returns the mouse and keyboard state that triggers \p action on \p handler \p withConstraint. + +If such a binding exists, results are stored in the \p key, \p modifiers and \p button +parameters. If the MouseAction \p action is not bound, \p button is set to \c Qt::NoButton. +If several mouse and keyboard states trigger the MouseAction, one of them is returned. + +See also setMouseBinding(), getClickActionBinding() and getWheelActionBinding(). */ +void QGLViewer::getMouseActionBinding(MouseHandler handler, MouseAction action, bool withConstraint, + Qt::Key& key, Qt::KeyboardModifiers& modifiers, Qt::MouseButton& button) const +{ + for (QMap::ConstIterator it=mouseBinding_.begin(), end=mouseBinding_.end(); it != end; ++it) { + if ( (it.value().handler == handler) && (it.value().action == action) && (it.value().withConstraint == withConstraint) ) { + key = it.key().key; + modifiers = it.key().modifiers; + button = it.key().button; + return; + } + } + + key = Qt::Key(0); + modifiers = Qt::NoModifier; + button = Qt::NoButton; +} + +/*! Returns the MouseAction (if any) that is performed when using the wheel, when the \p modifiers and \p key keyboard keys are pressed. + +Returns NO_MOUSE_ACTION if no such binding has been defined using setWheelBinding(). + +Same as mouseAction(), but for the wheel action. See also wheelHandler(). +*/ +QGLViewer::MouseAction QGLViewer::wheelAction(Qt::Key key, Qt::KeyboardModifiers modifiers) const +{ + WheelBindingPrivate wbp(modifiers, key); + if (wheelBinding_.contains(wbp)) + return wheelBinding_[wbp].action; + else + return NO_MOUSE_ACTION; +} + +/*! Returns the MouseHandler (if any) that receives wheel events when the \p modifiers and \p key keyboard keys are pressed. + + Returns -1 if no no such binding has been defined using setWheelBinding(). See also wheelAction(). +*/ +int QGLViewer::wheelHandler(Qt::Key key, Qt::KeyboardModifiers modifiers) const +{ + WheelBindingPrivate wbp(modifiers, key); + if (wheelBinding_.contains(wbp)) + return wheelBinding_[wbp].handler; + else + return -1; +} + +/*! Same as mouseAction(), but for the ClickAction set using setMouseBinding(). + +Returns NO_CLICK_ACTION if no click action is associated with this keyboard and mouse buttons combination. */ +QGLViewer::ClickAction QGLViewer::clickAction(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button, + bool doubleClick, Qt::MouseButtons buttonsBefore) const { + ClickBindingPrivate cbp(modifiers, button, doubleClick, buttonsBefore, key); + if (clickBinding_.contains(cbp)) + return clickBinding_[cbp]; + else + return NO_CLICK_ACTION; +} + +#ifndef DOXYGEN +/*! This method is deprecated since version 2.5.0 + + Use wheelAction(Qt::Key key, Qt::KeyboardModifiers modifiers) instead. */ +QGLViewer::MouseAction QGLViewer::wheelAction(Qt::KeyboardModifiers modifiers) const { + qWarning("wheelAction() is deprecated. Use the new wheelAction() method with a key parameter instead"); + return wheelAction(Qt::Key(0), modifiers); +} + +/*! This method is deprecated since version 2.5.0 + + Use wheelHandler(Qt::Key key, Qt::KeyboardModifiers modifiers) instead. */ +int QGLViewer::wheelHandler(Qt::KeyboardModifiers modifiers) const { + qWarning("wheelHandler() is deprecated. Use the new wheelHandler() method with a key parameter instead"); + return wheelHandler(Qt::Key(0), modifiers); +} + +/*! This method is deprecated since version 2.5.0 + + Use wheelAction() and wheelHandler() instead. */ +unsigned int QGLViewer::wheelButtonState(MouseHandler handler, MouseAction action, bool withConstraint) const +{ + qWarning("wheelButtonState() is deprecated. Use the wheelAction() and wheelHandler() instead"); + for (QMap::ConstIterator it=wheelBinding_.begin(), end=wheelBinding_.end(); it!=end; ++it) + if ( (it.value().handler == handler) && (it.value().action == action) && (it.value().withConstraint == withConstraint) ) + return it.key().key + it.key().modifiers; + + return -1; +} + +/*! This method is deprecated since version 2.5.0 + + Use clickAction(Qt::KeyboardModifiers, Qt::MouseButtons, bool, Qt::MouseButtons) instead. +*/ +QGLViewer::ClickAction QGLViewer::clickAction(unsigned int state, bool doubleClick, Qt::MouseButtons buttonsBefore) const { + qWarning("clickAction(int state,...) is deprecated. Use the modifier/button equivalent"); + return clickAction(Qt::Key(0), + keyboardModifiersFromState(state), + mouseButtonFromState(state), + doubleClick, + buttonsBefore); +} + +/*! This method is deprecated since version 2.5.0 + + Use getClickActionState(ClickAction, Qt::Key, Qt::KeyboardModifiers, Qt::MouseButton, bool, Qt::MouseButtons) instead. +*/ +void QGLViewer::getClickButtonState(ClickAction action, unsigned int& state, bool& doubleClick, Qt::MouseButtons& buttonsBefore) const { + qWarning("getClickButtonState(int state,...) is deprecated. Use the modifier/button equivalent"); + Qt::KeyboardModifiers modifiers; + Qt::MouseButton button; + Qt::Key key; + getClickActionBinding(action, key, modifiers, button, doubleClick, buttonsBefore); + state = (unsigned int) modifiers | (unsigned int) button | (unsigned int) key; +} +#endif + +/*! Returns the mouse and keyboard state that triggers \p action. + +If such a binding exists, results are stored in the \p key, \p modifiers, \p button, \p doubleClick and \p buttonsBefore +parameters. If the ClickAction \p action is not bound, \p button is set to \c Qt::NoButton. +If several mouse buttons trigger in the ClickAction, one of them is returned. + +See also setMouseBinding(), getMouseActionBinding() and getWheelActionBinding(). */ +void QGLViewer::getClickActionBinding(ClickAction action, Qt::Key& key, Qt::KeyboardModifiers& modifiers, Qt::MouseButton &button, bool& doubleClick, Qt::MouseButtons& buttonsBefore) const +{ + for (QMap::ConstIterator it=clickBinding_.begin(), end=clickBinding_.end(); it != end; ++it) + if (it.value() == action) { + modifiers = it.key().modifiers; + button = it.key().button; + doubleClick = it.key().doubleClick; + buttonsBefore = it.key().buttonsBefore; + key = it.key().key; + return; + } + + modifiers = Qt::NoModifier; + button = Qt::NoButton; + doubleClick = false; + buttonsBefore = Qt::NoButton; + key = Qt::Key(0); +} + +/*! This function should be used in conjunction with toggleCameraMode(). It returns \c true when at +least one mouse button is binded to the \c ROTATE mouseAction. This is crude way of determining +which "mode" the camera is in. */ +bool QGLViewer::cameraIsInRotateMode() const +{ + //#CONNECTION# used in toggleCameraMode() and keyboardString() + Qt::Key key; + Qt::KeyboardModifiers modifiers; + Qt::MouseButton button; + getMouseActionBinding(CAMERA, ROTATE, true /*constraint*/, key, modifiers, button); + return button != Qt::NoButton; +} + +/*! Swaps between two predefined camera mouse bindings. + +The first mode makes the camera observe the scene while revolving around the +qglviewer::Camera::pivotPoint(). The second mode is designed for walkthrough applications +and simulates a flying camera. + +Practically, the three mouse buttons are respectively binded to: +\arg In rotate mode: QGLViewer::ROTATE, QGLViewer::ZOOM, QGLViewer::TRANSLATE. +\arg In fly mode: QGLViewer::MOVE_FORWARD, QGLViewer::LOOK_AROUND, QGLViewer::MOVE_BACKWARD. + +The current mode is determined by checking if a mouse button is binded to QGLViewer::ROTATE for +the QGLViewer::CAMERA. The state key that was previously used to move the camera is preserved. */ +void QGLViewer::toggleCameraMode() +{ + Qt::Key key; + Qt::KeyboardModifiers modifiers; + Qt::MouseButton button; + getMouseActionBinding(CAMERA, ROTATE, true /*constraint*/, key, modifiers, button); + bool rotateMode = button != Qt::NoButton; + + if (!rotateMode) { + getMouseActionBinding(CAMERA, MOVE_FORWARD, true /*constraint*/, key, modifiers, button); + } + + //#CONNECTION# setDefaultMouseBindings() + if (rotateMode) + { + camera()->frame()->updateSceneUpVector(); + camera()->frame()->stopSpinning(); + + setMouseBinding(modifiers, Qt::LeftButton, CAMERA, MOVE_FORWARD); + setMouseBinding(modifiers, Qt::MidButton, CAMERA, LOOK_AROUND); + setMouseBinding(modifiers, Qt::RightButton, CAMERA, MOVE_BACKWARD); + + setMouseBinding(Qt::Key_R, modifiers, Qt::LeftButton, CAMERA, ROLL); + + setMouseBinding(Qt::NoModifier, Qt::LeftButton, NO_CLICK_ACTION, true); + setMouseBinding(Qt::NoModifier, Qt::MidButton, NO_CLICK_ACTION, true); + setMouseBinding(Qt::NoModifier, Qt::RightButton, NO_CLICK_ACTION, true); + + setWheelBinding(modifiers, CAMERA, MOVE_FORWARD); + } + else + { + // Should stop flyTimer. But unlikely and not easy. + setMouseBinding(modifiers, Qt::LeftButton, CAMERA, ROTATE); + setMouseBinding(modifiers, Qt::MidButton, CAMERA, ZOOM); + setMouseBinding(modifiers, Qt::RightButton, CAMERA, TRANSLATE); + + setMouseBinding(Qt::Key_R, modifiers, Qt::LeftButton, CAMERA, SCREEN_ROTATE); + + setMouseBinding(Qt::NoModifier, Qt::LeftButton, ALIGN_CAMERA, true); + setMouseBinding(Qt::NoModifier, Qt::MidButton, SHOW_ENTIRE_SCENE, true); + setMouseBinding(Qt::NoModifier, Qt::RightButton, CENTER_SCENE, true); + + setWheelBinding(modifiers, CAMERA, ZOOM); + } +} + +//////////////////////////////////////////////////////////////////////////////// +// M a n i p u l a t e d f r a m e s // +//////////////////////////////////////////////////////////////////////////////// + +/*! Sets the viewer's manipulatedFrame(). + +Several objects can be manipulated simultaneously, as is done the multiSelect example. + +Defining the \e own viewer's camera()->frame() as the manipulatedFrame() is possible and will result +in a classical camera manipulation. See the luxo example for an +illustration. + +Note that a qglviewer::ManipulatedCameraFrame can be set as the manipulatedFrame(): it is possible +to manipulate the camera of a first viewer in a second viewer. */ +void QGLViewer::setManipulatedFrame(ManipulatedFrame* frame) +{ + if (manipulatedFrame()) + { + manipulatedFrame()->stopSpinning(); + + if (manipulatedFrame() != camera()->frame()) + { + disconnect(manipulatedFrame(), SIGNAL(manipulated()), this, SLOT(update())); + disconnect(manipulatedFrame(), SIGNAL(spun()), this, SLOT(update())); + } + } + + manipulatedFrame_ = frame; + + manipulatedFrameIsACamera_ = ((manipulatedFrame() != camera()->frame()) && + (dynamic_cast(manipulatedFrame()) != NULL)); + + if (manipulatedFrame()) + { + // Prevent multiple connections, that would result in useless display updates + if (manipulatedFrame() != camera()->frame()) + { + connect(manipulatedFrame(), SIGNAL(manipulated()), SLOT(update())); + connect(manipulatedFrame(), SIGNAL(spun()), SLOT(update())); + } + } +} + +#ifndef DOXYGEN +//////////////////////////////////////////////////////////////////////////////// +// V i s u a l H i n t s // +//////////////////////////////////////////////////////////////////////////////// +/*! Defines the mask that will be used to drawVisualHints(). The only available mask is currently 1, +corresponding to the display of the qglviewer::Camera::pivotPoint(). resetVisualHints() is +automatically called after \p delay milliseconds (default is 2 seconds). */ +void QGLViewer::setVisualHintsMask(int mask, int delay) +{ + visualHint_ = visualHint_ | mask; + QTimer::singleShot(delay, this, SLOT(resetVisualHints())); +} + +/*! Reset the mask used by drawVisualHints(). Called by setVisualHintsMask() after 2 seconds to reset the display. */ +void QGLViewer::resetVisualHints() +{ + visualHint_ = 0; +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// S t a t i c m e t h o d s : Q G L V i e w e r P o o l // +//////////////////////////////////////////////////////////////////////////////// + +/*! saveStateToFile() is called on all the QGLViewers using the QGLViewerPool(). */ +void QGLViewer::saveStateToFileForAllViewers() +{ + Q_FOREACH (QGLViewer* viewer, QGLViewer::QGLViewerPool()) + { + if (viewer) + viewer->saveStateToFile(); + } +} + +////////////////////////////////////////////////////////////////////////// +// S a v e s t a t e b e t w e e n s e s s i o n s // +////////////////////////////////////////////////////////////////////////// + +/*! Returns the state file name. Default value is \c .qglviewer.xml. + +This is the name of the XML file where saveStateToFile() saves the viewer state (camera state, +widget geometry, display flags... see domElement()) on exit. Use restoreStateFromFile() to restore +this state later (usually in your init() method). + +Setting this value to \c QString::null will disable the automatic state file saving that normally +occurs on exit. + +If more than one viewer are created by the application, this function will return a numbered file +name (as in ".qglviewer1.xml", ".qglviewer2.xml"... using QGLViewer::QGLViewerIndex()) for extra +viewers. Each viewer will then read back its own information in restoreStateFromFile(), provided +that the viewers are created in the same order, which is usually the case. */ +QString QGLViewer::stateFileName() const +{ + QString name = stateFileName_; + + if (!name.isEmpty() && QGLViewer::QGLViewerIndex(this) > 0) + { + QFileInfo fi(name); + if (fi.suffix().isEmpty()) + name += QString::number(QGLViewer::QGLViewerIndex(this)); + else + name = fi.absolutePath() + '/' + fi.completeBaseName() + QString::number(QGLViewer::QGLViewerIndex(this)) + "." + fi.suffix(); + } + + return name; +} + +/*! Saves in stateFileName() an XML representation of the QGLViewer state, obtained from +domElement(). + +Use restoreStateFromFile() to restore this viewer state. + +This method is automatically called when a viewer is closed (using Escape or using the window's +upper right \c x close button). setStateFileName() to \c QString::null to prevent this. */ +void QGLViewer::saveStateToFile() +{ + QString name = stateFileName(); + + if (name.isEmpty()) + return; + + QFileInfo fileInfo(name); + + if (fileInfo.isDir()) + { + QMessageBox::warning(this, tr("Save to file error", "Message box window title"), tr("State file name (%1) references a directory instead of a file.").arg(name)); + return; + } + + const QString dirName = fileInfo.absolutePath(); + if (!QFileInfo(dirName).exists()) + { + QDir dir; + if (!(dir.mkdir(dirName))) + { + QMessageBox::warning(this, tr("Save to file error", "Message box window title"), tr("Unable to create directory %1").arg(dirName)); + return; + } + } + + // Write the DOM tree to file + QFile f(name); + if (f.open(QIODevice::WriteOnly)) + { + QTextStream out(&f); + QDomDocument doc("QGLVIEWER"); + doc.appendChild(domElement("QGLViewer", doc)); + doc.save(out, 2); + f.flush(); + f.close(); + } + else + QMessageBox::warning(this, tr("Save to file error", "Message box window title"), tr("Unable to save to file %1").arg(name) + ":\n" + f.errorString()); +} + +/*! Restores the QGLViewer state from the stateFileName() file using initFromDOMElement(). + +States are saved using saveStateToFile(), which is automatically called on viewer exit. + +Returns \c true when the restoration is successful. Possible problems are an non existing or +unreadable stateFileName() file, an empty stateFileName() or an XML syntax error. + +A manipulatedFrame() should be defined \e before calling this method, so that its state can be +restored. Initialization code put \e after this function will override saved values: +\code +void Viewer::init() +{ +// Default initialization goes here (including the declaration of a possible manipulatedFrame). + +if (!restoreStateFromFile()) +showEntireScene(); // Previous state cannot be restored: fit camera to scene. + +// Specific initialization that overrides file savings goes here. +} +\endcode */ +bool QGLViewer::restoreStateFromFile() +{ + QString name = stateFileName(); + + if (name.isEmpty()) + return false; + + QFileInfo fileInfo(name); + + if (!fileInfo.isFile()) + // No warning since it would be displayed at first start. + return false; + + if (!fileInfo.isReadable()) + { + QMessageBox::warning(this, tr("Problem in state restoration", "Message box window title"), tr("File %1 is not readable.").arg(name)); + return false; + } + + // Read the DOM tree form file + QFile f(name); + if (f.open(QIODevice::ReadOnly)) + { + QDomDocument doc; + doc.setContent(&f); + f.close(); + QDomElement main = doc.documentElement(); + initFromDOMElement(main); + } + else + { + QMessageBox::warning(this, tr("Open file error", "Message box window title"), tr("Unable to open file %1").arg(name) + ":\n" + f.errorString()); + return false; + } + + return true; +} + +/*! Returns an XML \c QDomElement that represents the QGLViewer. + +Used by saveStateToFile(). restoreStateFromFile() uses initFromDOMElement() to restore the +QGLViewer state from the resulting \c QDomElement. + +\p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create +QDomElement. + +The created QDomElement contains state values (axisIsDrawn(), FPSIsDisplayed(), isFullScreen()...), +viewer geometry, as well as camera() (see qglviewer::Camera::domElement()) and manipulatedFrame() +(if defined, see qglviewer::ManipulatedFrame::domElement()) states. + +Overload this method to add your own attributes to the state file: +\code +QDomElement Viewer::domElement(const QString& name, QDomDocument& document) const +{ +// Creates a custom node for a light +QDomElement de = document.createElement("Light"); +de.setAttribute("state", (lightIsOn()?"on":"off")); +// Note the include of the ManipulatedFrame domElement method. +de.appendChild(lightManipulatedFrame()->domElement("LightFrame", document)); + +// Get default state domElement and append custom node +QDomElement res = QGLViewer::domElement(name, document); +res.appendChild(de); +return res; +} +\endcode +See initFromDOMElement() for the associated restoration code. + +\attention For the manipulatedFrame(), qglviewer::Frame::constraint() and +qglviewer::Frame::referenceFrame() are not saved. See qglviewer::Frame::domElement(). */ +QDomElement QGLViewer::domElement(const QString& name, QDomDocument& document) const +{ + QDomElement de = document.createElement(name); + de.setAttribute("version", QGLViewerVersionString()); + + QDomElement stateNode = document.createElement("State"); + // hasMouseTracking() is not saved + stateNode.appendChild(DomUtils::QColorDomElement(foregroundColor(), "foregroundColor", document)); + stateNode.appendChild(DomUtils::QColorDomElement(backgroundColor(), "backgroundColor", document)); + DomUtils::setBoolAttribute(stateNode, "stereo", displaysInStereo()); + // Revolve or fly camera mode is not saved + de.appendChild(stateNode); + + QDomElement displayNode = document.createElement("Display"); + DomUtils::setBoolAttribute(displayNode, "axisIsDrawn", axisIsDrawn()); + DomUtils::setBoolAttribute(displayNode, "gridIsDrawn", gridIsDrawn()); + DomUtils::setBoolAttribute(displayNode, "FPSIsDisplayed", FPSIsDisplayed()); + DomUtils::setBoolAttribute(displayNode, "cameraIsEdited", cameraIsEdited()); + // textIsEnabled() is not saved + de.appendChild(displayNode); + + QDomElement geometryNode = document.createElement("Geometry"); + DomUtils::setBoolAttribute(geometryNode, "fullScreen", isFullScreen()); + if (isFullScreen()) + { + geometryNode.setAttribute("prevPosX", QString::number(prevPos_.x())); + geometryNode.setAttribute("prevPosY", QString::number(prevPos_.y())); + } + else + { + QWidget* tlw = topLevelWidget(); + geometryNode.setAttribute("width", QString::number(tlw->width())); + geometryNode.setAttribute("height", QString::number(tlw->height())); + geometryNode.setAttribute("posX", QString::number(tlw->pos().x())); + geometryNode.setAttribute("posY", QString::number(tlw->pos().y())); + } + de.appendChild(geometryNode); + + // Restore original Camera zClippingCoefficient before saving. + if (cameraIsEdited()) + camera()->setZClippingCoefficient(previousCameraZClippingCoefficient_); + de.appendChild(camera()->domElement("Camera", document)); + if (cameraIsEdited()) + // #CONNECTION# 5.0 from setCameraIsEdited() + camera()->setZClippingCoefficient(5.0); + + if (manipulatedFrame()) + de.appendChild(manipulatedFrame()->domElement("ManipulatedFrame", document)); + + return de; +} + +/*! Restores the QGLViewer state from a \c QDomElement created by domElement(). + +Used by restoreStateFromFile() to restore the QGLViewer state from a file. + +Overload this method to retrieve custom attributes from the QGLViewer state file. This code +corresponds to the one given in the domElement() documentation: +\code +void Viewer::initFromDOMElement(const QDomElement& element) +{ +// Restore standard state +QGLViewer::initFromDOMElement(element); + +QDomElement child=element.firstChild().toElement(); +while (!child.isNull()) +{ +if (child.tagName() == "Light") +{ +if (child.hasAttribute("state")) +setLightOn(child.attribute("state").lower() == "on"); + +// Assumes there is only one child. Otherwise you need to parse child's children recursively. +QDomElement lf = child.firstChild().toElement(); +if (!lf.isNull() && lf.tagName() == "LightFrame") +lightManipulatedFrame()->initFromDomElement(lf); +} +child = child.nextSibling().toElement(); +} +} +\endcode + +See also qglviewer::Camera::initFromDOMElement(), qglviewer::ManipulatedFrame::initFromDOMElement(). + +\note The manipulatedFrame() \e pointer is not modified by this method. If defined, its state is +simply set from the \p element values. */ +void QGLViewer::initFromDOMElement(const QDomElement& element) +{ + const QString version = element.attribute("version"); + // if (version != QGLViewerVersionString()) + if (version[0] != '2') + // Patches for previous versions should go here when the state file syntax is modified. + qWarning("State file created using QGLViewer version %s may not be correctly read.", version.toLatin1().constData()); + + QDomElement child=element.firstChild().toElement(); + bool tmpCameraIsEdited = cameraIsEdited(); + while (!child.isNull()) + { + if (child.tagName() == "State") + { + // #CONNECTION# default values from defaultConstructor() + // setMouseTracking(DomUtils::boolFromDom(child, "mouseTracking", false)); + setStereoDisplay(DomUtils::boolFromDom(child, "stereo", false)); + //if ((child.attribute("cameraMode", "revolve") == "fly") && (cameraIsInRevolveMode())) + // toggleCameraMode(); + + QDomElement ch=child.firstChild().toElement(); + while (!ch.isNull()) + { + if (ch.tagName() == "foregroundColor") + setForegroundColor(DomUtils::QColorFromDom(ch)); + if (ch.tagName() == "backgroundColor") + setBackgroundColor(DomUtils::QColorFromDom(ch)); + ch = ch.nextSibling().toElement(); + } + } + + if (child.tagName() == "Display") + { + // #CONNECTION# default values from defaultConstructor() + setAxisIsDrawn(DomUtils::boolFromDom(child, "axisIsDrawn", false)); + setGridIsDrawn(DomUtils::boolFromDom(child, "gridIsDrawn", false)); + setFPSIsDisplayed(DomUtils::boolFromDom(child, "FPSIsDisplayed", false)); + // See comment below. + tmpCameraIsEdited = DomUtils::boolFromDom(child, "cameraIsEdited", false); + // setTextIsEnabled(DomUtils::boolFromDom(child, "textIsEnabled", true)); + } + + if (child.tagName() == "Geometry") + { + setFullScreen(DomUtils::boolFromDom(child, "fullScreen", false)); + + if (isFullScreen()) + { + prevPos_.setX(DomUtils::intFromDom(child, "prevPosX", 0)); + prevPos_.setY(DomUtils::intFromDom(child, "prevPosY", 0)); + } + else + { + int width = DomUtils::intFromDom(child, "width", 600); + int height = DomUtils::intFromDom(child, "height", 400); + topLevelWidget()->resize(width, height); + camera()->setScreenWidthAndHeight(this->width(), this->height()); + + QPoint pos; + pos.setX(DomUtils::intFromDom(child, "posX", 0)); + pos.setY(DomUtils::intFromDom(child, "posY", 0)); + topLevelWidget()->move(pos); + } + } + + if (child.tagName() == "Camera") + { + connectAllCameraKFIInterpolatedSignals(false); + camera()->initFromDOMElement(child); + connectAllCameraKFIInterpolatedSignals(); + } + + if ((child.tagName() == "ManipulatedFrame") && (manipulatedFrame())) + manipulatedFrame()->initFromDOMElement(child); + + child = child.nextSibling().toElement(); + } + + // The Camera always stores its "real" zClippingCoef in domElement(). If it is edited, + // its "real" coef must be saved and the coef set to 5.0, as is done in setCameraIsEdited(). + // BUT : Camera and Display are read in an arbitrary order. We must initialize Camera's + // "real" coef BEFORE calling setCameraIsEdited. Hence this temp cameraIsEdited and delayed call + cameraIsEdited_ = tmpCameraIsEdited; + if (cameraIsEdited_) + { + previousCameraZClippingCoefficient_ = camera()->zClippingCoefficient(); + // #CONNECTION# 5.0 from setCameraIsEdited. + camera()->setZClippingCoefficient(5.0); + } +} + +#ifndef DOXYGEN +/*! This method is deprecated since version 1.3.9-5. Use saveStateToFile() and setStateFileName() +instead. */ +void QGLViewer::saveToFile(const QString& fileName) +{ + if (!fileName.isEmpty()) + setStateFileName(fileName); + + qWarning("saveToFile() is deprecated, use saveStateToFile() instead."); + saveStateToFile(); +} + +/*! This function is deprecated since version 1.3.9-5. Use restoreStateFromFile() and +setStateFileName() instead. */ +bool QGLViewer::restoreFromFile(const QString& fileName) +{ + if (!fileName.isEmpty()) + setStateFileName(fileName); + + qWarning("restoreFromFile() is deprecated, use restoreStateFromFile() instead."); + return restoreStateFromFile(); +} +#endif + +/*! Makes a copy of the current buffer into a texture. + +Creates a texture (when needed) and uses glCopyTexSubImage2D() to directly copy the buffer in it. + +Use \p internalFormat and \p format to define the texture format and hence which and how components +of the buffer are copied into the texture. See the glTexImage2D() documentation for details. + +When \p format is c GL_NONE (default), its value is set to \p internalFormat, which fits most +cases. Typical \p internalFormat (and \p format) values are \c GL_DEPTH_COMPONENT and \c GL_RGBA. +Use \c GL_LUMINANCE as the \p internalFormat and \c GL_RED, \c GL_GREEN or \c GL_BLUE as \p format +to capture a single color component as a luminance (grey scaled) value. Note that \c GL_STENCIL is +not supported as a format. + +The texture has dimensions which are powers of two. It is as small as possible while always being +larger or equal to the current size of the widget. The buffer image hence does not entirely fill +the texture: it is stuck to the lower left corner (corresponding to the (0,0) texture coordinates). +Use bufferTextureMaxU() and bufferTextureMaxV() to get the upper right corner maximum u and v +texture coordinates. Use bufferTextureId() to retrieve the id of the created texture. + +Here is how to display a grey-level image of the z-buffer: +\code +copyBufferToTexture(GL_DEPTH_COMPONENT); + +glMatrixMode(GL_TEXTURE); +glLoadIdentity(); + +glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE); +glEnable(GL_TEXTURE_2D); + +startScreenCoordinatesSystem(true); + +glBegin(GL_QUADS); +glTexCoord2f(0.0, 0.0); glVertex2i(0, 0); +glTexCoord2f(bufferTextureMaxU(), 0.0); glVertex2i(width(), 0); +glTexCoord2f(bufferTextureMaxU(), bufferTextureMaxV()); glVertex2i(width(), height()); +glTexCoord2f(0.0, bufferTextureMaxV()); glVertex2i(0, height()); +glEnd(); + +stopScreenCoordinatesSystem(); + +glDisable(GL_TEXTURE_2D); +\endcode + +Use glReadBuffer() to select which buffer is copied into the texture. See also \c +glPixelTransfer(), \c glPixelZoom() and \c glCopyPixel() for pixel color transformations during +copy. + +Call makeCurrent() before this method to make the OpenGL context active if needed. + +\note The \c GL_DEPTH_COMPONENT format may not be supported by all hardware. It may sometimes be +emulated in software, resulting in poor performances. + +\note The bufferTextureId() texture is binded at the end of this method. */ +void QGLViewer::copyBufferToTexture(GLint internalFormat, GLenum format) +{ + int h = 16; + int w = 16; + // Todo compare performance with qt code. + while (w < width()) + w <<= 1; + while (h < height()) + h <<= 1; + + bool init = false; + + if ((w != bufferTextureWidth_) || (h != bufferTextureHeight_)) + { + bufferTextureWidth_ = w; + bufferTextureHeight_ = h; + bufferTextureMaxU_ = width() / qreal(bufferTextureWidth_); + bufferTextureMaxV_ = height() / qreal(bufferTextureHeight_); + init = true; + } + + if (bufferTextureId() == 0) + { + glGenTextures(1, &bufferTextureId_); + glBindTexture(GL_TEXTURE_2D, bufferTextureId_); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + init = true; + } + else + glBindTexture(GL_TEXTURE_2D, bufferTextureId_); + + if ((format != previousBufferTextureFormat_) || + (internalFormat != previousBufferTextureInternalFormat_)) + { + previousBufferTextureFormat_ = format; + previousBufferTextureInternalFormat_ = internalFormat; + init = true; + } + + if (init) + { + if (format == GL_NONE) + format = GLenum(internalFormat); + + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, bufferTextureWidth_, bufferTextureHeight_, 0, format, GL_UNSIGNED_BYTE, NULL); + } + + glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width(), height()); +} + +/*! Returns the texture id of the texture created by copyBufferToTexture(). + +Use glBindTexture() to use this texture. Note that this is already done by copyBufferToTexture(). + +Returns \c 0 is copyBufferToTexture() was never called or if the texure was deleted using +glDeleteTextures() since then. */ +GLuint QGLViewer::bufferTextureId() const +{ + if (glIsTexture(bufferTextureId_)) + return bufferTextureId_; + else + return 0; +} diff --git a/QGLViewer/qglviewer.h b/QGLViewer/qglviewer.h new file mode 100644 index 0000000..c5fc3df --- /dev/null +++ b/QGLViewer/qglviewer.h @@ -0,0 +1,1278 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_QGLVIEWER_H +#define QGLVIEWER_QGLVIEWER_H + +#include "camera.h" + +#include +#include +#include + +class QTabWidget; + +namespace qglviewer { +class MouseGrabber; +class ManipulatedFrame; +class ManipulatedCameraFrame; +} + +/*! \brief A versatile 3D OpenGL viewer based on QOpenGLWidget. +\class QGLViewer qglviewer.h QGLViewer/qglviewer.h + +It features many classical viewer functionalities, such as a camera trackball, manipulated objects, +snapshot saving and much more. Its main goal is to ease the development +of new 3D applications. + +New users should read the introduction page to get familiar with +important notions such as sceneRadius(), sceneCenter() and the world coordinate system. Try the +numerous simple examples to discover the possibilities and +understand how it works. + +

Usage

+ +To use a QGLViewer, derive you viewer class from the QGLViewer and overload its draw() virtual +method. See the simpleViewer example for an illustration. + +An other option is to connect your drawing methods to the signals emitted by the QGLViewer (Qt's +callback mechanism). See the callback example for a +complete implementation. + +\nosubgrouping */ +class QGLVIEWER_EXPORT QGLViewer : public QOpenGLWidget +{ + Q_OBJECT + +public: + // Complete implementation is provided so that the constructor is defined with QT3_SUPPORT when .h is included. + // (Would not be available otherwise since lib is compiled without QT3_SUPPORT). +#ifdef QT3_SUPPORT + explicit QGLViewer(QWidget* parent=NULL, const char* name=0, const QOpenGLWidget* shareWidget=0, Qt::WindowFlags flags=0) + : QOpenGLWidget(parent, name, shareWidget, flags) + { defaultConstructor(); } + + explicit QGLViewer(const QSurfaceFormat& format, QWidget* parent=0, const char* name=0, const QOpenGLWidget* shareWidget=0,Qt::WindowFlags flags=0) + : QOpenGLWidget(format, parent, name, shareWidget, flags) + { defaultConstructor(); } + + QGLViewer(QOpenGLContext* context, QWidget* parent, const char* name=0, const QOpenGLWidget* shareWidget=0, Qt::WindowFlags flags=0) + : QOpenGLWidget(context, parent, name, shareWidget, flags) { + defaultConstructor(); } + +#else + + explicit QGLViewer(QWidget* parent=0, Qt::WindowFlags flags=0); + explicit QGLViewer(QOpenGLContext *context, QWidget* parent=0, const QOpenGLWidget* shareWidget=0, Qt::WindowFlags flags=0); + explicit QGLViewer(const QSurfaceFormat& format, QWidget* parent=0, const QOpenGLWidget* shareWidget=0, Qt::WindowFlags flags=0); +#endif + + virtual ~QGLViewer(); + + /*! @name Display of visual hints */ + //@{ +public: + /*! Returns \c true if the world axis is drawn by the viewer. + + Set by setAxisIsDrawn() or toggleAxisIsDrawn(). Default value is \c false. */ + bool axisIsDrawn() const { return axisIsDrawn_; } + /*! Returns \c true if a XY grid is drawn by the viewer. + + Set by setGridIsDrawn() or toggleGridIsDrawn(). Default value is \c false. */ + bool gridIsDrawn() const { return gridIsDrawn_; } + /*! Returns \c true if the viewer displays the current frame rate (Frames Per Second). + + Use QApplication::setFont() to define the display font (see drawText()). + + Set by setFPSIsDisplayed() or toggleFPSIsDisplayed(). Use currentFPS() to get the current FPS. + Default value is \c false. */ + bool FPSIsDisplayed() const { return FPSIsDisplayed_; } + /*! Returns \c true if text display (see drawText()) is enabled. + + Set by setTextIsEnabled() or toggleTextIsEnabled(). This feature conveniently removes all the + possibly displayed text, cleaning display. Default value is \c true. */ + bool textIsEnabled() const { return textIsEnabled_; } + + /*! Returns \c true if the camera() is being edited in the viewer. + + Set by setCameraIsEdited() or toggleCameraIsEdited(). Default value is \p false. + + The current implementation is limited: the defined camera() paths (see + qglviewer::Camera::keyFrameInterpolator()) are simply displayed using + qglviewer::Camera::drawAllPaths(). Actual camera and path edition will be implemented in the + future. */ + bool cameraIsEdited() const { return cameraIsEdited_; } + + +public Q_SLOTS: + /*! Sets the state of axisIsDrawn(). Emits the axisIsDrawnChanged() signal. See also toggleAxisIsDrawn(). */ + void setAxisIsDrawn(bool draw=true) { axisIsDrawn_ = draw; Q_EMIT axisIsDrawnChanged(draw); update(); } + /*! Sets the state of gridIsDrawn(). Emits the gridIsDrawnChanged() signal. See also toggleGridIsDrawn(). */ + void setGridIsDrawn(bool draw=true) { gridIsDrawn_ = draw; Q_EMIT gridIsDrawnChanged(draw); update(); } + /*! Sets the state of FPSIsDisplayed(). Emits the FPSIsDisplayedChanged() signal. See also toggleFPSIsDisplayed(). */ + void setFPSIsDisplayed(bool display=true) { FPSIsDisplayed_ = display; Q_EMIT FPSIsDisplayedChanged(display); update(); } + /*! Sets the state of textIsEnabled(). Emits the textIsEnabledChanged() signal. See also toggleTextIsEnabled(). */ + void setTextIsEnabled(bool enable=true) { textIsEnabled_ = enable; Q_EMIT textIsEnabledChanged(enable); update(); } + void setCameraIsEdited(bool edit=true); + + /*! Toggles the state of axisIsDrawn(). See also setAxisIsDrawn(). */ + void toggleAxisIsDrawn() { setAxisIsDrawn(!axisIsDrawn()); } + /*! Toggles the state of gridIsDrawn(). See also setGridIsDrawn(). */ + void toggleGridIsDrawn() { setGridIsDrawn(!gridIsDrawn()); } + /*! Toggles the state of FPSIsDisplayed(). See also setFPSIsDisplayed(). */ + void toggleFPSIsDisplayed() { setFPSIsDisplayed(!FPSIsDisplayed()); } + /*! Toggles the state of textIsEnabled(). See also setTextIsEnabled(). */ + void toggleTextIsEnabled() { setTextIsEnabled(!textIsEnabled()); } + /*! Toggles the state of cameraIsEdited(). See also setCameraIsEdited(). */ + void toggleCameraIsEdited() { setCameraIsEdited(!cameraIsEdited()); } + //@} + + + /*! @name Viewer's colors */ + //@{ +public: + /*! Returns the background color of the viewer. + + This method is provided for convenience since the background color is an OpenGL state variable + set with \c glClearColor(). However, this internal representation has the advantage that it is + saved (resp. restored) with saveStateToFile() (resp. restoreStateFromFile()). + + Use setBackgroundColor() to define and activate a background color. + + \attention Each QColor component is an integer ranging from 0 to 255. This differs from the qreal + values used by \c glClearColor() which are in the 0.0-1.0 range. Default value is (51, 51, 51) + (dark gray). You may have to change foregroundColor() accordingly. + + \attention This method does not return the current OpenGL clear color as \c glGet() does. Instead, + it returns the QGLViewer internal variable. If you directly use \c glClearColor() or \c + qglClearColor() instead of setBackgroundColor(), the two results will differ. */ + QColor backgroundColor() const { return backgroundColor_; } + + /*! Returns the foreground color used by the viewer. + + This color is used when FPSIsDisplayed(), gridIsDrawn(), to display the camera paths when the + cameraIsEdited(). + + \attention Each QColor component is an integer in the range 0-255. This differs from the qreal + values used by \c glColor3f() which are in the range 0-1. Default value is (180, 180, 180) (light + gray). + + Use \c qglColor(foregroundColor()) to set the current OpenGL color to the foregroundColor(). + + See also backgroundColor(). */ + QColor foregroundColor() const { return foregroundColor_; } +public Q_SLOTS: + /*! Sets the backgroundColor() of the viewer and calls \c qglClearColor(). See also + setForegroundColor(). */ + void setBackgroundColor(const QColor& color) + { + backgroundColor_=color; + glClearColor( // FORMERLY qglClearColor(color) + static_cast(color.redF()), + static_cast(color.greenF()), + static_cast(color.blueF()), + static_cast(color.alphaF())); + } + + /*! Sets the foregroundColor() of the viewer, used to draw visual hints. See also setBackgroundColor(). */ + void setForegroundColor(const QColor& color) { foregroundColor_ = color; } + //@} + + + /*! @name Scene dimensions */ + //@{ +public: + /*! Returns the scene radius. + + The entire displayed scene should be included in a sphere of radius sceneRadius(), centered on + sceneCenter(). + + This approximate value is used by the camera() to set qglviewer::Camera::zNear() and + qglviewer::Camera::zFar(). It is also used to showEntireScene() or to scale the world axis + display.. + + Default value is 1.0. This method is equivalent to camera()->sceneRadius(). See + setSceneRadius(). */ + qreal sceneRadius() const { return camera()->sceneRadius(); } + /*! Returns the scene center, defined in world coordinates. + + See sceneRadius() for details. + + Default value is (0,0,0). Simply a wrapper for camera()->sceneCenter(). Set using + setSceneCenter(). + + Do not mismatch this value (that only depends on the scene) with the qglviewer::Camera::pivotPoint(). */ + qglviewer::Vec sceneCenter() const { return camera()->sceneCenter(); } + +public Q_SLOTS: + /*! Sets the sceneRadius(). + + The camera() qglviewer::Camera::flySpeed() is set to 1% of this value by this method. Simple + wrapper around camera()->setSceneRadius(). */ + virtual void setSceneRadius(qreal radius) { camera()->setSceneRadius(radius); } + + /*! Sets the sceneCenter(), defined in world coordinates. + + \attention The qglviewer::Camera::pivotPoint() is set to the sceneCenter() value by this + method. */ + virtual void setSceneCenter(const qglviewer::Vec& center) { camera()->setSceneCenter(center); } + + /*! Convenient way to call setSceneCenter() and setSceneRadius() from a (world axis aligned) bounding box of the scene. + + This is equivalent to: + \code + setSceneCenter((min+max) / 2.0); + setSceneRadius((max-min).norm() / 2.0); + \endcode */ + void setSceneBoundingBox(const qglviewer::Vec& min, const qglviewer::Vec& max) { camera()->setSceneBoundingBox(min,max); } + + /*! Moves the camera so that the entire scene is visible. + + Simple wrapper around qglviewer::Camera::showEntireScene(). */ + void showEntireScene() { camera()->showEntireScene(); update(); } + //@} + + + /*! @name Associated objects */ + //@{ +public: + /*! Returns the associated qglviewer::Camera, never \c NULL. */ + qglviewer::Camera* camera() const { return camera_; } + + /*! Returns the viewer's qglviewer::ManipulatedFrame. + + This qglviewer::ManipulatedFrame can be moved with the mouse when the associated mouse bindings + are used (default is when pressing the \c Control key with any mouse button). Use + setMouseBinding() to define new bindings. + + See the manipulatedFrame example for a complete + implementation. + + Default value is \c NULL, meaning that no qglviewer::ManipulatedFrame is set. */ + qglviewer::ManipulatedFrame* manipulatedFrame() const { return manipulatedFrame_; } + +public Q_SLOTS: + void setCamera(qglviewer::Camera* const camera); + void setManipulatedFrame(qglviewer::ManipulatedFrame* frame); + //@} + + + /*! @name Mouse grabbers */ + //@{ +public: + /*! Returns the current qglviewer::MouseGrabber, or \c NULL if no qglviewer::MouseGrabber + currently grabs mouse events. + + When qglviewer::MouseGrabber::grabsMouse(), the different mouse events are sent to the + mouseGrabber() instead of their usual targets (camera() or manipulatedFrame()). + + See the qglviewer::MouseGrabber documentation for details on MouseGrabber's mode of operation. + + In order to use MouseGrabbers, you need to enable mouse tracking (so that mouseMoveEvent() is + called even when no mouse button is pressed). Add this line in init() or in your viewer + constructor: + \code + setMouseTracking(true); + \endcode + Note that mouse tracking is disabled by default. Use QWidget::hasMouseTracking() to + retrieve current state. */ + qglviewer::MouseGrabber* mouseGrabber() const { return mouseGrabber_; } + + void setMouseGrabberIsEnabled(const qglviewer::MouseGrabber* const mouseGrabber, bool enabled=true); + /*! Returns \c true if \p mouseGrabber is enabled. + + Default value is \c true for all MouseGrabbers. When set to \c false using + setMouseGrabberIsEnabled(), the specified \p mouseGrabber will never become the mouseGrabber() of + this QGLViewer. This is useful when you use several viewers: some MouseGrabbers may only have a + meaning for some specific viewers and should not be selectable in others. + + You can also use qglviewer::MouseGrabber::removeFromMouseGrabberPool() to completely disable a + MouseGrabber in all the QGLViewers. */ + bool mouseGrabberIsEnabled(const qglviewer::MouseGrabber* const mouseGrabber) { return !disabledMouseGrabbers_.contains(reinterpret_cast(mouseGrabber)); } +public Q_SLOTS: + void setMouseGrabber(qglviewer::MouseGrabber* mouseGrabber); + //@} + + + /*! @name State of the viewer */ + //@{ +public: + /*! Returns the aspect ratio of the viewer's widget (width() / height()). */ + qreal aspectRatio() const { return width() / static_cast(height()); } + /*! Returns the current averaged viewer frame rate. + + This value is computed and averaged over 20 successive frames. It only changes every 20 draw() + (previously computed value is otherwise returned). + + This method is useful for true real-time applications that may adapt their computational load + accordingly in order to maintain a given frequency. + + This value is meaningful only when draw() is regularly called, either using a \c QTimer, when + animationIsStarted() or when the camera is manipulated with the mouse. */ + qreal currentFPS() { return f_p_s_; } + /*! Returns \c true if the viewer is in fullScreen mode. + + Default value is \c false. Set by setFullScreen() or toggleFullScreen(). + + Note that if the QGLViewer is embedded in an other QWidget, it returns \c true when the top level + widget is in full screen mode. */ + bool isFullScreen() const { return fullScreen_; } + /*! Returns \c true if the viewer displays in stereo. + + The QGLViewer object must be created with a stereo format to handle stereovision: + \code + QGLFormat format; + format.setStereoDisplay( TRUE ); + QGLViewer viewer(format); + \endcode + The hardware needs to support stereo display. Try the stereoViewer example to check. + + Set by setStereoDisplay() or toggleStereoDisplay(). Default value is \c false. + + Stereo is performed using the Parallel axis asymmetric frustum perspective projection method. + See Camera::loadProjectionMatrixStereo() and Camera::loadModelViewMatrixStereo(). + + The stereo parameters are defined by the camera(). See qglviewer::Camera::setIODistance(), + qglviewer::Camera::setPhysicalScreenWidth() and + qglviewer::Camera::setFocusDistance(). */ + bool displaysInStereo() const { return stereo_; } + /*! Returns the recommended size for the QGLViewer. Default value is 600x400 pixels. */ + virtual QSize sizeHint() const { return QSize(600, 400); } + +public Q_SLOTS: + void setFullScreen(bool fullScreen=true); + void setStereoDisplay(bool stereo=true); + /*! Toggles the state of isFullScreen(). See also setFullScreen(). */ + void toggleFullScreen() { setFullScreen(!isFullScreen()); } + /*! Toggles the state of displaysInStereo(). See setStereoDisplay(). */ + void toggleStereoDisplay() { setStereoDisplay(!stereo_); } + void toggleCameraMode(); + +private: + bool cameraIsInRotateMode() const; + //@} + + + /*! @name Display methods */ + //@{ +public: + void displayMessage(const QString& message, int delay=2000); + // void draw3DText(const qglviewer::Vec& pos, const qglviewer::Vec& normal, const QString& string, GLfloat height=0.1); + +private: + void displayFPS(); + /*! Vectorial rendering callback method. */ + void drawVectorial() { paintGL(); } + +#ifndef DOXYGEN + friend void drawVectorial(void* param); +#endif + //@} + + +#ifdef DOXYGEN + /*! @name Useful inherited methods */ + //@{ +public: + /*! Returns viewer's widget width (in pixels). See QGLWidget documentation. */ + int width() const; + /*! Returns viewer's widget height (in pixels). See QGLWidget documentation. */ + int height() const; + /*! Updates the display. Do not call draw() directly, use this method instead. See QGLWidget documentation. */ + virtual void updateGL(); + /*! Converts \p image into the unnamed format expected by OpenGL methods such as glTexImage2D(). + See QGLWidget documentation. */ + static QImage convertToGLFormat(const QImage & image); + /*! Calls \c glColor3. See QGLWidget::qglColor(). */ + void qglColor(const QColor& color) const; + /*! Calls \c glClearColor. See QGLWidget documentation. */ + void qglClearColor(const QColor& color) const; + /*! Returns \c true if the widget has a valid GL rendering context. See QGLWidget + documentation. */ + bool isValid() const; + /*! Returns \c true if display list sharing with another QGLWidget was requested in the + constructor. See QGLWidget documentation. */ + bool isSharing() const; + /*! Makes this widget's rendering context the current OpenGL rendering context. Useful with + several viewers. See QGLWidget documentation. */ + virtual void makeCurrent(); + /*! Returns \c true if mouseMoveEvent() is called even when no mouse button is pressed. + + You need to setMouseTracking() to \c true in order to use MouseGrabber (see mouseGrabber()). See + details in the QWidget documentation. */ + bool hasMouseTracking () const; +public Q_SLOTS: + /*! Resizes the widget to size \p width by \p height pixels. See also width() and height(). */ + virtual void resize(int width, int height); + /*! Sets the hasMouseTracking() value. */ + virtual void setMouseTracking(bool enable); +protected: + /*! Returns \c true when buffers are automatically swapped (default). See details in the QGLWidget + documentation. */ + bool autoBufferSwap() const; +protected Q_SLOTS: + /*! Sets the autoBufferSwap() value. */ + void setAutoBufferSwap(bool on); + //@} +#endif + + + /*! @name Snapshots */ + //@{ +public: + /*! Returns the snapshot file name used by saveSnapshot(). + + This value is used in \p automatic mode (see saveSnapshot()). A dialog is otherwise popped-up to + set it. + + You can also directly provide a file name using saveSnapshot(const QString&, bool). + + If the file name is relative, the current working directory at the moment of the method call is + used. Set using setSnapshotFileName(). */ + const QString& snapshotFileName() const { return snapshotFileName_; } +#ifndef DOXYGEN + const QString& snapshotFilename() const; +#endif + /*! Returns the snapshot file format used by saveSnapshot(). + + This value is used when saveSnapshot() is passed the \p automatic flag. It is defined using a + saveAs pop-up dialog otherwise. + + The available formats are those handled by Qt. Classical values are \c "JPEG", \c "PNG", + \c "PPM", \c "BMP". Use the following code to get the actual list: + \code + QList formatList = QImageReader::supportedImageFormats(); + // or with Qt version 2 or 3: + QStringList formatList = QImage::outputFormatList(); + \endcode + + If the library was compiled with the vectorial rendering option (default), three additional + vectorial formats are available: \c "EPS", \c "PS" and \c "XFIG". \c "SVG" and \c "PDF" formats + should soon be available. The VRender library + was created by Cyril Soler. + + Note that the VRender library has some limitations: vertex shader effects are not reproduced and + \c PASS_THROUGH tokens are not handled so one can not change point and line size in the middle of + a drawing. + + Default value is the first supported among "JPEG, PNG, EPS, PS, PPM, BMP", in that order. + + This value is set using setSnapshotFormat() or with openSnapshotFormatDialog(). + + \attention No verification is performed on the provided format validity. The next call to + saveSnapshot() may fail if the format string is not supported. */ + const QString& snapshotFormat() const { return snapshotFormat_; } + /*! Returns the value of the counter used to name snapshots in saveSnapshot() when \p automatic is + \c true. + + Set using setSnapshotCounter(). Default value is 0, and it is incremented after each \p automatic + snapshot. See saveSnapshot() for details. */ + int snapshotCounter() const { return snapshotCounter_; } + /*! Defines the image quality of the snapshots produced with saveSnapshot(). + + Values must be in the range -1..100. Use 0 for lowest quality and 100 for highest quality (and + larger files). -1 means use Qt default quality. Default value is 95. + + Set using setSnapshotQuality(). See also the QImage::save() documentation. + + \note This value has no impact on the images produced in vectorial format. */ + int snapshotQuality() { return snapshotQuality_; } + + // Qt 2.3 does not support qreal default value parameters in slots. + // Remove "Q_SLOTS" from the following line to compile with Qt 2.3 +public Q_SLOTS: + void saveSnapshot(bool automatic=true, bool overwrite=false); + +public Q_SLOTS: + void saveSnapshot(const QString& fileName, bool overwrite=false); + void setSnapshotFileName(const QString& name); + + /*! Sets the snapshotFormat(). */ + void setSnapshotFormat(const QString& format) { snapshotFormat_ = format; } + /*! Sets the snapshotCounter(). */ + void setSnapshotCounter(int counter) { snapshotCounter_ = counter; } + /*! Sets the snapshotQuality(). */ + void setSnapshotQuality(int quality) { snapshotQuality_ = quality; } + bool openSnapshotFormatDialog(); + void snapshotToClipboard(); + +private: + bool saveImageSnapshot(const QString& fileName); + +#ifndef DOXYGEN + /* This class is used internally for screenshot that require tiling (image size size different + from window size). Only in that case, is the private tileRegion_ pointer non null. + It then contains the current tiled region, which is used by startScreenCoordinatesSystem + to adapt the coordinate system. Not using it would result in a tiled drawing of the parts + that use startScreenCoordinatesSystem. Also used by scaledFont for same purposes. */ + class TileRegion { public : qreal xMin, yMin, xMax, yMax, textScale; }; +#endif + +public: + /*! Return a possibly scaled version of \p font, used for snapshot rendering. + + From a user's point of view, this method simply returns \p font and can be used transparently. + + However when internally rendering a screen snapshot using saveSnapshot(), it returns a scaled version + of the font, so that the size of the rendered text on the snapshot is identical to what is displayed on screen, + even if the snapshot uses image tiling to create an image of dimensions different from those of the + current window. This scaled version will only be used when saveSnapshot() calls your draw() method + to generate the snapshot. + + All your calls to QGLWidget::renderText() function hence should use this method. + \code + renderText(x, y, z, "My Text", scaledFont(QFont())); + \endcode + will guarantee that this text will be properly displayed on arbitrary sized snapshots. + + Note that this method is not needed if you use drawText() which already calls it internally. */ + QFont scaledFont(const QFont& font) const { + if (tileRegion_ == NULL) + return font; + else { + QFont f(font); + if (f.pixelSize() == -1) + f.setPointSizeF(f.pointSizeF() * tileRegion_->textScale); + else + f.setPixelSize(int(f.pixelSize() * tileRegion_->textScale)); + return f; + } + } + //@} + + + /*! @name Buffer to texture */ + //@{ +public: + GLuint bufferTextureId() const; + /*! Returns the texture coordinate corresponding to the u extremum of the bufferTexture. + + The bufferTexture is created by copyBufferToTexture(). The texture size has powers of two + dimensions and the buffer image hence only fills a part of it. This value corresponds to the u + coordinate of the extremum right side of the buffer image. + + Use (0,0) to (bufferTextureMaxU(), bufferTextureMaxV()) texture coordinates to map the entire + texture on a quad. */ + qreal bufferTextureMaxU() const { return bufferTextureMaxU_; } + /*! Same as bufferTextureMaxU(), but for the v texture coordinate. */ + qreal bufferTextureMaxV() const { return bufferTextureMaxV_; } +public Q_SLOTS: + void copyBufferToTexture(GLint internalFormat, GLenum format=GL_NONE); + //@} + + /*! @name Animation */ + //@{ +public: + /*! Return \c true when the animation loop is started. + + During animation, an infinite loop calls animate() and draw() and then waits for animationPeriod() + milliseconds before calling animate() and draw() again. And again. + + Use startAnimation(), stopAnimation() or toggleAnimation() to change this value. + + See the animation example for illustration. */ + bool animationIsStarted() const { return animationStarted_; } + /*! The animation loop period, in milliseconds. + + When animationIsStarted(), this is delay waited after draw() to call animate() and draw() again. + Default value is 40 milliseconds (25 Hz). + + This value will define the currentFPS() when animationIsStarted() (provided that your animate() + and draw() methods are fast enough). + + If you want to know the maximum possible frame rate of your machine on a given scene, + setAnimationPeriod() to \c 0, and startAnimation() (keyboard shortcut is \c Enter). The display + will then be updated as often as possible, and the frame rate will be meaningful. + + \note This value is taken into account only the next time you call startAnimation(). If + animationIsStarted(), you should stopAnimation() first. */ + int animationPeriod() const { return animationPeriod_; } + +public Q_SLOTS: + /*! Sets the animationPeriod(), in milliseconds. */ + void setAnimationPeriod(int period) { animationPeriod_ = period; } + virtual void startAnimation(); + virtual void stopAnimation(); + /*! Scene animation method. + + When animationIsStarted(), this method is in charge of the scene update before each draw(). + Overload it to define how your scene evolves over time. The time should either be regularly + incremented in this method (frame-rate independent animation) or computed from actual time (for + instance using QTime::elapsed()) for real-time animations. + + Note that KeyFrameInterpolator (which regularly updates a Frame) does not use this method + to animate a Frame, but rather rely on a QTimer signal-slot mechanism. + + See the animation example for an illustration. */ + virtual void animate() { Q_EMIT animateNeeded(); } + /*! Calls startAnimation() or stopAnimation(), depending on animationIsStarted(). */ + void toggleAnimation() { if (animationIsStarted()) stopAnimation(); else startAnimation(); } + //@} + +public: +Q_SIGNALS: + /*! Signal emitted by the default init() method. + + Connect this signal to the methods that need to be called to initialize your viewer or overload init(). */ + void viewerInitialized(); + + /*! Signal emitted by the default draw() method. + + Connect this signal to your main drawing method or overload draw(). See the callback example for an illustration. */ + void drawNeeded(); + + /*! Signal emitted at the end of the QGLViewer::paintGL() method, when frame is drawn. + + Can be used to notify an image grabbing process that the image is ready. A typical example is to + connect this signal to the saveSnapshot() method, so that a (numbered) snapshot is generated after + each new display, in order to create a movie: + \code + connect(viewer, SIGNAL(drawFinished(bool)), SLOT(saveSnapshot(bool))); + \endcode + + The \p automatic bool variable is always \c true and has been added so that the signal can be + connected to saveSnapshot() with an \c automatic value set to \c true. */ + void drawFinished(bool automatic); + + /*! Signal emitted by the default animate() method. + + Connect this signal to your scene animation method or overload animate(). */ + void animateNeeded(); + + /*! Signal emitted by the default QGLViewer::help() method. + + Connect this signal to your own help method or overload help(). */ + void helpRequired(); + + /*! This signal is emitted whenever axisIsDrawn() changes value. */ + void axisIsDrawnChanged(bool drawn); + /*! This signal is emitted whenever gridIsDrawn() changes value. */ + void gridIsDrawnChanged(bool drawn); + /*! This signal is emitted whenever FPSIsDisplayed() changes value. */ + void FPSIsDisplayedChanged(bool displayed); + /*! This signal is emitted whenever textIsEnabled() changes value. */ + void textIsEnabledChanged(bool enabled); + /*! This signal is emitted whenever cameraIsEdited() changes value.. */ + void cameraIsEditedChanged(bool edited); + /*! This signal is emitted whenever displaysInStereo() changes value. */ + void stereoChanged(bool on); + /*! Signal emitted by select(). + + Connect this signal to your selection method or overload select(), or more probably simply + drawWithNames(). */ + void pointSelected(const QMouseEvent* e); + + /*! Signal emitted by setMouseGrabber() when the mouseGrabber() is changed. + + \p mouseGrabber is a pointer to the new MouseGrabber. Note that this signal is emitted with a \c + NULL parameter each time a MouseGrabber stops grabbing mouse. */ + void mouseGrabberChanged(qglviewer::MouseGrabber* mouseGrabber); + + /*! @name Help window */ + //@{ +public: + /*! Returns the QString displayed in the help() window main tab. + + Overload this method to define your own help string, which should shortly describe your + application and explain how it works. Rich-text (HTML) tags can be used (see QStyleSheet() + documentation for available tags): + \code + QString myViewer::helpString() const + { + QString text("

M y V i e w e r

"); + text += "Displays a Scene using OpenGL. Move the camera using the mouse."; + return text; + } + \endcode + + See also mouseString() and keyboardString(). */ + virtual QString helpString() const { return tr("No help available."); } + + virtual QString mouseString() const; + virtual QString keyboardString() const; + +#ifndef DOXYGEN + /*! This method is deprecated, use mouseString() instead. */ + virtual QString mouseBindingsString () const { return mouseString(); } + /*! This method is deprecated, use keyboardString() instead. */ + virtual QString shortcutBindingsString () const { return keyboardString(); } +#endif + +public Q_SLOTS: + virtual void help(); + virtual void aboutQGLViewer(); + +protected: + /*! Returns a pointer to the help widget. + + Use this only if you want to directly modify the help widget. Otherwise use helpString(), + setKeyDescription() and setMouseBindingDescription() to customize the text displayed in the help + window tabs. */ + QTabWidget* helpWidget() { return helpWidget_; } + //@} + + + /*! @name Drawing methods */ + //@{ +protected: + virtual void resizeGL(int width, int height); + virtual void initializeGL(); + + /*! Initializes the viewer OpenGL context. + + This method is called before the first drawing and should be overloaded to initialize some of the + OpenGL flags. The default implementation is empty. See initializeGL(). + + Typical usage include camera() initialization (showEntireScene()), previous viewer state + restoration (restoreStateFromFile()), OpenGL state modification and display list creation. + + Note that initializeGL() modifies the standard OpenGL context. These values can be restored back + in this method. + + \attention You should not call updateGL() (or any method that calls it) in this method, as it will + result in an infinite loop. The different QGLViewer set methods (setAxisIsDrawn(), + setFPSIsDisplayed()...) are protected against this problem and can safely be called. + + \note All the OpenGL specific initializations must be done in this method: the OpenGL context is + not yet available in your viewer constructor. */ + virtual void init() { Q_EMIT viewerInitialized(); } + + virtual void paintGL(); + virtual void preDraw(); + virtual void preDrawStereo(bool leftBuffer=true); + + /*! The core method of the viewer, that draws the scene. + + If you build a class that inherits from QGLViewer, this is the method you want to overload. See + the simpleViewer example for an illustration. + + The camera modelView matrix set in preDraw() converts from the world to the camera coordinate + systems. Vertices given in draw() can then be considered as being given in the world coordinate + system. The camera is moved in this world using the mouse. This representation is much more + intuitive than the default camera-centric OpenGL standard. + + \attention The \c GL_PROJECTION matrix should not be modified by this method, to correctly display + visual hints (axis, grid, FPS...) in postDraw(). Use push/pop or call + camera()->loadProjectionMatrix() at the end of draw() if you need to change the projection matrix + (unlikely). On the other hand, the \c GL_MODELVIEW matrix can be modified and left in a arbitrary + state. */ + virtual void draw() {} + virtual void fastDraw(); + virtual void postDraw(); + //@} + + /*! @name Mouse, keyboard and event handlers */ + //@{ +protected: + virtual void mousePressEvent(QMouseEvent *); + virtual void mouseMoveEvent(QMouseEvent *); + virtual void mouseReleaseEvent(QMouseEvent *); + virtual void mouseDoubleClickEvent(QMouseEvent *); + virtual void wheelEvent(QWheelEvent *); + virtual void keyPressEvent(QKeyEvent *); + virtual void keyReleaseEvent(QKeyEvent *); + virtual void timerEvent(QTimerEvent *); + virtual void closeEvent(QCloseEvent *); + //@} + + /*! @name Object selection */ + //@{ +public: + /*! Returns the name (an integer value) of the entity that was last selected by select(). This + value is set by endSelection(). See the select() documentation for details. + + As a convention, this method returns -1 if the selectBuffer() was empty, meaning that no object + was selected. + + Return value is -1 before the first call to select(). This value is modified using setSelectedName(). */ + int selectedName() const { return selectedObjectId_; } + /*! Returns the selectBuffer() size. + + See the select() documentation for details. Use setSelectBufferSize() to change this value. + + Default value is 4000 (i.e. 1000 objects in selection region, since each object pushes 4 values). + This size should be over estimated to prevent a buffer overflow when many objects are drawn under + the mouse cursor. */ + int selectBufferSize() const { return selectBufferSize_; } + + /*! Returns the width (in pixels) of a selection frustum, centered on the mouse cursor, that is + used to select objects. + + The height of the selection frustum is defined by selectRegionHeight(). + + The objects that will be drawn in this region by drawWithNames() will be recorded in the + selectBuffer(). endSelection() then analyzes this buffer and setSelectedName() to the name of the + closest object. See the gluPickMatrix() documentation for details. + + The default value is 3, which is adapted to standard applications. A smaller value results in a + more precise selection but the user has to be careful for small feature selection. + + See the multiSelect example for an illustration. */ + int selectRegionWidth() const { return selectRegionWidth_; } + /*! See the selectRegionWidth() documentation. Default value is 3 pixels. */ + int selectRegionHeight() const { return selectRegionHeight_; } + + /*! Returns a pointer to an array of \c GLuint. + + This buffer is used by the \c GL_SELECT mode in select() to perform object selection. The buffer + size can be modified using setSelectBufferSize(). If you overload endSelection(), you will analyze + the content of this buffer. See the \c glSelectBuffer() man page for details. */ + GLuint* selectBuffer() { return selectBuffer_; } + +public Q_SLOTS: +protected: + /*! This method is called by select() and should draw selectable entities. + + Default implementation is empty. Overload and draw the different elements of your scene you want + to be able to select. The default select() implementation relies on the \c GL_SELECT, and requires + that each selectable element is drawn within a \c glPushName() - \c glPopName() block. A typical + usage would be (see the select example): +\code +void Viewer::drawWithNames() { + for (int i=0; idraw(); + glPopName(); + } +} +\endcode + + The resulting selected name is computed by endSelection(), which setSelectedName() to the integer + id pushed by this method (a value of -1 means no selection). Use selectedName() to update your + selection, probably in the postSelection() method. + + \attention If your selected objects are points, do not use \c glBegin(GL_POINTS); and \c glVertex3fv() + in the above \c draw() method (not compatible with raster mode): use \c glRasterPos3fv() instead. */ + virtual void drawWithNames() {} + /*! This method is called at the end of the select() procedure. It should finalize the selection + process and update the data structure/interface/computation/display... according to the newly + selected entity. + + The default implementation is empty. Overload this method if needed, and use selectedName() to + retrieve the selected entity name (returns -1 if no object was selected). See the select example for an illustration. */ + virtual void postSelection(const QPoint& point) { Q_UNUSED(point); } + //@} + + + /*! @name Keyboard customization */ + //@{ +public: + /*! Defines the different actions that can be associated with a keyboard shortcut using + setShortcut(). + + See the keyboard page for details. */ + enum KeyboardAction { DRAW_AXIS, DRAW_GRID, DISPLAY_FPS, ENABLE_TEXT, EXIT_VIEWER, + SAVE_SCREENSHOT, CAMERA_MODE, FULL_SCREEN, STEREO, ANIMATION, HELP, EDIT_CAMERA, + MOVE_CAMERA_LEFT, MOVE_CAMERA_RIGHT, MOVE_CAMERA_UP, MOVE_CAMERA_DOWN, + INCREASE_FLYSPEED, DECREASE_FLYSPEED, SNAPSHOT_TO_CLIPBOARD }; + + unsigned int shortcut(KeyboardAction action) const; +#ifndef DOXYGEN + // QGLViewer 1.x + unsigned int keyboardAccelerator(KeyboardAction action) const; + Qt::Key keyFrameKey(unsigned int index) const; + Qt::KeyboardModifiers playKeyFramePathStateKey() const; + // QGLViewer 2.0 without Qt4 support + Qt::KeyboardModifiers addKeyFrameStateKey() const; + Qt::KeyboardModifiers playPathStateKey() const; +#endif + Qt::Key pathKey(unsigned int index) const; + Qt::KeyboardModifiers addKeyFrameKeyboardModifiers() const; + Qt::KeyboardModifiers playPathKeyboardModifiers() const; + +public Q_SLOTS: + void setShortcut(KeyboardAction action, unsigned int key); +#ifndef DOXYGEN + void setKeyboardAccelerator(KeyboardAction action, unsigned int key); +#endif + void setKeyDescription(unsigned int key, QString description); + void clearShortcuts(); + + // Key Frames shortcut keys +#ifndef DOXYGEN + // QGLViewer 1.x compatibility methods + virtual void setKeyFrameKey(unsigned int index, int key); + virtual void setPlayKeyFramePathStateKey(unsigned int buttonState); + // QGLViewer 2.0 without Qt4 support + virtual void setPlayPathStateKey(unsigned int buttonState); + virtual void setAddKeyFrameStateKey(unsigned int buttonState); +#endif + virtual void setPathKey(int key, unsigned int index = 0); + virtual void setPlayPathKeyboardModifiers(Qt::KeyboardModifiers modifiers); + virtual void setAddKeyFrameKeyboardModifiers(Qt::KeyboardModifiers modifiers); + //@} + + +public: + /*! @name Mouse customization */ + //@{ + /*! Defines the different mouse handlers: camera() or manipulatedFrame(). + + Used by setMouseBinding(), setMouseBinding(Qt::KeyboardModifiers modifiers, Qt::MouseButtons, ClickAction, bool, int) + and setWheelBinding() to define which handler receives the mouse events. */ + enum MouseHandler { CAMERA, FRAME }; + + /*! Defines the possible actions that can be binded to a mouse click using + setMouseBinding(Qt::KeyboardModifiers, Qt::MouseButtons, ClickAction, bool, int). + + See the mouse page for details. */ + enum ClickAction { NO_CLICK_ACTION, ZOOM_ON_PIXEL, ZOOM_TO_FIT, SELECT, RAP_FROM_PIXEL, RAP_IS_CENTER, + CENTER_FRAME, CENTER_SCENE, SHOW_ENTIRE_SCENE, ALIGN_FRAME, ALIGN_CAMERA }; + + + /*! Defines the possible actions that can be binded to a mouse action (a click, followed by a + mouse displacement). + + These actions may be binded to the camera() or to the manipulatedFrame() (see QGLViewer::MouseHandler) using + setMouseBinding(). */ + enum MouseAction { NO_MOUSE_ACTION, + ROTATE, ZOOM, TRANSLATE, + MOVE_FORWARD, LOOK_AROUND, MOVE_BACKWARD, + SCREEN_ROTATE, ROLL, DRIVE, + SCREEN_TRANSLATE, ZOOM_ON_REGION }; + +#ifndef DOXYGEN + MouseAction mouseAction(unsigned int state) const; + int mouseHandler(unsigned int state) const; + int mouseButtonState(MouseHandler handler, MouseAction action, bool withConstraint=true) const; + ClickAction clickAction(unsigned int state, bool doubleClick, Qt::MouseButtons buttonsBefore) const; + void getClickButtonState(ClickAction action, unsigned int & state, bool& doubleClick, Qt::MouseButtons& buttonsBefore) const; + unsigned int wheelButtonState(MouseHandler handler, MouseAction action, bool withConstraint=true) const; +#endif + + MouseAction mouseAction(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button) const; + int mouseHandler(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button) const; + + void getMouseActionBinding(MouseHandler handler, MouseAction action, bool withConstraint, + Qt::Key& key, Qt::KeyboardModifiers& modifiers, Qt::MouseButton& button) const; + + ClickAction clickAction(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button, + bool doubleClick=false, Qt::MouseButtons buttonsBefore=Qt::NoButton) const; + + void getClickActionBinding(ClickAction action, Qt::Key& key, Qt::KeyboardModifiers& modifiers, + Qt::MouseButton& button, bool& doubleClick, Qt::MouseButtons& buttonsBefore) const; + + MouseAction wheelAction(Qt::Key key, Qt::KeyboardModifiers modifiers) const; + int wheelHandler(Qt::Key key, Qt::KeyboardModifiers modifiers) const; + + void getWheelActionBinding(MouseHandler handler, MouseAction action, bool withConstraint, + Qt::Key& key, Qt::KeyboardModifiers& modifiers) const; + +public Q_SLOTS: +#ifndef DOXYGEN + void setMouseBinding(unsigned int state, MouseHandler handler, MouseAction action, bool withConstraint=true); + void setMouseBinding(unsigned int state, ClickAction action, bool doubleClick=false, Qt::MouseButtons buttonsBefore=Qt::NoButton); + void setMouseBindingDescription(unsigned int state, QString description, bool doubleClick=false, Qt::MouseButtons buttonsBefore=Qt::NoButton); +#endif + + void setMouseBinding(Qt::KeyboardModifiers modifiers, Qt::MouseButton buttons, MouseHandler handler, MouseAction action, bool withConstraint=true); + void setMouseBinding(Qt::KeyboardModifiers modifiers, Qt::MouseButton button, ClickAction action, bool doubleClick=false, Qt::MouseButtons buttonsBefore=Qt::NoButton); + void setWheelBinding(Qt::KeyboardModifiers modifiers, MouseHandler handler, MouseAction action, bool withConstraint=true); + void setMouseBindingDescription(Qt::KeyboardModifiers modifiers, Qt::MouseButton button, QString description, bool doubleClick=false, Qt::MouseButtons buttonsBefore=Qt::NoButton); + + void setMouseBinding(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton buttons, MouseHandler handler, MouseAction action, bool withConstraint=true); + void setMouseBinding(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button, ClickAction action, bool doubleClick=false, Qt::MouseButtons buttonsBefore=Qt::NoButton); + void setWheelBinding(Qt::Key key, Qt::KeyboardModifiers modifiers, MouseHandler handler, MouseAction action, bool withConstraint=true); + void setMouseBindingDescription(Qt::Key key, Qt::KeyboardModifiers modifiers, Qt::MouseButton button, QString description, bool doubleClick=false, Qt::MouseButtons buttonsBefore=Qt::NoButton); + + void clearMouseBindings(); + +#ifndef DOXYGEN + MouseAction wheelAction(Qt::KeyboardModifiers modifiers) const; + int wheelHandler(Qt::KeyboardModifiers modifiers) const; + + void setHandlerKeyboardModifiers(MouseHandler handler, Qt::KeyboardModifiers modifiers); + void setHandlerStateKey(MouseHandler handler, unsigned int buttonState); + void setMouseStateKey(MouseHandler handler, unsigned int buttonState); +#endif + +private: + static QString mouseActionString(QGLViewer::MouseAction ma); + static QString clickActionString(QGLViewer::ClickAction ca); + //@} + + + /*! @name State persistence */ + //@{ +public: + QString stateFileName() const; + virtual QDomElement domElement(const QString& name, QDomDocument& document) const; + +public Q_SLOTS: + virtual void initFromDOMElement(const QDomElement& element); + virtual void saveStateToFile(); // cannot be const because of QMessageBox + virtual bool restoreStateFromFile(); + + /*! Defines the stateFileName() used by saveStateToFile() and restoreStateFromFile(). + + The file name can have an optional prefix directory (no prefix meaning current directory). If the + directory does not exist, it will be created by saveStateToFile(). + + \code + // Name depends on the displayed 3D model. Saved in current directory. + setStateFileName(3DModelName() + ".xml"); + + // Files are stored in a dedicated directory under user's home directory. + setStateFileName(QDir::homeDirPath + "/.config/myApp.xml"); + \endcode */ + void setStateFileName(const QString& name) { stateFileName_ = name; } + +#ifndef DOXYGEN + void saveToFile(const QString& fileName=QString::null); + bool restoreFromFile(const QString& fileName=QString::null); +#endif + +private: + static void saveStateToFileForAllViewers(); + //@} + + + /*! @name QGLViewer pool */ + //@{ +public: + /*! Returns a \c QList that contains pointers to all the created QGLViewers. + Note that this list may contain \c NULL pointers if the associated viewer has been deleted. + + Can be useful to apply a method or to connect a signal to all the viewers: + \code + foreach (QGLViewer* viewer, QGLViewer::QGLViewerPool()) + connect(myObject, SIGNAL(IHaveChangedSignal()), viewer, SLOT(update())); + \endcode + + \attention With Qt version 3, this method returns a \c QPtrList instead. Use a \c QPtrListIterator + to iterate on the list instead.*/ + static const QList& QGLViewerPool() { return QGLViewer::QGLViewerPool_; } + + + /*! Returns the index of the QGLViewer \p viewer in the QGLViewerPool(). This index in unique and + can be used to identify the different created QGLViewers (see stateFileName() for an application + example). + + When a QGLViewer is deleted, the QGLViewers' indexes are preserved and NULL is set for that index. + When a QGLViewer is created, it is placed in the first available position in that list. + Returns -1 if the QGLViewer could not be found (which should not be possible). */ + static int QGLViewerIndex(const QGLViewer* const viewer) { return QGLViewer::QGLViewerPool_.indexOf(const_cast(viewer)); } + //@} + +#ifndef DOXYGEN + /*! @name Visual hints */ + //@{ +public: + virtual void setVisualHintsMask(int mask, int delay = 2000); + +public Q_SLOTS: + virtual void resetVisualHints(); + //@} +#endif + +private Q_SLOTS: + // Patch for a Qt bug with fullScreen on startup + void delayedFullScreen() { move(prevPos_); setFullScreen(); } + void hideMessage(); + +private: + // Copy constructor and operator= are declared private and undefined + // Prevents everyone from trying to use them + QGLViewer(const QGLViewer& v); + QGLViewer& operator=(const QGLViewer& v); + + // Set parameters to their default values. Called by the constructors. + void defaultConstructor(); + + void handleKeyboardAction(KeyboardAction id); + + // C a m e r a + qglviewer::Camera* camera_; + bool cameraIsEdited_; + qreal previousCameraZClippingCoefficient_; + unsigned int previousPathId_; // double key press recognition + void connectAllCameraKFIInterpolatedSignals(bool connection=true); + + // C o l o r s + QColor backgroundColor_, foregroundColor_; + + // D i s p l a y f l a g s + bool axisIsDrawn_; // world axis + bool gridIsDrawn_; // world XY grid + bool FPSIsDisplayed_; // Frame Per Seconds + bool textIsEnabled_; // drawText() actually draws text or not + bool stereo_; // stereo display + bool fullScreen_; // full screen mode + QPoint prevPos_; // Previous window position, used for full screen mode + + // A n i m a t i o n + bool animationStarted_; // animation mode started + int animationPeriod_; // period in msecs + int animationTimerId_; + + // F P S d i s p l a y + QTime fpsTime_; + unsigned int fpsCounter_; + QString fpsString_; + qreal f_p_s_; + + // M e s s a g e s + QString message_; + bool displayMessage_; + QTimer messageTimer_; + + // M a n i p u l a t e d f r a m e + qglviewer::ManipulatedFrame* manipulatedFrame_; + bool manipulatedFrameIsACamera_; + + // M o u s e G r a b b e r + qglviewer::MouseGrabber* mouseGrabber_; + bool mouseGrabberIsAManipulatedFrame_; + bool mouseGrabberIsAManipulatedCameraFrame_; + QMap disabledMouseGrabbers_; + + // S e l e c t i o n + int selectRegionWidth_, selectRegionHeight_; + int selectBufferSize_; + GLuint* selectBuffer_; + int selectedObjectId_; + + // V i s u a l h i n t s + int visualHint_; + + // S h o r t c u t k e y s + void setDefaultShortcuts(); + QString cameraPathKeysString() const; + QMap keyboardActionDescription_; + QMap keyboardBinding_; + QMap keyDescription_; + + // K e y F r a m e s s h o r t c u t s + QMap pathIndex_; + Qt::KeyboardModifiers addKeyFrameKeyboardModifiers_, playPathKeyboardModifiers_; + + // B u f f e r T e x t u r e + GLuint bufferTextureId_; + qreal bufferTextureMaxU_, bufferTextureMaxV_; + int bufferTextureWidth_, bufferTextureHeight_; + unsigned int previousBufferTextureFormat_; + int previousBufferTextureInternalFormat_; + +#ifndef DOXYGEN + // M o u s e a c t i o n s + struct MouseActionPrivate { + MouseHandler handler; + MouseAction action; + bool withConstraint; + }; + + // M o u s e b i n d i n g s + struct MouseBindingPrivate { + const Qt::KeyboardModifiers modifiers; + const Qt::MouseButton button; + const Qt::Key key; + + MouseBindingPrivate(Qt::KeyboardModifiers m, Qt::MouseButton b, Qt::Key k) + : modifiers(m), button(b), key(k) {} + + // This sort order is used in mouseString() to display sorted mouse bindings + bool operator<(const MouseBindingPrivate& mbp) const + { + if (key != mbp.key) + return key < mbp.key; + if (modifiers != mbp.modifiers) + return modifiers < mbp.modifiers; + return button < mbp.button; + } + }; + + // W h e e l b i n d i n g s + struct WheelBindingPrivate { + const Qt::KeyboardModifiers modifiers; + const Qt::Key key; + + WheelBindingPrivate(Qt::KeyboardModifiers m, Qt::Key k) + : modifiers(m), key(k) {} + + // This sort order is used in mouseString() to display sorted wheel bindings + bool operator<(const WheelBindingPrivate& wbp) const + { + if (key != wbp.key) + return key < wbp.key; + return modifiers < wbp.modifiers; + } + }; + + // C l i c k b i n d i n g s + struct ClickBindingPrivate { + const Qt::KeyboardModifiers modifiers; + const Qt::MouseButton button; + const bool doubleClick; + const Qt::MouseButtons buttonsBefore; // only defined when doubleClick is true + const Qt::Key key; + + ClickBindingPrivate(Qt::KeyboardModifiers m, Qt::MouseButton b, bool dc, Qt::MouseButtons bb, Qt::Key k) + : modifiers(m), button(b), doubleClick(dc), buttonsBefore(bb), key(k) {} + + // This sort order is used in mouseString() to display sorted mouse bindings + bool operator<(const ClickBindingPrivate& cbp) const + { + if (key != cbp.key) + return key < cbp.key; + if (buttonsBefore != cbp.buttonsBefore) + return buttonsBefore < cbp.buttonsBefore; + if (modifiers != cbp.modifiers) + return modifiers < cbp.modifiers; + if (button != cbp.button) + return button < cbp.button; + return doubleClick != cbp.doubleClick; + } + }; +#endif + static QString formatClickActionPrivate(ClickBindingPrivate cbp); + static bool isValidShortcutKey(int key); + + QMap mouseDescription_; + + void setDefaultMouseBindings(); + void performClickAction(ClickAction ca, const QMouseEvent* const e); + QMap mouseBinding_; + QMap wheelBinding_; + QMap clickBinding_; + Qt::Key currentlyPressedKey_; + + // S n a p s h o t s + void initializeSnapshotFormats(); + QImage frameBufferSnapshot(); + QString snapshotFileName_, snapshotFormat_; + int snapshotCounter_, snapshotQuality_; + TileRegion* tileRegion_; + + // Q G L V i e w e r p o o l + static QList QGLViewerPool_; + + // S t a t e F i l e + QString stateFileName_; + + // H e l p w i n d o w + QTabWidget* helpWidget_; +}; + +#endif // QGLVIEWER_QGLVIEWER_H diff --git a/QGLViewer/qglviewer.icns b/QGLViewer/qglviewer.icns new file mode 100644 index 0000000000000000000000000000000000000000..ab03127828390fdf931aa6d53b408e43833e313a GIT binary patch literal 185404 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

adtFT6_)U(`kV?(cmH`cQAw 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

NLbjdUH>tQ-z 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 UTBRBP*=b{LRsaA13_|b#08C?Q*Z=?k literal 0 HcmV?d00001 diff --git a/QGLViewer/qglviewer_fr.qm b/QGLViewer/qglviewer_fr.qm new file mode 100644 index 0000000000000000000000000000000000000000..4e9253f0970f24dbb3e2bf710a1be202e75ef56e GIT binary patch literal 14815 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 literal 0 HcmV?d00001 diff --git a/QGLViewer/qglviewer_fr.ts b/QGLViewer/qglviewer_fr.ts new file mode 100644 index 0000000..72372d1 --- /dev/null +++ b/QGLViewer/qglviewer_fr.ts @@ -0,0 +1,608 @@ + + + + + ImageInterface + + Image settings + RĂ©glages d'image + + + Width + Largeur + + + px + px + + + Width of the image (in pixels) + Largeur de l'image (en pixels) + + + Height + Hauteur + + + Height of the image (in pixels) + Hauteur de l'image (en pixels) + + + Image quality + QualitĂ© d'image + + + Between 0 (smallest files) and 100 (highest quality) + Entre 0 (taille de fichier minimale) et 100 (qualitĂ© maximale) + + + Oversampling + SurĂ©chantillonage + + + x + x + + + Antialiases image (when larger then 1.0) + Anti-alliassage de l'image (si supĂ©rieur Ă  1.0) + + + Use white background + Fond blanc + + + Use white as background color + Mettre du blanc en couleur de fond + + + Expand frustum if needed + Etendre la pyramide de vue ( frustum) si nĂ©cessaire + + + When image aspect ratio differs from viewer's one, expand frustum as needed. Fits inside current frustum otherwise. + Lorsque le rapport de dimensions de l'image diffère de celui de la fenĂªtre, Ă©tendre la pyramide de vue (frustum) en consĂ©quence. L'image est ajustĂ©e Ă  l'intĂ©rieur de la vue actuelle sinon. + + + OK + Ok + + + Cancel + Annuler + + + + QGLViewer + + snapshot + Default snapshot file name + capture + + + %1Hz + Frames per seconds, in Hertz + %1Hz + + + Toggles the display of the FPS + DISPLAY_FPS action description + Active ou non l'affichage de la frĂ©quence d'affiichage + + + Saves a screenshot + SAVE_SCREENSHOT action description + Sauvegarde une capture d'Ă©cran + + + Toggles full screen display + FULL_SCREEN action description + Passe ou non en mode plein Ă©cran + + + Toggles the display of the world axis + DRAW_AXIS action description + Affiche ou non le repère du monde + + + Toggles the display of the XY grid + DRAW_GRID action description + Affiche ou non la grille XY + + + Changes camera mode (observe or fly) + CAMERA_MODE action description + Change le mode de la camĂ©ra (observateur ou vol) + + + Toggles stereo display + STEREO action description + Affiche ou non en stĂ©rĂ©o + + + Opens this help window + HELP action description + Ouvre la fenĂªtre d'aide + + + Starts/stops the animation + ANIMATION action description + DĂ©marre/arrĂªte l'animation + + + Toggles camera paths display + EDIT_CAMERA action description + Affiche ou non les chemins de camĂ©ra + + + Toggles the display of the text + ENABLE_TEXT action description + Affiche ou non les textes + + + Exits program + EXIT_VIEWER action description + Quitte l'application + + + Moves camera left + MOVE_CAMERA_LEFT action description + DĂ©place la camĂ©ra sur la gauche + + + Moves camera right + MOVE_CAMERA_RIGHT action description + DĂ©place la camĂ©ra sur la droite + + + Moves camera up + MOVE_CAMERA_UP action description + DĂ©place la camĂ©ra vers le haut + + + Moves camera down + MOVE_CAMERA_DOWN action description + DĂ©place la camĂ©ra vers le bas + + + Increases fly speed + INCREASE_FLYSPEED action description + Augmente la vitesse de vol + + + Decreases fly speed + DECREASE_FLYSPEED action description + Diminue la vitesse de vol + + + Copies a snapshot to clipboard + SNAPSHOT_TO_CLIPBOARD action description + Place une capture d'Ă©cran dans le presse-papier + + + Stereo not supported + Message box window title + StĂ©rĂ©o non supportĂ©e + + + Stereo is not supported on this display. + Affichage en stĂ©rĂ©o non supportĂ© sur cette machine. + + + Rotates + ROTATE mouse action + Tourne + + + Zooms + ZOOM mouse action + Zoome + + + Translates + TRANSLATE mouse action + Translate + + + Moves backward + MOVE_BACKWARD mouse action + Recule + + + Horizontally/Vertically translates + SCREEN_TRANSLATE mouse action + Translate horizontalement/verticalement + + + Moves forward + MOVE_FORWARD mouse action + Avance + + + Looks around + LOOK_AROUND mouse action + Regarde + + + Rotates in screen plane + SCREEN_ROTATE mouse action + Pivote dans le plan Ă©cran + + + Rolls + ROLL mouse action + Pivote + + + Drives + DRIVE mouse action + Avance + + + Zooms on region for + ZOOM_ON_REGION mouse action + Zoome sur la rĂ©gion pour + + + Zooms on pixel + ZOOM_ON_PIXEL click action + Zoome sur le pixel + + + Zooms to fit scene + ZOOM_TO_FIT click action + Zoome pour ajuster Ă  la scène + + + Selects + SELECT click action + SĂ©lectionne + + + Sets pivot point + RAP_FROM_PIXEL click action + DĂ©finit le point de rotation + + + Resets pivot point + RAP_IS_CENTER click action + Restaure le point de rotation + + + Centers manipulated frame + CENTER_FRAME click action + Centre le repère manipulĂ© + + + Centers scene + CENTER_SCENE click action + Centre la scène + + + Shows entire scene + SHOW_ENTIRE_SCENE click action + Affiche toute la scène + + + Aligns manipulated frame + ALIGN_FRAME click action + Aligne le repère manipulĂ© + + + Aligns camera + ALIGN_CAMERA click action + Aligne la camĂ©ra + + + Camera paths are controlled using the %1 keys (noted <i>Fx</i> below): + Help window key tab camera keys + Les chemins de camĂ©ra sont contrĂ´lĂ©s avec les touches %1 (notĂ©es <i>Fx</i> ci-dessous) : + + + Key(s) + Keys column header in help window mouse tab + Touche(s) + + + Description + Description column header in help window mouse tab + Description + + + Standard viewer keys + In help window keys tab + Raccourcis standards + + + Fx + Generic function key (F1..F12) + Fx + + + Plays path (or resets saved position) + Joue le chemin (ou restaure la position sauvegardĂ©e) + + + Adds a key frame to path (or defines a position) + Ajoute une position clef au chemin (ou dĂ©finit une position) + + + Deletes path (or saved position) + Supprime le chemin (ou la position) + + + Button(s) + Buttons column header in help window mouse tab + Bouton(s) + + + Standard mouse bindings + In help window mouse tab + Actions souris standards + + + Wheel + Mouse wheel + Molette + + + &Help + Help window tab title + &Aide + + + &Keyboard + Help window tab title + &Clavier + + + &Mouse + Help window tab title + &Souris + + + Help + Help window title + Aide + + + Path %1 deleted + Feedback message + Chemin %1 supprimĂ© + + + Position %1 deleted + Feedback message + Position %1 supprimĂ©e + + + Path %1, position %2 added + Feedback message + Chemin %1, position %2 ajoutĂ©e + + + Position %1 saved + Feedback message + Position %1 sauvegardĂ©e + + + Camera in observer mode + Feedback message + CamĂ©ra en mode observateur + + + Camera in fly mode + Feedback message + CamĂ©ra en mode vol + + + Save to file error + Message box window title + Erreur lors de la sauvegarde + + + State file name (%1) references a directory instead of a file. + Le nom du fichier d'Ă©tat (%1) rĂ©fĂ©rence un rĂ©pĂ©rtoire et non un fichier. + + + Unable to create directory %1 + Le rĂ©pĂ©rtoire %1 ne peut Ăªtre créé + + + Unable to save to file %1 + Impossible de sauvegarder le fichier %1 + + + Problem in state restoration + Message box window title + Problème lors de la restauration de l'Ă©tat + + + File %1 is not readable. + Le fichier %1 n'est pas lisible. + + + Open file error + Message box window title + Erreur d'ouverture de fichier + + + Unable to open file %1 + Le fichier %1 ne peut Ăªtre ouvert + + + Left + left mouse button + Gauche + + + Middle + middle mouse button + Milieu + + + Right + right mouse button + Droit + + + double click + Suffix after mouse button + Double clic + + + camera + Suffix after action + la camĂ©ra + + + manipulated frame + Suffix after action + le repère manipulĂ© + + + with + As in : Left button with Ctrl pressed + avec + + + pressed + As in : Left button with Ctrl pressed + enfoncĂ© + + + %1%2%3%4%5%6 + Modifier / button or wheel / double click / with / button / pressed + %1%3%2%4%5%6 + + + No help available. + Pas d'aide disponible. + + + Exporter error + Message box window title + Erreur d'export + + + Unable to open file %1. + Impossible d'ouvrir le ficher %1. + + + BSP Construction + Construction du BSP + + + Exporting to file %1 + Export vers le fichier %1 + + + Parsing feedback buffer. + Parcours du feedback buffer. + + + Topological sort + Tri topologique + + + Advanced topological sort + Tri topologique avancĂ© + + + Rendering... + Rendu... + + + Visibility optimization + Optimisation de la visibilitĂ© + + + &About + Help window about title + Ă€ &propos + + + <h1>libQGLViewer</h1><h3>Version %1</h3><br>A versatile 3D viewer based on OpenGL and Qt<br>Copyright 2002-%2 Gilles Debunne<br><code>%3</code> + <h1>libQGLViewer</h1><h3>Version %1</h3><br>Un afficheur 3D gĂ©nĂ©raliste basĂ© sur OpenGL et Qt<br>Copyright 2002-%2 Gilles Debunne<br><code>%3</code> + + + + VRenderInterface + + Vectorial rendering options + Options de rendu vectoriel + + + Include hidden parts + Inclure les parties cachĂ©es + + + Cull back faces + Supprimer les faces arrières + + + Back faces (non clockwise point ordering) are removed from the output + Les faces orientĂ©es vers l'arrière (points ordonnĂ©s dans le sens anti-horaire) sont supprimĂ©es du rĂ©sultat (Back Face Culling) + + + Black and white + Noir et blanc + + + Black and white rendering + Rendu en noir et blanc + + + Color background + Fond avec une couleur + + + Use current background color instead of white + Utiliser la couleur de fond actuelle Ă  la place du blanc + + + Tighten bounding box + Ajuster la boĂ®te englobante + + + Fit output bounding box to current display + Ajuster la boĂ®te englobante de la sortie Ă  ce qui est actuellement affichĂ© + + + Polygon depth sorting method + MĂ©thode de tri de la profondeur des polygĂ´nes + + + No sorting + Pas de tri + + + Topological + Topologique + + + Advanced topological + Topologique avancĂ© + + + Save + Sauvegarder + + + Cancel + Annuler + + + Hidden polygons are also included in the output (usually twice bigger) + Inclure les polygĂ´nes cachĂ©s dans le rĂ©sultat (alors habituellement deux fois plus gros) + + + Sort method: + MĂ©thode de tri : + + + BSP + BSP + + + diff --git a/QGLViewer/quaternion.cpp b/QGLViewer/quaternion.cpp new file mode 100644 index 0000000..0c30809 --- /dev/null +++ b/QGLViewer/quaternion.cpp @@ -0,0 +1,552 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "domUtils.h" +#include "quaternion.h" +#include // RAND_MAX + +// All the methods are declared inline in Quaternion.h +using namespace qglviewer; +using namespace std; + +/*! Constructs a Quaternion that will rotate from the \p from direction to the \p to direction. + +Note that this rotation is not uniquely defined. The selected axis is usually orthogonal to \p from +and \p to, minimizing the rotation angle. This method is robust and can handle small or almost identical vectors. */ +Quaternion::Quaternion(const Vec& from, const Vec& to) +{ + const qreal epsilon = 1E-10; + + const qreal fromSqNorm = from.squaredNorm(); + const qreal toSqNorm = to.squaredNorm(); + // Identity Quaternion when one vector is null + if ((fromSqNorm < epsilon) || (toSqNorm < epsilon)) + { + q[0]=q[1]=q[2]=0.0; + q[3]=1.0; + } + else + { + Vec axis = cross(from, to); + const qreal axisSqNorm = axis.squaredNorm(); + + // Aligned vectors, pick any axis, not aligned with from or to + if (axisSqNorm < epsilon) + axis = from.orthogonalVec(); + + qreal angle = asin(sqrt(axisSqNorm / (fromSqNorm * toSqNorm))); + + if (from*to < 0.0) + angle = M_PI-angle; + + setAxisAngle(axis, angle); + } +} + +/*! Returns the image of \p v by the Quaternion inverse() rotation. + +rotate() performs an inverse transformation. Same as inverse().rotate(v). */ +Vec Quaternion::inverseRotate(const Vec& v) const +{ + return inverse().rotate(v); +} + +/*! Returns the image of \p v by the Quaternion rotation. + +See also inverseRotate() and operator*(const Quaternion&, const Vec&). */ +Vec Quaternion::rotate(const Vec& v) const +{ + const qreal q00 = 2.0 * q[0] * q[0]; + const qreal q11 = 2.0 * q[1] * q[1]; + const qreal q22 = 2.0 * q[2] * q[2]; + + const qreal q01 = 2.0 * q[0] * q[1]; + const qreal q02 = 2.0 * q[0] * q[2]; + const qreal q03 = 2.0 * q[0] * q[3]; + + const qreal q12 = 2.0 * q[1] * q[2]; + const qreal q13 = 2.0 * q[1] * q[3]; + + const qreal q23 = 2.0 * q[2] * q[3]; + + return Vec((1.0 - q11 - q22)*v[0] + ( q01 - q23)*v[1] + ( q02 + q13)*v[2], + ( q01 + q23)*v[0] + (1.0 - q22 - q00)*v[1] + ( q12 - q03)*v[2], + ( q02 - q13)*v[0] + ( q12 + q03)*v[1] + (1.0 - q11 - q00)*v[2] ); +} + +/*! Set the Quaternion from a (supposedly correct) 3x3 rotation matrix. + + The matrix is expressed in European format: its three \e columns are the images by the rotation of + the three vectors of an orthogonal basis. Note that OpenGL uses a symmetric representation for its + matrices. + + setFromRotatedBasis() sets a Quaternion from the three axis of a rotated frame. It actually fills + the three columns of a matrix with these rotated basis vectors and calls this method. */ +void Quaternion::setFromRotationMatrix(const qreal m[3][3]) +{ + // Compute one plus the trace of the matrix + const qreal onePlusTrace = 1.0 + m[0][0] + m[1][1] + m[2][2]; + + if (onePlusTrace > 1E-5) + { + // Direct computation + const qreal s = sqrt(onePlusTrace) * 2.0; + q[0] = (m[2][1] - m[1][2]) / s; + q[1] = (m[0][2] - m[2][0]) / s; + q[2] = (m[1][0] - m[0][1]) / s; + q[3] = 0.25 * s; + } + else + { + // Computation depends on major diagonal term + if ((m[0][0] > m[1][1])&(m[0][0] > m[2][2])) + { + const qreal s = sqrt(1.0 + m[0][0] - m[1][1] - m[2][2]) * 2.0; + q[0] = 0.25 * s; + q[1] = (m[0][1] + m[1][0]) / s; + q[2] = (m[0][2] + m[2][0]) / s; + q[3] = (m[1][2] - m[2][1]) / s; + } + else + if (m[1][1] > m[2][2]) + { + const qreal s = sqrt(1.0 + m[1][1] - m[0][0] - m[2][2]) * 2.0; + q[0] = (m[0][1] + m[1][0]) / s; + q[1] = 0.25 * s; + q[2] = (m[1][2] + m[2][1]) / s; + q[3] = (m[0][2] - m[2][0]) / s; + } + else + { + const qreal s = sqrt(1.0 + m[2][2] - m[0][0] - m[1][1]) * 2.0; + q[0] = (m[0][2] + m[2][0]) / s; + q[1] = (m[1][2] + m[2][1]) / s; + q[2] = 0.25 * s; + q[3] = (m[0][1] - m[1][0]) / s; + } + } + normalize(); +} + +#ifndef DOXYGEN +void Quaternion::setFromRotationMatrix(const float m[3][3]) +{ + qWarning("setFromRotationMatrix now expects a double[3][3] parameter"); + + qreal mat[3][3]; + for (int i=0; i<3; ++i) + for (int j=0; j<3; ++j) + mat[i][j] = qreal(m[i][j]); + + setFromRotationMatrix(mat); +} + +void Quaternion::setFromRotatedBase(const Vec& X, const Vec& Y, const Vec& Z) +{ + qWarning("setFromRotatedBase is deprecated, use setFromRotatedBasis instead"); + setFromRotatedBasis(X,Y,Z); +} +#endif + +/*! Sets the Quaternion from the three rotated vectors of an orthogonal basis. + + The three vectors do not have to be normalized but must be orthogonal and direct (X^Y=k*Z, with k>0). + + \code + Quaternion q; + q.setFromRotatedBasis(X, Y, Z); + // Now q.rotate(Vec(1,0,0)) == X and q.inverseRotate(X) == Vec(1,0,0) + // Same goes for Y and Z with Vec(0,1,0) and Vec(0,0,1). + \endcode + + See also setFromRotationMatrix() and Quaternion(const Vec&, const Vec&). */ +void Quaternion::setFromRotatedBasis(const Vec& X, const Vec& Y, const Vec& Z) +{ + qreal m[3][3]; + qreal normX = X.norm(); + qreal normY = Y.norm(); + qreal normZ = Z.norm(); + + for (int i=0; i<3; ++i) + { + m[i][0] = X[i] / normX; + m[i][1] = Y[i] / normY; + m[i][2] = Z[i] / normZ; + } + + setFromRotationMatrix(m); +} + +/*! Returns the axis vector and the angle (in radians) of the rotation represented by the Quaternion. + See the axis() and angle() documentations. */ +void Quaternion::getAxisAngle(Vec& axis, qreal& angle) const +{ + angle = 2.0 * acos(q[3]); + axis = Vec(q[0], q[1], q[2]); + const qreal sinus = axis.norm(); + if (sinus > 1E-8) + axis /= sinus; + + if (angle > M_PI) + { + angle = 2.0 * qreal(M_PI) - angle; + axis = -axis; + } +} + +/*! Returns the normalized axis direction of the rotation represented by the Quaternion. + +It is null for an identity Quaternion. See also angle() and getAxisAngle(). */ +Vec Quaternion::axis() const +{ + Vec res = Vec(q[0], q[1], q[2]); + const qreal sinus = res.norm(); + if (sinus > 1E-8) + res /= sinus; + return (acos(q[3]) <= M_PI/2.0) ? res : -res; +} + +/*! Returns the angle (in radians) of the rotation represented by the Quaternion. + + This value is always in the range [0-pi]. Larger rotational angles are obtained by inverting the + axis() direction. + + See also axis() and getAxisAngle(). */ +qreal Quaternion::angle() const +{ + const qreal angle = 2.0 * acos(q[3]); + return (angle <= M_PI) ? angle : 2.0*M_PI - angle; +} + +/*! Returns an XML \c QDomElement that represents the Quaternion. + + \p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create + QDomElement. + + When output to a file, the resulting QDomElement will look like: + \code + + \endcode + + Use initFromDOMElement() to restore the Quaternion state from the resulting \c QDomElement. See + also the Quaternion(const QDomElement&) constructor. + + See the Vec::domElement() documentation for a complete QDomDocument creation and saving example. + + See also Frame::domElement(), Camera::domElement(), KeyFrameInterpolator::domElement()... */ +QDomElement Quaternion::domElement(const QString& name, QDomDocument& document) const +{ + QDomElement de = document.createElement(name); + de.setAttribute("q0", QString::number(q[0])); + de.setAttribute("q1", QString::number(q[1])); + de.setAttribute("q2", QString::number(q[2])); + de.setAttribute("q3", QString::number(q[3])); + return de; +} + +/*! Restores the Quaternion state from a \c QDomElement created by domElement(). + + The \c QDomElement should contain the \c q0, \c q1 , \c q2 and \c q3 attributes. If one of these + attributes is missing or is not a number, a warning is displayed and these fields are respectively + set to 0.0, 0.0, 0.0 and 1.0 (identity Quaternion). + + See also the Quaternion(const QDomElement&) constructor. */ +void Quaternion::initFromDOMElement(const QDomElement& element) +{ + Quaternion q(element); + *this = q; +} + +/*! Constructs a Quaternion from a \c QDomElement representing an XML code of the form + \code< anyTagName q0=".." q1=".." q2=".." q3=".." />\endcode + + If one of these attributes is missing or is not a number, a warning is displayed and the associated + value is respectively set to 0, 0, 0 and 1 (identity Quaternion). + + See also domElement() and initFromDOMElement(). */ +Quaternion::Quaternion(const QDomElement& element) +{ + QStringList attribute; + attribute << "q0" << "q1" << "q2" << "q3"; + for (int i=0; i +#include + +namespace qglviewer { +/*! \brief The Quaternion class represents 3D rotations and orientations. + \class Quaternion quaternion.h QGLViewer/quaternion.h + + The Quaternion is an appropriate (although not very intuitive) representation for 3D rotations and + orientations. Many tools are provided to ease the definition of a Quaternion: see constructors, + setAxisAngle(), setFromRotationMatrix(), setFromRotatedBasis(). + + You can apply the rotation represented by the Quaternion to 3D points using rotate() and + inverseRotate(). See also the Frame class that represents a coordinate system and provides other + conversion functions like Frame::coordinatesOf() and Frame::transformOf(). + + You can apply the Quaternion \c q rotation to the OpenGL matrices using: + \code + glMultMatrixd(q.matrix()); + // equvalent to glRotate(q.angle()*180.0/M_PI, q.axis().x, q.axis().y, q.axis().z); + \endcode + + Quaternion is part of the \c qglviewer namespace, specify \c qglviewer::Quaternion or use the qglviewer + namespace: \code using namespace qglviewer; \endcode + +

Internal representation

+ + The internal representation of a Quaternion corresponding to a rotation around axis \c axis, with an angle + \c alpha is made of four qreals (i.e. doubles) q[i]: + \code + {q[0],q[1],q[2]} = sin(alpha/2) * {axis[0],axis[1],axis[2]} + q[3] = cos(alpha/2) + \endcode + + Note that certain implementations place the cosine term in first position (instead of last here). + + The Quaternion is always normalized, so that its inverse() is actually its conjugate. + + See also the Vec and Frame classes' documentations. + \nosubgrouping */ +class QGLVIEWER_EXPORT Quaternion +{ +public: + /*! @name Defining a Quaternion */ + //@{ + /*! Default constructor, builds an identity rotation. */ + Quaternion() + { q[0]=q[1]=q[2]=0.0; q[3]=1.0; } + + /*! Constructor from rotation axis (non null) and angle (in radians). See also setAxisAngle(). */ + Quaternion(const Vec& axis, qreal angle) + { + setAxisAngle(axis, angle); + } + + Quaternion(const Vec& from, const Vec& to); + + /*! Constructor from the four values of a Quaternion. First three values are axis*sin(angle/2) and + last one is cos(angle/2). + + \attention The identity Quaternion is Quaternion(0,0,0,1) and \e not Quaternion(0,0,0,0) (which is + not unitary). The default Quaternion() creates such identity Quaternion. */ + Quaternion(qreal q0, qreal q1, qreal q2, qreal q3) + { q[0]=q0; q[1]=q1; q[2]=q2; q[3]=q3; } + + /*! Copy constructor. */ + Quaternion(const Quaternion& Q) + { for (int i=0; i<4; ++i) q[i] = Q.q[i]; } + + /*! Equal operator. */ + Quaternion& operator=(const Quaternion& Q) + { + for (int i=0; i<4; ++i) + q[i] = Q.q[i]; + return (*this); + } + + /*! Sets the Quaternion as a rotation of axis \p axis and angle \p angle (in radians). + + \p axis does not need to be normalized. A null \p axis will result in an identity Quaternion. */ + void setAxisAngle(const Vec& axis, qreal angle) + { + const qreal norm = axis.norm(); + if (norm < 1E-8) + { + // Null rotation + q[0] = 0.0; q[1] = 0.0; q[2] = 0.0; q[3] = 1.0; + } + else + { + const qreal sin_half_angle = sin(angle / 2.0); + q[0] = sin_half_angle*axis[0]/norm; + q[1] = sin_half_angle*axis[1]/norm; + q[2] = sin_half_angle*axis[2]/norm; + q[3] = cos(angle / 2.0); + } + } + + /*! Sets the Quaternion value. See the Quaternion(qreal, qreal, qreal, qreal) constructor documentation. */ + void setValue(qreal q0, qreal q1, qreal q2, qreal q3) + { q[0]=q0; q[1]=q1; q[2]=q2; q[3]=q3; } + +#ifndef DOXYGEN + void setFromRotationMatrix(const float m[3][3]); + void setFromRotatedBase(const Vec& X, const Vec& Y, const Vec& Z); +#endif + void setFromRotationMatrix(const qreal m[3][3]); + void setFromRotatedBasis(const Vec& X, const Vec& Y, const Vec& Z); + //@} + + + /*! @name Accessing values */ + //@{ + Vec axis() const; + qreal angle() const; + void getAxisAngle(Vec& axis, qreal& angle) const; + + /*! Bracket operator, with a constant return value. \p i must range in [0..3]. See the Quaternion(qreal, qreal, qreal, qreal) documentation. */ + qreal operator[](int i) const { return q[i]; } + + /*! Bracket operator returning an l-value. \p i must range in [0..3]. See the Quaternion(qreal, qreal, qreal, qreal) documentation. */ + qreal& operator[](int i) { return q[i]; } + //@} + + + /*! @name Rotation computations */ + //@{ + /*! Returns the composition of the \p a and \p b rotations. + + The order is important. When applied to a Vec \c v (see operator*(const Quaternion&, const Vec&) + and rotate()) the resulting Quaternion acts as if \p b was applied first and then \p a was + applied. This is obvious since the image \c v' of \p v by the composited rotation satisfies: \code + v'= (a*b) * v = a * (b*v) \endcode + + Note that a*b usually differs from b*a. + + \attention For efficiency reasons, the resulting Quaternion is not normalized. Use normalize() in + case of numerical drift with small rotation composition. */ + friend Quaternion operator*(const Quaternion& a, const Quaternion& b) + { + return Quaternion(a.q[3]*b.q[0] + b.q[3]*a.q[0] + a.q[1]*b.q[2] - a.q[2]*b.q[1], + a.q[3]*b.q[1] + b.q[3]*a.q[1] + a.q[2]*b.q[0] - a.q[0]*b.q[2], + a.q[3]*b.q[2] + b.q[3]*a.q[2] + a.q[0]*b.q[1] - a.q[1]*b.q[0], + a.q[3]*b.q[3] - b.q[0]*a.q[0] - a.q[1]*b.q[1] - a.q[2]*b.q[2]); + } + + /*! Quaternion rotation is composed with \p q. + + See operator*(), since this is equivalent to \c this = \c this * \p q. + + \note For efficiency reasons, the resulting Quaternion is not normalized. + You may normalize() it after each application in case of numerical drift. */ + Quaternion& operator*=(const Quaternion &q) + { + *this = (*this)*q; + return *this; + } + + /*! Returns the image of \p v by the rotation \p q. + + Same as q.rotate(v). See rotate() and inverseRotate(). */ + friend Vec operator*(const Quaternion& q, const Vec& v) + { + return q.rotate(v); + } + + Vec rotate(const Vec& v) const; + Vec inverseRotate(const Vec& v) const; + //@} + + + /*! @name Inversion */ + //@{ + /*! Returns the inverse Quaternion (inverse rotation). + + Result has a negated axis() direction and the same angle(). A composition (see operator*()) of a + Quaternion and its inverse() results in an identity function. + + Use invert() to actually modify the Quaternion. */ + Quaternion inverse() const { return Quaternion(-q[0], -q[1], -q[2], q[3]); } + + /*! Inverses the Quaternion (same rotation angle(), but negated axis()). + + See also inverse(). */ + void invert() { q[0] = -q[0]; q[1] = -q[1]; q[2] = -q[2]; } + + /*! Negates all the coefficients of the Quaternion. + + This results in an other representation of the \e same rotation (opposite rotation angle, but with + a negated axis direction: the two cancel out). However, note that the results of axis() and + angle() are unchanged after a call to this method since angle() always returns a value in [0,pi]. + + This method is mainly useful for Quaternion interpolation, so that the spherical + interpolation takes the shortest path on the unit sphere. See slerp() for details. */ + void negate() { invert(); q[3] = -q[3]; } + + /*! Normalizes the Quaternion coefficients. + + This method should not need to be called since we only deal with unit Quaternions. This is however + useful to prevent numerical drifts, especially with small rotational increments. See also + normalized(). */ + qreal normalize() + { + const qreal norm = sqrt(q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]); + for (int i=0; i<4; ++i) + q[i] /= norm; + return norm; + } + + /*! Returns a normalized version of the Quaternion. + + See also normalize(). */ + Quaternion normalized() const + { + qreal Q[4]; + const qreal norm = sqrt(q[0]*q[0] + q[1]*q[1] + q[2]*q[2] + q[3]*q[3]); + for (int i=0; i<4; ++i) + Q[i] = q[i] / norm; + return Quaternion(Q[0], Q[1], Q[2], Q[3]); + } + //@} + + + /*! @name Associated matrix */ + //@{ + const GLdouble* matrix() const; + void getMatrix(GLdouble m[4][4]) const; + void getMatrix(GLdouble m[16]) const; + + void getRotationMatrix(qreal m[3][3]) const; + + const GLdouble* inverseMatrix() const; + void getInverseMatrix(GLdouble m[4][4]) const; + void getInverseMatrix(GLdouble m[16]) const; + + void getInverseRotationMatrix(qreal m[3][3]) const; + //@} + + + /*! @name Slerp interpolation */ + //@{ + static Quaternion slerp(const Quaternion& a, const Quaternion& b, qreal t, bool allowFlip=true); + static Quaternion squad(const Quaternion& a, const Quaternion& tgA, const Quaternion& tgB, const Quaternion& b, qreal t); + /*! Returns the "dot" product of \p a and \p b: a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3]. */ + static qreal dot(const Quaternion& a, const Quaternion& b) { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3]; } + + Quaternion log(); + Quaternion exp(); + static Quaternion lnDif(const Quaternion& a, const Quaternion& b); + static Quaternion squadTangent(const Quaternion& before, const Quaternion& center, const Quaternion& after); + //@} + + /*! @name Random Quaternion */ + //@{ + static Quaternion randomQuaternion(); + //@} + + /*! @name XML representation */ + //@{ + explicit Quaternion(const QDomElement& element); + QDomElement domElement(const QString& name, QDomDocument& document) const; + void initFromDOMElement(const QDomElement& element); + //@} + +#ifdef DOXYGEN + /*! @name Output stream */ + //@{ + /*! Output stream operator. Enables debugging code like: + \code + Quaternion rot(...); + cout << "Rotation=" << rot << endl; + \endcode */ + std::ostream& operator<<(std::ostream& o, const qglviewer::Vec&); + //@} +#endif + +private: + /*! The internal data representation is private, use operator[] to access values. */ + qreal q[4]; +}; + +} // namespace + +std::ostream& operator<<(std::ostream& o, const qglviewer::Quaternion&); + +#endif // QGLVIEWER_QUATERNION_H diff --git a/QGLViewer/saveSnapshot.cpp b/QGLViewer/saveSnapshot.cpp new file mode 100644 index 0000000..5290010 --- /dev/null +++ b/QGLViewer/saveSnapshot.cpp @@ -0,0 +1,494 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "qglviewer.h" + +#include "ui_ImageInterface.h" + +// Output format list +# include + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +////// Static global variables - local to this file ////// +// List of available output file formats, formatted for QFileDialog. +static QString formats; +// Converts QFileDialog resulting format to Qt snapshotFormat. +static QMap Qtformat; +// Converts Qt snapshotFormat to QFileDialog menu string. +static QMap FDFormatString; +// Converts snapshotFormat to file extension +static QMap extension; + + +/*! Sets snapshotFileName(). */ +void QGLViewer::setSnapshotFileName(const QString& name) +{ + snapshotFileName_ = QFileInfo(name).absoluteFilePath(); +} + +#ifndef DOXYGEN +const QString& QGLViewer::snapshotFilename() const +{ + qWarning("snapshotFilename is deprecated. Use snapshotFileName() (uppercase N) instead."); + return snapshotFileName(); +} +#endif + + +/*! Opens a dialog that displays the different available snapshot formats. + +Then calls setSnapshotFormat() with the selected one (unless the user cancels). + +Returns \c false if the user presses the Cancel button and \c true otherwise. */ +bool QGLViewer::openSnapshotFormatDialog() +{ + bool ok = false; + QStringList list = formats.split(";;", QString::SkipEmptyParts); + int current = list.indexOf(FDFormatString[snapshotFormat()]); + QString format = QInputDialog::getItem(this, "Snapshot format", "Select a snapshot format", list, current, false, &ok); + if (ok) + setSnapshotFormat(Qtformat[format]); + return ok; +} + + +// Finds all available Qt output formats, so that they can be available in +// saveSnapshot dialog. Initialize snapshotFormat() to the first one. +void QGLViewer::initializeSnapshotFormats() +{ + QList list = QImageWriter::supportedImageFormats(); + QStringList formatList; + for (int i=0; i < list.size(); ++i) + formatList << QString(list.at(i).toUpper()); + // qWarning("Available image formats: "); + // QStringList::Iterator it = formatList.begin(); + // while( it != formatList.end() ) + // qWarning((*it++).); QT4 change this. qWarning no longer accepts QString + + // Check that the interesting formats are available and add them in "formats" + // Unused formats: XPM XBM PBM PGM + QStringList QtText, MenuText, Ext; + QtText += "JPEG"; MenuText += "JPEG (*.jpg)"; Ext += "jpg"; + QtText += "PNG"; MenuText += "PNG (*.png)"; Ext += "png"; + QtText += "EPS"; MenuText += "Encapsulated Postscript (*.eps)"; Ext += "eps"; + QtText += "PS"; MenuText += "Postscript (*.ps)"; Ext += "ps"; + QtText += "PPM"; MenuText += "24bit RGB Bitmap (*.ppm)"; Ext += "ppm"; + QtText += "BMP"; MenuText += "Windows Bitmap (*.bmp)"; Ext += "bmp"; + QtText += "XFIG"; MenuText += "XFig (*.fig)"; Ext += "fig"; + + QStringList::iterator itText = QtText.begin(); + QStringList::iterator itMenu = MenuText.begin(); + QStringList::iterator itExt = Ext.begin(); + + while (itText != QtText.end()) + { + //QMessageBox::information(this, "Snapshot ", "Trying format\n"+(*itText)); + if (formatList.contains((*itText))) + { + //QMessageBox::information(this, "Snapshot ", "Recognized format\n"+(*itText)); + if (formats.isEmpty()) + setSnapshotFormat(*itText); + else + formats += ";;"; + formats += (*itMenu); + Qtformat[(*itMenu)] = (*itText); + FDFormatString[(*itText)] = (*itMenu); + extension[(*itText)] = (*itExt); + } + // Synchronize parsing + itText++; + itMenu++; + itExt++; + } +} + +// Returns false if the user refused to use the fileName +static bool checkFileName(QString& fileName, QWidget* widget, const QString& snapshotFormat) +{ + if (fileName.isEmpty()) + return false; + + // Check that extension has been provided + QFileInfo info(fileName); + + if (info.suffix().isEmpty()) + { + // No extension given. Silently add one + if (fileName.right(1) != ".") + fileName += "."; + fileName += extension[snapshotFormat]; + info.setFile(fileName); + } + else if (info.suffix() != extension[snapshotFormat]) + { + // Extension is not appropriate. Propose a modification + QString modifiedName = info.absolutePath() + '/' + info.baseName() + "." + extension[snapshotFormat]; + QFileInfo modifInfo(modifiedName); + int i=(QMessageBox::warning(widget,"Wrong extension", + info.fileName()+" has a wrong extension.\nSave as "+modifInfo.fileName()+" instead ?", + QMessageBox::Yes, + QMessageBox::No, + QMessageBox::Cancel)); + if (i==QMessageBox::Cancel) + return false; + + if (i==QMessageBox::Yes) + { + fileName = modifiedName; + info.setFile(fileName); + } + } + + return true; +} + +class ImageInterface: public QDialog, public Ui::ImageInterface +{ +public: ImageInterface(QWidget *parent) : QDialog(parent) { setupUi(this); } +}; + + +// Pops-up an image settings dialog box and save to fileName. +// Returns false in case of problem. +bool QGLViewer::saveImageSnapshot(const QString& fileName) +{ + static ImageInterface* imageInterface = NULL; + + if (!imageInterface) + imageInterface = new ImageInterface(this); + + imageInterface->imgWidth->setValue(width()); + imageInterface->imgHeight->setValue(height()); + + imageInterface->imgQuality->setValue(snapshotQuality()); + + if (imageInterface->exec() == QDialog::Rejected) + return true; + + // Hide closed dialog + qApp->processEvents(); + + setSnapshotQuality(imageInterface->imgQuality->value()); + + QColor previousBGColor = backgroundColor(); + if (imageInterface->whiteBackground->isChecked()) + setBackgroundColor(Qt::white); + + QSize finalSize(imageInterface->imgWidth->value(), imageInterface->imgHeight->value()); + + qreal oversampling = imageInterface->oversampling->value(); + QSize subSize(int(this->width()/oversampling), int(this->height()/oversampling)); + + qreal aspectRatio = width() / static_cast(height()); + qreal newAspectRatio = finalSize.width() / static_cast(finalSize.height()); + + qreal zNear = camera()->zNear(); + //qreal zFar = camera()->zFar(); + + qreal xMin, yMin; + bool expand = imageInterface->expandFrustum->isChecked(); + if (camera()->type() == qglviewer::Camera::PERSPECTIVE) + if ((expand && (newAspectRatio>aspectRatio)) || (!expand && (newAspectRatiofieldOfView() / 2.0); + xMin = newAspectRatio * yMin; + } + else + { + xMin = zNear * tan(camera()->fieldOfView() / 2.0) * aspectRatio; + yMin = xMin / newAspectRatio; + } + else + { + camera()->getOrthoWidthHeight(xMin, yMin); + if ((expand && (newAspectRatio>aspectRatio)) || (!expand && (newAspectRatio(finalSize.width()); + qreal scaleY = subSize.height() / static_cast(finalSize.height()); + + //qreal deltaX = 2.0 * xMin * scaleX; + //qreal deltaY = 2.0 * yMin * scaleY; + + int nbX = finalSize.width() / subSize.width(); + int nbY = finalSize.height() / subSize.height(); + + // Extra subimage on the right/bottom border(s) if needed + if (nbX * subSize.width() < finalSize.width()) + nbX++; + if (nbY * subSize.height() < finalSize.height()) + nbY++; + + makeCurrent(); + + // tileRegion_ is used by startScreenCoordinatesSystem to appropriately set the local + // coordinate system when tiling + tileRegion_ = new TileRegion(); + qreal tileXMin, tileWidth, tileYMin, tileHeight; + if ((expand && (newAspectRatio>aspectRatio)) || (!expand && (newAspectRatiotextScale = 1.0 / scaleY; + } + else + { + qreal tileTotalHeight = width() / newAspectRatio; + tileYMin = (height() - tileTotalHeight) / 2.0; + tileHeight = tileTotalHeight * scaleY; + tileXMin = 0.0; + tileWidth = width() * scaleX; + tileRegion_->textScale = 1.0 / scaleX; + } + + int count=0; + for (int i=0; ixMin = tileXMin + i * tileWidth; + tileRegion_->xMax = tileXMin + (i+1) * tileWidth; + tileRegion_->yMin = tileYMin + j * tileHeight; + tileRegion_->yMax = tileYMin + (j+1) * tileHeight; + + draw(); + postDraw(); + + // ProgressDialog::hideProgressDialog(); + // qApp->processEvents(); + + QImage snapshot = grabFramebuffer(); + + // ProgressDialog::showProgressDialog(this); + // ProgressDialog::updateProgress(count / (qreal)(nbX*nbY), + // "Generating image ["+QString::number(count)+"/"+QString::number(nbX*nbY)+"]"); + // qApp->processEvents(); + + QImage subImage = snapshot.scaled(subSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + + // Copy subImage in image + for (int ii=0; iiwhiteBackground->isChecked()) + setBackgroundColor(previousBGColor); + + return saveOK; +} + + +/*! Saves a snapshot of the current image displayed by the widget. + + Options are set using snapshotFormat(), snapshotFileName() and snapshotQuality(). For non vectorial + image formats, the image size is equal to the current viewer's dimensions (see width() and + height()). See snapshotFormat() for details on supported formats. + + If \p automatic is \c false (or if snapshotFileName() is empty), a file dialog is opened to ask for + the file name. + + When \p automatic is \c true, the file name is set to \c NAME-NUMBER, where \c NAME is + snapshotFileName() and \c NUMBER is snapshotCounter(). The snapshotCounter() is automatically + incremented after each snapshot saving. This is useful to create videos from your application: + \code + void Viewer::init() + { + resize(720, 576); // PAL DV format (use 720x480 for NTSC DV) + connect(this, SIGNAL(drawFinished(bool)), SLOT(saveSnapshot(bool))); + } + \endcode + Then call draw() in a loop (for instance using animate() and/or a camera() KeyFrameInterpolator + replay) to create your image sequence. + + If you want to create a Quicktime VR panoramic sequence, simply use code like this: + \code + void Viewer::createQuicktime() + { + const int nbImages = 36; + for (int i=0; isetOrientation(2.0*M_PI/nbImages, 0.0); // Theta-Phi orientation + showEntireScene(); + update(); // calls draw(), which emits drawFinished(), which calls saveSnapshot() + } + } + \endcode + + If snapshotCounter() is negative, no number is appended to snapshotFileName() and the + snapshotCounter() is not incremented. This is useful to force the creation of a file, overwriting + the previous one. + + When \p overwrite is set to \c false (default), a window asks for confirmation if the file already + exists. In \p automatic mode, the snapshotCounter() is incremented (if positive) until a + non-existing file name is found instead. Otherwise the file is overwritten without confirmation. + + The VRender library was written by Cyril Soler (Cyril dot Soler at imag dot fr). If the generated + PS or EPS file is not properly displayed, remove the anti-aliasing option in your postscript viewer. + + \note In order to correctly grab the frame buffer, the QGLViewer window is raised in front of + other windows by this method. */ +void QGLViewer::saveSnapshot(bool automatic, bool overwrite) +{ + // Ask for file name + if (snapshotFileName().isEmpty() || !automatic) + { + QString fileName; + QString selectedFormat = FDFormatString[snapshotFormat()]; + fileName = QFileDialog::getSaveFileName(this, "Choose a file name to save under", snapshotFileName(), formats, &selectedFormat, + overwrite?QFileDialog::DontConfirmOverwrite:QFlags(0)); + setSnapshotFormat(Qtformat[selectedFormat]); + + if (checkFileName(fileName, this, snapshotFormat())) + setSnapshotFileName(fileName); + else + return; + } + + QFileInfo fileInfo(snapshotFileName()); + + if ((automatic) && (snapshotCounter() >= 0)) + { + // In automatic mode, names have a number appended + const QString baseName = fileInfo.baseName(); + QString count; + count.sprintf("%.04d", snapshotCounter_++); + QString suffix; + suffix = fileInfo.suffix(); + if (suffix.isEmpty()) + suffix = extension[snapshotFormat()]; + fileInfo.setFile(fileInfo.absolutePath()+ '/' + baseName + '-' + count + '.' + suffix); + + if (!overwrite) + while (fileInfo.exists()) + { + count.sprintf("%.04d", snapshotCounter_++); + fileInfo.setFile(fileInfo.absolutePath() + '/' +baseName + '-' + count + '.' + fileInfo.suffix()); + } + } + + bool saveOK; + if (automatic) + { + QImage snapshot = frameBufferSnapshot(); + saveOK = snapshot.save(fileInfo.filePath(), snapshotFormat().toLatin1().constData(), snapshotQuality()); + } + else + saveOK = saveImageSnapshot(fileInfo.filePath()); + + if (!saveOK) + QMessageBox::warning(this, "Snapshot problem", "Unable to save snapshot in\n"+fileInfo.filePath()); +} + +QImage QGLViewer::frameBufferSnapshot() +{ + // Viewer must be on top of other windows. + makeCurrent(); + raise(); + // Hack: Qt has problems if the frame buffer is grabbed after QFileDialog is displayed. + // We grab the frame buffer before, even if it might be not necessary (vectorial rendering). + // The problem could not be reproduced on a simple example to submit a Qt bug. + // However, only grabs the backgroundImage in the eponym example. May come from the driver. + return grabFramebuffer(); +} + +/*! Same as saveSnapshot(), except that it uses \p fileName instead of snapshotFileName(). + + If \p fileName is empty, opens a file dialog to select the name. + + Snapshot settings are set from snapshotFormat() and snapshotQuality(). + + Asks for confirmation when \p fileName already exists and \p overwrite is \c false (default). + + \attention If \p fileName is a char* (as is "myFile.jpg"), it may be casted into a \c bool, and the + other saveSnapshot() method may be used instead. Pass QString("myFile.jpg") as a parameter to + prevent this. */ +void QGLViewer::saveSnapshot(const QString& fileName, bool overwrite) +{ + const QString previousName = snapshotFileName(); + const int previousCounter = snapshotCounter(); + setSnapshotFileName(fileName); + setSnapshotCounter(-1); + saveSnapshot(true, overwrite); + setSnapshotFileName(previousName); + setSnapshotCounter(previousCounter); +} + +/*! Takes a snapshot of the current display and pastes it to the clipboard. + +This action is activated by the KeyboardAction::SNAPSHOT_TO_CLIPBOARD enum, binded to \c Ctrl+C by default. +*/ +void QGLViewer::snapshotToClipboard() +{ + QClipboard *cb = QApplication::clipboard(); + cb->setImage(frameBufferSnapshot()); +} + diff --git a/QGLViewer/vec.cpp b/QGLViewer/vec.cpp new file mode 100644 index 0000000..669d6f7 --- /dev/null +++ b/QGLViewer/vec.cpp @@ -0,0 +1,164 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "domUtils.h" +#include "vec.h" + +// Most of the methods are declared inline in vec.h + +using namespace qglviewer; +using namespace std; + +/*! Projects the Vec on the axis of direction \p direction that passes through the origin. + +\p direction does not need to be normalized (but must be non null). */ +void Vec::projectOnAxis(const Vec& direction) +{ +#ifndef QT_NO_DEBUG + if (direction.squaredNorm() < 1.0E-10) + qWarning("Vec::projectOnAxis: axis direction is not normalized (norm=%f).", direction.norm()); +#endif + + *this = (((*this)*direction) / direction.squaredNorm()) * direction; +} + +/*! Projects the Vec on the plane whose normal is \p normal that passes through the origin. + +\p normal does not need to be normalized (but must be non null). */ +void Vec::projectOnPlane(const Vec& normal) +{ +#ifndef QT_NO_DEBUG + if (normal.squaredNorm() < 1.0E-10) + qWarning("Vec::projectOnPlane: plane normal is not normalized (norm=%f).", normal.norm()); +#endif + + *this -= (((*this)*normal) / normal.squaredNorm()) * normal; +} + +/*! Returns a Vec orthogonal to the Vec. Its norm() depends on the Vec, but is zero only for a + null Vec. Note that the function that associates an orthogonalVec() to a Vec is not continous. */ +Vec Vec::orthogonalVec() const +{ + // Find smallest component. Keep equal case for null values. + if ((fabs(y) >= 0.9*fabs(x)) && (fabs(z) >= 0.9*fabs(x))) + return Vec(0.0, -z, y); + else + if ((fabs(x) >= 0.9*fabs(y)) && (fabs(z) >= 0.9*fabs(y))) + return Vec(-z, 0.0, x); + else + return Vec(-y, x, 0.0); +} + +/*! Constructs a Vec from a \c QDomElement representing an XML code of the form + \code< anyTagName x=".." y=".." z=".." />\endcode + +If one of these attributes is missing or is not a number, a warning is displayed and the associated +value is set to 0.0. + +See also domElement() and initFromDOMElement(). */ +Vec::Vec(const QDomElement& element) +{ + QStringList attribute; + attribute << "x" << "y" << "z"; + for (int i=0; ioperator[](i) = DomUtils::qrealFromDom(element, attribute[i], 0.0); +#else + v_[i] = DomUtils::qrealFromDom(element, attribute[i], 0.0); +#endif +} + +/*! Returns an XML \c QDomElement that represents the Vec. + + \p name is the name of the QDomElement tag. \p doc is the \c QDomDocument factory used to create + QDomElement. + + When output to a file, the resulting QDomElement will look like: + \code + + \endcode + + Use initFromDOMElement() to restore the Vec state from the resulting \c QDomElement. See also the + Vec(const QDomElement&) constructor. + + Here is complete example that creates a QDomDocument and saves it into a file: + \code + Vec sunPos; + QDomDocument document("myDocument"); + QDomElement sunElement = document.createElement("Sun"); + document.appendChild(sunElement); + sunElement.setAttribute("brightness", sunBrightness()); + sunElement.appendChild(sunPos.domElement("sunPosition", document)); + // Other additions to the document hierarchy... + + // Save doc document + QFile f("myFile.xml"); + if (f.open(IO_WriteOnly)) + { + QTextStream out(&f); + document.save(out, 2); + f.close(); + } + \endcode + + See also Quaternion::domElement(), Frame::domElement(), Camera::domElement()... */ +QDomElement Vec::domElement(const QString& name, QDomDocument& document) const +{ + QDomElement de = document.createElement(name); + de.setAttribute("x", QString::number(x)); + de.setAttribute("y", QString::number(y)); + de.setAttribute("z", QString::number(z)); + return de; +} + +/*! Restores the Vec state from a \c QDomElement created by domElement(). + + The \c QDomElement should contain \c x, \c y and \c z attributes. If one of these attributes is + missing or is not a number, a warning is displayed and the associated value is set to 0.0. + + To restore the Vec state from an xml file, use: + \code + // Load DOM from file + QDomDocument doc; + QFile f("myFile.xml"); + if (f.open(IO_ReadOnly)) + { + doc.setContent(&f); + f.close(); + } + // Parse the DOM tree and initialize + QDomElement main=doc.documentElement(); + myVec.initFromDOMElement(main); + \endcode + + See also the Vec(const QDomElement&) constructor. */ +void Vec::initFromDOMElement(const QDomElement& element) +{ + const Vec v(element); + *this = v; +} + +ostream& operator<<(ostream& o, const Vec& v) +{ + return o << v.x << '\t' << v.y << '\t' << v.z; +} + diff --git a/QGLViewer/vec.h b/QGLViewer/vec.h new file mode 100644 index 0000000..429ab62 --- /dev/null +++ b/QGLViewer/vec.h @@ -0,0 +1,390 @@ +/**************************************************************************** + + Copyright (C) 2002-2014 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.6.3. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef QGLVIEWER_VEC_H +#define QGLVIEWER_VEC_H + +#include +#include + +# include + +// Included by all files as vec.h is at the end of the include hierarchy +#include "config.h" // Specific configuration options. + +namespace qglviewer { + +/*! \brief The Vec class represents 3D positions and 3D vectors. + \class Vec vec.h QGLViewer/vec.h + + Vec is used as a parameter and return type by many methods of the library. It provides classical + algebraic computational methods and is compatible with OpenGL: + + \code + // Draws a point located at 3.0 OpenGL units in front of the camera + Vec pos = camera()->position() + 3.0 * camera()->viewDirection(); + glBegin(GL_POINTS); + glVertex3fv(pos); + glEnd(); + \endcode + + This makes of Vec a good candidate for representing positions and vectors in your programs. Since + it is part of the \c qglviewer namespace, specify \c qglviewer::Vec or use the qglviewer + namespace: + \code + using namespace qglviewer; + \endcode + +

Interface with other vector classes

+ + Vec implements a universal explicit converter, based on the \c [] \c operator. + Everywhere a \c const \c Vec& argument is expected, you can use your own vector type + instead, as long as it implements this operator (see the Vec(const C& c) documentation). + + See also the Quaternion and the Frame documentations. + \nosubgrouping */ +class QGLVIEWER_EXPORT Vec +{ + + // If your compiler complains the "The class "qglviewer::Vec" has no member "x"." + // Add your architecture Q_OS_XXXX flag (see qglobal.h) in this list. +#if defined (Q_OS_IRIX) || defined (Q_OS_AIX) || defined (Q_OS_HPUX) +# define QGLVIEWER_UNION_NOT_SUPPORTED +#endif + +public: + /*! The internal data representation is public. One can use v.x, v.y, v.z. See also operator[](). */ +#if defined (DOXYGEN) || defined (QGLVIEWER_UNION_NOT_SUPPORTED) + qreal x, y, z; +#else + union + { + struct { qreal x, y, z; }; + qreal v_[3]; + }; +#endif + + /*! @name Setting the value */ + //@{ + /*! Default constructor. Value is set to (0,0,0). */ + Vec() : x(0.0), y(0.0), z(0.0) {} + + /*! Standard constructor with the x, y and z values. */ + Vec(qreal X, qreal Y, qreal Z) : x(X), y(Y), z(Z) {} + + /*! Universal explicit converter from any class to Vec. You can use your own vector class everywhere + a \c const \c Vec& parameter is required, as long as it implements the \c operator[ ]: + + \code + class MyVec + { + // ... + qreal operator[](int i) const { returns x, y or z when i=0, 1 or 2; } + } + + MyVec v(...); + camera()->setPosition(v); + \endcode + + Note that standard vector types (STL, \c qreal[3], ...) implement this operator and can hence + be used in place of Vec. See also operator const qreal*() .*/ + template + explicit Vec(const C& c) : x(c[0]), y(c[1]), z(c[2]) {} + // Should NOT be explicit to prevent conflicts with operator<<. + + // ! Copy constructor + // Vec(const Vec& v) : x(v.x), y(v.y), z(v.z) {} + + /*! Equal operator. */ + Vec& operator=(const Vec& v) + { + x = v.x; y = v.y; z = v.z; + return *this; + } + + /*! Set the current value. May be faster than using operator=() with a temporary Vec(x,y,z). */ + void setValue(qreal X, qreal Y, qreal Z) + { x=X; y=Y; z=Z; } + + // Universal equal operator which allows the use of any type in place of Vec, + // as long as the [] operator is implemented (v[0]=v.x, v[1]=v.y, v[2]=v.z). + // template + // Vec& operator=(const C& c) + // { + // x=c[0]; y=c[1]; z=c[2]; + // return *this; + // } + //@} + + /*! @name Accessing the value */ + //@{ + /*! Bracket operator, with a constant return value. \p i must range in [0..2]. */ + qreal operator[](int i) const { +#ifdef QGLVIEWER_UNION_NOT_SUPPORTED + return (&x)[i]; +#else + return v_[i]; +#endif + } + + /*! Bracket operator returning an l-value. \p i must range in [0..2]. */ + qreal& operator[](int i) { +#ifdef QGLVIEWER_UNION_NOT_SUPPORTED + return (&x)[i]; +#else + return v_[i]; +#endif + } + +#ifndef DOXYGEN + /*! This method is deprecated since version 2.0. Use operator const double* instead. */ + const double* address() const { qWarning("Vec::address() is deprecated, use operator const double* instead."); return operator const double*(); } +#endif + + /*! Conversion operator returning the memory address of the vector. + + Very convenient to pass a Vec pointer as a parameter to \c GLdouble OpenGL functions: + \code + Vec pos, normal; + glNormal3dv(normal); + glVertex3dv(pos); + \endcode */ + operator const double*() const { +#ifdef QGLVIEWER_UNION_NOT_SUPPORTED + return &x; +#else + return v_; +#endif + } + + /*! Non const conversion operator returning the memory address of the vector. + + Useful to pass a Vec to a method that requires and fills a \c double*, as provided by certain libraries. */ + operator double*() { +#ifdef QGLVIEWER_UNION_NOT_SUPPORTED + return &x; +#else + return v_; +#endif + } + + /*! Conversion operator returning the memory address of the vector. + + Very convenient to pass a Vec pointer as a \c float parameter to OpenGL functions: + \code + Vec pos, normal; + glNormal3fv(normal); + glVertex3fv(pos); + \endcode + \note The returned float array is a static shared by all \c Vec instances. */ + operator const float*() const { + static float* const result = new float[3]; + result[0] = (float)x; + result[1] = (float)y; + result[2] = (float)z; + return result; + } + //@} + + /*! @name Algebraic computations */ + //@{ + /*! Returns the sum of the two vectors. */ + friend Vec operator+(const Vec &a, const Vec &b) + { + return Vec(a.x+b.x, a.y+b.y, a.z+b.z); + } + + /*! Returns the difference of the two vectors. */ + friend Vec operator-(const Vec &a, const Vec &b) + { + return Vec(a.x-b.x, a.y-b.y, a.z-b.z); + } + + /*! Unary minus operator. */ + friend Vec operator-(const Vec &a) + { + return Vec(-a.x, -a.y, -a.z); + } + + /*! Returns the product of the vector with a scalar. */ + friend Vec operator*(const Vec &a, qreal k) + { + return Vec(a.x*k, a.y*k, a.z*k); + } + + /*! Returns the product of a scalar with the vector. */ + friend Vec operator*(qreal k, const Vec &a) + { + return a*k; + } + + /*! Returns the division of the vector with a scalar. + + Too small \p k values are \e not tested (unless the library was compiled with the "debug" Qt \c + CONFIG flag) and may result in \c NaN values. */ + friend Vec operator/(const Vec &a, qreal k) + { +#ifndef QT_NO_DEBUG + if (fabs(k) < 1.0E-10) + qWarning("Vec::operator / : dividing by a null value (%f)", k); +#endif + return Vec(a.x/k, a.y/k, a.z/k); + } + + /*! Returns \c true only when the two vector are not equal (see operator==()). */ + friend bool operator!=(const Vec &a, const Vec &b) + { + return !(a==b); + } + + /*! Returns \c true when the squaredNorm() of the difference vector is lower than 1E-10. */ + friend bool operator==(const Vec &a, const Vec &b) + { + const qreal epsilon = 1.0E-10; + return (a-b).squaredNorm() < epsilon; + } + + /*! Adds \p a to the vector. */ + Vec& operator+=(const Vec &a) + { + x += a.x; y += a.y; z += a.z; + return *this; + } + + /*! Subtracts \p a to the vector. */ + Vec& operator-=(const Vec &a) + { + x -= a.x; y -= a.y; z -= a.z; + return *this; + } + + /*! Multiply the vector by a scalar \p k. */ + Vec& operator*=(qreal k) + { + x *= k; y *= k; z *= k; + return *this; + } + + /*! Divides the vector by a scalar \p k. + + An absolute \p k value lower than 1E-10 will print a warning if the library was compiled with the + "debug" Qt \c CONFIG flag. Otherwise, no test is performed for efficiency reasons. */ + Vec& operator/=(qreal k) + { +#ifndef QT_NO_DEBUG + if (fabs(k)<1.0E-10) + qWarning("Vec::operator /= : dividing by a null value (%f)", k); +#endif + x /= k; y /= k; z /= k; + return *this; + } + + /*! Dot product of the two Vec. */ + friend qreal operator*(const Vec &a, const Vec &b) + { + return a.x*b.x + a.y*b.y + a.z*b.z; + } + + /*! Cross product of the two vectors. Same as cross(). */ + friend Vec operator^(const Vec &a, const Vec &b) + { + return cross(a,b); + } + + /*! Cross product of the two Vec. Mind the order ! */ + friend Vec cross(const Vec &a, const Vec &b) + { + return Vec(a.y*b.z - a.z*b.y, + a.z*b.x - a.x*b.z, + a.x*b.y - a.y*b.x); + } + + Vec orthogonalVec() const; + //@} + + /*! @name Norm of the vector */ + //@{ +#ifndef DOXYGEN + /*! This method is deprecated since version 2.0. Use squaredNorm() instead. */ + qreal sqNorm() const { return x*x + y*y + z*z; } +#endif + + /*! Returns the \e squared norm of the Vec. */ + qreal squaredNorm() const { return x*x + y*y + z*z; } + + /*! Returns the norm of the vector. */ + qreal norm() const { return sqrt(x*x + y*y + z*z); } + + /*! Normalizes the Vec and returns its original norm. + + Normalizing a null vector will result in \c NaN values. */ + qreal normalize() + { + const qreal n = norm(); +#ifndef QT_NO_DEBUG + if (n < 1.0E-10) + qWarning("Vec::normalize: normalizing a null vector (norm=%f)", n); +#endif + *this /= n; + return n; + } + + /*! Returns a unitary (normalized) \e representation of the vector. The original Vec is not modified. */ + Vec unit() const + { + Vec v = *this; + v.normalize(); + return v; + } + //@} + + /*! @name Projection */ + //@{ + void projectOnAxis(const Vec& direction); + void projectOnPlane(const Vec& normal); + //@} + + /*! @name XML representation */ + //@{ + explicit Vec(const QDomElement& element); + QDomElement domElement(const QString& name, QDomDocument& document) const; + void initFromDOMElement(const QDomElement& element); + //@} + +#ifdef DOXYGEN + /*! @name Output stream */ + //@{ + /*! Output stream operator. Enables debugging code like: + \code + Vec pos(...); + cout << "Position=" << pos << endl; + \endcode */ + std::ostream& operator<<(std::ostream& o, const qglviewer::Vec&); + //@} +#endif +}; + +} // namespace + +std::ostream& operator<<(std::ostream& o, const qglviewer::Vec&); + +#endif // QGLVIEWER_VEC_H diff --git a/log750-lab.pro b/log750-lab.pro new file mode 100644 index 0000000..6d7ba5b --- /dev/null +++ b/log750-lab.pro @@ -0,0 +1,73 @@ +#------------------------------------------------- +# +# Project created by QtCreator +# +#------------------------------------------------- + +QT += core gui xml opengl + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = simpleViewer +TEMPLATE = app + +QMAKE_CXXFLAGS += -std=c++0x + +SOURCES += QGLViewer/camera.cpp \ + QGLViewer/constraint.cpp \ + QGLViewer/frame.cpp \ + QGLViewer/keyFrameInterpolator.cpp \ + QGLViewer/manipulatedCameraFrame.cpp \ + QGLViewer/manipulatedFrame.cpp \ + QGLViewer/mouseGrabber.cpp \ + QGLViewer/qglviewer.cpp \ + QGLViewer/quaternion.cpp \ + QGLViewer/saveSnapshot.cpp \ + QGLViewer/vec.cpp \ + src/main.cpp \ + src/window/mainwindow.cpp \ + src/viewer/simpleViewer.cpp \ + src/glnodes/glnode.cpp \ + src/glnodes/shapes.cpp \ + src/glnodes/scenegroup.cpp + +HEADERS += QGLViewer/camera.h \ + QGLViewer/config.h \ + QGLViewer/constraint.h \ + QGLViewer/domUtils.h \ + QGLViewer/frame.h \ + QGLViewer/keyFrameInterpolator.h \ + QGLViewer/manipulatedCameraFrame.h \ + QGLViewer/manipulatedFrame.h \ + QGLViewer/mouseGrabber.h \ + QGLViewer/qglviewer.h \ + QGLViewer/quaternion.h \ + QGLViewer/vec.h \ + src/window/mainwindow.h \ + src/viewer/simpleViewer.h \ + src/glnodes/glnode.h \ + src/glnodes/shapes.h \ + src/interfaces/ivisitable.h \ + src/interfaces/visitor.h \ + src/glnodes/scenegroup.h + +DISTFILES += src/shaders/basicShader.vert \ + src/shaders/basicShader.frag + +FORMS += QGLViewer/ImageInterface.ui mainwindow.ui + + +CONFIG *= debug_and_release console qt opengl warn_on thread create_prl rtti + +DEFINES *= QGLVIEWER_STATIC +win32 { + DEFINES *= NOMINMAX +} + +win32 { + contains ( QT_MAJOR_VERSION, 5 ) { + greaterThan( QT_MINOR_VERSION, 4) { + LIBS *= -lopengl32 + } + } +} diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..3283213 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,176 @@ + + + MainWindow + + + + 0 + 0 + 588 + 499 + + + + MainWindow + + + + + + + + 1 + 1 + + + + + 500 + 500 + + + + + 500 + 500 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + Forme + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Couleur + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Vide + + + + + Triangle + + + + + Carré + + + + + Cercle + + + + + + + + Couleur + + + false + + + false + + + + + + + + + + + 0 + 0 + 588 + 20 + + + + + &File + + + + + + + + + &Quit + + + Esc + + + + + + + + action_Quit + triggered() + MainWindow + close() + + + -1 + -1 + + + 199 + 149 + + + + + diff --git a/src/glnodes/glnode.cpp b/src/glnodes/glnode.cpp new file mode 100644 index 0000000..022ede8 --- /dev/null +++ b/src/glnodes/glnode.cpp @@ -0,0 +1,17 @@ +#include "glnode.h" +#include + + +//GlNode& GlNode::getChild() { +// GlNode* tempNode = new GlNode(); +// return *tempNode; +//} + +//bool GlNode::hasNext(){ +// return false; +//} + +//void GlNode::accept(Visitor &v) { + +// //v.visit(this); +//} diff --git a/src/glnodes/glnode.h b/src/glnodes/glnode.h new file mode 100644 index 0000000..6d88ffc --- /dev/null +++ b/src/glnodes/glnode.h @@ -0,0 +1,14 @@ +#ifndef GLNODE +#define GLNODE +#include +#include "../interfaces/ivisitable.h" +#include "../interfaces/visitor.h" +class Viewer; +class GlNode : public IVisitable { +public: + + QMatrix4x4 transform; +}; + +#endif // GLNODE + diff --git a/src/glnodes/scenegroup.cpp b/src/glnodes/scenegroup.cpp new file mode 100644 index 0000000..551339d --- /dev/null +++ b/src/glnodes/scenegroup.cpp @@ -0,0 +1,42 @@ +#include "scenegroup.h" + +SceneGroup::SceneGroup() +{ + +} + +std::vector* SceneGroup::getChildren() { + return &children; +} + +GlNode* SceneGroup::childAt(int i) { + return children.at(i); +} + +GlNode* SceneGroup::getChild(){ + // Automatically loops + if(childIndex >= children.size() || childIndex < 0){ //just in case + childIndex = 0; + } + return children.at(childIndex++); +} + +bool SceneGroup::hasNext() +{ + if(childIndex >= children.size() || childIndex < 0) { + childIndex = 0; + return false; + } + + return true; +} + +void SceneGroup::addChild(GlNode* child){ + + children.push_back(child); + //children.push_back(std::shared_ptr(child)); +} + +void SceneGroup::accept(Visitor &v) { + v.visit(*this); +} diff --git a/src/glnodes/scenegroup.h b/src/glnodes/scenegroup.h new file mode 100644 index 0000000..9296bed --- /dev/null +++ b/src/glnodes/scenegroup.h @@ -0,0 +1,26 @@ +#ifndef SCENEGROUP_H +#define SCENEGROUP_H + +#include "glnode.h" +#include "../interfaces/ivisitable.h" +#include "../interfaces/visitor.h" + +class SceneGroup : public GlNode +{ +private: + std::vector children; + int childIndex = 0; +public: + void addChild(GlNode* c); + GlNode* getChild(); + bool hasNext(); + + std::vector* getChildren(); + + GlNode* childAt(int i); + + void accept(Visitor& v) override; + SceneGroup(); +}; + +#endif // SCENEGROUP_H diff --git a/src/glnodes/shapes.cpp b/src/glnodes/shapes.cpp new file mode 100644 index 0000000..55de8f2 --- /dev/null +++ b/src/glnodes/shapes.cpp @@ -0,0 +1,44 @@ +#include "shapes.h" +#include +#include + +void Square::accept(Visitor &v) { + v.visit(*this); +} + +void Circle::accept(Visitor &v) { + v.visit(*this); +} + +void Triangle::accept(Visitor &v) { + v.visit(*this); +} + +// *** + +void Square::setColor(QColor& c) { + color = QColor(c); +} + +void Circle::setColor(QColor& c) { + color = QColor(c); +} + +void Triangle::setColor(QColor& c) { + color = QColor(c); +} + + +QColor Triangle::getColor(){ + return color; +}; + + +QColor Circle::getColor(){ + return color; +}; + + +QColor Square::getColor(){ + return color; +}; diff --git a/src/glnodes/shapes.h b/src/glnodes/shapes.h new file mode 100644 index 0000000..2f92c5b --- /dev/null +++ b/src/glnodes/shapes.h @@ -0,0 +1,41 @@ +#ifndef SHAPES_H +#define SHAPES_H +#include "glnode.h" +#include +#include +#include "../interfaces/visitor.h" +class Shape : public GlNode +{ +public: + virtual void setColor(QColor& c) = 0; + virtual QColor getColor() = 0; +}; + +class Circle : public Shape +{ +public: + Circle(){} + QColor color; + void accept(Visitor& v) override; + void setColor(QColor& c); + QColor getColor(); +}; +class Triangle : public Shape +{ +public: + Triangle(){} + QColor color; + void accept(Visitor& v) override; + void setColor(QColor& c); + QColor getColor(); +}; +class Square : public Shape +{ +public: + Square(){} + QColor color; + void accept(Visitor& v) override; + void setColor(QColor& c); + QColor getColor(); +}; +#endif // SHAPES_H diff --git a/src/interfaces/ivisitable.h b/src/interfaces/ivisitable.h new file mode 100644 index 0000000..804baf7 --- /dev/null +++ b/src/interfaces/ivisitable.h @@ -0,0 +1,10 @@ +#ifndef IVISITABLE_H +#define IVISITABLE_H +class Visitor; + +class IVisitable { +public: + virtual void accept(Visitor& v) = 0; +}; + +#endif // IVISITABLE_H diff --git a/src/interfaces/visitor.h b/src/interfaces/visitor.h new file mode 100644 index 0000000..9786fb9 --- /dev/null +++ b/src/interfaces/visitor.h @@ -0,0 +1,15 @@ +#ifndef VISITOR_H +#define VISITOR_H +class SceneGroup; +class Square; +class Circle; +class Triangle; + +class Visitor { +public: + virtual void visit(SceneGroup &n) = 0; + virtual void visit(Square &s) = 0; + virtual void visit(Circle &s) = 0; + virtual void visit(Triangle &s) = 0; +}; +#endif // VISITOR_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..2c5842c --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,29 @@ +#include "src/viewer/simpleViewer.h" +#include "src/window/mainwindow.h" +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + QDesktopWidget dw; + + // Set the core profile and version of OpenGL shaders + QSurfaceFormat fmt; + fmt.setVersion(4, 0); + fmt.setProfile(QSurfaceFormat::CoreProfile); + QSurfaceFormat::setDefaultFormat(fmt); + + MainWindow w; + + // Instantiate and layout the viewer. + Viewer *v = new Viewer(); + w.addViewer(v); + + //w.setFixedSize(dw.width() * 0.7, dw.height() * 0.7); + + w.show(); + + return a.exec(); +} diff --git a/src/shaders/basicShader.frag b/src/shaders/basicShader.frag new file mode 100644 index 0000000..7252e9d --- /dev/null +++ b/src/shaders/basicShader.frag @@ -0,0 +1,9 @@ +#version 400 core +in vec4 ifColor; +out vec4 fColor; + +void +main() +{ + fColor = ifColor; +} diff --git a/src/shaders/basicShader.vert b/src/shaders/basicShader.vert new file mode 100644 index 0000000..8edff8e --- /dev/null +++ b/src/shaders/basicShader.vert @@ -0,0 +1,14 @@ +#version 400 core +uniform mat4 mvMatrix; +uniform mat4 projMatrix; +uniform vec4 color; +in vec4 vPosition; +out vec4 ifColor; + +void +main() +{ + gl_Position = projMatrix * mvMatrix * vPosition; + ifColor = color; +} + diff --git a/src/viewer/simpleViewer.cpp b/src/viewer/simpleViewer.cpp new file mode 100644 index 0000000..e982567 --- /dev/null +++ b/src/viewer/simpleViewer.cpp @@ -0,0 +1,444 @@ +/**************************************************************************** + + Copyright (C) 2002-2008 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.3.6. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#include "simpleViewer.h" +#include "../interfaces/visitor.h" +#include "../glnodes/scenegroup.h" +#include "../glnodes/shapes.h" + +#include +#include + +#include + +#include +#include +#include +#include +using namespace std; + +#define BUFFER_OFFSET(i) ((char *)NULL + (i)) +#define GRID_SIZE 10; + +namespace +{ +const int numVerticesSquare = 5; +const int numVerticesTriangle = 3; +const int numVerticesCircle = 26; +const double frame_limit = 5; +const double inc_mult = 5; +const double inc_offset = 1.05; +} + +Viewer::Viewer() +{ + activeColor = new QColor(255, 255, 255, 255); + activeCell = nullptr; + activeShape = 0; +} + +Viewer::~Viewer() +{ + cleanup(); +} + +void Viewer::cleanup() +{ + makeCurrent(); + + // Delete shaders + delete m_program; + m_program = 0; + + // Delete buffers + glDeleteBuffers(NumBuffers, m_Buffers); + glDeleteVertexArrays(NumVAOs, m_VAOs); + + doneCurrent(); +} + +void Viewer::draw() +{ + // Bind our vertex/fragment shaders + m_program->bind(); + + // Get projection and camera transformations + QMatrix4x4 projectionMatrix; + QMatrix4x4 modelViewMatrix; + camera()->getProjectionMatrix(projectionMatrix); + camera()->getModelViewMatrix(modelViewMatrix); + + // Prepare a transformation stack + + // stack modelStack; + + modelViewMatrix.translate(-4.5, -4.5); + modelViewMatrix.scale(0.95); + + m_program->setUniformValue(m_projMatrixLocation, projectionMatrix); + m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix); + + // Traverse the Scene in order to draw its components + + modelStack.push(modelViewMatrix); + root.accept(*this); +} + +void Viewer::mouseMoveEvent(QMouseEvent* e) { + cout << "Viewer::mouseMoveEvent(QMouseEvent* e)" << endl; + // Normal QGLViewer behavior. + //QGLViewer::mouseMoveEvent(e); +} + +void Viewer::mousePressEvent(QMouseEvent* e) { + + cout << "Viewer::mouseMoveEvent(QMouseEvent* e) : " << e->button() << endl; + + if(e->button() == 1){ // LMB + + int truY = 10 - (e->y()) / 500.0 * GRID_SIZE; + int truX = (e->x() / 500.0) * GRID_SIZE; + + cout << " -->Getting cell at " << truX << " : " << truY << endl; + + SceneGroup* row = dynamic_cast (root.childAt(truY)); + cout << " -->" << row << endl; + SceneGroup* cell = dynamic_cast (row->childAt(truX)); + cout << " -->" << cell << endl; + + if(e->modifiers() & Qt::ShiftModifier){ + // add a shape + if(!cell->getChildren()->size()){ + + // WARNING: CODE DEGEULASSE + + Shape* s = nullptr; + if(activeShape == 1){ + s = new Triangle(); + }else if(activeShape == 2){ + s = new Square(); + }else if(activeShape == 3){ + s = new Circle(); + } + + // WARNING: END OF CODE DEGEULASSE + + //activeCell->getChildren()->at(0) = s; + if(s != nullptr){ + s->setColor(*activeColor); + cell->addChild(s); + this->update(); + deselect(); + activeCell = cell; + } + } + } + + if(e->modifiers() & Qt::ControlModifier){ + // select a shape + deselect(); + activeCell = cell; + if(activeCell != nullptr && activeCell->getChildren()->size()){ + std::cout << "Cell has children..." << endl; + Shape* shape = dynamic_cast (activeCell->childAt(0)); + QColor newColor = shape->getColor(); + std::cout << newColor.Rgb << endl; + newColor.setAlpha(255); + shape->setColor(newColor); + //emit shapeSelected(getTypeIndex(typeof activeCell->childAt(0))); + int shapeId = 0; + if(typeid(*(activeCell->getChildren()->at(0))) == typeid(Triangle)) + shapeId = 1; + if(typeid(*(activeCell->getChildren()->at(0))) == typeid(Square)) + shapeId = 2; + if(typeid(*(activeCell->getChildren()->at(0))) == typeid(Circle)) + shapeId = 3; + emit shapeSelected(shapeId); + }else{ + emit shapeSelected(0); + } + } + } +} + +void Viewer::deselect(){ + std::cout << "Deselecting cell " << activeCell << endl; + if(activeCell != nullptr && activeCell->getChildren()->size()){ + std::cout << "Cell has children..." << endl; + Shape* shape = dynamic_cast (activeCell->childAt(0)); + QColor newColor = shape->getColor(); + std::cout << newColor.Rgb << endl; + newColor.setAlpha(180); + shape->setColor(newColor); + }else{ + std::cout << "Cell has no children, moving on" << endl; + } + this->update(); +} + +void Viewer::mouseReleaseEvent(QMouseEvent* e) { + cout << "Viewer::mouseReleaseEvent(QMouseEvent* e)" << endl; + //QGLViewer::mouseReleaseEvent(e); +} + +void Viewer::init() +{ + // We want to restrict ourselves to a 2D viewer. + camera()->setType(qglviewer::Camera::ORTHOGRAPHIC); + /*setMouseBinding(Qt::NoModifier, Qt::LeftButton, CAMERA, SCREEN_ROTATE); + setMouseBinding(Qt::AltModifier, Qt::LeftButton, CAMERA, NO_MOUSE_ACTION);*/ + setMouseBinding(Qt::NoModifier, Qt::MouseButton(Qt::LeftButton + Qt::MidButton), CAMERA, NO_MOUSE_ACTION); + setMouseBinding(Qt::ControlModifier, Qt::MouseButton(Qt::LeftButton + Qt::MidButton), CAMERA, NO_MOUSE_ACTION); + setMouseBinding(Qt::ShiftModifier, Qt::MouseButton(Qt::LeftButton + Qt::MidButton), CAMERA, NO_MOUSE_ACTION); + + // Our scene will be from -5 to 5 in X and Y (the grid will be 10x10). + setSceneRadius(5); + showEntireScene(); + + // Init OpenGL objects + connect(context(), &QOpenGLContext::aboutToBeDestroyed, this, &Viewer::cleanup); + initializeOpenGLFunctions(); + + // Init shaders & geometry + initShaders(); + initGeometries(); + initGrid(); +} + +void Viewer::initShaders() +{ + // Load vertex and fragment shaders + m_program = new QOpenGLShaderProgram; + if (!m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, "src/shaders/basicShader.vert")) { + cerr << "Unable to load Shader" << endl + << "Log file:" << endl; + qDebug() << m_program->log(); + } + if (!m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, "src/shaders/basicShader.frag")) { + cerr << "Unable to load Shader" << endl + << "Log file:" << endl; + qDebug() << m_program->log(); + } + m_program->link(); + m_program->bind(); // Note: This is equivalent to glUseProgram(programId()); + + // Specify shader input paramters + // The strings "vPosition", "mvMatrix", etc. have to match an attribute name in the vertex shader. + if ((m_vPositionLocation = m_program->attributeLocation("vPosition")) < 0) + qDebug() << "Unable to find shader location for " << "vPosition"; + + if ((m_colorLocation = m_program->uniformLocation("color")) < 0) + qDebug() << "Unable to find shader location for " << "color"; + + if ((m_mvMatrixLocation = m_program->uniformLocation("mvMatrix")) < 0) + qDebug() << "Unable to find shader location for " << "mvMatrix"; + + if ((m_projMatrixLocation = m_program->uniformLocation("projMatrix")) < 0) + qDebug() << "Unable to find shader location for " << "projMatrix"; +} + +// Creates the basic shapes in memory. We only have 3, so we just prep them all in advance. +void Viewer::initGeometries() +{ + + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable( GL_BLEND ); glClearColor(0.0,0.0,0.0,0.0); + + // Create our VertexArrays Objects and VertexBuffer Objects + glGenVertexArrays(NumVAOs, m_VAOs); + glGenBuffers(NumBuffers, m_Buffers); + + // Create our pentagone object, store its vertices on the graphic card, and + // bind the data to the vPosition attribute of the shader + GLfloat verticesSquare[numVerticesSquare][3] = { + { -0.5, 0.5, 0 }, + { -0.5, -0.5, 0 }, + { 0.5, -0.5, 0 }, + { 0.5, 0.5, 0 }, + { -0.5, 0.5, 0 } + }; + + glBindVertexArray(m_VAOs[VAO_Square]); + glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[VBO_Square]); + glBufferData(GL_ARRAY_BUFFER, sizeof(verticesSquare), verticesSquare, GL_STATIC_DRAW); + + glVertexAttribPointer(m_vPositionLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0)); + glEnableVertexAttribArray(m_vPositionLocation); + + // Create our triangle object, store its vertices on the graphic card, and + // bind the data to the vPosition attribute of the shader + GLfloat verticesTriangle[numVerticesTriangle][3] = { + { -0.5, -0.5, 0.0 }, + { 0.5, -0.5, 0.0 }, + { 0.0, 0.5, 0.0 } + }; + + glBindVertexArray(m_VAOs[VAO_Triangle]); + glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[VBO_Triangle]); + glBufferData(GL_ARRAY_BUFFER, sizeof(verticesTriangle), verticesTriangle, GL_STATIC_DRAW); + + glVertexAttribPointer(m_vPositionLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0)); + glEnableVertexAttribArray(m_vPositionLocation); + + // Create our Circle Object, and store its vertices with its bindings + + GLfloat verticesCircle[numVerticesCircle][3]; + + const float PI = 3.1415926f; + double increment = 2.0f * PI / (numVerticesCircle - 2); + + verticesCircle[0][0] = 0; + verticesCircle[0][1] = 0; + verticesCircle[0][2] = 0; + + for(int i = 1; i < numVerticesCircle; i++) { + double angle = increment * (i); + + verticesCircle[i][0] = 0.5 * cos(angle); + verticesCircle[i][1] = 0.5 * sin(angle); + verticesCircle[i][2] = 0; + } + + glBindVertexArray(m_VAOs[VAO_Circle]); + glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[VBO_Circle]); + glBufferData(GL_ARRAY_BUFFER, sizeof(verticesCircle), verticesCircle, GL_STATIC_DRAW); + + glVertexAttribPointer(m_vPositionLocation, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0)); + glEnableVertexAttribArray(m_vPositionLocation); +} + +void Viewer::initGrid() +{ + // Prepare construction loop. Also contains necessary translations + int i,j, limit; + limit = GRID_SIZE; + + for(i = 0; i < limit ; i++) { + SceneGroup *row = new SceneGroup(); + + row->transform.translate(0, inc_offset * i); + + + for(j = 0; j < limit ; j++) { + SceneGroup *cell = new SceneGroup(); + + cell->transform.translate(inc_offset * j, 0); + + row->addChild(cell); + } + + root.addChild(row); + } +} + +void Viewer::visit(Square &s) +{ + QMatrix4x4 modelViewMatrix = modelStack.top() * QMatrix4x4(s.transform); + + glBindVertexArray(m_VAOs[VAO_Square]); + m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix); + m_program->setUniformValue(m_colorLocation, s.color); + + glDrawArrays(GL_TRIANGLE_FAN, 0, numVerticesSquare); +} +void Viewer::visit(Circle &s) +{ + QMatrix4x4 modelViewMatrix = modelStack.top() * QMatrix4x4(s.transform); + + glBindVertexArray(m_VAOs[VAO_Circle]); + m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix); + m_program->setUniformValue(m_colorLocation, s.color); + + glDrawArrays(GL_TRIANGLE_FAN, 0, numVerticesCircle); +} +void Viewer::visit(Triangle &s) +{ + QMatrix4x4 modelViewMatrix = modelStack.top() * QMatrix4x4(s.transform); + + glBindVertexArray(m_VAOs[VAO_Triangle]); + m_program->setUniformValue(m_mvMatrixLocation, modelViewMatrix); + m_program->setUniformValue(m_colorLocation, s.color); + + glDrawArrays(GL_TRIANGLES, 0, numVerticesTriangle); +} + +void Viewer::visit(SceneGroup &s) +{ + // Build compound transformation matrix + QMatrix4x4 currentMatrix = modelStack.top() * QMatrix4x4(s.transform); + modelStack.push(currentMatrix); + + while(s.hasNext()) { + // Get next leaf + GlNode* current = s.getChild(); + + // Draw/Traverse child + current->accept(*this); + } + + // Return model matrix to previous state + modelStack.pop(); +} + +void Viewer::changeColor(QColor c){ + if(activeCell->getChildren()->size()){ + Shape* shape = dynamic_cast (activeCell->childAt(0)); + shape->setColor(c); + } + activeColor = new QColor(c); + this->update(); +} + +void Viewer::changeShape(int s){ + std::cout << "Chaging active shape from " << activeShape << " to " << s << endl; + + if(activeCell != nullptr && activeCell->getChildren()->size()){ + std::cout << "Chaging cell-bound shape... " << endl; + Shape* shape = dynamic_cast (activeCell->childAt(0)); + QColor c = shape->getColor(); + + // WARNING: CODE DEGEULASSE + + Shape* newShape = nullptr; + if(s == 1){ + newShape = new Triangle(); + }else if(s == 2){ + newShape = new Square(); + }else if(s == 3){ + newShape = new Circle(); + } + + if(newShape != nullptr){ + newShape->setColor(c); + activeCell->getChildren()->at(0) = newShape; + }else{ + activeCell->getChildren()->clear(); + } + + // WARNING: END OF CODE DEGEULASSE + + this->update(); + }else{ + std::cout << "Cell has no children, ignoring change." << endl; + } + + activeShape = s; +}; diff --git a/src/viewer/simpleViewer.h b/src/viewer/simpleViewer.h new file mode 100644 index 0000000..9f5c882 --- /dev/null +++ b/src/viewer/simpleViewer.h @@ -0,0 +1,95 @@ +/**************************************************************************** + + Copyright (C) 2002-2008 Gilles Debunne. All rights reserved. + + This file is part of the QGLViewer library version 2.3.6. + + http://www.libqglviewer.com - contact@libqglviewer.com + + This file may be used under the terms of the GNU General Public License + versions 2.0 or 3.0 as published by the Free Software Foundation and + appearing in the LICENSE file included in the packaging of this file. + In addition, as a special exception, Gilles Debunne gives you certain + additional rights, described in the file GPL_EXCEPTION in this package. + + libQGLViewer uses dual licensing. Commercial/proprietary software must + purchase a libQGLViewer Commercial License. + + This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE + WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + +*****************************************************************************/ + +#ifndef SIMPLEVIEWER_H +#define SIMPLEVIEWER_H + +#include +#include +#include +#include +#include +#include "../interfaces/visitor.h" +#include "../glnodes/glnode.h" +#include "../glnodes/scenegroup.h" +#include "../glnodes/shapes.h" +#include + +QT_FORWARD_DECLARE_CLASS(QOpenGLShaderProgram) + +class Viewer : public QGLViewer, protected QOpenGLFunctions_4_0_Core, public Visitor +{ +Q_OBJECT +public: + Viewer(); + ~Viewer(); + + virtual void visit(SceneGroup &s); + virtual void visit(Square &s); + virtual void visit(Circle &s); + virtual void visit(Triangle &s); + +public slots: + void cleanup(); + void changeColor(QColor); + void changeShape(int); + +signals: + int shapeSelected(int); + +protected : + virtual void draw(); + virtual void init(); + + virtual void mouseMoveEvent(QMouseEvent* e); + virtual void mouseReleaseEvent(QMouseEvent* e); + virtual void mousePressEvent(QMouseEvent *e); + + SceneGroup root; + std::stack modelStack; + +private: + void initShaders(); + void initGeometries(); + void initGrid(); + void deselect(); + + QOpenGLShaderProgram *m_program; + int m_vPositionLocation; + int m_colorLocation; + int m_projMatrixLocation; + int m_mvMatrixLocation; + + SceneGroup* activeCell; + QColor* activeColor; + int activeShape; + + enum VAO_IDs { VAO_Square, VAO_Triangle, VAO_Circle, NumVAOs }; + enum Buffer_IDs { VBO_Square, VBO_Triangle, VBO_Circle, NumBuffers }; + + GLuint m_VAOs[NumVAOs]; + GLuint m_Buffers[NumBuffers]; + + Shape* generateShapeFromIndex(int); +}; + +#endif // SIMPLEVIEWER_H diff --git a/src/window/mainwindow.cpp b/src/window/mainwindow.cpp new file mode 100644 index 0000000..47302fb --- /dev/null +++ b/src/window/mainwindow.cpp @@ -0,0 +1,35 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include +#include + +MainWindow::MainWindow(QWidget *parent) : + QMainWindow(parent), + ui(new Ui::MainWindow), + colordialog(new QColorDialog) +{ + ui->setupUi(this); + + connect(ui->pushButton, SIGNAL(clicked(bool)), this, SLOT(onColorPickerActivate())); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::addViewer(Viewer* viewer) +{ + QLayout *layout = new QHBoxLayout; + layout->addWidget(viewer); + ui->frame->setLayout(layout); + + connect(colordialog, SIGNAL(colorSelected(QColor)), viewer, SLOT(changeColor(QColor))); + connect(ui->comboBox, SIGNAL(currentIndexChanged(int)), viewer, SLOT(changeShape(int))); + connect(viewer, SIGNAL(shapeSelected(int)), ui->comboBox, SLOT(setCurrentIndex(int))); + +} + +void MainWindow::onColorPickerActivate(){ + colordialog->open(); +} diff --git a/src/window/mainwindow.h b/src/window/mainwindow.h new file mode 100644 index 0000000..8ec7803 --- /dev/null +++ b/src/window/mainwindow.h @@ -0,0 +1,30 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include "src/viewer/simpleViewer.h" + +namespace Ui { +class MainWindow; +} + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + + void addViewer(Viewer* viewer); + +public slots: + void onColorPickerActivate(); + +private: + Ui::MainWindow *ui; + QColorDialog *colordialog; +}; + +#endif // MAINWINDOW_H