diagrams-fable-qt/src/scene/EdgeItem.cpp
Ilya Ashikhmin b257c1a7cf feat(scene): add interactive graphics scene with grid, ports and styled edges
- 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>
2026-07-02 23:25:06 +02:00

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