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 try {
103 element = ElementFactory::buildElement(type);
104 } catch (const std::exception &e) {
105 return createErrorResponse(QString("Failed to create element of type %1: %2").arg(typeStr, e.what()),
107 } catch (...) {
108 return createErrorResponse(QString("Failed to create element of type: %1").arg(typeStr),
110 }
111
112 if (!element) {
113 return createErrorResponse(QString("Failed to create element of type: %1").arg(typeStr),
115 }
116
117 element->setPos(x, y);
118 if (!label.isEmpty()) {
119 element->setLabel(label);
120 }
121
122 // Match the interactive drag-and-drop path (SceneDropHandler::handleDrop): clear the
123 // previous selection before adding the new element so it alone ends up selected,
124 // rather than accumulating onto every element ever created in this session. Without
125 // this, a long MCP-driven build (many create_element calls with nothing else ever
126 // clearing selection) leaves the entire circuit selected, and every selection-tracking
127 // consumer (e.g. ElementEditor::selectionChanged) pays for the whole growing selection
128 // on every single element added — O(N) per call, O(N^2) over the full build.
129 scene->clearSelection();
130
131 try {
132 scene->receiveCommand(new AddItemsCommand({element}, scene));
133 } catch (const std::exception &e) {
134 delete element;
135 return createErrorResponse(QString("Failed to add element to scene: %1").arg(e.what()),
137 } catch (...) {
138 delete element;
139 return createErrorResponse("Failed to add element to scene: Unknown exception",
141 }
142
143 const QRectF bounds = element->boundingRect();
144
145 QJsonObject result;
146 result["element_id"] = element->id();
147 result["width"] = bounds.width();
148 result["height"] = bounds.height();
149
150 return createSuccessResponse(result, requestId);
151}
152
153QJsonObject ElementHandler::handleDeleteElement(const QJsonObject &params, const QJsonValue &requestId)
154{
155 if (!validateParameters(params, {"element_id"})) {
156 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
157 }
158
159 QString errorMsg;
160 auto *element = validatedElement(params, "element_id", errorMsg);
161 if (!element) {
162 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
163 }
164
165 Scene *scene = currentScene();
166 if (!scene) {
167 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
168 }
169
170 return tryCommand([&] {
171 scene->receiveCommand(new DeleteItemsCommand({element}, scene));
172 return createSuccessResponse(QJsonObject(), requestId);
173 }, "delete element", requestId);
174}
175
176QJsonObject ElementHandler::handleListElements(const QJsonObject &, const QJsonValue &requestId)
177{
178 Scene *scene = currentScene();
179 if (!scene) {
180 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
181 }
182
183 QJsonArray elements;
184 const auto sceneElements = scene->elements();
185
186 for (const auto *element : sceneElements) {
187 const QRectF br = element->boundingRect();
188 QJsonObject elementObj;
189 elementObj["element_id"] = element->id();
190 elementObj["type"] = ElementFactory::typeToText(element->elementType());
191 elementObj["x"] = element->pos().x();
192 elementObj["y"] = element->pos().y();
193 elementObj["width"] = br.width();
194 elementObj["height"] = br.height();
195 elementObj["label"] = element->label();
196 elementObj["rotation"] = element->rotation();
197 elementObj["input_size"] = element->inputSize();
198 elementObj["output_size"] = element->outputSize();
199
200 if (element->hasColors()) {
201 elementObj["color"] = element->color();
202 }
203 if (element->hasFrequency()) {
204 elementObj["frequency"] = element->frequency();
205 }
206 if (element->hasDelay()) {
207 elementObj["delay"] = element->delay();
208 }
209 if (element->hasTrigger()) {
210 elementObj["trigger"] = element->trigger().toString();
211 }
212 if (element->hasAudio()) {
213 elementObj["audio"] = element->audio();
214 }
215 if (element->hasVolume()) {
216 elementObj["volume"] = static_cast<double>(element->volume());
217 }
218 if (const auto *inputElm = qobject_cast<const GraphicElementInput *>(element)) {
219 elementObj["locked"] = inputElm->isLocked();
220 }
221
222 elements.append(elementObj);
223 }
224
225 QJsonObject result;
226 result["elements"] = elements;
227
228 return createSuccessResponse(result, requestId);
229}
230
231QJsonObject ElementHandler::handleMoveElement(const QJsonObject &params, const QJsonValue &requestId)
232{
233 if (!validateParameters(params, {"element_id", "x", "y"})) {
234 return createErrorResponse("Missing required parameters: element_id, x, y", requestId, JsonRpcError::InvalidParams);
235 }
236
237 QString errorMsg;
238 if (!validateNumeric(params.value("x"), "x", errorMsg)) {
239 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
240 }
241 if (!validateNumeric(params.value("y"), "y", errorMsg)) {
242 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
243 }
244
245 const int snap = Constants::gridSize / 2;
246 const int x = qRound(params.value("x").toDouble() / snap) * snap;
247 const int y = qRound(params.value("y").toDouble() / snap) * snap;
248
249 auto *element = validatedElement(params, "element_id", errorMsg);
250 if (!element) {
251 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
252 }
253
254 Scene *scene = currentScene();
255 if (!scene) {
256 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
257 }
258
259 QPointF oldPos = element->pos();
260 QPointF newPos(x, y);
261
262 element->setPos(newPos);
263
264 QList<GraphicElement *> elements = {element};
265 QList<QPointF> oldPositions = {oldPos};
266
267 try {
268 scene->receiveCommand(new MoveCommand(elements, oldPositions, scene));
269 } catch (const std::exception &e) {
270 element->setPos(oldPos);
271 return createErrorResponse(QString("Failed to move element: %1").arg(e.what()),
273 } catch (...) {
274 element->setPos(oldPos);
275 return createErrorResponse("Failed to move element: Unknown exception",
277 }
278
279 QJsonObject result;
280 result["old_position"] = QJsonObject{{"x", oldPos.x()}, {"y", oldPos.y()}};
281 result["new_position"] = QJsonObject{{"x", newPos.x()}, {"y", newPos.y()}};
282
283 return createSuccessResponse(result, requestId);
284}
285
286QJsonObject ElementHandler::handleSetElementProperties(const QJsonObject &params, const QJsonValue &requestId)
287{
288 if (!validateParameters(params, {"element_id"})) {
289 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
290 }
291
292 QString errorMsg;
293 auto *element = validatedElement(params, "element_id", errorMsg);
294 if (!element) {
295 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
296 }
297
298 // Reject structural changes before any mutation — these require dedicated commands.
299 if (params.contains("input_size") || params.contains("output_size")) {
300 return createErrorResponse("Use change_input_size / change_output_size commands to change port counts",
302 }
303
304 // Snapshot current state before any mutation so UpdateCommand can restore it on undo.
305 QByteArray oldData;
306 {
307 QDataStream stream(&oldData, QIODevice::WriteOnly);
309 element->save(stream, {.purpose = SerializationPurpose::InMemorySnapshot});
310 }
311
312 QJsonObject result;
313 QJsonObject oldProperties;
314 QJsonObject newProperties;
315
316 if (params.contains("label")) {
317 QString oldLabel = element->label();
318 QString newLabel = params.value("label").toString();
319
320 oldProperties["label"] = oldLabel;
321 newProperties["label"] = newLabel;
322
323 element->setLabel(newLabel);
324 }
325
326 if (params.contains("color") && element->hasColors()) {
327 QString oldColor = element->color();
328 QString newColor = params.value("color").toString();
329
330 oldProperties["color"] = oldColor;
331 newProperties["color"] = newColor;
332
333 element->setColor(newColor);
334 }
335
336 if (params.contains("frequency") && element->hasFrequency()) {
337 if (!validateNumeric(params.value("frequency"), "frequency", errorMsg)) {
338 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
339 }
340 double oldFreq = element->frequency();
341 double newFreq = params.value("frequency").toDouble();
342 if (newFreq <= 0) {
343 return createErrorResponse("Parameter 'frequency' must be a positive value", requestId, JsonRpcError::ValidationError);
344 }
345
346 oldProperties["frequency"] = oldFreq;
347 newProperties["frequency"] = newFreq;
348
349 element->setFrequency(newFreq);
350 }
351
352 if (params.contains("rotation")) {
353 qreal oldRotation = element->rotation();
354 qreal newRotation = params.value("rotation").toDouble();
355
356 oldProperties["rotation"] = oldRotation;
357 newProperties["rotation"] = newRotation;
358
359 element->setRotation(newRotation);
360 }
361
362 if (params.contains("delay") && element->hasDelay()) {
363 if (!validateNumeric(params.value("delay"), "delay", errorMsg)) {
364 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
365 }
366 double oldDelay = element->delay();
367 double newDelay = params.value("delay").toDouble();
368 if (newDelay < -1.0 || newDelay > 1.0) {
369 return createErrorResponse("Parameter 'delay' must be in [-1, 1] (fraction of period; negative advances the clock)",
371 }
372
373 oldProperties["delay"] = oldDelay;
374 newProperties["delay"] = newDelay;
375
376 element->setDelay(newDelay);
377 }
378
379 if (params.contains("trigger") && element->hasTrigger()) {
380 QString oldTrigger = element->trigger().toString();
381 QString newTrigger = params.value("trigger").toString();
382
383 oldProperties["trigger"] = oldTrigger;
384 newProperties["trigger"] = newTrigger;
385
386 element->setTrigger(QKeySequence(newTrigger));
387 }
388
389 if (params.contains("audio") && element->hasAudio()) {
390 QString oldAudio = element->audio();
391 QString newAudio = params.value("audio").toString();
392
393 oldProperties["audio"] = oldAudio;
394 newProperties["audio"] = newAudio;
395
396 element->setAudio(newAudio);
397 }
398
399 if (params.contains("locked")) {
400 auto *inputElm = qobject_cast<GraphicElementInput *>(element);
401 if (inputElm) {
402 bool oldLocked = inputElm->isLocked();
403 bool newLocked = params.value("locked").toBool();
404
405 oldProperties["locked"] = oldLocked;
406 newProperties["locked"] = newLocked;
407
408 inputElm->setLocked(newLocked);
409 }
410 }
411
412 if (params.contains("volume") && element->hasVolume()) {
413 if (!validateNumeric(params.value("volume"), "volume", errorMsg)) {
414 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
415 }
416 float oldVolume = element->volume();
417 float newVolume = static_cast<float>(params.value("volume").toDouble());
418 if (newVolume < 0.0f || newVolume > 1.0f) {
419 return createErrorResponse("Parameter 'volume' must be between 0.0 and 1.0", requestId, JsonRpcError::ValidationError);
420 }
421
422 oldProperties["volume"] = static_cast<double>(oldVolume);
423 newProperties["volume"] = static_cast<double>(newVolume);
424
425 element->setVolume(newVolume);
426 }
427
428 if (params.contains("appearance") && element->canChangeAppearance()) {
429 QString appearancePath = params.value("appearance").toString();
430 bool useDefault = appearancePath.isEmpty();
431 int appearanceIndex = params.contains("appearance_index") ? params.value("appearance_index").toInt() : -1;
432
433 newProperties["appearance"] = appearancePath;
434 newProperties["appearance_default"] = useDefault;
435
436 if (appearanceIndex >= 0) {
437 // Set appearance for a specific index directly (e.g., LED state)
438 element->setAppearanceAt(appearanceIndex, appearancePath);
439 newProperties["appearance_index"] = appearanceIndex;
440 } else {
441 element->setAppearance(useDefault, appearancePath);
442 }
443 }
444
445 // Wireless mode is only meaningful for Node elements; non-Nodes are silently ignored.
446 QList<QGraphicsItem *> wirelessConnsToDelete;
447 if (params.contains("wireless_mode")) {
448 if (auto *node = qobject_cast<Node *>(element)) {
449 int modeInt = params.value("wireless_mode").toInt();
450 if (modeInt < 0 || modeInt > 2) {
451 return createErrorResponse("Invalid wireless_mode. Must be 0 (None), 1 (Tx), or 2 (Rx)",
453 }
454 auto oldMode = static_cast<int>(node->wirelessMode());
455 auto newMode = static_cast<WirelessMode>(modeInt);
456
457 oldProperties["wireless_mode"] = oldMode;
458 newProperties["wireless_mode"] = modeInt;
459
460 // Collect connections on the port that will be hidden, so they can
461 // be deleted in an undo macro (same pattern as ElementEditor::apply).
462 Port *port = (newMode == WirelessMode::Rx) ? static_cast<Port *>(node->inputPort())
463 : (newMode == WirelessMode::Tx) ? static_cast<Port *>(node->outputPort())
464 : nullptr;
465 if (port) {
466 for (auto *conn : port->connections()) {
467 wirelessConnsToDelete.append(static_cast<QGraphicsItem *>(conn));
468 }
469 }
470
471 node->setWirelessMode(newMode);
472 }
473 }
474
475 // Push an undo command so Ctrl+Z in the GUI can revert MCP-applied property changes.
476 // When wireless mode changes sever connections, group the property update and the
477 // delete into a single undo macro so both are undone together.
478 if (!newProperties.isEmpty()) {
479 const bool needsMacro = !wirelessConnsToDelete.isEmpty();
480 auto *scene = currentScene();
481 if (scene) {
482 if (needsMacro) {
483 scene->undoStack()->beginMacro(QStringLiteral("Change wireless mode"));
484 }
485 scene->receiveCommand(new UpdateCommand({element}, oldData, scene));
486 if (needsMacro) {
487 scene->receiveCommand(new DeleteItemsCommand(wirelessConnsToDelete, scene));
488 scene->undoStack()->endMacro();
489 }
490 }
491 }
492
493 result["old_properties"] = oldProperties;
494 result["new_properties"] = newProperties;
495
496 return createSuccessResponse(result, requestId);
497}
498
499QJsonObject ElementHandler::handleSetInputValue(const QJsonObject &params, const QJsonValue &requestId)
500{
501 if (!validateParameters(params, {"element_id", "value"})) {
502 return createErrorResponse("Missing required parameters: element_id, value", requestId, JsonRpcError::InvalidParams);
503 }
504
505 QString errorMsg;
506 auto *element = validatedElement(params, "element_id", errorMsg);
507 if (!element) {
508 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
509 }
510 const bool value = params.value("value").toBool();
511
512 auto *inputElement = dynamic_cast<GraphicElementInput *>(element);
513 if (inputElement) {
514 inputElement->setOn(value);
515
516 Scene *scene = currentScene();
517 if (scene && scene->simulation()) {
518 scene->simulation()->update();
519 }
520 } else {
521 return createErrorResponse("Element is not an input element", requestId, JsonRpcError::ValidationError);
522 }
523
524 return createSuccessResponse(QJsonObject(), requestId);
525}
526
527QJsonObject ElementHandler::handleGetOutputValue(const QJsonObject &params, const QJsonValue &requestId)
528{
529 if (!validateParameters(params, {"element_id"})) {
530 return createErrorResponse("Missing required parameter: element_id", requestId, JsonRpcError::InvalidParams);
531 }
532
533 QString errorMsg;
534 auto *element = validatedElement(params, "element_id", errorMsg);
535 if (!element) {
536 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
537 }
538
539 // Optional port index (defaults to 0).
540 int portIndex = 0;
541 if (params.contains("port")) {
542 if (!validateNonNegativeInteger(params.value("port"), "port", errorMsg)) {
543 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
544 }
545 portIndex = params.value("port").toInt();
546 }
547
548 QJsonObject result;
549
550 auto *inputElement = dynamic_cast<GraphicElementInput *>(element);
551 if (inputElement) {
552 result["value"] = inputElement->isOn();
553 } else {
554 ElementGroup group = element->elementGroup();
555
556 if (group == ElementGroup::Output) {
557 if (!validatePortRange(element, portIndex, false, "port", errorMsg)) {
558 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
559 }
560 InputPort *inPort = element->inputPort(portIndex);
561 result["value"] = inPort ? (inPort->status() == Status::Active) : false;
562 } else {
563 if (!validatePortRange(element, portIndex, true, "port", errorMsg)) {
564 return createErrorResponse(errorMsg, requestId, JsonRpcError::ValidationError);
565 }
566 OutputPort *outPort = element->outputPort(portIndex);
567 result["value"] = outPort ? (outPort->status() == Status::Active) : false;
568 }
569 }
570
571 return createSuccessResponse(result, requestId);
572}
573
574QJsonObject ElementHandler::handleRotateElement(const QJsonObject &params, const QJsonValue &requestId)
575{
576 if (!validateParameters(params, {"element_id", "angle"})) {
577 return createErrorResponse("Missing required parameters: element_id, angle", requestId, JsonRpcError::InvalidParams);
578 }
579
580 QString errorMsg;
581 if (!validateNumeric(params.value("angle"), "angle", errorMsg)) {
582 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
583 }
584 int angle = params.value("angle").toInt();
585
586 auto *element = validatedElement(params, "element_id", errorMsg);
587 if (!element) {
588 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
589 }
590
591 // Fixed-graphic elements (inputs/outputs/displays) rotate by repositioning their ports,
592 // exactly as the GUI does — so rotation is valid for every element here.
593 Scene *scene = currentScene();
594 if (!scene) {
595 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
596 }
597
598 // Normalize angle to 0-360
599 angle = ((angle % 360) + 360) % 360;
600
601 return tryCommand([&]() -> QJsonObject {
602 scene->receiveCommand(new RotateCommand({element}, angle, scene));
603 QJsonObject result;
604 result["element_id"] = element->id();
605 result["angle"] = angle;
606 return createSuccessResponse(result, requestId);
607 }, "rotate element", requestId);
608}
609
610QJsonObject ElementHandler::handleFlipElement(const QJsonObject &params, const QJsonValue &requestId)
611{
612 if (!validateParameters(params, {"element_id", "axis"})) {
613 return createErrorResponse("Missing required parameters: element_id, axis", requestId, JsonRpcError::InvalidParams);
614 }
615
616 QString errorMsg;
617 if (!validateNonNegativeInteger(params.value("axis"), "axis", errorMsg)) {
618 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
619 }
620 const int axis = params.value("axis").toInt();
621 if (axis != 0 && axis != 1) {
622 return createErrorResponse("axis must be 0 (horizontal) or 1 (vertical)",
624 }
625
626 auto *element = validatedElement(params, "element_id", errorMsg);
627 if (!element) {
628 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
629 }
630
631 Scene *scene = currentScene();
632 if (!scene) {
633 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
634 }
635
636 return tryCommand([&]() -> QJsonObject {
637 scene->receiveCommand(new FlipCommand({element}, axis, scene));
638 QJsonObject result;
639 result["element_id"] = element->id();
640 result["axis"] = (axis == 0 ? "horizontal" : "vertical");
641 return createSuccessResponse(result, requestId);
642 }, "flip element", requestId);
643}
644
645QJsonObject ElementHandler::handleUpdateElement(const QJsonObject &params, const QJsonValue &requestId)
646{
647 // update_element is a thin wrapper around set_element_properties.
648 QJsonObject response = handleSetElementProperties(params, requestId);
649
650 // Propagate errors from the underlying handler.
651 if (response.contains("error")) {
652 return response;
653 }
654
655 // Return a simpler response format for update_element.
656 QJsonObject result;
657 result["element_id"] = params.value("element_id").toInt();
658
659 return createSuccessResponse(result, requestId);
660}
661
662QJsonObject ElementHandler::handleChangeInputSize(const QJsonObject &params, const QJsonValue &requestId)
663{
664 if (!validateParameters(params, {"element_id", "size"})) {
665 return createErrorResponse("Missing required parameters: element_id, size", requestId, JsonRpcError::InvalidParams);
666 }
667
668 QString errorMsg;
669 if (!validatePositiveInteger(params.value("size"), "size", errorMsg)) {
670 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
671 }
672 const int newSize = params.value("size").toInt();
673
674 auto *element = validatedElement(params, "element_id", errorMsg);
675 if (!element) {
676 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
677 }
678
679 if (newSize < element->minInputSize() || newSize > element->maxInputSize()) {
680 return createErrorResponse(QString("Invalid input size %1. Must be between %2 and %3")
681 .arg(newSize).arg(element->minInputSize()).arg(element->maxInputSize()),
683 }
684
685 Scene *scene = currentScene();
686 if (!scene) {
687 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
688 }
689
690 return tryCommand([&]() -> QJsonObject {
691 scene->receiveCommand(new ChangePortSizeCommand({element}, newSize, scene, true));
692 const QRectF bounds = element->boundingRect();
693 QJsonObject result;
694 result["element_id"] = element->id();
695 result["new_size"] = newSize;
696 result["width"] = bounds.width();
697 result["height"] = bounds.height();
698 return createSuccessResponse(result, requestId);
699 }, "change input size", requestId);
700}
701
702QJsonObject ElementHandler::handleChangeOutputSize(const QJsonObject &params, const QJsonValue &requestId)
703{
704 if (!validateParameters(params, {"element_id", "size"})) {
705 return createErrorResponse("Missing required parameters: element_id, size", requestId, JsonRpcError::InvalidParams);
706 }
707
708 QString errorMsg;
709 if (!validatePositiveInteger(params.value("size"), "size", errorMsg)) {
710 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
711 }
712 const int newSize = params.value("size").toInt();
713
714 auto *element = validatedElement(params, "element_id", errorMsg);
715 if (!element) {
716 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
717 }
718
719 if (newSize < element->minOutputSize() || newSize > element->maxOutputSize()) {
720 return createErrorResponse(QString("Invalid output size %1. Must be between %2 and %3")
721 .arg(newSize).arg(element->minOutputSize()).arg(element->maxOutputSize()),
723 }
724
725 Scene *scene = currentScene();
726 if (!scene) {
727 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
728 }
729
730 return tryCommand([&]() -> QJsonObject {
731 scene->receiveCommand(new ChangePortSizeCommand({element}, newSize, scene, false));
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 output size", requestId);
740}
741
742QJsonObject ElementHandler::handleToggleTruthTableOutput(const QJsonObject &params, const QJsonValue &requestId)
743{
744 if (!validateParameters(params, {"element_id", "position"})) {
745 return createErrorResponse("Missing required parameters: element_id, position", requestId, JsonRpcError::InvalidParams);
746 }
747
748 QString errorMsg;
749 if (!validateNonNegativeInteger(params.value("position"), "position", errorMsg)) {
750 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
751 }
752 const int position = params.value("position").toInt();
753
754 auto *element = validatedElement(params, "element_id", errorMsg);
755 if (!element) {
756 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
757 }
758
759 auto *truthTable = dynamic_cast<TruthTable *>(element);
760 if (!truthTable) {
761 return createErrorResponse(QString("Element %1 is not a TruthTable").arg(element->id()),
763 }
764
765 // The key stores 256 rows per output; positions beyond the current
766 // outputs address nothing the UI can ever toggle (and ≥ 2048 would be an
767 // out-of-bounds write on the 2048-bit key — the command guards that too).
768 const int maxPosition = 256 * truthTable->outputSize();
769 if (position >= maxPosition) {
770 return createErrorResponse(QString("position %1 out of range: TruthTable %2 has %3 outputs (valid positions 0..%4)")
771 .arg(position).arg(element->id()).arg(truthTable->outputSize()).arg(maxPosition - 1),
773 }
774
775 Scene *scene = currentScene();
776 if (!scene) {
777 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
778 }
779
780 return tryCommand([&]() -> QJsonObject {
781 // ToggleTruthTableOutputCommand flips one cell in the truth table's output column.
782 scene->receiveCommand(new ToggleTruthTableOutputCommand(truthTable, position, scene));
783 QJsonObject result;
784 result["element_id"] = element->id();
785 result["position"] = position;
786 return createSuccessResponse(result, requestId);
787 }, "toggle truth table output", requestId);
788}
789
790QJsonObject ElementHandler::handleMorphElement(const QJsonObject &params, const QJsonValue &requestId)
791{
792 if (!validateParameters(params, {"element_ids", "target_type"})) {
793 return createErrorResponse("Missing required parameters: element_ids, target_type", requestId, JsonRpcError::InvalidParams);
794 }
795
796 QString errorMsg;
797
798 // Validate element_ids is an array
799 if (!params.value("element_ids").isArray()) {
800 return createErrorResponse("element_ids must be an array", requestId, JsonRpcError::InvalidParams);
801 }
802
803 QJsonArray elementIdsArray = params.value("element_ids").toArray();
804 if (elementIdsArray.empty()) {
805 return createErrorResponse("element_ids array cannot be empty", requestId, JsonRpcError::InvalidParams);
806 }
807
808 // Validate target_type
809 if (!validateNonEmptyString(params.value("target_type"), "target_type", errorMsg)) {
810 return createErrorResponse(errorMsg, requestId, JsonRpcError::InvalidParams);
811 }
812
813 QString typeStr = params.value("target_type").toString();
814 ElementType targetType = ElementFactory::textToType(typeStr);
815 if (targetType == ElementType::Unknown) {
816 return createErrorResponse(QString("Invalid target element type: %1").arg(typeStr),
818 }
819
820 // Collect elements to morph
821 QList<GraphicElement *> elementsToMorph;
822 for (const QJsonValue &idValue : std::as_const(elementIdsArray)) {
823 if (!idValue.isDouble()) {
824 return createErrorResponse("element_ids must contain only integers", requestId, JsonRpcError::InvalidParams);
825 }
826
827 int elementId = idValue.toInt();
828 if (!validateElementId(elementId, "element_ids", errorMsg)) {
829 return createErrorResponse(errorMsg, requestId, JsonRpcError::ElementNotFound);
830 }
831
832 auto *item = currentScene()->itemById(elementId);
833 auto *element = dynamic_cast<GraphicElement *>(item);
834 if (!element) {
835 return createErrorResponse(QString("Item %1 is not a graphic element").arg(elementId),
837 }
838
839 elementsToMorph.append(element);
840 }
841
842 Scene *scene = currentScene();
843 if (!scene) {
844 return createErrorResponse("No active circuit scene available", requestId, JsonRpcError::SceneNotAvailable);
845 }
846
847 // Capture IDs before morph — element pointers become invalid after receiveCommand.
848 QJsonArray morphedIds;
849 for (const auto *element : elementsToMorph) {
850 morphedIds.append(element->id());
851 }
852
853 return tryCommand([&]() -> QJsonObject {
854 scene->receiveCommand(new MorphCommand(elementsToMorph, targetType, scene));
855 // MorphCommand preserves element IDs via updateItemId(), so the morphed
856 // elements keep their original IDs.
857 QJsonObject result;
858 result["morphed_elements"] = morphedIds;
859 result["target_type"] = typeStr;
860 return createSuccessResponse(result, requestId);
861 }, "morph elements", requestId);
862}
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::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:47
Status status() const
Returns the current logical status (Active/Inactive/Unknown/Error).
Definition Port.h:79
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.).