Visual Computing Library
Loading...
Searching...
No Matches
trackball.h
1/*****************************************************************************
2 * VCLib *
3 * Visual Computing Library *
4 * *
5 * Copyright(C) 2021-2025 *
6 * Visual Computing Lab *
7 * ISTI - Italian National Research Council *
8 * *
9 * All rights reserved. *
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the Mozilla Public License Version 2.0 as published *
13 * by the Mozilla Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 * This program is distributed in the hope that it will be useful, *
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
19 * Mozilla Public License Version 2.0 *
20 * (https://www.mozilla.org/en-US/MPL/2.0/) for more details. *
21 ****************************************************************************/
22
23#ifndef VCL_RENDER_VIEWER_TRACKBALL_H
24#define VCL_RENDER_VIEWER_TRACKBALL_H
25
26#include "camera.h"
27#include "lights/directional_light.h"
28
29#include <vclib/space/core/quaternion.h>
30
31#include <variant>
32
33namespace vcl {
34
50template<typename Scalar>
52{
53public:
54 enum MotionType {
55 ARC,
56 PAN,
57 ZMOVE,
58 ROLL,
59 SCALE,
60 FOV,
61 FOCUS,
62 DIR_LIGHT_ARC,
63 MOTION_NUMBER
64 };
65
67 {
68 Point3<Scalar> axis;
69 Scalar scalar; // could be an angle or a distance
70
71 TransformArgs(const Point3<Scalar>& a, Scalar s) : axis(a), scalar(s) {}
72 };
73
74 using AtomicMotionArg =
75 std::variant<TransformArgs, Scalar, Point3<Scalar>, std::monostate>;
76
77private:
78 // arcball radius ratio wrt minimum screen dimension
79 // set as the inverse of the golden ratio
80 static constexpr Scalar ARC_BALL_RADIUS_RATIO = 1.0 / 1.61803398875;
81
82 static constexpr Scalar FOCUS_SCALE_FACTOR = 1.15;
83
84 static constexpr Scalar DEFAULT_FOV_DEG = 54.0;
85
86 Camera<Scalar> mCamera;
87
88 // Similarity holding the manipulator transformation.
89 // The convention is that the transformation is expressed as
90 // TRANSLATION * ROTATION * SCALE
91 // to avoid error accumulation we should split the transformation
92 // into the three components (Point3, Quaternion, Scalar)
93 Affine3<Scalar> mTransform = Affine3<Scalar>::Identity();
94
95 Quaternion<Scalar> mDirectionalLightTransform;
96
97 Point2<Scalar> mScreenSize = {-1, -1};
98
99 // trackball radius in camera space
100 // this value affects the interaction and the visualization of the trackball
101 Scalar mRadius = ARC_BALL_RADIUS_RATIO;
102
103 // trackball interaction state
104 bool mDragging = false;
105 MotionType mCurrDragMotion = MOTION_NUMBER;
106
107 // initial arcball hit point
108 Point3<Scalar> mInitialPoint;
109 // initial transformation
110 Affine3<Scalar> mInitialTransform = Affine3<Scalar>::Identity();
111 // initial light rotation
112 Quaternion<Scalar> mInitialDirRotation = Quaternion<Scalar>::Identity();
113
114 Point2<Scalar> mCurrMousePosition;
115 Point2<Scalar> mPrevMousePosition;
116
117public:
118 TrackBall() { mCamera.setFieldOfViewAdaptingEyeDistance(DEFAULT_FOV_DEG); }
119
120 void reset()
121 {
122 auto screenSize = mScreenSize;
123 auto currMousePosition = mCurrMousePosition;
124 auto prevMousePosition = mPrevMousePosition;
125 *this = TrackBall();
126 setScreenSize(screenSize);
127 mCurrMousePosition = currMousePosition;
128 mPrevMousePosition = prevMousePosition;
129 }
130
136 void reset(const Point3<Scalar>& center, Scalar scale)
137 {
138 reset();
139 mTransform.scale(scale);
140 mTransform.translate(-center);
141 }
142
143 void resetDirectionalLight()
144 {
145 mDirectionalLightTransform = Quaternion<Scalar>();
146 }
147
148 Point3<Scalar> center() const
149 {
150 // obtain the point in world space that maps to zero when transformed
151 return mTransform.inverse().translation();
152 }
153
154 void setCenter(const Point3<Scalar>& center)
155 {
156 // transform the center (world space) using the current transformation
157 // then translate it to the origin
158 mTransform.pretranslate(-(mTransform * center));
159 }
160
161 Scalar scale() const
162 {
163 // return the average of the norm of linear part vectors
164 return mTransform.linear().colwise().norm().mean();
165 }
166
167 void setScale(Scalar scale)
168 {
169 // scale the linear part of the transformation
170 // so the manipulator will be scaled around the origin
171 mTransform.prescale(scale);
172 // TODO: scale also near/far?
173 }
174
175 void changeScale(Scalar factor) { mTransform.prescale(factor); }
176
177 void setRotation(const Quaternion<Scalar>& rotation)
178 {
179 Affine3<Scalar> tx = Affine3<Scalar>::Identity();
180 tx.rotate(rotation).scale(scale());
181 mTransform.linear() = tx.linear();
182 }
183
184 void setRotation(const Point3<Scalar>& axis, Scalar angle)
185 {
186 setRotation(Quaternion<Scalar>(angle, axis));
187 }
188
192 Scalar fovDeg() const { return mCamera.fieldOfView(); }
193
198 void setFovDeg(Scalar fov)
199 {
200 mCamera.setFieldOfViewAdaptingEyeDistance(fov);
201 }
202
203 Camera<Scalar>::ProjectionMode::Enum projectionMode() const
204 {
205 return mCamera.projectionMode();
206 }
207
208 void setProjectionMode(Camera<Scalar>::ProjectionMode::Enum mode)
209 {
210 mCamera.projectionMode() = mode;
211 mCamera.setFieldOfViewAdaptingEyeDistance(mCamera.fieldOfView());
212 }
213
214 void setScreenSize(Scalar width, Scalar height)
215 {
216 if (width > 1 || height > 1) {
217 mScreenSize.x() = width;
218 mScreenSize.y() = height;
219 mCamera.aspectRatio() = width / height;
220 mRadius = ARC_BALL_RADIUS_RATIO * mCamera.verticalHeight() / 2.0;
221 if (width < height)
222 mRadius *= mCamera.aspectRatio();
223 }
224 }
225
226 void setScreenSize(const Point2<Scalar>& sz)
227 {
228 setScreenSize(sz.x(), sz.y());
229 }
230
231 DirectionalLight<Scalar> light() const
232 {
233 // TODO: return a light direction stored in this class,
234 // in order to store also the light color
235 return DirectionalLight<Scalar>(
236 mDirectionalLightTransform * Point3<Scalar>(0, 0, 1));
237 }
238
239 const vcl::Camera<Scalar>& camera() const { return mCamera; }
240
241 Matrix44<Scalar> viewMatrix() const
242 {
243 return mCamera.viewMatrix() * mTransform.matrix();
244 }
245
246 Matrix44<Scalar> projectionMatrix() const { return mCamera.projMatrix(); }
247
248 Matrix44<Scalar> gizmoMatrix() const
249 {
250 Affine3<Scalar> rot_radius = Affine3<Scalar>::Identity();
251 rot_radius.rotate(mTransform.rotation()).scale(mRadius);
252 return mCamera.viewMatrix() * rot_radius.matrix();
253 }
254
255 Matrix44<Scalar> lightGizmoMatrix() const
256 {
257 Affine3<Scalar> rot_radius = Affine3<Scalar>::Identity();
258 rot_radius.rotate(mDirectionalLightTransform).scale(mRadius);
259 return mCamera.viewMatrix() * rot_radius.matrix();
260 }
261
262 bool isDragging() const { return mDragging; }
263
264 MotionType currentMotion() const { return mCurrDragMotion; }
265
266 // Atomic motions
267
291 MotionType motion,
292 AtomicMotionArg step = std::monostate())
293 {
294 if (std::holds_alternative<Scalar>(step)) {
295 Scalar inc = std::get<Scalar>(step);
296
297 switch (motion) {
298 case ROLL: roll(inc); break;
299 case SCALE: performScale(inc); break;
300 case FOV: performFov(inc); break;
301 case ZMOVE: performZmove(inc); break;
302 default: break;
303 }
304 }
305 else if (std::holds_alternative<TransformArgs>(step)) {
306 const TransformArgs& args = std::get<TransformArgs>(step);
307 switch (motion) {
308 case ARC: rotate(args.axis, args.scalar); break;
309 case DIR_LIGHT_ARC: rotateDirLight(args.axis, args.scalar); break;
310 default: break;
311 }
312 }
313 else if (std::holds_alternative<Point3<Scalar>>(step)) {
314 Point3<Scalar> val = std::get<Point3<Scalar>>(step);
315 switch (motion) {
316 case PAN: translate(val); break;
317 case FOCUS: setCenter(val); changeScale(FOCUS_SCALE_FACTOR);
318 default: break;
319 }
320 }
321 }
322
323 void applyScale(Scalar value) { applyAtomicMotion(SCALE, value); }
324
325 void applyRoll(Scalar angleRad) { applyAtomicMotion(ROLL, angleRad); }
326
327 void applyPan(const Point3<Scalar>& translation)
328 {
329 applyAtomicMotion(PAN, translation);
330 }
331
332 void applyArc(const Point3<Scalar>& axis, Scalar angle)
333 {
334 applyAtomicMotion(ARC, TransformArgs(axis, angle));
335 }
336
337 // Drag motions
338
339 void setMousePosition(Scalar x, Scalar y)
340 {
341 mPrevMousePosition = mCurrMousePosition;
342 mCurrMousePosition.x() = x;
343 mCurrMousePosition.y() = mScreenSize.y() - y;
344 }
345
346 void setMousePosition(const Point2<Scalar>& point)
347 {
348 setMousePosition(point.x(), point.y());
349 }
350
357 void beginDragMotion(MotionType motion)
358 {
359 assert(motion != MOTION_NUMBER && "Invalid motion type");
360
361 // no need to restart?
362 if (mCurrDragMotion == motion)
363 return;
364
365 // end previous motion
366 if (mCurrDragMotion != motion)
367 endDragMotion(mCurrDragMotion);
368
370 mInitialPoint = pointOnArcball(mCurrMousePosition);
371 mInitialTransform = mTransform;
372 mInitialDirRotation = mDirectionalLightTransform;
373 mDragging = true;
374 }
375
382 void endDragMotion(MotionType motion)
383 {
385 mDragging = false;
386 }
387
393 void update() // TODO: rename this function (it just updates the motion)
394 {
395 assert(
396 mDragging != (mCurrDragMotion == MOTION_NUMBER) &&
397 "Invalid state: dragging and no motion");
398 if (mDragging && mCurrMousePosition != mPrevMousePosition)
399 drag(mCurrDragMotion);
400 }
401
402private:
405 void setDragMotionValue(MotionType motion, bool value)
406 {
407 mCurrDragMotion = value ? motion : MOTION_NUMBER;
408 }
409
410 void drag(MotionType motion)
411 {
412 switch (motion) {
413 case ARC: dragArc(); break;
414 case PAN: dragPan(); break;
415 case ZMOVE: dragZmove(); break;
416 case ROLL: dragRoll(); break;
417 case SCALE: dragScale(); break;
418 case DIR_LIGHT_ARC: dragDirLightArc(); break;
419 default: break;
420 }
421 }
422
423 Scalar trackballToPixelRatio() const
424 {
425 return mCamera.verticalHeight() / mScreenSize.y();
426 }
427
428 Point3<Scalar> pointOnTrackballPlane(Point2<Scalar> screenCoord) const
429 {
430 // convert to camera space
431 // range [-1, 1] for Y and [-aspectRatio, aspectRatio] for X
432 screenCoord =
433 (screenCoord - mScreenSize / 2.0) * trackballToPixelRatio();
434 return Point3<Scalar>(screenCoord.x(), screenCoord.y(), 0.0);
435 }
436
437 Point3<Scalar> pointOnArcball(Point2<Scalar> screenCoord) const
438 {
439 // convert to range [-1, 1] for Y and [-aspectRatio, aspectRatio] for X
440 screenCoord =
441 (screenCoord - mScreenSize / 2.0) * trackballToPixelRatio();
442
443 // Solve the equations in 2D on the plane passing through the eye,
444 // the trackball center, and the intersection line.
445 // The X coordinate corresponds to the Z axis, while the Y coordinate
446 // is on the XY trackball plane.
447
448 const double h = screenCoord.norm();
449 Point2<Scalar> hitPoint;
450
451 if (mCamera.projectionMode() == Camera<Scalar>::ProjectionMode::ORTHO) {
452 // in ortho projection we can project the Y coordinate directly
453 if (h < (M_SQRT1_2 * mRadius)) {
454 // hit sphere
455 // Y^2 + X^2 = r^2
456 // X = sqrt(r^2 - Y^2)
457 hitPoint =
458 Point2<Scalar>(std::sqrt(mRadius * mRadius - h * h), h);
459 }
460 else {
461 // hit hyperbola
462 // X*Y = (r^2 / 2)
463 // X = r^2 / (2 * Y)
464 hitPoint = Point2<Scalar>(mRadius * mRadius / (2 * h), h);
465 }
466 }
467 else {
468 assert(
469 mCamera.projectionMode() ==
470 Camera<Scalar>::ProjectionMode::PERSPECTIVE &&
471 "invalid camera projection value is supported");
472 // PERSPECTIVE PROJECTION
473 //
474 // | ^
475 // | h /
476 //--target------------> Y axis
477 // | /
478 // | /
479 // | /
480 // | /
481 // | / intersecting line
482 // d | /
483 // | /
484 // | /
485 // | /
486 // |/
487 // eye
488 // /|
489 // / |
490 // / |
491 // \./ X axis
492
493 // Equation constants:
494 // d = distance from the eye to the target
495 // h = distance(target, line plane inter
496 const double d = (mCamera.eye() - mCamera.center()).norm();
497
498 const double m = -h / d;
499
500 bool sphereHit = false;
501 Point2<Scalar> spherePoint;
502 {
503 // hit point on the sphere
504 //
505 // line equation:
506 // Y = -(h/d) * X + h
507 //
508 // circle equation:
509 // X^2 + Y^2 = r^2
510 //
511 // substitute Y in the circle equation:
512 // X^2 + (-h/d * X + h)^2 = r^2
513 // X^2 + h^2/d^2 * X^2 - 2 * h^2/d * X + h^2 = r^2
514 // (1 + h^2/d^2) * X^2 - 2 * h^2/d * X + h^2 - r^2 = 0
515
516 // a = 1 + h^2/d^2
517 // b = -2 * h^2/d
518 // c = h^2 - r^2
519 Scalar a = 1 + m * m;
520 Scalar b = -2 * h * h / d;
521 Scalar c = h * h - mRadius * mRadius;
522
523 // X = (-b +- sqrt(b^2 - 4ac)) / 2a
524 // Y = -(h/d) * X + h
525 // we take the positive solution (hit point closest to the eye)
526
527 Scalar delta = b * b - 4 * a * c;
528 sphereHit = delta >= 0;
529 if (sphereHit) {
530 spherePoint.x() = (-b + std::sqrt(delta)) / (2 * a);
531 spherePoint.y() = m * spherePoint.x() + h;
532 }
533 }
534
535 bool hyperHit = false;
536 Point2<Scalar> hyperPoint;
537 {
538 // hit point on the hyperbola
539 //
540 // line equation:
541 // Y = -(h/d) * X + h
542 //
543 // hyperbola equation:
544 // Y = 1/X * (r^2 / 2)
545
546 // substitute Y in the hyperbola equation:
547 // -(h/d) * X + h = 1/X * (r^2 / 2)
548 // -(h/d) * X^2 + h * X = r^2 / 2
549 // -h * X^2 + d * h * X = d * r^2 / 2
550 // -2 * h * X^2 + 2 * d * h * X = d * r^2
551 // -2 * h * X^2 + 2 * d * h * X - d * r^2 = 0
552
553 // a = -2 * h
554 // b = 2 * d * h
555 // c = - d * r^2
556
557 Scalar a = -2 * h;
558 Scalar b = 2 * d * h;
559 Scalar c = -d * mRadius * mRadius;
560
561 // X = (-b +- sqrt(b^2 - 4ac)) / 2a
562 // Y = -(h/d) * X + h
563 // we take the solution with smallest x
564 // (hit point farthest from the eye)
565
566 Scalar delta = b * b - 4 * a * c;
567 hyperHit = delta >= 0;
568 if (hyperHit) {
569 hyperPoint.x() = (-b + std::sqrt(delta)) / (2 * a);
570 hyperPoint.y() = m * hyperPoint.x() + h;
571 }
572 }
573
574 // discriminate between sphere and hyperbola hit points
575 const int hitCase = sphereHit + 2 * hyperHit;
576 switch (hitCase) {
577 case 0: // no hit
578 {
579 // get closeset point of the line to the origin
580 // rotate the line vector by 90 degrees counter-clockwise
581 Point2<Scalar> lineVector = Point2<Scalar>(d, -h).normalized();
582 Point2<Scalar> lineNormal =
583 Point2<Scalar>(-lineVector.y(), lineVector.x());
584 // project the eye on the vector
585 hitPoint = lineNormal * lineNormal.dot(Point2<Scalar>(d, 0));
586 } break;
587 case 1: // sphere hit
588 hitPoint = spherePoint;
589 break;
590 case 2: // hyperbola hit
591 hitPoint = hyperPoint;
592 break;
593 case 3: // both hit
594 {
595 // check angle of sphere point
596 Scalar angle = std::atan2(spherePoint.y(), spherePoint.x());
597 // if angle is more than 45 degrees, take the hyperbola point
598 if (angle > M_PI / 4) {
599 hitPoint = hyperPoint;
600 }
601 else {
602 hitPoint = spherePoint;
603 }
604 } break;
605 default: assert(false && "Invalid hit case");
606 }
607 }
608
609 // convert hit point to 3D
610 // rescale in trackball space
611 // FIXME: Avoid cancellation issue with different solution
612 assert(hitPoint.x() == hitPoint.x());
613 double factor =
614 (h > 0.0 && hitPoint.y() > 0.0) ? hitPoint.y() / h : 0.0;
615 return Point3<Scalar>(
616 screenCoord.x() * factor, screenCoord.y() * factor, hitPoint.x());
617 }
618
620 // (general purpose and used for atomic operations)
621
622 void rotate(const Quaternion<Scalar>& q) { mTransform.prerotate(q); }
623
624 void rotate(Point3<Scalar> axis, Scalar angleRad)
625 {
626 mTransform.prerotate(Quaternion<Scalar>(angleRad, axis));
627 }
628
634 void translate(Point3<Scalar> t) { mTransform.pretranslate(t); }
635
636 void rotateDirLight(const Quaternion<Scalar>& rotation)
637 {
638 mDirectionalLightTransform = rotation * mDirectionalLightTransform;
639 }
640
641 void rotateDirLight(Point3<Scalar> axis, Scalar angle)
642 {
643 rotateDirLight(Quaternion<Scalar>(angle, axis));
644 }
645
650 void dragArc()
651 {
652 const Point3<Scalar> point = pointOnArcball(mCurrMousePosition);
653 Eigen::AngleAxis<Scalar> ax(
654 Eigen::Quaternion<Scalar>::FromTwoVectors(mInitialPoint, point));
655 // use angle proportional to the arc length
656 const Scalar phi = (point - mInitialPoint).norm() / mRadius;
657 ax.angle() = phi;
658
659 // rotate from freezed transformation
660 // mTransform = mInitialTransform;
661 // mTransform.prerotate(ax);
662
663 // modify rotation only
664 mTransform.prerotate(
665 ax * mInitialTransform.rotation() *
666 mTransform.rotation().inverse());
667 }
668
671 void roll(Scalar delta) { rotate(Point3<Scalar>(0, 0, 1), delta); }
672
673 void dragRoll()
674 {
675 static constexpr Scalar ROLL_DIST_TO_CENTER_THRESHOLD = 0.025;
676
677 const Point3<Scalar> prev = pointOnTrackballPlane(mPrevMousePosition);
678 const Point3<Scalar> curr = pointOnTrackballPlane(mCurrMousePosition);
679 if (prev.norm() < ROLL_DIST_TO_CENTER_THRESHOLD ||
681 return;
682
683 Scalar angle =
684 std::atan2(curr.y(), curr.x()) - std::atan2(prev.y(), prev.x());
685
686 roll(angle);
687 }
688
696 {
697 const Point2<Scalar> pan = pixelDelta * trackballToPixelRatio();
698 translate(Point3<Scalar>(pan.x(), pan.y(), 0.0));
699 }
700
701 // drag
702 void dragPan()
703 {
704 Point2<Scalar> pixelDelta = mCurrMousePosition - mPrevMousePosition;
706 }
707
713 void performZmove(const Scalar& pixelDelta)
714 {
715 const Scalar translation = pixelDelta * trackballToPixelRatio();
717 }
718
719 // drag
720 void dragZmove()
721 {
722 auto pixelDelta = mCurrMousePosition.y() - mPrevMousePosition.y();
724 }
725
728 // scrolling and scaling are setup with "magic" numbers
730 {
731 pixelDelta /= 60;
732 const auto factor = std::pow(1.2f, -pixelDelta);
733 changeScale(factor);
734 }
735
736 void dragScale()
737 {
738 Scalar pixelDelta = mCurrMousePosition.y() - mPrevMousePosition.y();
740 }
741
744 // drag
746 {
747 const Point3<Scalar> point = pointOnArcball(mCurrMousePosition);
748 Eigen::AngleAxis<Scalar> ax(
749 Eigen::Quaternion<Scalar>::FromTwoVectors(mInitialPoint, point));
750 // use angle proportional to the arc length
751 const Scalar phi = (point - mInitialPoint).norm() / mRadius;
752
753 mDirectionalLightTransform = mInitialDirRotation;
754 rotateDirLight(Point3<Scalar>(ax.axis()), phi);
755 }
756
760 {
761 static constexpr double MIN_FOV_DEG = 5.0;
762 static constexpr double MAX_FOV_DEG = 90.0;
763
764 pixelDelta /= 60.0;
765 double fov = fovDeg();
766
767 // ortho -> perspective
768 if (mCamera.projectionMode() == Camera<Scalar>::ProjectionMode::ORTHO) {
769 if (pixelDelta > 0) {
771 mCamera.projectionMode() =
773 }
774 }
775
776 // update fov
777 fov = std::clamp(fov + 1.2 * pixelDelta, MIN_FOV_DEG, MAX_FOV_DEG);
778
779 // perspective -> ortho
780 if (mCamera.projectionMode() ==
782 fov == MIN_FOV_DEG) {
783 mCamera.projectionMode() = Camera<Scalar>::ProjectionMode::ORTHO;
784 }
785
786 // commit fov
787 setFovDeg(fov);
788 }
789};
790
791} // namespace vcl
792
793#endif // VCL_RENDER_VIEWER_TRACKBALL_H
Definition camera.h:32
Quaternion class.
Definition quaternion.h:51
A class representing a line segment in n-dimensional space. The class is parameterized by a PointConc...
Definition segment.h:43
The TrackBall class implements a trackball camera.
Definition trackball.h:52
void setFovDeg(Scalar fov)
Set the vertical field of view adapting the eye distance.
Definition trackball.h:198
void roll(Scalar delta)
Definition trackball.h:671
void dragArc()
Definition trackball.h:650
void translate(Point3< Scalar > t)
Translate in the camera space.
Definition trackball.h:634
void applyAtomicMotion(MotionType motion, AtomicMotionArg step=std::monostate())
Applies an atomic motion to the trackball. Atomic motions are motions that are applied atomically to ...
Definition trackball.h:290
void endDragMotion(MotionType motion)
Ends a drag motion.
Definition trackball.h:382
void dragDirLightArc()
Definition trackball.h:745
Scalar fovDeg() const
Get the vertical field of view.
Definition trackball.h:192
void performPan(const Point2< Scalar > &pixelDelta)
perform a pan operation
Definition trackball.h:695
void reset(const Point3< Scalar > &center, Scalar scale)
Reset the manipulator to a given center and scale.
Definition trackball.h:136
void setDragMotionValue(MotionType motion, bool value)
Definition trackball.h:405
void performZmove(const Scalar &pixelDelta)
translate in the camera z direction
Definition trackball.h:713
void performScale(Scalar pixelDelta)
Definition trackball.h:729
void performFov(Scalar pixelDelta)
Definition trackball.h:759
void update()
Updates the state of the trackball during a drag motion.
Definition trackball.h:393
void rotate(const Quaternion< Scalar > &q)
Definition trackball.h:622
void beginDragMotion(MotionType motion)
Starts a drag motion.
Definition trackball.h:357
Definition trackball.h:67