diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 9670794e3..c0daa3aca 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -298,6 +298,9 @@ set(QET_SRC_FILES ${QET_DIR}/sources/dataBase/ui/summaryquerywidget.cpp ${QET_DIR}/sources/dataBase/ui/summaryquerywidget.h + ${QET_DIR}/sources/autobreakconductor.cpp + ${QET_DIR}/sources/autobreakconductor.h + ${QET_DIR}/sources/diagramevent/diagrameventaddelement.cpp ${QET_DIR}/sources/diagramevent/diagrameventaddelement.h ${QET_DIR}/sources/diagramevent/diagrameventaddimage.cpp diff --git a/sources/autobreakconductor.cpp b/sources/autobreakconductor.cpp new file mode 100644 index 000000000..944f106d8 --- /dev/null +++ b/sources/autobreakconductor.cpp @@ -0,0 +1,237 @@ +/* + 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 "autobreakconductor.h" + +#include "diagram.h" +#include "qetproject.h" +#include "conductorautonumerotation.h" +#include "qetgraphicsitem/element.h" +#include "qetgraphicsitem/terminal.h" +#include "qetgraphicsitem/conductor.h" +#include "undocommand/addgraphicsobjectcommand.h" +#include "undocommand/deleteqgraphicsitemcommand.h" + +#include +#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(); +} + +} // anonymous namespace + +/** + @brief autoBreakConductors + For each terminal of @a element, check if its dock point lies on an + existing conductor. If so, break the conductor and reconnect through + the element's terminal. Broken conductors from the same circuit + (sharing the same far-end endpoint) are broken together; independent + crossing conductors are left untouched. +*/ +QSet autoBreakConductors( + Diagram *diagram, + Element *element, + QUndoCommand *parent, + QList &conductors_handled, + QSet &used_terminals) +{ + QSet broken_endpoints; + + if (!diagram->project()->autoBreakConductor()) + return broken_endpoints; + + foreach (Terminal *t, element->terminals()) + { + if (used_terminals.contains(t)) + continue; + + QPointF t_dock = t->dockConductor(); + + struct ConductorMatch { + Conductor *conductor; + Terminal *connect_to; + Terminal *other; + }; + QList all_matches; + + foreach (Conductor *c, diagram->conductors()) + { + if (conductors_handled.contains(c) || + c->terminal1->parentElement() == element || + c->terminal2->parentElement() == element) + continue; + + 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; + + 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(); + } + } + + const QList &matches = groups[best_other]; + + used_terminals.insert(t); + + DiagramContent content; + for (const auto &m : matches) { + content.m_other_conductors.append(m.conductor); + conductors_handled.append(m.conductor); + } + new DeleteQGraphicsItemCommand(diagram, content, parent); + + for (const auto &m : matches) { + Conductor *new_c = new Conductor(m.connect_to, t); + new AddGraphicsObjectCommand(new_c, diagram, QPointF(), parent); + ConductorAutoNumerotation can(new_c, diagram, parent); + can.numerate(); + if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors()) + new_c->setFreezeLabel(true); + + broken_endpoints.insert(m.connect_to); + } + + 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; + + 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, diagram, QPointF(), parent); + ConductorAutoNumerotation can2(new_c2, diagram, parent); + can2.numerate(); + if (diagram->freezeNewConductors() || diagram->project()->isFreezeNewConductors()) + new_c2->setFreezeLabel(true); + + broken_endpoints.insert(best_other); + used_terminals.insert(other_terminal); + } + } + + return broken_endpoints; +} diff --git a/sources/autobreakconductor.h b/sources/autobreakconductor.h new file mode 100644 index 000000000..3818e6c5b --- /dev/null +++ b/sources/autobreakconductor.h @@ -0,0 +1,57 @@ +/* + 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 . +*/ +#ifndef AUTOBREAKCONDUCTOR_H +#define AUTOBREAKCONDUCTOR_H + +#include +#include + +class Diagram; +class Element; +class Terminal; +class Conductor; +class QUndoCommand; + +/** + @brief autoBreakConductors + For each terminal of @a element, check if its dock point lies on an + existing conductor. If so, break the conductor and reconnect through the + element's terminal. Broken conductors from the same circuit (sharing the + same far-end endpoint) are broken together; independent crossing + conductors are left untouched. + @param diagram the diagram containing the conductors + @param element the element whose terminals trigger breaks + @param parent undo command under which break/reconnect + sub-commands are created + @param conductors_handled shared list of conductors already claimed by + a previous call in the same batch (prevents + double-processing when multiple elements are + broken in one pass) + @param used_terminals shared set of terminals already used as + connect_to or other_terminal in the same batch + @return set of terminals that were connected to a broken conductor's + far-end (useful for preventing duplicate auto-connect) +*/ +QSet autoBreakConductors( + Diagram *diagram, + Element *element, + QUndoCommand *parent, + QList &conductors_handled, + QSet &used_terminals); + +#endif // AUTOBREAKCONDUCTOR_H diff --git a/sources/diagramcommands.cpp b/sources/diagramcommands.cpp index 2922c32b1..ab3a6fdeb 100644 --- a/sources/diagramcommands.cpp +++ b/sources/diagramcommands.cpp @@ -17,7 +17,9 @@ */ #include "diagramcommands.h" +#include "autobreakconductor.h" #include "diagram.h" +#include "qetproject.h" #include "qetgraphicsitem/conductortextitem.h" #include "qetgraphicsitem/element.h" #include "qetgraphicsitem/elementtextitemgroup.h" @@ -48,6 +50,7 @@ PasteDiagramCommand::PasteDiagramCommand( Diagram *dia, const DiagramContent &c, PasteDiagramCommand::~PasteDiagramCommand() { diagram -> qgiManager().release(content.items(filter)); + delete m_break_cmd; } /** @@ -58,6 +61,10 @@ void PasteDiagramCommand::undo() { diagram -> showMe(); + //Undo auto-break before removing items, so terminals are still on scene + if (m_break_cmd) + m_break_cmd->undo(); + foreach(QGraphicsItem *item, content.items(filter)) diagram->removeItem(item); } @@ -102,6 +109,27 @@ void PasteDiagramCommand::redo() } } } + + //Auto-break conductors on first paste. Items are already on the + //scene at this point (added before this command was created). + if (diagram->project()->autoBreakConductor()) + { + m_break_cmd = new QUndoCommand(); + QList conductors_handled; + QSet used_terminals; + for (Element *e : content.m_elements) { + autoBreakConductors(diagram, e, m_break_cmd, + conductors_handled, used_terminals); + } + if (m_break_cmd->childCount() == 0) { + delete m_break_cmd; + m_break_cmd = nullptr; + } + else + { + m_break_cmd->redo(); + } + } } else { @@ -109,6 +137,10 @@ void PasteDiagramCommand::redo() for (QGraphicsItem *item : qgis_list) { diagram->addItem(item); } + + //Re-execute the stored break commands (items are back on scene) + if (m_break_cmd) + m_break_cmd->redo(); } const QList qgis_list = content.items(); diff --git a/sources/diagramcommands.h b/sources/diagramcommands.h index 070214b85..5c05e151b 100644 --- a/sources/diagramcommands.h +++ b/sources/diagramcommands.h @@ -53,6 +53,8 @@ class PasteDiagramCommand : public QUndoCommand { int filter; /// prevent the first call to redo() bool first_redo; + /// auto-break conductor sub-commands (created on first redo) + QUndoCommand *m_break_cmd = nullptr; }; /** diff --git a/sources/diagramevent/diagrameventaddelement.cpp b/sources/diagramevent/diagrameventaddelement.cpp index 9949c54d8..94a84d7dc 100644 --- a/sources/diagramevent/diagrameventaddelement.cpp +++ b/sources/diagramevent/diagrameventaddelement.cpp @@ -28,37 +28,10 @@ #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/terminal.h" #include "../qet.h" +#include "../autobreakconductor.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 Defaut constructor @@ -292,191 +265,10 @@ void DiagramEventAddElement::addElement() //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()) - { - //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); - } - } - } + QList conductors_handled; + QSet used_terminals; + QSet broken_endpoints = autoBreakConductors(m_diagram, element, undo_object, + conductors_handled, used_terminals); //Auto-connect: collect all aligned pairs first, then filter and process. QList> aligned_pairs; diff --git a/sources/elementsmover.cpp b/sources/elementsmover.cpp index 59233c623..28c8c344d 100644 --- a/sources/elementsmover.cpp +++ b/sources/elementsmover.cpp @@ -17,6 +17,7 @@ */ #include "elementsmover.h" #include "qetproject.h" +#include "autobreakconductor.h" #include "conductorautonumerotation.h" #include "diagram.h" #include "qetgraphicsitem/conductor.h" @@ -178,6 +179,19 @@ void ElementsMover::endMovement() undo_object->setText(quc->text()); } + //Auto-break conductors: for each moved element, break any conductor + //whose path passes through a terminal dock point, and reconnect through + //the element. The element is already at its final position on screen. + if (m_diagram->project()->autoBreakConductor()) + { + QList conductors_handled; + QSet used_terminals; + for (Element *e : m_moved_content.m_elements) { + autoBreakConductors(m_diagram, e, undo_object, + conductors_handled, used_terminals); + } + } + //There is only one element moved, and project authorize auto conductor, //we try auto connection of conductor; typedef DiagramContent dc;