/* 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 "terminalstripdrawer.h" #include #include #include #include #include #include namespace TerminalStripDrawer { /** * @brief TerminalStripDrawer::TerminalStripDrawer * @param strip * @param pattern */ TerminalStripDrawer::TerminalStripDrawer(QSharedPointer strip, QSharedPointer layout) : m_strip { strip }, m_pattern { layout } {} void TerminalStripDrawer::setStrip(QSharedPointer strip) { m_strip = strip; } namespace { /* * The symbols are described in a box of 20 x 14 units centered on (0,0). * The current flow from the top to the bottom, like in the terminal strip. * drawTypeSymbol() scale this box to fit the real available area. */ constexpr qreal symbol_width{20}; constexpr qreal symbol_height{14}; void drawFuseSymbol(QPainter *painter) { painter->drawLine(QPointF{0, -7}, QPointF{0, 7}); painter->drawRect(QRectF{-3, -5, 6, 10}); } void drawSectionalSymbol(QPainter *painter) { painter->drawLine(QPointF{0, -7}, QPointF{0, 7}); painter->drawLine(QPointF{-4, 2}, QPointF{4, -2}); } void drawDiodeSymbol(QPainter *painter) { const qreal t{symbol_height / 3}; const QPointF triangle[3] { {-t, -t}, {t, -t}, {0, t} }; painter->drawLine(QPointF{0, -7}, QPointF{0, -t}); painter->drawPolygon(triangle, 3); painter->drawLine(QPointF{-t, t}, QPointF{t, t}); painter->drawLine(QPointF{0, t}, QPointF{0, 7}); } void drawLedSymbol(QPainter *painter) { drawDiodeSymbol(painter); const qreal t{symbol_height / 3}; //The two rays painter->drawLine(QPointF{t + 0.5, -1}, QPointF{t + 3.5, -4}); painter->drawLine(QPointF{t + 0.5, 2}, QPointF{t + 3.5, -1}); } void drawGroundSymbol(QPainter *painter) { painter->drawLine(QPointF{0, -7}, QPointF{0, -2}); painter->drawLine(QPointF{-5, -2}, QPointF{5, -2}); painter->drawLine(QPointF{-3, 1}, QPointF{3, 1}); painter->drawLine(QPointF{-1, 4}, QPointF{1, 4}); } //Two diodes side by side, wired in anti-parallel between the top and //the bottom of the terminal (current flows through the left one from //the top to the bottom and through the right one the other way) void drawAntiParallelDiodesSymbol(QPainter *painter) { painter->drawLine(QPointF{0, -7}, QPointF{0, -5}); painter->drawLine(QPointF{-5, -5}, QPointF{5, -5}); painter->drawLine(QPointF{0, 5}, QPointF{0, 7}); painter->drawLine(QPointF{-5, 5}, QPointF{5, 5}); //Left diode, anode on top const QPointF left_diode[3] { {-8, -3}, {-2, -3}, {-5, 2} }; painter->drawLine(QPointF{-5, -5}, QPointF{-5, -3}); painter->drawPolygon(left_diode, 3); painter->drawLine(QPointF{-8, 2}, QPointF{-2, 2}); painter->drawLine(QPointF{-5, 2}, QPointF{-5, 5}); //Right diode, anode at the bottom const QPointF right_diode[3] { {2, 2}, {8, 2}, {5, -3} }; painter->drawLine(QPointF{5, -5}, QPointF{5, -3}); painter->drawPolygon(right_diode, 3); painter->drawLine(QPointF{2, -3}, QPointF{8, -3}); painter->drawLine(QPointF{5, 2}, QPointF{5, 5}); } void drawResistorSymbol(QPainter *painter) { painter->drawLine(QPointF{0, -7}, QPointF{0, -5}); painter->drawRect(QRectF{-3, -5, 6, 10}); painter->drawLine(QPointF{0, 5}, QPointF{0, 7}); } //A resistor crossed by an oblique line ended by a short bar void drawVaristorSymbol(QPainter *painter) { drawResistorSymbol(painter); painter->drawLine(QPointF{-5, 5}, QPointF{5, -5}); painter->drawLine(QPointF{5, -5}, QPointF{8, -5}); } //A measuring point : the line is interrupted by a small circle void drawTestSocketSymbol(QPainter *painter) { const qreal r{2.5}; painter->drawLine(QPointF{0, -7}, QPointF{0, -r}); painter->drawEllipse(QPointF{0, 0}, r, r); painter->drawLine(QPointF{0, r}, QPointF{0, 7}); } //Same as the diode, the anode is at the bottom void drawDiodeReversedSymbol(QPainter *painter) { const qreal t{symbol_height / 3}; const QPointF triangle[3] { {-t, t}, {t, t}, {0, -t} }; painter->drawLine(QPointF{0, -7}, QPointF{0, -t}); painter->drawLine(QPointF{-t, -t}, QPointF{t, -t}); painter->drawPolygon(triangle, 3); painter->drawLine(QPointF{0, t}, QPointF{0, 7}); } /** * @brief drawScaled * Call @a draw with the painter moved by @a dx and scaled by @a factor. * The width of the pen is kept constant. */ template void drawScaled(QPainter *painter, qreal dx, qreal factor, Func draw) { painter->save(); painter->translate(dx, 0); painter->scale(factor, factor); auto pen_{painter->pen()}; pen_.setWidthF(pen_.widthF() / factor); painter->setPen(pen_); draw(painter); painter->restore(); } /** * @brief drawTypeSymbol * Draw the symbol of a terminal type in @a box. The symbol is scaled * to fit in @a box, so it never overflow on the neighbouring terminal. * The painter must have a pen set, the brush is ignored. * @param painter * @param box : area available for the symbol, in the painter coordinates * @param type : type of the terminal, a generic terminal have no symbol * @param led : if true a led symbol is drawn. If the terminal have * also a type (fuse...), the type symbol and the led are drawn side by side. */ void drawTypeSymbol(QPainter *painter, const QRectF &box, ElementData::TerminalType type, bool led) { const bool have_type{type != ElementData::TTGeneric}; if (!led && !have_type) { return; } const qreal scale_{qMin(box.width() / symbol_width, box.height() / symbol_height)}; if (scale_ <= 0) { return; } painter->save(); painter->setBrush(Qt::NoBrush); painter->translate(box.center()); painter->scale(scale_, scale_); auto pen_{painter->pen()}; pen_.setWidthF(pen_.widthF() / scale_); painter->setPen(pen_); auto draw_type = [type](QPainter *p) { switch (type) { case ElementData::TTFuse : drawFuseSymbol(p); break; case ElementData::TTSectional : drawSectionalSymbol(p); break; case ElementData::TTDiode : drawDiodeSymbol(p); break; case ElementData::TTGround : drawGroundSymbol(p); break; case ElementData::TTAntiParallelDiodes : drawAntiParallelDiodesSymbol(p); break; case ElementData::TTResistor : drawResistorSymbol(p); break; case ElementData::TTVaristor : drawVaristorSymbol(p); break; case ElementData::TTTestSocket : drawTestSocketSymbol(p); break; case ElementData::TTDiodeReversed : drawDiodeReversedSymbol(p); break; default: break; } }; if (led && have_type) { //Not enough place for two full size symbols, draw them smaller side by side drawScaled(painter, -5, 0.7, draw_type); drawScaled(painter, 2.5, 0.7, drawLedSymbol); } else if (led) { drawLedSymbol(painter); } else { draw_type(painter); } painter->restore(); } /** * @brief drawConnections * Draw a connection above and, if @a draw_bottom is true, a connection under * @a terminal_rect, in the middle of the rect. * A connection is a short line, without circle at its end. * @param painter * @param terminal_rect * @param length : the total length of a connection * @param draw_bottom : false if the cable is drawn under the terminal */ void drawConnections(QPainter *painter, const QRectF &terminal_rect, qreal length, bool draw_bottom) { const qreal x{terminal_rect.width() / 2}; const qreal top{terminal_rect.top()}; const qreal bottom{terminal_rect.top() + terminal_rect.height()}; painter->drawLine(QPointF{x, top}, QPointF{x, top - length}); if (draw_bottom) { painter->drawLine(QPointF{x, bottom}, QPointF{x, bottom + length}); } } /** * @brief The CableCell struct * What is needed to draw the cable under a terminal. */ struct CableCell { qreal x{0}; ///< x of the middle of the terminal, in the strip coordinates qreal bottom{0}; ///< y of the bottom of the terminal QString cable; ///< name of the cable (hose) QString wire; ///< color / number of the wire bool shield{false}; ///< true if the terminal is the shield of the cable }; /** * @brief drawCables * Draw the cables under the terminals. Consecutive terminals who have the same cable name * share the same cable: the wires go down from the terminals, join in the cable, * and leave the cable with the same wire marks. The shield is linked to a dashed ellipse * drawn around the cable. * @param painter : the pen and the font must be set * @param cells : one cell per terminal, in the drawing order * @param pattern */ void drawCables(QPainter *painter, const QVector &cells, const TerminalStripLayoutPattern &pattern) { const qreal wire_length{pattern.m_cable_wire_length}; if (wire_length <= 0 || cells.isEmpty()) { return; } const qreal cable_length{qMax(0, pattern.m_cable_length)}; const qreal end_length{qMax(0, pattern.m_cable_end_length)}; const QFontMetricsF font_metrics{painter->font()}; const qreal text_height{font_metrics.height()}; //Draw a mark (small oblique line) on a wire and the number of the wire next to it. //The text is written from the bottom to the top, at the left of the wire. const auto draw_wire_mark = [&](qreal x, qreal mark_y, const QString &text, bool text_above) { painter->drawLine(QPointF{x - 3, mark_y + 3}, QPointF{x + 3, mark_y - 3}); if (text.isEmpty()) { return; } const qreal text_width{font_metrics.horizontalAdvance(text)}; painter->save(); painter->translate(x - 1, text_above ? mark_y - 4 : mark_y + 4 + text_width); painter->rotate(270); painter->drawText(QRectF{0, -text_height, text_width + 2, text_height}, Qt::AlignLeft | Qt::AlignVCenter, text); painter->restore(); }; auto first{0}; while (first < cells.size()) { if (cells.at(first).cable.isEmpty()) { ++first; continue; } //Find the last terminal of the cable 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 << cells.at(i); } else { wires << cells.at(i); } } //A cable with only a shield is drawn like a cable with only one wire if (wires.isEmpty()) { wires = shields; shields.clear(); } const qreal min_x{wires.first().x}; const qreal max_x{wires.last().x}; const qreal cable_x{(min_x + max_x) / 2}; const qreal bar_1_y{max_bottom + wire_length}; const qreal bar_2_y{bar_1_y + cable_length}; const qreal end_y{bar_2_y + end_length}; //Wires between the terminals and the cable for (const auto &wire : std::as_const(wires)) { painter->drawLine(QPointF{wire.x, wire.bottom}, QPointF{wire.x, bar_1_y}); draw_wire_mark(wire.x, qMax(wire.bottom + 4, bar_1_y - 10), wire.wire, true); } painter->drawLine(QPointF{min_x, bar_1_y}, QPointF{max_x, bar_1_y}); //The cable painter->drawLine(QPointF{cable_x, bar_1_y}, QPointF{cable_x, bar_2_y}); //Wires after the cable painter->drawLine(QPointF{min_x, bar_2_y}, QPointF{max_x, bar_2_y}); for (const auto &wire : std::as_const(wires)) { painter->drawLine(QPointF{wire.x, bar_2_y}, QPointF{wire.x, end_y}); draw_wire_mark(wire.x, qMin(bar_2_y + 10, end_y), wire.wire, false); } //The shield, linked to an ellipse around the cable const qreal ellipse_ry{6}; const qreal ellipse_rx{qMax((max_x - min_x) / 2 + 4, 12)}; const qreal ellipse_y{shields.isEmpty() ? bar_2_y : qMax(bar_1_y + ellipse_ry, bar_2_y - 12)}; if (!shields.isEmpty()) { for (const auto &shield : std::as_const(shields)) { painter->drawLine(QPointF{shield.x, shield.bottom}, QPointF{shield.x, ellipse_y}); const qreal edge_x{shield.x > cable_x ? cable_x + ellipse_rx : cable_x - ellipse_rx}; painter->drawLine(QPointF{shield.x, ellipse_y}, QPointF{edge_x, ellipse_y}); } painter->save(); auto dashed_pen{painter->pen()}; dashed_pen.setStyle(Qt::DashLine); painter->setPen(dashed_pen); painter->drawEllipse(QPointF{cable_x, ellipse_y}, ellipse_rx, ellipse_ry); painter->restore(); } //Name of the cable, at the left of the cable, above the ellipse const qreal name_width{font_metrics.horizontalAdvance(cells.at(first).cable)}; const qreal name_top{bar_1_y}; const qreal name_bottom{shields.isEmpty() ? bar_2_y : ellipse_y - ellipse_ry}; painter->save(); painter->translate(cable_x - 2, (name_top + name_bottom) / 2 + name_width / 2); painter->rotate(270); painter->drawText(QRectF{0, -text_height, name_width + 2, text_height}, Qt::AlignLeft | Qt::AlignVCenter, cells.at(first).cable); painter->restore(); first = last + 1; } } /* * Width and color of a terminal. */ /** * The width of the terminals of the layout is the width of a terminal * of 5.2 mm (the usual width of the terminals of 2.5 mm2). * The width set by the user, in mm, is converted according to this reference. */ constexpr qreal reference_terminal_width_mm{5.2}; /** * @brief terminalRect * @param pattern * @param index : level of the terminal (0 to 3) * @param real_t : can be null * @return The rectangle of the terminal : the one of the layout, * with the width adapted to the width (in mm) of @a real_t if it is set. */ QRectF terminalRect(const TerminalStripLayoutPattern &pattern, int index, const QSharedPointer &real_t) { auto rect_{pattern.m_terminal_rect[index]}; if (real_t && real_t->widthMm() > 0) { rect_.setWidth(rect_.width() * real_t->widthMm() / reference_terminal_width_mm); } return rect_; } /** * @brief The TerminalColors struct * Colors used to fill the rectangle of a terminal. * An invalid fill color means that the rectangle is not filled. * The stripe color (if valid) is drawn as stripes over the fill color. */ struct TerminalColors { QColor fill; QColor stripe; }; /** * @brief terminalColors * @param real_t : can be null * @return The colors of the terminal : the color set by the user, * else the default color of the type of the terminal. */ TerminalColors terminalColors(const QSharedPointer &real_t) { if (!real_t) { return {}; } if (real_t->color().isValid()) { return {real_t->color(), real_t->stripeColor()}; } switch (real_t->type()) { case ElementData::TTGround: return {QColor(255, 216, 0), QColor(46, 158, 46)}; //Green/Yellow default: return {QColor(150, 150, 150), QColor()}; //Grey housing, like a real terminal block } } /** * @brief terminalBodyPath * @param rect : rectangle of the terminal * @return The outline of a terminal block seen from the front : a body of the full width * with, at the top and at the bottom, a slightly narrower cap (the wire entry), * linked to the body by a small slope. * Two neighbouring terminals touch each other by their bodies and leave a small notch between their caps. */ QPainterPath terminalBodyPath(const QRectF &rect) { QPainterPath path_; if (rect.width() <= 0 || rect.height() <= 0) { return path_; } const qreal inset_ = qMin(qreal(1.0), rect.width() * 0.1); const qreal cap_ = qMin(qreal(6.0), rect.height() * 0.06); const qreal slope_ = qMin(qreal(2.0), rect.height() * 0.03); const qreal l_ = rect.left(), r_ = rect.right(), t_ = rect.top(), b_ = rect.bottom(); QPolygonF outline_; outline_ << QPointF(l_ + inset_, t_) << QPointF(r_ - inset_, t_) << QPointF(r_ - inset_, t_ + cap_) << QPointF(r_, t_ + cap_ + slope_) << QPointF(r_, b_ - cap_ - slope_) << QPointF(r_ - inset_, b_ - cap_) << QPointF(r_ - inset_, b_) << QPointF(l_ + inset_, b_) << QPointF(l_ + inset_, b_ - cap_) << QPointF(l_, b_ - cap_ - slope_) << QPointF(l_, t_ + cap_ + slope_) << QPointF(l_ + inset_, t_ + cap_); path_.addPolygon(outline_); path_.closeSubpath(); return path_; } /** * @brief The Zone struct * A vertical zone (relative to the top of a terminal) already used by a text or a symbol. */ struct Zone { qreal top{0}; qreal bottom{0}; }; /** * @brief drawTerminalBody Draw a terminal housing seen from the front, * like in a terminal line-up diagram : a grey body with wire entries (caps) at both ends, * and clamping screws drawn in the free zones of the body. * @param painter * @param rect : rectangle of the terminal * @param used_zones : zones used by the texts and the symbols (in the frame of the strip), * the screws are drawn out of them. * @param bottom_line : draw the line between the body and the lower wire entry. * Must be false if a symbol (fuse, led...) is drawn over this line. * The details are deliberately schematic and scale with the terminal width. */ void drawTerminalBody(QPainter *painter, const QRectF &rect, const QVector &used_zones, bool bottom_line) { if (rect.width() <= 0 || rect.height() <= 0) { return; } painter->drawPath(terminalBodyPath(rect)); if (rect.width() < 5.0 || rect.height() < 12.0) { return; } const qreal cap_ = qMin(qreal(6.0), rect.height() * 0.06); const qreal slope_ = qMin(qreal(2.0), rect.height() * 0.03); const qreal body_top_ = rect.top() + cap_ + slope_; const qreal body_bottom_ = rect.bottom() - cap_ - slope_; //Limit between the body and the wire entries painter->drawLine(QPointF(rect.left(), body_top_), QPointF(rect.right(), body_top_)); if (bottom_line) { painter->drawLine(QPointF(rect.left(), body_bottom_), QPointF(rect.right(), body_bottom_)); } //Free zones of the body : what remains when the used zones are removed QVector free_zones_; free_zones_.append(Zone{body_top_, body_bottom_}); const qreal margin_ = 2.5; for (const auto &used_ : used_zones) { QVector new_free_; for (const auto &free_ : std::as_const(free_zones_)) { if (used_.bottom + margin_ <= free_.top || used_.top - margin_ >= free_.bottom) { new_free_.append(free_); continue; } if (used_.top - margin_ > free_.top) { new_free_.append(Zone{free_.top, used_.top - margin_}); } if (used_.bottom + margin_ < free_.bottom) { new_free_.append(Zone{used_.bottom + margin_, free_.bottom}); } } free_zones_ = new_free_; } //In each free zone, draw the clamping parts like on a terminal block seen from the front : //screw, castle tab, big center circle, castle tab, screw. //If the zone is too small, only the center circle or one screw is drawn. const qreal screw_r_ = qMin(qreal(5.0), rect.width() * 0.3); const qreal center_r_ = qMin(screw_r_ * 1.5, rect.width() * 0.4); if (screw_r_ < 1.0) { return; } const qreal screw_h_ = screw_r_ * 2 + 3; const qreal center_h_ = center_r_ * 2 + 2; const qreal tab_h_ = 4.0; const qreal tab_w_ = qMin(rect.width() * 0.3, qreal(5.0)); const qreal cx_ = rect.center().x(); auto draw_screw = [&](qreal y_, qreal r_, bool window_) { const QPointF c_{cx_, y_}; if (window_) { const qreal half_{qMin(r_ + 1.5, rect.width() / 2 - 0.8)}; if (half_ > r_ + 0.3) { painter->drawRoundedRect(QRectF(cx_ - half_, c_.y() - half_, half_ * 2, half_ * 2), 1.0, 1.0); } } painter->drawEllipse(c_, r_, r_); painter->drawLine(QPointF(c_.x() - r_ * 0.6, c_.y()), QPointF(c_.x() + r_ * 0.6, c_.y())); }; auto draw_tab = [&](qreal y_) { painter->drawRect(QRectF(cx_ - tab_w_, y_ - tab_h_ / 2, tab_w_ * 2, tab_h_)); }; for (const auto &free_ : std::as_const(free_zones_)) { const qreal h_ = free_.bottom - free_.top; if (h_ >= screw_h_ * 2 + center_h_ + tab_h_ * 2 + 2) { const qreal top_y_ = free_.top + screw_h_ / 2; const qreal bottom_y_ = free_.bottom - screw_h_ / 2; const qreal middle_y_ = (top_y_ + bottom_y_) / 2; draw_screw(top_y_, screw_r_, true); draw_tab((top_y_ + screw_h_ / 2 + middle_y_ - center_h_ / 2) / 2); draw_screw(middle_y_, center_r_, false); draw_tab((middle_y_ + center_h_ / 2 + bottom_y_ - screw_h_ / 2) / 2); draw_screw(bottom_y_, screw_r_, true); } else if (h_ >= center_h_ + 2) { draw_screw((free_.top + free_.bottom) / 2, center_r_, false); } else if (h_ >= screw_h_) { draw_screw((free_.top + free_.bottom) / 2, screw_r_, true); } } } /** * @brief markerTagLength * Like the real marker tags, every tag of a strip have the same size, * whatever the text written on it ("1", "10"...). * The length is the one needed by two digits, limited to the text zone. * @param metrics : metrics of the font used to draw the text * @param zone_length : length of the text zone, along the text * @return the length of the marker tag along the text */ qreal markerTagLength(const QFontMetricsF &metrics, qreal zone_length) { return qMin(zone_length, metrics.horizontalAdvance(QStringLiteral("00")) + 4.0); } /** * @brief markerTagZone * @return The vertical zone (in the frame of the strip) really covered by the marker tag of a text, * which is smaller than the rectangle of the text. * @param metrics : metrics of the font used to draw the text * @param text_y : y of the rectangle of the text * @param text_height : height of the rectangle of the text * @param text : text drawn * @param option : option used to draw the text * @param vertical : true if the text is rotated */ Zone markerTagZone(const QFontMetricsF &metrics, qreal text_y, qreal text_height, const QString &text, const QTextOption &option, bool vertical) { if (!vertical) { const qreal cross_ = qMin(text_height, metrics.height() + 2); return Zone{text_y + (text_height - cross_) / 2, text_y + (text_height + cross_) / 2}; } Q_UNUSED(text) //Same length for every terminal, so the body of every terminal is drawn the same way const qreal along_ = markerTagLength(metrics, text_height); if (option.alignment() & Qt::AlignRight) { return Zone{text_y, text_y + along_}; } if (option.alignment() & Qt::AlignHCenter) { return Zone{text_y + (text_height - along_) / 2, text_y + (text_height + along_) / 2}; } return Zone{text_y + text_height - along_, text_y + text_height}; } /** * @brief drawMarkerTag * Draw the marker tag of a terminal (white, or black and ribbed for an isolating terminal) * behind a text. The tag fits the text and follows its alignment. * Must be called with the painter in the frame of @a text_rect (the one used to draw the text). * @param painter * @param text_rect : rectangle used to draw the text * @param text : text drawn * @param option : option used to draw the text * @param vertical : true if the text is rotated * @param sectional : true for an isolating terminal */ void drawMarkerTag(QPainter *painter, const QRectF &text_rect, const QString &text, const QTextOption &option, bool vertical, bool sectional) { if (text.isEmpty()) { return; } const QFontMetricsF metrics_{painter->font()}; //Vertical text : fixed length, like a real marker tag. //Horizontal text : the tag take the whole width of the terminal. const qreal along_ = vertical ? markerTagLength(metrics_, text_rect.width()) : text_rect.width(); const qreal cross_ = vertical ? text_rect.height() - 2 * qMin(qreal(1.2), text_rect.height() * 0.12) : qMin(text_rect.height(), metrics_.height() + 2); qreal x_{text_rect.left()}; if (option.alignment() & Qt::AlignRight) { x_ = text_rect.right() - along_; } else if (option.alignment() & Qt::AlignHCenter) { x_ = text_rect.left() + (text_rect.width() - along_) / 2; } const QRectF tag_rect_{x_, text_rect.top() + (text_rect.height() - cross_) / 2, along_, cross_}; painter->save(); painter->setBrush(sectional ? Qt::black : Qt::white); painter->drawRoundedRect(tag_rect_, 0.8, 0.8); if (sectional) { //Ribbed grip of the isolating terminals painter->setPen(QPen(QColor(90, 90, 90), 0)); for (auto rib_x_ = tag_rect_.left() + 1.5 ; rib_x_ < tag_rect_.right() - 0.5 ; rib_x_ += 1.5) { painter->drawLine(QPointF(rib_x_, tag_rect_.top() + 0.5), QPointF(rib_x_, tag_rect_.bottom() - 0.5)); } } painter->restore(); painter->setPen(sectional ? Qt::white : Qt::black); //The text is longer than the tag ("100", "PE"...) : reduce the font, like on a real tag. const qreal available_ = along_ - 2.0; const qreal text_length_ = metrics_.horizontalAdvance(text); if (text_length_ > available_ && available_ > 0) { auto font_{painter->font()}; const auto size_{font_.pixelSize() > 0 ? qreal(font_.pixelSize()) : font_.pointSizeF()}; const auto new_size_{std::max(3.0, size_ * available_ / text_length_)}; if (font_.pixelSize() > 0) { font_.setPixelSize(std::max(1, static_cast(new_size_))); } else { font_.setPointSizeF(new_size_); } painter->setFont(font_); } } /** * @brief terminalNumber * @param label : label of a terminal, for example "XAUZ1:1" * @return The number of the terminal, the part after the last ':' ("1"). * The strip name is already written in the header of the strip. * If the label have no ':', return the label. */ QString terminalNumber(const QString &label) { const auto index_ = label.lastIndexOf(QLatin1Char(':')); return index_ < 0 ? label : label.mid(index_ + 1).trimmed(); } /** * @brief fitTextToCell * The text of a terminal is drawn vertically (or not) inside the width of the terminal. * If the font is too big for a narrow terminal, the text overlaps the next ones. * Clip the painter to the width of the terminal and reduce the font if needed. * Must be called with the painter at the left of the terminal, before any rotation. * @param painter * @param cell_width : width of the terminal * @param text_is_vertical : true if the text is rotated (the height of the font is then along the width of the terminal) */ void fitTextToCell(QPainter *painter, qreal cell_width, bool text_is_vertical) { painter->setClipRect(QRectF(0, -100000, cell_width, 200000), Qt::IntersectClip); if (!text_is_vertical || cell_width <= 0) { return; } auto font_{painter->font()}; const auto available_{std::max(1.0, cell_width - 1.0)}; const QFontMetricsF metrics_{font_}; if (metrics_.height() > available_ && metrics_.height() > 0) { const auto size_{font_.pixelSize() > 0 ? qreal(font_.pixelSize()) : font_.pointSizeF()}; const auto new_size_{std::max(3.0, size_ * available_ / metrics_.height())}; if (font_.pixelSize() > 0) { font_.setPixelSize(static_cast(new_size_)); } else { font_.setPointSizeF(new_size_); } painter->setFont(font_); } } /** * @brief drawStripes * Draw diagonal stripes of @a color inside @a rect. */ void drawStripes(QPainter *painter, const QRectF &rect, const QColor &color) { painter->save(); painter->setClipPath(terminalBodyPath(rect)); painter->setPen(Qt::NoPen); painter->setBrush(color); const auto band_{std::max(2, rect.width() / 2)}; for (auto x = rect.left() - rect.height() ; x < rect.right() ; x += 2 * band_) { QPolygonF polygon_; polygon_ << QPointF(x, rect.bottom()) << QPointF(x + band_, rect.bottom()) << QPointF(x + band_ + rect.height(), rect.top()) << QPointF(x + rect.height(), rect.top()); painter->drawPolygon(polygon_); } painter->restore(); } /** * @return The color (black or white) the more readable on @a background. * If @a background is invalid, return black. */ QColor contrastColor(const QColor &background) { if (!background.isValid()) { return Qt::black; } const auto luminance_{0.299 * background.redF() + 0.587 * background.greenF() + 0.114 * background.blueF()}; return luminance_ < 0.5 ? QColor(Qt::white) : QColor(Qt::black); } } //End anonymous namespace /** * @brief TerminalStripDrawer::paint * @param painter */ void TerminalStripDrawer::paint(QPainter *painter) { if (m_strip && m_pattern) { m_united_xref_text_rect = QRectF(); m_xref_rects.clear(); //To draw text, QPainter need a Qrect. Instead of create an instance //for each text, we re-use the same instance of QRect. QRect text_rect; painter->save(); auto pen_{painter->pen()}; pen_.setColor(Qt::black); pen_.setWidth(1); auto brush_ = painter->brush(); brush_.setColor(Qt::white); painter->setFont(m_pattern->font()); painter->setPen(pen_); painter->setBrush(brush_); if (m_preview_draw) { painter->save(); painter->setPen(Qt::blue); painter->drawRect(boundingRect()); painter->drawLine(QPointF{boundingRect().left(), boundingRect().center().y()}, QPointF{boundingRect().right(), boundingRect().center().y()}); painter->restore(); } //Draw header painter->drawRect(m_pattern->m_header_rect); //Draw the header text painter->save(); if (m_pattern->m_header_text_orientation == Qt::Horizontal) { text_rect.setRect(0,m_pattern->m_header_rect.y(),m_pattern->m_header_rect.width(),m_pattern->m_header_rect.height()); } else { painter->translate(m_pattern->m_header_rect.bottomLeft()); painter->rotate(270); text_rect.setRect(0,0,m_pattern->m_header_rect.height(),m_pattern->m_header_rect.width()); } const auto text_{m_strip->installation() + " " + m_strip->location() + " " + m_strip->name()}; painter->drawText(text_rect, text_, m_pattern->headerTextOption()); painter->restore(); //Move painter pos to next drawing painter->translate(m_pattern->m_header_rect.width(),0); qreal x_offset{m_pattern->m_header_rect.width()}; //Draw spacer painter->drawRect(m_pattern->m_spacer_rect); //Move painter pos to next drawing painter->translate(m_pattern->m_spacer_rect.width(),0); x_offset += m_pattern->m_spacer_rect.width(); //Draw terminals const auto terminals_text_orientation{m_pattern->m_terminals_text_orientation}; const auto terminals_text_option{m_pattern->terminalsTextOption()}; const auto terminals_text_height{m_pattern->m_terminals_text_height}; const auto terminals_text_y{m_pattern->m_terminals_text_y}; QRectF terminal_rect; const auto xref_text_orientation{m_pattern->m_xref_text_orientation}; const auto xref_text_option{m_pattern->xrefTextOption()}; const auto xref_text_height{m_pattern->m_xref_text_height}; const auto xref_text_y{m_pattern->m_xref_text_y}; QRectF xref_rect; QHash> bridges_anchor_points; QVector cable_cells; m_hovered_xref = hoverTerminal{}; //Length of the longest xref of the strip, used to reserve the same //zone in the body of every terminal. qreal xref_zone_length_{0}; { const QFontMetricsF xref_metrics_{painter->font()}; for (const auto &physical_t : m_strip->physicalTerminal()) { for (const auto &real_t : physical_t->realTerminals()) { if (real_t && !real_t->xref().isEmpty()) { xref_zone_length_ = qMax(xref_zone_length_, xref_metrics_.horizontalAdvance(real_t->xref()) + 4.0); } } } xref_zone_length_ = qMin(xref_zone_length_, qreal(xref_text_height)); } int physical_index = 0; //Loop over physical terminals for (const auto &physical_t : m_strip->physicalTerminal()) { //Get the good offset according to how many level have the current physical terminal const QVector> real_terminal_vector{physical_t->realTerminals()}; const auto real_t_count{real_terminal_vector.size()}; const auto offset_{4 - real_t_count}; //Loop over real terminals for (auto i=0 ; i= 4) { break; } 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(); } 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}; const auto text_color_{contrastColor(fill_color_)}; painter->save(); if (fill_color_.isValid()) { painter->setBrush(QBrush(fill_color_)); } QVector used_zones_; if (real_terminal_vector[i]) { const QFontMetricsF metrics_{painter->font()}; used_zones_.append(markerTagZone(metrics_, terminals_text_y, terminals_text_height, terminalNumber(real_terminal_vector[i]->label()), terminals_text_option, terminals_text_orientation == Qt::Vertical)); //The zone of the xref have the length of the longest xref of the strip, //so the body of every terminal is drawn the same way. if (xref_text_orientation == Qt::Vertical) { if (xref_text_option.alignment() & Qt::AlignRight) { used_zones_.append(Zone{xref_text_y, xref_text_y + xref_zone_length_}); } else if (xref_text_option.alignment() & Qt::AlignHCenter) { used_zones_.append(Zone{xref_text_y + (xref_text_height - xref_zone_length_) / 2, xref_text_y + (xref_text_height + xref_zone_length_) / 2}); } else { used_zones_.append(Zone{xref_text_y + xref_text_height - xref_zone_length_, xref_text_y + xref_text_height}); } } else { const qreal cross_ = qMin(xref_text_height, metrics_.height() + 2); used_zones_.append(Zone{xref_text_y + (xref_text_height - cross_) / 2, xref_text_y + (xref_text_height + cross_) / 2}); } } const bool have_symbol_{m_pattern->m_type_symbol_height > 0 && real_terminal_vector[i] && (real_terminal_vector[i]->type() != ElementData::TTGeneric || real_terminal_vector[i]->isLed())}; if (have_symbol_) { used_zones_.append(Zone{m_pattern->m_type_symbol_y, m_pattern->m_type_symbol_y + m_pattern->m_type_symbol_height}); } drawTerminalBody(painter, terminal_rect, used_zones_, !have_symbol_); if (colors_.stripe.isValid()) { drawStripes(painter, terminal_rect, colors_.stripe); painter->setBrush(Qt::NoBrush); drawTerminalBody(painter, terminal_rect, used_zones_, !have_symbol_); //Draw again the outline and the details, hidden by stripes } painter->restore(); //Draw the symbol of the terminal type (fuse, ground, led...) if (m_pattern->m_type_symbol_height > 0 && real_terminal_vector[i]) { painter->save(); if (fill_color_.isValid()) { painter->setPen(text_color_); } drawTypeSymbol(painter, QRectF{0, m_pattern->m_type_symbol_y, terminal_rect.width(), m_pattern->m_type_symbol_height}, real_terminal_vector[i]->type(), real_terminal_vector[i]->isLed()); painter->restore(); } //Draw the connections, above and under the terminal if (m_pattern->m_connection_length > 0) { //The cable replace the connection under the terminal drawConnections(painter, terminal_rect, m_pattern->m_connection_length, !have_cable); } //Draw a stronger line if the current terminal have level //and the current level is the first if (real_t_count > 1 && i == 0) { painter->save(); pen_ = painter->pen(); pen_.setWidth(4); pen_.setCapStyle(Qt::FlatCap); painter->setPen(pen_); const auto p1 { terminal_rect.topLeft() }; //We can't use terminal_rect.bottomLeft for p2 because //the returned value deviate from the true value //(see Qt documentation about QRect) const QPointF p2 { p1.x(), p1.y() + terminal_rect.height() }; painter->drawLine(p1, p2); painter->restore(); } if(m_preview_draw) { painter->save(); painter->setPen(Qt::yellow); painter->drawLine(QPointF{terminal_rect.x(), terminal_rect.y() + terminal_rect.height()/2}, QPointF{terminal_rect.width(), terminal_rect.y() + terminal_rect.height()/2}); painter->restore(); } //Draw text painter->save(); if (fill_color_.isValid()) { painter->setPen(text_color_); } fitTextToCell(painter, terminal_rect.width(), terminals_text_orientation == Qt::Vertical); text_rect.setRect(0, terminals_text_y, terminal_rect.width(), terminals_text_height); if (terminals_text_orientation == Qt::Vertical) { painter->translate(text_rect.bottomLeft()); painter->rotate(270); text_rect.setRect(0, 0, text_rect.height(), text_rect.width()); } const auto shared_real_terminal{real_terminal_vector[i]}; if (shared_real_terminal && terminal_rect.width() >= 4.0) { drawMarkerTag(painter, text_rect, terminalNumber(shared_real_terminal->label()), terminals_text_option, terminals_text_orientation == Qt::Vertical, shared_real_terminal->type() == ElementData::TTSectional); } painter->drawText(text_rect, shared_real_terminal ? terminalNumber(shared_real_terminal->label()) : QString(), terminals_text_option); if (m_preview_draw) { painter->setPen(Qt::blue); painter->drawRect(text_rect); } painter->restore(); //Draw xref xref_rect.setRect(0, xref_text_y, terminal_rect.width(), xref_text_height); painter->save(); if (fill_color_.isValid()) { painter->setPen(text_color_); } fitTextToCell(painter, terminal_rect.width(), xref_text_orientation == Qt::Vertical); if (xref_text_orientation == Qt::Vertical) { painter->translate(xref_rect.bottomLeft()); painter->rotate(270); xref_rect.setRect(0, 0, xref_rect.height(), xref_rect.width()); } QTransform transform; transform.translate(x_offset, 0); if (xref_text_orientation == Qt::Vertical) { transform.translate(0, xref_text_y + xref_text_height); transform.rotate(270); } auto xref_string = shared_real_terminal->xref(); const auto mapped_xref_text_rect = transform.mapRect(painter->boundingRect(xref_rect, xref_string, xref_text_option)); if (!xref_string.isEmpty()) { m_xref_rects.append({mapped_xref_text_rect, physical_index, i}); } if (m_united_xref_text_rect.isNull()) { m_united_xref_text_rect = mapped_xref_text_rect; } else { m_united_xref_text_rect = m_united_xref_text_rect.united(mapped_xref_text_rect); } //if mouse hover the xref text, draw it in blue to advise user the xref is clickable. if (!m_mouse_hover_pos.isNull() && mapped_xref_text_rect.contains(m_mouse_hover_pos)) { painter->setPen(Qt::blue); m_hovered_xref.physical = physical_index; m_hovered_xref.real = i; } painter->drawText(xref_rect, xref_string, xref_text_option); if (m_preview_draw) { painter->setPen(Qt::blue); painter->drawRect(xref_rect); } painter->restore(); //Add bridge anchor if (shared_real_terminal->isBridged()) { painter->save(); if (QScopedPointer bridge_ { shared_real_terminal->bridge() }) { const auto x_anchor{terminal_rect.width()/2}; const auto y_anchor {m_pattern->m_bridge_point_y_offset[index_]}; const auto radius_anchor{m_pattern->m_bridge_point_d/2}; painter->setBrush(Qt::SolidPattern); painter->drawEllipse(QPointF(x_anchor, y_anchor), radius_anchor, radius_anchor); auto anchor_points{bridges_anchor_points.value(bridge_->uuid())}; anchor_points.append(QPointF(x_offset + x_anchor, y_anchor)); bridges_anchor_points.insert(bridge_->uuid(), anchor_points); } painter->restore(); } //Move painter pos to next drawing painter->translate(terminal_rect.width(),0); x_offset += terminal_rect.width(); } physical_index++; } painter->restore(); //Draw the bridges for (const auto &points_ : std::as_const(bridges_anchor_points)) { painter->save(); auto pen_{painter->pen()}; pen_.setWidth(2); painter->setPen(pen_); painter->drawPolyline(QPolygonF(points_)); painter->restore(); } //Draw the cables painter->save(); auto cable_pen{painter->pen()}; cable_pen.setColor(Qt::black); cable_pen.setWidth(1); painter->setPen(cable_pen); painter->setFont(m_pattern->font()); painter->setBrush(Qt::NoBrush); drawCables(painter, cable_cells, *m_pattern); painter->restore(); } } QRectF TerminalStripDrawer::boundingRect() const { QRectF rect_{0, 0, width(), height()}; if (m_pattern) { qreal extra_bottom{0}; //The connections are drawn above and under the terminals if (m_pattern->m_connection_length > 0) { const auto length_{m_pattern->m_connection_length}; qreal top_{0}; for (const auto &terminal_rect : std::as_const(m_pattern->m_terminal_rect)) { top_ = std::min(top_, terminal_rect.top() - length_); } rect_.setTop(top_); extra_bottom = length_; } //The cables are drawn under the terminals if (m_strip && m_pattern->m_cable_wire_length > 0) { bool have_cable{false}; for (const auto &physical_t : m_strip->physicalTerminal()) { for (const auto &real_t : physical_t->realTerminals()) { if (real_t && !real_t->cable().isEmpty()) { have_cable = true; break; } } if (have_cable) { break; } } if (have_cable) { //4 is the diameter of the circle at the end of the wires 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); } } rect_.setBottom(rect_.bottom() + extra_bottom); } return rect_; } void TerminalStripDrawer::setLayout(QSharedPointer layout) { m_pattern = layout; } bool TerminalStripDrawer::haveLayout() const { return !m_pattern.isNull(); } void TerminalStripDrawer::setPreviewDraw(bool draw) { m_preview_draw = draw; } void TerminalStripDrawer::setMouseHoverPos(const QPointF &pos) { m_last_mouse_pos_in_xrefs_rect = m_united_xref_text_rect.contains(m_mouse_hover_pos); m_mouse_hover_pos = pos; } /** * @brief TerminalStripDrawer::mouseHoverXref * @return True if the mouse position (given through the function setMouseHoverPos) * hover the rect of a xref. */ bool TerminalStripDrawer::mouseHoverXref() const { return m_united_xref_text_rect.contains(m_mouse_hover_pos); } bool TerminalStripDrawer::needUpdate() { if (mouseHoverXref()) { return true; } else if (m_last_mouse_pos_in_xrefs_rect) { return true; } return false; } /** * @brief TerminalStripDrawer::hoveredXref * @return the current terminal hovered by the mouse * in the xref bounding rectangle */ hoverTerminal TerminalStripDrawer::hoveredXref() const { return m_hovered_xref; } /** * @brief TerminalStripDrawer::xrefRects * @return the rectangle of every xref text drawn by the last call * of paint(), in the coordinates of the painter. */ QVector TerminalStripDrawer::xrefRects() const { return m_xref_rects; } qreal TerminalStripDrawer::height() const { if (m_pattern) { auto height_{m_pattern->m_header_rect.y() + m_pattern->m_header_rect.height()}; height_ = std::max(height_, m_pattern->m_spacer_rect.y() + m_pattern->m_spacer_rect.height()); for (const auto &rect : m_pattern->m_terminal_rect) { height_ = std::max(height_, rect.y() + rect.height()); } return height_; } return 0; } qreal TerminalStripDrawer::width() const { if (m_pattern) { qreal width_{m_pattern->m_header_rect.width() + m_pattern->m_spacer_rect.width()}; if (m_strip) { //Loop over physical terminals for (const auto &physical_t : m_strip->physicalTerminal()) { //Get the good offset according to how many level have the current physical terminal const QVector> real_terminal_vector{physical_t->realTerminals()}; const auto real_t_count{real_terminal_vector.size()}; const auto offset_{4 - real_t_count}; //Loop over real terminals for (auto i=0 ; i= 4) { break; } width_ += terminalRect(*m_pattern, index_, real_terminal_vector[i]).width(); } } } return width_; } return 0; } } //End namespace TerminalStripDrawer