/* 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 . */ // SPDX-License-Identifier: GPL-2.0-or-later #include "wirehops.h" #include #include namespace { //Coordinates closer than this are the same; points are on a grid constexpr qreal tolerance = 0.01; bool isHorizontal(const QPointF &a, const QPointF &b) { return qAbs(a.y() - b.y()) < tolerance && qAbs(a.x() - b.x()) >= tolerance; } bool isVertical(const QPointF &a, const QPointF &b) { return qAbs(a.x() - b.x()) < tolerance && qAbs(a.y() - b.y()) >= tolerance; } //True if @a value is strictly between @a a and @a b, by more than @a margin bool strictlyBetween(qreal value, qreal a, qreal b, qreal margin) { return value > qMin(a, b) + margin && value < qMax(a, b) - margin; } //The hops of @a hops lying on the segment @a a - @a b, ordered from @a a QList hopsOnSegment(const QPointF &a, const QPointF &b, const QList &hops, bool horizontal) { QList on_segment; for (const QPointF &hop : hops) { if (horizontal ? (qAbs(hop.y() - a.y()) < tolerance && strictlyBetween(hop.x(), a.x(), b.x(), 0)) : (qAbs(hop.x() - a.x()) < tolerance && strictlyBetween(hop.y(), a.y(), b.y(), 0))) { on_segment.append(hop); } } std::sort(on_segment.begin(), on_segment.end(), [&a](const QPointF &p, const QPointF &q) { return QLineF(a, p).length() < QLineF(a, q).length(); }); return on_segment; } } /** @brief WireHops::toString @return the value written in the project file for @a mode */ QString WireHops::toString(Mode mode) { switch (mode) { case Mode::Horizontal: return QStringLiteral("horizontal"); case Mode::Vertical: return QStringLiteral("vertical"); case Mode::None: break; } return QStringLiteral("none"); } /** @brief WireHops::fromString @return the mode written as @a string in a project file, Mode::None for anything else, so an unknown value draws no hop. */ WireHops::Mode WireHops::fromString(const QString &string) { if (string == QLatin1String("horizontal")) return Mode::Horizontal; if (string == QLatin1String("vertical")) return Mode::Vertical; return Mode::None; } /** @brief WireHops::crossings @param wire : the points of the wire, in scene coordinates @param others : the points of every other wire that may cross it, in scene coordinates @param mode : which wire of a crossing hops @return the points where @a wire hops, in the order of @a wire. A crossing is kept only if it lies strictly inside the segment of the other wire (a wire ending or bending on this one is a junction, not a crossing) and at least one hop radius from the ends of this wire's segment, so the arc fits. Of two crossings closer than one arc width, only the first hops. */ QList WireHops::crossings(const QVector &wire, const QList> &others, Mode mode) { QList hops; if (mode == Mode::None || wire.size() < 2) { return hops; } const bool horizontal = mode == Mode::Horizontal; for (int i = 0 ; i < wire.size() - 1 ; ++i) { const QPointF a = wire.at(i); const QPointF b = wire.at(i + 1); if (horizontal ? !isHorizontal(a, b) : !isVertical(a, b)) { continue; } QList on_segment; for (const auto &other : others) { for (int j = 0 ; j < other.size() - 1 ; ++j) { const QPointF c = other.at(j); const QPointF d = other.at(j + 1); if (horizontal ? !isVertical(c, d) : !isHorizontal(c, d)) { continue; } const QPointF crossing = horizontal ? QPointF(c.x(), a.y()) : QPointF(a.x(), c.y()); const bool inside_wire = horizontal ? strictlyBetween(crossing.x(), a.x(), b.x(), radius) : strictlyBetween(crossing.y(), a.y(), b.y(), radius); const bool inside_other = horizontal ? strictlyBetween(crossing.y(), c.y(), d.y(), tolerance) : strictlyBetween(crossing.x(), c.x(), d.x(), tolerance); if (inside_wire && inside_other) { on_segment.append(crossing); } } } std::sort(on_segment.begin(), on_segment.end(), [&a](const QPointF &p, const QPointF &q) { return QLineF(a, p).length() < QLineF(a, q).length(); }); for (const QPointF &crossing : std::as_const(on_segment)) { if (!hops.isEmpty() && QLineF(hops.last(), crossing).length() < 2 * radius + tolerance) { continue; } hops.append(crossing); } } return hops; } /** @brief WireHops::path @param wire : the points of the wire @param hops : the points where the wire hops, from WireHops::crossings, in the same coordinates as @a wire @param mode : which wire of a crossing hops @return the path of the wire, with a half circle at each hop: above a horizontal wire, right of a vertical one. With no hop, the same path as the plain wire. */ QPainterPath WireHops::path(const QVector &wire, const QList &hops, Mode mode) { QPainterPath path; if (wire.isEmpty()) { return path; } const bool horizontal = mode == Mode::Horizontal; path.moveTo(wire.first()); for (int i = 0 ; i < wire.size() - 1 ; ++i) { const QPointF a = wire.at(i); const QPointF b = wire.at(i + 1); const bool hops_here = mode != Mode::None && (horizontal ? isHorizontal(a, b) : isVertical(a, b)); if (hops_here) { //1 when the wire runs towards +x (or +y), -1 otherwise const qreal direction = horizontal ? (b.x() > a.x() ? 1 : -1) : (b.y() > a.y() ? 1 : -1); for (const QPointF &hop : hopsOnSegment(a, b, hops, horizontal)) { const QRectF circle(hop.x() - radius, hop.y() - radius, 2 * radius, 2 * radius); if (horizontal) { //Over the top: from the left end to the right one, or back path.lineTo(hop.x() - direction * radius, hop.y()); path.arcTo(circle, direction > 0 ? 180 : 0, direction > 0 ? -180 : 180); } else { //On the right: from the top end to the bottom one, or back path.lineTo(hop.x(), hop.y() - direction * radius); path.arcTo(circle, direction > 0 ? 90 : 270, direction > 0 ? -180 : 180); } } } path.lineTo(b); } return path; }