/* 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 "cablemanager.h" #include "cable.h" #include "../autoNum/assignvariables.h" #include "../autoNum/numerotationcontextcommands.h" #include "../diagram.h" #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/terminal.h" #include "../qetproject.h" #include #include #include #include namespace { /** @brief Where two segments cross, if they do at all. Done by hand rather than with QLineF::intersects() because the collinear case has to be a miss: a cable drawn exactly on top of a conductor follows it, it does not cross it, and counting it would claim a core the cable never reaches. @param a1 start of the first segment @param a2 end of the first segment @param b1 start of the second segment @param b2 end of the second segment @param result receives the crossing point when there is one @return true when the two segments cross within their own length */ bool segmentIntersection(const QPointF &a1, const QPointF &a2, const QPointF &b1, const QPointF &b2, QPointF *result) { const QPointF r = a2 - a1; const QPointF s = b2 - b1; const QPointF u = b1 - a1; const qreal denominator = r.x() * s.y() - r.y() * s.x(); if (qFuzzyIsNull(denominator)) { return false; } const qreal t = (u.x() * s.y() - u.y() * s.x()) / denominator; const qreal v = (u.x() * r.y() - u.y() * r.x()) / denominator; if (t < 0.0 || t > 1.0 || v < 0.0 || v > 1.0) { return false; } if (result) { *result = a1 + t * r; } return true; } /** @brief How far a point stands from a segment, in scene units */ qreal distanceToSegment(const QPointF &point, const QPointF &a, const QPointF &b) { const QPointF ab = b - a; const qreal length_squared = ab.x() * ab.x() + ab.y() * ab.y(); if (qFuzzyIsNull(length_squared)) { return QLineF(point, a).length(); } const QPointF ap = point - a; qreal t = (ap.x() * ab.x() + ap.y() * ab.y()) / length_squared; t = qBound(0.0, t, 1.0); return QLineF(point, a + t * ab).length(); } /** @brief Whether some cable of the project already stands under this number. A number which is taken must not be handed out twice. That happens while a whole run of cables is numbered one after the other -- none of them has been added to the project by anything but its own creation yet -- and it happens when a rule has simply been given a value which some cable carries already. @param project @param designation the number to look for @return true when a cable of that project stands under it */ bool designationIsTaken(QETProject *project, const QString &designation) { if (!project || designation.isEmpty()) return false; for (Cable *cable : project->cables()) { if (cable && cable->designation() == designation) { return true; } } return false; } } // namespace /** @brief CableManager::crossedConductors Tell which conductors a line about to become a cable runs across, in the order they are met along the line. That order is what gives each core its place, and therefore its colour, so it has to be the order of the drawing and not the order the conductors happen to be stored in. @param diagram the folio the line is drawn on @param line the line, in scene coordinates @return the crossings, ordered from the start of the line */ QList CableManager::crossedConductors(Diagram *diagram, const QLineF &line) { QList crossings; if (!diagram) { return crossings; } const QPointF start = line.p1(); if (qFuzzyCompare(start, line.p2())) { return crossings; } const QPointF direction = line.p2() - start; for (Conductor *conductor : diagram->conductors()) { QPointF where; if (!crossingOf(diagram, line, conductor, &where)) continue; CableCrossing crossing; crossing.conductor = conductor; crossing.position = where; crossings.append(crossing); } std::sort(crossings.begin(), crossings.end(), [&start, &direction](const CableCrossing &a_, const CableCrossing &b_) { const QPointF da = a_.position - start; const QPointF db = b_.position - start; const qreal ta = da.x() * direction.x() + da.y() * direction.y(); const qreal tb = db.x() * direction.x() + db.y() * direction.y(); return ta < tb; }); return crossings; } /** @brief CableManager::crossingOf Where one line runs across one conductor, if it does at all. Done by hand rather than with QLineF::intersects() because the collinear case has to be a miss: a cable drawn exactly on top of a conductor follows it, it does not cross it, and counting it would claim a core the cable never reaches. @param diagram the folio the line is drawn on @param line the line, in scene coordinates @param conductor the wire being asked about @param result receives the crossing point, in scene coordinates @return true when the line really runs across that wire */ bool CableManager::crossingOf(Diagram *diagram, const QLineF &line, Conductor *conductor, QPointF *result) { if (!conductor || !diagram) return false; const QPointF start = line.p1(); const QPointF end = line.p2(); if (qFuzzyCompare(start, end)) return false; //Work in the conductor's own coordinates: its path is stored //there, and a conductor is never where it was when it was //created once the whole drawing has been moved about. const QPointF a = conductor->mapFromScene(start); const QPointF b = conductor->mapFromScene(end); const QPainterPath path = conductor->path(); for (int i = 0; i + 1 < path.elementCount(); ++i) { const QPainterPath::Element &e1 = path.elementAt(i); const QPainterPath::Element &e2 = path.elementAt(i + 1); //A MoveTo ends the segment before it: the jump to the new //subpath is not drawn, so nothing can cross it. if (e2.type == QPainterPath::MoveToElement) { continue; } QPointF where; if (segmentIntersection(a, b, QPointF(e1.x, e1.y), QPointF(e2.x, e2.y), &where)) { if (result) { *result = conductor->mapToScene(where); } return true; } } return false; } /** @brief CableManager::distanceTo How far a point of the sheet stands from a wire, in scene units. @param conductor @param scene_pos @return that distance, which is where the wire passes when the point is beside it rather than in front of one of its ends */ qreal CableManager::distanceTo(Conductor *conductor, const QPointF &scene_pos) { if (!conductor) { return std::numeric_limits::max(); } qreal best = std::numeric_limits::max(); const QPainterPath path = conductor->path(); for (int i = 0; i + 1 < path.elementCount(); ++i) { const QPainterPath::Element &e1 = path.elementAt(i); const QPainterPath::Element &e2 = path.elementAt(i + 1); if (e2.type == QPainterPath::MoveToElement) continue; const qreal d = distanceToSegment( scene_pos, conductor->mapToScene(QPointF(e1.x, e1.y)), conductor->mapToScene(QPointF(e2.x, e2.y))); if (d < best) { best = d; } } return best; } namespace { /** @brief The end of @a conductor which does not touch @a element. Which end that is tells where the wire goes on its own sheet: a wire ending on a report arrow points away from it, and that is the end which says something about where it comes from or where it runs to. @param conductor the wire to look at @param element the report arrow it ends on (may be nullptr) @return the scene position of the other end, (0, 0) when there is none */ QPointF farEndPosition(const Conductor *conductor, Element *element) { const Terminal *far = nullptr; if (conductor->terminal1 && conductor->terminal1->parentElement() != element) { far = conductor->terminal1; } else { far = conductor->terminal2 ? conductor->terminal2 : conductor->terminal1; } return far ? far->scenePos() : QPointF(); } /** @brief Whether wire @a a runs before wire @a b on its own sheet. Both are looked at from @a element: what tells them apart is where their other end goes. Putting the wires which come in to a report and the wires which go out of its partner in this order is what pairs a continuation up with the core it belongs to when several cores run through one and the same report. @param a @param b the two wires to compare @param element the report arrow both end on (may be nullptr) @return true when @a a comes first */ bool farBefore(const Conductor *a, const Conductor *b, Element *element) { const QPointF pa = farEndPosition(a, element); const QPointF pb = farEndPosition(b, element); if (pa.y() != pb.y()) return pa.y() < pb.y(); if (pa.x() != pb.x()) return pa.x() < pb.x(); return a->uuid() < b->uuid(); } } // namespace /** @brief CableManager::refreshLabels Rewrite the cable field of every conductor of the project from the cables themselves. The rule is one-directional and therefore cannot drift: a conductor which some cable claims gets the label that cable generates, a conductor which no cable claims any more -- because the cable was delete, the core was dragged away or the type lost that core -- has the generated label taken away. A conductor which never belonged to a cable keeps whatever it had, since only one with a reference is touched. A core which ends on a folio report goes on over there: the wire coming out of the linked arrow shows the same piece of copper, so it belongs to the same core and carries the same cable field. What the links say is worked out from scratch on every pass, and a wire which some cable already claims is never taken away from it, so the reference appears wherever a report carries a core and disappears again by itself as soon as the cable, the core or the link itself is gone. Chains of reports are followed as far as they go, so a cable may run over any number of sheets. @param project */ void CableManager::refreshLabels(QETProject *project) { if (!project) return; struct Binding { QUuid cable; int slot = -1; QString label; }; QHash bindings; for (Cable *cable : project->cables()) { if (!cable) continue; for (const CableCore &core : cable->usedCores()) { if (core.conductor.isNull()) continue; Binding binding; binding.cable = cable->uuid(); binding.slot = core.core; binding.label = cable->labelOfCore(core.core); bindings.insert(core.conductor, binding); } } //A core which ends on a folio report continues on the wire which //leaves the linked arrow on the other sheet. That wire shows the //same piece of copper, so it gets the same reference as the core //it continues. The walk repeats while it finds new wires, so a //chain of reports is followed across any number of sheets, and //it only ever fills a wire no cable has claimed yet: two cores //can never fight over the same wire, and a reference worked out //here is taken away again by the same pass as soon as the cable, //the core or the link does. QHash conductors_by_uuid; QHash> endings_on_report; for (Diagram *diagram : project->diagrams()) { if (!diagram) continue; for (Conductor *conductor : diagram->conductors()) { if (conductor) conductors_by_uuid.insert(conductor->uuid(), conductor); } } //Where a wire goes on its own sheet (farBefore, above): putting the //wires which come in and the wires which go out in the same order //is what pairs a continuation up with its core when several cores //run through one and the same report. bool grew = true; while (grew) { grew = false; endings_on_report.clear(); for (const QUuid &source_uuid : bindings.keys()) { Conductor *source = conductors_by_uuid.value(source_uuid); if (!source) continue; Terminal *ends[2] = {source->terminal1, source->terminal2}; for (Terminal *end : ends) { if (!end) continue; Element *element = end->parentElement(); //An arrow taken off the sheet carries nothing over //any more, whatever still links it if (!element || !element->scene()) continue; if (!(element->linkType() & Element::AllReport)) continue; QList &in = endings_on_report[element]; if (!in.contains(source)) in.append(source); } } for (auto it = endings_on_report.constBegin(); it != endings_on_report.constEnd(); ++it) { Element *element = it.key(); QList in = it.value(); std::sort(in.begin(), in.end(), [&](const Conductor *a, const Conductor *b) { return farBefore(a, b, element); }); for (Element *partner : element->linkedElements()) { if (!partner || partner == element || !partner->scene()) continue; QList out = partner->conductors(); std::sort(out.begin(), out.end(), [&](const Conductor *a, const Conductor *b) { return farBefore(a, b, partner); }); //One core coming in may branch out into several wires //over there -- they all show the same piece of copper. //Several cores coming in take one wire each, in the //order they run on their own sheet. const int count = (in.size() == 1) ? out.size() : qMin(in.size(), out.size()); for (int i = 0; i < count; ++i) { Conductor *continued = out.at(i); if (!continued || bindings.contains(continued->uuid())) continue; const int from = qMin(i, in.size() - 1); bindings.insert(continued->uuid(), bindings.value(in.at(from)->uuid())); grew = true; } } } } for (Diagram *diagram : project->diagrams()) { if (!diagram) continue; for (Conductor *conductor : diagram->conductors()) { if (!conductor) continue; const auto it = bindings.constFind(conductor->uuid()); if (it != bindings.constEnd()) { const ConductorProperties &properties = conductor->properties(); conductor->setCableReference(it->label, it->cable, it->slot); } else if (!conductor->properties().m_cable_uuid.isNull()) { conductor->setCableReference(QString(), QUuid(), -1); } } } } /** @brief CableManager::claimConductor Make a conductor a core of @a new_owner, taking it away from whichever cable held it before. A conductor is a single piece of wire: two cables cannot both claim it, and the one that loses it must not go on drawing a label for it. @param project @param conductor @param new_owner the cable which is about to use the conductor */ void CableManager::claimConductor(QETProject *project, Conductor *conductor, Cable *new_owner) { if (!project || !conductor) return; const QUuid owner_uuid = conductor->properties().m_cable_uuid; if (owner_uuid.isNull() || (new_owner && owner_uuid == new_owner->uuid())) { return; } if (Cable *previous = cableByUuid(project, owner_uuid)) { previous->removeCore(conductor->properties().m_cable_slot); } } /** @brief CableManager::cableByUuid @param project @param uuid @return the cable with that identity, or nullptr */ Cable *CableManager::cableByUuid(QETProject *project, const QUuid &uuid) { if (!project || uuid.isNull()) return nullptr; for (Cable *cable : project->cables()) { if (cable && cable->uuid() == uuid) return cable; } return nullptr; } /** @brief CableManager::conductorByUuid @param project @param uuid @return the conductor with that identity, or nullptr. Needed because a cable remembers its cores by identity: only the drawing knows where they ended up. */ Conductor *CableManager::conductorByUuid(QETProject *project, const QUuid &uuid) { if (!project || uuid.isNull()) return nullptr; for (Diagram *diagram : project->diagrams()) { if (!diagram) continue; for (Conductor *conductor : diagram->conductors()) { if (conductor && conductor->uuid() == uuid) return conductor; } } return nullptr; } /** @brief CableManager::nextDesignation Work out the number of the next cable drawn on a folio. When the project has a numbering rule for cables, that rule decides: whatever it holds at this moment is the number, read by the very same machinery the element numbering reads its own. When it has none, the plain letter W is handed out: a project which numbers its cables by no rule gets no numbers invented for it, and W -- the one part of the old numbers which was never a number -- is all such a cable is called until a rule is defined. A rule whose counter belongs to one folio is read from the beginning of the folio the cable is drawn on, so that the number this cable gets is the first free one of its own folio rather than whatever the project's rule happens to hold after numbering other folios. Nothing here moves the project's own rule on: that belongs to the moment the cable really comes into being. @param project @param diagram the folio the cable is being drawn on @return the number */ QString CableManager::nextDesignation(QETProject *project, Diagram *diagram) { NumerotationContext context; if (project) { context = project->cableAutoNum(project->cableCurrentAutoNum()); } //The folio-bound parts are put back to the start of that folio; //the stepping of the overload below then walks past every //number of this folio which is already taken context = autonum::resetFolioCounters(context); return nextDesignation(project, diagram, context); } /** @brief CableManager::nextDesignation Work the number of a cable out of a rule which the caller keeps in @p context -- the project's own rule as it stands, or a copy of it which the caller is numbering a whole run of cables with. Whatever the rule holds is the number, unless some cable already stands under it: then @p context is stepped on and the rule is read once more, so that a paste which brings in several cables at once numbers each of them differently although the project's own rule does not move until those cables really exist. A rule which cannot step -- one with nothing in it which counts -- hands out what it says and says no more, for what it says is all it knows. A project which numbers its cables by no rule is not read at all and is handed the plain letter W instead: no numbers are invented where none have been defined. @param project @param diagram the folio the cable is being drawn on, which every folio-bound part of a rule is read from @param context the rule to read; stepped on when what it says is already taken @return the number */ QString CableManager::nextDesignation(QETProject *project, Diagram *diagram, NumerotationContext &context) { if (!project || !diagram) return QString(); const QString key = project->cableCurrentAutoNum(); //Whether there was a rule to read at all, taken down before the //loop: stepping the rule on empties it, and that must not look //like a project which never had one. const bool ruled = !key.isEmpty() && !context.isEmpty(); QString previous; for (int attempt = 0; ruled && attempt < 1000 && !context.isEmpty(); ++attempt) { //The rule is read on a copy: reading it fills in what its //folio-bound parts are worth here and now, and that must //not creep into the rule itself. NumerotationContext evaluated = context; const QString formula = autonum::numerotationContextToFormula(evaluated); autonum::sequentialNumbers seq; autonum::setSequential(formula, seq, evaluated, diagram, key); const QString candidate = autonum::AssignVariables::formulaToLabel(formula, seq, diagram); if (candidate.isEmpty()) break; if (!designationIsTaken(project, candidate)) return candidate; //Taken already: step the rule on and read it once more. if (candidate == previous) return candidate; previous = candidate; NumerotationContextCommands ncc(context, diagram); const NumerotationContext following = ncc.next(); if (following.size() != context.size()) break; context = following; } //No rule at all: the plain letter W, and nothing more. The old //numbers read folio + W + index, and W is the only part of them //which was never a number -- so it is what a project which has //defined no numbering rule calls its cables, until it defines //one. if (!ruled) { return QStringLiteral("W"); } //A rule which says nothing usable here: no number at all. The //number is what the rule says, and a rule which says nothing //leaves the field to the user rather than having it filled in //with something it never held. return QString(); }