--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/glRenderer.cpp Tue Mar 03 22:29:27 2015 +0200 @@ -0,0 +1,1642 @@ +/* + * LDForge: LDraw parts authoring CAD + * Copyright (C) 2013 - 2015 Teemu Piippo + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#define GL_GLEXT_PROTOTYPES +#include <GL/glu.h> +#include <GL/glext.h> +#include <QGLWidget> +#include <QWheelEvent> +#include <QMouseEvent> +#include <QContextMenuEvent> +#include <QInputDialog> +#include <QToolTip> +#include <QTextDocument> +#include <QTimer> +#include <GL/glu.h> +#include "main.h" +#include "configuration.h" +#include "ldDocument.h" +#include "glRenderer.h" +#include "colors.h" +#include "mainWindow.h" +#include "miscallenous.h" +#include "editHistory.h" +#include "dialogs.h" +#include "addObjectDialog.h" +#include "messageLog.h" +#include "glCompiler.h" +#include "primitives.h" + +const LDFixedCamera g_FixedCameras[6] = +{ + {{ 1, 0, 0 }, X, Z, false, false, false }, // top + {{ 0, 0, 0 }, X, Y, false, true, false }, // front + {{ 0, 1, 0 }, Z, Y, true, true, false }, // left + {{ -1, 0, 0 }, X, Z, false, true, true }, // bottom + {{ 0, 0, 0 }, X, Y, true, true, true }, // back + {{ 0, -1, 0 }, Z, Y, false, true, true }, // right +}; + +CFGENTRY (String, BackgroundColor, "#FFFFFF") +CFGENTRY (String, MainColor, "#A0A0A0") +CFGENTRY (Float, MainColorAlpha, 1.0) +CFGENTRY (Int, LineThickness, 2) +CFGENTRY (Bool, BFCRedGreenView, false) +CFGENTRY (Int, Camera, EFreeCamera) +CFGENTRY (Bool, BlackEdges, false) +CFGENTRY (Bool, DrawAxes, false) +CFGENTRY (Bool, DrawWireframe, false) +CFGENTRY (Bool, UseLogoStuds, false) +CFGENTRY (Bool, AntiAliasedLines, true) +CFGENTRY (Bool, RandomColors, false) +CFGENTRY (Bool, HighlightObjectBelowCursor, true) +CFGENTRY (Bool, DrawSurfaces, true) +CFGENTRY (Bool, DrawEdgeLines, true) +CFGENTRY (Bool, DrawConditionalLines, true) + +// argh +const char* g_CameraNames[7] = +{ + QT_TRANSLATE_NOOP ("GLRenderer", "Top"), + QT_TRANSLATE_NOOP ("GLRenderer", "Front"), + QT_TRANSLATE_NOOP ("GLRenderer", "Left"), + QT_TRANSLATE_NOOP ("GLRenderer", "Bottom"), + QT_TRANSLATE_NOOP ("GLRenderer", "Back"), + QT_TRANSLATE_NOOP ("GLRenderer", "Right"), + QT_TRANSLATE_NOOP ("GLRenderer", "Free") +}; + +struct LDGLAxis +{ + const QColor col; + const Vertex vert; +}; + +// Definitions for visual axes, drawn on the screen +static const LDGLAxis g_GLAxes[3] = +{ + { QColor (192, 96, 96), Vertex (10000, 0, 0) }, // X + { QColor (48, 192, 48), Vertex (0, 10000, 0) }, // Y + { QColor (48, 112, 192), Vertex (0, 0, 10000) }, // Z +}; + +static bool RendererInitialized (false); + +// ============================================================================= +// +GLRenderer::GLRenderer (QWidget* parent) : QGLWidget (parent) +{ + m_isPicking = false; + m_camera = (ECamera) cfg::Camera; + m_drawToolTip = false; + m_editmode = AbstractEditMode::createByType (this, EditModeType::Select); + m_panning = false; + m_compiler = new GLCompiler (this); + setDrawOnly (false); + setMessageLog (null); + m_width = m_height = -1; + m_position3D = Origin; + m_toolTipTimer = new QTimer (this); + m_toolTipTimer->setSingleShot (true); + m_isCameraMoving = false; + m_thinBorderPen = QPen (QColor (0, 0, 0, 208), 1, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin); + m_thinBorderPen.setWidth (1); + setAcceptDrops (true); + connect (m_toolTipTimer, SIGNAL (timeout()), this, SLOT (slot_toolTipTimer())); + + // Init camera icons + for (ECamera cam = EFirstCamera; cam < ENumCameras; ++cam) + { + QString iconname = format ("camera-%1", tr (g_CameraNames[cam]).toLower()); + CameraIcon* info = &m_cameraIcons[cam]; + info->img = new QPixmap (GetIcon (iconname)); + info->cam = cam; + } + + calcCameraIcons(); +} + +// ============================================================================= +// +GLRenderer::~GLRenderer() +{ + for (int i = 0; i < 6; ++i) + delete currentDocumentData().overlays[i].img; + + for (CameraIcon& info : m_cameraIcons) + delete info.img; + + if (messageLog()) + messageLog()->setRenderer (null); + + m_compiler->setRenderer (null); + delete m_compiler; + delete m_editmode; + + glDeleteBuffers (1, &m_axesVBO); + glDeleteBuffers (1, &m_axesColorVBO); +} + +// ============================================================================= +// Calculates the "hitboxes" of the camera icons so that we can tell when the +// cursor is pointing at the camera icon. +// +void GLRenderer::calcCameraIcons() +{ + int i = 0; + + for (CameraIcon& info : m_cameraIcons) + { + // MATH + const long x1 = (m_width - (info.cam != EFreeCamera ? 48 : 16)) + ((i % 3) * 16) - 1, + y1 = ((i / 3) * 16) + 1; + + info.srcRect = QRect (0, 0, 16, 16); + info.destRect = QRect (x1, y1, 16, 16); + info.selRect = QRect ( + info.destRect.x(), + info.destRect.y(), + info.destRect.width() + 1, + info.destRect.height() + 1 + ); + + ++i; + } +} + +// ============================================================================= +// +void GLRenderer::initGLData() +{ + glEnable (GL_BLEND); + glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable (GL_POLYGON_OFFSET_FILL); + glPolygonOffset (1.0f, 1.0f); + + glEnable (GL_DEPTH_TEST); + glShadeModel (GL_SMOOTH); + glEnable (GL_MULTISAMPLE); + + if (cfg::AntiAliasedLines) + { + glEnable (GL_LINE_SMOOTH); + glEnable (GL_POLYGON_SMOOTH); + glHint (GL_LINE_SMOOTH_HINT, GL_NICEST); + glHint (GL_POLYGON_SMOOTH_HINT, GL_NICEST); + } else + { + glDisable (GL_LINE_SMOOTH); + glDisable (GL_POLYGON_SMOOTH); + } +} + +// ============================================================================= +// +void GLRenderer::needZoomToFit() +{ + if (document() != null) + currentDocumentData().needZoomToFit = true; +} + +// ============================================================================= +// +void GLRenderer::resetAngles() +{ + rot (X) = 30.0f; + rot (Y) = 325.f; + pan (X) = pan (Y) = rot (Z) = 0.0f; + needZoomToFit(); +} + +// ============================================================================= +// +void GLRenderer::resetAllAngles() +{ + ECamera oldcam = camera(); + + for (int i = 0; i < 7; ++i) + { + setCamera ((ECamera) i); + resetAngles(); + } + + setCamera (oldcam); +} + +// ============================================================================= +// +void GLRenderer::initializeGL() +{ +#ifdef USE_QT5 + initializeOpenGLFunctions(); +#endif + setBackground(); + glLineWidth (cfg::LineThickness); + glLineStipple (1, 0x6666); + setAutoFillBackground (false); + setMouseTracking (true); + setFocusPolicy (Qt::WheelFocus); + compiler()->initialize(); + initializeAxes(); + RendererInitialized = true; +} + +// ============================================================================= +// +void GLRenderer::initializeAxes() +{ + float axesdata[18]; + float colordata[18]; + memset (axesdata, 0, sizeof axesdata); + + for (int i = 0; i < 3; ++i) + { + for_axes (ax) + { + axesdata[(i * 6) + ax] = g_GLAxes[i].vert[ax]; + axesdata[(i * 6) + 3 + ax] = -g_GLAxes[i].vert[ax]; + } + + for (int j = 0; j < 2; ++j) + { + colordata[(i * 6) + (j * 3) + 0] = g_GLAxes[i].col.red(); + colordata[(i * 6) + (j * 3) + 1] = g_GLAxes[i].col.green(); + colordata[(i * 6) + (j * 3) + 2] = g_GLAxes[i].col.blue(); + } + } + + glGenBuffers (1, &m_axesVBO); + glBindBuffer (GL_ARRAY_BUFFER, m_axesVBO); + glBufferData (GL_ARRAY_BUFFER, sizeof axesdata, axesdata, GL_STATIC_DRAW); + glGenBuffers (1, &m_axesColorVBO); + glBindBuffer (GL_ARRAY_BUFFER, m_axesColorVBO); + glBufferData (GL_ARRAY_BUFFER, sizeof colordata, colordata, GL_STATIC_DRAW); + glBindBuffer (GL_ARRAY_BUFFER, 0); +} + +// ============================================================================= +// +QColor GLRenderer::getMainColor() +{ + QColor col (cfg::MainColor); + + if (not col.isValid()) + return QColor (0, 0, 0); + + col.setAlpha (cfg::MainColorAlpha * 255.f); + return col; +} + +// ============================================================================= +// +void GLRenderer::setBackground() +{ + if (isPicking()) + { + glClearColor (0.0f, 0.0f, 0.0f, 1.0f); + return; + } + + QColor col (cfg::BackgroundColor); + + if (not col.isValid()) + return; + + col.setAlpha (255); + + m_darkbg = Luma (col) < 80; + m_bgcolor = col; + qglClearColor (col); +} + +// ============================================================================= +// +void GLRenderer::refresh() +{ + update(); + + if (isVisible()) + swapBuffers(); +} + +// ============================================================================= +// +void GLRenderer::hardRefresh() +{ + if (not RendererInitialized) + return; + + compiler()->compileDocument (CurrentDocument()); + refresh(); +} + +// ============================================================================= +// +void GLRenderer::resizeGL (int w, int h) +{ + m_width = w; + m_height = h; + + calcCameraIcons(); + + glViewport (0, 0, w, h); + glMatrixMode (GL_PROJECTION); + glLoadIdentity(); + gluPerspective (45.0f, (double) w / (double) h, 1.0f, 10000.0f); + glMatrixMode (GL_MODELVIEW); +} + +// ============================================================================= +// +void GLRenderer::drawGLScene() +{ + if (document() == null) + return; + + if (currentDocumentData().needZoomToFit) + { + currentDocumentData().needZoomToFit = false; + zoomAllToFit(); + } + + if (cfg::DrawWireframe and not isPicking()) + glPolygonMode (GL_FRONT_AND_BACK, GL_LINE); + + glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glEnable (GL_DEPTH_TEST); + + if (camera() != EFreeCamera) + { + glMatrixMode (GL_PROJECTION); + glPushMatrix(); + + glLoadIdentity(); + glOrtho (-m_virtWidth, m_virtWidth, -m_virtHeight, m_virtHeight, -100.0f, 100.0f); + glTranslatef (pan (X), pan (Y), 0.0f); + + if (camera() != EFrontCamera and camera() != EBackCamera) + { + glRotatef (90.0f, g_FixedCameras[camera()].glrotate[0], + g_FixedCameras[camera()].glrotate[1], + g_FixedCameras[camera()].glrotate[2]); + } + + // Back camera needs to be handled differently + if (camera() == EBackCamera) + { + glRotatef (180.0f, 1.0f, 0.0f, 0.0f); + glRotatef (180.0f, 0.0f, 0.0f, 1.0f); + } + } + else + { + glMatrixMode (GL_MODELVIEW); + glPushMatrix(); + glLoadIdentity(); + + glTranslatef (0.0f, 0.0f, -2.0f); + glTranslatef (pan (X), pan (Y), -zoom()); + glRotatef (rot (X), 1.0f, 0.0f, 0.0f); + glRotatef (rot (Y), 0.0f, 1.0f, 0.0f); + glRotatef (rot (Z), 0.0f, 0.0f, 1.0f); + } + + glEnableClientState (GL_VERTEX_ARRAY); + glEnableClientState (GL_COLOR_ARRAY); + + if (isPicking()) + { + drawVBOs (VBOSF_Triangles, VBOCM_PickColors, GL_TRIANGLES); + drawVBOs (VBOSF_Quads, VBOCM_PickColors, GL_QUADS); + drawVBOs (VBOSF_Lines, VBOCM_PickColors, GL_LINES); + drawVBOs (VBOSF_CondLines, VBOCM_PickColors, GL_LINES); + } + else + { + if (cfg::BFCRedGreenView) + { + glEnable (GL_CULL_FACE); + glCullFace (GL_BACK); + drawVBOs (VBOSF_Triangles, VBOCM_BFCFrontColors, GL_TRIANGLES); + drawVBOs (VBOSF_Quads, VBOCM_BFCFrontColors, GL_QUADS); + glCullFace (GL_FRONT); + drawVBOs (VBOSF_Triangles, VBOCM_BFCBackColors, GL_TRIANGLES); + drawVBOs (VBOSF_Quads, VBOCM_BFCBackColors, GL_QUADS); + glDisable (GL_CULL_FACE); + } + else + { + if (cfg::RandomColors) + { + drawVBOs (VBOSF_Triangles, VBOCM_RandomColors, GL_TRIANGLES); + drawVBOs (VBOSF_Quads, VBOCM_RandomColors, GL_QUADS); + } + else + { + drawVBOs (VBOSF_Triangles, VBOCM_NormalColors, GL_TRIANGLES); + drawVBOs (VBOSF_Quads, VBOCM_NormalColors, GL_QUADS); + } + } + + drawVBOs (VBOSF_Lines, VBOCM_NormalColors, GL_LINES); + glEnable (GL_LINE_STIPPLE); + drawVBOs (VBOSF_CondLines, VBOCM_NormalColors, GL_LINES); + glDisable (GL_LINE_STIPPLE); + + if (cfg::DrawAxes) + { + glBindBuffer (GL_ARRAY_BUFFER, m_axesVBO); + glVertexPointer (3, GL_FLOAT, 0, NULL); + glBindBuffer (GL_ARRAY_BUFFER, m_axesVBO); + glColorPointer (3, GL_FLOAT, 0, NULL); + glDrawArrays (GL_LINES, 0, 6); + CHECK_GL_ERROR(); + } + } + + glPopMatrix(); + glBindBuffer (GL_ARRAY_BUFFER, 0); + glDisableClientState (GL_VERTEX_ARRAY); + glDisableClientState (GL_COLOR_ARRAY); + CHECK_GL_ERROR(); + glDisable (GL_CULL_FACE); + glMatrixMode (GL_MODELVIEW); + glPolygonMode (GL_FRONT_AND_BACK, GL_FILL); +} + +// ============================================================================= +// +void GLRenderer::drawVBOs (EVBOSurface surface, EVBOComplement colors, GLenum type) +{ + // Filter this through some configuration options + if ((Eq (surface, VBOSF_Quads, VBOSF_Triangles) and cfg::DrawSurfaces == false) or + (surface == VBOSF_Lines and cfg::DrawEdgeLines == false) or + (surface == VBOSF_CondLines and cfg::DrawConditionalLines == false)) + { + return; + } + + int surfacenum = m_compiler->vboNumber (surface, VBOCM_Surfaces); + int colornum = m_compiler->vboNumber (surface, colors); + m_compiler->prepareVBO (surfacenum); + m_compiler->prepareVBO (colornum); + GLuint surfacevbo = m_compiler->vbo (surfacenum); + GLuint colorvbo = m_compiler->vbo (colornum); + GLsizei count = m_compiler->vboSize (surfacenum) / 3; + + if (count > 0) + { + glBindBuffer (GL_ARRAY_BUFFER, surfacevbo); + glVertexPointer (3, GL_FLOAT, 0, null); + CHECK_GL_ERROR(); + glBindBuffer (GL_ARRAY_BUFFER, colorvbo); + glColorPointer (4, GL_FLOAT, 0, null); + CHECK_GL_ERROR(); + glDrawArrays (type, 0, count); + CHECK_GL_ERROR(); + } +} + +// ============================================================================= +// This converts a 2D point on the screen to a 3D point in the model. If 'snap' +// is true, the 3D point will snap to the current grid. +// +Vertex GLRenderer::coordconv2_3 (const QPoint& pos2d, bool snap) const +{ + assert (camera() != EFreeCamera); + + Vertex pos3d; + const LDFixedCamera* cam = &g_FixedCameras[camera()]; + const Axis axisX = cam->axisX; + const Axis axisY = cam->axisY; + const int negXFac = cam->negX ? -1 : 1, + negYFac = cam->negY ? -1 : 1; + + // Calculate cx and cy - these are the LDraw unit coords the cursor is at. + double cx = (-m_virtWidth + ((2 * pos2d.x() * m_virtWidth) / m_width) - pan (X)); + double cy = (m_virtHeight - ((2 * pos2d.y() * m_virtHeight) / m_height) - pan (Y)); + + if (snap) + { + cx = Grid::Snap (cx, Grid::Coordinate); + cy = Grid::Snap (cy, Grid::Coordinate); + } + + cx *= negXFac; + cy *= negYFac; + + RoundToDecimals (cx, 4); + RoundToDecimals (cy, 4); + + // Create the vertex from the coordinates + pos3d.setCoordinate (axisX, cx); + pos3d.setCoordinate (axisY, cy); + pos3d.setCoordinate ((Axis) (3 - axisX - axisY), getDepthValue()); + return pos3d; +} + +// ============================================================================= +// +// Inverse operation for the above - convert a 3D position to a 2D screen +// position. Don't ask me how this code manages to work, I don't even know. +// +QPoint GLRenderer::coordconv3_2 (const Vertex& pos3d) +{ + GLfloat m[16]; + const LDFixedCamera* cam = &g_FixedCameras[camera()]; + const Axis axisX = cam->axisX; + const Axis axisY = cam->axisY; + const int negXFac = cam->negX ? -1 : 1, + negYFac = cam->negY ? -1 : 1; + + glGetFloatv (GL_MODELVIEW_MATRIX, m); + + const double x = pos3d.x(); + const double y = pos3d.y(); + const double z = pos3d.z(); + + Vertex transformed; + transformed.setX ((m[0] * x) + (m[1] * y) + (m[2] * z) + m[3]); + transformed.setY ((m[4] * x) + (m[5] * y) + (m[6] * z) + m[7]); + transformed.setZ ((m[8] * x) + (m[9] * y) + (m[10] * z) + m[11]); + + double rx = (((transformed[axisX] * negXFac) + m_virtWidth + pan (X)) * m_width) / (2 * m_virtWidth); + double ry = (((transformed[axisY] * negYFac) - m_virtHeight + pan (Y)) * m_height) / (2 * m_virtHeight); + + return QPoint (rx, -ry); +} + +QPen GLRenderer::textPen() const +{ + return QPen (m_darkbg ? Qt::white : Qt::black); +} + +QPen GLRenderer::linePen() const +{ + QPen linepen (m_thinBorderPen); + linepen.setWidth (2); + linepen.setColor (Luma (m_bgcolor) < 40 ? Qt::white : Qt::black); + return linepen; +} + +// ============================================================================= +// +void GLRenderer::paintEvent (QPaintEvent*) +{ + doMakeCurrent(); + m_virtWidth = zoom(); + m_virtHeight = (m_height * m_virtWidth) / m_width; + initGLData(); + drawGLScene(); + + QPainter paint (this); + QFontMetrics metrics = QFontMetrics (QFont()); + paint.setRenderHint (QPainter::HighQualityAntialiasing); + + // If we wish to only draw the brick, stop here + if (isDrawOnly()) + return; + +#ifndef RELEASE + if (not isPicking()) + { + QString text = format ("Rotation: (%1, %2, %3)\nPanning: (%4, %5), Zoom: %6", + rot(X), rot(Y), rot(Z), pan(X), pan(Y), zoom()); + QRect textSize = metrics.boundingRect (0, 0, m_width, m_height, Qt::AlignCenter, text); + paint.setPen (textPen()); + paint.drawText ((width() - textSize.width()) / 2, height() - textSize.height(), textSize.width(), + textSize.height(), Qt::AlignCenter, text); + } +#endif + + if (camera() != EFreeCamera and not isPicking()) + { + // Paint the overlay image if we have one + const LDGLOverlay& overlay = currentDocumentData().overlays[camera()]; + + if (overlay.img != null) + { + QPoint v0 = coordconv3_2 (currentDocumentData().overlays[camera()].v0), + v1 = coordconv3_2 (currentDocumentData().overlays[camera()].v1); + + QRect targRect (v0.x(), v0.y(), Abs (v1.x() - v0.x()), Abs (v1.y() - v0.y())), + srcRect (0, 0, overlay.img->width(), overlay.img->height()); + paint.drawImage (targRect, *overlay.img, srcRect); + } + + // Paint the coordinates onto the screen. + QString text = format (tr ("X: %1, Y: %2, Z: %3"), m_position3D[X], m_position3D[Y], m_position3D[Z]); + QFontMetrics metrics = QFontMetrics (font()); + QRect textSize = metrics.boundingRect (0, 0, m_width, m_height, Qt::AlignCenter, text); + paint.setPen (textPen()); + paint.drawText (m_width - textSize.width(), m_height - 16, textSize.width(), + textSize.height(), Qt::AlignCenter, text); + } + + if (not isPicking()) + { + // Draw edit mode HUD + m_editmode->render (paint); + + // Draw a background for the selected camera + paint.setPen (m_thinBorderPen); + paint.setBrush (QBrush (QColor (0, 128, 160, 128))); + paint.drawRect (m_cameraIcons[camera()].selRect); + + // Draw the camera icons + for (CameraIcon& info : m_cameraIcons) + { + // Don't draw the free camera icon when in draw mode + if (&info == &m_cameraIcons[EFreeCamera] and not m_editmode->allowFreeCamera()) + continue; + + paint.drawPixmap (info.destRect, *info.img, info.srcRect); + } + + QString formatstr = tr ("%1 Camera"); + + // Draw a label for the current camera in the bottom left corner + { + const int margin = 4; + + QString label; + label = format (formatstr, tr (g_CameraNames[camera()])); + paint.setPen (textPen()); + paint.drawText (QPoint (margin, height() - (margin + metrics.descent())), label); + } + + // Tool tips + if (m_drawToolTip) + { + if (not m_cameraIcons[m_toolTipCamera].destRect.contains (m_mousePosition)) + m_drawToolTip = false; + else + { + QString label = format (formatstr, tr (g_CameraNames[m_toolTipCamera])); + QToolTip::showText (m_globalpos, label); + } + } + } + + // Message log + if (messageLog()) + { + int y = 0; + const int margin = 2; + QColor penColor = textPen().color(); + + for (const MessageManager::Line& line : messageLog()->getLines()) + { + penColor.setAlphaF (line.alpha); + paint.setPen (penColor); + paint.drawText (QPoint (margin, y + margin + metrics.ascent()), line.text); + y += metrics.height(); + } + } +} + +// ============================================================================= +// +void GLRenderer::drawBlip (QPainter& paint, QPointF pos) const +{ + QPen pen = m_thinBorderPen; + const int blipsize = 8; + pen.setWidth (1); + paint.setPen (pen); + paint.setBrush (QColor (64, 192, 0)); + paint.drawEllipse (pos.x() - blipsize / 2, pos.y() - blipsize / 2, blipsize, blipsize); +} + +// ============================================================================= +// +void GLRenderer::clampAngle (double& angle) const +{ + while (angle < 0) + angle += 360.0; + + while (angle > 360.0) + angle -= 360.0; +} + +// ============================================================================= +// +void GLRenderer::mouseReleaseEvent (QMouseEvent* ev) +{ + const bool wasLeft = (m_lastButtons & Qt::LeftButton) and not (ev->buttons() & Qt::LeftButton); + + Qt::MouseButtons releasedbuttons = m_lastButtons & ~ev->buttons(); + + if (m_panning) + m_panning = false; + + if (wasLeft) + { + // Check if we selected a camera icon + if (not mouseHasMoved()) + { + for (CameraIcon & info : m_cameraIcons) + { + if (info.destRect.contains (ev->pos())) + { + setCamera (info.cam); + goto end; + } + } + } + } + + if (not isDrawOnly()) + { + AbstractEditMode::MouseEventData data; + data.ev = ev; + data.mouseMoved = mouseHasMoved(); + data.keymods = m_keymods; + data.releasedButtons = releasedbuttons; + + if (m_editmode->mouseReleased (data)) + goto end; + } + +end: + update(); + m_totalmove = 0; +} + +// ============================================================================= +// +void GLRenderer::mousePressEvent (QMouseEvent* ev) +{ + m_totalmove = 0; + m_lastButtons = ev->buttons(); + + if (m_editmode->mousePressed (ev)) + ev->accept(); +} + +// ============================================================================= +// +void GLRenderer::mouseMoveEvent (QMouseEvent* ev) +{ + int dx = ev->x() - m_mousePosition.x(); + int dy = ev->y() - m_mousePosition.y(); + m_totalmove += Abs (dx) + Abs (dy); + setCameraMoving (false); + + if (not m_editmode->mouseMoved (ev)) + { + const bool left = ev->buttons() & Qt::LeftButton, + mid = ev->buttons() & Qt::MidButton, + shift = ev->modifiers() & Qt::ShiftModifier; + + if (mid or (left and shift)) + { + pan (X) += 0.03f * dx * (zoom() / 7.5f); + pan (Y) -= 0.03f * dy * (zoom() / 7.5f); + m_panning = true; + setCameraMoving (true); + } + elif (left and camera() == EFreeCamera) + { + rot (X) = rot (X) + dy; + rot (Y) = rot (Y) + dx; + + clampAngle (rot (X)); + clampAngle (rot (Y)); + setCameraMoving (true); + } + } + + // Start the tool tip timer + if (not m_drawToolTip) + m_toolTipTimer->start (500); + + // Update 2d position + m_mousePosition = ev->pos(); + m_globalpos = ev->globalPos(); + +#ifndef USE_QT5 + m_mousePositionF = ev->posF(); +#else + m_mousePositionF = ev->localPos(); +#endif + + // Calculate 3d position of the cursor + m_position3D = (camera() != EFreeCamera) ? coordconv2_3 (m_mousePosition, true) : Origin; + + highlightCursorObject(); + update(); + ev->accept(); +} + +// ============================================================================= +// +void GLRenderer::keyPressEvent (QKeyEvent* ev) +{ + m_keymods = ev->modifiers(); +} + +// ============================================================================= +// +void GLRenderer::keyReleaseEvent (QKeyEvent* ev) +{ + m_keymods = ev->modifiers(); + m_editmode->keyReleased (ev); + update(); +} + +// ============================================================================= +// +void GLRenderer::wheelEvent (QWheelEvent* ev) +{ + doMakeCurrent(); + + zoomNotch (ev->delta() > 0); + zoom() = Clamp (zoom(), 0.01, 10000.0); + setCameraMoving (true); + update(); + ev->accept(); +} + +// ============================================================================= +// +void GLRenderer::leaveEvent (QEvent* ev) +{ + (void) ev; + m_drawToolTip = false; + m_toolTipTimer->stop(); + update(); +} + +// ============================================================================= +// +void GLRenderer::contextMenuEvent (QContextMenuEvent* ev) +{ + g_win->spawnContextMenu (ev->globalPos()); +} + +// ============================================================================= +// +void GLRenderer::setCamera (const ECamera cam) +{ + // The edit mode may forbid the free camera. + if (cam == EFreeCamera and not m_editmode->allowFreeCamera()) + return; + + m_camera = cam; + cfg::Camera = (int) cam; + g_win->updateEditModeActions(); +} + +// ============================================================================= +// +void GLRenderer::pick (int mouseX, int mouseY, bool additive) +{ + pick (QRect (mouseX, mouseY, mouseX + 1, mouseY + 1), additive); +} + +// ============================================================================= +// +void GLRenderer::pick (QRect const& range, bool additive) +{ + doMakeCurrent(); + + // Clear the selection if we do not wish to add to it. + if (not additive) + { + LDObjectList oldsel = Selection(); + CurrentDocument()->clearSelection(); + + for (LDObjectPtr obj : oldsel) + compileObject (obj); + } + + // Paint the picking scene + setPicking (true); + drawGLScene(); + + int x0 = range.left(); + int y0 = range.top(); + int x1 = x0 + range.width(); + int y1 = y0 + range.height(); + + // Clamp the values to ensure they're within bounds + x0 = Max (0, x0); + y0 = Max (0, y0); + x1 = Min (x1, m_width); + y1 = Min (y1, m_height); + const int areawidth = (x1 - x0); + const int areaheight = (y1 - y0); + const qint32 numpixels = areawidth * areaheight; + + // Allocate space for the pixel data. + uchar* const pixeldata = new uchar[4 * numpixels]; + uchar* pixelptr = &pixeldata[0]; + + // Read pixels from the color buffer. + glReadPixels (x0, m_height - y1, areawidth, areaheight, GL_RGBA, GL_UNSIGNED_BYTE, pixeldata); + + LDObjectPtr removedObj; + QList<qint32> indices; + + // Go through each pixel read and add them to the selection. + // Note: black is background, those indices are skipped. + for (qint32 i = 0; i < numpixels; ++i) + { + qint32 idx = + (*(pixelptr + 0) * 0x10000) + + (*(pixelptr + 1) * 0x100) + + *(pixelptr + 2); + pixelptr += 4; + + if (idx != 0) + indices << idx; + } + + RemoveDuplicates (indices); + + for (qint32 idx : indices) + { + LDObjectPtr obj = LDObject::fromID (idx); + assert (obj != null); + + // If this is an additive single pick and the object is currently selected, + // we remove it from selection instead. + if (additive) + { + if (obj->isSelected()) + { + obj->deselect(); + removedObj = obj; + break; + } + } + + obj->select(); + } + + delete[] pixeldata; + + // Update everything now. + g_win->updateSelection(); + + // Recompile the objects now to update their color + for (LDObjectPtr obj : Selection()) + compileObject (obj); + + if (removedObj) + compileObject (removedObj); + + setPicking (false); + repaint(); +} + +// +// Simpler version of GLRenderer::pick which simply picks whatever object on the screen +// +LDObjectPtr GLRenderer::pickOneObject (int mouseX, int mouseY) +{ + uchar pixel[4]; + doMakeCurrent(); + setPicking (true); + drawGLScene(); + glReadPixels (mouseX, m_height - mouseY, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); + LDObjectPtr obj = LDObject::fromID ((pixel[0] * 0x10000) + (pixel[1] * 0x100) + pixel[2]); + setPicking (false); + repaint(); + return obj; +} + +// ============================================================================= +// +void GLRenderer::setEditMode (EditModeType a) +{ + if (m_editmode != null and m_editmode->type() == a) + return; + + delete m_editmode; + m_editmode = AbstractEditMode::createByType (this, a); + + // If we cannot use the free camera, use the top one instead. + if (camera() == EFreeCamera and not m_editmode->allowFreeCamera()) + setCamera (ETopCamera); + + g_win->updateEditModeActions(); + update(); +} + +// ============================================================================= +// +EditModeType GLRenderer::currentEditModeType() const +{ + return m_editmode->type(); +} + +// ============================================================================= +// +void GLRenderer::setDocument (LDDocumentPtr const& a) +{ + m_document = a; + + if (a != null) + { + initOverlaysFromObjects(); + + if (not currentDocumentData().init) + { + resetAllAngles(); + currentDocumentData().init = true; + } + + currentDocumentData().needZoomToFit = true; + } +} + +// ============================================================================= +// +void GLRenderer::setPicking (const bool& a) +{ + m_isPicking = a; + setBackground(); + + if (isPicking()) + { + glDisable (GL_DITHER); + + // Use particularly thick lines while picking ease up selecting lines. + glLineWidth (Max<double> (cfg::LineThickness, 6.5)); + } + else + { + glEnable (GL_DITHER); + + // Restore line thickness + glLineWidth (cfg::LineThickness); + } +} + +// ============================================================================= +// +void GLRenderer::getRelativeAxes (Axis& relX, Axis& relY) const +{ + const LDFixedCamera* cam = &g_FixedCameras[camera()]; + relX = cam->axisX; + relY = cam->axisY; +} + +// ============================================================================= +// +Axis GLRenderer::getRelativeZ() const +{ + const LDFixedCamera* cam = &g_FixedCameras[camera()]; + return (Axis) (3 - cam->axisX - cam->axisY); +} + +// ============================================================================= +// +static QList<Vertex> GetVerticesOf (LDObjectPtr obj) +{ + QList<Vertex> verts; + + if (obj->numVertices() >= 2) + { + for (int i = 0; i < obj->numVertices(); ++i) + verts << obj->vertex (i); + } + elif (obj->type() == OBJ_Subfile) + { + LDSubfilePtr ref = obj.staticCast<LDSubfile>(); + LDObjectList objs = ref->inlineContents (true, false); + + for (LDObjectPtr obj : objs) + { + verts << GetVerticesOf (obj); + obj->destroy(); + } + } + + return verts; +} + +// ============================================================================= +// +void GLRenderer::compileObject (LDObjectPtr obj) +{ + compiler()->stageForCompilation (obj); +} + +// ============================================================================= +// +void GLRenderer::forgetObject (LDObjectPtr obj) +{ + if (compiler() != null) + compiler()->dropObject (obj); +} + +// ============================================================================= +// +uchar* GLRenderer::getScreencap (int& w, int& h) +{ + w = m_width; + h = m_height; + uchar* cap = new uchar[4 * w * h]; + + m_screencap = true; + update(); + m_screencap = false; + + // Capture the pixels + glReadPixels (0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, cap); + + return cap; +} + +// ============================================================================= +// +void GLRenderer::slot_toolTipTimer() +{ + // We come here if the cursor has stayed in one place for longer than a + // a second. Check if we're holding it over a camera icon - if so, draw + // a tooltip. + for (CameraIcon & icon : m_cameraIcons) + { + if (icon.destRect.contains (m_mousePosition)) + { + m_toolTipCamera = icon.cam; + m_drawToolTip = true; + update(); + break; + } + } +} + +// ============================================================================= +// +Axis GLRenderer::getCameraAxis (bool y, ECamera camid) +{ + if (camid == (ECamera) -1) + camid = camera(); + + const LDFixedCamera* cam = &g_FixedCameras[camid]; + return (y) ? cam->axisY : cam->axisX; +} + +// ============================================================================= +// +bool GLRenderer::setupOverlay (ECamera cam, QString file, int x, int y, int w, int h) +{ + QImage* img = new QImage (QImage (file).convertToFormat (QImage::Format_ARGB32)); + LDGLOverlay& info = getOverlay (cam); + + if (img->isNull()) + { + Critical (tr ("Failed to load overlay image!")); + currentDocumentData().overlays[cam].invalid = true; + delete img; + return false; + } + + delete info.img; // delete the old image + + info.fname = file; + info.lw = w; + info.lh = h; + info.ox = x; + info.oy = y; + info.img = img; + info.invalid = false; + + if (info.lw == 0) + info.lw = (info.lh * img->width()) / img->height(); + elif (info.lh == 0) + info.lh = (info.lw * img->height()) / img->width(); + + const Axis x2d = getCameraAxis (false, cam), + y2d = getCameraAxis (true, cam); + const double negXFac = g_FixedCameras[cam].negX ? -1 : 1, + negYFac = g_FixedCameras[cam].negY ? -1 : 1; + + info.v0 = info.v1 = Origin; + info.v0.setCoordinate (x2d, -(info.ox * info.lw * negXFac) / img->width()); + info.v0.setCoordinate (y2d, (info.oy * info.lh * negYFac) / img->height()); + info.v1.setCoordinate (x2d, info.v0[x2d] + info.lw); + info.v1.setCoordinate (y2d, info.v0[y2d] + info.lh); + + // Set alpha of all pixels to 0.5 + for (long i = 0; i < img->width(); ++i) + for (long j = 0; j < img->height(); ++j) + { + uint32 pixel = img->pixel (i, j); + img->setPixel (i, j, 0x80000000 | (pixel & 0x00FFFFFF)); + } + + updateOverlayObjects(); + return true; +} + +// ============================================================================= +// +void GLRenderer::clearOverlay() +{ + if (camera() == EFreeCamera) + return; + + LDGLOverlay& info = currentDocumentData().overlays[camera()]; + delete info.img; + info.img = null; + + updateOverlayObjects(); +} + +// ============================================================================= +// +void GLRenderer::setDepthValue (double depth) +{ + assert (camera() < EFreeCamera); + currentDocumentData().depthValues[camera()] = depth; +} + +// ============================================================================= +// +double GLRenderer::getDepthValue() const +{ + assert (camera() < EFreeCamera); + return currentDocumentData().depthValues[camera()]; +} + +// ============================================================================= +// +const char* GLRenderer::getCameraName() const +{ + return g_CameraNames[camera()]; +} + +// ============================================================================= +// +LDGLOverlay& GLRenderer::getOverlay (int newcam) +{ + return currentDocumentData().overlays[newcam]; +} + +// ============================================================================= +// +void GLRenderer::zoomNotch (bool inward) +{ + zoom() *= inward ? 0.833f : 1.2f; +} + +// ============================================================================= +// +void GLRenderer::zoomToFit() +{ + zoom() = 30.0f; + + if (document() == null or m_width == -1 or m_height == -1) + return; + + bool lastfilled = false; + bool firstrun = true; + enum { black = 0xFF000000 }; + bool inward = true; + int runaway = 50; + + // Use the pick list while drawing the scene, this way we can tell whether borders + // are background or not. + setPicking (true); + + while (--runaway) + { + if (zoom() > 10000.0 or zoom() < 0.0) + { + // Nothing to draw if we get here. + zoom() = 30.0; + break; + } + + zoomNotch (inward); + QVector<unsigned char> capture (4 * m_width * m_height); + drawGLScene(); + glReadPixels (0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, capture.data()); + QImage image (capture.constData(), m_width, m_height, QImage::Format_ARGB32); + bool filled = false; + + // Check the top and bottom rows + for (int i = 0; i < image.width(); ++i) + { + if (image.pixel (i, 0) != black or image.pixel (i, m_height - 1) != black) + { + filled = true; + break; + } + } + + // Left and right edges + if (filled == false) + { + for (int i = 0; i < image.height(); ++i) + { + if (image.pixel (0, i) != black or image.pixel (m_width - 1, i) != black) + { + filled = true; + break; + } + } + } + + if (firstrun) + { + // If this is the first run, we don't know enough to determine + // whether the zoom was to fit, so we mark in our knowledge so + // far and start over. + inward = not filled; + firstrun = false; + } + else + { + // If this run filled the screen and the last one did not, the + // last run had ideal zoom - zoom a bit back and we should reach it. + if (filled and not lastfilled) + { + zoomNotch (false); + break; + } + + // If this run did not fill the screen and the last one did, we've + // now reached ideal zoom so we're done here. + if (not filled and lastfilled) + break; + + inward = not filled; + } + + lastfilled = filled; + } + + setPicking (false); +} + +// ============================================================================= +// +void GLRenderer::zoomAllToFit() +{ + zoomToFit(); +} + +// ============================================================================= +// +void GLRenderer::mouseDoubleClickEvent (QMouseEvent* ev) +{ + if (m_editmode->mouseDoubleClicked (ev)) + ev->accept(); +} + +// ============================================================================= +// +LDOverlayPtr GLRenderer::findOverlayObject (ECamera cam) +{ + for (LDObjectPtr obj : document()->objects()) + { + LDOverlayPtr ovlobj = obj.dynamicCast<LDOverlay>(); + + if (ovlobj != null and obj.staticCast<LDOverlay>()->camera() == cam) + return ovlobj; + } + + return LDOverlayPtr(); +} + +// ============================================================================= +// +// Read in overlays from the current file and update overlay info accordingly. +// +void GLRenderer::initOverlaysFromObjects() +{ + for (ECamera cam = EFirstCamera; cam < ENumCameras; ++cam) + { + if (cam == EFreeCamera) + continue; + + LDGLOverlay& meta = currentDocumentData().overlays[cam]; + LDOverlayPtr ovlobj = findOverlayObject (cam); + + if (ovlobj == null and meta.img != null) + { + delete meta.img; + meta.img = null; + } + elif (ovlobj != null and + (meta.img == null or meta.fname != ovlobj->fileName()) and + not meta.invalid) + { + setupOverlay (cam, ovlobj->fileName(), ovlobj->x(), + ovlobj->y(), ovlobj->width(), ovlobj->height()); + } + } +} + +// ============================================================================= +// +void GLRenderer::updateOverlayObjects() +{ + for (ECamera cam = EFirstCamera; cam < ENumCameras; ++cam) + { + if (cam == EFreeCamera) + continue; + + LDGLOverlay& meta = currentDocumentData().overlays[cam]; + LDOverlayPtr ovlobj = findOverlayObject (cam); + + if (meta.img == null and ovlobj != null) + { + // If this is the last overlay image, we need to remove the empty space after it as well. + LDObjectPtr nextobj = ovlobj->next(); + + if (nextobj and nextobj->type() == OBJ_Empty) + nextobj->destroy(); + + // If the overlay object was there and the overlay itself is + // not, remove the object. + ovlobj->destroy(); + } + elif (meta.img != null and ovlobj == null) + { + // Inverse case: image is there but the overlay object is + // not, thus create the object. + ovlobj = LDSpawn<LDOverlay>(); + + // Find a suitable position to place this object. We want to place + // this into the header, which is everything up to the first scemantic + // object. If we find another overlay object, place this object after + // the last one found. Otherwise, place it before the first schemantic + // object and put an empty object after it (though don't do this if + // there was no schemantic elements at all) + int i, lastOverlay = -1; + bool found = false; + + for (i = 0; i < document()->getObjectCount(); ++i) + { + LDObjectPtr obj = document()->getObject (i); + + if (obj->isScemantic()) + { + found = true; + break; + } + + if (obj->type() == OBJ_Overlay) + lastOverlay = i; + } + + if (lastOverlay != -1) + document()->insertObj (lastOverlay + 1, ovlobj); + else + { + document()->insertObj (i, ovlobj); + + if (found) + document()->insertObj (i + 1, LDSpawn<LDEmpty>()); + } + } + + if (meta.img != null and ovlobj != null) + { + ovlobj->setCamera (cam); + ovlobj->setFileName (meta.fname); + ovlobj->setX (meta.ox); + ovlobj->setY (meta.oy); + ovlobj->setWidth (meta.lw); + ovlobj->setHeight (meta.lh); + } + } + + if (g_win->R() == this) + g_win->refresh(); +} + +// ============================================================================= +// +void GLRenderer::highlightCursorObject() +{ + if (not cfg::HighlightObjectBelowCursor and objectAtCursor() == null) + return; + + LDObjectWeakPtr newObject; + LDObjectWeakPtr oldObject = objectAtCursor(); + qint32 newIndex; + + if (isCameraMoving() or not cfg::HighlightObjectBelowCursor) + { + newIndex = 0; + } + else + { + setPicking (true); + drawGLScene(); + setPicking (false); + + unsigned char pixel[4]; + glReadPixels (m_mousePosition.x(), m_height - m_mousePosition.y(), 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &pixel[0]); + newIndex = pixel[0] * 0x10000 | pixel[1] * 0x100 | pixel[2]; + } + + if (newIndex != (oldObject != null ? oldObject.toStrongRef()->id() : 0)) + { + if (newIndex != 0) + newObject = LDObject::fromID (newIndex); + + setObjectAtCursor (newObject); + + if (oldObject != null) + compileObject (oldObject); + + if (newObject != null) + compileObject (newObject); + } + + update(); +} + +void GLRenderer::dragEnterEvent (QDragEnterEvent* ev) +{ + if (g_win != null and ev->source() == g_win->getPrimitivesTree() and g_win->getPrimitivesTree()->currentItem() != null) + ev->acceptProposedAction(); +} + +void GLRenderer::dropEvent (QDropEvent* ev) +{ + if (g_win != null and ev->source() == g_win->getPrimitivesTree()) + { + QString primName = static_cast<SubfileListItem*> (g_win->getPrimitivesTree()->currentItem())->primitive()->name; + LDSubfilePtr ref = LDSpawn<LDSubfile>(); + ref->setColor (MainColor()); + ref->setFileInfo (GetDocument (primName)); + ref->setPosition (Origin); + ref->setTransform (IdentityMatrix); + LDDocument::current()->insertObj (g_win->getInsertionPoint(), ref); + ref->select(); + g_win->buildObjList(); + g_win->R()->refresh(); + ev->acceptProposedAction(); + } +} + +Vertex const& GLRenderer::position3D() const +{ + return m_position3D; +} + +LDFixedCamera const& GLRenderer::getFixedCamera (ECamera cam) const +{ + return g_FixedCameras[cam]; +} + +bool GLRenderer::mouseHasMoved() const +{ + return m_totalmove >= 10; +} + +QPoint const& GLRenderer::mousePosition() const +{ + return m_mousePosition; +} + +QPointF const& GLRenderer::mousePositionF() const +{ + return m_mousePositionF; +} + +void GLRenderer::doMakeCurrent() +{ + makeCurrent(); + initializeOpenGLFunctions(); +} + +int GLRenderer::depthNegateFactor() const +{ + return g_FixedCameras[camera()].negatedDepth ? -1 : 1; +} + +Qt::KeyboardModifiers GLRenderer::keyboardModifiers() const +{ + return m_keymods; +} + +LDFixedCamera const& GetFixedCamera (ECamera cam) +{ + assert (cam != EFreeCamera); + return g_FixedCameras[cam]; +}