diff --git a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp index b1ee952e6..26e68688c 100644 --- a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp @@ -542,6 +542,57 @@ bool haveCable(const AbstractTerminalStripInterface *strip) return false; } + /** + * @brief cableCells + * The cells the cables are drawn from : one per terminal of the strip + * carrying a cable, spread over the width of its body, all of them + * leaving the strip at its bottom edge. The paint and the depth the + * strip reserves under it both work on this list, so a box set lower + * than the others is never cut off by the drawing area. + */ + QVector cableCells(const AbstractTerminalStripInterface &strip, + const TerminalStripLayoutPattern &pattern) + { + const qreal s_{pattern.m_realistic_px_per_mm}; + const qreal length_{pattern.m_realistic_length_mm * s_}; + + //Left end stop and strip name : the terminals start after them + qreal x_{endStopWidthMm(pattern) * s_}; + if (pattern.m_realistic_group_marker) { + x_ += group_marker_width_mm * s_; + } + + QVector cells; + for (const auto &g_ : geometry(&strip)) + { + const qreal w_{g_.width_mm * s_}; + const int levels_{static_cast(g_.reals.size())}; + + //One wire exit by level, spread over the width of the body + for (int k = 0 ; k < levels_ ; ++k) + { + const auto &real_t{g_.reals.at(k)}; + if (!real_t || real_t->cable().isEmpty()) { + continue; + } + Helpers::CableCell cell_; + cell_.x = x_ + w_ * (k + 0.5) / levels_; + cell_.bottom = length_; + cell_.cable = real_t->cable(); + cell_.wire = real_t->cableWire(); + cell_.shield = real_t->isShield(); + cell_.terminal = real_t; + cells.append(cell_); + } + + x_ += w_; + if (g_.separator_after) { + x_ += separator_width_mm * s_; + } + } + return cells; + } + } //End anonymous namespace QRectF boundingRect(const AbstractTerminalStripInterface *strip, @@ -562,11 +613,16 @@ QRectF boundingRect(const AbstractTerminalStripInterface *strip, qreal extra_bottom_{0}; if (pattern.m_cable_wire_length > 0 && haveCable(strip)) { - //4 is the diameter of the circle at the end of the wires + //4 is the diameter of the circle at the end of the wires. + //A box standing in the way of another one is drawn lower than + //the end zone, and the room for it is reserved too (same rule, + //same cells as the drawing) or it is cut off extra_bottom_ = pattern.m_cable_wire_length + std::max(0, pattern.m_cable_length) + std::max(0, pattern.m_cable_end_length) - + 4; + + 4 + + Helpers::cableBoxesExtraDepth(cableCells(*strip, pattern), + pattern); } return QRectF{0, 0, width_mm_ * s_, length_ + extra_bottom_}; } @@ -628,7 +684,11 @@ void paint(QPainter *painter, } QHash> bridges_; - QVector cable_cells_; + //The cells the cables are drawn from, worked out once for the whole + //strip: the depth the strip reserves under it is built on the same + //list (boundingRect()), so a box set lower than another one is + //never cut off + QVector cable_cells_{cableCells(strip, pattern)}; for (const auto &g_ : geometry(&strip)) { @@ -637,24 +697,6 @@ void paint(QPainter *painter, const auto first_{g_.reals.first()}; const int levels_{static_cast(g_.reals.size())}; - //Cables (hoses) of this terminal : one wire exit by level, spread over - //the width of the body, all of them leaving the strip at its bottom edge - for (int k = 0 ; k < levels_ ; ++k) - { - const auto &real_t{g_.reals.at(k)}; - if (!real_t || real_t->cable().isEmpty()) { - continue; - } - Helpers::CableCell cell_; - cell_.x = x_ + w_ * (k + 0.5) / levels_; - cell_.bottom = length_; - cell_.cable = real_t->cable(); - cell_.wire = real_t->cableWire(); - cell_.shield = real_t->isShield(); - cell_.terminal = real_t; - cable_cells_.append(cell_); - } - //A terminal chosen in the catalog is drawn with its own clamp, so that a strip //mixing screw and spring terminals is drawn as it is. The spring-cage clamps //(opened with a screwdriver) are drawn like the push-in ones. diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index 52cd984e0..06877d9bb 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -263,6 +263,168 @@ void drawConnections(QPainter *painter, const QRectF &terminal_rect, qreal lengt } } +/** + One box at the end of a cable : which terminals the cable has and + where the box goes. The boxes of a strip are all measured before any + of them is drawn, so a box reaching into another one can be set + lower than it instead of being written over it. +*/ +struct TargetBox +{ + int first{-1}; /// lines; /// other.cable_x - other.width / 2 + && top < other.bottom() && bottom() > other.top; + } +}; + +/** + * @brief boxFont + * The font of a box naming where a cable goes : the font of the drawing, + * shrunk only while the lines of the target do not fit the height left + * for the box. The width never shrinks anything -- a line too long for + * the cross bar gives a wider box, it never gives a smaller text. + * @param room : the height left for the box + * @return the font the lines are written with + */ +QFont boxFont(const QFont &font, const QVector &lines, qreal room) +{ + QFont box_font{font}; + if (box_font.pixelSize() <= 0 && box_font.pointSizeF() > 0) { + box_font.setPixelSize(qMax(4, qRound(box_font.pointSizeF() * 96.0 / 72.0))); + } + const auto fits_the_height = [&lines, room](const QFont &font_) { + return QFontMetricsF{font_}.height() * lines.size() + 4 <= room; + }; + while (!fits_the_height(box_font) && box_font.pixelSize() > 4) { + box_font.setPixelSize(box_font.pixelSize() - 1); + } + return box_font; +} + +/** + * @brief targetBoxes + * Where the boxes naming where the cables go stand, and how big they + * are. Every box is measured first -- the lines of the target at the + * font of the drawing, a width of the widest line or of the cross bar + * the cable ends on, whichever is wider -- and they are then placed + * from the left to the right : a box which would reach into one + * already placed is set just low enough to stand clear of it, its stub + * drawn longer. Two cables of one strip therefore never write their + * boxes the one over the other, and no text is ever cut down to fit. + * @return one entry per cable which has something to name at its end + */ +QVector targetBoxes(const QVector &cells, + const TerminalStripLayoutPattern &pattern, + const QFont &font) +{ + QVector boxes; + const qreal wire_length{pattern.m_cable_wire_length}; + if (wire_length <= 0) { + return boxes; + } + const qreal cable_length{qMax(0, pattern.m_cable_length)}; + const qreal end_length{qMax(0, pattern.m_cable_end_length)}; + const qreal stub{qMin(6, end_length)}; + if (end_length - stub <= 4) { + return boxes; + } + constexpr qreal box_gap{2}; + + auto first{0}; + while (first < cells.size()) + { + if (cells.at(first).cable.isEmpty()) { + ++first; + continue; + } + + //Find the last terminal of the cable, the same groups the + //drawing makes + auto last{first}; + while (last + 1 < cells.size() && + cells.at(last + 1).cable == cells.at(first).cable) { + ++last; + } + + QVector wires, shields; + qreal max_bottom{cells.at(first).bottom}; + for (auto i = first ; i <= last ; ++i) + { + max_bottom = qMax(max_bottom, cells.at(i).bottom); + if (cells.at(i).shield) { + shields << i; + } else { + wires << i; + } + } + //A cable with only a shield is drawn like a cable with only one wire + if (wires.isEmpty()) { + wires = shields; + } + + const auto &first_wire{cells.at(wires.first())}; + TargetBox box; + box.first = first; + box.bar_2_y = max_bottom + wire_length + cable_length; + box.min_x = first_wire.x; + box.max_x = cells.at(wires.last()).x; + box.cable_x = (box.min_x + box.max_x) / 2; + box.lines = first_wire.terminal ? first_wire.terminal->cableTarget() + : QVector(); + + if (!box.lines.isEmpty()) + { + const QFontMetricsF metrics{boxFont(font, box.lines, end_length - stub)}; + box.width = qMax(box.max_x - box.min_x, 16); + for (const QString &line_ : std::as_const(box.lines)) { + box.width = qMax(box.width, metrics.horizontalAdvance(line_) + 6); + } + box.height = metrics.height() * box.lines.size() + 4; + box.top = box.bar_2_y + stub; + boxes << box; + } + + first = last + 1; + } + + //Place them : a box reaching into one already placed goes just + //under it, and the boxes set aside settle down again with it + for (auto i = 0 ; i < boxes.size() ; ++i) + { + bool settled{false}; + for (auto guard = 0 ; !settled && guard < boxes.size() + 2 ; ++guard) + { + settled = true; + for (auto j = 0 ; j < i ; ++j) + { + if (boxes.at(i).reachesInto(boxes.at(j)) + && boxes.at(i).top < boxes.at(j).bottom() + box_gap) + { + boxes[i].top = boxes.at(j).bottom() + box_gap; + settled = false; + } + } + } + } + return boxes; +} + /** * @brief drawCables * Draw the cables under the terminals. Consecutive terminals who have the same cable name @@ -333,6 +495,15 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin painter->restore(); }; + //Where every box stands, worked out for the whole strip before + //any of them is drawn: a box reaching into another one is set + //lower than it, never written over it + QHash box_of; + const auto boxes{targetBoxes(cells, pattern, painter->font())}; + for (const TargetBox &box_ : boxes) { + box_of.insert(box_.first, box_); + } + auto first{0}; while (first < cells.size()) { @@ -387,52 +558,42 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin //Location and Label (BMK) of the piece it lands on, the same //three fields the cable list shows. The box spans the cross bar //and never shrinks its text down to fit: when its lines are - //longer than the cross bar, the box is the one which grows. + //longer than the cross bar, the box is the one which grows. A + //box standing in the way of another one is set lower instead, + //its stub drawn longer, so no box is ever written over another. painter->drawLine(QPointF{min_x, bar_2_y}, QPointF{max_x, bar_2_y}); const qreal stub{qMin(6, end_length)}; - painter->drawLine(QPointF{cable_x, bar_2_y}, QPointF{cable_x, bar_2_y + stub}); - - const auto target_lines{wires.first().terminal ? wires.first().terminal->cableTarget() - : QVector()}; - if (!target_lines.isEmpty() && end_length - stub > 4) + const TargetBox box{box_of.value(first)}; + if (box.lines.isEmpty()) { + painter->drawLine(QPointF{cable_x, bar_2_y}, + QPointF{cable_x, bar_2_y + stub}); + } + else + { + //The stub goes down to the box, which stands lower than the + //end zone when another box of this strip was in its way + painter->drawLine(QPointF{cable_x, bar_2_y}, + QPointF{cable_x, box.top}); + //The font of the drawing, shrunk only while the lines do not //fit the height the settings leave for the box - const qreal room{end_length - stub}; - QFont box_font{painter->font()}; - if (box_font.pixelSize() <= 0 && box_font.pointSizeF() > 0) { - box_font.setPixelSize(qMax(4, qRound(box_font.pointSizeF() * 96.0 / 72.0))); - } - const auto fits_the_height = [&target_lines, room](const QFont &font_) { - return QFontMetricsF{font_}.height() * target_lines.size() + 4 <= room; - }; - while (!fits_the_height(box_font) && box_font.pixelSize() > 4) { - box_font.setPixelSize(box_font.pixelSize() - 1); - } - - //As wide as the cross bar the cable ends on -- the strip of - //one single terminal has no width to take, so a small floor - //keeps the box drawn at all -- and wider still when the lines - //of the target are longer than that + const QFont box_font{boxFont(painter->font(), box.lines, + end_length - stub)}; const QFontMetricsF box_metrics{box_font}; - qreal box_width{qMax(max_x - min_x, 16)}; - for (const QString &line_ : target_lines) { - box_width = qMax(box_width, box_metrics.horizontalAdvance(line_) + 6); - } - - const qreal box_height{box_metrics.height() * target_lines.size() + 4}; - const QRectF box{cable_x - box_width / 2, bar_2_y + stub, box_width, box_height}; + const QRectF box_rect{cable_x - box.width / 2, box.top, + box.width, box.height}; painter->save(); painter->setFont(box_font); - painter->drawRect(box); - for (auto i = 0 ; i < target_lines.size() ; ++i) + painter->drawRect(box_rect); + for (auto i = 0 ; i < box.lines.size() ; ++i) { - painter->drawText(QRectF{box.left() + 3, - box.top() + 2 + i * box_metrics.height(), - box.width() - 6, box_metrics.height()}, + painter->drawText(QRectF{box_rect.left() + 3, + box_rect.top() + 2 + i * box_metrics.height(), + box_rect.width() - 6, box_metrics.height()}, Qt::AlignCenter, - target_lines.at(i)); + box.lines.at(i)); } painter->restore(); } @@ -505,6 +666,53 @@ QRectF terminalRect(const TerminalStripLayoutPattern &pattern, return rect_; } +/** + * @brief cableCells + * The cells the cables are drawn from : one per real terminal of the + * strip, in the order they stand. The paint loop and the depth the + * strip reserves under its terminals both work on this list, so a box + * set lower than the others is never cut off by the drawing area. + * @param strip + * @param pattern + * @return the cells to hand over to Helpers::drawCables() + */ +QVector cableCells(const AbstractTerminalStripInterface &strip, + const TerminalStripLayoutPattern &pattern) +{ + QVector cells; + qreal x_offset{pattern.m_header_rect.width() + pattern.m_spacer_rect.width()}; + for (const auto &physical_t : strip.physicalTerminal()) + { + const QVector> real_terminal_vector{physical_t->realTerminals()}; + const auto real_t_count{real_terminal_vector.size()}; + const auto offset_{4 - real_t_count}; + + for (auto i=0 ; i= 4) { + break; + } + + const auto terminal_rect{terminalRect(pattern, index_, + real_terminal_vector[i])}; + CableCell cell; + cell.x = x_offset + terminal_rect.width()/2; + cell.bottom = terminal_rect.y() + terminal_rect.height(); + if (real_terminal_vector[i]) + { + cell.cable = real_terminal_vector[i]->cable(); + cell.wire = real_terminal_vector[i]->cableWire(); + cell.shield = real_terminal_vector[i]->isShield(); + cell.terminal = real_terminal_vector[i]; + } + cells.append(cell); + x_offset += terminal_rect.width(); + } + } + return cells; +} + /** * @brief The TerminalColors struct * Colors used to fill the rectangle of a terminal. @@ -936,6 +1144,22 @@ void Helpers::drawCables(QPainter *painter, const QVector &cells, ::TerminalStripDrawer::drawCables(painter, cells, pattern); } +qreal Helpers::cableBoxesExtraDepth(const QVector &cells, + const TerminalStripLayoutPattern &pattern) +{ + const qreal end_length{qMax(0, pattern.m_cable_end_length)}; + qreal extra{0}; + for (const auto &box_ : ::TerminalStripDrawer::targetBoxes(cells, pattern, + pattern.font())) + { + //How much deeper than the end zone the strip reserves this box + //stands -- the zone is the room of the box, a box set lower + //than the others needs room the strip must reserve too + extra = qMax(extra, box_.bottom() - (box_.bar_2_y + end_length)); + } + return qMax(0, extra); +} + /** * @brief TerminalStripDrawer::paint * @param painter @@ -1041,7 +1265,12 @@ void TerminalStripDrawer::paint(QPainter *painter) QRectF xref_rect; QHash> bridges_anchor_points; - QVector cable_cells; + //The cells the cables are drawn from, worked out once for the + //whole strip: the depth the strip reserves under its terminals + //is built on the same list (boundingRect()), so a box set lower + //than the others is never cut off + QVector cable_cells{cableCells(*m_strip, *m_pattern)}; + auto cell_index{0}; m_hovered_xref = hoverTerminal{}; @@ -1099,19 +1328,13 @@ void TerminalStripDrawer::paint(QPainter *painter) terminal_rect = terminalRect(*m_pattern, index_, real_terminal_vector[i]); - //Cable (hose) of this terminal - CableCell cable_cell; - if (real_terminal_vector[i]) - { - cable_cell.x = x_offset + terminal_rect.width()/2; - cable_cell.bottom = terminal_rect.y() + terminal_rect.height(); - cable_cell.cable = real_terminal_vector[i]->cable(); - cable_cell.wire = real_terminal_vector[i]->cableWire(); - cable_cell.shield = real_terminal_vector[i]->isShield(); - cable_cell.terminal = real_terminal_vector[i]; - } + //Cable (hose) of this terminal, and where this + //terminal stands: the very cell the depth of the strip + //was worked out from, so the drawing and the room it + //is given can never disagree + const CableCell &cable_cell{cable_cells.at(cell_index++)}; + x_offset = cable_cell.x - terminal_rect.width()/2; const bool have_cable{m_pattern->m_cable_wire_length > 0 && !cable_cell.cable.isEmpty()}; - cable_cells.append(cable_cell); //Draw terminal rect, filled with the color of the terminal (if any) const auto colors_{terminalColors(real_terminal_vector[i])}; const auto fill_color_{colors_.fill}; @@ -1412,12 +1635,18 @@ QRectF TerminalStripDrawer::boundingRect() const if (have_cable) { - //4 is the diameter of the circle at the end of the wires + //4 is the diameter of the circle at the end of the wires. + //A box standing in the way of another one is drawn lower + //than the end zone, and the room for it is reserved too + //(same rule, same cells as the drawing) or it is cut off extra_bottom = std::max(extra_bottom, m_pattern->m_cable_wire_length + std::max(0, m_pattern->m_cable_length) + std::max(0, m_pattern->m_cable_end_length) - + 4); + + 4 + + Helpers::cableBoxesExtraDepth( + cableCells(*m_strip, *m_pattern), + *m_pattern)); } } diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h index 494262358..d39bc76b6 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h @@ -58,6 +58,13 @@ namespace TerminalStripDrawer //in terminalstripdrawer.cpp for the whole drawing) void drawCables(QPainter *painter, const QVector &cells, const TerminalStripLayoutPattern &pattern); + //How much deeper than the end zone (m_cable_end_length) the boxes of + //the cables need: a box reaching into another one is set lower than + //the others, and the strip must reserve the room for it or the lower + //box is cut off. The same rule the drawing uses, worked out of the + //same cells, so the two can never disagree. + qreal cableBoxesExtraDepth(const QVector &cells, + const TerminalStripLayoutPattern &pattern); } } diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp index 1504b4761..36095fd31 100644 --- a/tests/qttest/tst_cabletarget.cpp +++ b/tests/qttest/tst_cabletarget.cpp @@ -53,7 +53,10 @@ room, so three lines never end up squeezed into a tiny text. A target typed in by hand is written the way the fields are shown in the column, "Installation / Location / Label", and the box draws one - line per field, with no slash in it. + line per field, with no slash in it. Two cables of one strip end + close to each other, and their boxes are wider than the room between + them: the box standing in the way of the other is set lower, its + stub drawn longer, and the strip reserves the room for it too. Both drawing styles are covered: the schematic one and the realistic one (the strip seen from above), which paint the same @@ -178,6 +181,7 @@ private slots: void wireLabelStaysOutOfTheTerminal(); void targetBoxKeepsTheDrawingFont(); void typedTargetIsDrawnInItsOwnLines(); + void boxesOfTwoCablesAreSetApart(); void realisticStyleDrawsTheCable(); }; @@ -612,6 +616,105 @@ void tst_cabletarget::typedTargetIsDrawnInItsOwnLines() "three fields must give a box two lines deeper than one field"); } +/** + Two cables of one strip end close to each other, and their boxes are + wider than the room between the two cables: the box standing in the + way of the other is set lower than it, its stub drawn longer -- the + two boxes are never written the one over the other, and the text is + never cut down to fit. The strip reserves the room for the lower box + too, or the drawing would be cut off there. +*/ +void tst_cabletarget::boxesOfTwoCablesAreSetApart() +{ + QVERIFY(wireCableToTheContact()); + + //The second cable of the strip : its wires only join the strip's + //own terminals, so it names nothing and takes what is typed in + Cable *cable_ = m_project->newCable(); + QVERIFY(cable_); + Conductor *inside_ = conductor( + QStringLiteral("{584b452e-a885-491a-98f1-fd348610b5e0}")); + QVERIFY(inside_); + claimCable(inside_, cable_->uuid()); + + const auto b_ = terminalInStrip( + QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(b_); + Element *second_ = b_->element(); + QVERIFY(second_); + + auto pattern_ = QSharedPointer{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const auto render = [&drawer](const QRectF &bounds) { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + painter.end(); + return image; + }; + const auto lowest_row = [](const QImage &image_) { + qreal lowest{-1}; + for (auto y = 0 ; y < image_.height() ; ++y) + { + for (auto x = 0 ; x < image_.width() ; ++x) + { + if (image_.pixel(x, y) != qRgba(0, 0, 0, 0)) { + lowest = y; + break; + } + } + } + return lowest; + }; + + //One cable with a target : one box, in the end zone + const QRectF one_bounds{drawer.boundingRect()}; + const qreal one_row{lowest_row(render(one_bounds))}; + QVERIFY(one_row >= 0); + + //The second cable gets a target of its own : its box is wider + //than the distance between the two cables and would be written + //over the first one + ElementData second_data_{second_->elementData()}; + second_data_.m_informations.addValue(RealTerminal::cableInfoKey(), + QStringLiteral("X98")); + second_data_.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + second_->setElementData(second_data_); + QCOMPARE(b_->cableTarget(), + (QVector{QStringLiteral("Plant 9"), + QStringLiteral("Cabinet 9"), + QStringLiteral("-S9")})); + + const QRectF two_bounds{drawer.boundingRect()}; + const qreal two_row{lowest_row(render(two_bounds))}; + + //The box in the way is set lower : the strip draws a whole box + //deeper than it did with one single box -- the second box stands + //under the first one, one box height plus the gap between them + const QFontMetricsF metrics{pattern_->font()}; + const qreal box_height{metrics.height() * 3 + 4}; + QVERIFY2(two_row - one_row > box_height, + "a box reaching into another one must be set lower, not " + "written over it"); + + //And the strip is deep enough for it, or the lower box would be + //cut off by the drawing area + QVERIFY2(two_bounds.height() > one_bounds.height(), + "the strip reserves no room for the box it sets lower"); + QVERIFY2(two_bounds.bottom() - one_bounds.bottom() > 0.5 * box_height, + "the room reserved for the lower box is too small"); +} + int main(int argc, char **argv) { QStandardPaths::setTestModeEnabled(true);