/* TikZiT - a GUI diagram editor for TikZ Copyright (C) 2018 Aleks Kissinger This program 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 3 of the License, or (at your option) any later version. This program 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 this program. If not, see . */ #include "edge.h" #include "tikzit.h" #include "util.h" #include #include Edge::Edge(Node *s, Node *t, QObject *parent) : QObject(parent), _source(s), _target(t) { _data = new GraphElementData(this); _edgeNode = nullptr; _path = nullptr; _dirty = true; if (s != t) { _basicBendMode = true; _bend = 0; _inAngle = 0; _outAngle = 0; _weight = 0.4; } else { _basicBendMode = false; _bend = 0; _inAngle = 135; _outAngle = 45; _weight = 1.0; } _style = noneEdgeStyle; updateControls(); } /*! * @brief Edge::copy makes a deep copy of an edge. * @param nodeTable is an optional pointer to a table mapping the old source/target * node pointers to their new, copied versions. This is used when making a copy of * an entire (sub)graph. * @return a copy of the edge */ Edge *Edge::copy(QMap *nodeTable) { Edge *e; if (nodeTable == nullptr) { e = new Edge(_source, _target); } else { Node *s = nodeTable->value(_source); Node *t = nodeTable->value(_target); e = new Edge((s != nullptr) ? s : _source, (t != nullptr) ? t : _target); } e->setData(_data->copy()); e->setBasicBendMode(_basicBendMode); e->setBend(_bend); e->setInAngle(_inAngle); e->setOutAngle(_outAngle); e->setWeight(_weight); e->attachStyle(); e->updateControls(); return e; } Node *Edge::source() const { return _source; } Node *Edge::target() const { return _target; } bool Edge::isSelfLoop() { return (_source == _target); } bool Edge::isStraight() { return (_basicBendMode && _bend == 0); } GraphElementData *Edge::data() const { return _data; } void Edge::setData(GraphElementData *data) { GraphElementData *oldData = _data; _data = data; oldData->deleteLater(); setAttributesFromData(); } QString Edge::styleName() const { QString nm = _data->property("style"); if (nm.isNull()) return "none"; else return nm; } void Edge::setStyleName(const QString &styleName) { if (!styleName.isNull() && styleName != "none") _data->setProperty("style", styleName); else _data->unsetProperty("style"); } QString Edge::sourceAnchor() const { return _sourceAnchor; } void Edge::setSourceAnchor(const QString &sourceAnchor) { _sourceAnchor = sourceAnchor; } QString Edge::targetAnchor() const { return _targetAnchor; } void Edge::setTargetAnchor(const QString &targetAnchor) { _targetAnchor = targetAnchor; } Node *Edge::edgeNode() const { return _edgeNode; } void Edge::setEdgeNode(Node *edgeNode) { Node *oldEdgeNode = _edgeNode; _edgeNode = edgeNode; if (oldEdgeNode != nullptr) oldEdgeNode->deleteLater(); } bool Edge::hasEdgeNode() { return _edgeNode != nullptr; } void Edge::updateControls() { //if (_dirty) { QPointF src = _source->point(); QPointF targ = _target->point(); qreal dx = (targ.x() - src.x()); qreal dy = (targ.y() - src.y()); qreal outAngleR = 0.0; qreal inAngleR = 0.0; if (_basicBendMode) { qreal angle = std::atan2(dy, dx); qreal bnd = static_cast(_bend) * (M_PI / 180.0); outAngleR = angle - bnd; inAngleR = M_PI + angle + bnd; // keep _inAngle and _outAngle snapped to increments of 15 degrees _outAngle = static_cast(roundToNearest(15.0, outAngleR * (180.0 / M_PI))); _inAngle = static_cast(roundToNearest(15.0, inAngleR * (180.0 / M_PI))); } else { outAngleR = static_cast(_outAngle) * (M_PI / 180.0); inAngleR = static_cast(_inAngle) * (M_PI / 180.0); } // TODO: calculate head and tail properly, not just for circles if (_source->style()->isNone()) { _tail = src; } else { _tail = QPointF(src.x() + std::cos(outAngleR) * 0.2, src.y() + std::sin(outAngleR) * 0.2); } if (_target->style()->isNone()) { _head = targ; } else { _head = QPointF(targ.x() + std::cos(inAngleR) * 0.2, targ.y() + std::sin(inAngleR) * 0.2); } // give a default distance for self-loops _cpDist = (almostZero(dx) && almostZero(dy)) ? _weight : std::sqrt(dx*dx + dy*dy) * _weight; _cp1 = QPointF(src.x() + (_cpDist * std::cos(outAngleR)), src.y() + (_cpDist * std::sin(outAngleR))); _cp2 = QPointF(targ.x() + (_cpDist * std::cos(inAngleR)), targ.y() + (_cpDist * std::sin(inAngleR))); _mid = bezierInterpolateFull (0.5, _tail, _cp1, _cp2, _head); _tailTangent = bezierTangent(0.0, 0.1); _headTangent = bezierTangent(1.0, 0.9); } void Edge::setAttributesFromData() { _basicBendMode = true; bool ok = true; if (_data->atom("bend left")) { _bend = -30; } else if (_data->atom("bend right")) { _bend = 30; } else if (_data->property("bend left") != nullptr) { _bend = -_data->property("bend left").toInt(&ok); if (!ok) _bend = -30; } else if (_data->property("bend right") != nullptr) { _bend = _data->property("bend right").toInt(&ok); if (!ok) _bend = 30; } else { _bend = 0; if (_data->property("in") != nullptr && _data->property("out") != nullptr) { _basicBendMode = false; _inAngle = _data->property("in").toInt(&ok); if (!ok) _inAngle = 0; _outAngle = _data->property("out").toInt(&ok); if (!ok) _outAngle = 180; } } if (!_data->property("looseness").isNull()) { _weight = _data->property("looseness").toDouble(&ok) / 2.5; if (!ok) _weight = 0.4; } else { _weight = (isSelfLoop()) ? 1.0 : 0.4; } //qDebug() << "bend: " << _bend << " in: " << _inAngle << " out: " << _outAngle; _dirty = true; } void Edge::updateData() { _data->unsetAtom("loop"); _data->unsetProperty("in"); _data->unsetProperty("out"); _data->unsetAtom("bend left"); _data->unsetAtom("bend right"); _data->unsetProperty("bend left"); _data->unsetProperty("bend right"); _data->unsetProperty("looseness"); if (_basicBendMode) { if (_bend != 0) { QString bendKey; int b; if (_bend < 0) { bendKey = "bend left"; b = -_bend; } else { bendKey = "bend right"; b = _bend; } if (b == 30) { _data->setAtom(bendKey); } else { _data->setProperty(bendKey, QString::number(b)); } } } else { _data->setProperty("in", QString::number(_inAngle)); _data->setProperty("out", QString::number(_outAngle)); } if (_source == _target) _data->setAtom("loop"); if (!isSelfLoop() && !isStraight() && !almostEqual(_weight, 0.4)) _data->setProperty("looseness", QString::number(_weight*2.5, 'f', 2)); if (_source->isBlankNode()) _sourceAnchor = "center"; else _sourceAnchor = ""; if (_target->isBlankNode()) _targetAnchor = "center"; else _targetAnchor = ""; } QPointF Edge::head() const { return _head; } QPointF Edge::tail() const { return _tail; } QPointF Edge::cp1() const { return _cp1; } QPointF Edge::cp2() const { return _cp2; } int Edge::bend() const { return _bend; } int Edge::inAngle() const { return _inAngle; } int Edge::outAngle() const { return _outAngle; } qreal Edge::weight() const { return _weight; } bool Edge::basicBendMode() const { return _basicBendMode; } qreal Edge::cpDist() const { return _cpDist; } void Edge::setBasicBendMode(bool mode) { _basicBendMode = mode; } void Edge::setBend(int bend) { _bend = bend; } void Edge::setInAngle(int inAngle) { _inAngle = inAngle; } void Edge::setOutAngle(int outAngle) { _outAngle = outAngle; } void Edge::setWeight(qreal weight) { _weight = weight; } void Edge::reverse() { Node *n = _source; _source = _target; _target = n; int a = _inAngle; _inAngle = _outAngle; _outAngle = a; _bend = -_bend; updateData(); } int Edge::tikzLine() const { return _tikzLine; } void Edge::setTikzLine(int tikzLine) { _tikzLine = tikzLine; } QPointF Edge::mid() const { return _mid; } QPointF Edge::headTangent() const { return _headTangent; } QPointF Edge::tailTangent() const { return _tailTangent; } void Edge::attachStyle() { QString nm = styleName(); if (nm.isNull()) _style = noneEdgeStyle; else _style = tikzit->styles()->edgeStyle(nm); } Style *Edge::style() const { return _style; } QPointF Edge::bezierTangent(qreal start, qreal end) const { qreal dx = bezierInterpolate(end, _tail.x(), _cp1.x(), _cp2.x(), _head.x()) - bezierInterpolate(start, _tail.x(), _cp1.x(), _cp2.x(), _head.x()); qreal dy = bezierInterpolate(end, _tail.y(), _cp1.y(), _cp2.y(), _head.y()) - bezierInterpolate(start, _tail.y(), _cp1.y(), _cp2.y(), _head.y()); // normalise qreal len = sqrt(dx*dx + dy*dy); if (!almostZero(len)) { dx = (dx / len) * 0.1; dy = (dy / len) * 0.1; } return QPointF(dx, dy); } Path *Edge::path() const { return _path; } void Edge::setPath(Path *path) { _path = path; }