diff --git a/sources/diagramevent/diagrameventaddelement.cpp b/sources/diagramevent/diagrameventaddelement.cpp index dee2834c0..9949c54d8 100644 --- a/sources/diagramevent/diagrameventaddelement.cpp +++ b/sources/diagramevent/diagrameventaddelement.cpp @@ -20,11 +20,44 @@ #include "../conductorautonumerotation.h" #include "../diagram.h" #include "../undocommand/addgraphicsobjectcommand.h" +#include "../undocommand/deleteqgraphicsitemcommand.h" #include "../factory/elementfactory.h" #include "../qetapp.h" #include "../qetdiagrameditor.h" #include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/conductor.h" +#include "../qetgraphicsitem/terminal.h" +#include "../qet.h" +#include +#include + +namespace { + /** + @brief distanceToSegment + @param point : point to measure from + @param segment : the finite segment (not the infinite line through it) + @return the distance from @a point to the closest point actually on + @a segment. Unlike QET::orthogonalProjection(), which reports a hit + for any point on the segment's infinite extension, this clamps the + projection to the segment itself: a point collinear with a wire but + past its actual drawn end is correctly reported as far away, not "on" + the wire. + */ + qreal distanceToSegment(const QPointF &point, const QLineF &segment) + { + const QPointF a = segment.p1(); + const QPointF b = segment.p2(); + const QPointF ab = b - a; + const qreal len2 = QPointF::dotProduct(ab, ab); + + if (len2 <= 0.0) + return QLineF(point, a).length(); + + qreal t = QPointF::dotProduct(point - a, ab) / len2; + t = qBound(0.0, t, 1.0); + return QLineF(point, a + t * ab).length(); + } +} /** @brief DiagramEventAddElement::DiagramEventAddElement @@ -248,16 +281,213 @@ void DiagramEventAddElement::addElement() QUndoCommand *undo_object = new QUndoCommand(tr("Ajouter %1").arg(element->name())); new AddGraphicsObjectCommand(element, m_diagram, m_element -> pos(), undo_object); - //When we search for free aligned terminal we temporally remove m_element to - //avoid any interaction with the function Element::AlignedFreeTerminals - //This is useful when an element has two (or more) terminals on opposite sides, - //because m_element is exactly at the same pos of the new element - //added to the scene so new conductor are created between terminal of the new element - //and the opposite terminal of m_element. + //When we search for free aligned terminal we temporally remove m_element to + //avoid any interaction with the function Element::AlignedFreeTerminals + //This is useful when an element has two (or more) terminals on opposite sides, + //because m_element is exactly at the same pos of the new element + //added to the scene so new conductor are created between terminal of the new element + //and the opposite terminal of m_element. m_diagram->removeItem(m_element); - while (!element -> AlignedFreeTerminals().isEmpty() && m_diagram -> project() -> autoConductor()) + + //Auto break conductor: if a terminal of the new element lies on an existing + //conductor, break the conductor and reconnect through the new element's terminal. + //Track the endpoints of broken conductors so auto-connect doesn't create duplicates. + QSet broken_endpoints; + + if (m_diagram->project()->autoBreakConductor()) { - QPair pair = element -> AlignedFreeTerminals().takeFirst(); + //Track which conductors we already handled for this element + QList conductors_handled; + //Track which terminals of the new element are already used (by break or other-connect) + QSet used_terminals; + + foreach (Terminal *t, element->terminals()) + { + //Skip terminals already used by a previous break+reconnect (other_terminal) + if (used_terminals.contains(t)) + continue; + + QPointF t_dock = t->dockConductor(); + + //Collect all conductors that pass through this dock point. + struct ConductorMatch { + Conductor *conductor; + Terminal *connect_to; //endpoint "before" the dock point (based on orientation) + Terminal *other; //endpoint "after" the dock point + }; + QList all_matches; + + foreach (Conductor *c, m_diagram->conductors()) + { + //Skip conductors we already handled or connected to the new element + if (conductors_handled.contains(c) || + c->terminal1->parentElement() == element || + c->terminal2->parentElement() == element) + continue; + + //Check if dock point lies on the conductor path. Distance is + //measured to the segment itself (clamped), not the infinite + //line through it -- see distanceToSegment(). + QPointF local_dock = c->mapFromScene(t_dock); + bool point_on_conductor = false; + QPainterPath path = c->path(); + for (int i = 0; i < path.elementCount() - 1; ++i) + { + const QPainterPath::Element &e1 = path.elementAt(i); + const QPainterPath::Element &e2 = path.elementAt(i + 1); + QLineF segment(QPointF(e1.x, e1.y), QPointF(e2.x, e2.y)); + if (distanceToSegment(local_dock, segment) < 5.0) + { + point_on_conductor = true; + break; + } + } + + if (!point_on_conductor) + continue; + + Terminal *c1 = c->terminal1; + Terminal *c2 = c->terminal2; + QPointF c1_dock = c1->dockConductor(); + QPointF c2_dock = c2->dockConductor(); + + Terminal *connect_to = nullptr; + Terminal *other = nullptr; + + switch (t->orientation()) { + case Qet::North: + if (c1_dock.y() < t_dock.y()) { connect_to = c1; other = c2; } + else if (c2_dock.y() < t_dock.y()) { connect_to = c2; other = c1; } + break; + case Qet::South: + if (c1_dock.y() > t_dock.y()) { connect_to = c1; other = c2; } + else if (c2_dock.y() > t_dock.y()) { connect_to = c2; other = c1; } + break; + case Qet::East: + if (c1_dock.x() > t_dock.x()) { connect_to = c1; other = c2; } + else if (c2_dock.x() > t_dock.x()) { connect_to = c2; other = c1; } + break; + case Qet::West: + if (c1_dock.x() < t_dock.x()) { connect_to = c1; other = c2; } + else if (c2_dock.x() < t_dock.x()) { connect_to = c2; other = c1; } + break; + } + + if (!connect_to) { + qreal d1 = QLineF(t_dock, c1_dock).length(); + qreal d2 = QLineF(t_dock, c2_dock).length(); + if (d1 <= d2) { connect_to = c1; other = c2; } + else { connect_to = c2; other = c1; } + } + + all_matches.append({c, connect_to, other}); + } + + if (all_matches.isEmpty()) + continue; + + //Group matches by their "other" endpoint and find the largest group. + //This ensures that when multiple independent circuits cross at the + //same dock point, the group with the most conductors gets priority, + //not whichever happens to have the nearest connect_to endpoint. + QMap> groups; + for (const auto &m : all_matches) { + groups[m.other].append(m); + } + Terminal *best_other = nullptr; + int best_count = 0; + for (auto it = groups.constBegin(); it != groups.constEnd(); ++it) { + if (it.value().size() > best_count) { + best_count = it.value().size(); + best_other = it.key(); + } + } + + //Only break conductors in the largest group (same "other" endpoint). + //This prevents bridging independent nets: if two unrelated conductors + //cross at a dock point, only the larger group gets broken. + const QList &matches = groups[best_other]; + + //Mark terminal as used + used_terminals.insert(t); + + //Delete all matched conductors in one batch + DiagramContent content; + for (const auto &m : matches) { + content.m_other_conductors.append(m.conductor); + conductors_handled.append(m.conductor); + } + new DeleteQGraphicsItemCommand(m_diagram, content, undo_object); + + //Create new conductors from each connect_to endpoint to this terminal. + //This creates junctions at the terminal when multiple conductors + //from the same circuit pass through the dock point (e.g. left and + //right conductors going to the same terminal strip terminal). + for (const auto &m : matches) { + Conductor *new_c = new Conductor(m.connect_to, t); + new AddGraphicsObjectCommand(new_c, m_diagram, QPointF(), undo_object); + ConductorAutoNumerotation can(new_c, m_diagram, undo_object); + can.numerate(); + if (m_diagram->freezeNewConductors() || m_diagram->project()->isFreezeNewConductors()) + new_c->setFreezeLabel(true); + + broken_endpoints.insert(m.connect_to); + } + + //Connect the shared "other" endpoint to the nearest free terminal + //with matching orientation. + QPointF other_dock = best_other->dockConductor(); + Terminal *other_terminal = nullptr; + qreal best_dist = std::numeric_limits::max(); + foreach (Terminal *ot, element->terminals()) + { + if (used_terminals.contains(ot)) + continue; + + //Check that the other_dock approaches from the correct direction + //relative to the candidate terminal's orientation. + QPointF ot_dock = ot->dockConductor(); + bool orientation_ok = false; + switch (ot->orientation()) { + case Qet::North: orientation_ok = other_dock.y() < ot_dock.y(); break; + case Qet::South: orientation_ok = other_dock.y() > ot_dock.y(); break; + case Qet::East: orientation_ok = other_dock.x() > ot_dock.x(); break; + case Qet::West: orientation_ok = other_dock.x() < ot_dock.x(); break; + } + if (!orientation_ok) + continue; + + qreal dist = QLineF(ot_dock, other_dock).length(); + if (dist < best_dist) { + best_dist = dist; + other_terminal = ot; + } + } + + if (other_terminal) { + Conductor *new_c2 = new Conductor(best_other, other_terminal); + new AddGraphicsObjectCommand(new_c2, m_diagram, QPointF(), undo_object); + ConductorAutoNumerotation can2(new_c2, m_diagram, undo_object); + can2.numerate(); + if (m_diagram->freezeNewConductors() || m_diagram->project()->isFreezeNewConductors()) + new_c2->setFreezeLabel(true); + + broken_endpoints.insert(best_other); + used_terminals.insert(other_terminal); + } + } + } + + //Auto-connect: collect all aligned pairs first, then filter and process. + QList> aligned_pairs; + if (m_diagram->project()->autoConductor()) + aligned_pairs = element->AlignedFreeTerminals(); + + for (const QPair &pair : aligned_pairs) + { + //Skip if the other terminal was an endpoint of a broken conductor + if (broken_endpoints.contains(pair.second)) + continue; Conductor *conductor = new Conductor(pair.first, pair.second); new AddGraphicsObjectCommand(conductor, m_diagram, QPointF(), undo_object); diff --git a/sources/qetdiagrameditor.cpp b/sources/qetdiagrameditor.cpp index 655bcd0d4..1ae771162 100644 --- a/sources/qetdiagrameditor.cpp +++ b/sources/qetdiagrameditor.cpp @@ -370,6 +370,21 @@ void QETDiagramEditor::setUpActions() pv->project()->setAutoConductor(ac); }); + //AutoBreakConductor + m_auto_break_conductor = new QAction (QET::Icons::Conductor, tr("Coupure automatique de conducteur(s)","Tool tip of auto break conductor"), this); + m_auto_break_conductor->setStatusTip (tr("Couper automatiquement les conducteurs existants lors du placement d'un élément", "Status tip of auto break conductor")); + m_auto_break_conductor->setCheckable (true); + { + QSettings settings; + m_auto_break_conductor->setChecked(settings.value("diagrameditor/auto_break_conductor", false).toBool()); + } + connect(m_auto_break_conductor, &QAction::triggered, [this](bool abc) { + QSettings settings; + settings.setValue("diagrameditor/auto_break_conductor", abc); + if (ProjectView *pv = currentProjectView()) + pv->project()->setAutoBreakConductor(abc); + }); + //Switch background color m_grey_background = new QAction (QET::Icons::DiagramBg, tr("Couleur de fond blanc/gris","Tool tip of white/grey background button"), this); m_grey_background -> setStatusTip (tr("Affiche la couleur de fond du folio en blanc ou en gris", "Status tip of white/grey background button")); @@ -809,6 +824,7 @@ void QETDiagramEditor::setUpToolBar() diagram_tool_bar -> addAction (m_edit_diagram_properties); diagram_tool_bar -> addAction (m_conductor_reset); diagram_tool_bar -> addAction (m_auto_conductor); + diagram_tool_bar -> addAction (m_auto_break_conductor); m_add_item_tool_bar = new QToolBar(tr("Ajouter"), this); m_add_item_tool_bar->setObjectName("adding"); @@ -1896,9 +1912,14 @@ void QETDiagramEditor::slot_updateModeActions() { m_auto_conductor -> setEnabled (true); m_auto_conductor -> setChecked (pv -> project() -> autoConductor()); + m_auto_break_conductor -> setEnabled (true); + m_auto_break_conductor -> setChecked (pv -> project() -> autoBreakConductor()); } else + { m_auto_conductor -> setDisabled(true); + m_auto_break_conductor -> setDisabled(true); + } } /** diff --git a/sources/qetdiagrameditor.h b/sources/qetdiagrameditor.h index f5666f781..21df421f7 100644 --- a/sources/qetdiagrameditor.h +++ b/sources/qetdiagrameditor.h @@ -192,6 +192,8 @@ class QETDiagramEditor : public QETMainWindow *redo, ///< Redo the latest cancelled operation *m_paste, ///< Paste clipboard content on the current diagram *m_auto_conductor, ///< Enable/Disable the use of auto conductor + *m_auto_break_conductor, ///< Enable/Disable the use of auto break conductor + *conductor_default, ///< Show a dialog to edit default conductor properties *m_grey_background, ///< Switch the background color in white or grey *m_draw_grid, ///< Switch the background grid display or not *m_draw_guides = nullptr, ///< Switch the custom guides display or not diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index 9b115458d..31dfb6365 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -69,6 +69,10 @@ m_project_properties_handler{this} init(); QSettings settings; + + //Read auto break conductor default from global settings + m_auto_break_conductor = settings.value(QStringLiteral("diagrameditor/auto_break_conductor"), false).toBool(); + int size = settings.beginReadArray(QStringLiteral("diagrameditor/defaultguides")); for (int i = 0; i < size; ++i) { settings.setArrayIndex(i); @@ -927,6 +931,28 @@ void QETProject::setAutoConductor(bool ac) m_auto_conductor = ac; } +/** + @brief QETProject::autoBreakConductor + @return true if use of auto break conductor is authorized. + See also Q_PROPERTY autoBreakConductor +*/ +bool QETProject::autoBreakConductor() const +{ + return m_auto_break_conductor; +} + +/** + @brief QETProject::setAutoBreakConductor + @param abc + Enable the use of auto break conductor if true + See also Q_PROPERTY autoBreakConductor +*/ +void QETProject::setAutoBreakConductor(bool abc) +{ + if (abc != m_auto_break_conductor) + m_auto_break_conductor = abc; +} + /** @brief QETProject::autoFolioNumberingNewFolios emit Signal to add new Diagram with autonum @@ -1735,6 +1761,7 @@ void QETProject::readDefaultPropertiesXml(QDomDocument &xml_project) { m_current_conductor_autonum = conds_autonums.attribute(QStringLiteral("current_autonum")); m_freeze_new_conductors = conds_autonums.attribute(QStringLiteral("freeze_new_conductors")) == QLatin1String("true"); + m_auto_break_conductor = conds_autonums.attribute(QStringLiteral("auto_break_conductors")) == QLatin1String("true"); for (auto elmt : QET::findInDomElement(conds_autonums, QStringLiteral("conductor_autonum"))) { NumerotationContext nc; @@ -1861,6 +1888,7 @@ void QETProject::writeDefaultPropertiesXml(QDomElement &xml_element) QDomElement conductor_autonums = xml_document.createElement("conductors_autonums"); conductor_autonums.setAttribute("current_autonum", m_current_conductor_autonum); conductor_autonums.setAttribute("freeze_new_conductors", m_freeze_new_conductors ? "true" : "false"); + conductor_autonums.setAttribute("auto_break_conductors", m_auto_break_conductor ? "true" : "false"); foreach (QString key, conductorAutoNum().keys()) { QDomElement conductor_autonum = conductorAutoNum(key).toXml(xml_document, "conductor_autonum"); if (key != "" && conductorAutoNumFormula(key) != "") { diff --git a/sources/qetproject.h b/sources/qetproject.h index 61427ca72..b08e2b5b4 100644 --- a/sources/qetproject.h +++ b/sources/qetproject.h @@ -90,6 +90,7 @@ class QETProject : public QObject }; Q_PROPERTY(bool autoConductor READ autoConductor WRITE setAutoConductor) + Q_PROPERTY(bool autoBreakConductor READ autoBreakConductor WRITE setAutoBreakConductor) // constructors, destructor public: @@ -181,9 +182,11 @@ class QETProject : public QObject void setFreezeNewConductors(bool); bool autoConductor () const; + bool autoBreakConductor () const; bool autoElement () const; bool autoFolio () const; void setAutoConductor (bool ac); + void setAutoBreakConductor (bool abc); void setAutoElement (bool ae); void autoFolioNumberingNewFolios (); void autoFolioNumberingSelectedFolios(int, int, const QString&); @@ -318,6 +321,7 @@ class QETProject : public QObject QHash m_element_autonum; //Title and NumContext hash QString m_current_element_autonum; bool m_auto_conductor = true; + bool m_auto_break_conductor = false; XmlElementCollection *m_elements_collection = nullptr; bool m_freeze_new_elements = false; bool m_freeze_new_conductors = false;