/** * @file LLVirtualTrackball.cpp * @author Andrey Lihatskiy * @brief Implementation for LLVirtualTrackball * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2018, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ // A control for positioning the sun and the moon in the celestial sphere. #include "linden_common.h" #include "llvirtualtrackball.h" #include "llstring.h" #include "llrect.h" #include "lluictrlfactory.h" #include "llrender.h" // Globals static LLDefaultChildRegistry::Register register_virtual_trackball("sun_moon_trackball"); const LLVector3 VectorZero(1.0f, 0.0f, 0.0f); LLVirtualTrackball::Params::Params() : border("border"), image_moon_back("image_moon_back"), image_moon_front("image_moon_front"), image_sphere("image_sphere"), image_sun_back("image_sun_back"), image_sun_front("image_sun_front"), btn_rotate_top("button_rotate_top"), btn_rotate_bottom("button_rotate_bottom"), btn_rotate_left("button_rotate_left"), btn_rotate_right("button_rotate_right"), thumb_mode("thumb_mode"), lbl_N("labelN"), lbl_S("labelS"), lbl_W("labelW"), lbl_E("labelE"), increment_angle_mouse("increment_angle_mouse", 0.5f), increment_angle_btn("increment_angle_btn", 3.0f) { } LLVirtualTrackball::LLVirtualTrackball(const LLVirtualTrackball::Params& p) : LLUICtrl(p), mImgMoonBack(p.image_moon_back), mImgMoonFront(p.image_moon_front), mImgSunBack(p.image_sun_back), mImgSunFront(p.image_sun_front), mImgSphere(p.image_sphere), mThumbMode(p.thumb_mode() == "moon" ? ThumbMode::MOON : ThumbMode::SUN), mIncrementMouse(DEG_TO_RAD * p.increment_angle_mouse()), mIncrementBtn(DEG_TO_RAD * p.increment_angle_btn()) { LLRect border_rect = getLocalRect(); S32 centerX = border_rect.getCenterX(); S32 centerY = border_rect.getCenterY(); U32 btn_size = 32; // width & height U32 axis_offset_lt = 16; // offset from the axis for left/top sides U32 axis_offset_rb = btn_size - axis_offset_lt; // and for right/bottom LLViewBorder::Params border = p.border; border.rect(border_rect); mBorder = LLUICtrlFactory::create(border); addChild(mBorder); LLButton::Params btn_rt = p.btn_rotate_top; btn_rt.rect(LLRect(centerX - axis_offset_lt, border_rect.mTop, centerX + axis_offset_rb, border_rect.mTop - btn_size)); btn_rt.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateTopClick, this)); btn_rt.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateTopClick, this)); btn_rt.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateTopMouseEnter, this)); mBtnRotateTop = LLUICtrlFactory::create(btn_rt); addChild(mBtnRotateTop); LLTextBox::Params lbl_N = p.lbl_N; LLRect rect_N = btn_rt.rect; //rect_N.translate(btn_rt.rect().getWidth(), 0); lbl_N.rect = rect_N; lbl_N.initial_value(lbl_N.label()); mLabelN = LLUICtrlFactory::create(lbl_N); addChild(mLabelN); LLButton::Params btn_rr = p.btn_rotate_right; btn_rr.rect(LLRect(border_rect.mRight - btn_size, centerY + axis_offset_lt, border_rect.mRight, centerY - axis_offset_rb)); btn_rr.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateRightClick, this)); btn_rr.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateRightClick, this)); btn_rr.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateRightMouseEnter, this)); mBtnRotateRight = LLUICtrlFactory::create(btn_rr); addChild(mBtnRotateRight); LLTextBox::Params lbl_E = p.lbl_E; LLRect rect_E = btn_rr.rect; //rect_E.translate(0, -1 * btn_rr.rect().getHeight()); lbl_E.rect = rect_E; lbl_E.initial_value(lbl_E.label()); mLabelE = LLUICtrlFactory::create(lbl_E); addChild(mLabelE); LLButton::Params btn_rb = p.btn_rotate_bottom; btn_rb.rect(LLRect(centerX - axis_offset_lt, border_rect.mBottom + btn_size, centerX + axis_offset_rb, border_rect.mBottom)); btn_rb.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateBottomClick, this)); btn_rb.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateBottomClick, this)); btn_rb.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateBottomMouseEnter, this)); mBtnRotateBottom = LLUICtrlFactory::create(btn_rb); addChild(mBtnRotateBottom); LLTextBox::Params lbl_S = p.lbl_S; LLRect rect_S = btn_rb.rect; //rect_S.translate(btn_rb.rect().getWidth(), 0); lbl_S.rect = rect_S; lbl_S.initial_value(lbl_S.label()); mLabelS = LLUICtrlFactory::create(lbl_S); addChild(mLabelS); LLButton::Params btn_rl = p.btn_rotate_left; btn_rl.rect(LLRect(border_rect.mLeft, centerY + axis_offset_lt, border_rect.mLeft + btn_size, centerY - axis_offset_rb)); btn_rl.click_callback.function(boost::bind(&LLVirtualTrackball::onRotateLeftClick, this)); btn_rl.mouse_held_callback.function(boost::bind(&LLVirtualTrackball::onRotateLeftClick, this)); btn_rl.mouseenter_callback.function(boost::bind(&LLVirtualTrackball::onRotateLeftMouseEnter, this)); mBtnRotateLeft = LLUICtrlFactory::create(btn_rl); addChild(mBtnRotateLeft); LLTextBox::Params lbl_W = p.lbl_W; LLRect rect_W = btn_rl.rect; //rect_W.translate(0, -1* btn_rl.rect().getHeight()); lbl_W.rect = rect_W; lbl_W.initial_value(lbl_W.label()); mLabelW = LLUICtrlFactory::create(lbl_W); addChild(mLabelW); LLPanel::Params touch_area; touch_area.rect = LLRect(centerX - mImgSphere->getWidth() / 2, centerY + mImgSphere->getHeight() / 2, centerX + mImgSphere->getWidth() / 2, centerY - mImgSphere->getHeight() / 2); mTouchArea = LLUICtrlFactory::create(touch_area); addChild(mTouchArea); } LLVirtualTrackball::~LLVirtualTrackball() { } bool LLVirtualTrackball::postBuild() { return true; } void LLVirtualTrackball::drawThumb(S32 x, S32 y, ThumbMode mode, bool upperHemi) { LLUIImage* thumb; if (mode == ThumbMode::SUN) { if (upperHemi) { thumb = mImgSunFront; } else { thumb = mImgSunBack; } } else { if (upperHemi) { thumb = mImgMoonFront; } else { thumb = mImgMoonBack; } } thumb->draw(LLRect(x - thumb->getWidth() / 2, y + thumb->getHeight() / 2, x + thumb->getWidth() / 2, y - thumb->getHeight() / 2)); } bool LLVirtualTrackball::pointInTouchCircle(S32 x, S32 y) const { S32 centerX = mTouchArea->getRect().getCenterX(); S32 centerY = mTouchArea->getRect().getCenterY(); bool in_circle = pow(x - centerX, 2) + pow(y - centerY, 2) <= pow(mTouchArea->getRect().getWidth() / 2, 2); return in_circle; } void LLVirtualTrackball::draw() { LLVector3 draw_point = VectorZero * mValue; S32 halfwidth = mTouchArea->getRect().getWidth() / 2; S32 halfheight = mTouchArea->getRect().getHeight() / 2; draw_point.mV[VX] = (draw_point.mV[VX] + 1.0) * halfwidth + mTouchArea->getRect().mLeft; draw_point.mV[VY] = (draw_point.mV[VY] + 1.0) * halfheight + mTouchArea->getRect().mBottom; bool upper_hemisphere = (draw_point.mV[VZ] >= 0.f); mImgSphere->draw(mTouchArea->getRect(), upper_hemisphere ? UI_VERTEX_COLOR : UI_VERTEX_COLOR % 0.5f); drawThumb(draw_point.mV[VX], draw_point.mV[VY], mThumbMode, upper_hemisphere); if (LLView::sDebugRects) { gGL.color4fv(LLColor4::red.mV); gl_circle_2d(mTouchArea->getRect().getCenterX(), mTouchArea->getRect().getCenterY(), mImgSphere->getWidth() / 2, 60, false); gl_circle_2d(draw_point.mV[VX], draw_point.mV[VY], mImgSunFront->getWidth() / 2, 12, false); } // hide the direction labels when disabled bool enabled = isInEnabledChain(); mLabelN->setVisible(enabled); mLabelE->setVisible(enabled); mLabelS->setVisible(enabled); mLabelW->setVisible(enabled); LLView::draw(); } void LLVirtualTrackball::onRotateTopClick() { if (getEnabled()) { LLQuaternion delta; delta.setAngleAxis(mIncrementBtn, 1, 0, 0); mValue *= delta; setValueAndCommit(mValue); make_ui_sound("UISndClick"); } } void LLVirtualTrackball::onRotateBottomClick() { if (getEnabled()) { LLQuaternion delta; delta.setAngleAxis(mIncrementBtn, -1, 0, 0); mValue *= delta; setValueAndCommit(mValue); make_ui_sound("UISndClick"); } } void LLVirtualTrackball::onRotateLeftClick() { if (getEnabled()) { LLQuaternion delta; delta.setAngleAxis(mIncrementBtn, 0, 1, 0); mValue *= delta; setValueAndCommit(mValue); make_ui_sound("UISndClick"); } } void LLVirtualTrackball::onRotateRightClick() { if (getEnabled()) { LLQuaternion delta; delta.setAngleAxis(mIncrementBtn, 0, -1, 0); mValue *= delta; setValueAndCommit(mValue); make_ui_sound("UISndClick"); } } void LLVirtualTrackball::onRotateTopMouseEnter() { mBtnRotateTop->setHighlight(true); } void LLVirtualTrackball::onRotateBottomMouseEnter() { mBtnRotateBottom->setHighlight(true); } void LLVirtualTrackball::onRotateLeftMouseEnter() { mBtnRotateLeft->setHighlight(true); } void LLVirtualTrackball::onRotateRightMouseEnter() { mBtnRotateRight->setHighlight(true); } void LLVirtualTrackball::setValue(const LLSD& value) { if (value.isArray() && value.size() == 4) { mValue.setValue(value); } } void LLVirtualTrackball::setRotation(const LLQuaternion &value) { mValue = value; } void LLVirtualTrackball::setValue(F32 x, F32 y, F32 z, F32 w) { mValue.set(x, y, z, w); } void LLVirtualTrackball::setValueAndCommit(const LLQuaternion &value) { mValue = value; onCommit(); } LLSD LLVirtualTrackball::getValue() const { return mValue.getValue(); } LLQuaternion LLVirtualTrackball::getRotation() const { return mValue; } // static void LLVirtualTrackball::getAzimuthAndElevation(const LLQuaternion &quat, F32 &azimuth, F32 &elevation) { // LLQuaternion has own function to get azimuth, but it doesn't appear to return correct values (meant for 2d?) LLVector3 point = VectorZero * quat; if (!is_approx_zero(point.mV[VX]) || !is_approx_zero(point.mV[VY])) { azimuth = atan2f(point.mV[VX], point.mV[VY]); } else { azimuth = 0; } azimuth -= F_PI_BY_TWO; if (azimuth < 0) { azimuth += F_PI * 2; } // while vector is '1', F32 is not sufficiently precise and we can get // values like 1.0000012 which will result in -90deg angle instead of 90deg F32 z = llclamp(point.mV[VZ], -1.f, 1.f); elevation = asin(z); // because VectorZero's length is '1' } // static void LLVirtualTrackball::getAzimuthAndElevationDeg(const LLQuaternion &quat, F32 &azimuth, F32 &elevation) { getAzimuthAndElevation(quat, azimuth, elevation); azimuth *= RAD_TO_DEG; elevation *= RAD_TO_DEG; } bool LLVirtualTrackball::handleHover(S32 x, S32 y, MASK mask) { if (hasMouseCapture()) { if (mDragMode == DRAG_SCROLL) { // trackball (move to roll) mode LLQuaternion delta; F32 rotX = x - mPrevX; F32 rotY = y - mPrevY; if (abs(rotX) > 1) { F32 direction = (rotX < 0) ? -1 : 1; delta.setAngleAxis(mIncrementMouse * abs(rotX), 0, direction, 0); // changing X - rotate around Y axis mValue *= delta; } if (abs(rotY) > 1) { F32 direction = (rotY < 0) ? 1 : -1; // reverse for Y (value increases from bottom to top) delta.setAngleAxis(mIncrementMouse * abs(rotY), direction, 0, 0); // changing Y - rotate around X axis mValue *= delta; } } else { // set on click mode if (!pointInTouchCircle(x, y)) { return true; // don't drag outside the circle } F32 radius = mTouchArea->getRect().getWidth() / 2; F32 xx = x - mTouchArea->getRect().getCenterX(); F32 yy = y - mTouchArea->getRect().getCenterY(); F32 dist = sqrt(pow(xx, 2) + pow(yy, 2)); F32 azimuth = llclamp(acosf(xx / dist), 0.0f, F_PI); F32 altitude = llclamp(acosf(dist / radius), 0.0f, F_PI_BY_TWO); if (yy < 0) { azimuth = F_TWO_PI - azimuth; } LLVector3 draw_point = VectorZero * mValue; if (draw_point.mV[VZ] >= 0.f) { if (is_approx_zero(altitude)) // don't change the hemisphere { altitude = F_APPROXIMATELY_ZERO; } altitude *= -1; } mValue.setAngleAxis(altitude, 0, 1, 0); LLQuaternion az_quat; az_quat.setAngleAxis(azimuth, 0, 0, 1); mValue *= az_quat; } // we are doing a lot of F32 mathematical operations with loss of precision, // re-normalize to compensate mValue.normalize(); mPrevX = x; mPrevY = y; onCommit(); } return true; } bool LLVirtualTrackball::handleMouseUp(S32 x, S32 y, MASK mask) { if (hasMouseCapture()) { mPrevX = 0; mPrevY = 0; gFocusMgr.setMouseCapture(NULL); make_ui_sound("UISndClickRelease"); } return LLView::handleMouseUp(x, y, mask); } bool LLVirtualTrackball::handleMouseDown(S32 x, S32 y, MASK mask) { if (pointInTouchCircle(x, y)) { mPrevX = x; mPrevY = y; gFocusMgr.setMouseCapture(this); mDragMode = (mask == MASK_CONTROL) ? DRAG_SCROLL : DRAG_SET; make_ui_sound("UISndClick"); } return LLView::handleMouseDown(x, y, mask); } bool LLVirtualTrackball::handleRightMouseDown(S32 x, S32 y, MASK mask) { if (pointInTouchCircle(x, y)) { //make_ui_sound("UISndClick"); } return LLView::handleRightMouseDown(x, y, mask); } bool LLVirtualTrackball::handleKeyHere(KEY key, MASK mask) { bool handled = false; switch (key) { case KEY_DOWN: onRotateTopClick(); handled = true; break; case KEY_LEFT: onRotateRightClick(); handled = true; break; case KEY_UP: onRotateBottomClick(); handled = true; break; case KEY_RIGHT: onRotateLeftClick(); handled = true; break; default: break; } return handled; }