wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
ElementHandler.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 <QIODevice>
7#include <QJsonArray>
8
9#include "App/Core/Common.h"
10#include "App/Core/Constants.h"
11#include "App/Core/Enums.h"
19#include "App/Scene/Commands.h"
20#include "App/Scene/Scene.h"
23#include "App/Wiring/Port.h"
24
26 : BaseHandler(mainWindow, validator)
27{
28}
29
30QJsonObject ElementHandler::handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId)
31{
32 if (command == "create_element") {
33 return handleCreateElement(params, requestId);
34 } else if (command == "delete_element") {
35 return handleDeleteElement(params, requestId);
36 } else if (command == "list_elements") {
37 return handleListElements(params, requestId);
38 } else if (command == "move_element") {
39 return handleMoveElement(params, requestId);
40 } else if (command == "set_element_properties") {
41 return handleSetElementProperties(params, requestId);
42 } else if (command == "set_input_value") {
43 return handleSetInputValue(params, requestId);
44 } else if (command == "get_output_value") {
45 return handleGetOutputValue(params, requestId);
46 } else if (command == "rotate_element") {
47 return handleRotateElement(params, requestId);
48 } else if (command == "flip_element") {
49 return handleFlipElement(params, requestId);
50 } else if (command == "update_element") {
51 return handleUpdateElement(params, requestId);
52 } else if (command == "change_input_size") {
53 return handleChangeInputSize(params, requestId);
54 } else if (command == "change_output_size") {
55 return handleChangeOutputSize(params, requestId);
56 } else if (command == "toggle_truth_table_output") {
57 return handleToggleTruthTableOutput(params, requestId);
58 } else if (command == "morph_element") {
59 return handleMorphElement(params, requestId);
60 } else {
61 return createErrorResponse(QString("Unknown element command: %1").arg(command),
63 }
64}
65
66QJsonObject ElementHandler::handleCreateElement(const QJsonObject &params, const QJsonValue &requestId)
67{
68 if (!validateParameters(params, {"type", "x", "y"})) {
69 return createErrorResponse("Missing required parameters: type, x, y", requestId, JsonRpcError::InvalidParams);
70 }
71
72 QString errorMsg;
73 if (!validateNonEmptyString(params.value("type"), "type", errorMsg)) {
74 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
75 }
76
77 if (!validateNumeric(params.value("x"), "x", errorMsg)) {
78 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
79 }
80
81 if (!validateNumeric(params.value("y"), "y", errorMsg)) {
82 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
83 }
84
85 QString typeStr = params.value("type").toString();
86 double x = params.value("x").toDouble();
87 double y = params.value("y").toDouble();
88 QString label = params.value("label").toString();
89
91 if (type == ElementType::Unknown) {
92 return createErrorResponse(QString("Invalid element type: %1").arg(typeStr),
94 }
95
96 Scene *scene = currentScene();
97 if (!scene) {
98 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
99 }
100
101 GraphicElement *element = nullptr;
102 // Unreachable: ElementFactory::buildElement() only throws for ElementType::Unknown (already
103 // rejected above) or a type with no registered creator -- every real type reaching here has
104 // one (confirmed by reading ElementFactory.cpp's creator map).
105 try {
106 element = ElementFactory::buildElement(type);
107 } catch (const std::exception &e) { // LCOV_EXCL_LINE
108 return createErrorResponse(QString("Failed to create element of type %1: %2").arg(typeStr, e.what()), // LCOV_EXCL_LINE
109 requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
110 } catch (...) { // LCOV_EXCL_LINE
111 return createErrorResponse(QString("Failed to create element of type: %1").arg(typeStr), // LCOV_EXCL_LINE
112 requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
113 } // LCOV_EXCL_LINE
114
115 // Unreachable: a `new` expression (which buildElement() uses internally) never returns
116 // null without throwing -- the catch blocks above already handle every throwing path.
117 if (!element) { // LCOV_EXCL_LINE
118 return createErrorResponse(QString("Failed to create element of type: %1").arg(typeStr), // LCOV_EXCL_LINE
119 requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
120 } // LCOV_EXCL_LINE
121
122 element->setPos(x, y);
123 if (!label.isEmpty()) {
124 element->setLabel(label);
125 }
126
127 // Match the interactive drag-and-drop path (SceneDropHandler::handleDrop): clear the
128 // previous selection before adding the new element so it alone ends up selected,
129 // rather than accumulating onto every element ever created in this session. Without
130 // this, a long MCP-driven build (many create_element calls with nothing else ever
131 // clearing selection) leaves the entire circuit selected, and every selection-tracking
132 // consumer (e.g. ElementEditor::selectionChanged) pays for the whole growing selection
133 // on every single element added — O(N) per call, O(N^2) over the full build.
134 scene->clearSelection();
135
136 // Unreachable: scene->receiveCommand() with a well-formed AddItemsCommand never throws
137 // (confirmed across the whole App/Scene/Commands.cpp sweep).
138 try {
139 scene->receiveCommand(new AddItemsCommand({element}, scene));
140 } catch (const std::exception &e) { // LCOV_EXCL_LINE
141 delete element; // LCOV_EXCL_LINE
142 return createErrorResponse(QString("Failed to add element to scene: %1").arg(e.what()), // LCOV_EXCL_LINE
143 requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
144 } catch (...) { // LCOV_EXCL_LINE
145 delete element; // LCOV_EXCL_LINE
146 return createErrorResponse("Failed to add element to scene: Unknown exception", // LCOV_EXCL_LINE
147 requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
148 } // LCOV_EXCL_LINE
149
150 const QRectF bounds = element->boundingRect();
151
152 QJsonObject result;
153 result["element_id"] = element->id();
154 result["width"] = bounds.width();
155 result["height"] = bounds.height();
156
157 return createSuccessResponse(result, requestId);
158}
159
160QJsonObject ElementHandler::handleDeleteElement(const QJsonObject &params, const QJsonValue &requestId)
161{
162 if (!validateParameters(params, {"element_id"})) {
163 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
164 }
165
166 QString errorMsg;
167 auto *element = validatedElement(params, "element_id", errorMsg);
168 if (!element) {
169 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
170 }
171
172 Scene *scene = currentScene();
173 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
174 // this exact message) if there's no active scene, before this point can ever be reached.
175 if (!scene) {
176 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
177 }
178
179 return tryCommand([&] { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
180 scene->receiveCommand(new DeleteItemsCommand({element}, scene));
181 return createSuccessResponse(QJsonObject(), requestId);
182 }, "delete element", requestId);
183}
184
185QJsonObject ElementHandler::handleListElements(const QJsonObject &, const QJsonValue &requestId)
186{
187 Scene *scene = currentScene();
188 if (!scene) {
189 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
190 }
191
192 QJsonArray elements;
193 const auto sceneElements = scene->elements();
194
195 for (const auto *element : sceneElements) {
196 const QRectF br = element->boundingRect();
197 QJsonObject elementObj;
198 elementObj["element_id"] = element->id();
199 elementObj["type"] = ElementFactory::typeToText(element->elementType());
200 elementObj["x"] = element->pos().x();
201 elementObj["y"] = element->pos().y();
202 elementObj["width"] = br.width();
203 elementObj["height"] = br.height();
204 elementObj["label"] = element->label();
205 elementObj["rotation"] = element->rotation();
206 elementObj["input_size"] = element->inputSize();
207 elementObj["output_size"] = element->outputSize();
208
209 if (element->hasColors()) {
210 elementObj["color"] = element->color();
211 }
212 if (element->hasFrequency()) {
213 elementObj["frequency"] = element->frequency();
214 }
215 if (element->hasDelay()) {
216 elementObj["delay"] = element->delay();
217 }
218 if (element->hasTrigger()) {
219 elementObj["trigger"] = element->trigger().toString();
220 }
221 if (element->hasAudio()) {
222 elementObj["audio"] = element->audio(); // LCOV_EXCL_LINE -- hasAudio() is always false across every GraphicElement type, so this branch is dead
223 }
224 if (element->hasVolume()) {
225 elementObj["volume"] = static_cast<double>(element->volume());
226 }
227 if (const auto *inputElm = qobject_cast<const GraphicElementInput *>(element)) {
228 elementObj["locked"] = inputElm->isLocked();
229 }
230
231 elements.append(elementObj);
232 }
233
234 QJsonObject result;
235 result["elements"] = elements;
236
237 return createSuccessResponse(result, requestId);
238}
239
240QJsonObject ElementHandler::handleMoveElement(const QJsonObject &params, const QJsonValue &requestId)
241{
242 if (!validateParameters(params, {"element_id", "x", "y"})) {
243 return createErrorResponse("Missing required parameters: element_id, x, y", requestId, JsonRpcError::InvalidParams);
244 }
245
246 QString errorMsg;
247 if (!validateNumeric(params.value("x"), "x", errorMsg)) {
248 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
249 }
250 if (!validateNumeric(params.value("y"), "y", errorMsg)) {
251 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
252 }
253
254 const int snap = Constants::gridSize / 2;
255 const int x = qRound(params.value("x").toDouble() / snap) * snap;
256 const int y = qRound(params.value("y").toDouble() / snap) * snap;
257
258 auto *element = validatedElement(params, "element_id", errorMsg);
259 if (!element) {
260 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
261 }
262
263 Scene *scene = currentScene();
264 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
265 // this exact message) if there's no active scene, before this point can ever be reached.
266 if (!scene) {
267 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
268 }
269
270 QPointF oldPos = element->pos();
271 QPointF newPos(x, y);
272
273 element->setPos(newPos);
274
275 QList<GraphicElement *> elements = {element};
276 QList<QPointF> oldPositions = {oldPos};
277
278 // Unreachable: scene->receiveCommand() with a well-formed MoveCommand never throws
279 // (confirmed across the whole App/Scene/Commands.cpp sweep).
280 try {
281 scene->receiveCommand(new MoveCommand(elements, oldPositions, scene));
282 } catch (const std::exception &e) { // LCOV_EXCL_LINE
283 element->setPos(oldPos); // LCOV_EXCL_LINE
284 return createErrorResponse(QString("Failed to move element: %1").arg(e.what()), // LCOV_EXCL_LINE
285 requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
286 } catch (...) { // LCOV_EXCL_LINE
287 element->setPos(oldPos); // LCOV_EXCL_LINE
288 return createErrorResponse("Failed to move element: Unknown exception", // LCOV_EXCL_LINE
289 requestId, JsonRpcError::OperationFailed); // LCOV_EXCL_LINE
290 } // LCOV_EXCL_LINE
291
292 QJsonObject result;
293 result["old_position"] = QJsonObject{{"x", oldPos.x()}, {"y", oldPos.y()}};
294 result["new_position"] = QJsonObject{{"x", newPos.x()}, {"y", newPos.y()}};
295
296 return createSuccessResponse(result, requestId);
297}
298
299namespace {
300
305QString statusName(const Status status)
306{
307 switch (status) {
308 case Status::Active: return QStringLiteral("high");
309 case Status::Inactive: return QStringLiteral("low");
310 case Status::Error: return QStringLiteral("error");
311 default: return QStringLiteral("unknown");
312 }
313}
314
315} // namespace
316
317QJsonObject ElementHandler::handleSetElementProperties(const QJsonObject &params, const QJsonValue &requestId)
318{
319 if (!validateParameters(params, {"element_id"})) {
320 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
321 }
322
323 QString errorMsg;
324 auto *element = validatedElement(params, "element_id", errorMsg);
325 if (!element) {
326 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
327 }
328
329 // Reject structural changes before any mutation — these require dedicated commands.
330 if (params.contains("input_size") || params.contains("output_size")) {
331 return createErrorResponse("Use change_input_size / change_output_size commands to change port counts",
333 }
334
335 // Snapshot current state before any mutation so UpdateCommand can restore it on undo.
336 QByteArray oldData;
337 {
338 QDataStream stream(&oldData, QIODevice::WriteOnly);
340 element->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
341 }
342
343 QJsonObject result;
344 QJsonObject oldProperties;
345 QJsonObject newProperties;
346
347 if (params.contains("label")) {
348 QString oldLabel = element->label();
349 QString newLabel = params.value("label").toString();
350
351 oldProperties["label"] = oldLabel;
352 newProperties["label"] = newLabel;
353
354 element->setLabel(newLabel);
355 }
356
357 if (params.contains("color") && element->hasColors()) {
358 QString oldColor = element->color();
359 QString newColor = params.value("color").toString();
360
361 oldProperties["color"] = oldColor;
362 newProperties["color"] = newColor;
363
364 element->setColor(newColor);
365 }
366
367 if (params.contains("frequency") && element->hasFrequency()) {
368 if (!validateNumeric(params.value("frequency"), "frequency", errorMsg)) {
369 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
370 }
371 double oldFreq = element->frequency();
372 double newFreq = params.value("frequency").toDouble();
373 if (newFreq <= 0) {
374 return createErrorResponse("Parameter 'frequency' must be a positive value", requestId, JsonRpcError::ValidationError);
375 }
376
377 oldProperties["frequency"] = oldFreq;
378 newProperties["frequency"] = newFreq;
379
380 element->setFrequency(newFreq);
381 }
382
383 if (params.contains("rotation")) {
384 qreal oldRotation = element->rotation();
385 qreal newRotation = params.value("rotation").toDouble();
386
387 oldProperties["rotation"] = oldRotation;
388 newProperties["rotation"] = newRotation;
389
390 element->setRotation(newRotation);
391 }
392
393 if (params.contains("delay") && element->hasDelay()) {
394 if (!validateNumeric(params.value("delay"), "delay", errorMsg)) {
395 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
396 }
397 double oldDelay = element->delay();
398 double newDelay = params.value("delay").toDouble();
399 if (newDelay < -1.0 || newDelay > 1.0) {
400 return createErrorResponse("Parameter 'delay' must be in [-1, 1] (fraction of period; negative advances the clock)",
402 }
403
404 oldProperties["delay"] = oldDelay;
405 newProperties["delay"] = newDelay;
406
407 element->setDelay(newDelay);
408 }
409
410 if (params.contains("trigger") && element->hasTrigger()) {
411 QString oldTrigger = element->trigger().toString();
412 QString newTrigger = params.value("trigger").toString();
413
414 oldProperties["trigger"] = oldTrigger;
415 newProperties["trigger"] = newTrigger;
416
417 element->setTrigger(QKeySequence(newTrigger));
418 }
419
420 // Unreachable body: hasAudio() is always false across every GraphicElement type.
421 if (params.contains("audio") && element->hasAudio()) {
422 QString oldAudio = element->audio(); // LCOV_EXCL_LINE
423 QString newAudio = params.value("audio").toString(); // LCOV_EXCL_LINE
424
425 oldProperties["audio"] = oldAudio; // LCOV_EXCL_LINE
426 newProperties["audio"] = newAudio; // LCOV_EXCL_LINE
427
428 element->setAudio(newAudio); // LCOV_EXCL_LINE
429 } // LCOV_EXCL_LINE
430
431 if (params.contains("locked")) {
432 auto *inputElm = qobject_cast<GraphicElementInput *>(element);
433 if (inputElm) {
434 bool oldLocked = inputElm->isLocked();
435 bool newLocked = params.value("locked").toBool();
436
437 oldProperties["locked"] = oldLocked;
438 newProperties["locked"] = newLocked;
439
440 inputElm->setLocked(newLocked);
441 }
442 }
443
444 if (params.contains("volume") && element->hasVolume()) {
445 if (!validateNumeric(params.value("volume"), "volume", errorMsg)) {
446 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
447 }
448 float oldVolume = element->volume();
449 float newVolume = static_cast<float>(params.value("volume").toDouble());
450 if (newVolume < 0.0f || newVolume > 1.0f) {
451 return createErrorResponse("Parameter 'volume' must be between 0.0 and 1.0", requestId, JsonRpcError::ValidationError);
452 }
453
454 oldProperties["volume"] = static_cast<double>(oldVolume);
455 newProperties["volume"] = static_cast<double>(newVolume);
456
457 element->setVolume(newVolume);
458 }
459
460 if (params.contains("appearance") && element->canChangeAppearance()) {
461 QString appearancePath = params.value("appearance").toString();
462 bool useDefault = appearancePath.isEmpty();
463 int appearanceIndex = params.contains("appearance_index") ? params.value("appearance_index").toInt() : -1;
464
465 newProperties["appearance"] = appearancePath;
466 newProperties["appearance_default"] = useDefault;
467
468 if (appearanceIndex >= 0) {
469 // Set appearance for a specific index directly (e.g., LED state)
470 element->setAppearanceAt(appearanceIndex, appearancePath);
471 newProperties["appearance_index"] = appearanceIndex;
472 } else {
473 element->setAppearance(useDefault, appearancePath);
474 }
475 }
476
477 // Wireless mode is only meaningful for Node elements; non-Nodes are silently ignored.
478 QList<QGraphicsItem *> wirelessConnsToDelete;
479 if (params.contains("wireless_mode")) {
480 if (auto *node = qobject_cast<Node *>(element)) {
481 int modeInt = params.value("wireless_mode").toInt();
482 if (modeInt < 0 || modeInt > 2) {
483 return createErrorResponse("Invalid wireless_mode. Must be 0 (None), 1 (Tx), or 2 (Rx)",
485 }
486 auto oldMode = static_cast<int>(node->wirelessMode());
487 auto newMode = static_cast<WirelessMode>(modeInt);
488
489 oldProperties["wireless_mode"] = oldMode;
490 newProperties["wireless_mode"] = modeInt;
491
492 // Collect connections on the port that will be hidden, so they can
493 // be deleted in an undo macro (same pattern as ElementEditor::apply).
494 Port *port = (newMode == WirelessMode::Rx) ? static_cast<Port *>(node->inputPort())
495 : (newMode == WirelessMode::Tx) ? static_cast<Port *>(node->outputPort())
496 : nullptr;
497 if (port) {
498 for (auto *conn : port->connections()) {
499 wirelessConnsToDelete.append(static_cast<QGraphicsItem *>(conn));
500 }
501 }
502
503 node->setWirelessMode(newMode);
504 }
505 }
506
507 // Push an undo command so Ctrl+Z in the GUI can revert MCP-applied property changes.
508 // When wireless mode changes sever connections, group the property update and the
509 // delete into a single undo macro so both are undone together.
510 if (!newProperties.isEmpty()) {
511 const bool needsMacro = !wirelessConnsToDelete.isEmpty();
512 auto *scene = currentScene();
513 if (scene) {
514 if (needsMacro) {
515 scene->undoStack()->beginMacro(QStringLiteral("Change wireless mode"));
516 }
517 scene->receiveCommand(new UpdateCommand({element}, oldData, scene));
518 if (needsMacro) {
519 scene->receiveCommand(new DeleteItemsCommand(wirelessConnsToDelete, scene));
520 scene->undoStack()->endMacro();
521 }
522 }
523 }
524
525 result["old_properties"] = oldProperties;
526 result["new_properties"] = newProperties;
527
528 return createSuccessResponse(result, requestId);
529}
530
531QJsonObject ElementHandler::handleSetInputValue(const QJsonObject &params, const QJsonValue &requestId)
532{
533 if (!validateParameters(params, {"element_id", "value"})) {
534 return createErrorResponse("Missing required parameters: element_id, value", requestId, JsonRpcError::InvalidParams);
535 }
536
537 QString errorMsg;
538 auto *element = validatedElement(params, "element_id", errorMsg);
539 if (!element) {
540 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
541 }
542 const bool value = params.value("value").toBool();
543
544 auto *inputElement = dynamic_cast<GraphicElementInput *>(element);
545 if (inputElement) {
546 inputElement->setOn(value);
547
548 Scene *scene = currentScene();
549 if (scene && scene->simulation()) {
550 scene->simulation()->update();
551 }
552 } else {
553 return createErrorResponse("Element is not an input element", requestId, JsonRpcError::ValidationError);
554 }
555
556 return createSuccessResponse(QJsonObject(), requestId);
557}
558
559QJsonObject ElementHandler::handleGetOutputValue(const QJsonObject &params, const QJsonValue &requestId)
560{
561 if (!validateParameters(params, {"element_id"})) {
562 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
563 }
564
565 QString errorMsg;
566 auto *element = validatedElement(params, "element_id", errorMsg);
567 if (!element) {
568 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
569 }
570
571 // Optional port index (defaults to 0).
572 int portIndex = 0;
573 if (params.contains("port")) {
574 if (!validateNonNegativeInteger(params.value("port"), "port", errorMsg)) {
575 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
576 }
577 portIndex = params.value("port").toInt();
578 }
579
580 QJsonObject result;
581
582 auto *inputElement = dynamic_cast<GraphicElementInput *>(element);
583 if (inputElement) {
584 result["value"] = inputElement->isOn();
585 } else {
586 ElementGroup group = element->elementGroup();
587
588 if (group == ElementGroup::Output) {
589 if (!validatePortRange(element, portIndex, false, "port", errorMsg)) {
590 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
591 }
592 InputPort *inPort = element->inputPort(portIndex);
593 result["value"] = inPort ? (inPort->status() == Status::Active) : false;
594 result["status"] = statusName(inPort ? inPort->status() : Status::Unknown);
595 } else {
596 if (!validatePortRange(element, portIndex, true, "port", errorMsg)) {
597 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
598 }
599 OutputPort *outPort = element->outputPort(portIndex);
600 result["value"] = outPort ? (outPort->status() == Status::Active) : false;
601 result["status"] = statusName(outPort ? outPort->status() : Status::Unknown);
602 }
603 }
604
605 return createSuccessResponse(result, requestId);
606}
607
608QJsonObject ElementHandler::handleRotateElement(const QJsonObject &params, const QJsonValue &requestId)
609{
610 if (!validateParameters(params, {"element_id", "angle"})) {
611 return createErrorResponse("Missing required parameters: element_id, angle", requestId, JsonRpcError::InvalidParams);
612 }
613
614 QString errorMsg;
615 if (!validateNumeric(params.value("angle"), "angle", errorMsg)) {
616 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
617 }
618 int angle = params.value("angle").toInt();
619
620 auto *element = validatedElement(params, "element_id", errorMsg);
621 if (!element) {
622 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
623 }
624
625 // Fixed-graphic elements (inputs/outputs/displays) rotate by repositioning their ports,
626 // exactly as the GUI does — so rotation is valid for every element here.
627 Scene *scene = currentScene();
628 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
629 // this exact message) if there's no active scene, before this point can ever be reached.
630 if (!scene) {
631 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
632 }
633
634 // Normalize angle to 0-360
635 angle = ((angle % 360) + 360) % 360;
636
637 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
638 scene->receiveCommand(new RotateCommand({element}, angle, scene));
639 QJsonObject result;
640 result["element_id"] = element->id();
641 result["angle"] = angle;
642 return createSuccessResponse(result, requestId);
643 }, "rotate element", requestId);
644}
645
646QJsonObject ElementHandler::handleFlipElement(const QJsonObject &params, const QJsonValue &requestId)
647{
648 if (!validateParameters(params, {"element_id", "axis"})) {
649 return createErrorResponse("Missing required parameters: element_id, axis", requestId, JsonRpcError::InvalidParams);
650 }
651
652 QString errorMsg;
653 if (!validateNonNegativeInteger(params.value("axis"), "axis", errorMsg)) {
654 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
655 }
656 const int axis = params.value("axis").toInt();
657 if (axis != 0 && axis != 1) {
658 return createErrorResponse("axis must be 0 (horizontal) or 1 (vertical)",
660 }
661
662 auto *element = validatedElement(params, "element_id", errorMsg);
663 if (!element) {
664 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
665 }
666
667 Scene *scene = currentScene();
668 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
669 // this exact message) if there's no active scene, before this point can ever be reached.
670 if (!scene) {
671 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
672 }
673
674 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
675 scene->receiveCommand(new FlipCommand({element}, axis, scene));
676 QJsonObject result;
677 result["element_id"] = element->id();
678 result["axis"] = (axis == 0 ? "horizontal" : "vertical");
679 return createSuccessResponse(result, requestId);
680 }, "flip element", requestId);
681}
682
683QJsonObject ElementHandler::handleUpdateElement(const QJsonObject &params, const QJsonValue &requestId)
684{
685 // update_element is a thin wrapper around set_element_properties.
686 QJsonObject response = handleSetElementProperties(params, requestId);
687
688 // Propagate errors from the underlying handler.
689 if (response.contains("error")) {
690 return response;
691 }
692
693 // Return a simpler response format for update_element.
694 QJsonObject result;
695 result["element_id"] = params.value("element_id").toInt();
696
697 return createSuccessResponse(result, requestId);
698}
699
700QJsonObject ElementHandler::handleChangeInputSize(const QJsonObject &params, const QJsonValue &requestId)
701{
702 if (!validateParameters(params, {"element_id", "size"})) {
703 return createErrorResponse("Missing required parameters: element_id, size", requestId, JsonRpcError::InvalidParams);
704 }
705
706 QString errorMsg;
707 if (!validatePositiveInteger(params.value("size"), "size", errorMsg)) {
708 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
709 }
710 const int newSize = params.value("size").toInt();
711
712 auto *element = validatedElement(params, "element_id", errorMsg);
713 if (!element) {
714 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
715 }
716
717 if (newSize < element->minInputSize() || newSize > element->maxInputSize()) {
718 return createErrorResponse(QString("Invalid input size %1. Must be between %2 and %3") // LCOV_EXCL_LINE -- pattern 8: gcov misattributes this multi-line chained-.arg() call's first line even though it's genuinely reached (see testHandleChangeInputSizeRejectsOutOfRangeSize)
719 .arg(newSize).arg(element->minInputSize()).arg(element->maxInputSize()),
721 }
722
723 Scene *scene = currentScene();
724 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
725 // this exact message) if there's no active scene, before this point can ever be reached.
726 if (!scene) {
727 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
728 }
729
730 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
731 scene->receiveCommand(new ChangePortSizeCommand({element}, newSize, scene, true));
732 const QRectF bounds = element->boundingRect();
733 QJsonObject result;
734 result["element_id"] = element->id();
735 result["new_size"] = newSize;
736 result["width"] = bounds.width();
737 result["height"] = bounds.height();
738 return createSuccessResponse(result, requestId);
739 }, "change input size", requestId);
740}
741
742QJsonObject ElementHandler::handleChangeOutputSize(const QJsonObject &params, const QJsonValue &requestId)
743{
744 if (!validateParameters(params, {"element_id", "size"})) {
745 return createErrorResponse("Missing required parameters: element_id, size", requestId, JsonRpcError::InvalidParams);
746 }
747
748 QString errorMsg;
749 if (!validatePositiveInteger(params.value("size"), "size", errorMsg)) {
750 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
751 }
752 const int newSize = params.value("size").toInt();
753
754 auto *element = validatedElement(params, "element_id", errorMsg);
755 if (!element) {
756 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
757 }
758
759 if (newSize < element->minOutputSize() || newSize > element->maxOutputSize()) {
760 return createErrorResponse(QString("Invalid output size %1. Must be between %2 and %3") // LCOV_EXCL_LINE -- pattern 8: gcov misattributes this multi-line chained-.arg() call's first line even though it's genuinely reached (see testHandleChangeOutputSizeRejectsOutOfRangeSize)
761 .arg(newSize).arg(element->minOutputSize()).arg(element->maxOutputSize()),
763 }
764
765 Scene *scene = currentScene();
766 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
767 // this exact message) if there's no active scene, before this point can ever be reached.
768 if (!scene) {
769 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
770 }
771
772 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
773 scene->receiveCommand(new ChangePortSizeCommand({element}, newSize, scene, false));
774 const QRectF bounds = element->boundingRect();
775 QJsonObject result;
776 result["element_id"] = element->id();
777 result["new_size"] = newSize;
778 result["width"] = bounds.width();
779 result["height"] = bounds.height();
780 return createSuccessResponse(result, requestId);
781 }, "change output size", requestId);
782}
783
784QJsonObject ElementHandler::handleToggleTruthTableOutput(const QJsonObject &params, const QJsonValue &requestId)
785{
786 if (!validateParameters(params, {"element_id", "position"})) {
787 return createErrorResponse("Missing required parameters: element_id, position", requestId, JsonRpcError::InvalidParams);
788 }
789
790 QString errorMsg;
791 if (!validateNonNegativeInteger(params.value("position"), "position", errorMsg)) {
792 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
793 }
794 const int position = params.value("position").toInt();
795
796 auto *element = validatedElement(params, "element_id", errorMsg);
797 if (!element) {
798 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
799 }
800
801 auto *truthTable = dynamic_cast<TruthTable *>(element);
802 if (!truthTable) {
803 return createErrorResponse(QString("Element %1 is not a TruthTable").arg(element->id()),
805 }
806
807 // The key stores 256 rows per output; positions beyond the current
808 // outputs address nothing the UI can ever toggle (and ≥ 2048 would be an
809 // out-of-bounds write on the 2048-bit key — the command guards that too).
810 const int maxPosition = 256 * truthTable->outputSize();
811 if (position >= maxPosition) {
812 return createErrorResponse(QString("position %1 out of range: TruthTable %2 has %3 outputs (valid positions 0..%4)") // LCOV_EXCL_LINE -- pattern 8: gcov misattributes this multi-line chained-.arg() call's first line even though it's genuinely reached (see testHandleToggleTruthTableOutputRejectsOutOfRangePosition)
813 .arg(position).arg(element->id()).arg(truthTable->outputSize()).arg(maxPosition - 1),
815 }
816
817 Scene *scene = currentScene();
818 // Unreachable: validatedElement() above already fails (returning ElementNotFound, with
819 // this exact message) if there's no active scene, before this point can ever be reached.
820 if (!scene) {
821 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
822 }
823
824 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
825 // ToggleTruthTableOutputCommand flips one cell in the truth table's output column.
826 scene->receiveCommand(new ToggleTruthTableOutputCommand(truthTable, position, scene));
827 QJsonObject result;
828 result["element_id"] = element->id();
829 result["position"] = position;
830 return createSuccessResponse(result, requestId);
831 }, "toggle truth table output", requestId);
832}
833
834QJsonObject ElementHandler::handleMorphElement(const QJsonObject &params, const QJsonValue &requestId)
835{
836 if (!validateParameters(params, {"element_ids", "target_type"})) {
837 return createErrorResponse("Missing required parameters: element_ids, target_type", requestId, JsonRpcError::InvalidParams);
838 }
839
840 QString errorMsg;
841
842 // Validate element_ids is an array
843 if (!params.value("element_ids").isArray()) {
844 return createErrorResponse("element_ids must be an array", requestId, JsonRpcError::InvalidParams);
845 }
846
847 QJsonArray elementIdsArray = params.value("element_ids").toArray();
848 if (elementIdsArray.empty()) {
849 return createErrorResponse("element_ids array cannot be empty", requestId, JsonRpcError::InvalidParams);
850 }
851
852 // Validate target_type
853 if (!validateNonEmptyString(params.value("target_type"), "target_type", errorMsg)) {
854 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
855 }
856
857 QString typeStr = params.value("target_type").toString();
858 ElementType targetType = ElementFactory::textToType(typeStr);
859 if (targetType == ElementType::Unknown) {
860 return createErrorResponse(QString("Invalid target element type: %1").arg(typeStr),
862 }
863
864 // Collect elements to morph
865 QList<GraphicElement *> elementsToMorph;
866 for (const QJsonValue &idValue : std::as_const(elementIdsArray)) {
867 if (!idValue.isDouble()) {
868 return createErrorResponse("element_ids must contain only integers", requestId, JsonRpcError::InvalidParams);
869 }
870
871 int elementId = idValue.toInt();
872 if (!validateElementId(elementId, "element_ids", errorMsg)) {
873 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
874 }
875
876 auto *item = currentScene()->itemById(elementId);
877 auto *element = dynamic_cast<GraphicElement *>(item);
878 if (!element) {
879 return createErrorResponse(QString("Item %1 is not a graphic element").arg(elementId),
881 }
882
883 elementsToMorph.append(element);
884 }
885
886 Scene *scene = currentScene();
887 // Unreachable: elementIdsArray is non-empty (checked above), so the loop above already
888 // calls validateElementId() at least once, which fails first if there's no active scene.
889 if (!scene) {
890 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable); // LCOV_EXCL_LINE
891 }
892
893 // Capture IDs before morph — element pointers become invalid after receiveCommand.
894 QJsonArray morphedIds;
895 for (const auto *element : elementsToMorph) {
896 morphedIds.append(element->id());
897 }
898
899 return tryCommand([&]() -> QJsonObject { // LCOV_EXCL_LINE -- pattern 45: gcov misattributes this multi-line lambda-taking call's entry; the lambda body below is genuinely covered
900 scene->receiveCommand(new MorphCommand(elementsToMorph, targetType, scene));
901 // MorphCommand preserves element IDs via updateItemId(), so the morphed
902 // elements keep their original IDs.
903 QJsonObject result;
904 result["morphed_elements"] = morphedIds;
905 result["target_type"] = typeStr;
906 return createSuccessResponse(result, requestId);
907 }, "morph elements", requestId);
908}
All QUndoCommand subclasses and the CommandUtils helper namespace.
Common logging utilities, the Pandaception error type, and helper macros.
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.
Central enumeration types for element types, groups, and signal status.
Enums::ElementType ElementType
Definition Enums.h:107
Enums::Status Status
Definition Enums.h:106
Enums::WirelessMode WirelessMode
Definition Enums.h:109
Enums::ElementGroup ElementGroup
Definition Enums.h:108
Abstract base class for user-controllable input elements.
Abstract base class for all graphical circuit elements.
Graphic element for a wire junction node.
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.
Synchronous cycle-based simulation engine with event-driven clock support.
Graphic element for a user-programmable truth table.
bool validatePortRange(GraphicElement *element, int portIndex, bool isOutput, const QString &paramName, QString &errorMsg) const
QJsonObject createSuccessResponse(const QJsonObject &result={}, const QJsonValue &requestId=QJsonValue()) const
QJsonObject tryCommand(Fn &&fn, const QString &action, const QJsonValue &requestId=QJsonValue())
Wraps fn in a try/catch, returning an error response on exception.
Definition BaseHandler.h:55
bool validateElementId(int elementId, const QString &paramName, QString &errorMsg) const
GraphicElement * validatedElement(const QJsonObject &params, const QString &paramName, QString &errorMsg)
Validates paramName in params, looks up the element, and returns it.
bool validateNonNegativeInteger(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
BaseHandler(MainWindow *mainWindow, const MCPValidator *validator)
QJsonObject createErrorResponse(const QString &error, const QJsonValue &requestId=QJsonValue(), int code=JsonRpcError::InternalError) const
bool validateParameters(const QJsonObject &params, const QStringList &required) const
bool validatePositiveInteger(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
bool validateNonEmptyString(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
bool validateNumeric(const QJsonValue &value, const QString &paramName, QString &errorMsg) const
Scene * currentScene()
static QString typeToText(const ElementType type)
Converts type to its internal name string.
static ElementType textToType(const QString &text)
Converts an element type name string to its ElementType enum value.
static GraphicElement * buildElement(const ElementType type)
Constructs and returns a new graphic element of the given type.
ElementHandler(MainWindow *mainWindow, const MCPValidator *validator)
QJsonObject handleCommand(const QString &command, const QJsonObject &params, const QJsonValue &requestId) override
virtual void setFrequency(const double freq)
Sets the clock frequency to freq (overridden by clock elements).
ElementType elementType() const
Returns the type identifier for this element.
bool hasDelay() const
Returns true if this element type exposes a configurable clock phase delay.
virtual void setColor(const QString &color)
Sets the element color to color and refreshes the pixmap.
bool hasVolume() const
Returns true if this element type supports volume control.
virtual void setAudio(const QString &audio)
Sets the audio file associated with this element to audio.
int inputSize() const
Returns the current number of input ports.
qreal rotation() const
Returns the current rotation angle of this element in degrees.
void setRotation(const qreal angle)
Rotates the element to angle degrees and updates port positions.
QString label() const
Returns the user-visible label text for this element.
int maxOutputSize() const
Returns the maximum allowed number of output ports.
void setTrigger(const QKeySequence &trigger)
Sets the keyboard shortcut to trigger and updates the label.
InputPort * inputPort(const int index=0) const
Returns the input port at index (default 0).
void setAppearanceAt(const int index, const QString &fileName)
Sets a custom appearance at a specific index in the appearance list.
int minInputSize() const
Returns the minimum allowed number of input ports.
virtual QString audio() const
Returns the name of the audio file currently associated with this element.
virtual double delay() const
Returns the clock phase delay in seconds (overridden by Clock; returns 0 for other elements).
virtual double frequency() const
Returns the clock frequency in Hz (overridden by Clock; returns 0 for other elements).
QKeySequence trigger() const
Returns the keyboard shortcut that activates this element.
void setLabel(const QString &label)
Sets the label text to label and refreshes the display.
virtual QString color() const
Returns the name of the color currently applied to this element.
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).
bool hasFrequency() const
Returns true if this element type exposes a configurable clock frequency.
bool hasColors() const
Returns true if this element type supports color selection.
ElementGroup elementGroup() const
Returns the group this element belongs to.
int maxInputSize() const
Returns the maximum allowed number of input ports.
virtual void save(QDataStream &stream, SerializationOptions options) const
virtual void setDelay(const double delay)
Sets the clock phase delay to delay (overridden by clock elements).
bool canChangeAppearance() const
Returns true if the user is allowed to choose a custom appearance for this element.
virtual void setAppearance(const bool defaultAppearance, const QString &fileName)
Switches the element's appearance.
int minOutputSize() const
Returns the minimum allowed number of output ports.
virtual void setVolume(float vol)
Sets the audio playback volume to vol (0.0–1.0).
QRectF boundingRect() const override
Returns the bounding rectangle of this element in local coordinates.
virtual float volume() const
Returns the audio playback volume (0.0–1.0).
bool hasAudio() const
Returns true if this element type supports audio output.
bool hasTrigger() const
Returns true if this element type supports a keyboard trigger.
int id() const
Returns the unique integer identifier of this item, or -1 if unassigned.
Definition ItemWithId.h:40
JSON Schema validator for MCP commands and responses using native json-schema-validator.
The top-level application window hosting the tab bar, menus, element palette, and editor.
Definition MainWindow.h:46
Status status() const
Returns the current logical status (Active/Inactive/Unknown/Error).
Definition Port.h:84
const QList< Connection * > & connections() const
Returns the list of wires attached to this port.
Definition Port.cpp:57
const QVector< GraphicElement * > elements() const
Returns all graphic elements in the scene.
Definition Scene.cpp:336
void receiveCommand(QUndoCommand *cmd)
Pushes cmd onto the undo stack (immediately executes its redo()).
Definition Scene.cpp:520
ItemWithId * itemById(int id) const
Returns the item registered under id, or nullptr if not found.
Definition Scene.cpp:171
Simulation * simulation()
Returns the simulation engine associated with this scene.
Definition Scene.cpp:288
QUndoStack * undoStack()
Returns the scene's undo stack.
Definition Scene.cpp:672
static void writePandaHeader(QDataStream &stream)
Writes the .panda circuit file header to stream.
void update()
Executes one simulation step (used by tests to advance the simulation manually).
constexpr int gridSize
Scene grid unit in pixels (elements snap to gridSize/2).
Definition Constants.h:12
constexpr int SceneNotAvailable
No active circuit scene to operate on.
constexpr int MethodNotFound
The requested method does not exist or is unavailable.
constexpr int OperationFailed
Generic Qt API / handler operation failure (catch-all for tryCommand).
constexpr int ElementNotFound
Referenced element id does not exist in the scene.
constexpr int ValidationError
Semantic validation failure (e.g. port index out of range, enum value not allowed).
constexpr int InvalidParams
Invalid method parameters (missing required, wrong type, etc.).