diff --git a/CMakeLists.txt b/CMakeLists.txt index feffb03..32d1bff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,17 +9,19 @@ set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGLWidgets OpenGL) +find_package(OpenCV REQUIRED) add_executable(weld_measure main.cpp main_window.h main_window.cpp - weld_opengl_widget.h - weld_opengl_widget.cpp + measurement_widget.h + measurement_widget.cpp ) target_link_libraries(weld_measure PRIVATE Qt6::Widgets Qt6::OpenGLWidgets Qt6::OpenGL + ${OpenCV_LIBS} ) diff --git a/main_window.cpp b/main_window.cpp index b7ed1a3..a0fcebc 100644 --- a/main_window.cpp +++ b/main_window.cpp @@ -8,6 +8,7 @@ #include #include #include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) @@ -16,7 +17,6 @@ MainWindow::MainWindow(QWidget *parent) resize(1200, 800); createUI(); - createDefaultSampleImage(); } void MainWindow::createUI() @@ -46,23 +46,26 @@ void MainWindow::createUI() panelLayout->addWidget(grpFile); // Calibration setup section - QGroupBox *grpCalib = new QGroupBox("2. Calibración por Escala", panel); + QGroupBox *grpCalib = new QGroupBox("2. Calibración", panel); QVBoxLayout *calibLayout = new QVBoxLayout(grpCalib); - QFormLayout *formCalib = new QFormLayout(); - m_spinCalibMm = new QDoubleSpinBox(grpCalib); - m_spinCalibMm->setRange(0.001, 10000.0); - m_spinCalibMm->setValue(10.0); - m_spinCalibMm->setSuffix(" mm"); - m_spinCalibMm->setDecimals(3); - formCalib->addRow("Largo Real Patrón:", m_spinCalibMm); - calibLayout->addLayout(formCalib); + QFormLayout *formGrid = new QFormLayout(); + m_spinSquareSize = new QDoubleSpinBox(grpCalib); + m_spinSquareSize->setRange(0.1, 1000.0); + m_spinSquareSize->setValue(30.0); + m_spinSquareSize->setSuffix(" mm"); + m_spinSquareSize->setDecimals(1); + + formGrid->addRow("Lado Cuadrado (Ajedrez):", m_spinSquareSize); + calibLayout->addLayout(formGrid); + + QPushButton *btnCalibChessboard = new QPushButton("Autocalibrar Ajedrez", grpCalib); + btnCalibChessboard->setFixedHeight(30); + btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;"); + calibLayout->addWidget(btnCalibChessboard); + - m_btnCalibrate = new QPushButton("Calibrar Escala", grpCalib); - m_btnCalibrate->setFixedHeight(34); - m_btnCalibrate->setStyleSheet("font-weight: bold; background-color: #2b5b84; color: white; border-radius: 4px;"); - calibLayout->addWidget(m_btnCalibrate); panelLayout->addWidget(grpCalib); // Measurement readout section @@ -80,59 +83,71 @@ void MainWindow::createUI() panelLayout->addStretch(); // OpenGL Central Viewport Widget - m_glWidget = new WeldOpenGLWidget(centralWidget); + m_glWidget = new MeasurementWidget(centralWidget); mainLayout->addWidget(panel); mainLayout->addWidget(m_glWidget, 1); // Connect signals & slots connect(btnOpen, &QPushButton::clicked, this, &MainWindow::openImage); - connect(m_btnCalibrate, &QPushButton::clicked, this, &MainWindow::onCalibrateClicked); - connect(m_glWidget, &WeldOpenGLWidget::measurementUpdated, this, &MainWindow::updateMeasurementInfo); -} + connect(m_glWidget, &MeasurementWidget::measurementUpdated, this, &MainWindow::updateMeasurementInfo); -void MainWindow::createDefaultSampleImage() -{ - QImage img(1000, 700, QImage::Format_RGB32); - img.fill(QColor(40, 42, 48)); + // Connect progress signal to dynamically update the gradient color of the button! + connect(m_glWidget, &MeasurementWidget::calibrationProgress, this, [btnCalibChessboard](int percent) { + double ratio = percent / 100.0; + btnCalibChessboard->setStyleSheet(QString( + "font-weight: bold; color: white; border-radius: 4px;" + "background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2e7d32, stop:%1 #2e7d32, stop:%2 #b71c1c, stop:1 #b71c1c);" + ).arg(ratio).arg(qMin(1.0, ratio + 0.001))); + }); - QPainter painter(&img); - painter.setRenderHint(QPainter::Antialiasing); + // Autocalibrate using chessboard + connect(btnCalibChessboard, &QPushButton::clicked, this, [this, btnCalibChessboard, btnOpen]() { + btnCalibChessboard->setEnabled(false); + btnOpen->setEnabled(false); - // Draw simulated weld seam - for (int x = 100; x < 900; x += 6) { - double y = 350 + qSin(x / 30.0) * 15 + qCos(x / 7.0) * 8; - double r = 25 + qSin(x / 10.0) * 5; - painter.setBrush(QColor(130, 135, 145)); - painter.setPen(QPen(QColor(170, 175, 185), 1.5)); - painter.drawEllipse(QPointF(x, y), r, r * 0.5); - } + int gx = 0; // Pass 0 to auto-detect + int gy = 0; // Pass 0 to auto-detect + double sqSize = m_spinSquareSize->value(); + + bool success = m_glWidget->calibrateWithChessboard(gx, gy, sqSize); + + if (success) { + // Keep green when calibrated successfully + btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #2e7d32; color: white; border-radius: 4px;"); + } else { + // Return to red if calibration failed + btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;"); + } + btnCalibChessboard->setEnabled(true); + btnOpen->setEnabled(true); - // Draw reference pattern bar (200px = 10mm) - int scaleX = 150; - int scaleY = 560; - int scaleLenPx = 200; + if (success) { + int squaresX = gx + 1; + int squaresY = gy + 1; + + QMessageBox::information(this, "Calibración Exitosa", + QString("Se detectó automáticamente un tablero de ajedrez.\n\n" + "Dimensiones identificadas:\n" + "- Intersecciones internas: %1 x %2\n" + "- Cuadrados totales: %3 x %4\n" + "- Medida del lado: %5 mm\n\n" + "La distorsión del lente y la perspectiva han sido calibradas con éxito.") + .arg(gx).arg(gy).arg(squaresX).arg(squaresY).arg(sqSize, 0, 'f', 1)); + } else { + QMessageBox::warning(this, "Error de Calibración", + QString("No se pudo detectar automáticamente ningún tablero de ajedrez en la imagen.\n\n" + "Por favor verifica:\n" + "1. Que el tablero esté bien iluminado y plano.\n" + "2. Que el tablero esté completamente visible en la foto.")); + } + }); - painter.setBrush(QColor(230, 230, 230)); - painter.setPen(Qt::NoPen); - painter.drawRoundedRect(scaleX - 20, scaleY - 30, scaleLenPx + 40, 70, 8, 8); - - QPen rulerPen(Qt::black, 4, Qt::SolidLine, Qt::SquareCap); - painter.setPen(rulerPen); - painter.drawLine(scaleX, scaleY, scaleX + scaleLenPx, scaleY); - painter.drawLine(scaleX, scaleY - 15, scaleX, scaleY + 15); - painter.drawLine(scaleX + scaleLenPx, scaleY - 15, scaleX + scaleLenPx, scaleY + 15); - - painter.setFont(QFont("Arial", 11, QFont::Bold)); - painter.drawText(QRect(scaleX - 20, scaleY + 10, scaleLenPx + 40, 25), Qt::AlignCenter, "PATRÓN DE PRUEBA: 10.0 mm"); - - img.save("/home/sebastian/repos/weld_measure/sample_weld.png"); - m_glWidget->setImage(img); - - // Position single line right over the sample pattern bar (200px = 10mm) - m_glWidget->setLinePoints(QPointF(scaleX, scaleY), QPointF(scaleX + scaleLenPx, scaleY)); - m_glWidget->calibrateWithCurrentLine(10.0); + // Reset calibration button color to red when a new image is loaded + connect(m_glWidget, &MeasurementWidget::imageLoaded, this, [btnCalibChessboard]() { + btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;"); + }); } void MainWindow::openImage() @@ -145,15 +160,6 @@ void MainWindow::openImage() } } -void MainWindow::onCalibrateClicked() -{ - double knownMm = m_spinCalibMm->value(); - m_glWidget->calibrateWithCurrentLine(knownMm); - QMessageBox::information(this, "Calibración Exitosa", - QString("Escala fijada correctamente:\n%1 mm/px\n\nAhora puede mover la misma línea para medir cualquier parte de la imagen.") - .arg(m_glWidget->mmPerPixel(), 0, 'g', 5)); -} - void MainWindow::updateMeasurementInfo(double px, double mm) diff --git a/main_window.h b/main_window.h index 52bb307..a90131b 100644 --- a/main_window.h +++ b/main_window.h @@ -3,9 +3,11 @@ #include #include +#include #include #include -#include "weld_opengl_widget.h" +#include +#include "measurement_widget.h" class MainWindow : public QMainWindow { @@ -17,18 +19,16 @@ public: private slots: void openImage(); - void onCalibrateClicked(); void updateMeasurementInfo(double px, double mm); private: void createUI(); - void createDefaultSampleImage(); - WeldOpenGLWidget *m_glWidget = nullptr; + MeasurementWidget *m_glWidget = nullptr; - QDoubleSpinBox *m_spinCalibMm = nullptr; - QPushButton *m_btnCalibrate = nullptr; QLabel *m_lblMeasurement = nullptr; + + QDoubleSpinBox *m_spinSquareSize = nullptr; }; #endif // MAIN_WINDOW_H diff --git a/measurement_widget.cpp b/measurement_widget.cpp new file mode 100644 index 0000000..68cf46f --- /dev/null +++ b/measurement_widget.cpp @@ -0,0 +1,745 @@ +#include "measurement_widget.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MeasurementWidget::MeasurementWidget(QWidget *parent) + : QOpenGLWidget(parent) +{ + setMouseTracking(true); +} + +MeasurementWidget::~MeasurementWidget() +{ + makeCurrent(); + if (m_texture) { + delete m_texture; + m_texture = nullptr; + } + doneCurrent(); +} + +void MeasurementWidget::initializeGL() +{ + initializeOpenGLFunctions(); + glClearColor(0.15f, 0.15f, 0.18f, 1.0f); +} + +void MeasurementWidget::resizeGL(int w, int h) +{ + glViewport(0, 0, w, h); +} + +bool MeasurementWidget::loadImage(const QString &filePath) +{ + QImage img(filePath); + if (img.isNull()) return false; + + setImage(img); + return true; +} + +void MeasurementWidget::setImage(const QImage &image) +{ + m_image = image.convertToFormat(QImage::Format_RGBA8888); + m_textureNeedsUpdate = true; + + // Reset zoom and center image + m_zoomLevel = 1.0; + m_panOffset = QPointF(0, 0); + + // Initialize single measurement/calibration line + int w = m_image.width(); + int h = m_image.height(); + + m_head = QPointF(w * 0.3, h * 0.5); + m_tail = QPointF(w * 0.7, h * 0.5); + m_hasLine = true; + + // Reset calibration parameters + m_mmPerPixel = 0.0; + m_isCalibrated = false; + m_hasHomography = false; + m_calibrationMode = false; + m_activeCornerIdx = -1; + m_calibCorners.clear(); + + emit imageLoaded(w, h); + emit calibrationUpdated(m_mmPerPixel); + emit measurementUpdated(measuredPx(), measuredMm()); + + update(); +} + + + +double MeasurementWidget::measuredPx() const +{ + if (!m_hasLine) return 0.0; + return QLineF(m_head, m_tail).length(); +} + +double MeasurementWidget::measuredMm() const +{ + if (m_hasHomography) { + QPointF h = m_head; + QPointF t = m_tail; + + if (m_hasLensCalibration) { + std::vector src = { cv::Point2f(m_head.x(), m_head.y()), cv::Point2f(m_tail.x(), m_tail.y()) }; + std::vector dst; + cv::undistortPoints(src, dst, m_cameraMatrix, m_distCoeffs, cv::Mat(), m_cameraMatrix); + h = QPointF(dst[0].x, dst[0].y); + t = QPointF(dst[1].x, dst[1].y); + } + + QPointF headReal = m_homography.map(h); + QPointF tailReal = m_homography.map(t); + return QLineF(headReal, tailReal).length(); + } + return measuredPx() * m_mmPerPixel; +} + +void MeasurementWidget::resetLine() +{ + if (!m_image.isNull()) { + int w = m_image.width(); + int h = m_image.height(); + m_head = QPointF(w * 0.3, h * 0.5); + m_tail = QPointF(w * 0.7, h * 0.5); + m_hasLine = true; + emit measurementUpdated(measuredPx(), measuredMm()); + update(); + } +} + +void MeasurementWidget::setLinePoints(const QPointF &head, const QPointF &tail) +{ + m_head = head; + m_tail = tail; + m_hasLine = true; + emit measurementUpdated(measuredPx(), measuredMm()); + update(); +} + +void MeasurementWidget::paintGL() +{ + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + if (m_image.isNull()) return; + + makeCurrent(); + + if (m_textureNeedsUpdate) { + if (m_texture) delete m_texture; + m_texture = new QOpenGLTexture(m_image); + m_texture->setMinificationFilter(QOpenGLTexture::Linear); + m_texture->setMagnificationFilter(QOpenGLTexture::Linear); + m_textureNeedsUpdate = false; + } + + // Render 2D image via QPainter over OpenGL viewport + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setRenderHint(QPainter::TextAntialiasing, true); + painter.setRenderHint(QPainter::SmoothPixmapTransform, true); + + // Calculate image destination rectangle with zoom and pan + double widgetAspect = (double)width() / height(); + double imageAspect = (double)m_image.width() / m_image.height(); + + double drawW, drawH; + if (widgetAspect > imageAspect) { + drawH = height() * m_zoomLevel; + drawW = drawH * imageAspect; + } else { + drawW = width() * m_zoomLevel; + drawH = drawW / imageAspect; + } + + double drawX = (width() - drawW) / 2.0 + m_panOffset.x(); + double drawY = (height() - drawH) / 2.0 + m_panOffset.y(); + + QRectF targetRect(drawX, drawY, drawW, drawH); + painter.drawImage(targetRect, m_image); + + // Draw the single measurement/calibration line + if (m_hasLine && !m_calibrationMode) { + QColor color(255, 45, 45); // Smooth Vivid Red + drawLineOverlay(painter, m_head, m_tail, color); + } + + if (m_calibrationMode) { + // Draw the 4 corners and connecting lines + QPolygonF wPoly; + for (const auto &p : m_calibCorners) { + wPoly << imageToWidgetCoords(p); + } + + // Draw lines connecting them to form a quad + QPen polyPen(QColor(0, 191, 255), 2.0, Qt::DashLine); // Deep Sky Blue + painter.setPen(polyPen); + painter.setBrush(Qt::NoBrush); + if (wPoly.size() == 4) { + painter.drawPolygon(wPoly); + } + + // Draw handles at each corner + double handleRadius = 8.0; + painter.setPen(QPen(QColor(0, 191, 255), 2.0)); + QStringList labels = {"1 (Sup-Izq)", "2 (Sup-Der)", "3 (Inf-Der)", "4 (Inf-Izq)"}; + for (int i = 0; i < wPoly.size(); ++i) { + if (i == m_activeCornerIdx) { + painter.setBrush(QBrush(QColor(255, 165, 0, 200))); // Orange when dragging + } else { + painter.setBrush(QBrush(QColor(0, 191, 255, 150))); + } + painter.drawEllipse(wPoly[i], handleRadius, handleRadius); + + // Draw label + painter.setFont(QFont("Arial", 8, QFont::Bold)); + painter.setPen(Qt::white); + painter.drawText(wPoly[i] + QPointF(12, 4), labels[i]); + painter.setPen(QPen(QColor(0, 191, 255), 2.0)); // Restore pen + } + } +} + +void MeasurementWidget::drawLineOverlay(QPainter &painter, const QPointF &imgHead, const QPointF &imgTail, + const QColor &color) +{ + QPointF wHead = imageToWidgetCoords(imgHead); + QPointF wTail = imageToWidgetCoords(imgTail); + + painter.setRenderHint(QPainter::Antialiasing, true); + + // Pure single red line (1.0px thin stroke with antialiasing) + QPen linePen(color, 1.0, Qt::SolidLine, Qt::RoundCap); + painter.setPen(linePen); + painter.drawLine(wHead, wTail); + + // Compact drag handles (short perpendicular ticks) + QLineF line(wHead, wTail); + double dx = line.dx(); + double dy = line.dy(); + double len = line.length(); + if (len > 0.0) { + double px = -dy / len; + double py = dx / len; + double tickHalfLen = 6.0; // short length perpendicular to measurement line + + QPointF headP1 = wHead + QPointF(px * tickHalfLen, py * tickHalfLen); + QPointF headP2 = wHead - QPointF(px * tickHalfLen, py * tickHalfLen); + painter.drawLine(headP1, headP2); + + QPointF tailP1 = wTail + QPointF(px * tickHalfLen, py * tickHalfLen); + QPointF tailP2 = wTail - QPointF(px * tickHalfLen, py * tickHalfLen); + painter.drawLine(tailP1, tailP2); + } + + // End labels + painter.setFont(QFont("Arial", 8, QFont::Bold)); + painter.setPen(Qt::white); + painter.drawText(wHead + QPointF(10, -6), "A"); + painter.drawText(wTail + QPointF(10, -6), "B"); +} + +QPointF MeasurementWidget::widgetToImageCoords(const QPointF &widgetPos) const +{ + if (m_image.isNull()) return QPointF(); + + double widgetAspect = (double)width() / height(); + double imageAspect = (double)m_image.width() / m_image.height(); + + double drawW, drawH; + if (widgetAspect > imageAspect) { + drawH = height() * m_zoomLevel; + drawW = drawH * imageAspect; + } else { + drawW = width() * m_zoomLevel; + drawH = drawW / imageAspect; + } + + double drawX = (width() - drawW) / 2.0 + m_panOffset.x(); + double drawY = (height() - drawH) / 2.0 + m_panOffset.y(); + + double normX = (widgetPos.x() - drawX) / drawW; + double normY = (widgetPos.y() - drawY) / drawH; + + return QPointF(normX * m_image.width(), normY * m_image.height()); +} + +QPointF MeasurementWidget::imageToWidgetCoords(const QPointF &imgPos) const +{ + if (m_image.isNull()) return QPointF(); + + double widgetAspect = (double)width() / height(); + double imageAspect = (double)m_image.width() / m_image.height(); + + double drawW, drawH; + if (widgetAspect > imageAspect) { + drawH = height() * m_zoomLevel; + drawW = drawH * imageAspect; + } else { + drawW = width() * m_zoomLevel; + drawH = drawW / imageAspect; + } + + double drawX = (width() - drawW) / 2.0 + m_panOffset.x(); + double drawY = (height() - drawH) / 2.0 + m_panOffset.y(); + + double normX = imgPos.x() / m_image.width(); + double normY = imgPos.y() / m_image.height(); + + return QPointF(drawX + normX * drawW, drawY + normY * drawH); +} + +MeasurementWidget::DragHandle MeasurementWidget::hitTestLine(const QPointF &imgPos, const QPointF &head, const QPointF &tail) const +{ + QPointF wPos = imageToWidgetCoords(imgPos); + QPointF wHead = imageToWidgetCoords(head); + QPointF wTail = imageToWidgetCoords(tail); + + const double handleRadius = 15.0; // Hit test tolerance in pixels + + if (QLineF(wPos, wHead).length() <= handleRadius) return DragHandle::Head; + if (QLineF(wPos, wTail).length() <= handleRadius) return DragHandle::Tail; + + // Check if clicked near line body + QLineF line(wHead, wTail); + double lineLen = line.length(); + if (lineLen > 0) { + double u = ((wPos.x() - wHead.x()) * (wTail.x() - wHead.x()) + (wPos.y() - wHead.y()) * (wTail.y() - wHead.y())) / (lineLen * lineLen); + if (u >= 0.0 && u <= 1.0) { + QPointF proj = wHead + u * (wTail - wHead); + if (QLineF(wPos, proj).length() <= handleRadius) { + return DragHandle::Body; + } + } + } + + return DragHandle::None; +} + +void MeasurementWidget::mousePressEvent(QMouseEvent *event) +{ + if (m_image.isNull()) return; + + if (m_calibrationMode) { + if (event->button() == Qt::LeftButton) { + QPointF imgPos = widgetToImageCoords(event->position()); + QPointF wPos = event->position(); + m_activeCornerIdx = -1; + const double handleRadius = 15.0; + for (int i = 0; i < m_calibCorners.size(); ++i) { + QPointF wCorner = imageToWidgetCoords(m_calibCorners[i]); + if (QLineF(wPos, wCorner).length() <= handleRadius) { + m_activeCornerIdx = i; + m_dragStartImgPos = imgPos; + update(); + break; + } + } + if (m_activeCornerIdx == -1) { + m_isPanning = true; + m_lastMousePos = event->pos(); + setCursor(Qt::ClosedHandCursor); + } + return; + } + if (event->button() == Qt::RightButton) { + m_isPanning = true; + m_lastMousePos = event->pos(); + setCursor(Qt::ClosedHandCursor); + return; + } + return; + } + + if (event->button() == Qt::LeftButton) { + QPointF imgPos = widgetToImageCoords(event->position()); + m_activeHandle = hitTestLine(imgPos, m_head, m_tail); + if (m_activeHandle != DragHandle::None) { + m_dragStartImgPos = imgPos; + m_dragStartHead = m_head; + m_dragStartTail = m_tail; + update(); + } else { + // Clicked background -> Pan image on left click drag + m_isPanning = true; + m_lastMousePos = event->pos(); + setCursor(Qt::ClosedHandCursor); + } + return; + } + + if (event->button() == Qt::RightButton) { + m_isPanning = true; + m_lastMousePos = event->pos(); + setCursor(Qt::ClosedHandCursor); + return; + } +} + +void MeasurementWidget::mouseMoveEvent(QMouseEvent *event) +{ + if (m_isPanning) { + QPoint delta = event->pos() - m_lastMousePos; + m_panOffset += QPointF(delta.x(), delta.y()); + m_lastMousePos = event->pos(); + update(); + return; + } + + QPointF imgPos = widgetToImageCoords(event->position()); + + if (m_calibrationMode) { + if (m_activeCornerIdx != -1) { + double x = qBound(0.0, imgPos.x(), (double)m_image.width()); + double y = qBound(0.0, imgPos.y(), (double)m_image.height()); + m_calibCorners[m_activeCornerIdx] = QPointF(x, y); + update(); + return; + } + // Update cursor on hover + QPointF wPos = event->position(); + bool overCorner = false; + const double handleRadius = 15.0; + for (int i = 0; i < m_calibCorners.size(); ++i) { + QPointF wCorner = imageToWidgetCoords(m_calibCorners[i]); + if (QLineF(wPos, wCorner).length() <= handleRadius) { + overCorner = true; + break; + } + } + if (overCorner) { + setCursor(Qt::PointingHandCursor); + } else { + setCursor(Qt::OpenHandCursor); + } + return; + } + + if (m_activeHandle != DragHandle::None) { + if (m_activeHandle == DragHandle::Head) { + m_head = imgPos; + } else if (m_activeHandle == DragHandle::Tail) { + m_tail = imgPos; + } else if (m_activeHandle == DragHandle::Body) { + QPointF delta = imgPos - m_dragStartImgPos; + m_head = m_dragStartHead + delta; + m_tail = m_dragStartTail + delta; + } + emit measurementUpdated(measuredPx(), measuredMm()); + update(); + return; + } + + // Update cursor feedback based on hover + DragHandle hover = hitTestLine(imgPos, m_head, m_tail); + if (hover == DragHandle::Head || hover == DragHandle::Tail) { + setCursor(Qt::CrossCursor); + } else if (hover == DragHandle::Body) { + setCursor(Qt::SizeAllCursor); + } else { + setCursor(Qt::OpenHandCursor); + } +} + +void MeasurementWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_calibrationMode) { + if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) { + m_isPanning = false; + setCursor(Qt::OpenHandCursor); + } + if (event->button() == Qt::LeftButton && m_activeCornerIdx != -1) { + m_activeCornerIdx = -1; + setCursor(Qt::OpenHandCursor); + } + return; + } + + if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) { + m_isPanning = false; + QPointF imgPos = widgetToImageCoords(event->position()); + DragHandle hover = hitTestLine(imgPos, m_head, m_tail); + if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor); + else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor); + else setCursor(Qt::OpenHandCursor); + } + + if (event->button() == Qt::LeftButton && m_activeHandle != DragHandle::None) { + m_activeHandle = DragHandle::None; + QPointF imgPos = widgetToImageCoords(event->position()); + DragHandle hover = hitTestLine(imgPos, m_head, m_tail); + if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor); + else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor); + else setCursor(Qt::OpenHandCursor); + } +} + +void MeasurementWidget::wheelEvent(QWheelEvent *event) +{ + double zoomFactor = 1.15; + if (event->angleDelta().y() < 0) { + zoomFactor = 1.0 / zoomFactor; + } + + double newZoom = m_zoomLevel * zoomFactor; + if (newZoom < 0.2) newZoom = 0.2; + if (newZoom > 25.0) newZoom = 25.0; + + QPointF mousePos = event->position(); + QPointF imgPosBefore = widgetToImageCoords(mousePos); + + m_zoomLevel = newZoom; + + QPointF widgetPosAfter = imageToWidgetCoords(imgPosBefore); + m_panOffset += (mousePos - widgetPosAfter); + + update(); +} + +void MeasurementWidget::setCalibrationMode(bool active) +{ + if (m_image.isNull()) return; + m_calibrationMode = active; + if (active && m_calibCorners.isEmpty()) { + double w = m_image.width(); + double h = m_image.height(); + m_calibCorners.clear(); + m_calibCorners.append(QPointF(w * 0.1, h * 0.1)); + m_calibCorners.append(QPointF(w * 0.9, h * 0.1)); + m_calibCorners.append(QPointF(w * 0.9, h * 0.9)); + m_calibCorners.append(QPointF(w * 0.1, h * 0.9)); + } + update(); +} + +void MeasurementWidget::cancelCalibrationMode() +{ + m_calibrationMode = false; + m_activeCornerIdx = -1; + update(); +} + +void MeasurementWidget::applyPerspectiveCalibration(double realX, double realY) +{ + if (m_image.isNull() || m_calibCorners.size() != 4) return; + + QPolygonF srcQuad; + srcQuad << m_calibCorners[0] << m_calibCorners[1] << m_calibCorners[2] << m_calibCorners[3]; + + QPolygonF destQuad; + destQuad << QPointF(0, 0) << QPointF(realX, 0) << QPointF(realX, realY) << QPointF(0, realY); + + if (QTransform::quadToQuad(srcQuad, destQuad, m_homography)) { + m_hasHomography = true; + m_isCalibrated = true; + + m_calibrationMode = false; + m_activeCornerIdx = -1; + + // Calculate a representative/average scale factor for compatibility + double imgW = QLineF(m_calibCorners[0], m_calibCorners[1]).length(); + if (imgW > 0) { + m_mmPerPixel = realX / imgW; + } + + emit calibrationUpdated(m_mmPerPixel); + emit measurementUpdated(measuredPx(), measuredMm()); + update(); + } +} + +void MeasurementWidget::resetCalibration() +{ + m_mmPerPixel = 0.0; + m_isCalibrated = false; + m_hasHomography = false; + m_hasLensCalibration = false; + m_calibrationMode = false; + m_activeCornerIdx = -1; + m_calibCorners.clear(); + + emit calibrationUpdated(m_mmPerPixel); + emit measurementUpdated(measuredPx(), measuredMm()); + update(); +} + +bool MeasurementWidget::calibrateWithChessboard(int &innerCornersX, int &innerCornersY, double squareSizeMm) +{ + if (m_image.isNull()) return false; + + // Convert QImage to cv::Mat + cv::Mat mat(m_image.height(), m_image.width(), CV_8UC4, + const_cast(m_image.bits()), + m_image.bytesPerLine()); + + cv::Mat gray; + cv::cvtColor(mat, gray, cv::COLOR_RGBA2GRAY); + + // If the image is very high resolution, resize it for the initial corner detection search + // to reduce high-frequency printer noise/banding and speed up detection significantly (using 600px width). + cv::Mat graySearch = gray; + double scale = 1.0; + if (gray.cols > 600) { + scale = 600.0 / gray.cols; + int newHeight = static_cast(gray.rows * scale); + cv::resize(gray, graySearch, cv::Size(600, newHeight), 0, 0, cv::INTER_AREA); + } + + std::vector searchCorners; + bool found = false; + int bestX = 0; + int bestY = 0; + + if (innerCornersX > 0 && innerCornersY > 0) { + // Try user specified dimensions + cv::Size patternSize(innerCornersX, innerCornersY); + found = cv::findChessboardCorners(graySearch, patternSize, searchCorners, + cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); + if (!found) { + cv::Mat graySearchInv = 255 - graySearch; + found = cv::findChessboardCorners(graySearchInv, patternSize, searchCorners, + cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); + } + if (found) { + bestX = innerCornersX; + bestY = innerCornersY; + } + } else { + // Auto-detect size: scan common sizes from largest to smallest (10 down to 4) + int totalSteps = (10 - 4 + 1) * (10 - 4 + 1); + int step = 0; + for (int cx = 10; cx >= 4; --cx) { + for (int cy = 10; cy >= 4; --cy) { + step++; + emit calibrationProgress(step * 100 / totalSteps); + QCoreApplication::processEvents(); + QThread::msleep(8); // Tiny sleep for smooth visual progress bar + + cv::Size patternSize(cx, cy); + found = cv::findChessboardCorners(graySearch, patternSize, searchCorners, + cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); + if (!found) { + cv::Mat graySearchInv = 255 - graySearch; + found = cv::findChessboardCorners(graySearchInv, patternSize, searchCorners, + cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE); + } + if (found) { + bestX = cx; + bestY = cy; + emit calibrationProgress(100); + QCoreApplication::processEvents(); + break; + } + } + if (found) break; + } + } + + if (!found) { + return false; + } + + // Assign detected dimensions back to reference variables to update UI + innerCornersX = bestX; + innerCornersY = bestY; + + // Scale corner coordinates back to the original image resolution + std::vector corners; + for (const auto &p : searchCorners) { + corners.push_back(cv::Point2f(p.x / scale, p.y / scale)); + } + + // Refine corner positions on the original high-res image + cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1), + cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1)); + + // Prepare calibration points + std::vector> objectPoints; + std::vector> imagePoints; + + std::vector obj; + for (int i = 0; i < innerCornersY; ++i) { + for (int j = 0; j < innerCornersX; ++j) { + obj.push_back(cv::Point3f(j * squareSizeMm, i * squareSizeMm, 0.0f)); + } + } + + objectPoints.push_back(obj); + imagePoints.push_back(corners); + + // Calibrate camera + m_cameraMatrix = cv::Mat::eye(3, 3, CV_64F); + m_distCoeffs = cv::Mat::zeros(5, 1, CV_64F); + std::vector rvecs, tvecs; + + double rms = cv::calibrateCamera(objectPoints, imagePoints, + cv::Size(m_image.width(), m_image.height()), + m_cameraMatrix, m_distCoeffs, rvecs, tvecs, + cv::CALIB_FIX_K3); + + qDebug() << "Calibration RMS error:" << rms; + + // Extreme corners: + // Top-Left (index 0) -> maps to (0, 0) + // Top-Right (index innerCornersX - 1) -> maps to ((innerCornersX-1)*sq, 0) + // Bottom-Right (index innerCornersX * innerCornersY - 1) -> maps to ((innerCornersX-1)*sq, (innerCornersY-1)*sq) + // Bottom-Left (index innerCornersX * (innerCornersY - 1)) -> maps to (0, (innerCornersY-1)*sq) + + std::vector srcPointsDistorted = { + corners[0], + corners[innerCornersX - 1], + corners[innerCornersX * innerCornersY - 1], + corners[innerCornersX * (innerCornersY - 1)] + }; + + // Undistort these 4 points + std::vector srcPointsUndistorted; + cv::undistortPoints(srcPointsDistorted, srcPointsUndistorted, m_cameraMatrix, m_distCoeffs, cv::Mat(), m_cameraMatrix); + + QPolygonF srcQuad; + srcQuad << QPointF(srcPointsUndistorted[0].x, srcPointsUndistorted[0].y) + << QPointF(srcPointsUndistorted[1].x, srcPointsUndistorted[1].y) + << QPointF(srcPointsUndistorted[2].x, srcPointsUndistorted[2].y) + << QPointF(srcPointsUndistorted[3].x, srcPointsUndistorted[3].y); + + double realX = (innerCornersX - 1) * squareSizeMm; + double realY = (innerCornersY - 1) * squareSizeMm; + + QPolygonF destQuad; + destQuad << QPointF(0, 0) << QPointF(realX, 0) << QPointF(realX, realY) << QPointF(0, realY); + + if (QTransform::quadToQuad(srcQuad, destQuad, m_homography)) { + m_hasHomography = true; + m_hasLensCalibration = true; + m_isCalibrated = true; + + m_calibrationMode = false; + m_activeCornerIdx = -1; + + // Compute average scale + double imgW = QLineF(srcQuad[0], srcQuad[1]).length(); + if (imgW > 0) { + m_mmPerPixel = realX / imgW; + } + + emit calibrationUpdated(m_mmPerPixel); + emit measurementUpdated(measuredPx(), measuredMm()); + update(); + return true; + } + + return false; +} diff --git a/weld_opengl_widget.h b/measurement_widget.h similarity index 66% rename from weld_opengl_widget.h rename to measurement_widget.h index 0df0fc0..41e8fe1 100644 --- a/weld_opengl_widget.h +++ b/measurement_widget.h @@ -1,5 +1,5 @@ -#ifndef WELD_OPENGL_WIDGET_H -#define WELD_OPENGL_WIDGET_H +#ifndef MEASUREMENT_WIDGET_H +#define MEASUREMENT_WIDGET_H #include #include @@ -7,8 +7,9 @@ #include #include #include +#include -class WeldOpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions +class MeasurementWidget : public QOpenGLWidget, protected QOpenGLFunctions { Q_OBJECT @@ -20,13 +21,12 @@ public: Body }; - explicit WeldOpenGLWidget(QWidget *parent = nullptr); - ~WeldOpenGLWidget() override; + explicit MeasurementWidget(QWidget *parent = nullptr); + ~MeasurementWidget() override; bool loadImage(const QString &filePath); void setImage(const QImage &image); - void calibrateWithCurrentLine(double knownMm); bool isCalibrated() const { return m_isCalibrated; } double mmPerPixel() const { return m_mmPerPixel; } double measuredPx() const; @@ -35,10 +35,18 @@ public: void resetLine(); void setLinePoints(const QPointF &head, const QPointF &tail); + void setCalibrationMode(bool active); + bool isCalibrationMode() const { return m_calibrationMode; } + void applyPerspectiveCalibration(double realX, double realY); + void cancelCalibrationMode(); + void resetCalibration(); + bool calibrateWithChessboard(int &innerCornersX, int &innerCornersY, double squareSizeMm); + signals: void calibrationUpdated(double mmPerPx); void measurementUpdated(double px, double mm); void imageLoaded(int width, int height); + void calibrationProgress(int percent); protected: void initializeGL() override; @@ -71,6 +79,20 @@ private: double m_mmPerPixel = 0.0; bool m_isCalibrated = false; + // Homography matrix for perspective calibration + QTransform m_homography; + bool m_hasHomography = false; + + // Lens Calibration Matrix & Coefficients + cv::Mat m_cameraMatrix; + cv::Mat m_distCoeffs; + bool m_hasLensCalibration = false; + + // Perspective Calibration state + bool m_calibrationMode = false; + QList m_calibCorners; + int m_activeCornerIdx = -1; + // Mouse interaction state DragHandle m_activeHandle = DragHandle::None; QPointF m_dragStartImgPos; @@ -84,4 +106,4 @@ private: QPoint m_lastMousePos; }; -#endif // WELD_OPENGL_WIDGET_H +#endif // MEASUREMENT_WIDGET_H diff --git a/weld_opengl_widget.cpp b/weld_opengl_widget.cpp deleted file mode 100644 index 585eb34..0000000 --- a/weld_opengl_widget.cpp +++ /dev/null @@ -1,387 +0,0 @@ -#include "weld_opengl_widget.h" -#include -#include -#include -#include -#include - -WeldOpenGLWidget::WeldOpenGLWidget(QWidget *parent) - : QOpenGLWidget(parent) -{ - setMouseTracking(true); -} - -WeldOpenGLWidget::~WeldOpenGLWidget() -{ - makeCurrent(); - if (m_texture) { - delete m_texture; - m_texture = nullptr; - } - doneCurrent(); -} - -void WeldOpenGLWidget::initializeGL() -{ - initializeOpenGLFunctions(); - glClearColor(0.15f, 0.15f, 0.18f, 1.0f); -} - -void WeldOpenGLWidget::resizeGL(int w, int h) -{ - glViewport(0, 0, w, h); -} - -bool WeldOpenGLWidget::loadImage(const QString &filePath) -{ - QImage img(filePath); - if (img.isNull()) return false; - - setImage(img); - return true; -} - -void WeldOpenGLWidget::setImage(const QImage &image) -{ - m_image = image.convertToFormat(QImage::Format_RGBA8888); - m_textureNeedsUpdate = true; - - // Reset zoom and center image - m_zoomLevel = 1.0; - m_panOffset = QPointF(0, 0); - - // Initialize single measurement/calibration line - int w = m_image.width(); - int h = m_image.height(); - - m_head = QPointF(w * 0.3, h * 0.5); - m_tail = QPointF(w * 0.7, h * 0.5); - m_hasLine = true; - - emit imageLoaded(w, h); - emit calibrationUpdated(m_mmPerPixel); - emit measurementUpdated(measuredPx(), measuredMm()); - - update(); -} - -void WeldOpenGLWidget::calibrateWithCurrentLine(double knownMm) -{ - double px = measuredPx(); - if (px > 0.0 && knownMm > 0.0) { - m_mmPerPixel = knownMm / px; - m_isCalibrated = true; - } - emit calibrationUpdated(m_mmPerPixel); - emit measurementUpdated(measuredPx(), measuredMm()); - update(); -} - -double WeldOpenGLWidget::measuredPx() const -{ - if (!m_hasLine) return 0.0; - return QLineF(m_head, m_tail).length(); -} - -double WeldOpenGLWidget::measuredMm() const -{ - return measuredPx() * m_mmPerPixel; -} - -void WeldOpenGLWidget::resetLine() -{ - if (!m_image.isNull()) { - int w = m_image.width(); - int h = m_image.height(); - m_head = QPointF(w * 0.3, h * 0.5); - m_tail = QPointF(w * 0.7, h * 0.5); - m_hasLine = true; - emit measurementUpdated(measuredPx(), measuredMm()); - update(); - } -} - -void WeldOpenGLWidget::setLinePoints(const QPointF &head, const QPointF &tail) -{ - m_head = head; - m_tail = tail; - m_hasLine = true; - emit measurementUpdated(measuredPx(), measuredMm()); - update(); -} - -void WeldOpenGLWidget::paintGL() -{ - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - if (m_image.isNull()) return; - - makeCurrent(); - - if (m_textureNeedsUpdate) { - if (m_texture) delete m_texture; - m_texture = new QOpenGLTexture(m_image); - m_texture->setMinificationFilter(QOpenGLTexture::Linear); - m_texture->setMagnificationFilter(QOpenGLTexture::Linear); - m_textureNeedsUpdate = false; - } - - // Render 2D image via QPainter over OpenGL viewport - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::TextAntialiasing, true); - painter.setRenderHint(QPainter::SmoothPixmapTransform, true); - - // Calculate image destination rectangle with zoom and pan - double widgetAspect = (double)width() / height(); - double imageAspect = (double)m_image.width() / m_image.height(); - - double drawW, drawH; - if (widgetAspect > imageAspect) { - drawH = height() * m_zoomLevel; - drawW = drawH * imageAspect; - } else { - drawW = width() * m_zoomLevel; - drawH = drawW / imageAspect; - } - - double drawX = (width() - drawW) / 2.0 + m_panOffset.x(); - double drawY = (height() - drawH) / 2.0 + m_panOffset.y(); - - QRectF targetRect(drawX, drawY, drawW, drawH); - painter.drawImage(targetRect, m_image); - - // Draw the single measurement/calibration line - if (m_hasLine) { - QColor color(255, 45, 45); // Smooth Vivid Red - drawLineOverlay(painter, m_head, m_tail, color); - } - - -} - -void WeldOpenGLWidget::drawLineOverlay(QPainter &painter, const QPointF &imgHead, const QPointF &imgTail, - const QColor &color) -{ - QPointF wHead = imageToWidgetCoords(imgHead); - QPointF wTail = imageToWidgetCoords(imgTail); - - painter.setRenderHint(QPainter::Antialiasing, true); - - // Pure single red line (1.0px thin stroke with antialiasing) - QPen linePen(color, 1.0, Qt::SolidLine, Qt::RoundCap); - painter.setPen(linePen); - painter.drawLine(wHead, wTail); - - // Compact drag handles (short perpendicular ticks) - QLineF line(wHead, wTail); - double dx = line.dx(); - double dy = line.dy(); - double len = line.length(); - if (len > 0.0) { - double px = -dy / len; - double py = dx / len; - double tickHalfLen = 6.0; // short length perpendicular to measurement line - - QPointF headP1 = wHead + QPointF(px * tickHalfLen, py * tickHalfLen); - QPointF headP2 = wHead - QPointF(px * tickHalfLen, py * tickHalfLen); - painter.drawLine(headP1, headP2); - - QPointF tailP1 = wTail + QPointF(px * tickHalfLen, py * tickHalfLen); - QPointF tailP2 = wTail - QPointF(px * tickHalfLen, py * tickHalfLen); - painter.drawLine(tailP1, tailP2); - } - - // End labels - painter.setFont(QFont("Arial", 8, QFont::Bold)); - painter.setPen(Qt::white); - painter.drawText(wHead + QPointF(10, -6), "A"); - painter.drawText(wTail + QPointF(10, -6), "B"); -} - -QPointF WeldOpenGLWidget::widgetToImageCoords(const QPointF &widgetPos) const -{ - if (m_image.isNull()) return QPointF(); - - double widgetAspect = (double)width() / height(); - double imageAspect = (double)m_image.width() / m_image.height(); - - double drawW, drawH; - if (widgetAspect > imageAspect) { - drawH = height() * m_zoomLevel; - drawW = drawH * imageAspect; - } else { - drawW = width() * m_zoomLevel; - drawH = drawW / imageAspect; - } - - double drawX = (width() - drawW) / 2.0 + m_panOffset.x(); - double drawY = (height() - drawH) / 2.0 + m_panOffset.y(); - - double normX = (widgetPos.x() - drawX) / drawW; - double normY = (widgetPos.y() - drawY) / drawH; - - return QPointF(normX * m_image.width(), normY * m_image.height()); -} - -QPointF WeldOpenGLWidget::imageToWidgetCoords(const QPointF &imgPos) const -{ - if (m_image.isNull()) return QPointF(); - - double widgetAspect = (double)width() / height(); - double imageAspect = (double)m_image.width() / m_image.height(); - - double drawW, drawH; - if (widgetAspect > imageAspect) { - drawH = height() * m_zoomLevel; - drawW = drawH * imageAspect; - } else { - drawW = width() * m_zoomLevel; - drawH = drawW / imageAspect; - } - - double drawX = (width() - drawW) / 2.0 + m_panOffset.x(); - double drawY = (height() - drawH) / 2.0 + m_panOffset.y(); - - double normX = imgPos.x() / m_image.width(); - double normY = imgPos.y() / m_image.height(); - - return QPointF(drawX + normX * drawW, drawY + normY * drawH); -} - -WeldOpenGLWidget::DragHandle WeldOpenGLWidget::hitTestLine(const QPointF &imgPos, const QPointF &head, const QPointF &tail) const -{ - QPointF wPos = imageToWidgetCoords(imgPos); - QPointF wHead = imageToWidgetCoords(head); - QPointF wTail = imageToWidgetCoords(tail); - - const double handleRadius = 15.0; // Hit test tolerance in pixels - - if (QLineF(wPos, wHead).length() <= handleRadius) return DragHandle::Head; - if (QLineF(wPos, wTail).length() <= handleRadius) return DragHandle::Tail; - - // Check if clicked near line body - QLineF line(wHead, wTail); - double lineLen = line.length(); - if (lineLen > 0) { - double u = ((wPos.x() - wHead.x()) * (wTail.x() - wHead.x()) + (wPos.y() - wHead.y()) * (wTail.y() - wHead.y())) / (lineLen * lineLen); - if (u >= 0.0 && u <= 1.0) { - QPointF proj = wHead + u * (wTail - wHead); - if (QLineF(wPos, proj).length() <= handleRadius) { - return DragHandle::Body; - } - } - } - - return DragHandle::None; -} - -void WeldOpenGLWidget::mousePressEvent(QMouseEvent *event) -{ - if (m_image.isNull()) return; - - if (event->button() == Qt::LeftButton) { - QPointF imgPos = widgetToImageCoords(event->position()); - m_activeHandle = hitTestLine(imgPos, m_head, m_tail); - if (m_activeHandle != DragHandle::None) { - m_dragStartImgPos = imgPos; - m_dragStartHead = m_head; - m_dragStartTail = m_tail; - update(); - } else { - // Clicked background -> Pan image on left click drag - m_isPanning = true; - m_lastMousePos = event->pos(); - setCursor(Qt::ClosedHandCursor); - } - return; - } - - if (event->button() == Qt::RightButton) { - m_isPanning = true; - m_lastMousePos = event->pos(); - setCursor(Qt::ClosedHandCursor); - return; - } -} - -void WeldOpenGLWidget::mouseMoveEvent(QMouseEvent *event) -{ - if (m_isPanning) { - QPoint delta = event->pos() - m_lastMousePos; - m_panOffset += QPointF(delta.x(), delta.y()); - m_lastMousePos = event->pos(); - update(); - return; - } - - QPointF imgPos = widgetToImageCoords(event->position()); - - if (m_activeHandle != DragHandle::None) { - if (m_activeHandle == DragHandle::Head) { - m_head = imgPos; - } else if (m_activeHandle == DragHandle::Tail) { - m_tail = imgPos; - } else if (m_activeHandle == DragHandle::Body) { - QPointF delta = imgPos - m_dragStartImgPos; - m_head = m_dragStartHead + delta; - m_tail = m_dragStartTail + delta; - } - emit measurementUpdated(measuredPx(), measuredMm()); - update(); - return; - } - - // Update cursor feedback based on hover - DragHandle hover = hitTestLine(imgPos, m_head, m_tail); - if (hover == DragHandle::Head || hover == DragHandle::Tail) { - setCursor(Qt::CrossCursor); - } else if (hover == DragHandle::Body) { - setCursor(Qt::SizeAllCursor); - } else { - setCursor(Qt::OpenHandCursor); - } -} - -void WeldOpenGLWidget::mouseReleaseEvent(QMouseEvent *event) -{ - if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) { - m_isPanning = false; - QPointF imgPos = widgetToImageCoords(event->position()); - DragHandle hover = hitTestLine(imgPos, m_head, m_tail); - if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor); - else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor); - else setCursor(Qt::OpenHandCursor); - } - - if (event->button() == Qt::LeftButton && m_activeHandle != DragHandle::None) { - m_activeHandle = DragHandle::None; - QPointF imgPos = widgetToImageCoords(event->position()); - DragHandle hover = hitTestLine(imgPos, m_head, m_tail); - if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor); - else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor); - else setCursor(Qt::OpenHandCursor); - } -} - -void WeldOpenGLWidget::wheelEvent(QWheelEvent *event) -{ - double zoomFactor = 1.15; - if (event->angleDelta().y() < 0) { - zoomFactor = 1.0 / zoomFactor; - } - - double newZoom = m_zoomLevel * zoomFactor; - if (newZoom < 0.2) newZoom = 0.2; - if (newZoom > 25.0) newZoom = 25.0; - - QPointF mousePos = event->position(); - QPointF imgPosBefore = widgetToImageCoords(mousePos); - - m_zoomLevel = newZoom; - - QPointF widgetPosAfter = imageToWidgetCoords(imgPosBefore); - m_panOffset += (mousePos - widgetPosAfter); - - update(); -}