wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
Commands.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 <algorithm>
7#include <cmath>
8
9#include <QCoreApplication>
10#include <QIODevice>
11
12#include "App/Core/Common.h"
13#include "App/Core/Constants.h"
20#include "App/Scene/Scene.h"
24#include "App/Wiring/Port.h"
25
26namespace CommandUtils {
27
28void storeIds(const QList<QGraphicsItem *> &items, QList<int> &ids)
29{
30 ids.reserve(items.size());
31
32 for (auto *item : items) {
33 if (auto *itemId = dynamic_cast<ItemWithId *>(item)) {
34 ids.append(itemId->id());
35 }
36 }
37}
38
39void storeOtherIds(const QList<QGraphicsItem *> &connections, const QList<int> &ids, QList<int> &otherIds)
40{
41 // Track elements on the opposite end of connections that are NOT being deleted.
42 // These elements must be re-saved/re-loaded during undo so their port state
43 // (connected/disconnected) is correctly restored after the operation.
44 for (auto *item : connections) {
45 if (auto *conn = qgraphicsitem_cast<Connection *>(item)) {
46 if (auto *port1 = conn->startPort(); port1 && port1->graphicElement() && !ids.contains(port1->graphicElement()->id())) {
47 otherIds.append(port1->graphicElement()->id());
48 }
49
50 if (auto *port2 = conn->endPort(); port2 && port2->graphicElement() && !ids.contains(port2->graphicElement()->id())) {
51 otherIds.append(port2->graphicElement()->id());
52 }
53 }
54 }
55}
56
57const QList<QGraphicsItem *> loadList(const QList<QGraphicsItem *> &items, QList<int> &ids, QList<int> &otherIds)
58{
59 // --- Collect elements in the operation ---
60 QList<QGraphicsItem *> elements;
61 /* Stores selected graphicElements */
62 for (auto *item : items) {
63 if (item->type() == GraphicElement::Type) {
64 if (!elements.contains(item)) {
65 elements.append(item);
66 }
67 }
68 }
69
70 // --- Collect all wires attached to those elements ---
71 // Always include the wires connected to the affected elements, even if the
72 // user only selected the element body; this keeps the topology consistent
73 QList<QGraphicsItem *> connections;
74 /* Stores all the wires linked to these elements */
75 for (auto *item : elements) {
76 if (auto *elm = qgraphicsitem_cast<GraphicElement *>(item)) {
77 for (auto *port : elm->inputs()) {
78 for (auto *conn : port->connections()) {
79 if (!connections.contains(conn)) {
80 connections.append(conn);
81 }
82 }
83 }
84
85 for (auto *port : elm->outputs()) {
86 for (auto *conn : port->connections()) {
87 if (!connections.contains(conn)) {
88 connections.append(conn);
89 }
90 }
91 }
92 }
93 }
94
95 /* Stores the other wires selected */
96 for (auto *item : items) {
97 if (item->type() == Connection::Type) {
98 if (!connections.contains(item)) {
99 connections.append(item);
100 }
101 }
102 }
103
104 // Store the ids of all elements listed in items.
105 storeIds(elements + connections, ids);
106 // Store all elements linked to each connection that will not be deleted.
107 storeOtherIds(connections, ids, otherIds);
108 return elements + connections;
109}
110
111const QList<QGraphicsItem *> findItems(Scene *scene, const QList<int> &ids)
112{
113 QList<QGraphicsItem *> items;
114 items.reserve(ids.size());
115
116 for (const int id : ids) {
117 if (auto *item = dynamic_cast<QGraphicsItem *>(scene->itemById(id))) {
118 items.append(item);
119 }
120 }
121
122 if (items.size() != ids.size()) {
123 throw PANDACEPTION_WITH_CONTEXT("commands", "One or more items was not found on the scene.");
124 }
125
126 return items;
127}
128
129const QList<GraphicElement *> findElements(Scene *scene, const QList<int> &ids)
130{
131 QList<GraphicElement *> items;
132 items.reserve(ids.size());
133
134 for (const int id : ids) {
135 if (auto *item = dynamic_cast<GraphicElement *>(scene->itemById(id))) {
136 items.append(item);
137 }
138 }
139
140 if (items.size() != ids.size()) {
141 throw PANDACEPTION_WITH_CONTEXT("commands", "One or more elements was not found on the scene.");
142 }
143
144 return items;
145}
146
147Connection *findConn(Scene *scene, const int id)
148{
149 return dynamic_cast<Connection *>(scene->itemById(id));
150}
151
152GraphicElement *findElm(Scene *scene, const int id)
153{
154 return dynamic_cast<GraphicElement *>(scene->itemById(id));
155}
156
157void saveItems(Scene *scene, QByteArray &itemData, const QList<QGraphicsItem *> &items, const QList<int> &otherIds)
158{
159 itemData.clear();
160 QDataStream stream(&itemData, QIODevice::WriteOnly);
162
163 const auto others = CommandUtils::findElements(scene, otherIds);
164
165 for (auto *elm : others) {
166 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
167 }
168
170}
171
172void addItems(Scene *scene, const QList<QGraphicsItem *> &items)
173{
174 for (auto *item : items) {
175 if (item->scene() != scene) {
176 scene->addItem(item);
177 }
178
179 if (item->type() == GraphicElement::Type) {
180 item->setSelected(true);
181 }
182 }
183}
184
185const QList<QGraphicsItem *> loadItems(Scene *scene, QByteArray &itemData, const QList<int> &ids, QList<int> &otherIds)
186{
187 if (itemData.isEmpty()) {
188 return {};
189 }
190
191 QDataStream stream(&itemData, QIODevice::ReadOnly);
192 QVersionNumber version = Serialization::readPandaHeader(stream);
193
194 QHash<quint64, Port *> portMap;
195 auto context = scene->deserializationContext(portMap, version, SerializationPurpose::InMemorySnapshot);
196
197 for (auto *elm : CommandUtils::findElements(scene, otherIds)) {
198 elm->load(stream, context);
199 }
200
201 /* Assuming that all connections are stored after the elements, we will deserialize the elements first.
202 * We will store one additional information: The element IDs! */
203 const auto items = Serialization::deserialize(stream, context);
204
205 if (items.size() != ids.size()) {
206 // None of these items were added to the scene yet, so they'd otherwise leak.
207 qDeleteAll(items);
208 throw PANDACEPTION_WITH_CONTEXT("commands", "One or more elements were not found on scene. Expected %1, found %2.", static_cast<int>(ids.size()), static_cast<int>(items.size()));
209 }
210
211 // Re-assign the original IDs so undo/redo chains that store IDs remain valid
212 for (int i = 0; i < items.size(); ++i) {
213 if (auto *itemId = dynamic_cast<ItemWithId *>(items.at(i))) {
214 scene->updateItemId(itemId, ids.at(i));
215 }
216 }
217
218 addItems(scene, items);
219 return items;
220}
221
222void deleteItems(Scene *scene, const QList<QGraphicsItem *> &items)
223{
224 /* Delete items on reverse order */
225 for (auto it = items.rbegin(); it != items.rend(); ++it) {
226 scene->removeItem(*it);
227 delete *it;
228 }
229}
230
231void drainPortConnections(GraphicElement *elm, int fromPort, int toPort,
232 bool isInput, QDataStream &stream, Scene *scene)
233{
234 int connCount = 0;
235 for (int port = fromPort; port < toPort; ++port) {
236 Port *p = isInput ? static_cast<Port *>(elm->inputPort(port)) : static_cast<Port *>(elm->outputPort(port));
237 connCount += static_cast<int>(p->connections().size());
238 }
239 stream << connCount;
240
241 for (int port = fromPort; port < toPort; ++port) {
242 Port *p = isInput ? static_cast<Port *>(elm->inputPort(port)) : static_cast<Port *>(elm->outputPort(port));
243 while (!p->connections().isEmpty()) {
244 auto *conn = p->connections().constFirst();
245 stream << conn->id();
246 conn->save(stream);
247 conn->setStartPort(nullptr);
248 conn->setEndPort(nullptr);
249 scene->removeItem(conn);
250 delete conn;
251 }
252 }
253}
254
255} // namespace CommandUtils
256
257ElementsCommand::ElementsCommand(const QList<GraphicElement *> &elements, Scene *scene, QUndoCommand *parent)
258 : QUndoCommand(parent)
259 , m_scene(scene)
260{
261 m_ids.reserve(elements.size());
262 for (auto *elm : elements) {
263 m_ids.append(elm->id());
264 }
265}
266
267QList<GraphicElement *> ElementsCommand::elements() const
268{
270}
271
272AddItemsCommand::AddItemsCommand(const QList<QGraphicsItem *> &items, Scene *scene, QUndoCommand *parent)
273 : QUndoCommand(parent)
274 , m_scene(scene)
275{
276 // Simulation must be paused while items are added to avoid a partial-topology update
277 SimulationBlocker blocker(m_scene->simulation());
278 // Add items to scene first so they receive positive scene-local IDs before
279 // loadList captures them in storeIds. Items already in scene are skipped.
280 CommandUtils::addItems(m_scene, items);
281 // Collect canonical list (elements + attached wires) and store their IDs.
282 // The second addItems call handles any wires discovered via port traversal.
283 const auto items_ = CommandUtils::loadList(items, m_ids, m_otherIds);
284 CommandUtils::addItems(m_scene, items_);
285 setText(tr("Add %1 elements").arg(items_.size()));
286}
287
289{
290 qCDebug(zero) << text();
291 SimulationBlocker blocker(m_scene->simulation());
292 const auto items = CommandUtils::findItems(m_scene, m_ids);
293 CommandUtils::saveItems(m_scene, m_itemData, items, m_otherIds);
294 CommandUtils::deleteItems(m_scene, items);
295 m_scene->setCircuitUpdateRequired();
296}
297
299{
300 qCDebug(zero) << text();
301 SimulationBlocker blocker(m_scene->simulation());
302 CommandUtils::loadItems(m_scene, m_itemData, m_ids, m_otherIds);
303 m_scene->setCircuitUpdateRequired();
304}
305
306DeleteItemsCommand::DeleteItemsCommand(const QList<QGraphicsItem *> &items, Scene *scene, QUndoCommand *parent)
307 : QUndoCommand(parent)
308 , m_scene(scene)
309{
310 const auto items_ = CommandUtils::loadList(items, m_ids, m_otherIds);
311 setText(tr("Delete %1 elements").arg(items_.size()));
312}
313
315{
316 qCDebug(zero) << text();
317 SimulationBlocker blocker(m_scene->simulation());
318 CommandUtils::loadItems(m_scene, m_itemData, m_ids, m_otherIds);
319 m_scene->setCircuitUpdateRequired();
320}
321
323{
324 qCDebug(zero) << text();
325 SimulationBlocker blocker(m_scene->simulation());
326 const auto items = CommandUtils::findItems(m_scene, m_ids);
327 CommandUtils::saveItems(m_scene, m_itemData, items, m_otherIds);
328 CommandUtils::deleteItems(m_scene, items);
329 m_scene->setCircuitUpdateRequired();
330}
331
332RotateCommand::RotateCommand(const QList<GraphicElement *> &items, const int angle, Scene *scene, QUndoCommand *parent)
333 : ElementsCommand(items, scene, parent)
334 , m_angle(angle)
335{
336 setText(tr("Rotate %1 degrees").arg(m_angle));
337 m_positions.reserve(items.size());
338
339 for (auto *item : items) {
340 m_positions.append(item->pos());
341 item->setPos(item->pos());
342
343 }
344}
345
347{
348 const auto elements = this->elements();
349
350 for (int i = 0; i < elements.size(); ++i) {
351 auto *elm = elements.at(i);
352 elm->setRotation(elm->rotation() - m_angle);
353 elm->setPos(m_positions.at(i));
354 elm->update();
355 elm->setSelected(true);
356 }
357
358 m_scene->setAutosaveRequired();
359}
360
362{
363 const auto elements = this->elements();
364 double cx = 0;
365 double cy = 0;
366 int sz = 0;
367
368 for (auto *elm : elements) {
369 cx += elm->pos().x();
370 cy += elm->pos().y();
371 ++sz;
372 }
373
374 if (sz != 0) {
375 cx /= sz;
376 cy /= sz;
377 }
378
379 // --- Apply group rotation via 2D transform ---
380 // Translate-rotate-translate-back maps each element position around the centroid
381 QTransform transform;
382 transform.translate(cx, cy);
383 transform.rotate(m_angle);
384 transform.translate(-cx, -cy);
385
386 for (auto *elm : elements) {
387 elm->setPos(transform.map(elm->pos()));
388 elm->setRotation(elm->rotation() + m_angle);
389 }
390
391 m_scene->setAutosaveRequired();
392}
393
394MoveCommand::MoveCommand(const QList<GraphicElement *> &list, const QList<QPointF> &oldPositions, Scene *scene, QUndoCommand *parent)
395 : ElementsCommand(list, scene, parent)
396 , m_oldPositions(oldPositions)
397{
398 m_newPositions.reserve(list.size());
399
400 for (auto *elm : list) {
401 m_newPositions.append(elm->pos());
402 }
403
404 setText(tr("Move elements"));
405}
406
408{
409 const auto elements = this->elements();
410
411 for (int i = 0; i < elements.size(); ++i) {
412 elements.at(i)->setPos(m_oldPositions.at(i));
413 }
414
415 m_scene->setAutosaveRequired();
416}
417
419{
420 const auto elements = this->elements();
421
422 for (int i = 0; i < elements.size(); ++i) {
423 elements.at(i)->setPos(m_newPositions.at(i));
424 }
425
426 m_scene->setAutosaveRequired();
427}
428
429UpdateCommand::UpdateCommand(const QList<GraphicElement *> &elements, const QByteArray &oldData, Scene *scene, QUndoCommand *parent)
430 : ElementsCommand(elements, scene, parent)
431 // oldData must be captured by the caller *before* applying the change, so that
432 // undo() can restore the previous state; the constructor cannot capture it here
433 // because the change has already been applied by the time construction runs.
434 , m_oldData(oldData)
435{
436 QDataStream stream(&m_newData, QIODevice::WriteOnly);
438
439 // Snapshot the current (post-change) state as the "new" data used by redo()
440 for (auto *elm : elements) {
441 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
442 }
443
444 // Precompute once whether old→new changes wireless routing (a mode, or the label of a
445 // Tx/Rx node — the label IS the channel). The live elements currently hold the NEW
446 // state, so briefly round-trip through oldData to observe the OLD one. This cannot be
447 // detected inside redo()/undo() around loadData(): the first redo()'s load is a no-op
448 // (the caller mutated the elements before pushing), so old and new would compare equal
449 // exactly when the escalation matters most. Only selections containing a
450 // wireless-capable element pay for the round-trip.
451 const auto newState = snapshotWirelessState();
452 if (!newState.isEmpty()) {
453 SimulationBlocker blocker(m_scene->simulation());
454 loadData(m_oldData);
455 m_wirelessTopologyChange = wirelessStateDiffers(snapshotWirelessState(), newState);
456 loadData(m_newData);
457 }
458
459 setText(tr("Update %1 elements").arg(elements.size()));
460}
461
463{
464 qCDebug(zero) << text();
465 // elm->load() can free and replace an IC's internal graph; a simulation
466 // tick firing between that free and setPropertyUpdateRequired() (via
467 // Application::notify + QMessageBox nested event loop) would fault.
468 SimulationBlocker blocker(m_scene->simulation());
469 loadData(m_oldData);
470 refreshRuntimeState();
471}
472
474{
475 qCDebug(zero) << text();
476 SimulationBlocker blocker(m_scene->simulation());
477 loadData(m_newData);
478 refreshRuntimeState();
479}
480
481QVector<UpdateCommand::WirelessState> UpdateCommand::snapshotWirelessState() const
482{
483 QVector<WirelessState> state;
484 const auto elements = this->elements();
485 for (auto *elm : elements) {
486 if (elm && elm->hasWirelessMode()) {
487 state.append({elm, elm->wirelessMode(), elm->label()});
488 }
489 }
490 return state;
491}
492
493bool UpdateCommand::wirelessStateDiffers(const QVector<WirelessState> &before,
494 const QVector<WirelessState> &after)
495{
496 // The wireless channel graph is built only inside Simulation::initialize()
497 // (buildTxMap()/connectWirelessElements() read labels and modes there and nowhere
498 // else), so a change here re-routes nothing until a full rebuild. A label change on
499 // a node whose mode is None on both sides is decorative and keeps the fast path.
500 if (before.size() != after.size()) {
501 return true; // LCOV_EXCL_LINE — hasWirelessMode() is purely type-based (Node always true, base class always false) and never reads instance state elm->load() could change; the same fixed elements() list can't change size between the before/after captures, so this can't currently be reached. Defensive: the set of wireless-capable elements itself changed.
502 }
503 for (int i = 0; i < before.size(); ++i) {
504 const auto &b = before.at(i);
505 const auto &a = after.at(i);
506 if (b.element != a.element || b.mode != a.mode) {
507 return true;
508 }
509 if (b.label != a.label && a.mode != WirelessMode::None) {
510 return true;
511 }
512 }
513 return false;
514}
515
516void UpdateCommand::refreshRuntimeState()
517{
518 // A wireless mode/label edit IS a topology change (the label is the Tx→Rx channel),
519 // and the running simulation resolves channels only during initialize() — without a
520 // full rebuild the old routing silently stays live.
521 if (m_wirelessTopologyChange) {
522 m_scene->setCircuitUpdateRequired();
523 return;
524 }
525
526 // Everything else this command carries is property-only (label, color, ...) — never
527 // topology — so it must not force a full Simulation::initialize(), which would rebuild
528 // every Clock's phase from scratch and disrupt a running simulation over an unrelated
529 // edit. setPropertyUpdateRequired() refreshes visuals/dirty-state only.
530 m_scene->setPropertyUpdateRequired();
531}
532
533void UpdateCommand::loadData(QByteArray &itemData)
534{
535 const auto elements = this->elements();
536
537 if (elements.isEmpty()) {
538 return;
539 }
540
541 QDataStream stream(&itemData, QIODevice::ReadOnly);
542 QVersionNumber version = Serialization::readPandaHeader(stream);
543
544 QHash<quint64, Port *> portMap;
545 auto context = m_scene->deserializationContext(portMap, version, SerializationPurpose::InMemorySnapshot);
546
547 for (auto *elm : elements) {
548 elm->load(stream, context);
549 elm->setSelected(true);
550 }
551}
552
553SplitCommand::SplitCommand(Connection *conn, QPointF mousePos, Scene *scene, QUndoCommand *parent)
554 : QUndoCommand(parent)
555 , m_scene(scene)
556{
557 auto *node = ElementFactory::buildElement(ElementType::Node);
558
559 /* Align node to Grid */
560 // Subtract pixmapCenter so the node's visual center lands on the mouse click,
561 // then snap to the nearest grid intersection
562 m_nodePos = mousePos - node->pixmapCenter();
563 const int gridSize = Constants::gridSize;
564 qreal xV = qRound(m_nodePos.x() / gridSize) * gridSize;
565 qreal yV = qRound(m_nodePos.y() / gridSize) * gridSize;
566 m_nodePos = QPointF(xV, yV);
567
568 /* Rotate line according to angle between p1 and p2 */
569 // QLineF::angle() is counter-clockwise from the positive X axis (0–360°).
570 // Dividing by 90 and rounding quantises to the nearest cardinal direction (0–3),
571 // then (360 − 90*step) converts that back to a clockwise Qt rotation angle so
572 // the node's arrow graphic points in the correct direction along the wire.
573 const int angle = static_cast<int>(conn->angle());
574 m_nodeAngle = static_cast<int>(360 - 90 * (std::round(angle / 90.0)));
575
576 /* Assigning class attributes */
577 auto *startPort = conn->startPort();
578 auto *endPort = conn->endPort();
579 if (!startPort || !endPort) {
580 throw PANDACEPTION("Invalid connection ports in SplitCommand constructor"); // LCOV_EXCL_LINE — both current callers (SceneInteraction::mouseDoubleClick(), MCP ConnectionHandler::handleSplitConnection()) already require a matched connection with real start/end ports before constructing this command.
581 }
582
583 auto *startElement = startPort->graphicElement();
584 auto *endElement = endPort->graphicElement();
585 if (!startElement || !endElement) {
586 throw PANDACEPTION("Invalid graphic elements in SplitCommand constructor"); // LCOV_EXCL_LINE — every Port has a non-null owner set unconditionally by ElementPorts::addPort() at construction (the "port always has an owner" invariant established throughout this sweep).
587 }
588
589 m_elm1Id = startElement->id();
590 m_elm2Id = endElement->id();
591
592 m_c1Id = conn->id();
593
594 // Reserve a stable ID for the second wire segment (conn2) by briefly registering
595 // it in the scene so it receives a real scene-local ID, then removing it.
596 // redo() will recreate conn2 using updateItemId() to restore this same ID.
597 auto *conn2 = new Connection();
598 m_scene->addItem(conn2);
599 m_c2Id = conn2->id();
600 m_scene->removeItem(conn2);
601 delete conn2;
602
603 // Reserve a stable ID for the node the same way.
604 m_scene->addItem(node);
605 m_nodeId = node->id();
606 m_scene->removeItem(node);
607 delete node;
608
609 setText(tr("Wire split"));
610}
611
613{
614 qCDebug(zero) << text();
615 // Block the 1 ms timer while connections and the split node are
616 // deleted/rewired: a tick observing the scene mid-mutation could read a
617 // momentarily detached port or a not-yet-fully-wired element.
618 SimulationBlocker blocker(m_scene->simulation());
619 auto *conn1 = CommandUtils::findConn(m_scene, m_c1Id);
620 auto *elm1 = CommandUtils::findElm(m_scene, m_elm1Id);
621 auto *elm2 = CommandUtils::findElm(m_scene, m_elm2Id);
622
623 // Throw before any heap allocation so the throw-path can't leak.
624 if (!conn1 || !elm1 || !elm2) {
625 throw PANDACEPTION("Error trying to redo %1", text());
626 }
627
628 auto *endPort = conn1->endPort();
629 if (!endPort) {
630 throw PANDACEPTION("Error: endPort is null in SplitCommand::redo()");
631 }
632
633 auto *conn2 = CommandUtils::findConn(m_scene, m_c2Id);
634 auto *node = CommandUtils::findElm(m_scene, m_nodeId);
635
636 // After undo(), conn2 and node were deleted; recreate them with the same
637 // stable IDs so subsequent redo() calls find them correctly via findConn/findElm
638 if (!conn2) {
639 conn2 = new Connection();
640 m_scene->updateItemId(conn2, m_c2Id);
641 }
642
643 if (!node) {
644 node = ElementFactory::buildElement(ElementType::Node);
645 m_scene->updateItemId(node, m_nodeId);
646 }
647
648 node->setPos(m_nodePos);
649 node->setRotation(m_nodeAngle);
650
651 // Wire topology after split: elm1 → conn1 → node → conn2 → elm2
652 conn2->setStartPort(node->outputPort());
653 conn2->setEndPort(endPort);
654 conn1->setEndPort(node->inputPort());
655
656 m_scene->addItem(node);
657 m_scene->addItem(conn2);
658
659 conn1->updatePosFromPorts();
660 conn2->updatePosFromPorts();
661
662 m_scene->setCircuitUpdateRequired();
663}
664
666{
667 qCDebug(zero) << text();
668 // delete conn2 / delete node below must not race with the simulation tick.
669 SimulationBlocker blocker(m_scene->simulation());
670 auto *conn1 = CommandUtils::findConn(m_scene, m_c1Id);
671 auto *conn2 = CommandUtils::findConn(m_scene, m_c2Id);
672 auto *node = CommandUtils::findElm(m_scene, m_nodeId);
673 auto *elm1 = CommandUtils::findElm(m_scene, m_elm1Id);
674 auto *elm2 = CommandUtils::findElm(m_scene, m_elm2Id);
675
676 if (!conn1 || !conn2 || !elm1 || !elm2 || !node) {
677 throw PANDACEPTION("Error trying to undo %1", text());
678 }
679
680 // Restore the original direct wire: conn1 skips the node and connects straight to elm2
681 conn1->setEndPort(conn2->endPort());
682
683 conn1->updatePosFromPorts();
684
685 // Remove the node and the second wire segment introduced by the split
686 m_scene->removeItem(conn2);
687 m_scene->removeItem(node);
688
689 delete conn2;
690 delete node;
691
692 m_scene->setCircuitUpdateRequired();
693}
694
695MorphCommand::MorphCommand(const QList<GraphicElement *> &elements, ElementType type, Scene *scene, QUndoCommand *parent)
696 : ElementsCommand(elements, scene, parent)
697 , m_newType(type)
698{
699 m_types.reserve(elements.size());
700
701 for (auto *oldElm : elements) {
702 // Store both the ID and the original type so undo() can rebuild the old element
703 // with the correct type and then re-assign the same ID via updateItemId()
704 m_types.append(oldElm->elementType());
705 }
706
707 setText(tr("Morph %1 elements to %2").arg(elements.size()).arg(elements.constFirst()->objectName()));
708}
709
710void MorphCommand::restoreDeletedConnections(const QList<DeletedConnectionInfo> &deleted)
711{
712 // By this point transferConnections has already placed the restored elements in the
713 // scene under their original IDs, so itemById() resolves correctly.
714 for (const auto &info : deleted) {
715 auto *morphedElm = dynamic_cast<GraphicElement *>(m_scene->itemById(info.morphedElementId));
716 auto *otherElm = dynamic_cast<GraphicElement *>(m_scene->itemById(info.otherElementId));
717 if (!morphedElm || !otherElm) {
718 continue;
719 }
720
721 auto *conn = new Connection();
722 if (info.isInput) {
723 conn->setStartPort(otherElm->outputPort(info.otherPortIndex));
724 conn->setEndPort(morphedElm->inputPort(info.portIndex));
725 } else {
726 conn->setStartPort(morphedElm->outputPort(info.portIndex));
727 conn->setEndPort(otherElm->inputPort(info.otherPortIndex));
728 }
729 // Restore the original ID so any undo command that stored this connection's
730 // ID (e.g. DeleteItemsCommand) can still find it via scene->itemById().
731 m_scene->updateItemId(conn, info.connectionId);
732 m_scene->addItem(conn);
733 }
734}
735
737{
738 // transferConnections() deletes the current elements; a sim tick on
739 // the torn state between delete and setCircuitUpdateRequired() faults.
740 SimulationBlocker blocker(m_scene->simulation());
741
742 auto newElms = elements();
743 decltype(newElms) oldElms;
744 oldElms.reserve(m_ids.size());
745
746 for (int i = 0; i < m_ids.size(); ++i) {
747 oldElms << ElementFactory::buildElement(m_types.at(i));
748 }
749
750 m_deletedConnectionsOnUndo.clear();
751 transferConnections(newElms, oldElms, &m_deletedConnectionsOnUndo);
752
753 // Restore connections that were deleted when the last redo() morphed to a smaller element.
754 restoreDeletedConnections(m_deletedConnections);
755
756 m_scene->setCircuitUpdateRequired();
757}
758
760{
761 SimulationBlocker blocker(m_scene->simulation());
762
763 auto oldElms = elements();
764 decltype(oldElms) newElms;
765 newElms.reserve(m_ids.size());
766
767 for (int i = 0; i < m_ids.size(); ++i) {
768 newElms << ElementFactory::buildElement(m_newType);
769 }
770
771 m_deletedConnections.clear();
772 transferConnections(oldElms, newElms, &m_deletedConnections);
773
774 // Restore connections that were deleted when the last undo() reverted to a smaller
775 // original type — symmetric with undo()'s restoration above, so a connection added
776 // while morphed and later dropped by undo() isn't lost permanently.
777 restoreDeletedConnections(m_deletedConnectionsOnUndo);
778
779 m_scene->setCircuitUpdateRequired();
780}
781
782void MorphCommand::transferConnections(const QList<GraphicElement *> &from, const QList<GraphicElement *> &to,
783 QList<DeletedConnectionInfo> *deleted)
784{
785 for (int elm = 0; elm < from.size(); ++elm) {
786 auto *oldElm = from.at(elm);
787 auto *newElm = to.at(elm);
788
789 newElm->setInputSize(oldElm->inputSize());
790 newElm->setPos(oldElm->pos());
791
792 // Not↔Node morphs need a 16px position adjustment because the two element
793 // types have different pixmap sizes and their visual centers differ by that amount
794 if ((oldElm->elementType() == ElementType::Not) && (newElm->elementType() == ElementType::Node)) {
795 newElm->moveBy(16, 16);
796 }
797
798 if ((oldElm->elementType() == ElementType::Node) && (newElm->elementType() == ElementType::Not)) {
799 newElm->moveBy(-16, -16);
800 }
801
802 // Copy over all compatible properties; each is guarded by capability checks
803 // so that mismatched element types are silently skipped
804 if (newElm->rotatesGraphic() && oldElm->rotatesGraphic()) {
805 newElm->setRotation(oldElm->rotation());
806 }
807
808 if (newElm->hasLabel() && oldElm->hasLabel()) {
809 newElm->setLabel(oldElm->label());
810 }
811
812 if (newElm->hasColors() && oldElm->hasColors()) {
813 newElm->setColor(oldElm->color());
814 }
815
816 if (newElm->hasFrequency() && oldElm->hasFrequency()) {
817 newElm->setFrequency(oldElm->frequency());
818 }
819
820 if (newElm->hasTrigger() && oldElm->hasTrigger()) {
821 newElm->setTrigger(oldElm->trigger());
822 }
823
824 // Mirror state is a base property of every element (not type-specific), so it is
825 // always carried over — otherwise morphing a flipped element silently un-flips it.
826 newElm->setFlippedX(oldElm->isFlippedX());
827 newElm->setFlippedY(oldElm->isFlippedY());
828
829 if (newElm->hasAudio() && oldElm->hasAudio()) {
830 newElm->setAudio(oldElm->audio()); // LCOV_EXCL_LINE — no element type currently sets hasAudio=true in its ElementInfo, so this can never be true for any oldElm/newElm pair.
831 }
832
833 if (newElm->hasVolume() && oldElm->hasVolume()) {
834 newElm->setVolume(oldElm->volume());
835 }
836
837 if (newElm->hasDelay() && oldElm->hasDelay()) {
838 newElm->setDelay(oldElm->delay()); // LCOV_EXCL_LINE — Clock is the only element type with hasDelay=true, so no two distinct types can both satisfy this check.
839 }
840
841 // --- Migrate existing wires to the new element's ports ---
842 // The while loop drains the connection list; setEndPort/setStartPort calls
843 // internally detach from oldElm and attach to newElm, so the list shrinks.
844 // When the new element has fewer ports (e.g. Display14→Display7), connections
845 // on the removed ports are deleted to avoid leaving dangling wires in the scene.
846 transferPortConnections(oldElm, newElm, true, deleted);
847 transferPortConnections(oldElm, newElm, false, deleted);
848
849 // Reuse the old element's ID on the new element so that any external
850 // references (e.g. undo commands) remain valid after the morph
851 const int oldId = oldElm->id();
852 m_scene->removeItem(oldElm);
853 delete oldElm;
854
855 m_scene->updateItemId(newElm, oldId);
856 m_scene->addItem(newElm);
857 newElm->updatePortsProperties();
858 }
859}
860
861void MorphCommand::transferPortConnections(GraphicElement *oldElm, GraphicElement *newElm,
862 bool isInput, QList<DeletedConnectionInfo> *deleted)
863{
864 const int portCount = isInput ? oldElm->inputSize() : oldElm->outputSize();
865 for (int port = 0; port < portCount; ++port) {
866 Port *oldPort = isInput ? static_cast<Port *>(oldElm->inputPort(port))
867 : static_cast<Port *>(oldElm->outputPort(port));
868 while (!oldPort->connections().isEmpty()) {
869 auto *conn = oldPort->connections().constFirst();
870 if (!conn) { break; }
871 const bool ownsSide = isInput ? (conn->endPort() == oldElm->inputPort(port))
872 : (conn->startPort() == oldElm->outputPort(port));
873 if (!ownsSide) { break; }
874 Port *newPort = isInput ? static_cast<Port *>(newElm->inputPort(port))
875 : static_cast<Port *>(newElm->outputPort(port));
876 if (newPort) {
877 if (isInput) {
878 conn->setEndPort(static_cast<InputPort *>(newPort));
879 } else {
880 conn->setStartPort(static_cast<OutputPort *>(newPort));
881 }
882 conn->setHighLight(false);
883 } else {
884 // Port no longer exists on the morphed element — record before deleting
885 Port *otherPort = isInput ? static_cast<Port *>(conn->startPort())
886 : static_cast<Port *>(conn->endPort());
887 if (deleted && otherPort && otherPort->graphicElement()) {
888 deleted->append({conn->id(), oldElm->id(), port, isInput, otherPort->graphicElement()->id(), otherPort->index()});
889 }
890 conn->setStartPort(nullptr);
891 conn->setEndPort(nullptr);
892 m_scene->removeItem(conn);
893 delete conn;
894 }
895 }
896 }
897}
898
899FlipCommand::FlipCommand(const QList<GraphicElement *> &items, const int axis, Scene *scene, QUndoCommand *parent)
900 : ElementsCommand(items, scene, parent)
901 , m_axis(axis)
902{
903 if (items.isEmpty()) {
904 return;
905 }
906
907 setText(tr("Flip %1 elements in axis %2").arg(items.size()).arg(axis));
908 m_positions.reserve(items.size());
909
910 // Compute the bounding box of all selected elements so redo() can mirror
911 // each position about the selection's own axis rather than the scene origin
912 double xmin = items.constFirst()->pos().rx();
913 double ymin = items.constFirst()->pos().ry();
914 double xmax = xmin;
915 double ymax = ymin;
916
917 for (auto *item : items) {
918 m_positions.append(item->pos());
919 xmin = (std::min)(xmin, item->pos().rx());
920 ymin = (std::min)(ymin, item->pos().ry());
921 xmax = (std::max)(xmax, item->pos().rx());
922 ymax = (std::max)(ymax, item->pos().ry());
923 }
924
925 m_minPos = QPointF(xmin, ymin);
926 m_maxPos = QPointF(xmax, ymax);
927} // LCOV_EXCL_LINE — recurring pattern 1: compiler-generated cleanup for the constructor's local QList<GraphicElement *>/QPointF members' exception-unwind path, never reached after the return above (both the empty-items early return and the full non-empty body are independently exercised).
928
930{
931 qCDebug(zero) << text();
932 // Flip is an involution: applying it twice returns to the original state,
933 // so undo is identical to redo (position formula and +180° rotation cancel out)
934 redo();
935}
936
938{
939 for (auto *elm : elements()) {
940 auto pos = elm->pos();
941
942 // axis == 0: mirror across the vertical axis (flip horizontally)
943 // axis == 1: mirror across the horizontal axis (flip vertically)
944 // The formula reflects the coordinate: newX = xmin + (xmax - oldX)
945 (m_axis == 0) ? pos.setX(m_minPos.rx() + (m_maxPos.rx() - pos.rx()))
946 : pos.setY(m_minPos.ry() + (m_maxPos.ry() - pos.ry()));
947
948 elm->setPos(pos);
949
950 // Toggle the element's mirror flag for the appropriate axis. Rotatable elements get a
951 // true single-axis reflection of the whole item (scale(-1) about the pixmap centre);
952 // non-rotatable input/output elements mirror only their ports, keeping the graphic
953 // upright. Toggling is an involution, so undo == redo.
954 (m_axis == 0) ? elm->setFlippedX(!elm->isFlippedX())
955 : elm->setFlippedY(!elm->isFlippedY());
956 }
957
958 m_scene->setAutosaveRequired();
959}
960
961ChangePortSizeCommand::ChangePortSizeCommand(const QList<GraphicElement *> &elements, const int newPortSize, Scene *scene, const bool isInput, QUndoCommand *parent)
962 : ElementsCommand(elements, scene, parent)
963 , m_newPortSize(newPortSize)
964 , m_isInput(isInput)
965{
966 setText(isInput ? tr("Change input size to %1").arg(newPortSize)
967 : tr("Change output size to %1").arg(newPortSize));
968}
969
971{
972 // drainPortConnections() deletes Connections for removed ports; a
973 // simulation tick between that delete and setCircuitUpdateRequired()
974 // could still read a freed port/connection via a live element's
975 // now-stale port list before the topology is rebuilt.
976 SimulationBlocker blocker(m_scene->simulation());
977
978 const auto elements = this->elements();
979
980 QList<GraphicElement *> serializationOrder;
981 serializationOrder.reserve(elements.size());
982 m_oldData.clear();
983
984 QDataStream stream(&m_oldData, QIODevice::WriteOnly);
986
987 for (auto *elm : elements) {
988 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
989 serializationOrder.append(elm);
990
991 const int oldSize = m_isInput ? elm->inputSize() : elm->outputSize();
992
993 for (int port = m_newPortSize; port < oldSize; ++port) {
994 Port *nport = m_isInput ? static_cast<Port *>(elm->inputPort(port)) : elm->outputPort(port);
995 for (auto *conn : nport->connections()) {
996 Port *otherPort = m_isInput ? static_cast<Port *>(conn->startPort()) : conn->endPort();
997 otherPort->graphicElement()->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
998 serializationOrder.append(otherPort->graphicElement());
999 }
1000 }
1001 }
1002
1003 for (auto *elm : elements) {
1004 CommandUtils::drainPortConnections(elm, m_newPortSize, m_isInput ? elm->inputSize() : elm->outputSize(), m_isInput, stream, m_scene);
1005 if (m_isInput) {
1006 elm->setInputSize(m_newPortSize);
1007 } else {
1008 elm->setOutputSize(m_newPortSize);
1009 elm->setSelected(true);
1010 }
1011 }
1012
1013 m_order.clear();
1014
1015 for (auto *elm : serializationOrder) {
1016 m_order.append(elm->id());
1017 }
1018
1019 m_scene->setCircuitUpdateRequired();
1020}
1021
1023{
1024 SimulationBlocker blocker(m_scene->simulation());
1025
1026 const auto elements = this->elements();
1027 const auto serializationOrder = CommandUtils::findElements(m_scene, m_order);
1028
1029 QDataStream stream(&m_oldData, QIODevice::ReadOnly);
1030 QVersionNumber version = Serialization::readPandaHeader(stream);
1031
1032 QHash<quint64, Port *> portMap;
1033 auto context = m_scene->deserializationContext(portMap, version, SerializationPurpose::InMemorySnapshot);
1034
1035 for (auto *elm : serializationOrder) {
1036 elm->load(stream, context);
1037 }
1038
1039 for (auto *elm : elements) {
1040 int connCount; stream >> connCount;
1041 for (int i = 0; i < connCount; ++i) {
1042 int connId; stream >> connId;
1043 auto *conn = new Connection();
1044 conn->load(stream, context);
1045 m_scene->updateItemId(conn, connId);
1046 m_scene->addItem(conn);
1047 }
1048
1049 elm->setSelected(true);
1050 }
1051
1052 m_scene->setCircuitUpdateRequired();
1053}
1054
1056 : QUndoCommand(parent)
1057 , m_pos(pos)
1058{
1059 m_id = element->id();
1060 m_scene = scene;
1061 setText(tr("Toggle TruthTable Output at position: %1").arg(m_pos));
1062}
1063
1065{
1066 qCDebug(zero) << text();
1067
1068 auto *truthtable = qobject_cast<TruthTable *>(CommandUtils::findElm(m_scene, m_id));
1069
1070 if (!truthtable) throw PANDACEPTION("Could not find truthtable element!");
1071
1072 // The key holds a fixed number of bits, derived from the element's port limits in
1073 // TruthTable.cpp; toggleBit outside it is an out-of-bounds write. This command is the model
1074 // boundary shared by the UI and the MCP server, and undo() == redo(), so
1075 // the bound is enforced here regardless of caller.
1076 if (m_pos < 0 || m_pos >= truthtable->key().size()) {
1077 throw PANDACEPTION("TruthTable toggle position out of range: %1", QString::number(m_pos));
1078 }
1079
1080 truthtable->key().toggleBit(m_pos);
1081
1082 m_scene->setCircuitUpdateRequired();
1083 emit m_scene->truthTableElementChanged(truthtable);
1084}
1085
1087{
1088 // toggleBit is self-inverse: undo == redo
1089 redo();
1090}
1091
1092// --- UpdateBlobCommand ---
1093
1094QList<UpdateBlobCommand::ConnectionInfo> UpdateBlobCommand::captureConnections(const QList<GraphicElement *> &targets)
1095{
1096 QList<ConnectionInfo> connections;
1097 for (auto *target : targets) {
1098 for (int i = 0; i < target->inputSize(); ++i) {
1099 for (auto *conn : target->inputPort(i)->connections()) {
1100 auto *otherPort = conn->startPort();
1101 if (otherPort && otherPort->graphicElement()) {
1102 connections.append({conn->id(), target->id(), i, true,
1103 otherPort->graphicElement()->id(), otherPort->index()});
1104 }
1105 }
1106 }
1107 for (int i = 0; i < target->outputSize(); ++i) {
1108 for (auto *conn : target->outputPort(i)->connections()) {
1109 auto *otherPort = conn->endPort();
1110 if (otherPort && otherPort->graphicElement()) {
1111 connections.append({conn->id(), target->id(), i, false,
1112 otherPort->graphicElement()->id(), otherPort->index()});
1113 }
1114 }
1115 }
1116 }
1117 return connections;
1118} // LCOV_EXCL_LINE — recurring pattern 1: compiler-generated cleanup for the returned QList<ConnectionInfo>, never reached after the return above.
1119
1120// --- RegisterBlobCommand ---
1121
1122RegisterBlobCommand::RegisterBlobCommand(const QString &blobName, const QByteArray &data, Scene *scene, QUndoCommand *parent)
1123 : QUndoCommand(parent)
1124 , m_blobName(blobName)
1125 , m_data(data)
1126 , m_scene(scene)
1127{
1128 setText(tr("Register blob \"%1\"").arg(blobName));
1129}
1130
1132{
1133 m_scene->icRegistry()->registerBlob(m_blobName, m_data);
1134}
1135
1137{
1138 m_scene->icRegistry()->removeBlob(m_blobName);
1139}
1140
1141// --- RemoveBlobCommand ---
1142
1143RemoveBlobCommand::RemoveBlobCommand(const QString &blobName, Scene *scene, QUndoCommand *parent)
1144 : QUndoCommand(parent)
1145 , m_blobName(blobName)
1146 // Snapshot the blob bytes at construction so undo can restore them even
1147 // if the registry has been mutated in the meantime.
1148 , m_data(scene->icRegistry()->blob(blobName))
1149 , m_scene(scene)
1150{
1151 setText(tr("Remove blob \"%1\"").arg(blobName));
1152}
1153
1155{
1156 m_scene->icRegistry()->removeBlob(m_blobName);
1157}
1158
1160{
1161 m_scene->icRegistry()->setBlob(m_blobName, m_data);
1162}
1163
1164// --- RenameBlobCommand ---
1165
1166RenameBlobCommand::RenameBlobCommand(const QString &oldName, const QString &newName, Scene *scene, QUndoCommand *parent)
1167 : QUndoCommand(parent)
1168 , m_oldName(oldName)
1169 , m_newName(newName)
1170 , m_scene(scene)
1171{
1172 setText(tr("Rename IC \"%1\" to \"%2\"").arg(oldName, newName));
1173}
1174
1176{
1177 m_scene->icRegistry()->renameBlob(m_oldName, m_newName);
1178}
1179
1181{
1182 m_scene->icRegistry()->renameBlob(m_newName, m_oldName);
1183}
1184
1185// --- UpdateBlobCommand ---
1186
1187UpdateBlobCommand::UpdateBlobCommand(const QList<GraphicElement *> &elements, const QByteArray &oldData,
1188 const QList<ConnectionInfo> &connections, Scene *scene, QUndoCommand *parent)
1189 : ElementsCommand(elements, scene, parent)
1190 , m_oldData(oldData)
1191 , m_connections(connections)
1192{
1193 QDataStream stream(&m_newData, QIODevice::WriteOnly);
1195
1196 for (auto *elm : elements) {
1197 elm->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
1198 }
1199
1200 if (!elements.isEmpty()) {
1201 m_blobName = elements.first()->blobName();
1202 m_newBlob = m_scene->icRegistry()->blob(m_blobName);
1203 }
1204
1205 setText(tr("Update %1 IC blobs").arg(elements.size()));
1206}
1207
1209{
1210 qCDebug(zero) << text();
1211 SimulationBlocker blocker(m_scene->simulation());
1212 auto *reg = m_scene->icRegistry();
1213
1214 if (!m_blobName.isEmpty()) {
1215 if (m_newBlob.isEmpty()) {
1216 reg->removeBlob(m_blobName);
1217 } else {
1218 reg->setBlob(m_blobName, m_newBlob);
1219 }
1220 }
1221 loadData(m_newData);
1222 reconnectConnections();
1223 m_scene->setCircuitUpdateRequired();
1224}
1225
1227{
1228 qCDebug(zero) << text();
1229 SimulationBlocker blocker(m_scene->simulation());
1230 auto *reg = m_scene->icRegistry();
1231
1232 if (!m_blobName.isEmpty()) {
1233 if (m_oldBlob.isEmpty()) {
1234 reg->removeBlob(m_blobName);
1235 } else {
1236 reg->setBlob(m_blobName, m_oldBlob);
1237 }
1238 }
1239 loadData(m_oldData);
1240 reconnectConnections();
1241 m_scene->setCircuitUpdateRequired();
1242}
1243
1244void UpdateBlobCommand::loadData(QByteArray &itemData)
1245{
1246 const auto elements = this->elements();
1247 if (elements.isEmpty()) {
1248 return;
1249 }
1250
1251 QDataStream stream(&itemData, QIODevice::ReadOnly);
1252 QVersionNumber version = Serialization::readPandaHeader(stream);
1253
1254 QHash<quint64, Port *> portMap;
1255 auto context = m_scene->deserializationContext(portMap, version, SerializationPurpose::InMemorySnapshot);
1256
1257 for (auto *elm : elements) {
1258 elm->load(stream, context);
1259 elm->setSelected(true);
1260 }
1261}
1262
1263void UpdateBlobCommand::reconnectConnections()
1264{
1265 for (const auto &ci : std::as_const(m_connections)) {
1266 auto *elm = dynamic_cast<GraphicElement *>(m_scene->itemById(ci.elementId));
1267 auto *otherElm = dynamic_cast<GraphicElement *>(m_scene->itemById(ci.otherElementId));
1268 if (!elm || !otherElm) {
1269 continue;
1270 }
1271
1272 InputPort *inPort = nullptr;
1273 OutputPort *outPort = nullptr;
1274
1275 if (ci.isInput) {
1276 inPort = (ci.portIndex >= 0 && ci.portIndex < elm->inputSize()) ? elm->inputPort(ci.portIndex) : nullptr;
1277 outPort = (ci.otherPortIndex >= 0 && ci.otherPortIndex < otherElm->outputSize()) ? otherElm->outputPort(ci.otherPortIndex) : nullptr;
1278 } else {
1279 outPort = (ci.portIndex >= 0 && ci.portIndex < elm->outputSize()) ? elm->outputPort(ci.portIndex) : nullptr;
1280 inPort = (ci.otherPortIndex >= 0 && ci.otherPortIndex < otherElm->inputSize()) ? otherElm->inputPort(ci.otherPortIndex) : nullptr;
1281 }
1282
1283 if (!inPort || !outPort) {
1284 // Port shrunk: the Connection that occupied this slot was
1285 // cascade-deleted by Qt when the IC's port was destroyed. Its
1286 // registry entry is already gone -- ItemWithId self-unregisters
1287 // from the registry in its own destructor, on any destruction
1288 // path. Undo restores the IC's port count and recreates the
1289 // connection with the original ID below.
1290 continue;
1291 }
1292
1293 bool alreadyConnected = false;
1294 for (auto *conn : inPort->connections()) {
1295 if (conn->startPort() == outPort) {
1296 alreadyConnected = true;
1297 break;
1298 }
1299 }
1300 if (alreadyConnected) {
1301 continue;
1302 }
1303
1304 auto *conn = new Connection();
1305 conn->setStartPort(outPort);
1306 conn->setEndPort(inPort);
1307 m_scene->updateItemId(conn, ci.connectionId);
1308 m_scene->addItem(conn);
1309 }
1310}
All QUndoCommand subclasses and the CommandUtils helper namespace.
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
Definition Common.h:98
#define PANDACEPTION_WITH_CONTEXT(context, msg,...)
Definition Common.h:102
#define qCDebug(category)
Definition Common.h:29
Connection: a wire that connects an output port to an input port in the circuit scene.
Shared numeric constants used across layers.
Singleton factory for all circuit element types.
Enums::ElementType ElementType
Definition Enums.h:107
Abstract base class for all graphical circuit elements.
IC definition registry with file watching and embedded blob storage.
Port classes: Port (base), InputPort, and OutputPort.
Main circuit editing scene with undo/redo and user interaction.
Deserialization/serialization context structs passed through load()/save() call chains.
Circuit and waveform file serialization/deserialization utilities.
RAII guard that temporarily stops the simulation while in scope.
Synchronous cycle-based simulation engine with event-driven clock support.
Graphic element for a user-programmable truth table.
void undo() override
Removes the items from the scene.
Definition Commands.cpp:288
void redo() override
Adds the items to the scene.
Definition Commands.cpp:298
AddItemsCommand(const QList< QGraphicsItem * > &items, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the command for adding items.
Definition Commands.cpp:272
void undo() override
Restores the old port count and connections.
void redo() override
Applies the new port count.
Definition Commands.cpp:970
ChangePortSizeCommand(const QList< GraphicElement * > &elements, const int newPortSize, Scene *scene, const bool isInput, QUndoCommand *parent=nullptr)
Constructs the command.
Definition Commands.cpp:961
A bezier-curve wire connecting an output port to an input port in the scene.
Definition Connection.h:38
OutputPort * startPort() const
Returns the output port this connection originates from.
InputPort * endPort() const
Returns the input port this connection leads to.
double angle()
Returns the current angle of the bezier midpoint in radians.
DeleteItemsCommand(const QList< QGraphicsItem * > &items, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the command for deleting items.
Definition Commands.cpp:306
void undo() override
Restores the items to the scene.
Definition Commands.cpp:314
void redo() override
Removes the items from the scene.
Definition Commands.cpp:322
static GraphicElement * buildElement(const ElementType type)
Constructs and returns a new graphic element of the given type.
QList< int > m_ids
Definition Commands.h:59
ElementsCommand(const QList< GraphicElement * > &elements, Scene *scene, QUndoCommand *parent=nullptr)
Definition Commands.cpp:257
Scene * m_scene
Definition Commands.h:58
QList< GraphicElement * > elements() const
Returns the live element pointers for this command's targets, looked up by stored ID.
Definition Commands.cpp:267
FlipCommand(const QList< GraphicElement * > &items, const int axis, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the flip command.
Definition Commands.cpp:899
void redo() override
Applies the flip transformation.
Definition Commands.cpp:937
void undo() override
Reverses the flip transformation (involution: flip twice = identity).
Definition Commands.cpp:929
Abstract base class for all graphical circuit elements in wiRedPanda.
int inputSize() const
Returns the current number of input ports.
InputPort * inputPort(const int index=0) const
Returns the input port at index (default 0).
int outputSize() const
Returns the current number of output ports.
OutputPort * outputPort(const int index=0) const
Returns the output port at index (default 0).
virtual void save(QDataStream &stream, SerializationOptions options) const
Base class providing a unique integer identifier for circuit items.
Definition ItemWithId.h:31
int id() const
Returns the unique integer identifier of this item, or -1 if unassigned.
Definition ItemWithId.h:40
MorphCommand(const QList< GraphicElement * > &elements, ElementType type, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the morph command.
Definition Commands.cpp:695
void undo() override
Restores the original element types.
Definition Commands.cpp:736
void redo() override
Replaces elements with instances of the new type.
Definition Commands.cpp:759
MoveCommand(const QList< GraphicElement * > &list, const QList< QPointF > &oldPositions, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the command capturing old and new positions.
Definition Commands.cpp:394
void undo() override
Restores elements to their old positions.
Definition Commands.cpp:407
void redo() override
Moves elements to their new positions.
Definition Commands.cpp:418
Abstract base class for circuit element ports (connection endpoints).
Definition Port.h:39
GraphicElement * graphicElement()
Returns the graphic element that owns this port.
Definition Port.h:71
int index() const
Returns the port's visual/logical index within the element.
Definition Port.cpp:138
const QList< Connection * > & connections() const
Returns the list of wires attached to this port.
Definition Port.cpp:57
void redo() override
void undo() override
RegisterBlobCommand(const QString &blobName, const QByteArray &data, Scene *scene, QUndoCommand *parent=nullptr)
void redo() override
void undo() override
RemoveBlobCommand(const QString &blobName, Scene *scene, QUndoCommand *parent=nullptr)
void redo() override
void undo() override
RenameBlobCommand(const QString &oldName, const QString &newName, Scene *scene, QUndoCommand *parent=nullptr)
void undo() override
Reverses the rotation.
Definition Commands.cpp:346
void redo() override
Applies the rotation.
Definition Commands.cpp:361
RotateCommand(const QList< GraphicElement * > &items, const int angle, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the command.
Definition Commands.cpp:332
Main circuit editing scene.
Definition Scene.h:56
void removeItem(QGraphicsItem *item)
Removes item from the scene and unregisters it from the per-scene ID registry.
Definition Scene.cpp:160
ItemWithId * itemById(int id) const
Returns the item registered under id, or nullptr if not found.
Definition Scene.cpp:171
void addItem(QGraphicsItem *item)
Adds item to the scene and registers it in the per-scene ID registry.
Definition Scene.cpp:119
void updateItemId(ItemWithId *item, int newId)
Reassigns the ID of item to newId without adding it to the scene.
Definition Scene.cpp:196
SerializationContext deserializationContext(QHash< quint64, Port * > &portMap, const QVersionNumber &version, SerializationPurpose purpose)
Definition Scene.cpp:216
static QList< QGraphicsItem * > deserialize(QDataStream &stream, SerializationContext &context)
Deserializes items from stream until the stream is exhausted.
static void serialize(const QList< QGraphicsItem * > &items, QDataStream &stream, SerializationOptions options)
Serializes items to stream in the current .panda binary format.
static void writePandaHeader(QDataStream &stream)
Writes the .panda circuit file header to stream.
static QVersionNumber readPandaHeader(QDataStream &stream)
Reads and validates the .panda circuit file header; returns the stored version number.
RAII guard that stops the simulation on construction and restarts it on destruction.
SplitCommand(Connection *conn, QPointF mousePos, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the split command.
Definition Commands.cpp:553
void undo() override
Removes the node and restores the original connection.
Definition Commands.cpp:665
void redo() override
Inserts the node and splits the connection.
Definition Commands.cpp:612
void undo() override
Toggles the bit again (involution).
void redo() override
Toggles the bit at pos.
ToggleTruthTableOutputCommand(GraphicElement *element, int pos, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the toggle command.
UpdateBlobCommand(const QList< GraphicElement * > &elements, const QByteArray &oldData, const QList< ConnectionInfo > &connections, Scene *scene, QUndoCommand *parent=nullptr)
void undo() override
static QList< ConnectionInfo > captureConnections(const QList< GraphicElement * > &targets)
Captures connection topology for all target elements before a blob operation.
void redo() override
UpdateCommand(const QList< GraphicElement * > &elements, const QByteArray &oldData, Scene *scene, QUndoCommand *parent=nullptr)
Constructs the command.
Definition Commands.cpp:429
void undo() override
Restores the old property values.
Definition Commands.cpp:462
void redo() override
Applies the new property values.
Definition Commands.cpp:473
const QList< GraphicElement * > findElements(Scene *scene, const QList< int > &ids)
Definition Commands.cpp:129
void addItems(Scene *scene, const QList< QGraphicsItem * > &items)
Definition Commands.cpp:172
void saveItems(Scene *scene, QByteArray &itemData, const QList< QGraphicsItem * > &items, const QList< int > &otherIds)
Definition Commands.cpp:157
GraphicElement * findElm(Scene *scene, const int id)
Definition Commands.cpp:152
void storeIds(const QList< QGraphicsItem * > &items, QList< int > &ids)
Definition Commands.cpp:28
const QList< QGraphicsItem * > findItems(Scene *scene, const QList< int > &ids)
Definition Commands.cpp:111
Connection * findConn(Scene *scene, const int id)
Definition Commands.cpp:147
const QList< QGraphicsItem * > loadItems(Scene *scene, QByteArray &itemData, const QList< int > &ids, QList< int > &otherIds)
Definition Commands.cpp:185
void drainPortConnections(GraphicElement *elm, int fromPort, int toPort, bool isInput, QDataStream &stream, Scene *scene)
Saves and deletes connections on ports in range [fromPort, toPort).
Definition Commands.cpp:231
const QList< QGraphicsItem * > loadList(const QList< QGraphicsItem * > &items, QList< int > &ids, QList< int > &otherIds)
Definition Commands.cpp:57
void storeOtherIds(const QList< QGraphicsItem * > &connections, const QList< int > &ids, QList< int > &otherIds)
Definition Commands.cpp:39
void deleteItems(Scene *scene, const QList< QGraphicsItem * > &items)
Definition Commands.cpp:222
constexpr int gridSize
Scene grid unit in pixels (elements snap to gridSize/2).
Definition Constants.h:12