/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech 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 2 of the License, or (at your option) any later version. QElectroTech 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 QElectroTech. If not, see . */ #include "diagrameventfillet.h" #include "../QPropertyUndoCommand/qpropertyundocommand.h" #include "../diagram.h" #include "../qetapp.h" #include "../qetdiagrameditor.h" #include "../qetgraphicsitem/qetshapeitem.h" #include "../qgimanager.h" #include #include #include #include #include #include #include #include #include #include namespace { const char *radiusSettingKey = "diagrameditor/fillet_radius"; /** Adds the fillet's arc to the diagram exactly where it was computed. AddGraphicsObjectCommand is not used: its redo() calls setPos(), and QetShapeItem::setPos() snaps the outline's corner to the grid, which would pull the arc off the two lines it has to touch. */ class AddFilletArcCommand : public QUndoCommand { public: AddFilletArcCommand(QetShapeItem *arc, Diagram *diagram, QUndoCommand *parent) : QUndoCommand(parent), m_arc(arc), m_diagram(diagram) { m_diagram->qgiManager().manage(m_arc); } ~AddFilletArcCommand() override { m_diagram->qgiManager().release(m_arc); } void redo() override { m_diagram->addItem(m_arc); } void undo() override { m_diagram->removeItem(m_arc); } private: QetShapeItem *m_arc; Diagram *m_diagram; }; qreal length(const QPointF &v) { return std::hypot(v.x(), v.y()); } qreal dot(const QPointF &a, const QPointF &b) { return a.x() * b.x() + a.y() * b.y(); } qreal distanceToSegment(const QPointF &p, const QLineF &segment) { const QPointF d = segment.p2() - segment.p1(); const qreal len2 = dot(d, d); if (len2 <= 0) return length(p - segment.p1()); const qreal t = qBound(qreal(0), dot(p - segment.p1(), d) / len2, qreal(1)); return length(p - (segment.p1() + t * d)); } QLineF sceneLine(const QetShapeItem *line) { return QLineF(line->mapToScene(line->line().p1()), line->mapToScene(line->line().p2())); } //Angle of a point on a circle, in the convention QPainterPath::arcTo() //and QetShapeItem use: degrees, anticlockwise from three o'clock. qreal arcAngle(const QPointF ¢re, const QPointF &p) { return qRadiansToDegrees(std::atan2(-(p.y() - centre.y()), p.x() - centre.x())); } //Same test as QetShapeItem::setStartAngle()/setEndAngle() use to snap //an arc back to a full ellipse. bool nearlyFullTurn(qreal span) { qreal wrapped = std::fmod(span, 360.0); if (wrapped < 0) wrapped += 360.0; return wrapped < 5.0 || wrapped > 355.0; } } // namespace DiagramEventFillet::DiagramEventFillet(Diagram *diagram) : DiagramEventInterface(diagram) { m_running = true; //Deferred for the same reason as DiagramEventAddShape's hint: //the previous tool's destructor clears the status bar after //this constructor returns. QTimer::singleShot(0, this, [this]() { showDefaultHint(); }); } DiagramEventFillet::~DiagramEventFillet() { clearFirstPick(); if (m_diagram && !m_diagram->views().isEmpty()) { if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst())) editor->statusBar()->clearMessage(); } } void DiagramEventFillet::mousePressEvent(QGraphicsSceneMouseEvent *event) { event->setAccepted(true); if (Q_UNLIKELY(m_diagram->isReadOnly())) return; if (event->button() == Qt::RightButton) { if (m_first_line) { clearFirstPick(); showDefaultHint(); } else { m_running = false; emit finish(); } return; } if (event->button() != Qt::LeftButton) return; const QPointF pos = event->scenePos(); const QList lines = linesNear(pos); if (lines.isEmpty()) { showHint(tr("Aucune ligne ici : cliquez sur une ligne dessinée sur le folio")); return; } if (!m_first_line) { //One click where two lines meet picks both of them if (lines.size() >= 2) { filletLines({lines.at(0), pos}, {lines.at(1), pos}); return; } m_first = {lines.first(), pos}; m_first_line = lines.first(); m_diagram->clearSelection(); m_first_line->setSelected(true); showHint(tr("Cliquez sur la deuxième ligne ; clic droit : annuler")); return; } const auto other = std::find_if(lines.cbegin(), lines.cend(), [this](QetShapeItem *line) { return line != m_first_line; }); if (other == lines.cend()) { showHint(tr("Cliquez sur une autre ligne ; clic droit : annuler")); return; } const Pick first = m_first; clearFirstPick(); filletLines(first, {*other, pos}); } /** @brief DiagramEventFillet::linesNear @return the Line shapes passing within a few screen pixels of scenePos, nearest first. Only the items under the cursor are looked at; the diagram is not walked. */ QList DiagramEventFillet::linesNear(const QPointF &scenePos) const { qreal scale = 1; if (!m_diagram->views().isEmpty()) scale = m_diagram->views().constFirst()->transform().m11(); const qreal tolerance = 6.0 / qMax(scale, qreal(0.01)); QList> found; const QRectF area(scenePos - QPointF(tolerance, tolerance), QSizeF(2 * tolerance, 2 * tolerance)); for (QGraphicsItem *item : m_diagram->items(area)) { if (item->type() != QetShapeItem::Type) continue; auto *shape = static_cast(item); if (shape->shapeType() != QetShapeItem::Line) continue; const qreal distance = distanceToSegment(scenePos, sceneLine(shape)); if (distance <= tolerance) found.append({distance, shape}); } std::sort(found.begin(), found.end(), [](const auto &a, const auto &b) { return a.first < b.first; }); QList lines; for (const auto &f : found) lines.append(f.second); return lines; } /** @brief DiagramEventFillet::filletLines Rounds the corner between the two picked lines. For each line, the part kept is the one on the side of the corner where it was clicked; a click on the corner itself keeps the longer part. The other end of each line moves to the point where the arc touches it -- shortening the line, or lengthening it when the two lines did not reach each other. One undo step undoes the whole fillet. */ void DiagramEventFillet::filletLines(const Pick &a, const Pick &b) { const QLineF s1 = sceneLine(a.line); const QLineF s2 = sceneLine(b.line); QPointF corner; if (s1.intersects(s2, &corner) == QLineF::NoIntersection) { showHint(tr("Ces deux lignes sont parallèles : pas de congé possible")); return; } qreal scale = 1; if (!m_diagram->views().isEmpty()) scale = m_diagram->views().constFirst()->transform().m11(); const qreal cornerTolerance = 6.0 / qMax(scale, qreal(0.01)); auto keptEnd = [&](const QLineF &segment, const QPointF &pick) -> int { const QPointF ends[2] = {segment.p1(), segment.p2()}; const QPointF towardPick = pick - corner; const bool onCorner = length(towardPick) < cornerTolerance; int best = -1; for (int pass = 0; pass < 2 && best < 0; ++pass) { qreal bestLength = -1; for (int i = 0; i < 2; ++i) { const QPointF v = ends[i] - corner; if (pass == 0 && !onCorner && dot(v, towardPick) <= 0) continue; if (length(v) > bestLength) { best = i; bestLength = length(v); } } } return best; }; const int keep1 = keptEnd(s1, a.pos); const int keep2 = keptEnd(s2, b.pos); const QPointF e1 = keep1 == 0 ? s1.p1() : s1.p2(); const QPointF e2 = keep2 == 0 ? s2.p1() : s2.p2(); const qreal len1 = length(e1 - corner); const qreal len2 = length(e2 - corner); if (len1 < 1e-6 || len2 < 1e-6) { showHint(tr("Une des lignes s'arrête au coin : rien à arrondir de ce côté")); return; } const QPointF u1 = (e1 - corner) / len1; const QPointF u2 = (e2 - corner) / len2; const qreal theta = std::acos(qBound(qreal(-1), dot(u1, u2), qreal(1))); if (qRadiansToDegrees(theta) > 174.0) { showHint(tr("Ces deux lignes sont presque alignées : pas de congé possible")); return; } QSettings settings; bool ok = false; QWidget *parent = m_diagram->views().isEmpty() ? nullptr : m_diagram->views().constFirst(); const qreal radius = QInputDialog::getDouble(parent, tr("Congé"), tr("Rayon :"), settings.value(radiusSettingKey, 20.0).toDouble(), 0.1, 100000.0, 1, &ok); if (!ok || !m_diagram) return; settings.setValue(radiusSettingKey, radius); const qreal tangentDistance = radius / std::tan(theta / 2); if (tangentDistance > len1 + 1e-6 || tangentDistance > len2 + 1e-6) { showHint(tr("Rayon trop grand pour ces lignes (%1 au plus)") .arg(std::min(len1, len2) * std::tan(theta / 2), 0, 'f', 1)); return; } const QPointF t1 = corner + u1 * tangentDistance; const QPointF t2 = corner + u2 * tangentDistance; const QPointF bisector = (u1 + u2) / length(u1 + u2); const QPointF centre = corner + bisector * (radius / std::sin(theta / 2)); //Each line keeps its picked end and moves its other end to the //tangent point; p1/p2 keep their roles, mapped back into the //line's own coordinates (it may be rotated or skewed). auto newLine = [](QetShapeItem *line, int keep, const QPointF &kept, const QPointF &tangent) { const QPointF p1 = keep == 0 ? kept : tangent; const QPointF p2 = keep == 0 ? tangent : kept; return QLineF(line->mapFromScene(p1), line->mapFromScene(p2)); }; auto *arc = new QetShapeItem(centre - QPointF(radius, radius), centre + QPointF(radius, radius), QetShapeItem::Ellipse); arc->setPen(a.line->pen()); qreal start = std::fmod(arcAngle(centre, t1), 360.0); if (start < 0) start += 360.0; const qreal span = std::fmod(arcAngle(centre, t2) - start + 540.0, 360.0) - 180.0; //Set the two angles in an order whose intermediate state is not //close to a full turn, which the setters would snap to an ellipse. if (!nearlyFullTurn(360.0 - start)) { arc->setStartAngle(start); arc->setEndAngle(start + span); } else { arc->setEndAngle(start + span); arc->setStartAngle(start); } auto *undo = new QUndoCommand(tr("Ajouter un congé")); new QPropertyUndoCommand(a.line, "line", a.line->line(), newLine(a.line, keep1, e1, t1), undo); new QPropertyUndoCommand(b.line, "line", b.line->line(), newLine(b.line, keep2, e2, t2), undo); new AddFilletArcCommand(arc, m_diagram, undo); m_diagram->undoStack().push(undo); showDefaultHint(); } void DiagramEventFillet::clearFirstPick() { if (m_first_line) m_first_line->setSelected(false); m_first_line.clear(); m_first = Pick(); } void DiagramEventFillet::showHint(const QString &text) const { if (!m_diagram || m_diagram->views().isEmpty()) return; if (auto *editor = QETApp::diagramEditorAncestorOf(m_diagram->views().constFirst())) editor->statusBar()->showMessage(text); } void DiagramEventFillet::showDefaultHint() const { showHint(tr("Congé : cliquez sur deux lignes, ou une fois là où elles se rejoignent ; " "Échap ou clic droit : terminer")); }