Files
qelectrotech-source-mirror/sources/diagramevent/diagrameventaddelement.cpp
T
2026-08-07 14:58:42 +02:00

513 lines
16 KiB
C++

/*
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 <http://www.gnu.org/licenses/>.
*/
#include "diagrameventaddelement.h"
#include "../qetproject.h"
#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 <QPainterPath>
#include <limits>
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
@param location :location of diagram
@param diagram : diagram owner of this event
@param pos : first pos of item ( optional, by defaut QPointF(0,0) )
*/
DiagramEventAddElement::DiagramEventAddElement(ElementsLocation &location, Diagram *diagram, QPointF pos) :
DiagramEventInterface(diagram),
m_location(location),
m_element(nullptr)
{
//Check if there is an element at this location
if (location.isElement() && location.exist())
{
//location is an element, we build it, if build fail,
//m_running stay to false (by default), so this interface will be deleted at next event
if (buildElement())
{
init();
m_element -> setPos(pos);
m_element -> displayHelpLine(true);
m_element -> setFlag(QGraphicsItem::ItemIsSelectable, false);
m_diagram -> addItem(m_element);
m_running = true;
if (!diagram->views().isEmpty()) {
const auto qde = QETApp::diagramEditorAncestorOf(diagram->views().at(0));
if (qde) {
m_status_bar = qde->statusBar();
}
} else {
m_status_bar.clear();
}
}
}
}
/**
@brief DiagramEventAddElement::~DiagramEventAddElement
Destructor
Enable context menu for each view of diagram
*/
DiagramEventAddElement::~DiagramEventAddElement()
{
if (m_element)
{
m_diagram->removeItem(m_element);
m_element->deleteLater();
}
if (m_status_bar) {
m_status_bar->clearMessage();
}
for (auto view : m_diagram->views())
view -> setContextMenuPolicy(Qt::DefaultContextMenu);
}
/**
@brief DiagramEventAddElement::mouseMoveEvent
Move the element to new pos of mouse
the event is always accepted
@param event
*/
void DiagramEventAddElement::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
if (m_element)
{
const auto pos_{Diagram::snapToGrid(event->scenePos())};
m_element->setPos(pos_);
if (m_status_bar) {
m_status_bar->showMessage(QString("x %1 : y %2").arg(QString::number(pos_.x()), QString::number(pos_.y())));
}
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddElement::mousePressEvent
Do nothing, but return true for not transit the event to other thing in diagram.
the event is always accepted
@param event
*/
void DiagramEventAddElement::mousePressEvent(QGraphicsSceneMouseEvent *event) {
event->setAccepted(true);
}
/**
@brief DiagramEventAddElement::mouseReleaseEvent
Right button finish this event (isRunning = false) and emit finish.
Left button add an element to diagram
the event is always accepted
@param event
*/
void DiagramEventAddElement::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
if (m_element)
{
if (event->button() == Qt::RightButton)
{
m_diagram->removeItem(m_element);
m_element->deleteLater();
m_element = nullptr;
m_running = false;
emit finish();
}
else if (event->button() == Qt::LeftButton)
{
addElement();
}
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddElement::mouseDoubleClickEvent
If mouse left double clic, finish this event (isRunning = false) and emit finish
the event is always accepted
@param event
*/
void DiagramEventAddElement::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (m_element && (event -> button() == Qt::LeftButton))
{
m_diagram->removeItem(m_element);
m_element->deleteLater();
m_element = nullptr;
m_running = false;
emit finish();
}
event->setAccepted(true);
}
/**
@brief DiagramEventAddElement::keyPressEvent
Press space key rotate the element to 90° (return true)
else call DiagramEventInterface::keyPressEvent(event), and return the value.
@param event
*/
void DiagramEventAddElement::keyPressEvent(QKeyEvent *event)
{
if (m_element && event->key() == Qt::Key_Space)
{
m_element->setRotation(m_element->rotation() + 90);
event->setAccepted(true);
}
else {
DiagramEventInterface::keyPressEvent(event);
}
}
/**
@brief DiagramEventAddElement::init
Init this event.
*/
void DiagramEventAddElement::init()
{
foreach(QGraphicsView *view, m_diagram->views())
view->setContextMenuPolicy(Qt::NoContextMenu);
}
/**
@brief DiagramEventAddElement::buildElement
Build the element, if the element is build successfully, we return true, otherwise false
*/
bool DiagramEventAddElement::buildElement()
{
ElementsLocation import_loc = m_diagram->project()->importElement(m_location);
if (import_loc.exist()) {
m_integrate_path = import_loc.projectCollectionPath();
}
else {
qDebug() << "DiagramView::addDroppedElement : Impossible d'ajouter l'element.";
return false;
}
int state;
ElementsLocation loc(m_integrate_path);
m_element = ElementFactory::Instance() -> createElement(loc, nullptr, &state);
//The creation of element failed, we delete it
if (state) {
delete m_element;
return(false);
}
//Everything is good
return true;
}
/**
@brief DiagramEventAddElement::addElement
Add an element at the current pos and current rotation,
if project autoconductor option is enable, and the element can be wired, we do it.
*/
void DiagramEventAddElement::addElement()
{
int state;
Element *element;
if (m_integrate_path.isEmpty())
element = ElementFactory::Instance() -> createElement(m_location, nullptr, &state);
else
element = ElementFactory::Instance() -> createElement(ElementsLocation(m_integrate_path), nullptr, &state);
//Build failed
if (state)
{
delete element;
return;
}
//We must add item to scene (even if addItemCommand do this)
//for create the autoconnection below
element -> setPos(m_element->pos());
element -> setRotation(m_element -> rotation());
m_diagram -> addItem(element);
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.
m_diagram->removeItem(m_element);
//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<Terminal *> broken_endpoints;
if (m_diagram->project()->autoBreakConductor())
{
//Track which conductors we already handled for this element
QList<Conductor *> conductors_handled;
//Track which terminals of the new element are already used (by break or other-connect)
QSet<Terminal *> 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<ConductorMatch> 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<Terminal *, QList<ConductorMatch>> 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<ConductorMatch> &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<qreal>::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<QPair<Terminal *, Terminal *>> aligned_pairs;
if (m_diagram->project()->autoConductor())
aligned_pairs = element->AlignedFreeTerminals();
for (const QPair<Terminal *, Terminal *> &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);
//Autonum the new conductor, the undo command associated for this, have for parent undo_object
ConductorAutoNumerotation can (conductor, m_diagram, undo_object);
can.numerate();
if (m_diagram->freezeNewConductors() || m_diagram->project()->isFreezeNewConductors()) {
conductor->setFreezeLabel(true);
}
}
m_diagram->addItem(m_element);
//Autonum the new element before pushing undo_object, so the counter
//change it triggers is part of the same undo macro as the element's
//own placement (one Ctrl+Z reverts both, instead of silently
//leaving the counter advanced).
element->setUpFormula(true, undo_object);
m_diagram -> undoStack().push(undo_object);
element->freezeNewAddedElement();
}