#include "Canvas.h" // =================================================================== // Вспомогательные функции // =================================================================== /// Вычислить расстояние между двумя точками static double dist_P2P(QPointF p1, QPointF p2) { return sqrt(pow(p2.x() - p1.x(), 2) + pow(p2.y() - p1.y(), 2)); } /// Создать упорядоченную пару (чтобы pair(obj1, obj2) и pair(obj2, obj1) считались одинаковыми) 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); setAutoFillBackground(true); } Canvas::~Canvas() { // Очистка динамически выделенной памяти for (double* param : params) { delete param; } for (Point* pt : points) { delete pt; } for (Line* line : lines) { delete line; } // Очистка контейнеров params.clear(); points.clear(); lines.clear(); parallelPairs.clear(); P2Ppairs.clear(); VERT_pairs.clear(); HORIZ_pairs.clear(); // Очистка временных указателей if (current_line) delete current_line; if (firstPoint) delete firstPoint; } // =================================================================== // Методы изменения режима // =================================================================== void Canvas::changeMode(Mode _mode) { mode = _mode; } // =================================================================== // Методы поиска и проверки // =================================================================== Line* Canvas::findAt(QPointF& pos, qreal tolerance) { // TODO: реализовать проверку, находится ли точка на линии for (Line* line : lines) { if (line->contains(pos, tolerance)) { return line; } } return nullptr; } Point* Canvas::findPointAt(QPointF pos, qreal tolerance) { Point* temp = nullptr; for (Line* line : lines) { 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; } 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)); } // =================================================================== // Методы работы с ограничениями // =================================================================== void Canvas::remove_constraints() { // Удаляем ограничение из солвера sys.clearByTag(constraints_count - 1); // Удаляем из соответствующих контейнеров в зависимости от типа ограничения switch (lastConstraint.mode) { case Mode::Horizontal: { if (auto* pair = std::get_if(&lastConstraint.data)) { HORIZ_pairs.erase(*pair); } break; } case Mode::Vertical: { if (auto* pair = std::get_if(&lastConstraint.data)) { VERT_pairs.erase(*pair); } break; } case Mode::Parallel: { if (auto* pair = std::get_if(&lastConstraint.data)) { parallelPairs.erase(*pair); } break; } case Mode::Coincedent: { if (auto* pair = std::get_if(&lastConstraint.data)) { P2Ppairs.erase(*pair); } break; } default: break; } // Сбрасываем информацию о последнем ограничении lastConstraint.mode = Mode::None; constraints_count--; } // =================================================================== // Обработчики событий мыши // =================================================================== 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; dragOffset = scene - QPointF(*p->x, *p->y); return; } Line* found = findAt(scene); if (found) { draggedLine = found; QPointF lineCenter( (*found->p1.x + *found->p2.x) / 2.0, (*found->p1.y + *found->p2.y) / 2.0 ); dragOffset = scene - lineCenter; return; } } // ====================== Режим Horizontal/Vertical ====================== else if (mode == Mode::Horizontal || mode == Mode::Vertical) { Line* found = 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); lastConstraint.mode = Mode::Horizontal; lastConstraint.data = pair; } else { sys.addConstraintVertical(*found, constraints_count++); auto pair = makeOrderedPair(found->start_ref, found->end_ref); VERT_pairs.insert(pair); lastConstraint.mode = Mode::Vertical; lastConstraint.data = pair; } update(); after_constraint = true; } mode = Mode::None; } // ====================== Режим DrawingLine: рисование линий ====================== else if (mode == Mode::DrawingLine) { if (!current_line) { // Первый клик: создаем новую линию current_line = new Line(); current_line->set_tag(obj_count); // Создаем координаты для точек линии 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); update(); return; } // Добавляем координаты второй точки в параметры params.push_back(current_line->p2.x); params.push_back(current_line->p2.y); // Завершаем создание линии lines.append(current_line); current_line = nullptr; mode = Mode::None; obj_count++; after_constraint = true; } update(); return; } // ====================== Режим Parallel: задание параллельности ====================== else if (mode == Mode::Parallel) { Line* found = findAt(scene); if (!found) { current_line = nullptr; update(); return; } // Первый клик: выбираем первую линию if (!current_line) { current_line = found; update(); return; } // Повторный клик на ту же линию: сброс выбора if (found == current_line) { current_line = nullptr; update(); return; } // Второй клик на другую линию: добавляем ограничение параллельности if (!areAlreadyParallel(found, current_line)) { auto pair = makeOrderedPair(found, current_line); sys.addConstraintParallel(*found, *current_line, constraints_count++); parallelPairs.insert(pair); lastConstraint.mode = Mode::Parallel; lastConstraint.data = pair; current_line = nullptr; mode = Mode::None; after_constraint = true; update(); } 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; update(); return; } } // ====================== Режим Coincedent: совпадение точек ====================== else if (mode == Mode::Coincedent) { Point* clickedPoint = findPointAt(scene); if (!clickedPoint) { firstPoint = nullptr; update(); return; } // Первый клик: выбираем первую точку if (!firstPoint) { firstPoint = clickedPoint; update(); return; } // Повторный клик на ту же точку: сброс выбора if (clickedPoint == firstPoint) { firstPoint = nullptr; update(); return; } // Находим линии, к которым принадлежат точки Line* l1 = nullptr; Line* l2 = nullptr; for (Line* l : lines) { 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; update(); return; } // Добавляем ограничение совпадения точек sys.addConstraintP2PCoincident(*clickedPoint, *firstPoint, constraints_count++); auto pair = makeOrderedPair(clickedPoint, firstPoint); P2Ppairs.insert(pair); lastConstraint.mode = Mode::Coincedent; lastConstraint.data = pair; firstPoint = nullptr; mode = Mode::None; after_constraint = true; update(); return; } } void Canvas::mouseMoveEvent(QMouseEvent* event) { // ====================== Перемещение точки ====================== if (draggedPoint) { QPointF pos = UCS_POSITION - dragOffset; auto coincidentGroup = getCoincidentGroup(draggedPoint); for (Point* pt : coincidentGroup) { *pt->x = pos.x(); *pt->y = pos.y(); } for (Point* basePt : coincidentGroup) { for (Point* other : points) { if (areHorizontalVertical(basePt, other, true)) { *other->x = pos.x(); } if (areHorizontalVertical(basePt, other, false)) { *other->y = pos.y(); } } } *draggedPoint->x = pos.x(); *draggedPoint->y = pos.y(); update(); } // ====================== Перемещение линии ====================== else if (draggedLine) { 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; } } } update(); } #ifdef _DEBUG else showObjectTag(WIDGET_POSITION); #endif } void Canvas::mouseReleaseEvent(QMouseEvent* event) { Q_UNUSED(event); if (draggedPoint) { draggedPoint = nullptr; update(); } if (draggedLine) { draggedLine = nullptr; update(); } } // =================================================================== // Метод отрисовки // =================================================================== 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(1.0, -1.0); p.setPen(Qt::red); p.drawLine(-5, 0, 5, 0); p.drawLine(0, -5, 0, 5); // ====================== Решение системы уравнений ====================== if (!params.empty()) { int res = sys.solve(params); if (res == SolveStatus::Success || res == SolveStatus::Converged) { sys.applySolution(); } else if (res == SolveStatus::Failed && after_constraint) { // Ошибка решения: удаляем последнее добавленное ограничение QMessageBox::warning(this, QString("Error!"), QString("Last constraint is unavailable!")); remove_constraints(); } after_constraint = false; } // ====================== Отрисовка линий ====================== for (Line* line : lines) { 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); } // ====================== Подсветка выбранной точки (режим 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::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.ry() = -logical.ry(); return logical; } std::vector Canvas::getCoincidentGroup(Point* p) { std::vector group; group.push_back(p); for (Point* other : points) { if (other != p && areCoincident(p, other)) { group.push_back(other); } } return group; }