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