#include "Canvas.h" #define WIDGET_POSITION event->pos() #define UCS_POSITION screenToLogical(WIDGET_POSITION) std::set groups; // =================================================================== // Вспомогательные функции // =================================================================== /** * @brief Вычислить расстояние между двумя точками * @param p1 Первая точка * @param p2 Вторая точка * @return Расстояние между точками */ static double dist_P2P(QPointF p1, QPointF p2) { return sqrt(pow(p2.x() - p1.x(), 2) + pow(p2.y() - p1.y(), 2)); } /** * @brief Создать упорядоченную пару * * Создает пару, в которой элементы упорядочены по указателям, * чтобы pair(obj1, obj2) и pair(obj2, obj1) считались одинаковыми * * @tparam T Тип возвращаемой пары * @tparam A Тип первого элемента * @tparam B Тип второго элемента * @param obj1 Первый объект * @param obj2 Второй объект * @return Упорядоченная пара */ template T makeOrderedPair(A* obj1, B* obj2) { return (obj1 < obj2) ? std::make_pair(obj1, obj2) : std::make_pair(obj2, obj1); } // =================================================================== // Конструктор и деструктор // =================================================================== Canvas::Canvas(QWidget* parent) : QWidget(parent) { sys = System(); setMouseTracking(true); setBackgroundRole(QPalette::Base); setFocusPolicy(Qt::StrongFocus); setAutoFillBackground(true); } Canvas::~Canvas() { clearCanvas(); } // =================================================================== // Методы изменения режима и масштаба // =================================================================== void Canvas::changeMode(Mode _mode) { mode = _mode; } void Canvas::zoomIn() { scaleFactor += ZOOM_STEP; update(); } void Canvas::zoomOut() { scaleFactor -= ZOOM_STEP; if (scaleFactor < 0.1) // Минимальный масштаб 10% scaleFactor = 0.1; update(); } void Canvas::zoomReset() { scaleFactor = 1.0; update(); } // =================================================================== // Методы поиска и проверки // =================================================================== Curve* Canvas::findAt(QPointF& pos, qreal tolerance) { for (Curve* curve : curves) { if (Line* line = dynamic_cast(curve)) { if (line->contains(pos, tolerance)) { return line; } } else if (Circle* circle = dynamic_cast(curve)) { QPointF center(*circle->center.x, *circle->center.y); double radius = *circle->rad; double distToCenter = dist_P2P(center, pos); // Проверяем, лежит ли точка вблизи окружности (с допуском) if (std::abs(distToCenter - radius) <= tolerance) { return circle; } } } return nullptr; } Point* Canvas::findPointAt(QPointF pos, qreal tolerance) { Point* temp = nullptr; for (Curve* curve : curves) { if (Line* line = dynamic_cast(curve)) { QPointF p1(*line->p1.x, *line->p1.y); QPointF p2(*line->p2.x, *line->p2.y); if (dist_P2P(p1, pos) <= tolerance) temp = line->start_ref; if (dist_P2P(p2, pos) <= tolerance) temp = line->end_ref; } else if (Circle* circle = dynamic_cast(curve)) { QPointF center(circle->center.get_X(), circle->center.get_Y()); if (dist_P2P(center, pos) <= tolerance) temp = circle->center_ref; } } return temp; } bool Canvas::areCoincident(Point* p1, Point* p2) { return P2Ppairs.count(makeOrderedPair(p1, p2)); } bool Canvas::areHorizontalVertical(Point* p1, Point* p2, bool mode) { // mode = false - проверка горизонтальности // mode = true - проверка вертикальности if (!mode) return HORIZ_pairs.count(makeOrderedPair(p1, p2)); else return VERT_pairs.count(makeOrderedPair(p1, p2)); } bool Canvas::isLineHorizontal(Line* line) { if (!line || !line->start_ref || !line->end_ref) return false; return HORIZ_pairs.count(makeOrderedPair(line->start_ref, line->end_ref)); } bool Canvas::isLineVertical(Line* line) { if (!line || !line->start_ref || !line->end_ref) return false; return VERT_pairs.count(makeOrderedPair(line->start_ref, line->end_ref)); } bool Canvas::areAlreadyParallel(Line* l1, Line* l2) { return parallelPairs.count(makeOrderedPair(l1, l2)); } bool Canvas::areAlreadyPerpendicular(Line* line1, Line* line2) { return perpendicularPairs.count(makeOrderedPair(line1, line2)); } // =================================================================== // Методы работы с ограничениями // =================================================================== void Canvas::remove_constraint(int tag) { auto it = C_Info.find(tag); if (it == C_Info.end()) return; ConstraintInfo info = it->second; // Удаляем ограничение из солвера sys.clearByTag(tag); // Удаляем из соответствующих контейнеров в зависимости от типа ограничения switch (info.mode) { case Mode::Horizontal: { if (auto* pair = std::get_if(&info.data)) { HORIZ_pairs.erase(*pair); } break; } case Mode::Vertical: { if (auto* pair = std::get_if(&info.data)) { VERT_pairs.erase(*pair); } break; } case Mode::Parallel: { if (auto* pair = std::get_if(&info.data)) { parallelPairs.erase(*pair); } break; } case Mode::Coincedent: { if (auto* pair = std::get_if(&info.data)) { P2Ppairs.erase(*pair); } break; } case Mode::Perpendicular: { if (auto* pair = std::get_if(&info.data)) { perpendicularPairs.erase(*pair); } break; } default: break; } C_Info.erase(it); } // =================================================================== // Обработчики событий мыши // =================================================================== void Canvas::mousePressEvent(QMouseEvent* event) { QPointF scene = UCS_POSITION; #ifdef _DEBUG qDebug() << "Scene point in" << scene.x() << scene.y(); #endif // ====================== Режим None: перемещение объектов ====================== if (mode == Mode::None) { Point* p = findPointAt(scene); if (p) { draggedPoint = p; getCoincidentGroup(draggedPoint); dragOffset = scene - QPointF(*p->x, *p->y); return; } Line* found = dynamic_cast(findAt(scene)); if (found) { draggedCurve = found; QPointF lineCenter( (*found->p1.x + *found->p2.x) / 2.0, (*found->p1.y + *found->p2.y) / 2.0 ); dragOffset = scene - lineCenter; return; } Circle* found_circle = dynamic_cast(findAt(scene)); if (found_circle) { draggedCurve = found_circle; QPointF center(*found_circle->center.x, *found_circle->center.y); dragOffset = scene - center; return; } } // ====================== Режим Horizontal/Vertical ====================== else if (mode == Mode::Horizontal || mode == Mode::Vertical) { Line* found = dynamic_cast(findAt(scene)); if (found) { // Проверка: если линия уже вертикальна, нельзя сделать её горизонтальной if (mode == Mode::Horizontal && isLineVertical(found)) { QMessageBox::warning(this, QString("Невозможно"), QString("Эта линия уже вертикальна и не может быть горизонтальной!"), QMessageBox::Ok ); mode = Mode::None; return; } // Проверка: если линия уже горизонтальна, нельзя сделать её вертикальной if (mode == Mode::Vertical && isLineHorizontal(found)) { QMessageBox::warning(this, QString("Невозможно"), QString("Эта линия уже горизонтальна и не может быть вертикальной!"), QMessageBox::Ok ); mode = Mode::None; return; } if (mode == Mode::Horizontal) { sys.addConstraintHorizontal(*found, constraints_count++); auto pair = makeOrderedPair(found->start_ref, found->end_ref); HORIZ_pairs.insert(pair); C_Info[constraints_count - 1] = { Mode::Horizontal, pair }; } else { sys.addConstraintVertical(*found, constraints_count++); auto pair = makeOrderedPair(found->start_ref, found->end_ref); VERT_pairs.insert(pair); C_Info[constraints_count - 1] = { Mode::Vertical, pair }; } solve_for_canvas(); after_constraint = true; } mode = Mode::None; } // ====================== Режим DrawingLine: рисование линий ====================== else if (mode == Mode::DrawingLine) { if (!current_line) { // Первый клик: создаем новую линию current_line = new Line(); // Создаем координаты для точек линии double* x1 = new double(scene.x()); double* y1 = new double(scene.y()); double* x2 = new double(scene.x()); double* y2 = new double(scene.y()); // Добавляем параметры в солвер params.push_back(x1); params.push_back(y1); // Создаем точки и привязываем к линии points.push_back(new Point(x1, y1, obj_count++)); points.push_back(new Point(x2, y2, obj_count++)); current_line->p1.x = x1; current_line->p1.y = y1; current_line->p2.x = x2; current_line->p2.y = y2; current_line->start_ref = points[points.size() - 2]; current_line->end_ref = points[points.size() - 1]; } else { // Второй клик: завершаем рисование линии *current_line->p2.x = scene.x(); *current_line->p2.y = scene.y(); // Проверка минимальной длины линии double len = sqrt(pow(*current_line->p2.x - *current_line->p1.x, 2) + pow(*current_line->p2.y - *current_line->p1.y, 2)); if (len < 10) { // Линия слишком короткая - отменяем создание delete current_line; current_line = nullptr; // Удаляем созданные точки и параметры delete points.back(); points.pop_back(); delete points.back(); points.pop_back(); delete params.back(); params.pop_back(); delete params.back(); params.pop_back(); mode = Mode::None; QMessageBox::critical(this, "WHOOPS", "Sorry, your line is very short", QMessageBox::Ok); solve_for_canvas(); return; } // Добавляем координаты второй точки в параметры params.push_back(current_line->p2.x); params.push_back(current_line->p2.y); current_line->set_tag(obj_count++); // Завершаем создание линии curves.append(current_line); current_line = nullptr; mode = Mode::None; } solve_for_canvas(); return; } else if (mode == Mode::DrawingCircle) { if (!current_circle) { current_circle = new Circle(); // Создаем координаты для центра окружности double* x = new double(scene.x()); double* y = new double(scene.y()); points.push_back(new Point(x, y, obj_count++)); // Добавляем параметры в солвер params.push_back(x); params.push_back(y); current_circle->center.x = x; current_circle->center.y = y; current_circle->center_ref = points[points.size() - 1]; } else { double *r = new double(dist_P2P(QPointF(current_circle->center.get_X(), current_circle->center.get_Y()), QPointF(scene.x(), scene.y()))); current_circle->rad = r; params.push_back(r); curves.append(current_circle); current_circle->set_tag(obj_count++); current_circle = nullptr; mode = Mode::None; } solve_for_canvas(); return; } // ====================== Режим Parallel: задание параллельности ====================== else if (mode == Mode::Parallel) { Line* found = dynamic_cast(findAt(scene)); if (!found) { current_line = nullptr; solve_for_canvas(); return; } // Первый клик: выбираем первую линию if (!current_line) { current_line = found; solve_for_canvas(); return; } // Повторный клик на ту же линию: сброс выбора if (found == current_line) { current_line = nullptr; solve_for_canvas(); return; } // Второй клик на другую линию: добавляем ограничение параллельности if (!areAlreadyParallel(found, current_line)) { auto pair = makeOrderedPair(found, current_line); sys.addConstraintParallel(*found, *current_line, constraints_count++); parallelPairs.insert(pair); C_Info[constraints_count - 1] = { Mode::Parallel, pair }; current_line = nullptr; mode = Mode::None; after_constraint = true; solve_for_canvas(); } else { // Линии уже параллельны - сообщаем об ошибке #ifdef _DEBUG qDebug() << "Line" << current_line << "and" << found << "are parallel. Abort!"; #endif QMessageBox::warning(this, QString("Wrong"), QString("Parallel lines can not be more parallel!"), QMessageBox::Ok ); current_line = nullptr; solve_for_canvas(); return; } } // ====================== Режим Coincedent: совпадение точек ====================== else if (mode == Mode::Coincedent) { Point* clickedPoint = findPointAt(scene); if (!clickedPoint) { firstPoint = nullptr; solve_for_canvas(); return; } // Первый клик: выбираем первую точку if (!firstPoint) { firstPoint = clickedPoint; solve_for_canvas(); return; } // Повторный клик на ту же точку: сброс выбора if (clickedPoint == firstPoint) { firstPoint = nullptr; solve_for_canvas(); return; } // Находим линии, к которым принадлежат точки Line* l1 = nullptr; Line* l2 = nullptr; for (Curve* curve : curves) { if (Line* l = dynamic_cast(curve)) { if (l->start_ref == firstPoint || l->end_ref == firstPoint) l1 = l; if (l->start_ref == clickedPoint || l->end_ref == clickedPoint) l2 = l; } } // Проверка на невозможность ограничения if ((l1 == l2 && l1 && l2) || areCoincident(firstPoint, clickedPoint)) { QMessageBox::critical(this, QString("NO!"), QString("P2P failed")); firstPoint = nullptr; mode = Mode::None; solve_for_canvas(); return; } // Добавляем ограничение совпадения точек sys.addConstraintP2PCoincident(*clickedPoint, *firstPoint, constraints_count++); auto pair = makeOrderedPair(clickedPoint, firstPoint); P2Ppairs.insert(pair); C_Info[constraints_count - 1] = { Mode::Coincedent, pair }; firstPoint = nullptr; mode = Mode::None; after_constraint = true; solve_for_canvas(); return; } else if (mode == Mode::Perpendicular) { Line* found = dynamic_cast(findAt(scene)); if (!found) { current_line = nullptr; solve_for_canvas(); return; } // Первый клик: выбираем первую линию if (!current_line) { current_line = found; solve_for_canvas(); return; } // Повторный клик на ту же линию: сброс выбора if (found == current_line) { current_line = nullptr; solve_for_canvas(); return; } if (!areAlreadyPerpendicular(found, current_line)) { auto pair = makeOrderedPair(found, current_line); sys.addConstraintPerpendicular(*found, *current_line, constraints_count++); perpendicularPairs.insert(pair); C_Info[constraints_count - 1] = { Mode::Perpendicular, pair }; current_line = nullptr; mode = Mode::None; after_constraint = true; solve_for_canvas(); } else { QMessageBox::warning(this, QString("Wrong"), QString("Perpendicular lines can not be more perpendicular!"), QMessageBox::Ok ); current_line = nullptr; solve_for_canvas(); return; } } } void Canvas::mouseMoveEvent(QMouseEvent* event) { // ====================== Перемещение точки ====================== if (draggedPoint) { QPointF pos = UCS_POSITION - dragOffset; for (Point* pt : groups) { *pt->x = pos.x(); *pt->y = pos.y(); } for (Point* basePt : groups) { for (Point* other : points) { if (areHorizontalVertical(basePt, other, true)) { *other->x = pos.x(); } if (areHorizontalVertical(basePt, other, false)) { *other->y = pos.y(); } } } } // ====================== Перемещение линии ====================== else if (draggedCurve) { if (Line* draggedLine = dynamic_cast(draggedCurve)) { QPointF newCenter = UCS_POSITION - dragOffset; QPointF oldCenter( (*draggedLine->p1.x + *draggedLine->p2.x) / 2.0, (*draggedLine->p1.y + *draggedLine->p2.y) / 2.0 ); // Вычисляем смещение double dx = newCenter.x() - oldCenter.x(); double dy = newCenter.y() - oldCenter.y(); // Перемещаем обе точки линии Point* linePoints[2] = { draggedLine->start_ref, draggedLine->end_ref }; for (int i = 0; i < 2; i++) { Point* currentPoint = linePoints[i]; *currentPoint->x += dx; *currentPoint->y += dy; // Обновляем совпадающие точки for (Point* pair : points) { if (pair != currentPoint && areCoincident(currentPoint, pair)) { *pair->x = *currentPoint->x; *pair->y = *currentPoint->y; } } } } else if (Circle* circle = dynamic_cast(draggedCurve)) { QPointF newCenter = UCS_POSITION - dragOffset; *circle->center.x = newCenter.x(); *circle->center.y = newCenter.y(); *circle->center_ref->x = newCenter.x(); *circle->center_ref->y = newCenter.y(); // Также обновляем все совпадающие точки (если центр совпадает с другими точками через P2P) for (Point* other : points) { if (other != circle->center_ref && areCoincident(circle->center_ref, other)) { *other->x = newCenter.x(); *other->y = newCenter.y(); } } } } #ifdef _DEBUG else showObjectTag(WIDGET_POSITION); #endif solve_for_canvas(); } void Canvas::mouseReleaseEvent(QMouseEvent* event) { Q_UNUSED(event); if (draggedPoint) { draggedPoint = nullptr; solve_for_canvas(); } if (draggedCurve) { draggedCurve = nullptr; solve_for_canvas(); } groups.clear(); } // =================================================================== // Метод отрисовки // =================================================================== void Canvas::paintEvent(QPaintEvent* event) { Q_UNUSED(event); QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); p.translate(width() / 2.0, height() / 2.0); p.scale(scaleFactor, -scaleFactor); p.setPen(Qt::red); p.drawLine(-5, 0, 5, 0); p.drawLine(0, -5, 0, 5); // ====================== Отрисовка линий ====================== for (Curve* curve : curves) { if (Line* line = dynamic_cast(curve)) { bool isSelected = (mode == Mode::Parallel && line == current_line); // Настройка пера для линии QPen linePen = isSelected ? QPen(Qt::red, 4) : QPen(Qt::black, 2); p.setPen(linePen); // Рисуем линию p.drawLine( QPointF(*line->p1.x, *line->p1.y), QPointF(*line->p2.x, *line->p2.y) ); // Рисуем конечные точки линии QBrush pointBrush = isSelected ? QBrush(Qt::red) : QBrush(Qt::darkBlue); p.setBrush(pointBrush); p.setPen(Qt::NoPen); p.drawEllipse(QPointF(*line->p1.x, *line->p1.y), 5, 5); p.drawEllipse(QPointF(*line->p2.x, *line->p2.y), 5, 5); } else if (Circle* circle = dynamic_cast(curve)) { p.setPen(QPen(Qt::black, 2)); p.setBrush(Qt::NoBrush); p.drawEllipse(QPointF(*circle->center.x, *circle->center.y), *circle->rad, *circle->rad); p.setBrush(QBrush(Qt::darkBlue)); p.setPen(Qt::NoPen); p.drawEllipse(QPointF(*circle->center.x, *circle->center.y), 5, 5); p.setBrush(Qt::NoBrush); } } // ====================== Подсветка выбранной точки (режим Coincedent) ====================== if (mode == Mode::Coincedent && firstPoint) { QPointF pt(*firstPoint->x, *firstPoint->y); p.setPen(Qt::NoPen); p.setBrush(Qt::red); p.drawEllipse(pt, 12, 12); p.setBrush(Qt::white); p.drawEllipse(pt, 8, 8); p.setBrush(Qt::red); p.drawEllipse(pt, 4, 4); // маленький центр } // ====================== Отрисовка текущей линии (режим DrawingLine) ====================== if (current_line && mode == Mode::DrawingLine) { // Рисуем начальную точку текущей линии p.setBrush(Qt::blue); p.setPen(Qt::NoPen); p.drawEllipse(QPointF(*current_line->p1.x, *current_line->p1.y), 6, 6); } } // =================================================================== // Обработчики событий клавиатуры и колесика мыши // =================================================================== void Canvas::keyPressEvent(QKeyEvent* event) { switch (event->key()) { case Qt::Key_Delete: case Qt::Key_Clear: { QMessageBox::StandardButton reply; reply = QMessageBox::question(this, "Очистка canvas", "Удалить все объекты и ограничения?", QMessageBox::Yes | QMessageBox::No); if (reply == QMessageBox::Yes) { clearCanvas(); } break; } case Qt::Key_Escape: { // Отмена текущего действия if (mode == Mode::DrawingLine && current_line) { // Отмена рисования линии delete current_line; current_line = nullptr; // Удаляем созданные точки и параметры if (points.size() >= 2) { delete points.back(); points.pop_back(); delete points.back(); points.pop_back(); } if (params.size() >= 4) { delete params.back(); params.pop_back(); delete params.back(); params.pop_back(); delete params.back(); params.pop_back(); delete params.back(); params.pop_back(); } mode = Mode::None; update(); } else if (mode != Mode::None) { // Отмена любого другого режима mode = Mode::None; current_line = nullptr; firstPoint = nullptr; groups.clear(); update(); } break; } case Qt::Key_Z: { if (constraints_count > 0) { remove_constraint(constraints_count - 1); constraints_count--; solve_for_canvas(); } break; } default: QWidget::keyPressEvent(event); } } void Canvas::wheelEvent(QWheelEvent* event) { if (event->angleDelta().y() > 0) { zoomIn(); } else { zoomOut(); } event->accept(); } void Canvas::leaveEvent(QEvent* event) { Q_UNUSED(event); QToolTip::hideText(); } #ifdef _DEBUG void Canvas::showObjectTag(QPointF pos) { //QPointF l = screenToLogical(pos); //Line* lineUnderCursor = findAt(l, 1.0); //if (lineUnderCursor && lineUnderCursor != draggedLine) { // QPointF p1(*lineUnderCursor->p1.x, *lineUnderCursor->p1.y); // QPointF p2(*lineUnderCursor->p2.x, *lineUnderCursor->p2.y); // QLineF info_line(p1, p2); // QString Text = QString("Line %1\nLength = %2mm") // .arg(lineUnderCursor->get_tag() + 1) // .arg(info_line.length()); // QToolTip::showText(mapToGlobal(pos.toPoint()), Text, this); //} //else // QToolTip::hideText(); } #endif // =================================================================== // Вспомогательные методы // =================================================================== QPointF Canvas::screenToLogical(const QPointF& screenPos) const { QPointF logical = screenPos; logical.rx() -= width() / 2.0; logical.ry() -= height() / 2.0; logical.rx() /= scaleFactor; logical.ry() /= -scaleFactor; return logical; } void Canvas::getCoincidentGroup(Point* p) { for (Point* other : points) { if (other != p && areCoincident(p, other)) { groups.insert(other); break; } } for (size_t i = 0; i < points.size(); ++i) { for (size_t j = i + 1; j < points.size(); j++) { if (*points[i] == *points[j] && groups.contains(points[j])) { groups.insert(points[i]); } } } groups.insert(p); } void Canvas::solve_for_canvas() { bool flag = false; int res = sys.solve(params); if (res != SolveStatus::Success && res != SolveStatus::Converged) { flag = true; } else { sys.applySolution(); for (Curve* curve : curves) { if (Line* line = dynamic_cast(curve)) { if (abs(*line->p1.x - *line->p2.x) < EPS && abs(*line->p1.y - *line->p2.y) < EPS && after_constraint) { sys.undoSolution(); flag = true; break; } } } } if (flag && after_constraint) { QMessageBox::warning(this, QString("Error!"), QString("Last constraint is unavailable!")); remove_constraint(constraints_count - 1); C_Info.erase(constraints_count - 1); constraints_count--; } else if (flag) { sys.undoSolution(); } after_constraint = false; update(); } // =================================================================== // Очистка всего canvas // =================================================================== void Canvas::clearCanvas() { // Очистка солвера sys.clear(); // Очистка динамически выделенной памяти for (double* param : params) { delete param; } for (Point* pt : points) { delete pt; } for (Curve* curve : curves){ delete curve; } // Очистка контейнеров params.clear(); points.clear(); curves.clear(); // Очистка контейнеров ограничений parallelPairs.clear(); P2Ppairs.clear(); VERT_pairs.clear(); HORIZ_pairs.clear(); perpendicularPairs.clear(); // Очистка информации об ограничениях C_Info.clear(); // Сброс счетчиков obj_count = 0; constraints_count = 0; // Очистка временных указателей if (current_line) { delete current_line; current_line = nullptr; } if (firstPoint) { delete firstPoint; firstPoint = nullptr; } draggedPoint = nullptr; draggedLine = nullptr; // Сброс режима mode = Mode::None; // Обновление отображения update(); groups.clear(); }