mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 13:24:14 +02:00
0d08a4e265
Saving an unmodified project produced a different byte stream on every run: QGraphicsScene::items() returns items in stacking order, and ties between same-Z items follow the scene's internal index -- not any content-derived order -- so it isn't reproducible across process runs. The legacy terminal-id table inherits the same instability, since ids are assigned sequentially in element order. Sort list_elements and list_conductors into a deterministic order before serializing, using a key built from data that's actually stable across loads (position), not Element::uuid()/Conductor::uuid(): for an item with no persisted uuid attribute, fromXml() invents a fresh random one on every load, so sorting by uuid would still be non-deterministic across process runs for any legacy file -- which this corpus has plenty of. Also fixes a second, related source of byte-level non-determinism found while verifying the above: Conductor::toXml() unconditionally wrote m_uuid back out, including the synthetic value fromXml() just invented for a conductor with no uuid attribute in the file. Every conductor in every example project checked has no persisted uuid at all, so this alone meant no project with conductors could ever resave identically, regardless of ordering. Conductor gets a m_persist_uuid flag, false only when the uuid it's holding was synthesized rather than loaded, so toXml() stops writing a value that was never meant to be permanent. Deliberately NOT applying the same uuid-persistence fix to Element: element uuids are cross-referenced by other elements' <links_uuids> blocks for master/slave/report linking (element.cpp, tmp_uuids_link, matched by elmt->uuid() == stored uuid on load). Making an element's own uuid non-persistent would silently break that match for any linked element without one already -- a real regression, not a theoretical one. Left as a smaller, separate residual: 1-6 elements per project across the corpus (a few tenths of a percent) still get a fresh uuid on each load, same class of bug, needs the link-aware version of this fix instead of this one. Verified against 8 example projects (the ones with conductors, plus the two zero-conductor control cases from FINDINGS.md F002), 5 resaves each in isolated HOME/XDG environments: - Element and conductor ORDER: 0 churning sections across the whole corpus (previously the majority of diagrams in industrial.qet, m_000.qet and tremie_vibrante.qet churned on every run). - Conductor uuid VALUES: 0 churn (previously every conductor in every project, since none have a persisted uuid). - 6 of 8 projects are now byte-for-byte identical (md5) across all 5 runs. The remaining 2 (industrial.qet, m_000.qet) differ only in the handful of element uuids covered by the known Element residual above -- confirmed by checking those uuids specifically, not inferred. - Element/conductor counts before and after resave match exactly on every project (no data loss from the sort). Fixes #754.
2133 lines
64 KiB
C++
2133 lines
64 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 "../qetgraphicsitem/conductor.h"
|
|
#include "../qetproject.h"
|
|
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
|
#include "../autoNum/numerotationcontextcommands.h"
|
|
#include "../conductorautonumerotation.h"
|
|
#include "../conductorsegment.h"
|
|
#include "../conductorsegmentprofile.h"
|
|
#include "../diagram.h"
|
|
#include "../diagramcommands.h"
|
|
#include "../qetdiagrameditor.h"
|
|
#include "../qetgraphicsitem/terminal.h"
|
|
#include "../ui/conductorpropertiesdialog.h"
|
|
#include "conductortextitem.h"
|
|
#include "element.h"
|
|
#include "../QetGraphicsItemModeler/qetgraphicshandleritem.h"
|
|
#include "../utils/qetutils.h"
|
|
|
|
#include <QMultiHash>
|
|
#include <QtDebug>
|
|
|
|
#define PR(x) qDebug() << #x " = " << x;
|
|
|
|
bool Conductor::pen_and_brush_initialized = false;
|
|
QPen Conductor::conductor_pen = QPen();
|
|
QBrush Conductor::conductor_brush = QBrush();
|
|
|
|
|
|
class ConductorXmlRetroCompatibility
|
|
{
|
|
friend class Conductor;
|
|
|
|
static void loadSequential(const QDomElement &dom_element, const QString& seq, QStringList* list)
|
|
{
|
|
int i = 0;
|
|
while (!dom_element.attribute(seq + QString::number(i+1)).isEmpty())
|
|
{
|
|
list->append(dom_element.attribute(seq + QString::number(i+1)));
|
|
i++;
|
|
}
|
|
}
|
|
|
|
static void loadSequential(const QDomElement &dom_element, Conductor *conductor)
|
|
{
|
|
autonum::sequentialNumbers sn;
|
|
|
|
loadSequential(dom_element,"sequ_",&sn.unit);
|
|
loadSequential(dom_element,"sequf_",&sn.unit_folio);
|
|
loadSequential(dom_element,"seqt_",&sn.ten);
|
|
loadSequential(dom_element,"seqtf_",&sn.ten_folio);
|
|
loadSequential(dom_element,"seqh_",&sn.hundred);
|
|
loadSequential(dom_element,"seqhf_",&sn.hundred_folio);
|
|
|
|
conductor->rSequenceNum() = sn;
|
|
}
|
|
};
|
|
|
|
/**
|
|
@brief Conductor::Conductor
|
|
Default constructor.
|
|
@param p1 : first terminal of this conductor.
|
|
@param p2 : second terminal of this conductor.
|
|
*/
|
|
Conductor::Conductor(Terminal *p1, Terminal* p2) :
|
|
terminal1(p1),
|
|
terminal2(p2),
|
|
m_mouse_over(false),
|
|
m_text_item(nullptr),
|
|
segments(nullptr),
|
|
m_moving_segment(false),
|
|
modified_path(false),
|
|
has_to_save_profile(false),
|
|
must_highlight_(Conductor::None)
|
|
{
|
|
//set Zvalue at 11 to be upper than the DiagramImageItem and element
|
|
setZValue(11);
|
|
m_previous_z_value = zValue();
|
|
m_uuid = QUuid::createUuid();
|
|
|
|
//Add this conductor to the list of conductors of each of the two terminals
|
|
bool ajout_p1 = terminal1 -> addConductor(this);
|
|
bool ajout_p2 = terminal2 -> addConductor(this);
|
|
//m_valid become false if the conductor can't be added to terminal (conductor already exist)
|
|
m_valid = (!ajout_p1 || !ajout_p2) ? false : true;
|
|
|
|
//Default attribute to paint a conductor
|
|
if (!pen_and_brush_initialized)
|
|
{
|
|
conductor_pen.setJoinStyle(Qt::MiterJoin);
|
|
conductor_pen.setCapStyle(Qt::SquareCap);
|
|
conductor_pen.setColor(Qt::black);
|
|
conductor_pen.setStyle(Qt::SolidLine);
|
|
conductor_pen.setWidthF(1.0);
|
|
conductor_brush.setColor(Qt::white);
|
|
conductor_brush.setStyle(Qt::NoBrush);
|
|
pen_and_brush_initialized = true;
|
|
}
|
|
|
|
//By default, the 4 profiles are nuls -> we must use priv_calculeConductor
|
|
conductor_profiles.insert(Qt::TopLeftCorner, ConductorProfile());
|
|
conductor_profiles.insert(Qt::TopRightCorner, ConductorProfile());
|
|
conductor_profiles.insert(Qt::BottomLeftCorner, ConductorProfile());
|
|
conductor_profiles.insert(Qt::BottomRightCorner, ConductorProfile());
|
|
|
|
//Generate the path of this conductor.
|
|
generateConductorPath(terminal1 -> dockConductor(), terminal1 -> orientation(), terminal2 -> dockConductor(), terminal2 -> orientation());
|
|
setFlags(QGraphicsItem::ItemIsSelectable | QGraphicsItem::ItemSendsScenePositionChanges);
|
|
setAcceptHoverEvents(true);
|
|
|
|
// Add the text field
|
|
m_text_item = new ConductorTextItem(m_properties.text, this);
|
|
connect(m_text_item, &ConductorTextItem::textEdited, this, &Conductor::displayedTextChanged);
|
|
|
|
//Set the default conductor properties.
|
|
if (p1->diagram())
|
|
setProperties(p1->diagram()->defaultConductorProperties);
|
|
else if (p2->diagram())
|
|
setProperties(p2->diagram()->defaultConductorProperties);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::~Conductor
|
|
Destructor. The conductor is removed from its terminal
|
|
*/
|
|
Conductor::~Conductor()
|
|
{
|
|
removeHandler();
|
|
terminal1->removeConductor(this);
|
|
terminal2->removeConductor(this);
|
|
deleteSegments();
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::isValid
|
|
@return true if conductor is valid else false;
|
|
A non valid conductor, is a conductor without two terminal
|
|
*/
|
|
bool Conductor::isValid() const
|
|
{
|
|
return m_valid;
|
|
}
|
|
|
|
/**
|
|
Met a jour la representation graphique du conducteur en recalculant son
|
|
trace. Cette fonction est typiquement appelee lorsqu'une seule des bornes du
|
|
conducteur a change de position.
|
|
@param rect Rectangle a mettre a jour
|
|
@see QGraphicsPathItem::update()
|
|
*/
|
|
void Conductor::updatePath(const QRectF &rect) {
|
|
QPointF p1, p2;
|
|
p1 = terminal1 -> dockConductor();
|
|
p2 = terminal2 -> dockConductor();
|
|
if (segmentsCount() && !conductor_profiles[currentPathType()].isNull())
|
|
updateConductorPath(p1, terminal1 -> orientation(), p2, terminal2 -> orientation());
|
|
else
|
|
generateConductorPath(p1, terminal1 -> orientation(), p2, terminal2 -> orientation());
|
|
calculateTextItemPosition();
|
|
QGraphicsObject::update(rect);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::segmentsToPath
|
|
Generate the QPainterPath from the list of points
|
|
*/
|
|
void Conductor::segmentsToPath()
|
|
{
|
|
QPainterPath path;
|
|
|
|
if (segments == nullptr)
|
|
setPath(path);
|
|
|
|
//Start the path
|
|
path.moveTo(segments -> firstPoint());
|
|
//Each segments
|
|
ConductorSegment *segment = segments;
|
|
while(segment -> hasNextSegment()) {
|
|
path.lineTo(segment -> secondPoint());
|
|
segment = segment -> nextSegment();
|
|
}
|
|
//Finish the path
|
|
path.lineTo(segment -> secondPoint());
|
|
|
|
setPath(path);
|
|
|
|
//If conductor is selected and he's not being modified
|
|
//we update the position of the handlers
|
|
if (isSelected() && !m_moving_segment)
|
|
{
|
|
if(handlerPoints().size() == m_handler_vector.size())
|
|
adjustHandlerPos();
|
|
else
|
|
{
|
|
removeHandler();
|
|
addHandler();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Gere les updates
|
|
@param p1 Coordonnees du point d'amarrage de la borne 1
|
|
@param o1 Orientation de la borne 1
|
|
@param p2 Coordonnees du point d'amarrage de la borne 2
|
|
@param o2 Orientation de la borne 2
|
|
*/
|
|
void Conductor::updateConductorPath(const QPointF &p1, Qet::Orientation o1, const QPointF &p2, Qet::Orientation o2) {
|
|
Q_UNUSED(o1);
|
|
Q_UNUSED(o2);
|
|
|
|
ConductorProfile &conductor_profile = conductor_profiles[currentPathType()];
|
|
|
|
Q_ASSERT_X(conductor_profile.segmentsCount(QET::Both) > 1, "Conductor::priv_modifieConductor", "pas de points a modifier");
|
|
Q_ASSERT_X(!conductor_profile.isNull(), "Conductor::priv_modifieConductor", "pas de profil utilisable");
|
|
|
|
// recupere les coordonnees fournies des bornes
|
|
// retrieve the coordinates provided for the terminals
|
|
QPointF new_p1 = mapFromScene(p1);
|
|
QPointF new_p2 = mapFromScene(p2);
|
|
QRectF new_rect = QRectF(new_p1, new_p2);
|
|
|
|
// recupere la largeur et la hauteur du profil
|
|
// retrieve the width and height of the profile
|
|
qreal profile_width = conductor_profile.width();
|
|
qreal profile_height = conductor_profile.height();
|
|
|
|
// calcule les differences verticales et horizontales a appliquer
|
|
// calculates the vertical and horizontal differences to be applied
|
|
qreal h_diff = (qAbs(new_rect.width()) - qAbs(profile_width) ) * getSign(profile_width);
|
|
qreal v_diff = (qAbs(new_rect.height()) - qAbs(profile_height)) * getSign(profile_height);
|
|
|
|
// applique les differences aux segments
|
|
// apply the differences to the segments
|
|
QMultiHash<ConductorSegmentProfile *, qreal> segments_lengths;
|
|
segments_lengths.unite(shareOffsetBetweenSegments(h_diff, conductor_profile.horizontalSegments()));
|
|
segments_lengths.unite(shareOffsetBetweenSegments(v_diff, conductor_profile.verticalSegments()));
|
|
|
|
// en deduit egalement les coefficients d'inversion (-1 pour une inversion, +1 pour conserver le meme sens)
|
|
// also deduce the inversion coefficients (-1 for inversion, +1 to keep the same direction)
|
|
int horiz_coeff = getCoeff(new_rect.width(), profile_width);
|
|
int verti_coeff = getCoeff(new_rect.height(), profile_height);
|
|
|
|
// genere les nouveaux points
|
|
// generate the new points
|
|
QList<QPointF> points;
|
|
points << new_p1;
|
|
int limit = conductor_profile.segments.count() - 1;
|
|
for (int i = 0 ; i < limit ; ++ i) {
|
|
// dernier point
|
|
// last point
|
|
QPointF previous_point = points.last();
|
|
|
|
// profil de segment de conducteur en cours
|
|
// current conductor segment profile
|
|
ConductorSegmentProfile *csp = conductor_profile.segments.at(i);
|
|
|
|
// coefficient et offset a utiliser pour ce point
|
|
// coefficient and offset to be used for this point
|
|
qreal coeff = csp -> isHorizontal ? horiz_coeff : verti_coeff;
|
|
qreal offset_applied = segments_lengths.value(csp);
|
|
|
|
// applique l'offset et le coeff au point
|
|
// apply coefficient and offset to point
|
|
if (csp -> isHorizontal) {
|
|
points << QPointF (
|
|
previous_point.x() + (coeff * offset_applied),
|
|
previous_point.y()
|
|
);
|
|
} else {
|
|
points << QPointF (
|
|
previous_point.x(),
|
|
previous_point.y() + (coeff * offset_applied)
|
|
);
|
|
}
|
|
}
|
|
points << new_p2;
|
|
pointsToSegments(points);
|
|
segmentsToPath();
|
|
}
|
|
|
|
/**
|
|
@param offset Longueur a repartir entre les segments
|
|
@param segments_list Segments sur lesquels il faut repartir la longueur
|
|
@param precision seuil en-deca duquel on considere qu'il ne reste rien a repartir
|
|
*/
|
|
QHash<ConductorSegmentProfile *, qreal> Conductor::shareOffsetBetweenSegments(
|
|
const qreal &offset,
|
|
const QList<ConductorSegmentProfile *> &segments_list,
|
|
const qreal &precision
|
|
) const
|
|
{
|
|
// construit le QHash qui sera retourne
|
|
QHash<ConductorSegmentProfile *, qreal> segments_hash;
|
|
foreach(ConductorSegmentProfile *csp, segments_list) {
|
|
segments_hash.insert(csp, csp -> length);
|
|
}
|
|
|
|
// memorise le signe de la longueur de chaque segement
|
|
QHash<ConductorSegmentProfile *, int> segments_signs;
|
|
foreach(ConductorSegmentProfile *csp, segments_hash.keys()) {
|
|
segments_signs.insert(csp, getSign(csp -> length));
|
|
}
|
|
|
|
//qDebug() << "repartition d'un offset de" << offset << "px sur" << segments_list.count() << "segments";
|
|
|
|
// repartit l'offset sur les segments
|
|
qreal remaining_offset = offset;
|
|
while (remaining_offset > precision || remaining_offset < -precision) {
|
|
// recupere le nombre de segments differents ayant une longueur non nulle
|
|
uint segments_count = 0;
|
|
foreach(ConductorSegmentProfile *csp, segments_hash.keys()) if (segments_hash[csp]) ++ segments_count;
|
|
//qDebug() << " remaining_offset =" << remaining_offset;
|
|
qreal local_offset = remaining_offset / segments_count;
|
|
//qDebug() << " repartition d'un offset local de" << local_offset << "px sur" << segments_count << "segments";
|
|
remaining_offset = 0.0;
|
|
foreach(ConductorSegmentProfile *csp, segments_hash.keys()) {
|
|
// ignore les segments de longueur nulle
|
|
if (!segments_hash[csp]) continue;
|
|
// applique l'offset au segment
|
|
//qreal segment_old_length = segments_hash[csp];
|
|
segments_hash[csp] += local_offset;
|
|
|
|
// (la longueur du segment change de signe) <=> (le segment n'a pu absorbe tout l'offset)
|
|
if (segments_signs[csp] != getSign(segments_hash[csp])) {
|
|
|
|
// on remet le trop-plein dans la reserve d'offset
|
|
remaining_offset += qAbs(segments_hash[csp]) * getSign(local_offset);
|
|
//qDebug() << " trop-plein de" << qAbs(segments_hash[csp]) * getSign(local_offset) << "remaining_offset =" << remaining_offset;
|
|
segments_hash[csp] = 0.0;
|
|
} else {
|
|
//qDebug() << " offset local de" << local_offset << "accepte";
|
|
}
|
|
}
|
|
}
|
|
|
|
return(segments_hash);
|
|
}
|
|
|
|
/**
|
|
Calcule un trajet "par defaut" pour le conducteur
|
|
@param p1 Coordonnees du point d'amarrage de la borne 1
|
|
@param o1 Orientation de la borne 1
|
|
@param p2 Coordonnees du point d'amarrage de la borne 2
|
|
@param o2 Orientation de la borne 2
|
|
*/
|
|
void Conductor::generateConductorPath(const QPointF &p1, Qet::Orientation o1, const QPointF &p2, Qet::Orientation o2) {
|
|
QPointF sp1, sp2, depart, newp1, newp2, arrivee, depart0, arrivee0;
|
|
Qet::Orientation ori_depart, ori_arrivee;
|
|
|
|
// s'assure qu'il n'y a ni points
|
|
QList<QPointF> points;
|
|
|
|
// mappe les points par rapport a la scene
|
|
sp1 = mapFromScene(p1);
|
|
sp2 = mapFromScene(p2);
|
|
|
|
// prolonge les bornes
|
|
newp1 = extendTerminal(sp1, o1);
|
|
newp2 = extendTerminal(sp2, o2);
|
|
|
|
// distingue le depart de l'arrivee : le trajet se fait toujours de gauche a droite (apres prolongation)
|
|
if (newp1.x() <= newp2.x()) {
|
|
depart = newp1;
|
|
arrivee = newp2;
|
|
depart0 = sp1;
|
|
arrivee0 = sp2;
|
|
ori_depart = o1;
|
|
ori_arrivee = o2;
|
|
} else {
|
|
depart = newp2;
|
|
arrivee = newp1;
|
|
depart0 = sp2;
|
|
arrivee0 = sp1;
|
|
ori_depart = o2;
|
|
ori_arrivee = o1;
|
|
}
|
|
|
|
// debut du trajet
|
|
points << depart0;
|
|
|
|
// prolongement de la borne de depart
|
|
// extend start terminal
|
|
points << depart;
|
|
|
|
// commence le vrai trajet
|
|
// starts the real path
|
|
if (depart.y() < arrivee.y()) {
|
|
// trajet descendant
|
|
if ((ori_depart == Qet::North && (ori_arrivee == Qet::South || ori_arrivee == Qet::West)) || (ori_depart == Qet::East && ori_arrivee == Qet::West)) {
|
|
// cas "3"
|
|
int ligne_inter_x = qRound(depart.x() + arrivee.x()) / 2;
|
|
while (ligne_inter_x % Diagram::xGrid) -- ligne_inter_x;
|
|
points << QPointF(ligne_inter_x, depart.y());
|
|
points << QPointF(ligne_inter_x, arrivee.y());
|
|
} else if ((ori_depart == Qet::South && (ori_arrivee == Qet::North || ori_arrivee == Qet::East)) || (ori_depart == Qet::West && ori_arrivee == Qet::East)) {
|
|
// cas "4"
|
|
int ligne_inter_y = qRound(depart.y() + arrivee.y()) / 2;
|
|
while (ligne_inter_y % Diagram::yGrid) -- ligne_inter_y;
|
|
points << QPointF(depart.x(), ligne_inter_y);
|
|
points << QPointF(arrivee.x(), ligne_inter_y);
|
|
} else if ((ori_depart == Qet::North || ori_depart == Qet::East) && (ori_arrivee == Qet::North || ori_arrivee == Qet::East)) {
|
|
points << QPointF(arrivee.x(), depart.y()); // cas "2"
|
|
} else {
|
|
points << QPointF(depart.x(), arrivee.y()); // cas "1"
|
|
}
|
|
} else {
|
|
// trajet montant
|
|
if ((ori_depart == Qet::West && (ori_arrivee == Qet::East || ori_arrivee == Qet::South)) || (ori_depart == Qet::North && ori_arrivee == Qet::South)) {
|
|
// cas "3"
|
|
int ligne_inter_y = qRound(depart.y() + arrivee.y()) / 2;
|
|
while (ligne_inter_y % Diagram::yGrid) -- ligne_inter_y;
|
|
points << QPointF(depart.x(), ligne_inter_y);
|
|
points << QPointF(arrivee.x(), ligne_inter_y);
|
|
} else if ((ori_depart == Qet::East && (ori_arrivee == Qet::West || ori_arrivee == Qet::North)) || (ori_depart == Qet::South && ori_arrivee == Qet::North)) {
|
|
// cas "4"
|
|
int ligne_inter_x = qRound(depart.x() + arrivee.x()) / 2;
|
|
while (ligne_inter_x % Diagram::xGrid) -- ligne_inter_x;
|
|
points << QPointF(ligne_inter_x, depart.y());
|
|
points << QPointF(ligne_inter_x, arrivee.y());
|
|
} else if ((ori_depart == Qet::West || ori_depart == Qet::North) && (ori_arrivee == Qet::West || ori_arrivee == Qet::North)) {
|
|
points << QPointF(depart.x(), arrivee.y()); // cas "2"
|
|
} else {
|
|
points << QPointF(arrivee.x(), depart.y()); // cas "1"
|
|
}
|
|
}
|
|
|
|
// fin du vrai trajet
|
|
points << arrivee;
|
|
|
|
// prolongement de la borne d'arrivee
|
|
points << arrivee0;
|
|
|
|
// inverse eventuellement l'ordre des points afin que le trajet soit exprime de la borne 1 vers la borne 2
|
|
if (newp1.x() > newp2.x()) {
|
|
QList<QPointF> points2;
|
|
for (int i = points.size() - 1 ; i >= 0 ; -- i) points2 << points.at(i);
|
|
points = points2;
|
|
}
|
|
|
|
pointsToSegments(points);
|
|
segmentsToPath();
|
|
}
|
|
|
|
/**
|
|
* @brief Conductor::extendTerminal
|
|
* @param terminal : point to extend (must be the docking point of a terminal)
|
|
* @param terminal_orientation : the orientation of the terminal
|
|
* @param ext_size : how many to extrend (10 by default)
|
|
* @return the point with an extension of @ext_size
|
|
* and rounded to nearest multiple of ten
|
|
* in order to be snapped to the grid of the diagram.
|
|
*/
|
|
QPointF Conductor::extendTerminal(const QPointF &terminal, Qet::Orientation terminal_orientation, qreal ext_size)
|
|
{
|
|
QPointF extended_terminal;
|
|
switch(terminal_orientation) {
|
|
case Qet::North:
|
|
extended_terminal = QPointF(terminal.x(),
|
|
std::round((terminal.y() - ext_size)/10)*10);
|
|
break;
|
|
case Qet::East:
|
|
extended_terminal = QPointF(std::round((terminal.x() + ext_size)/10)*10,
|
|
terminal.y());
|
|
break;
|
|
case Qet::South:
|
|
extended_terminal = QPointF(terminal.x(),
|
|
std::round((terminal.y() + ext_size)/10)*10);
|
|
break;
|
|
case Qet::West:
|
|
extended_terminal = QPointF(std::round((terminal.x() - ext_size)/10)*10,
|
|
terminal.y());
|
|
break;
|
|
default: extended_terminal = terminal;
|
|
}
|
|
|
|
return(extended_terminal);
|
|
}
|
|
|
|
/**
|
|
Dessine le conducteur sans antialiasing.
|
|
@param painter Le QPainter a utiliser pour dessiner le conducteur
|
|
@param options Les options de style pour le conducteur
|
|
@param qw Le QWidget sur lequel on dessine
|
|
*/
|
|
void Conductor::paint(QPainter *painter, const QStyleOptionGraphicsItem *options, QWidget *qw)
|
|
{
|
|
Q_UNUSED(qw);
|
|
painter -> save();
|
|
painter -> setRenderHint(QPainter::Antialiasing, false);
|
|
|
|
// Set the color of conductor
|
|
QColor final_conductor_color(m_properties.color);
|
|
if (must_highlight_ == Normal) {
|
|
final_conductor_color = QColor::fromRgb(69, 137, 255, 255);
|
|
} else if (must_highlight_ == Alert) {
|
|
final_conductor_color =QColor::fromRgb(255, 69, 0, 255);
|
|
} else if (isSelected()) {
|
|
final_conductor_color = Qt::red;
|
|
} else {
|
|
if (Diagram *parent_diagram = diagram()) {
|
|
if (!parent_diagram -> drawColoredConductors()) {
|
|
final_conductor_color = Qt::black;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Draw the conductor bigger when is hovered
|
|
conductor_pen.setWidthF(m_mouse_over? (m_properties.cond_size) +4 : (m_properties.cond_size));
|
|
|
|
//Set the QPen and QBrush to the QPainter
|
|
painter -> setBrush(conductor_brush);
|
|
QPen final_conductor_pen = conductor_pen;
|
|
|
|
//Set the conductor style
|
|
final_conductor_pen.setColor(final_conductor_color);
|
|
final_conductor_pen.setStyle(m_properties.style);
|
|
final_conductor_pen.setJoinStyle(Qt::SvgMiterJoin); // better rendering with dot
|
|
|
|
//Use a cosmetic line, below a certain zoom
|
|
if (options && options->levelOfDetailFromTransform(painter->worldTransform()) < 0.5)
|
|
{
|
|
final_conductor_pen.setCosmetic(true);
|
|
}
|
|
|
|
painter -> setPen(final_conductor_pen);
|
|
|
|
//Draw the conductor
|
|
painter -> drawPath(path());
|
|
//Draw the second color
|
|
if(m_properties.m_bicolor)
|
|
{
|
|
final_conductor_pen.setColor(m_properties.m_color_2);
|
|
final_conductor_pen.setStyle(Qt::CustomDashLine);
|
|
QVector<qreal> dash_pattern;
|
|
dash_pattern << m_properties.m_dash_size << m_properties.m_dash_size *2;
|
|
final_conductor_pen.setDashPattern(dash_pattern);
|
|
painter->save();
|
|
painter->setPen(final_conductor_pen);
|
|
painter->drawPath(path());
|
|
painter->restore();
|
|
}
|
|
|
|
if (m_properties.type == ConductorProperties::Single) {
|
|
painter -> setBrush(final_conductor_color);
|
|
m_properties.singleLineProperties.draw(
|
|
painter,
|
|
middleSegment() -> isHorizontal() ? QET::Horizontal : QET::Vertical,
|
|
QRectF(middleSegment() -> middle() - QPointF(12.0, 12.0), QSizeF(24.0, 24.0))
|
|
);
|
|
if (isSelected()) painter -> setBrush(Qt::NoBrush);
|
|
}
|
|
|
|
//Draw the junctions
|
|
QList<QPointF> junctions_list = junctions();
|
|
if (!junctions_list.isEmpty()) {
|
|
final_conductor_pen.setStyle(Qt::SolidLine);
|
|
QBrush junction_brush(final_conductor_color, Qt::SolidPattern);
|
|
painter -> setPen(final_conductor_pen);
|
|
painter -> setBrush(junction_brush);
|
|
painter -> setRenderHint(QPainter::Antialiasing, true);
|
|
foreach(QPointF point, junctions_list) {
|
|
painter -> drawEllipse(QRectF(point.x() - 1.5, point.y() - 1.5, 3.0, 3.0));
|
|
}
|
|
}
|
|
|
|
painter -> restore();
|
|
}
|
|
|
|
/// @return le Diagram auquel ce conducteur appartient, ou 0 si ce conducteur est independant
|
|
Diagram *Conductor::diagram() const
|
|
{
|
|
return(qobject_cast<Diagram *>(scene()));
|
|
}
|
|
|
|
/**4
|
|
@return le champ de texte associe a ce conducteur
|
|
*/
|
|
ConductorTextItem *Conductor::textItem() const
|
|
{
|
|
return(m_text_item);
|
|
}
|
|
|
|
/**
|
|
Methode de validation d'element XML
|
|
@param e Un element XML sense represente un Conducteur
|
|
@return true si l'element XML represente bien un Conducteur ; false sinon
|
|
*/
|
|
bool Conductor::valideXml(QDomElement &e){
|
|
// verifie le nom du tag
|
|
if (e.tagName() != "conductor") return(false);
|
|
|
|
// verifie la presence des attributs minimaux
|
|
if (!e.hasAttribute("terminal1")) return(false);
|
|
if (!e.hasAttribute("terminal2")) return(false);
|
|
|
|
bool conv_ok;
|
|
// parse l'abscisse
|
|
if (e.hasAttribute("element1")) {
|
|
if (QUuid(e.attribute("element1")).isNull())
|
|
return false;
|
|
if (QUuid(e.attribute("terminal1")).isNull())
|
|
return false;
|
|
} else {
|
|
e.attribute("terminal1").toInt(&conv_ok);
|
|
if (!conv_ok) return(false);
|
|
}
|
|
|
|
// parse l'ordonnee
|
|
if (e.hasAttribute("element2")) {
|
|
if (QUuid(e.attribute("element2")).isNull())
|
|
return false;
|
|
if (QUuid(e.attribute("terminal2")).isNull())
|
|
return false;
|
|
} else {
|
|
e.attribute("terminal2").toInt(&conv_ok);
|
|
if (!conv_ok) return(false);
|
|
}
|
|
return(true);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::mouseDoubleClickEvent
|
|
Manage the mouse double click
|
|
@param event
|
|
*/
|
|
void Conductor::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event) {
|
|
event->accept();
|
|
editProperty();
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::mousePressEvent
|
|
Manage the mouse press event
|
|
@param event
|
|
*/
|
|
void Conductor::mousePressEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
QGraphicsObject::mousePressEvent(event);
|
|
|
|
if (event->modifiers() & Qt::ControlModifier)
|
|
setSelected(!isSelected());
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::mouseReleaseEvent
|
|
@param event
|
|
*/
|
|
void Conductor::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
|
|
{
|
|
if (!(event -> modifiers() & Qt::ControlModifier))
|
|
QGraphicsObject::mouseReleaseEvent(event);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::hoverEnterEvent
|
|
Manage the hover enter event
|
|
@param event
|
|
*/
|
|
void Conductor::hoverEnterEvent(QGraphicsSceneHoverEvent *event) {
|
|
Q_UNUSED(event);
|
|
m_mouse_over = true;
|
|
update();
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::hoverLeaveEvent
|
|
Manage the mouse leave event
|
|
@param event
|
|
*/
|
|
void Conductor::hoverLeaveEvent(QGraphicsSceneHoverEvent *event) {
|
|
Q_UNUSED(event);
|
|
update();
|
|
m_mouse_over = false;
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::itemChange
|
|
@param change
|
|
@param value
|
|
@return
|
|
*/
|
|
QVariant Conductor::itemChange(GraphicsItemChange change, const QVariant &value)
|
|
{
|
|
if (change == QGraphicsItem::ItemSelectedChange)
|
|
{
|
|
if (value.toBool())
|
|
{
|
|
m_previous_z_value = zValue();
|
|
setZValue(qAbs(m_previous_z_value) + 10000);
|
|
addHandler();
|
|
}
|
|
else
|
|
{
|
|
setZValue(m_previous_z_value);
|
|
removeHandler();
|
|
}
|
|
}
|
|
else if (change == QGraphicsItem::ItemSceneHasChanged)
|
|
{
|
|
calculateTextItemPosition();
|
|
|
|
if(!scene())
|
|
removeHandler();
|
|
else if (scene() && isSelected())
|
|
addHandler();
|
|
}
|
|
else if (change == QGraphicsItem::ItemVisibleHasChanged) {
|
|
calculateTextItemPosition();
|
|
}
|
|
else if (change == QGraphicsItem::ItemPositionHasChanged && isSelected()) {
|
|
adjustHandlerPos();
|
|
}
|
|
|
|
return(QGraphicsObject::itemChange(change, value));
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::sceneEventFilter
|
|
@param watched
|
|
@param event
|
|
@return
|
|
*/
|
|
bool Conductor::sceneEventFilter(QGraphicsItem *watched, QEvent *event)
|
|
{
|
|
//Watched must be an handler
|
|
if(watched->type() == QetGraphicsHandlerItem::Type)
|
|
{
|
|
QetGraphicsHandlerItem *qghi = qgraphicsitem_cast<QetGraphicsHandlerItem *>(watched);
|
|
|
|
if(m_handler_vector.contains(qghi)) //Handler must be in m_vector_index, then we can start resize
|
|
{
|
|
m_vector_index = m_handler_vector.indexOf(qghi);
|
|
if (m_vector_index != -1)
|
|
{
|
|
if(event->type() == QEvent::GraphicsSceneMousePress) //Click
|
|
{
|
|
handlerMousePressEvent(qghi, static_cast<QGraphicsSceneMouseEvent *>(event));
|
|
return true;
|
|
}
|
|
else if(event->type() == QEvent::GraphicsSceneMouseMove) //Move
|
|
{
|
|
handlerMouseMoveEvent(qghi, static_cast<QGraphicsSceneMouseEvent *>(event));
|
|
return true;
|
|
}
|
|
else if (event->type() == QEvent::GraphicsSceneMouseRelease) //Release
|
|
{
|
|
handlerMouseReleaseEvent(qghi, static_cast<QGraphicsSceneMouseEvent *>(event));
|
|
return true;
|
|
}
|
|
else if (event->type() == QEvent::GraphicsSceneMouseDoubleClick) //Double click
|
|
{
|
|
editProperty();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::adjustHandlerPos
|
|
Adjust the position of the handler item
|
|
*/
|
|
void Conductor::adjustHandlerPos()
|
|
{
|
|
if (m_handler_vector.isEmpty())
|
|
return;
|
|
|
|
if (m_handler_vector.size() == handlerPoints().size())
|
|
{
|
|
QVector <QPointF> points_vector = mapToScene(handlerPoints());
|
|
for (int i = 0 ; i < points_vector.size() ; ++i)
|
|
m_handler_vector.at(i)->setPos(points_vector.at(i));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::handlerMousePressEvent
|
|
@param qghi
|
|
@param event
|
|
*/
|
|
void Conductor::handlerMousePressEvent(QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event)
|
|
{
|
|
Q_UNUSED(event);
|
|
|
|
//we get the segment corresponding to the handler
|
|
if (m_vector_index > -1)
|
|
{
|
|
qghi->setColor(Qt::cyan);
|
|
m_moving_segment = true;
|
|
m_moved_segment = segmentsList().at(m_vector_index+1);
|
|
before_mov_text_pos_ = m_text_item -> pos();
|
|
|
|
for(QetGraphicsHandlerItem *handler : m_handler_vector)
|
|
if(handler != qghi)
|
|
handler->hide();
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::handlerMouseMoveEvent
|
|
@param qghi
|
|
@param event
|
|
*/
|
|
void Conductor::handlerMouseMoveEvent(QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event)
|
|
{
|
|
if (m_moving_segment)
|
|
{
|
|
//Snap the mouse pos to grid
|
|
QPointF pos_ = Diagram::snapToGrid(mapFromScene(event->scenePos()));
|
|
|
|
//Position of the last point
|
|
QPointF p = m_moved_segment -> middle();
|
|
|
|
//Calculate the movement
|
|
m_moved_segment -> moveX(pos_.x() - p.x());
|
|
m_moved_segment -> moveY(pos_.y() - p.y());
|
|
|
|
//Apply the movement
|
|
modified_path = true;
|
|
has_to_save_profile = true;
|
|
segmentsToPath();
|
|
calculateTextItemPosition();
|
|
qghi->setPos(mapToScene(m_moved_segment->middle()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::handlerMouseReleaseEvent
|
|
@param qghi
|
|
@param event
|
|
*/
|
|
void Conductor::handlerMouseReleaseEvent(QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event)
|
|
{
|
|
Q_UNUSED(event);
|
|
Q_UNUSED(qghi);
|
|
|
|
m_vector_index = -1;
|
|
|
|
m_moving_segment = false;
|
|
if (has_to_save_profile)
|
|
{
|
|
saveProfile();
|
|
has_to_save_profile = false;
|
|
}
|
|
//When handler is released, the conductor can have more segment than before the handler was moved
|
|
//then we remove all handles and new ones are added
|
|
removeHandler();
|
|
addHandler();
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::addHandler
|
|
Add handlers for this item
|
|
*/
|
|
void Conductor::addHandler()
|
|
{
|
|
if (m_handler_vector.isEmpty() && scene())
|
|
{
|
|
m_handler_vector = QetGraphicsHandlerItem::handlerForPoint(mapToScene(handlerPoints()), QETUtils::graphicsHandlerSize(this));
|
|
|
|
for(QetGraphicsHandlerItem *handler : m_handler_vector)
|
|
{
|
|
handler->setColor(Qt::blue);
|
|
scene()->addItem(handler);
|
|
handler->installSceneEventFilter(this);
|
|
handler->setZValue(this->zValue()+1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::removeHandler
|
|
Remove the handlers of this item
|
|
*/
|
|
void Conductor::removeHandler()
|
|
{
|
|
if (!m_handler_vector.isEmpty())
|
|
{
|
|
qDeleteAll(m_handler_vector);
|
|
m_handler_vector.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::boundingRect
|
|
@return
|
|
*/
|
|
QRectF Conductor::boundingRect() const
|
|
{
|
|
QRectF br = shape().boundingRect();
|
|
return br.adjusted(-10, -10, 10, 10);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::shape
|
|
@return the shape of conductor.
|
|
The shape thickness is bigger when conductor is hovered
|
|
*/
|
|
QPainterPath Conductor::shape() const
|
|
{
|
|
QPainterPathStroker pps;
|
|
pps.setWidth(m_mouse_over? 5 : 1);
|
|
pps.setJoinStyle(conductor_pen.joinStyle());
|
|
|
|
QPainterPath shape_(pps.createStroke(path()));
|
|
|
|
return shape_;
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::nearShape
|
|
@return : An area in which it is considered a point is near this conductor.
|
|
*/
|
|
QPainterPath Conductor::nearShape() const
|
|
{
|
|
QPainterPathStroker pps;
|
|
pps.setWidth(1300);
|
|
pps.setJoinStyle(conductor_pen.joinStyle());
|
|
return pps.createStroke(path());
|
|
}
|
|
|
|
/**
|
|
@param type Type de Segments
|
|
@return Le nombre de segments composant le conducteur.
|
|
*/
|
|
uint Conductor::segmentsCount(QET::ConductorSegmentType type) const
|
|
{
|
|
QList<ConductorSegment *> segments_list = segmentsList();
|
|
if (type == QET::Both) return(segments_list.count());
|
|
uint nb_seg = 0;
|
|
foreach(ConductorSegment *conductor_segment, segments_list) {
|
|
if (conductor_segment -> type() == type) ++ nb_seg;
|
|
}
|
|
return(nb_seg);
|
|
}
|
|
|
|
/**
|
|
Genere une liste de points a partir des segments de ce conducteur
|
|
@return La liste de points representant ce conducteur
|
|
*/
|
|
QList<QPointF> Conductor::segmentsToPoints() const
|
|
{
|
|
// liste qui sera retournee
|
|
QList<QPointF> points_list;
|
|
|
|
// on retourne la liste tout de suite s'il n'y a pas de segments
|
|
if (segments == nullptr) return(points_list);
|
|
|
|
// recupere le premier point
|
|
points_list << segments -> firstPoint();
|
|
|
|
// parcourt les segments pour recuperer les autres points
|
|
ConductorSegment *segment = segments;
|
|
while(segment -> hasNextSegment()) {
|
|
points_list << segment -> secondPoint();
|
|
segment = segment -> nextSegment();
|
|
}
|
|
|
|
// recupere le dernier point
|
|
points_list << segment -> secondPoint();
|
|
|
|
//retourne la liste
|
|
return(points_list);
|
|
}
|
|
|
|
/**
|
|
Regenere les segments de ce conducteur a partir de la liste de points passee en parametre
|
|
@param points_list Liste de points a utiliser pour generer les segments
|
|
*/
|
|
void Conductor::pointsToSegments(const QList<QPointF>& points_list) {
|
|
// supprime les segments actuels
|
|
deleteSegments();
|
|
|
|
// cree les segments a partir de la liste de points
|
|
ConductorSegment *last_segment = nullptr;
|
|
for (int i = 0 ; i < points_list.size() - 1 ; ++ i) {
|
|
last_segment = new ConductorSegment(points_list.at(i), points_list.at(i + 1), last_segment);
|
|
if (!i) segments = last_segment;
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::fromXml
|
|
Load the conductor and its information from xml element
|
|
@param dom_element
|
|
@return true if loading succeeded else return false
|
|
*/
|
|
bool Conductor::fromXml(QDomElement &dom_element)
|
|
{
|
|
//Older project files have no conductor uuid attribute at all --
|
|
//generate one on load, same treatment terminal uuids got when
|
|
//that field was introduced (see terminal1/terminal2 handling in
|
|
//toXml() below).
|
|
m_uuid = QUuid(dom_element.attribute(QStringLiteral("uuid")));
|
|
m_persist_uuid = !m_uuid.isNull();
|
|
if (m_uuid.isNull()) {
|
|
//Absent, empty or malformed: mint one. A null uuid is not a usable
|
|
//identity -- every conductor carrying one would collide with every
|
|
//other on the conductor table's primary key. It's runtime-only,
|
|
//though: toXml() must not write it back out, or a legacy file
|
|
//with no conductor uuids gets a freshly different one on every
|
|
//single load-and-resave (see #754).
|
|
m_uuid = QUuid::createUuid();
|
|
}
|
|
|
|
setPos(dom_element.attribute("x", nullptr).toDouble(),
|
|
dom_element.attribute("y", nullptr).toDouble());
|
|
|
|
bool retval = pathFromXml(dom_element);
|
|
|
|
m_text_item -> fromXml(dom_element);
|
|
ConductorProperties pr;
|
|
pr.fromXml(dom_element);
|
|
|
|
//Load Sequential Values
|
|
if (dom_element.hasAttribute("sequ_1") || dom_element.hasAttribute("sequf_1") || dom_element.hasAttribute("seqt_1") || dom_element.hasAttribute("seqtf_1") || dom_element.hasAttribute("seqh_1") || dom_element.hasAttribute("sequf_1"))
|
|
ConductorXmlRetroCompatibility::loadSequential(dom_element, this);
|
|
else
|
|
m_autoNum_seq.fromXml(dom_element.firstChildElement("sequentialNumbers"));
|
|
|
|
m_freeze_label = dom_element.attribute("freezeLabel") == "true" ? true : false;
|
|
|
|
setProperties(pr);
|
|
|
|
return retval;
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::toXml
|
|
Exporte les caracteristiques du conducteur sous forme d'une element XML.
|
|
@param dom_document :
|
|
Le document XML a utiliser pour creer l'element XML
|
|
@param table_adr_id :
|
|
Hash stockant les correspondances entre les ids des
|
|
bornes dans le document XML et leur adresse en memoire
|
|
@return Un element XML representant le conducteur
|
|
*/
|
|
QDomElement Conductor::toXml(QDomDocument &dom_document,
|
|
QHash<Terminal *,
|
|
int> &table_adr_id) const
|
|
{
|
|
QDomElement dom_element = dom_document.createElement("conductor");
|
|
|
|
if (m_persist_uuid)
|
|
dom_element.setAttribute("uuid", m_uuid.toString());
|
|
dom_element.setAttribute("x", QString::number(pos().x()));
|
|
dom_element.setAttribute("y", QString::number(pos().y()));
|
|
|
|
// Terminal is uniquely identified by the uuid of the terminal and the element
|
|
if (terminal1->uuid().isNull()) {
|
|
// legacy method to identify the terminal
|
|
dom_element.setAttribute("terminal1", table_adr_id.value(terminal1)); // for backward compatibility
|
|
} else {
|
|
dom_element.setAttribute("element1", terminal1->parentElement()->uuid().toString());
|
|
dom_element.setAttribute("element1_label", terminal1->parentElement()->actualLabel());
|
|
if (terminal1->parentElement()->linkedElements().isEmpty()) {
|
|
//
|
|
} else {
|
|
dom_element.setAttribute("element1_linked", terminal1->parentElement()->linkedElements().last()->actualLabel());
|
|
}
|
|
dom_element.setAttribute("element1_name", terminal1->parentElement()->name());
|
|
dom_element.setAttribute("terminal1", terminal1->uuid().toString());
|
|
dom_element.setAttribute("terminalname1", terminal1->name());
|
|
}
|
|
|
|
if (terminal2->uuid().isNull()) {
|
|
// legacy method to identify the terminal
|
|
dom_element.setAttribute("terminal2", table_adr_id.value(terminal2)); // for backward compatibility
|
|
} else {
|
|
dom_element.setAttribute("element2", terminal2->parentElement()->uuid().toString());
|
|
dom_element.setAttribute("element2_label", terminal2->parentElement()->actualLabel());
|
|
if (terminal2->parentElement()->linkedElements().isEmpty()) {
|
|
//
|
|
} else {
|
|
dom_element.setAttribute("element2_linked", terminal2->parentElement()->linkedElements().last()->actualLabel());
|
|
}
|
|
dom_element.setAttribute("element2_name", terminal2->parentElement()->name());
|
|
dom_element.setAttribute("terminal2", terminal2->uuid().toString());
|
|
dom_element.setAttribute("terminalname2", terminal2->name());
|
|
}
|
|
dom_element.setAttribute("freezeLabel", m_freeze_label? "true" : "false");
|
|
|
|
// on n'exporte les segments du conducteur que si ceux-ci ont
|
|
// ete modifies par l'utilisateur
|
|
if (modified_path)
|
|
{
|
|
// parcours et export des segments
|
|
QDomElement current_segment;
|
|
foreach(ConductorSegment *segment, segmentsList())
|
|
{
|
|
current_segment = dom_document.createElement("segment");
|
|
current_segment.setAttribute("orientation", segment -> isHorizontal() ? "horizontal" : "vertical");
|
|
current_segment.setAttribute("length", QString("%1").arg(segment -> length()));
|
|
dom_element.appendChild(current_segment);
|
|
}
|
|
}
|
|
|
|
QDomElement dom_seq = m_autoNum_seq.toXml(dom_document);
|
|
dom_element.appendChild(dom_seq);
|
|
|
|
// Export the properties and text
|
|
m_properties. toXml(dom_element);
|
|
if(m_text_item->wasMovedByUser())
|
|
{
|
|
dom_element.setAttribute("userx", QString::number(m_text_item->pos().x()));
|
|
dom_element.setAttribute("usery", QString::number(m_text_item->pos().y()));
|
|
}
|
|
if(m_text_item->wasRotatedByUser())
|
|
dom_element.setAttribute("rotation", QString::number(m_text_item->rotation()));
|
|
|
|
return(dom_element);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::pathFromXml
|
|
Generate the path (of the line) from xml file by checking the segments in the xml
|
|
file
|
|
@param e
|
|
@return true if generate path success else return false
|
|
*/
|
|
bool Conductor::pathFromXml(const QDomElement &e) {
|
|
// parcourt les elements XML "segment" et en extrait deux listes de longueurs
|
|
// les segments non valides sont ignores
|
|
QList<qreal> segments_x, segments_y;
|
|
for (QDomNode node = e.firstChild() ; !node.isNull() ; node = node.nextSibling()) {
|
|
// on s'interesse aux elements XML "segment"
|
|
QDomElement current_segment = node.toElement();
|
|
if (current_segment.isNull() || current_segment.tagName() != "segment") continue;
|
|
|
|
// le segment doit avoir une longueur
|
|
if (!current_segment.hasAttribute("length")) continue;
|
|
|
|
// cette longueur doit etre un reel
|
|
bool ok;
|
|
qreal segment_length = current_segment.attribute("length").toDouble(&ok);
|
|
if (!ok) continue;
|
|
|
|
if (current_segment.attribute("orientation") == "horizontal") {
|
|
segments_x << segment_length;
|
|
segments_y << 0.0;
|
|
} else {
|
|
segments_x << 0.0;
|
|
segments_y << segment_length;
|
|
}
|
|
}
|
|
|
|
// If there is no segment, we generate an automatic path and return true
|
|
if (!segments_x.size()) {
|
|
generateConductorPath(terminal1 -> dockConductor(), terminal1 -> orientation(), terminal2 -> dockConductor(), terminal2 -> orientation());
|
|
return(true);
|
|
}
|
|
|
|
// les longueurs recueillies doivent etre coherentes avec les positions des bornes
|
|
// The collected lengths must be consistent with the positions of the terminals
|
|
qreal width = 0.0, height = 0.0;
|
|
foreach (qreal t, segments_x) width += t;
|
|
foreach (qreal t, segments_y) height += t;
|
|
QPointF t1 = terminal1 -> dockConductor();
|
|
QPointF t2 = terminal2 -> dockConductor();
|
|
qreal expected_width = t2.x() - t1.x();
|
|
qreal expected_height = t2.y() - t1.y();
|
|
|
|
// on considere que le trajet est incoherent a partir d'une unite de difference avec l'espacement entre les bornes
|
|
if (
|
|
qAbs(expected_width - width) > 1.0 ||
|
|
qAbs(expected_height - height) > 1.0
|
|
) {
|
|
qDebug() << "Conductor::fromXml : les segments du conducteur ne semblent pas coherents - utilisation d'un trajet automatique";
|
|
return(false);
|
|
}
|
|
|
|
/* on recree les segments a partir des donnes XML */
|
|
// cree la liste de points
|
|
QList<QPointF> points_list;
|
|
points_list << mapFromScene(t1);
|
|
for (int i = 0 ; i < segments_x.size() ; ++ i) {
|
|
points_list << QPointF(
|
|
points_list.last().x() + segments_x.at(i),
|
|
points_list.last().y() + segments_y.at(i)
|
|
);
|
|
}
|
|
|
|
pointsToSegments(points_list);
|
|
|
|
// initialise divers parametres lies a la modification des conducteurs
|
|
modified_path = true;
|
|
saveProfile(false);
|
|
|
|
segmentsToPath();
|
|
return(true);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::handlerPoints
|
|
@return The points used to draw the handler square, used to modify
|
|
the path of the conductor.
|
|
The points stored in the QVector are the middle point of each segments that compose the conductor,
|
|
at exception of the first and last segment because there just here to extend the terminal.
|
|
*/
|
|
QVector<QPointF> Conductor::handlerPoints() const
|
|
{
|
|
QList <ConductorSegment *> sl = segmentsList();
|
|
if (sl.size() >= 3)
|
|
{
|
|
sl.removeFirst();
|
|
sl.removeLast();
|
|
}
|
|
|
|
QVector <QPointF> middle_points;
|
|
|
|
foreach(ConductorSegment *segment, sl)
|
|
middle_points.append(segment->middle());
|
|
|
|
return middle_points;
|
|
}
|
|
|
|
/// @return les segments de ce conducteur
|
|
const QList<ConductorSegment *> Conductor::segmentsList() const
|
|
{
|
|
if (segments == nullptr) return(QList<ConductorSegment *>());
|
|
|
|
QList<ConductorSegment *> segments_vector;
|
|
ConductorSegment *segment = segments;
|
|
|
|
while (segment -> hasNextSegment()) {
|
|
segments_vector << segment;
|
|
segment = segment -> nextSegment();
|
|
}
|
|
segments_vector << segment;
|
|
return(segments_vector);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::length
|
|
@return the length of this conductor
|
|
*/
|
|
qreal Conductor::length() const{
|
|
return path().length();
|
|
}
|
|
|
|
/**
|
|
@return Le segment qui contient le point au milieu du conducteur
|
|
*/
|
|
ConductorSegment *Conductor::middleSegment()
|
|
{
|
|
if (segments == nullptr) return(nullptr);
|
|
|
|
qreal half_length = length() / 2.0;
|
|
|
|
ConductorSegment *s = segments;
|
|
qreal l = 0;
|
|
|
|
while (s -> hasNextSegment()) {
|
|
l += qAbs(s -> length());
|
|
if (l >= half_length) break;
|
|
s = s -> nextSegment();
|
|
}
|
|
// s est le segment qui contient le point au milieu du conducteur
|
|
return(s);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::posForText
|
|
Calculate and return the better pos for text.
|
|
@param flag : flag is used to know if text pos is near of
|
|
a vertical or horizontal conductor segment.
|
|
*/
|
|
QPointF Conductor::posForText(Qt::Orientations &flag)
|
|
{
|
|
ConductorSegment *segment = segments;
|
|
bool all_segments_are_vertical = true;
|
|
bool all_segments_are_horizontal = true;
|
|
|
|
//Go to first segment
|
|
while (!segment->isFirstSegment()) {
|
|
segment = segment->previousSegment();
|
|
}
|
|
|
|
QPointF p1 = segment -> firstPoint(); //<First point of conductor
|
|
ConductorSegment *longest_segment = segment; //<the longest segment of conductor.
|
|
|
|
// check if all segments are horizontal or vertical ... first segment:
|
|
if (segment -> firstPoint().x() != segment -> secondPoint().x())
|
|
all_segments_are_vertical = false;
|
|
if (segment -> firstPoint().y() != segment -> secondPoint().y())
|
|
all_segments_are_horizontal = false;
|
|
// find longest segment
|
|
while (segment -> hasNextSegment())
|
|
{
|
|
segment = segment -> nextSegment();
|
|
if (segment -> firstPoint().x() != segment -> secondPoint().x())
|
|
all_segments_are_vertical = false;
|
|
if (segment -> firstPoint().y() != segment -> secondPoint().y())
|
|
all_segments_are_horizontal = false;
|
|
|
|
if (qAbs(segment->length()) > qAbs(longest_segment -> length()))
|
|
longest_segment = segment;
|
|
}
|
|
|
|
QPointF p2 = segment -> secondPoint(); //<Last point of conductor
|
|
|
|
//If the conductor is horizontal or vertical
|
|
//Return the point at the middle of conductor
|
|
if (all_segments_are_vertical) { //<Vertical
|
|
flag = Qt::Vertical;
|
|
if (p1.y() > p2.y()) {
|
|
p1.setY(p1.y() - (length()/2));
|
|
} else {
|
|
p1.setY(p1.y() + (length()/2));
|
|
}
|
|
} else if (all_segments_are_horizontal) { //<Horizontal
|
|
flag = Qt::Horizontal;
|
|
if (p1.x() > p2.x()) {
|
|
p1.setX(p1.x() - (length()/2));
|
|
} else {
|
|
p1.setX(p1.x() + (length()/2));
|
|
}
|
|
} else { //Return the point at the middle of longest segment.
|
|
p1 = longest_segment->middle();
|
|
flag = (longest_segment->isHorizontal())? Qt::Horizontal : Qt::Vertical;
|
|
}
|
|
return p1;
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::calculateTextItemPosition
|
|
Move the text at middle of conductor (if is vertical or horizontal)
|
|
otherwise, move conductor at the middle of the longest segment of conductor.
|
|
If text was moved by user, this function do nothing, except check if text is near conductor.
|
|
*/
|
|
void Conductor::calculateTextItemPosition()
|
|
{
|
|
if (!m_text_item || !diagram() || m_properties.type != ConductorProperties::Multi)
|
|
return;
|
|
|
|
if (diagram() -> defaultConductorProperties.m_one_text_per_folio == true &&
|
|
relatedPotentialConductors(false).size() > 0)
|
|
{
|
|
|
|
Conductor *longest_conductor = longestConductorInPotential(this);
|
|
|
|
//This conductor isn't the longest conductor?
|
|
//We call calculateTextItemPosition for the longest conductor
|
|
if(longest_conductor != this)
|
|
{
|
|
longest_conductor -> calculateTextItemPosition();
|
|
return;
|
|
}
|
|
|
|
//At this point this conductor is the longest conductor we hide all text of conductor_list
|
|
foreach (Conductor *c, relatedPotentialConductors(false)) {
|
|
c -> textItem() -> setVisible(false);
|
|
}
|
|
//Make sure text item is visible
|
|
m_text_item -> setVisible(true);
|
|
}
|
|
|
|
//position
|
|
if (m_text_item -> wasMovedByUser())
|
|
{
|
|
//Text field was moved by user:
|
|
//we check if text field is yet near the conductor
|
|
QPointF text_item_pos = m_text_item -> pos();
|
|
QPainterPath near_shape = nearShape();
|
|
if (!near_shape.contains(text_item_pos)) {
|
|
m_text_item -> setPos(movePointIntoPolygon(text_item_pos, near_shape));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//Position and rotation of text is calculated.
|
|
Qt::Orientations rotation;
|
|
QPointF text_pos = posForText(rotation);
|
|
|
|
if (!m_text_item -> wasRotatedByUser())
|
|
{
|
|
rotation == Qt::Vertical ? m_text_item -> setRotation(m_properties.verti_rotate_text):
|
|
m_text_item -> setRotation(m_properties.horiz_rotate_text);
|
|
}
|
|
|
|
//Adjust the position of text if his rotation
|
|
//is 0° or 270°, to be exactly centered to the conductor
|
|
if (m_text_item -> rotation() == 0)
|
|
{
|
|
text_pos.rx() -= m_text_item -> boundingRect().width()/2;
|
|
if(m_properties.m_horizontal_alignment == Qt::AlignTop)
|
|
text_pos.ry() -= m_text_item->boundingRect().height();
|
|
}
|
|
else if (m_text_item -> rotation() == 270)
|
|
{
|
|
text_pos.ry() += m_text_item -> boundingRect().width()/2;
|
|
if(m_properties.m_vertical_alignment == Qt::AlignLeft)
|
|
text_pos.rx() -= m_text_item->boundingRect().height();
|
|
}
|
|
|
|
m_text_item -> setPos(text_pos);
|
|
|
|
//Ensure text item does not collide with this conductor
|
|
while (m_text_item->collidesWithItem(this))
|
|
{
|
|
if(rotation == Qt::Vertical)
|
|
{
|
|
if(m_properties.m_vertical_alignment == Qt::AlignRight)
|
|
m_text_item->setX(m_text_item->x()+1);
|
|
else if (m_properties.m_vertical_alignment == Qt::AlignLeft)
|
|
m_text_item->setX(m_text_item->x()-1);
|
|
else
|
|
return; //avoid infinite loop
|
|
}
|
|
else if (rotation == Qt::Horizontal)
|
|
{
|
|
if(m_properties.m_horizontal_alignment == Qt::AlignTop)
|
|
m_text_item->setY(m_text_item->y()-1);
|
|
else if (m_properties.m_horizontal_alignment == Qt::AlignBottom)
|
|
m_text_item->setY(m_text_item->y()+1);
|
|
else
|
|
return; //avoid infinite loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Sauvegarde le profil courant du conducteur pour l'utiliser ulterieurement
|
|
dans priv_modifieConductor.
|
|
Save the current conductors profile for later use in priv_modifieConductor.
|
|
*/
|
|
void Conductor::saveProfile(bool undo) {
|
|
Qt::Corner current_path_type = currentPathType();
|
|
ConductorProfile old_profile(conductor_profiles[current_path_type]);
|
|
conductor_profiles[current_path_type].fromConductor(this);
|
|
Diagram *dia = diagram();
|
|
if (undo && dia) {
|
|
ChangeConductorCommand *undo_object = new ChangeConductorCommand(
|
|
this,
|
|
old_profile,
|
|
conductor_profiles[current_path_type],
|
|
current_path_type
|
|
);
|
|
undo_object -> setConductorTextItemMove(before_mov_text_pos_, m_text_item -> pos());
|
|
dia -> undoStack().push(undo_object);
|
|
}
|
|
}
|
|
|
|
/**
|
|
@param value1 Premiere valeur
|
|
@param value2 Deuxieme valeur
|
|
@return 1 si les deux valeurs sont de meme signe, -1 sinon
|
|
*/
|
|
int Conductor::getCoeff(const qreal &value1, const qreal &value2) {
|
|
return(getSign(value1) * getSign(value2));
|
|
}
|
|
|
|
/**
|
|
@param value valeur
|
|
@return 1 si valeur est negatif, 1 s'il est positif ou nul
|
|
*/
|
|
int Conductor::getSign(const qreal &value) {
|
|
return(value < 0 ? -1 : 1);
|
|
}
|
|
|
|
/**
|
|
Applique un nouveau profil a ce conducteur
|
|
@param cp Profil a appliquer a ce conducteur
|
|
@param path_type Type de trajet pour lequel ce profil convient
|
|
*/
|
|
void Conductor::setProfile(const ConductorProfile &cp, Qt::Corner path_type) {
|
|
conductor_profiles[path_type] = cp;
|
|
// si le type de trajet correspond a l'actuel
|
|
if (currentPathType() == path_type) {
|
|
if (conductor_profiles[path_type].isNull()) {
|
|
generateConductorPath(terminal1 -> dockConductor(), terminal1 -> orientation(), terminal2 -> dockConductor(), terminal2 -> orientation());
|
|
modified_path = false;
|
|
} else {
|
|
updateConductorPath(terminal1 -> dockConductor(), terminal1 -> orientation(), terminal2 -> dockConductor(), terminal2 -> orientation());
|
|
modified_path = true;
|
|
}
|
|
if (type() == ConductorProperties::Multi) {
|
|
calculateTextItemPosition();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// @return le profil de ce conducteur
|
|
ConductorProfile Conductor::profile(Qt::Corner path_type) const
|
|
{
|
|
return(conductor_profiles[path_type]);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::refreshText
|
|
Refresh the text of this conductor.
|
|
recalculate and set the text according to the formula.
|
|
*/
|
|
void Conductor::refreshText()
|
|
{
|
|
if (m_freeze_label)
|
|
{
|
|
m_text_item->setPlainText(m_properties.text);
|
|
}
|
|
else
|
|
{
|
|
if (!m_properties.m_formula.isEmpty())
|
|
{
|
|
if (diagram())
|
|
{
|
|
QString text = autonum::AssignVariables::formulaToLabel(m_properties.m_formula, m_autoNum_seq, diagram(), nullptr, this);
|
|
m_properties.text = text;
|
|
m_text_item->setPlainText(text);
|
|
}
|
|
else
|
|
{
|
|
m_properties.text = m_properties.m_formula;
|
|
m_text_item->setPlainText(m_properties.text);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_text_item->setPlainText(m_properties.text);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Conductor::setPath(const QPainterPath &path)
|
|
{
|
|
if(path == m_path)
|
|
return;
|
|
|
|
prepareGeometryChange();
|
|
m_path = path;
|
|
update();
|
|
}
|
|
|
|
QPainterPath Conductor::path() const
|
|
{
|
|
return m_path;
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::setPropertiesToPotential
|
|
@param property
|
|
@param only_text
|
|
Set property to conductor and every conductors in
|
|
the same potential of conductor.
|
|
If only_text is true only formula, text,
|
|
function and tension/protocol is set
|
|
to other conductor in the same potential,
|
|
the other values of property stay unmodified
|
|
*/
|
|
void Conductor::setPropertyToPotential(const ConductorProperties &property,
|
|
bool only_text)
|
|
{
|
|
setProperties(property);
|
|
QSet <Conductor *> potential_list = relatedPotentialConductors();
|
|
|
|
foreach(Conductor *other_conductor, potential_list)
|
|
{
|
|
if (only_text)
|
|
{
|
|
ConductorProperties other_properties = other_conductor->properties();
|
|
other_properties.m_formula = m_properties.m_formula;
|
|
other_properties.text = m_properties.text;
|
|
other_properties.m_function = m_properties.m_function;
|
|
other_properties.m_tension_protocol = m_properties.m_tension_protocol;
|
|
other_properties.m_wire_color = m_properties.m_wire_color;
|
|
other_properties.m_wire_section = m_properties.m_wire_section;
|
|
other_conductor->setProperties(other_properties);
|
|
}
|
|
else
|
|
{
|
|
other_conductor->setProperties(property);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::setProperties
|
|
Set property as current property of conductor
|
|
@param property : properties
|
|
*/
|
|
void Conductor::setProperties(const ConductorProperties &property)
|
|
{
|
|
if (m_properties == property) return;
|
|
|
|
QString formula = m_properties.m_formula;
|
|
m_properties = property;
|
|
|
|
if (!m_properties.m_formula.isEmpty())
|
|
{
|
|
if (diagram())
|
|
{
|
|
QString text = autonum::AssignVariables::formulaToLabel(m_properties.m_formula, m_autoNum_seq, diagram(), nullptr, this);
|
|
m_properties.text = text;
|
|
}
|
|
else if (m_properties.text.isEmpty())
|
|
{
|
|
m_properties.text = m_properties.m_formula;
|
|
}
|
|
|
|
setUpConnectionForFormula(formula, m_properties.m_formula);
|
|
}
|
|
|
|
m_text_item->setPlainText(m_properties.text);
|
|
QFont font = m_text_item->font();
|
|
font.setPointSize(m_properties.text_size);
|
|
m_text_item->setFont(font);
|
|
m_text_item->setColor(m_properties.text_color);
|
|
|
|
if (m_properties.type != ConductorProperties::Multi)
|
|
m_text_item->setVisible(false);
|
|
else
|
|
m_text_item->setVisible(m_properties.m_show_text);
|
|
|
|
calculateTextItemPosition();
|
|
update();
|
|
|
|
emit propertiesChange();
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::properties
|
|
@return the properties of this Conductor
|
|
*/
|
|
ConductorProperties Conductor::properties() const
|
|
{
|
|
return(m_properties);
|
|
}
|
|
|
|
/**
|
|
@return true si le conducteur est mis en evidence
|
|
*/
|
|
Conductor::Highlight Conductor::highlight() const
|
|
{
|
|
return(must_highlight_);
|
|
}
|
|
|
|
/**
|
|
@param hl true pour mettre le conducteur en evidence, false sinon
|
|
*/
|
|
void Conductor::setHighlighted(Conductor::Highlight hl) {
|
|
must_highlight_ = hl;
|
|
update();
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::displayedTextChanged
|
|
Update the properties (text) of this conductor and other conductors
|
|
at the same potential of this conductor.
|
|
*/
|
|
void Conductor::displayedTextChanged()
|
|
{
|
|
QVariant old_value, new_value;
|
|
old_value.setValue(m_properties);
|
|
ConductorProperties new_properties(m_properties);
|
|
new_properties.m_formula = m_text_item->toPlainText();
|
|
new_properties.text = m_text_item->toPlainText();
|
|
new_value.setValue(new_properties);
|
|
|
|
|
|
QUndoCommand *undo = new QUndoCommand(tr("Modifier les propriétés d'un conducteur", "undo caption"));
|
|
new QPropertyUndoCommand(this, "properties", old_value, new_value, undo);
|
|
|
|
if (!relatedPotentialConductors().isEmpty())
|
|
{
|
|
undo->setText(tr("Modifier les propriétés de plusieurs conducteurs", "undo caption"));
|
|
|
|
foreach (Conductor *potential_conductor, relatedPotentialConductors())
|
|
{
|
|
old_value.setValue(potential_conductor->properties());
|
|
ConductorProperties new_properties = potential_conductor->properties();
|
|
new_properties.m_formula = m_text_item->toPlainText();
|
|
new_properties.text = m_text_item->toPlainText();
|
|
new_value.setValue(new_properties);
|
|
new QPropertyUndoCommand (potential_conductor, "properties", old_value, new_value, undo);
|
|
}
|
|
}
|
|
|
|
diagram()->undoStack().push(undo);
|
|
}
|
|
|
|
|
|
/**
|
|
@brief Conductor::relatedPotentialConductors
|
|
Return all conductors at the same potential of this conductor,
|
|
this conductor isn't part of the returned QSet.
|
|
@param all_diagram : if true search in all diagram of the project,
|
|
false search only in the parent diagram of this conductor
|
|
@param t_list : a list of terminal already found for this potential.
|
|
@return a QSet of conductor at the same potential.
|
|
*/
|
|
QSet<Conductor *> Conductor::relatedPotentialConductors(const bool all_diagram, QList <Terminal *> *t_list)
|
|
{
|
|
bool declar_t_list = false;
|
|
if (t_list == nullptr)
|
|
{
|
|
declar_t_list = true;
|
|
t_list = new QList <Terminal *>;
|
|
}
|
|
|
|
QSet <Conductor *> other_conductors;
|
|
QList <Terminal *> this_terminal;
|
|
this_terminal << terminal1 << terminal2;
|
|
|
|
// Return all conductors of terminal 1 and 2
|
|
for (Terminal *terminal : this_terminal)
|
|
{
|
|
if (!t_list->contains(terminal))
|
|
{
|
|
t_list->append(terminal);
|
|
QList <Conductor *> other_conductors_list_t = terminal->conductors();
|
|
|
|
//Get the other terminals of the parent element of @terminal, who share the same potential
|
|
//This is use for element type "folio report" and "terminal element"
|
|
for (Terminal *t : relatedPotentialTerminal(terminal, all_diagram))
|
|
{
|
|
if (!t_list->contains(t))
|
|
{
|
|
t_list -> append(t);
|
|
other_conductors_list_t += t->conductors();
|
|
}
|
|
}
|
|
|
|
other_conductors_list_t.removeAll(this);
|
|
//Get the conductors at the same potential for each conductors of other_conductors_list_t
|
|
for (Conductor *c : other_conductors_list_t) {
|
|
other_conductors += c->relatedPotentialConductors(all_diagram, t_list);
|
|
}
|
|
|
|
other_conductors += QSet<Conductor*>(other_conductors_list_t.begin(),other_conductors_list_t.end());
|
|
}
|
|
}
|
|
|
|
other_conductors.remove(this);
|
|
|
|
if (declar_t_list) delete t_list;
|
|
return(other_conductors);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::diagramEditor
|
|
@return The parent diagram editor or nullptr;
|
|
*/
|
|
QETDiagramEditor* Conductor::diagramEditor() const
|
|
{
|
|
if (!diagram()) return nullptr;
|
|
if (diagram() -> views().isEmpty()) return nullptr;
|
|
|
|
QWidget *w = const_cast<QGraphicsView *>(diagram() -> views().at(0));
|
|
while (w -> parentWidget() && !w -> isWindow()) {
|
|
w = w -> parentWidget();
|
|
}
|
|
return(qobject_cast<QETDiagramEditor *>(w));
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::editProperty
|
|
*/
|
|
void Conductor::editProperty()
|
|
{
|
|
ConductorPropertiesDialog::PropertiesDialog(this, diagramEditor());
|
|
}
|
|
|
|
void Conductor::setSequenceNum(const autonum::sequentialNumbers& sn)
|
|
{
|
|
m_autoNum_seq = sn;
|
|
refreshText();
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::setUpConnectionForFormula
|
|
setup connection according to the variable of formula
|
|
@param old_formula
|
|
@param new_formula
|
|
*/
|
|
void Conductor::setUpConnectionForFormula(QString old_formula, QString new_formula)
|
|
{
|
|
Diagram *diagram_ {diagram()};
|
|
if (!diagram_) {
|
|
diagram_ = terminal1->diagram();
|
|
}
|
|
if (diagram_)
|
|
{
|
|
//Because the variable %F is a reference to another text which can contain variables,
|
|
//we must replace %F by the real text, to check if the real text contains the variable %id
|
|
if (old_formula.contains("%F"))
|
|
old_formula.replace("%F", diagram_->border_and_titleblock.folio());
|
|
|
|
if (old_formula.contains("%id"))
|
|
disconnect(diagram_->project(), &QETProject::projectDiagramsOrderChanged, this, &Conductor::refreshText);
|
|
|
|
//Label is frozen, so we don't update it.
|
|
if (m_freeze_label == true)
|
|
return;
|
|
|
|
if (new_formula.contains("%F"))
|
|
new_formula.replace("%F", diagram_->border_and_titleblock.folio());
|
|
|
|
if (new_formula.contains("%id"))
|
|
connect(diagram_->project(), &QETProject::projectDiagramsOrderChanged, this, &Conductor::refreshText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
@param a point
|
|
@param b point
|
|
@param c point
|
|
@return true si le point a est contenu dans le rectangle delimite par les points b et c
|
|
*/
|
|
bool isContained(const QPointF &a, const QPointF &b, const QPointF &c) {
|
|
return(
|
|
isBetween(a.x(), b.x(), c.x()) &&
|
|
isBetween(a.y(), b.y(), c.y())
|
|
);
|
|
}
|
|
|
|
/**
|
|
@return la liste des positions des jonctions avec d'autres conducteurs
|
|
*/
|
|
QList<QPointF> Conductor::junctions() const
|
|
{
|
|
QList<QPointF> junctions_list;
|
|
|
|
// pour qu'il y ait des jonctions, il doit y avoir d'autres conducteurs et des bifurcations
|
|
QList<Conductor *> other_conductors = relatedConductors(this);
|
|
QList<ConductorBend> bends_list = bends();
|
|
if (other_conductors.isEmpty() || bends_list.isEmpty()) {
|
|
return(junctions_list);
|
|
}
|
|
|
|
QList<QPointF> points = segmentsToPoints();
|
|
for (int i = 1 ; i < (points.size() -1) ; ++ i) {
|
|
QPointF point = points.at(i);
|
|
|
|
// determine si le point est une bifurcation ou non
|
|
bool is_bend = false;
|
|
Qt::Corner current_bend_type = Qt::TopLeftCorner;
|
|
foreach(ConductorBend cb, bends_list)
|
|
{
|
|
if (cb.first == point)
|
|
{
|
|
is_bend = true;
|
|
current_bend_type = cb.second;
|
|
break;
|
|
}
|
|
}
|
|
// si le point n'est pas une bifurcation, il ne peut etre une jonction (enfin pas au niveau de ce conducteur)
|
|
if (!is_bend) continue;
|
|
|
|
bool is_junction = false;
|
|
QPointF scene_point = mapToScene(point);
|
|
foreach(Conductor *c, other_conductors)
|
|
{
|
|
// exprime le point dans les coordonnees de l'autre conducteur
|
|
QPointF conductor_point = c -> mapFromScene(scene_point);
|
|
// recupere les segments de l'autre conducteur
|
|
QList<ConductorSegment *> c_segments = c -> segmentsList();
|
|
if (c_segments.isEmpty())
|
|
continue;
|
|
// parcoure les segments a la recherche d'un point commun
|
|
for (int j = 0 ; j < c_segments.count() ; ++ j)
|
|
{
|
|
ConductorSegment *segment = c_segments[j];
|
|
// un point commun a ete trouve sur ce segment
|
|
if (isContained(conductor_point, segment -> firstPoint(), segment -> secondPoint()))
|
|
{
|
|
is_junction = true;
|
|
// ce point commun ne doit pas etre une bifurcation identique a celle-ci
|
|
QList<ConductorBend> other_conductor_bends = c -> bends();
|
|
foreach(ConductorBend cb, other_conductor_bends)
|
|
{
|
|
if (cb.first == conductor_point && cb.second == current_bend_type)
|
|
{
|
|
is_junction = false;
|
|
}
|
|
}
|
|
}
|
|
if (is_junction) junctions_list << point;
|
|
}
|
|
}
|
|
}
|
|
return(junctions_list);
|
|
}
|
|
|
|
/**
|
|
@return la liste des bifurcations de ce conducteur ; ConductorBend est un
|
|
typedef pour une QPair\<QPointF, Qt::Corner\>. Le point indique la position
|
|
(en coordonnees locales) de la bifurcation tandis que le Corner indique le
|
|
type de bifurcation.
|
|
*/
|
|
QList<ConductorBend> Conductor::bends() const
|
|
{
|
|
QList<ConductorBend> points;
|
|
if (!segments) return(points);
|
|
|
|
// recupere la liste des segments de taille non nulle
|
|
QList<ConductorSegment *> visible_segments;
|
|
ConductorSegment *segment = segments;
|
|
while (segment -> hasNextSegment()) {
|
|
if (!segment -> isPoint()) visible_segments << segment;
|
|
segment = segment -> nextSegment();
|
|
}
|
|
if (!segment -> isPoint()) visible_segments << segment;
|
|
|
|
ConductorSegment *next_segment;
|
|
for (int i = 0 ; i < visible_segments.count() -1 ; ++ i) {
|
|
segment = visible_segments[i];
|
|
next_segment = visible_segments[i + 1];
|
|
if (!segment -> isPoint() && !next_segment -> isPoint()) {
|
|
// si les deux segments ne sont pas dans le meme sens, on a une bifurcation
|
|
if (next_segment -> type() != segment -> type()) {
|
|
Qt::Corner bend_type;
|
|
qreal sl = segment -> length();
|
|
qreal nsl = next_segment -> length();
|
|
|
|
if (segment -> isHorizontal()) {
|
|
if (sl < 0 && nsl < 0) {
|
|
bend_type = Qt::BottomLeftCorner;
|
|
} else if (sl < 0 && nsl > 0) {
|
|
bend_type = Qt::TopLeftCorner;
|
|
} else if (sl > 0 && nsl < 0) {
|
|
bend_type = Qt::BottomRightCorner;
|
|
} else {
|
|
bend_type = Qt::TopRightCorner;
|
|
}
|
|
} else {
|
|
if (sl < 0 && nsl < 0) {
|
|
bend_type = Qt::TopRightCorner;
|
|
} else if (sl < 0 && nsl > 0) {
|
|
bend_type = Qt::TopLeftCorner;
|
|
} else if (sl > 0 && nsl < 0) {
|
|
bend_type = Qt::BottomRightCorner;
|
|
} else {
|
|
bend_type = Qt::BottomLeftCorner;
|
|
}
|
|
}
|
|
points << qMakePair(segment -> secondPoint(), bend_type);
|
|
}
|
|
}
|
|
}
|
|
return(points);
|
|
}
|
|
|
|
/**
|
|
@param start Point de depart
|
|
@param end Point d'arrivee
|
|
@return le coin vers lequel se dirige le trajet de start vers end
|
|
*/
|
|
Qt::Corner Conductor::movementType(const QPointF &start, const QPointF &end) {
|
|
Qt::Corner result = Qt::BottomRightCorner;
|
|
if (start.x() <= end.x()) {
|
|
result = start.y() <= end.y() ? Qt::BottomRightCorner : Qt::TopRightCorner;
|
|
} else {
|
|
result = start.y() <= end.y() ? Qt::BottomLeftCorner : Qt::TopLeftCorner;
|
|
}
|
|
return(result);
|
|
}
|
|
|
|
/// @return le type de trajet actuel de ce conducteur
|
|
Qt::Corner Conductor::currentPathType() const
|
|
{
|
|
return(movementType(terminal1 -> dockConductor(), terminal2 -> dockConductor()));
|
|
}
|
|
|
|
/// @return les profils de ce conducteur
|
|
ConductorProfilesGroup Conductor::profiles() const
|
|
{
|
|
return(conductor_profiles);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::setProfiles
|
|
@param cpg : the new profils of conductor
|
|
*/
|
|
void Conductor::setProfiles(const ConductorProfilesGroup &cpg) {
|
|
conductor_profiles = cpg;
|
|
if (conductor_profiles[currentPathType()].isNull()) {
|
|
generateConductorPath(terminal1 -> dockConductor(), terminal1 -> orientation(), terminal2 -> dockConductor(), terminal2 -> orientation());
|
|
modified_path = false;
|
|
} else {
|
|
updateConductorPath(terminal1 -> dockConductor(), terminal1 -> orientation(), terminal2 -> dockConductor(), terminal2 -> orientation());
|
|
modified_path = true;
|
|
}
|
|
if (properties().type == ConductorProperties::Multi) {
|
|
calculateTextItemPosition();
|
|
}
|
|
}
|
|
|
|
/// Supprime les segments
|
|
void Conductor::deleteSegments()
|
|
{
|
|
if (segments != nullptr) {
|
|
while (segments -> hasNextSegment()) delete segments -> nextSegment();
|
|
delete segments;
|
|
segments = nullptr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Conductor::movePointIntoPolygon
|
|
* @param point : A point located outside the polygon
|
|
* @param polygon : The polygon in which we want to move the point
|
|
* @return the position of the point, once brought back into the polygon,
|
|
* or more exactly on the edge of the polygon
|
|
*/
|
|
QPointF Conductor::movePointIntoPolygon(const QPointF &point, const QPainterPath &polygon)
|
|
{
|
|
// decomposes the polygon into lines and points
|
|
const QList<QPolygonF> polygons = polygon.simplified().toSubpathPolygons();
|
|
QList<QLineF> lines;
|
|
QList<QPointF> points;
|
|
|
|
for (QPolygonF polygon : polygons)
|
|
{
|
|
if (polygon.count() <= 1)
|
|
continue;
|
|
|
|
// lines and points are counted
|
|
for (int i = 1 ; i < polygon.count() ; ++ i) {
|
|
lines << QLineF(polygon.at(i - 1), polygon.at(i));
|
|
points << polygon.at(i -1);
|
|
}
|
|
}
|
|
|
|
// we make orthogonal projections of the point on the different
|
|
// segments of the polygon, sorting them by increasing length
|
|
QMap<qreal, QPointF> intersections;
|
|
for (QLineF line : lines)
|
|
{
|
|
QPointF intersection_point;
|
|
if (QET::orthogonalProjection(point, line, &intersection_point)) {
|
|
intersections.insert(QLineF(intersection_point, point).length(), intersection_point);
|
|
}
|
|
}
|
|
|
|
if (intersections.count())
|
|
{
|
|
// the shortest length for an orthogonal project is determined
|
|
QPointF the_point = intersections[intersections.keys().first()];
|
|
return(the_point);
|
|
}
|
|
else
|
|
{
|
|
// determines the corner of the polygon closest to the outer point
|
|
qreal minimum_length = -1;
|
|
int point_index = -1;
|
|
for (int i = 0 ; i < points.count() ; ++ i)
|
|
{
|
|
qreal length = qAbs(QLineF(points.at(i), point).length());
|
|
if (minimum_length < 0 || length < minimum_length) {
|
|
minimum_length = length;
|
|
point_index = i;
|
|
}
|
|
}
|
|
// we now know the closest corner of the text
|
|
|
|
|
|
if (point_index == -1 ||
|
|
point_index+1 > points.size()) {
|
|
return QPointF(0,0);
|
|
}
|
|
|
|
// no orthogonal projection gave anything, we put the text on one of the corners of the polygon
|
|
return(points.at(point_index));
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief longestConductorInPotential
|
|
@param conductor : a conductor in the potential to search
|
|
@param all_diagram : true -> search in the whole project, false -> search only in the diagram of conductor
|
|
@return the longest conductor in the same potential of conductor
|
|
*/
|
|
Conductor * longestConductorInPotential(Conductor *conductor, bool all_diagram) {
|
|
Conductor *longest_conductor = conductor;
|
|
//Search the longest conductor
|
|
foreach (Conductor *c, conductor -> relatedPotentialConductors(all_diagram))
|
|
if (c -> length() > longest_conductor -> length())
|
|
longest_conductor = c;
|
|
|
|
return longest_conductor;
|
|
}
|
|
|
|
/**
|
|
@brief relatedConductors
|
|
@param conductor
|
|
@return return all conductors who share the same terminals
|
|
of conductor given as parameter,
|
|
except conductor itself.
|
|
*/
|
|
QList <Conductor *> relatedConductors(const Conductor *conductor) {
|
|
QList<Conductor *> other_conductors_list = conductor -> terminal1 -> conductors();
|
|
other_conductors_list << conductor -> terminal2->conductors();
|
|
other_conductors_list.removeAll(const_cast<Conductor *> (conductor));
|
|
return(other_conductors_list);
|
|
}
|
|
|
|
/**
|
|
@brief Conductor::setFreezeLabel
|
|
Freeze this conductor label if true
|
|
Unfreeze this conductor label if false
|
|
@param freeze
|
|
*/
|
|
void Conductor::setFreezeLabel(bool freeze)
|
|
{
|
|
m_freeze_label = freeze;
|
|
|
|
if (m_freeze_label != freeze)
|
|
{
|
|
m_freeze_label = freeze;
|
|
QString f = m_properties.m_formula;
|
|
setUpConnectionForFormula(f,f);
|
|
}
|
|
}
|