diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index f348bee7c..282729d30 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -270,14 +270,18 @@ struct CableCell 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 + QSharedPointer terminal; ///< to ask where the cable goes }; /** * @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. + * share the same cable: the wires go down from the terminals and join in the cable. + * The cable ends together -- a short stub, then a box naming where the cable goes + * (Installation, Location and Label of the piece it lands on). It does not fan out + * again after the cable: the wires drawn there suggested each core went its own way + * towards the target, which said nothing about where that target is. + * 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 @@ -350,7 +354,6 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin 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)) @@ -363,12 +366,51 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin //The cable painter->drawLine(QPointF{cable_x, bar_1_y}, QPointF{cable_x, bar_2_y}); - //Wires after the cable + //The bundle stays together at the end: the cross bar, a short + //stub, and a box naming where the cable goes -- the Installation, + //Location and Label (BMK) of the piece it lands on, the same + //three fields the cable list shows. The lines shrink to the end + //zone the settings leave, so the box never grows past the strip. painter->drawLine(QPointF{min_x, bar_2_y}, QPointF{max_x, bar_2_y}); - for (const auto &wire : std::as_const(wires)) + 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) { - 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); + 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 qreal room{end_length - stub}; + const auto fits_the_end_zone = [&target_lines, room](const QFont &font_) { + return QFontMetricsF{font_}.height() * target_lines.size() + 4 <= room; + }; + while (!fits_the_end_zone(box_font) && box_font.pixelSize() > 4) { + box_font.setPixelSize(box_font.pixelSize() - 1); + } + + const QFontMetricsF box_metrics{box_font}; + qreal box_width{6}; + 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}; + + painter->save(); + painter->setFont(box_font); + painter->drawRect(box); + for (auto i = 0 ; i < target_lines.size() ; ++i) + { + painter->drawText(QRectF{box.left() + 1, + box.top() + 2 + i * box_metrics.height(), + box.width() - 2, box_metrics.height()}, + Qt::AlignCenter, + target_lines.at(i)); + } + painter->restore(); } //The shield, linked to an ellipse around the cable @@ -1037,6 +1079,7 @@ void TerminalStripDrawer::paint(QPainter *painter) 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]; } const bool have_cable{m_pattern->m_cable_wire_length > 0 && !cable_cell.cable.isEmpty()}; cable_cells.append(cable_cell); diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h index 5588092fd..db080d234 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h @@ -78,6 +78,12 @@ namespace TerminalStripDrawer virtual QString cable() const { return QString(); } virtual QString cableWire() const { return QString(); } virtual bool isShield() const { return false; } + //Installation, Location and Label (BMK) of the piece the cable + //goes to at its far end -- the same three fields the cable list + //shows for a target -- drawn in a box under the cable. Empty + //when the terminal has no cable, or when the cable lands on + //nothing past the strip's own terminals. + virtual QVector cableTarget() const { return QVector(); } //Color and width (mm) of the terminal, only used to draw the terminal. //An invalid color means "no override" : the default color of the type is used. //A width <= 0 means "unknown" : the default width of the layout is used. diff --git a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp index d5248fc2e..8a0199ab3 100644 --- a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp +++ b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp @@ -153,6 +153,11 @@ namespace TerminalStripDrawer return m_real ? m_real->isShield() : false; } + QVector TrueRealTerminal::cableTarget() const + { + return m_real ? m_real->cableTarget() : QVector(); + } + QColor TrueRealTerminal::color() const { return m_real ? m_real->color() : QColor(); diff --git a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h index abde6dbd2..ac20d2bc8 100644 --- a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h +++ b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h @@ -64,6 +64,7 @@ namespace TerminalStripDrawer QString cable() const override; QString cableWire() const override; bool isShield() const override; + QVector cableTarget() const override; QColor color() const override; QColor stripeColor() const override; qreal widthMm() const override; diff --git a/sources/TerminalStrip/realterminal.cpp b/sources/TerminalStrip/realterminal.cpp index 3ec7ad462..787735cd7 100644 --- a/sources/TerminalStrip/realterminal.cpp +++ b/sources/TerminalStrip/realterminal.cpp @@ -22,7 +22,15 @@ #include "terminalcolor.h" #include "terminalcatalog.h" #include "../qetgraphicsitem/conductor.h" +#include "../qetgraphicsitem/element.h" +#include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" +#include "../qetproject.h" +#include "../cable/cable.h" +#include "../diagram.h" + +#include +#include /** * @brief RealTerminal @@ -322,6 +330,144 @@ bool RealTerminal::isShield() const { return false; } +/** + * @brief RealTerminal::cableTarget + * Where the cable of this terminal goes at its far end: the + * Installation, Location and Label (BMK) of the first piece the + * wires of the cable land on, past the strip's own terminals, in + * plan order -- the same three fields the cable list shows for a + * target, in the same order. An empty field is left out; a terminal + * with no cable, or a cable which lands on nothing else, gives no + * lines at all. + * + * The cable is the one the wires drawn to this terminal carry; a + * cable name typed by hand, with no wire behind it, is looked up by + * its name among the cables of the project. + * @return the lines to write in the box under the cable + */ +QVector RealTerminal::cableTarget() const +{ + if (m_spare || m_element.isNull()) { + return QVector(); + } + TerminalStrip *strip_ = parentStrip(); + if (!strip_ || !strip_->project()) { + return QVector(); + } + QETProject *project_{strip_->project()}; + + //The cable the wires drawn to this terminal are part of + QUuid cable_uuid_; + for (Terminal *terminal_ : m_element->terminals()) + { + if (!terminal_) { + continue; + } + for (Conductor *conductor_ : terminal_->conductors()) + { + if (conductor_ && !conductor_->properties().m_cable_uuid.isNull()) { + cable_uuid_ = conductor_->properties().m_cable_uuid; + break; + } + } + if (!cable_uuid_.isNull()) { + break; + } + } + if (cable_uuid_.isNull()) + { + //A cable name typed by hand, no wire behind it: the first + //cable of the project drawn under that name + const auto name_{cable()}; + if (name_.isEmpty()) { + return QVector(); + } + for (Cable *cable_ : project_->cables()) + { + if (cable_ && cable_->designation() == name_) { + cable_uuid_ = cable_->uuid(); + break; + } + } + if (cable_uuid_.isNull()) { + return QVector(); + } + } + + //The own side: this strip's terminals never are the target + QSet own_; + for (const auto &physical_ : strip_->physicalTerminal()) + { + if (!physical_) { + continue; + } + for (const auto &real_ : physical_->realTerminals()) { + if (real_ && real_->element()) { + own_.insert(real_->element()->uuid()); + } + } + } + + //Where the wires of the cable land: one piece whatever the + //number of its cores, never a report (the cable continues on + //the sheet the report points to) and never the strip's own + //terminals. Same rule as the cable list (cablelistrows.cpp). + QHash landed_; + for (Diagram *diagram_ : project_->diagrams()) + { + for (Conductor *conductor_ : diagram_->conductors()) + { + if (!conductor_ || conductor_->properties().m_cable_uuid != cable_uuid_) { + continue; + } + Terminal *ends_[2] = {conductor_->terminal1, conductor_->terminal2}; + for (Terminal *terminal_ : ends_) + { + if (!terminal_) { + continue; + } + Element *element_ = terminal_->parentElement(); + if (!element_ || (element_->linkType() & Element::AllReport)) { + continue; + } + if (own_.contains(element_->uuid())) { + continue; + } + landed_.insert(element_->uuid(), element_); + } + } + } + if (landed_.isEmpty()) { + return QVector(); + } + + //The first piece in plan order is the target, as the cable + //list chooses it + Element *target_{nullptr}; + for (Element *element_ : std::as_const(landed_)) { + if (!target_ || comparPos(element_, target_)) { + target_ = element_; + } + } + + const DiagramContext informations_{target_->elementInformations()}; + const auto installation_{informations_.value(QETInformation::ELMT_PLANT).toString()}; + const auto location_{informations_.value(QETInformation::ELMT_LOCATION).toString()}; + const auto label_{informations_.value(QETInformation::ELMT_LABEL).toString()}; + + QVector lines_; + if (!installation_.isEmpty()) { + lines_ << installation_; + } + if (!location_.isEmpty()) { + lines_ << location_; + } + if (!label_.isEmpty()) { + lines_ << label_; + } + return lines_; +} + /** * @brief RealTerminal::color * @return The color set by the user for this terminal, drawn in the diff --git a/sources/TerminalStrip/realterminal.h b/sources/TerminalStrip/realterminal.h index bf8d39bbd..d969871d7 100644 --- a/sources/TerminalStrip/realterminal.h +++ b/sources/TerminalStrip/realterminal.h @@ -74,6 +74,7 @@ class RealTerminal QString cable() const; QString cableWire() const; bool isShield() const; + QVector cableTarget() const; QColor color() const; QColor stripeColor() const; qreal section() const; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 644cf4725..a2e6496aa 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -332,6 +332,11 @@ target_link_libraries(tst_textresizehandles PRIVATE Qt::Test Qt::Widgets) # rectangle's does; a pivot placed by hand stays. qet_add_app_test(tst_linepivotresize tst_linepivotresize.cpp) +# The strip graphic names where a cable goes at its far end -- Installation, +# Location and Label of the first piece its wires land on past the strip, in +# plan order, the same three fields the cable list computes for a target. +qet_add_app_test(tst_cabletarget tst_cabletarget.cpp) + # legacyelementtypes.h is header-only: the rewrite of element type names in # nomenclature queries saved before June 2022. add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp) diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp new file mode 100644 index 000000000..b87385aca --- /dev/null +++ b/tests/qttest/tst_cabletarget.cpp @@ -0,0 +1,307 @@ +/* + 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 "qetapptest.h" + +#include "TerminalStrip/GraphicsItem/trueterminalstrip.h" +#include "TerminalStrip/physicalterminal.h" +#include "TerminalStrip/realterminal.h" +#include "TerminalStrip/terminalstrip.h" +#include "cable/cable.h" +#include "conductorproperties.h" +#include "diagram.h" +#include "properties/elementdata.h" +#include "qetgraphicsitem/conductor.h" +#include "qetgraphicsitem/element.h" +#include "qetinformation.h" +#include "qetproject.h" + +#include +#include +#include + +/** + The terminal strip graphic names where a cable goes: Installation, + Location and Label (BMK) of the first piece the wires of the cable + land on, past the strip's own terminals, in plan order -- the same + three fields the cable list computes for a target + (cablelistrows.cpp), so the strip and the list can never disagree. + The graphic draws them in a small box under the cable, the bundle + staying together (RealTerminal::cableTarget()). + + fixtures/every_feature.qet has the strip X1 (elements 891b70f2 and + 4861e268) and the contact fc9cc920 wired to 891b70f2 by the + conductor 8bd16d7a; the test gives the contact the Installation, + Location and Label it reads back. The conductor 584b452e joins the + two strip terminals to each other. No conductor of the fixture + belongs to a cable yet. +*/ +class tst_cabletarget : public QObject +{ + Q_OBJECT + + QETProject *m_project = nullptr; + + Element *element(const QString &uuid) const + { + for (Diagram *diagram_ : m_project->diagrams()) { + for (Element *element_ : diagram_->elements()) { + if (element_->uuid().toString() == uuid) { + return element_; + } + } + } + return nullptr; + } + + QSharedPointer terminalInStrip(const QString &element_uuid) const + { + const auto strips_ = m_project->terminalStrip(); + if (strips_.isEmpty()) { + return {}; + } + for (const auto &real_ : strips_.first()->realTerminals()) { + if (real_ && real_->element() + && real_->element()->uuid().toString() == element_uuid) { + return real_; + } + } + return {}; + } + + Conductor *conductor(const QString &uuid) const + { + for (Diagram *diagram_ : m_project->diagrams()) { + for (Conductor *conductor_ : diagram_->conductors()) { + if (conductor_ && conductor_->uuid().toString() == uuid) { + return conductor_; + } + } + } + return nullptr; + } + + /// Let the drawn wires of @a conductor_ belong to the cable @a cable_uuid. + void claimCable(Conductor *conductor_, const QUuid &cable_uuid) const + { + auto properties_ = conductor_->properties(); + properties_.m_cable_uuid = cable_uuid; + conductor_->setProperties(properties_); + } + + /** + Same setup for the value and the drawing test: the contact the + strip's wire lands on carries the three fields the test reads + back (the fixture's own block has none), the wire belongs to a + new cable, and the terminal carries that cable's name the way + the strip editor stores it -- without a name the strip draws no + cable at all. @return false when the fixture is not the one this + test was written for. + */ + bool wireCableToTheContact() + { + Element *contact_ = element( + QStringLiteral("{fc9cc920-5822-421a-916e-a17100acdcf6}")); + if (!contact_) { + return false; + } + ElementData contact_data_{contact_->elementData()}; + contact_data_.m_informations.addValue(QETInformation::ELMT_PLANT, + QStringLiteral("Plant 7")); + contact_data_.m_informations.addValue(QETInformation::ELMT_LOCATION, + QStringLiteral("Cabinet 7")); + contact_data_.m_informations.addValue(QETInformation::ELMT_LABEL, + QStringLiteral("-S7")); + contact_->setElementData(contact_data_); + + Cable *cable_ = m_project->newCable(); + Conductor *wired_ = conductor( + QStringLiteral("{8bd16d7a-18c4-4fa0-9495-8dab65fd6408}")); + if (!cable_ || !wired_) { + return false; + } + claimCable(wired_, cable_->uuid()); + + //The name of the cable on the terminal, the way the strip + //editor stores it: without it the strip draws no cable at all + Element *terminal_ = element( + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + if (!terminal_) { + return false; + } + ElementData terminal_data_{terminal_->elementData()}; + terminal_data_.m_informations.addValue(RealTerminal::cableInfoKey(), + QStringLiteral("X99")); + terminal_->setElementData(terminal_data_); + return true; + } + +private slots: + void init(); + void cleanup(); + + void noCableNoTarget(); + void targetIsTheOtherPiece(); + void ownStripIsNoTarget(); + void spareHasNoTarget(); + void paintsWithTargetBox(); +}; + +void tst_cabletarget::init() +{ + const QString fixture_ = QFINDTESTDATA("fixtures/every_feature.qet"); + QVERIFY(!fixture_.isEmpty()); + m_project = new QETProject(fixture_); + QCOMPARE(m_project->state(), QETProject::Ok); +} + +void tst_cabletarget::cleanup() +{ + delete m_project; + m_project = nullptr; +} + +/** + Nothing to name when no wire of the terminal belongs to a cable and + no cable of the project is drawn under a name the terminal carries. +*/ +void tst_cabletarget::noCableNoTarget() +{ + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + QVERIFY(a_->cable().isEmpty()); + QVERIFY(a_->cableTarget().isEmpty()); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + The cable of the strip lands on the contact past it: the three + fields of that piece, in the order the cable list shows them + (Installation, Location, Label), and nothing of the strip itself. +*/ +void tst_cabletarget::targetIsTheOtherPiece() +{ + QVERIFY(wireCableToTheContact()); + + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(a_); + QCOMPARE(a_->cableTarget(), + QVector({ + QStringLiteral("Plant 7"), + QStringLiteral("Cabinet 7"), + QStringLiteral("-S7"), + })); + + //The other strip terminal carries no claimed wire of its own + //(584b452e joins it to a_ and is not claimed) and no cable name, + //so it has no target to name + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(b_); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + A cable whose wires only ever join the terminals of the strip + itself lands on nothing past the strip: no target, no box. +*/ +void tst_cabletarget::ownStripIsNoTarget() +{ + Cable *cable_ = m_project->newCable(); + QVERIFY(cable_); + Conductor *inside_ = conductor( + QStringLiteral("{584b452e-a885-491a-98f1-fd348610b5e0}")); + QVERIFY(inside_); + claimCable(inside_, cable_->uuid()); + + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + QVERIFY(a_->cableTarget().isEmpty()); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + A spare terminal stands on no folio and is wired to nothing: it has + no target to name either. +*/ +void tst_cabletarget::spareHasNoTarget() +{ + ElementData data_; + data_.m_informations.addValue(QStringLiteral("label"), + QStringLiteral("AAA")); + const auto spare_ = RealTerminal::createSpare(data_); + QVERIFY(spare_); + QVERIFY(spare_->isSpare()); + QVERIFY(spare_->cableTarget().isEmpty()); +} + +/** + The strip graphic draws the box: the same strip painted with + something to name at the end of the cable and painted with nothing + to name come out as two different pictures -- the box is the + difference. Painting it also runs the whole drawCables() path. +*/ +void tst_cabletarget::paintsWithTargetBox() +{ + QVERIFY(wireCableToTheContact()); + + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + QSharedPointer{new TerminalStripLayoutPattern}}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + + const auto render = [&drawer, &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); + return image; + }; + + const QImage with_box{render()}; + + //Nothing left to name at the far end of the cable + Element *contact_ = element( + QStringLiteral("{fc9cc920-5822-421a-916e-a17100acdcf6}")); + QVERIFY(contact_); + ElementData contact_data_{contact_->elementData()}; + contact_data_.m_informations.addValue(QETInformation::ELMT_PLANT, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LOCATION, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LABEL, QString()); + contact_->setElementData(contact_data_); + + const QImage without_box{render()}; + QVERIFY(with_box != without_box); +} + +int main(int argc, char **argv) +{ + QStandardPaths::setTestModeEnabled(true); + return QetAppTest::run(argc, argv); +} + +#include "tst_cabletarget.moc"