wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
InputRotary.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QGraphicsSceneMouseEvent>
7#include <QPainter>
8#include <QSvgRenderer>
9
14#include "App/IO/VersionInfo.h"
15#include "App/Wiring/Port.h"
16
17template<>
20 .type = ElementType::InputRotary,
21 .group = ElementGroup::Input,
22 .minOutputSize = 2,
23 .maxOutputSize = 16,
24 .canChangeAppearance = true,
25 .hasTrigger = true,
26 .hasLabel = true,
27 };
28 static_assert(validate(constraints));
29
31 {
33 meta.pixmapPath = []{ return QStringLiteral(":/Components/Input/rotary_icon.svg"); };
34 meta.titleText = QT_TRANSLATE_NOOP("InputRotary", "ROTARY SWITCH");
35 meta.translatedName = QT_TRANSLATE_NOOP("InputRotary", "Rotary Switch");
36 meta.trContext = "InputRotary";
37 meta.defaultAppearances = QStringList({
38 ":/Components/Input/rotary.svg",
39 ":/Components/Input/rotary_arrow.svg",
40 });
41 return meta;
42 }
43
44 static inline const bool registered = []() {
46 ElementFactory::registerCreator(constraints.type, [] { return new InputRotary(); });
47 return true;
48 }();
49};
50
51InputRotary::InputRotary(QGraphicsItem *parent)
53{
54 m_rotary = m_appearance.defaultAppearances().at(0);
55
57
58 // The dial starts at position 0, which drives HIGH while every other
59 // position drives LOW: push those definite levels to the output ports,
60 // which otherwise stay at the undriven Unknown default.
61 InputRotary::setOn(true, 0);
62}
63
65{
66 // Guard against stale currentPort if output size was reduced (e.g. via undo)
67 if (m_currentPort >= InputRotary::outputSize()) {
68 m_currentPort = 0;
69 }
70
71 update();
72}
73
75{
76 // Ports are placed around the perimeter of the 64x64 bounding box,
77 // evenly distributed to match the visual rotary positions on the SVG.
78 // Only specific output counts (2, 3, 4, 6, 8, 10, 12, 16) have defined
79 // layouts; these correspond to standard rotary switch configurations.
80 switch (InputRotary::outputSize()) {
81 case 2: {
82 outputPort(0)->setPos(32, 0); outputPort(0)->setName("0");
83 outputPort(1)->setPos(32, 64); outputPort(1)->setName("1");
84 break;
85 }
86 case 3: {
87 outputPort(0)->setPos(32, 0); outputPort( 0)->setName("0");
88 outputPort(1)->setPos(64, 48); outputPort( 1)->setName("1");
89 outputPort(2)->setPos(0, 48); outputPort( 2)->setName("2");
90 break;
91 }
92 case 4: {
93 outputPort(0)->setPos(32, 0); outputPort(0)->setName("0");
94 outputPort(1)->setPos(64, 32); outputPort(1)->setName("1");
95 outputPort(2)->setPos(32, 64); outputPort(2)->setName("2");
96 outputPort(3)->setPos( 0, 32); outputPort(3)->setName("3");
97 break;
98 }
99 case 6: {
100 outputPort(0)->setPos(32, 0); outputPort( 0)->setName("0");
101 outputPort(1)->setPos(64, 16); outputPort( 1)->setName("1");
102 outputPort(2)->setPos(64, 48); outputPort( 2)->setName("2");
103 outputPort(3)->setPos(32, 64); outputPort( 3)->setName("3");
104 outputPort(4)->setPos( 0, 48); outputPort( 4)->setName("4");
105 outputPort(5)->setPos( 0, 16); outputPort( 5)->setName("5");
106 break;
107 }
108 case 8: {
109 outputPort(0)->setPos(32, 0); outputPort(0)->setName("0");
110 outputPort(1)->setPos(64, 0); outputPort(1)->setName("1");
111 outputPort(2)->setPos(64, 32); outputPort(2)->setName("2");
112 outputPort(3)->setPos(64, 64); outputPort(3)->setName("3");
113 outputPort(4)->setPos(32, 64); outputPort(4)->setName("4");
114 outputPort(5)->setPos( 0, 64); outputPort(5)->setName("5");
115 outputPort(6)->setPos( 0, 32); outputPort(6)->setName("6");
116 outputPort(7)->setPos( 0, 0); outputPort(7)->setName("7");
117 break;
118 }
119 case 10: {
120 outputPort(0)->setPos(32, 0); outputPort(0)->setName("0");
121 outputPort(1)->setPos(56, 0); outputPort(1)->setName("1");
122 outputPort(2)->setPos(64, 24); outputPort(2)->setName("2");
123 outputPort(3)->setPos(64, 40); outputPort(3)->setName("3");
124 outputPort(4)->setPos(56, 64); outputPort(4)->setName("4");
125 outputPort(5)->setPos(32, 64); outputPort(5)->setName("5");
126 outputPort(6)->setPos( 8, 64); outputPort(6)->setName("6");
127 outputPort(7)->setPos( 0, 40); outputPort(7)->setName("7");
128 outputPort(8)->setPos( 0, 24); outputPort(8)->setName("8");
129 outputPort(9)->setPos( 8, 0); outputPort(9)->setName("9");
130 break;
131 }
132 case 12: {
133 outputPort( 0)->setPos(32, 0); outputPort( 0)->setName("0");
134 outputPort( 1)->setPos(48, 0); outputPort( 1)->setName("1");
135 outputPort( 2)->setPos(64, 16); outputPort( 2)->setName("2");
136 outputPort( 3)->setPos(64, 32); outputPort( 3)->setName("3");
137 outputPort( 4)->setPos(64, 48); outputPort( 4)->setName("4");
138 outputPort( 5)->setPos(48, 64); outputPort( 5)->setName("5");
139 outputPort( 6)->setPos(32, 64); outputPort( 6)->setName("6");
140 outputPort( 7)->setPos(16, 64); outputPort( 7)->setName("7");
141 outputPort( 8)->setPos( 0, 48); outputPort( 8)->setName("8");
142 outputPort( 9)->setPos( 0, 32); outputPort( 9)->setName("9");
143 // Ports >= 10 use hex labels (A, B, ...) to stay single-character
144 outputPort(10)->setPos( 0, 16); outputPort(10)->setName("A");
145 outputPort(11)->setPos(16, 0); outputPort(11)->setName("B");
146 break;
147 }
148 case 16: {
149 outputPort( 0)->setPos(32, 0); outputPort( 0)->setName("0");
150 outputPort( 1)->setPos(48, 0); outputPort( 1)->setName("1");
151 outputPort( 2)->setPos(64, 0); outputPort( 2)->setName("2");
152 outputPort( 3)->setPos(64, 16); outputPort( 3)->setName("3");
153 outputPort( 4)->setPos(64, 32); outputPort( 4)->setName("4");
154 outputPort( 5)->setPos(64, 48); outputPort( 5)->setName("5");
155 outputPort( 6)->setPos(64, 64); outputPort( 6)->setName("6");
156 outputPort( 7)->setPos(48, 64); outputPort( 7)->setName("7");
157 outputPort( 8)->setPos(32, 64); outputPort( 8)->setName("8");
158 outputPort( 9)->setPos(16, 64); outputPort( 9)->setName("9");
159 outputPort(10)->setPos( 0, 64); outputPort(10)->setName("A");
160 outputPort(11)->setPos( 0, 48); outputPort(11)->setName("B");
161 outputPort(12)->setPos( 0, 32); outputPort(12)->setName("C");
162 outputPort(13)->setPos( 0, 16); outputPort(13)->setName("D");
163 outputPort(14)->setPos( 0, 0); outputPort(14)->setName("E");
164 outputPort(15)->setPos(16, 0); outputPort(15)->setName("F");
165 break;
166 }
167
168 default:
169 // Handle unexpected output sizes gracefully
170 // For unhandled sizes, use a simple default positioning
171 for (int i = 0; i < outputSize(); ++i) {
172 outputPort(i)->setPos(32, i * 16);
173 outputPort(i)->setName(QString::number(i + 1));
174 }
175 break;
176 }
177
179}
180
185static QSvgRenderer &rotaryArrowRenderer()
186{
187 static QSvgRenderer renderer(QStringLiteral(":/Components/Input/rotary_arrow.svg"));
188 return renderer;
189}
190
191void InputRotary::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
192{
193 GraphicElement::paint(painter, option, widget);
194
195 // Draw a small position mark for every output port, evenly spaced around the dial.
196 // The active port also gets the arrow overlay drawn on top.
197 for (int port = 0; port < outputSize(); ++port) {
198 painter->save();
199
200 // Distribute marks evenly: port 0 is at the top (12 o'clock), increasing clockwise
201 const double angle = 360. / outputSize() * port;
202 const QPointF center = pixmapCenter();
203 // Mark rect coordinates are in the pre-rotation (top) position — the painter
204 // transform rotates the entire coordinate system around the dial center.
205 // 30.2, 8.727 = top-centre of the dial face (horizontally centred on the 64px body,
206 // placed near the rim rather than the hub so it's visible on the SVG).
207 // 3.6 × 6.4 = small rectangular pip sized to match the tick marks on the SVG artwork.
208 const QRectF mark = QRectF{30.2, 8.727, 3.6, 6.4};
209
210 // Off-white / cream colour matches the dial face artwork, making the tick marks blend naturally
211 painter->setBrush(QBrush{QColor{255, 246, 213}});
212 painter->translate(center.x(), center.y());
213 painter->rotate(angle);
214 painter->translate(-center.x(), -center.y());
215 painter->drawRect(mark);
216
217 painter->restore();
218
219 if (m_currentPort != port) {
220 continue;
221 }
222
223 // Overlay the arrow at the same rotation as the active mark
224 painter->save();
225
226 painter->translate(center.x(), center.y());
227 painter->rotate(angle);
228 painter->translate(-center.x(), -center.y());
229 rotaryArrowRenderer().render(painter, QRectF(0, 0, 64, 64));
230
231 painter->restore();
232 }
233}
234
235bool InputRotary::isOn(const int port) const
236{
237 return (m_currentPort == port);
238}
239
241{
242}
243
245{
246 // Advance the dial one position forward, wrapping around to 0 after the last port
247 const int port = (outputValue() + 1) % outputSize();
248 InputRotary::setOn(true, port);
249}
250
251void InputRotary::setOn(const bool value, const int port)
252{
253 // value is ignored — the rotary is not a simple toggle; the selected port determines state
254 Q_UNUSED(value)
255 m_currentPort = port;
256
257 // Clamp both directions: load() feeds this raw values from the file, and
258 // a negative position would otherwise leave no port selected at all.
259 if (m_currentPort < 0 || m_currentPort >= outputSize()) {
260 m_currentPort = 0;
261 }
262
263 update();
264
265 // Exactly one output port is Active (the selected position); all others are Inactive
266 for (int index = 0; index < outputSize(); ++index) {
267 outputPort(index)->setStatus((m_currentPort == index) ? Status::Active : Status::Inactive);
268 }
269}
270
271void InputRotary::setWaveformValue(const bool value, const int port)
272{
273 // Only a high cell selects this port; a low cell is implicit (writing it would wrongly
274 // re-select that port, since setOn ignores the value and selects by port index).
275 if (value) {
276 setOn(true, port);
277 }
278}
279
280void InputRotary::mousePressEvent(QGraphicsSceneMouseEvent *event)
281{
282 if (!m_locked && (event->button() == Qt::LeftButton)) {
283 setOn(true, (m_currentPort + 1) % outputSize());
284 event->accept();
285 }
286
287 QGraphicsItem::mousePressEvent(event);
288}
289
290void InputRotary::save(QDataStream &stream, SerializationOptions options) const
291{
292 GraphicElement::save(stream, options);
293
294 const bool slim = (options.purpose == SerializationPurpose::PortableFile);
295
296 QMap<QString, QVariant> map;
297 // PortableFile streams omit defaults; fresh-loaded elements start there
298 // anyway. Snapshots write unconditionally (reloaded into live elements).
299 if (!slim || m_currentPort != 0) {
300 map.insert("currentPort", m_currentPort);
301 }
302 if (!slim || m_locked) {
303 map.insert("locked", m_locked);
304 }
305
306 stream << map;
307}
308
309void InputRotary::load(QDataStream &stream, SerializationContext &context)
310{
311 GraphicElement::load(stream, context);
312
313 if (!VersionInfo::hasQMapFormat(context.version)) {
314 // v1.x–4.0 stored currentPort as a bare int; locked added in v3.1
315 stream >> m_currentPort;
316
317 if (VersionInfo::hasLockState(context.version)) {
318 stream >> m_locked;
319 }
320 }
321
322 if (VersionInfo::hasQMapFormat(context.version)) {
323 // v4.1+ uses a key-value map
324 QMap<QString, QVariant> map = Serialization::readBoundedMetadata(stream);
325
326 if (map.contains("currentPort")) {
327 m_currentPort = map.value("currentPort").toInt();
328 }
329
330 if (map.contains("locked")) {
331 m_locked = map.value("locked").toBool();
332 }
333 }
334
335 // Apply loaded state to ports so the simulation reflects the saved position
336 setOn(true, m_currentPort);
337}
338
339void InputRotary::setAppearance(const bool defaultAppearance, const QString &fileName)
340{
341 if (defaultAppearance) {
342 m_appearance.resetAlternativeToDefault();
343 } else {
344 m_appearance.setAlternativeAppearanceAt(0, fileName);
345 }
346
347 m_appearance.setUsingDefaultAppearance(defaultAppearance);
348 setPixmap(0);
349}
350
352{
353 return static_cast<int>(outputs().size());
354}
355
357{
358 return m_currentPort;
359}
Singleton factory for all circuit element types.
Self-registering element trait template and compile-time constraint validation.
ElementMetadata metadataFromConstraints(const ElementConstraints &c)
Converts ElementConstraints to an ElementMetadata with all constraint-derived fields set.
Definition ElementInfo.h:80
constexpr bool validate(const ElementConstraints &c)
Validates element constraints at compile time.
Definition ElementInfo.h:48
Enums::ElementType ElementType
Definition Enums.h:107
static QSvgRenderer & rotaryArrowRenderer()
Graphic element for the rotary switch input.
Port classes: Port (base), InputPort, and OutputPort.
Deserialization/serialization context structs passed through load()/save() call chains.
Circuit and waveform file serialization/deserialization utilities.
Named version predicates for file-format compatibility checks.
static void registerCreator(ElementType type, std::function< GraphicElement *()> creator)
Registers a creator lambda for type, used by buildElement().
static void registerMetadata(const ElementMetadata &meta)
Registers meta in the global map (called once per element type at startup).
GraphicElementInput(ElementType type, QGraphicsItem *parent=nullptr)
QPointF pixmapCenter() const
Returns the centre point of the element's pixmap in local coordinates.
ElementAppearance m_appearance
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
Paints the element onto the scene.
virtual void load(QDataStream &stream, SerializationContext &context)
Loads the graphic element through a binary data stream.
OutputPort * outputPort(const int index=0) const
Returns the output port at index (default 0).
virtual void save(QDataStream &stream, SerializationOptions options) const
void setPixmap(const QString &pixmapPath)
Loads and applies the pixmap located at pixmapPath.
const QVector< OutputPort * > & outputs() const
Returns a const reference to the vector of all output ports.
Rotary switch input that activates one output port at a time.
Definition InputRotary.h:20
int outputValue() const override
Returns the index of the currently active output port.
InputRotary(QGraphicsItem *parent=nullptr)
Constructs the rotary switch with optional parent.
void setOn() override
Activates port 0 (no-op if already at 0).
void updatePortsProperties() override
Recalculates port positions based on the current output count.
bool isOn(const int port=0) const override
Returns true if output port port is the currently active selection.
void save(QDataStream &stream, SerializationOptions options) const override
Serializes the current port selection to stream.
void setWaveformValue(const bool value, const int port=0) override
void mousePressEvent(QGraphicsSceneMouseEvent *event) override
int outputSize() const override
Returns the number of selectable output ports.
void setAppearance(const bool defaultAppearance, const QString &fileName) override
Applies custom appearance images for the rotary body and arrow.
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
Paints the rotary wheel with the indicator arrow pointing to the active port.
void setOff() override
Resets the selection to port 0 (logic-0 on all ports).
void refresh() override
Refreshes the arrow direction and port positions.
void load(QDataStream &stream, SerializationContext &context) override
Deserializes the current port selection.
void setStatus(const Status status) override
Definition Port.cpp:361
void setName(const QString &name)
Sets the label text shown next to the port.
Definition Port.cpp:166
static QMap< QString, QVariant > readBoundedMetadata(QDataStream &stream)
Reads the file-level metadata QMap<QString,QVariant> from stream without calling QList::reserve() wit...
bool hasLockState(const QVersionNumber &v)
V3.1: Lock state for input elements; color for some display/button types.
Definition VersionInfo.h:51
bool hasQMapFormat(const QVersionNumber &v)
V4.1: Format changed from flat binary to keyed QMap; port serial IDs; rotation fix.
Definition VersionInfo.h:60
Compile-time-validatable subset of ElementMetadata.
Definition ElementInfo.h:21
static const bool registered
static ElementMetadata metadata()
static constexpr ElementConstraints constraints
Self-registering element information trait.
Compile-time-registered properties for one element type.
Bundles all per-deserialization state so that load() overrides receive it through one explicit parame...
QVersionNumber version
File-format version read from the stream header.
Options passed to GraphicElement::save() (and friends); the save-side counterpart of SerializationCon...
SerializationPurpose purpose
What this serialization is for; see SerializationPurpose. Mandatory, no default.