wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
Connection.cpp
Go to the documentation of this file.
1// Copyright (c) 2012, STANISLAW ADASZEWSKI
2// SPDX-License-Identifier: BSD-3-Clause
3//
4// Portions Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
5// SPDX-License-Identifier: GPL-3.0-or-later
6
8
9#include <QGraphicsSceneMouseEvent>
10#include <QPainter>
11#include <QPainterPath>
12#include <QPen>
13#include <QPolygonF>
14#include <QStyleOptionGraphicsItem>
15
18#include "App/Scene/Scene.h"
20#include "App/Wiring/Port.h"
21
22Connection::Connection(QGraphicsItem *parent)
23 : QGraphicsPathItem(parent)
24{
25 setFlag(QGraphicsItem::ItemIsSelectable);
26 // Deliberately NOT DeviceCoordinateCache (the default NoCache stays): a wire is a thin
27 // stroke crossing a huge, mostly-empty bounding box, so a device-cache tile costs a
28 // bbox-sized pixmap clear + blend on every status colour change -- vastly more than just
29 // re-stroking the path -- and multi-MB tiles for long wires thrash the global QPixmapCache.
30 // Draw wires behind elements so port dots and element bodies always render on top
31 setZValue(-1);
32
33 // m_status starts at Status::Unknown; updateTheme() applies its pen via
34 // applyStatusPen(). The wire colour is updated once both ports are attached.
36}
37
39{
40 if (m_startPort) {
41 m_startPort->detachConnection(this);
42 }
43
44 if (m_endPort) {
45 m_endPort->detachConnection(this);
46 }
47}
48
49void Connection::setStartPos(const QPointF point)
50{
51 m_startPos = point;
52}
53
54void Connection::setEndPos(const QPointF point)
55{
56 m_endPos = point;
57}
58
59void Connection::changePortAttachment(Port *oldPort, Port *newPort)
60{
61 // Detach from the previous port before attaching to the new one to avoid
62 // dangling entries in the old port's connection list (e.g. during IC hot-reload)
63 if (oldPort && (oldPort != newPort)) {
64 oldPort->detachConnection(this);
65 }
66 if (newPort) {
67 newPort->attachConnection(this);
68 }
69}
70
72{
73 auto *oldPort = m_startPort;
74 m_startPort = port;
75 changePortAttachment(oldPort, port);
76 if (port) {
77 setStartPos(port->scenePos());
78 // Inherit the source port's signal status so the wire colour is correct immediately
79 setStatus(port->status());
80 }
81}
82
84{
85 auto *oldPort = m_endPort;
86 m_endPort = port;
87 changePortAttachment(oldPort, port);
88 if (port) {
89 setEndPos(port->scenePos());
90 // Push the current wire status into the destination port so it displays correctly
91 // even before the next simulation step
92 port->setStatus(status());
93 }
94}
95
97{
98 const QPointF newStartPos = m_startPort ? m_startPort->scenePos() : m_startPos;
99 const QPointF newEndPos = m_endPort ? m_endPort->scenePos() : m_endPos;
100
101 // Skip the Bézier rebuild -- and the QPainterPath comparison setPath() does internally
102 // to decide whether a repaint is even needed -- when neither endpoint actually moved.
103 // Some callers (bulk commands, deserialization) refresh connections without knowing in
104 // advance which ones were actually affected; QPointF equality is exact and cheap, so
105 // this turns those into a no-op instead of a full path rebuild + Qt's own comparison.
106 if (newStartPos == m_startPos && newEndPos == m_endPos) {
107 return;
108 }
109
110 m_startPos = newStartPos;
111 m_endPos = newEndPos;
112
113 updatePath();
114}
115
117{
118 // Skip expensive Bézier path construction when there is no visible rendering.
119 // Tests and fuzz harnesses never paint, so building and comparing
120 // QPainterPaths is pure waste there. This must NOT key off interactiveMode:
121 // both MCP modes run non-interactively yet still render (the --mcp-gui
122 // window, and export_image painting the scene off-screen in headless --mcp).
124 return;
125 }
126
127 QPainterPath path;
128
129 path.moveTo(m_startPos);
130
131 qreal dx = m_endPos.x() - m_startPos.x();
132 qreal dy = m_endPos.y() - m_startPos.y();
133
134 // Cubic Bézier control points chosen so the wire leaves/arrives roughly horizontally
135 // (small Y fraction) and curves gently in the middle (0.25/0.75 X fraction).
136 // This gives an S-curve appearance typical of schematic wire routing tools.
137 QPointF ctr1(m_startPos.x() + dx * 0.25, m_startPos.y() + dy * 0.1);
138 QPointF ctr2(m_startPos.x() + dx * 0.75, m_startPos.y() + dy * 0.9);
139
140 path.cubicTo(ctr1, ctr2, m_endPos);
141
142 setPath(path);
143 m_shapeDirty = true;
144}
145
147{
148 return m_startPort;
149}
150
152{
153 return m_endPort;
154}
155
157{
158 Port *port1 = m_startPort;
159 Port *port2 = m_endPort;
160
161 if (port1 && port2) {
162 // Normalise: port1 = output, port2 = input regardless of which end was set first.
163 // QLineF::angle() measures from positive X axis counter-clockwise, so the
164 // returned angle always describes the direction from output to input.
165 if (port2->isOutput()) {
166 std::swap(port1, port2);
167 }
168
169 return QLineF(port1->scenePos(), port2->scenePos()).angle();
170 }
171
172 return 0.0;
173}
174
175void Connection::save(QDataStream &stream) const
176{
177 ConnectionSerializer::save(*this, stream);
178}
179
180void Connection::load(QDataStream &stream, SerializationContext &context)
181{
182 ConnectionSerializer::load(*this, stream, context);
183}
184
185Port *Connection::otherPort(const Port *port) const
186{
187 if (port == m_startPort) {
188 return m_endPort;
189 }
190
191 return m_startPort;
192}
193
195{
196 return m_status;
197}
198
200{
201 if (status == m_status) {
202 return;
203 }
204
205 m_status = status;
206
207 // Feed idle detection for adaptive wire antialiasing: a stream of status changes IS
208 // the simulation repaint storm, and it stops exactly when the simulation does.
209 if (auto *scene_ = qobject_cast<Scene *>(scene())) {
210 scene_->noteWireActivity();
211 }
212
213 applyStatusPen();
214
215 // Propagate to the destination port so its fill colour also reflects the signal state
216 if (endPort()) {
218 }
219}
220
221void Connection::applyStatusPen()
222{
223 // Error wires are drawn thicker (5 px) to draw attention to the problem;
224 // other wires are thinner (3 px) to reduce visual clutter during simulation.
225 // Unknown (undriven) wires use a distinct gray from Error (red).
226 switch (m_status) {
227 case Status::Unknown: m_statusPen = QPen(m_unknownColor, 3); break;
228 case Status::Inactive: m_statusPen = QPen(m_inactiveColor, 3); break;
229 case Status::Active: m_statusPen = QPen(m_activeColor, 3); break;
230 case Status::Error: m_statusPen = QPen(m_errorColor, 5); break;
231 }
232
233 // paint() draws with m_statusPen, not the item's own pen() -- boundingRect() is a fixed
234 // margin independent of pen width (see its own comment), so calling the real setPen()
235 // here would pay for a BSP-tree re-index (prepareGeometryChange()) on every status colour
236 // change for no actual geometry benefit. The one exception is width: shape()'s default
237 // hit-testing *does* stroke the path using the item's real pen width, so the real setPen()
238 // still runs whenever the width actually changes (Error <-> non-Error) to keep an
239 // Error-status wire's (wider) click target accurate.
240 if (!qFuzzyCompare(m_statusPen.widthF(), pen().widthF())) {
241 setPen(m_statusPen);
242 // The real pen's width sets shape()'s stroke tolerance -- rebuild the cached shape.
243 m_shapeDirty = true;
244 } else {
245 update();
246 }
247}
248
250{
251 const auto &theme = ThemeManager::attributes();
252 m_unknownColor = theme.m_connectionUnknown;
253 m_inactiveColor = theme.m_connectionInactive;
254 m_activeColor = theme.m_connectionActive;
255 m_errorColor = theme.m_connectionError;
256 m_selectedColor = theme.m_connectionSelected;
257
258 // Re-derive the pen from the refreshed palette; without this the wire keeps
259 // the previous theme's colour until its status next changes
260 applyStatusPen();
261
262 update();
263}
264
265void Connection::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
266{
267 Q_UNUSED(widget)
268 Q_UNUSED(option)
269
270 // Adaptive quality (see Scene's wire-antialiasing accessors): while measured paint
271 // passes are too slow for interactivity, drop this wire's antialiasing -- never force
272 // it on, so a global choice like Fast Mode still wins. The painter state is
273 // saved/restored around each item's paint by QGraphicsView (the DontSavePainterState
274 // optimization flag is not set), so the hint can't leak into other items.
275 if (auto *scene_ = qobject_cast<Scene *>(scene()); scene_ && !scene_->wireAntialiasingEnabled()) {
276 painter->setRenderHint(QPainter::Antialiasing, false);
277 }
278
279 // Highlight is drawn as a wider blue halo beneath the normal wire, so users can
280 // easily see which wires belong to a selected element
281 if (m_highLight) {
282 painter->save();
283 painter->setPen(QPen(Qt::blue, 10));
284 painter->drawPath(path());
285 painter->restore();
286 }
287
288 // When the wire itself is selected (clicked), switch to the selection colour;
289 // otherwise use the status-driven pen set by setStatus() via applyStatusPen()
290 painter->setPen(isSelected() ? QPen(m_selectedColor, 5) : m_statusPen);
291 painter->drawPath(path());
292}
293
294QVariant Connection::itemChange(GraphicsItemChange change, const QVariant &value)
295{
296 // When the wire is selected/deselected, visually highlight both endpoint ports
297 // so the user can see which element pins are connected by this wire
298 if (change == ItemSelectedChange) {
299 if (value.toBool()) {
300 if (startPort()) startPort()->hoverEnter();
301 if (endPort()) endPort()->hoverEnter();
302 } else {
303 if (startPort()) startPort()->hoverLeave();
304 if (endPort()) endPort()->hoverLeave();
305 }
306 }
307
308 return QGraphicsPathItem::itemChange(change, value);
309}
310
312{
313 return m_highLight;
314}
315
317{
318 m_highLight = highLight;
319 update();
320}
321
323{
324 // Expand beyond the path's tight bounding box by 10 px on all sides to ensure
325 // the thick selection outline and highlight halo are fully covered during repaints
326 return path().boundingRect().adjusted(-10, -10, 10, 10);
327}
328
329QPainterPath Connection::shape() const
330{
331 if (m_shapeDirty) {
332 // Same stroke semantics as Qt's default (qt_graphicsItem_shapeFromPath): the real
333 // pen's width sets the click tolerance along the wire -- which is why the real
334 // setPen() still runs on Error<->normal width transitions (see applyStatusPen()).
335 // Stroking a flattened (polygonal) copy instead of the raw Bézier keeps later
336 // intersects()/contains() tests on line segments; the flattening error is
337 // sub-pixel, far inside the stroke tolerance.
338 QPainterPathStroker stroker;
339 stroker.setWidth(qMax(pen().widthF(), 0.00000001));
340 stroker.setCapStyle(pen().capStyle());
341 stroker.setJoinStyle(pen().joinStyle());
342 stroker.setMiterLimit(pen().miterLimit());
343
344 // toSubpathPolygons() keeps open subpaths open -- toFillPolygon() would close the
345 // polyline back to its start, adding a phantom straight segment to the hit area.
346 QPainterPath flattened;
347 for (const QPolygonF &polygon : path().toSubpathPolygons()) {
348 flattened.addPolygon(polygon);
349 }
350
351 m_cachedShape = stroker.createStroke(flattened);
352 m_shapeDirty = false;
353 }
354
355 return m_cachedShape;
356}
357
358bool Connection::sceneEvent(QEvent *event)
359{
360 // Swallow Ctrl+click so the scene can use Ctrl+click for multi-selection without
361 // the wire consuming the event and blocking the rubber-band/deselect behaviour
362 if (auto mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent *>(event)) {
363 if (mouseEvent->modifiers().testFlag(Qt::ControlModifier)) {
364 return true;
365 }
366 }
367
368 return QGraphicsPathItem::sceneEvent(event);
369}
Custom QApplication subclass with exception handling and main-window access.
ConnectionSerializer: reads/writes a Connection to a versioned QDataStream.
Connection: a wire that connects an output port to an input port in the circuit scene.
Enums::Status Status
Definition Enums.h:106
Port classes: Port (base), InputPort, and OutputPort.
Main circuit editing scene with undo/redo and user interaction.
Theme management types and singleton ThemeManager.
static bool renderingEnabled
Definition Application.h:80
static void save(const Connection &connection, QDataStream &stream)
Writes connection's endpoint port serial IDs to stream.
static void load(Connection &connection, QDataStream &stream, SerializationContext &context)
Reads connection's endpoints from stream, resolving ports via context.
Connection(QGraphicsItem *parent=nullptr)
Constructs an unconnected wire.
OutputPort * startPort() const
Returns the output port this connection originates from.
void updateTheme()
Refreshes wire colours from the current ThemeManager palette.
void setHighLight(const bool highLight)
Enables or disables connection highlighting.
Status status() const
Returns the current four-state signal status (Active/Inactive/Unknown/Error).
void setEndPos(const QPointF point)
Sets the visual end position to point (used when dragging).
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
void updatePath()
Recomputes the bezier control points from the current start/end positions.
InputPort * endPort() const
Returns the input port this connection leads to.
~Connection() override
QRectF boundingRect() const override
bool highLight()
Returns true if this connection is highlighted.
void setStatus(const Status status)
Sets the connection status and triggers a visual refresh.
double angle()
Returns the current angle of the bezier midpoint in radians.
void save(QDataStream &stream) const
Serializes the connection endpoints to stream.
QPainterPath shape() const override
QVariant itemChange(GraphicsItemChange change, const QVariant &value) override
void updatePosFromPorts()
Moves the wire endpoints to match the current port positions.
void load(QDataStream &stream, SerializationContext &context)
Deserializes the connection from stream, resolving ports via context.
void setEndPort(InputPort *port)
Sets the input port this connection leads to.
Port * otherPort(const Port *port) const
Returns the port at the other end of this connection from port.
void setStartPort(OutputPort *port)
Sets the output port this connection originates from.
bool sceneEvent(QEvent *event) override
void setStartPos(const QPointF point)
Sets the visual start position to point (used when dragging).
A port that receives a signal (the destination end of a wire).
Definition Port.h:229
void setStatus(const Status status) override
Definition Port.cpp:307
A port that drives a signal (the source end of a wire).
Definition Port.h:261
Abstract base class for circuit element ports (connection endpoints).
Definition Port.h:39
virtual bool isOutput() const =0
Returns true if this is an output port. Pure virtual.
void attachConnection(Connection *conn)
Registers conn as a connection attached to this port.
Definition Port.cpp:62
void hoverEnter()
Applies hover-enter visual feedback.
Definition Port.cpp:218
void detachConnection(Connection *conn)
Removes conn from this port's connection list.
Definition Port.cpp:77
Status status() const
Returns the current logical status (Active/Inactive/Unknown/Error).
Definition Port.h:79
void hoverLeave()
Reverts hover-enter visual feedback.
Definition Port.cpp:213
static const ThemeAttributes & attributes()
Returns the current ThemeAttributes color set.
Bundles all per-deserialization state so that load() overrides receive it through one explicit parame...