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 // Unreachable: m_endPort is always an InputPort (setEndPort()'s parameter type
167 // enforces it at compile time), and InputPort::isOutput() is hardcoded false.
168 std::swap(port1, port2); // LCOV_EXCL_LINE
169 }
170
171 return QLineF(port1->scenePos(), port2->scenePos()).angle();
172 }
173
174 return 0.0;
175}
176
177void Connection::save(QDataStream &stream) const
178{
179 ConnectionSerializer::save(*this, stream);
180}
181
182void Connection::load(QDataStream &stream, SerializationContext &context)
183{
184 ConnectionSerializer::load(*this, stream, context);
185}
186
187Port *Connection::otherPort(const Port *port) const
188{
189 if (port == m_startPort) {
190 return m_endPort;
191 }
192
193 return m_startPort;
194}
195
197{
198 return m_status;
199}
200
202{
203 if (status == m_status) {
204 return;
205 }
206
207 m_status = status;
208
209 // Feed idle detection for adaptive wire antialiasing: a stream of status changes IS
210 // the simulation repaint storm, and it stops exactly when the simulation does.
211 if (auto *scene_ = qobject_cast<Scene *>(scene())) {
212 scene_->noteWireActivity();
213 }
214
215 applyStatusPen();
216
217 // Propagate to the destination port so its fill colour also reflects the signal state
218 if (endPort()) {
220 }
221}
222
223void Connection::applyStatusPen()
224{
225 // Error wires are drawn thicker (5 px) to draw attention to the problem;
226 // other wires are thinner (3 px) to reduce visual clutter during simulation.
227 // Unknown (undriven) wires use a distinct gray from Error (red).
228 switch (m_status) {
229 case Status::Unknown: m_statusPen = QPen(m_unknownColor, 3); break;
230 case Status::Inactive: m_statusPen = QPen(m_inactiveColor, 3); break;
231 case Status::Active: m_statusPen = QPen(m_activeColor, 3); break;
232 case Status::Error: m_statusPen = QPen(m_errorColor, 5); break;
233 }
234
235 // paint() draws with m_statusPen, not the item's own pen() -- boundingRect() is a fixed
236 // margin independent of pen width (see its own comment), so calling the real setPen()
237 // here would pay for a BSP-tree re-index (prepareGeometryChange()) on every status colour
238 // change for no actual geometry benefit. The one exception is width: shape()'s default
239 // hit-testing *does* stroke the path using the item's real pen width, so the real setPen()
240 // still runs whenever the width actually changes (Error <-> non-Error) to keep an
241 // Error-status wire's (wider) click target accurate.
242 if (!qFuzzyCompare(m_statusPen.widthF(), pen().widthF())) {
243 setPen(m_statusPen);
244 // The real pen's width sets shape()'s stroke tolerance -- rebuild the cached shape.
245 m_shapeDirty = true;
246 } else {
247 update();
248 }
249}
250
252{
253 const auto &theme = ThemeManager::attributes();
254 m_unknownColor = theme.m_connectionUnknown;
255 m_inactiveColor = theme.m_connectionInactive;
256 m_activeColor = theme.m_connectionActive;
257 m_errorColor = theme.m_connectionError;
258 m_selectedColor = theme.m_connectionSelected;
259
260 // Re-derive the pen from the refreshed palette; without this the wire keeps
261 // the previous theme's colour until its status next changes
262 applyStatusPen();
263
264 update();
265}
266
267void Connection::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
268{
269 Q_UNUSED(widget)
270 Q_UNUSED(option)
271
272 // Adaptive quality (see Scene's wire-antialiasing accessors): while measured paint
273 // passes are too slow for interactivity, drop this wire's antialiasing -- never force
274 // it on, so a global choice like Fast Mode still wins. The painter state is
275 // saved/restored around each item's paint by QGraphicsView (the DontSavePainterState
276 // optimization flag is not set), so the hint can't leak into other items.
277 if (auto *scene_ = qobject_cast<Scene *>(scene()); scene_ && !scene_->wireAntialiasingEnabled()) {
278 painter->setRenderHint(QPainter::Antialiasing, false);
279 }
280
281 // Highlight is drawn as a wider blue halo beneath the normal wire, so users can
282 // easily see which wires belong to a selected element
283 if (m_highLight) {
284 painter->save();
285 painter->setPen(QPen(Qt::blue, 10));
286 painter->drawPath(path());
287 painter->restore();
288 }
289
290 // When the wire itself is selected (clicked), switch to the selection colour;
291 // otherwise use the status-driven pen set by setStatus() via applyStatusPen()
292 painter->setPen(isSelected() ? QPen(m_selectedColor, 5) : m_statusPen);
293 painter->drawPath(path());
294}
295
296QVariant Connection::itemChange(GraphicsItemChange change, const QVariant &value)
297{
298 // When the wire is selected/deselected, visually highlight both endpoint ports
299 // so the user can see which element pins are connected by this wire
300 if (change == ItemSelectedChange) {
301 if (value.toBool()) {
302 if (startPort()) startPort()->hoverEnter();
303 if (endPort()) endPort()->hoverEnter();
304 } else {
305 if (startPort()) startPort()->hoverLeave();
306 if (endPort()) endPort()->hoverLeave();
307 }
308 }
309
310 return QGraphicsPathItem::itemChange(change, value);
311}
312
314{
315 return m_highLight;
316}
317
319{
320 m_highLight = highLight;
321 update();
322}
323
325{
326 // Expand beyond the path's tight bounding box by 10 px on all sides to ensure
327 // the thick selection outline and highlight halo are fully covered during repaints
328 return path().boundingRect().adjusted(-10, -10, 10, 10);
329}
330
331QPainterPath Connection::shape() const
332{
333 if (m_shapeDirty) {
334 // Same stroke semantics as Qt's default (qt_graphicsItem_shapeFromPath): the real
335 // pen's width sets the click tolerance along the wire -- which is why the real
336 // setPen() still runs on Error<->normal width transitions (see applyStatusPen()).
337 // Stroking a flattened (polygonal) copy instead of the raw Bézier keeps later
338 // intersects()/contains() tests on line segments; the flattening error is
339 // sub-pixel, far inside the stroke tolerance.
340 QPainterPathStroker stroker;
341 stroker.setWidth(qMax(pen().widthF(), 0.00000001));
342 stroker.setCapStyle(pen().capStyle());
343 stroker.setJoinStyle(pen().joinStyle());
344 stroker.setMiterLimit(pen().miterLimit());
345
346 // toSubpathPolygons() keeps open subpaths open -- toFillPolygon() would close the
347 // polyline back to its start, adding a phantom straight segment to the hit area.
348 QPainterPath flattened;
349 for (const QPolygonF &polygon : path().toSubpathPolygons()) {
350 flattened.addPolygon(polygon);
351 }
352
353 m_cachedShape = stroker.createStroke(flattened);
354 m_shapeDirty = false;
355 }
356
357 return m_cachedShape;
358}
359
360bool Connection::sceneEvent(QEvent *event)
361{
362 // Swallow Ctrl+click so the scene can use Ctrl+click for multi-selection without
363 // the wire consuming the event and blocking the rubber-band/deselect behaviour
364 if (auto mouseEvent = dynamic_cast<QGraphicsSceneMouseEvent *>(event)) {
365 if (mouseEvent->modifiers().testFlag(Qt::ControlModifier)) {
366 return true;
367 }
368 }
369
370 return QGraphicsPathItem::sceneEvent(event);
371}
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:88
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:234
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:266
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:84
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...