- SVG node items with snap-to-grid dragging; ports appear on focus/hover and highlight compatible targets during a connection drag - Orthogonally routed edge items with line styles, head/tail decorations (arrow, circle, diamond, bar), selection halo and title labels; later edges jump crossings with arcs when enabled - Grid background view with cursor-anchored zoom, middle-button pan, rubber-band selection and palette drag-and-drop - Undoable commands: add/move/delete/connect/property/paste with clipboard fragments; light/dark/high-contrast themes - Flow overlay: animated dashes, direction and density follow flow rate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
168 lines
5.8 KiB
C++
168 lines
5.8 KiB
C++
#include "EdgeItem.h"
|
|
|
|
#include "DiagramScene.h"
|
|
#include "core/NetworkModel.h"
|
|
#include "core/Router.h"
|
|
|
|
#include <QPainter>
|
|
#include <QPainterPathStroker>
|
|
#include <QtMath>
|
|
|
|
namespace diag {
|
|
|
|
EdgeItem::EdgeItem(DiagramScene* scene, QUuid edgeId) : m_scene(scene), m_id(edgeId)
|
|
{
|
|
setFlag(ItemIsSelectable);
|
|
setZValue(0);
|
|
setAcceptHoverEvents(true);
|
|
}
|
|
|
|
NetworkModel* EdgeItem::model() const
|
|
{
|
|
return m_scene->model();
|
|
}
|
|
|
|
void EdgeItem::setRoute(const QList<QPointF>& poly, const QList<QPointF>& hops)
|
|
{
|
|
prepareGeometryChange();
|
|
m_poly = poly;
|
|
m_hops = hops;
|
|
const RouteConfig& cfg = m_scene->routeConfig();
|
|
m_path = Router::toPath(poly, cfg.arcOnIntersection ? hops : QList<QPointF>{},
|
|
cfg.arcRadius);
|
|
update();
|
|
}
|
|
|
|
QRectF EdgeItem::boundingRect() const
|
|
{
|
|
const double m = 14;
|
|
return m_path.boundingRect().adjusted(-m, -m, m, m);
|
|
}
|
|
|
|
QPainterPath EdgeItem::shape() const
|
|
{
|
|
QPainterPathStroker stroker;
|
|
stroker.setWidth(qMax(10.0, basePen().widthF() + 6));
|
|
return stroker.createStroke(m_path);
|
|
}
|
|
|
|
QPen EdgeItem::basePen() const
|
|
{
|
|
const Theme& theme = m_scene->theme();
|
|
const Edge* e = model()->edge(m_id);
|
|
QColor color;
|
|
const QString colorName = model()->edgeProperty(m_id, QStringLiteral("color")).toString();
|
|
if (QColor::isValidColorName(colorName))
|
|
color = QColor(colorName);
|
|
else
|
|
color = theme.relationColor(e ? model()->registry()->relation(e->relation) : nullptr);
|
|
|
|
const double width = model()->edgeProperty(m_id, QStringLiteral("lineWidth")).toDouble();
|
|
const QString style = model()->edgeProperty(m_id, QStringLiteral("lineStyle")).toString();
|
|
Qt::PenStyle penStyle = Qt::SolidLine;
|
|
if (style == QLatin1String("Dashed"))
|
|
penStyle = Qt::DashLine;
|
|
else if (style == QLatin1String("Dotted"))
|
|
penStyle = Qt::DotLine;
|
|
else if (style == QLatin1String("DashDot"))
|
|
penStyle = Qt::DashDotLine;
|
|
|
|
QPen pen(color, qMax(0.5, width), penStyle, Qt::FlatCap, Qt::MiterJoin);
|
|
return pen;
|
|
}
|
|
|
|
void EdgeItem::paint(QPainter* painter, const QStyleOptionGraphicsItem*, QWidget*)
|
|
{
|
|
if (m_poly.size() < 2)
|
|
return;
|
|
painter->setRenderHint(QPainter::Antialiasing);
|
|
const Theme& theme = m_scene->theme();
|
|
const QPen pen = basePen();
|
|
|
|
if (isSelected()) {
|
|
QColor halo = theme.selection;
|
|
halo.setAlpha(90);
|
|
painter->setPen(QPen(halo, pen.widthF() + 6, Qt::SolidLine, Qt::RoundCap));
|
|
painter->setBrush(Qt::NoBrush);
|
|
painter->drawPath(m_path);
|
|
}
|
|
|
|
painter->setPen(pen);
|
|
painter->setBrush(Qt::NoBrush);
|
|
painter->drawPath(m_path);
|
|
|
|
// End decorations sit on the raw polyline ends (arcs never reach ends).
|
|
const QString head = model()->edgeProperty(m_id, QStringLiteral("headDecoration")).toString();
|
|
const QString tail = model()->edgeProperty(m_id, QStringLiteral("tailDecoration")).toString();
|
|
drawDecoration(painter, head, m_poly.last(), m_poly.at(m_poly.size() - 2), pen.color(),
|
|
pen.widthF());
|
|
drawDecoration(painter, tail, m_poly.first(), m_poly.at(1), pen.color(), pen.widthF());
|
|
|
|
if (m_scene->flowAnimationEnabled())
|
|
drawFlowOverlay(painter, m_path, pen.widthF());
|
|
|
|
const QString title = model()->edgeProperty(m_id, QStringLiteral("title")).toString();
|
|
if (!title.isEmpty()) {
|
|
const QPointF mid = Router::pointAt(m_poly, 0.5);
|
|
painter->setPen(theme.label);
|
|
QFont f = painter->font();
|
|
f.setPointSizeF(8.0);
|
|
painter->setFont(f);
|
|
painter->drawText(QRectF(mid.x() - 60, mid.y() - 18, 120, 14), Qt::AlignCenter, title);
|
|
}
|
|
}
|
|
|
|
void EdgeItem::drawDecoration(QPainter* painter, const QString& kind, QPointF tip, QPointF back,
|
|
const QColor& color, double width) const
|
|
{
|
|
if (kind.isEmpty() || kind == QLatin1String("None"))
|
|
return;
|
|
QLineF dir(back, tip);
|
|
if (dir.length() < 1e-6)
|
|
return;
|
|
dir.setLength(1.0);
|
|
const QPointF u = dir.p2() - dir.p1(); // unit vector toward the tip
|
|
const QPointF n(-u.y(), u.x()); // unit normal
|
|
const double s = qMax(8.0, width * 3.5); // decoration size
|
|
|
|
painter->save();
|
|
painter->setPen(QPen(color, qMax(1.0, width * 0.8)));
|
|
painter->setBrush(color);
|
|
if (kind == QLatin1String("Arrow")) {
|
|
QPolygonF poly{tip, tip - u * s + n * s * 0.45, tip - u * s - n * s * 0.45};
|
|
painter->drawPolygon(poly);
|
|
} else if (kind == QLatin1String("Circle")) {
|
|
painter->drawEllipse(tip - u * s * 0.5, s * 0.5, s * 0.5);
|
|
} else if (kind == QLatin1String("Diamond")) {
|
|
const QPointF c = tip - u * s * 0.5;
|
|
QPolygonF poly{tip, c + n * s * 0.4, tip - u * s, c - n * s * 0.4};
|
|
painter->drawPolygon(poly);
|
|
} else if (kind == QLatin1String("Bar")) {
|
|
painter->setPen(QPen(color, qMax(1.5, width)));
|
|
painter->drawLine(tip + n * s * 0.5, tip - n * s * 0.5);
|
|
}
|
|
painter->restore();
|
|
}
|
|
|
|
void EdgeItem::drawFlowOverlay(QPainter* painter, const QPainterPath& path, double width) const
|
|
{
|
|
const Edge* e = model()->edge(m_id);
|
|
if (!e || qAbs(e->flowRate) < 1e-9)
|
|
return;
|
|
const double maxFlow = qMax(1e-9, m_scene->maxAbsFlow());
|
|
const double ratio = qBound(0.15, qAbs(e->flowRate) / maxFlow, 1.0);
|
|
|
|
// Moving dashes: speed and thickness encode volume, offset sign encodes
|
|
// direction (positive flow runs from tail to head).
|
|
QPen pen(m_scene->theme().flowOverlay, qMax(1.0, width * (0.25 + 0.45 * ratio)),
|
|
Qt::CustomDashLine, Qt::FlatCap);
|
|
pen.setDashPattern({1.2, 3.2});
|
|
const double dir = e->flowRate > 0 ? -1.0 : 1.0;
|
|
pen.setDashOffset(dir * m_scene->animationPhase() * (0.4 + 1.6 * ratio));
|
|
painter->setPen(pen);
|
|
painter->setBrush(Qt::NoBrush);
|
|
painter->drawPath(path);
|
|
}
|
|
|
|
} // namespace diag
|