diff --git a/0.icon.json b/0.icon.json new file mode 100644 index 0000000..5575d4c --- /dev/null +++ b/0.icon.json @@ -0,0 +1,25 @@ +{ + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "0" + } + }, + "port_positions": { + "497b1f74-1186-471f-976a-36b07a451caf": [ + -8, + -8 + ] + } +} diff --git a/1.icon.json b/1.icon.json new file mode 100644 index 0000000..f6c0182 --- /dev/null +++ b/1.icon.json @@ -0,0 +1,25 @@ +{ + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "1" + } + }, + "port_positions": { + "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3": [ + -8, + -8 + ] + } +} diff --git a/C.icon.json b/C.icon.json new file mode 100644 index 0000000..8122bbb --- /dev/null +++ b/C.icon.json @@ -0,0 +1,29 @@ +{ + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64.0, + "height": 64.0, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "C" + } + }, + "port_positions": { + "c87881f1-23b4-4a69-b586-8e3c7bf6e21e": [ + -8, + -8 + ], + "c62de8eb-e13b-4849-bea5-c8cc5332269e": [ + 16, + -32 + ] + } +} diff --git a/I.icon.json b/I.icon.json new file mode 100644 index 0000000..ed73013 --- /dev/null +++ b/I.icon.json @@ -0,0 +1,29 @@ +{ + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "I" + } + }, + "port_positions": { + "0b9036b1-e4e4-437e-8c35-f5bb391b6cbd": [ + -8, + -8 + ], + "fb25f35d-4c0c-4cfa-92d4-1a18a71e2c11": [ + 16, + -32 + ] + } +} diff --git a/R.icon.json b/R.icon.json new file mode 100644 index 0000000..a1a464b --- /dev/null +++ b/R.icon.json @@ -0,0 +1,29 @@ +{ + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "R" + } + }, + "port_positions": { + "0ac7d4ea-77f7-4c0d-8e37-406ef66e4740": [ + -8, + -8 + ], + "d1f5aab2-7bff-4b1e-8697-98fb6c3d8f02": [ + 16, + -32 + ] + } +} diff --git a/src/bedit_gui/models.py b/src/bedit_gui/models.py index ba517a7..e5daddc 100644 --- a/src/bedit_gui/models.py +++ b/src/bedit_gui/models.py @@ -22,6 +22,8 @@ class Shape: def from_data(cls, data: Mapping[str, Any]) -> Shape: if cls is Shape and data.get("type") == "rectangle": return Rectangle.from_data(data) + if cls is Shape and data.get("type") == "text": + return Text.from_data(data) pos = data.get("pos", [0, 0]) return cls(layer=int(data.get("layer", 0)), type=data.get("type"), pos=(int(pos[0]), int(pos[1]))) @@ -59,14 +61,22 @@ class Rectangle(Shape): @dataclass class Text(Shape): type: str = field(init=False, default="text") - width: float = 100.0 - height: float = 100.0 + width: float = 32.0 + height: float = 16.0 color: str = "#000000ff" bold: bool = False italic: bool = False size: float = 16.0 text: str = "" + @classmethod + def from_data(cls, data: Mapping[str, Any]) -> Text: + pos = data.get("pos", [0, 0]) + return cls(layer=int(data.get("layer", 0)), pos=(int(pos[0]), int(pos[1])), width=float(data.get("width", 32.0)), height=float(data.get("height", 16.0)), color=str(data.get("color", "#000000ff")), bold=bool(data.get("bold", False)), italic=bool(data.get("italic", False)), size=float(data.get("size", 16.0)), text=str(data.get("text", ""))) + + def to_data(self) -> dict[str, Any]: + return {**super().to_data(), "width": self.width, "height": self.height, "color": self.color, "bold": self.bold, "italic": self.italic, "size": self.size, "text": self.text} + @dataclass class Icon: shapes: dict[ShapeID, Shape] = field(default_factory=dict) diff --git a/src/bedit_gui/services/icon_files.py b/src/bedit_gui/services/icon_files.py new file mode 100644 index 0000000..cc4e23d --- /dev/null +++ b/src/bedit_gui/services/icon_files.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from bedit_gui.models import Icon + + +def load(path: str | Path) -> Icon: + data = json.loads(Path(path).read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise TypeError("icon file must contain a JSON object") + return Icon.from_data(data) + + +def save(icon: Icon, path: str | Path) -> None: + Path(path).write_text(json.dumps(icon.to_data(), indent=2) + "\n", encoding="utf-8") diff --git a/src/bedit_gui/ui/forms/icon_editor_window.ui b/src/bedit_gui/ui/forms/icon_editor_window.ui index a4b1a30..6b0804f 100644 --- a/src/bedit_gui/ui/forms/icon_editor_window.ui +++ b/src/bedit_gui/ui/forms/icon_editor_window.ui @@ -39,10 +39,18 @@ + + + + File + + + + @@ -176,6 +184,42 @@ QAction::MenuRole::NoRole + + + + :/icons/icons/document-save-as.png:/icons/icons/document-save-as.png + + + Save to File + + + Save icon to a file + + + Ctrl+Shift+S + + + QAction::MenuRole::NoRole + + + + + + :/icons/icons/document-open.png:/icons/icons/document-open.png + + + Open from File + + + Open icon from File + + + Ctrl+Shift+O + + + QAction::MenuRole::NoRole + + diff --git a/src/bedit_gui/views/icon_editor_window.py b/src/bedit_gui/views/icon_editor_window.py index 044dc65..c298bbf 100644 --- a/src/bedit_gui/views/icon_editor_window.py +++ b/src/bedit_gui/views/icon_editor_window.py @@ -4,14 +4,18 @@ from copy import deepcopy from PySide6.QtCore import QEvent, QObject, Qt, Signal from PySide6.QtGui import QKeySequence, QPainter, QShortcut, QUndoCommand, QUndoStack, QWheelEvent, QShowEvent -from PySide6.QtWidgets import QDialog, QGraphicsView, QMainWindow, QWidget +from PySide6.QtWidgets import QDialog, QFileDialog, QGraphicsView, QMainWindow, QMessageBox, QWidget from bedit_core.models import Port, PortID, SignalDirection from bedit_gui.models import Icon, Shape, ShapeID +from bedit_gui.services import icon_files +from bedit_gui.services.application_logging import get_logger from bedit_gui.ui.generated.ui_icon_editor_window import Ui_iconEditor -from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool +from bedit_gui.views.icon_graphics_scene import IconGraphicsScene, RectangleCreationTool, TextCreationTool from bedit_gui.views.shape_options_dialog import ShapeOptionsDialog +logger = get_logger(__name__) + class ChangeIconDraftCommand(QUndoCommand): def __init__(self, editor: IconEditorWindow, icon: Icon, text: str) -> None: @@ -102,10 +106,11 @@ class IconEditorWindow(QMainWindow): self.setWindowTitle("Icon Editor") self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) self._icon = deepcopy(icon) + self._ports = deepcopy(ports) self.scene = IconGraphicsScene(self) - self.scene.set_ports(ports) - self._ensure_port_positions(ports) + self.scene.set_ports(self._ports) + self._ensure_port_positions(self._ports) self.scene.set_icon(self._icon) self.scene.port_moved.connect(self._port_moved) self.scene.shape_created.connect(self._shape_created) @@ -118,6 +123,8 @@ class IconEditorWindow(QMainWindow): self.ui.graphicsView.viewport().installEventFilter(self) self.ui.actionAdd_Rectangle.setCheckable(True) self.ui.actionAdd_Rectangle.triggered.connect(self._start_rectangle_tool) + self.ui.actionAdd_Text.setCheckable(True) + self.ui.actionAdd_Text.triggered.connect(self._start_text_tool) self.cancel_tool_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Escape), self) self.cancel_tool_shortcut.activated.connect(self.scene.cancel_creation_tool) self.delete_shortcut = QShortcut(QKeySequence(Qt.Key.Key_Delete), self) @@ -135,6 +142,8 @@ class IconEditorWindow(QMainWindow): self.ui.actionUndo.triggered.connect(self.undo_stack.undo) self.ui.actionRedo.triggered.connect(self.undo_stack.redo) self.ui.actionSave.triggered.connect(self.save) + self.ui.actionSave_to_File.triggered.connect(self.save_to_file) + self.ui.actionOpen_from_File.triggered.connect(self.open_from_file) self.ui.actionCancel.triggered.connect(self.close) self.undo_stack.canUndoChanged.connect(self.ui.actionUndo.setEnabled) self.undo_stack.canRedoChanged.connect(self.ui.actionRedo.setEnabled) @@ -157,6 +166,32 @@ class IconEditorWindow(QMainWindow): self.saved.emit(self.icon()) self.close() + def save_to_file(self) -> None: + file_name, _ = QFileDialog.getSaveFileName(self, "Save Icon", "", "JSON files (*.json)") + if not file_name: + return + path = file_name if file_name.lower().endswith(".json") else f"{file_name}.json" + try: + icon_files.save(self._icon, path) + except (OSError, TypeError, ValueError) as exc: + logger.exception("Could not save icon to %s", path) + QMessageBox.critical(self, "Could not save icon", str(exc)) + return + logger.info("Saved icon to: %s", path) + + def open_from_file(self) -> None: + file_name, _ = QFileDialog.getOpenFileName(self, "Open Icon", "", "JSON files (*.json)") + if not file_name: + return + try: + icon = icon_files.load(file_name) + except (OSError, TypeError, ValueError) as exc: + logger.exception("Could not open icon from %s", file_name) + QMessageBox.critical(self, "Could not open icon", str(exc)) + return + self.apply_change(icon, "Load icon from file") + logger.info("Loaded icon from: %s", file_name) + def zoom_in(self) -> None: self._zoom(self.zoom_step) @@ -187,6 +222,7 @@ class IconEditorWindow(QMainWindow): def _set_icon(self, icon: Icon) -> None: self._icon = deepcopy(icon) + self._ensure_port_positions(self._ports) self.scene.set_icon(self._icon) self.icon_changed.emit(self.icon()) @@ -268,10 +304,18 @@ class IconEditorWindow(QMainWindow): def _start_rectangle_tool(self) -> None: layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1 + self.ui.actionAdd_Text.setChecked(False) self.scene.set_creation_tool(RectangleCreationTool(self.scene, layer)) + def _start_text_tool(self) -> None: + layer = max((shape.layer for shape in self._icon.shapes.values()), default=-1) + 1 + self.ui.actionAdd_Rectangle.setChecked(False) + self.scene.set_creation_tool(TextCreationTool(self.scene, layer)) + def _tool_active_changed(self, active: bool) -> None: - self.ui.actionAdd_Rectangle.setChecked(active) + if not active: + self.ui.actionAdd_Rectangle.setChecked(False) + self.ui.actionAdd_Text.setChecked(False) cursor = Qt.CursorShape.CrossCursor if active else Qt.CursorShape.ArrowCursor self.ui.graphicsView.viewport().setCursor(cursor) diff --git a/src/bedit_gui/views/icon_graphics_scene.py b/src/bedit_gui/views/icon_graphics_scene.py index 54cf96a..1254461 100644 --- a/src/bedit_gui/views/icon_graphics_scene.py +++ b/src/bedit_gui/views/icon_graphics_scene.py @@ -5,11 +5,11 @@ from copy import deepcopy from typing import Protocol from PySide6.QtCore import QObject, QPointF, QRectF, Qt, Signal -from PySide6.QtGui import QBrush, QColor, QPainter, QPainterPath, QPen +from PySide6.QtGui import QBrush, QColor, QFont, QPainter, QPainterPath, QPen from PySide6.QtWidgets import QGraphicsItem, QGraphicsPathItem, QGraphicsRectItem, QGraphicsScene, QGraphicsSceneContextMenuEvent, QGraphicsSceneMouseEvent, QMenu, QStyleOptionGraphicsItem, QWidget from bedit_core.models import Port, PortID, SignalDirection -from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID +from bedit_gui.models import Icon, LineType, Rectangle, Shape, ShapeID, Text class ShapeCreationTool(Protocol): @@ -36,7 +36,7 @@ class RectangleCreationTool: position = self._bounded(position) self.preview.setRect(QRectF(self.start, position).normalized()) - def finish(self, position: QPointF) -> Rectangle | None: + def finish(self, position: QPointF) -> Shape | None: if self.start is None: return None position = self._bounded(position) @@ -44,6 +44,9 @@ class RectangleCreationTool: self.cancel() if rect.width() < 1 or rect.height() < 1: return None + return self.create_shape(rect) + + def create_shape(self, rect: QRectF) -> Shape: return Rectangle(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height()) def cancel(self) -> None: @@ -59,6 +62,11 @@ class RectangleCreationTool: return QPointF(x, y) +class TextCreationTool(RectangleCreationTool): + def create_shape(self, rect: QRectF) -> Shape: + return Text(layer=self.layer, pos=(round(rect.x()), round(rect.y())), width=rect.width(), height=rect.height(), text="Text") + + class ShapeGraphicsItem(QGraphicsPathItem): handle_size = 6.0 @@ -173,6 +181,48 @@ class RectangleGraphicsItem(ShapeGraphicsItem): self.setPath(path) +class TextGraphicsItem(ShapeGraphicsItem): + def __init__(self, shape_id: ShapeID, shape: Text, scene: IconGraphicsScene) -> None: + super().__init__(shape_id, shape, scene) + self.text = deepcopy(shape) + self.setPos(shape.pos[0], shape.pos[1]) + self._set_size(shape.width, shape.height) + self.setPen(QPen(Qt.PenStyle.NoPen)) + self.setBrush(QBrush(QColor(0, 0, 0, 0))) + self.setZValue(shape.layer) + + def current_shape(self) -> Text: + shape = deepcopy(self.text) + shape.pos = (round(self.pos().x()), round(self.pos().y())) + rect = self.path().boundingRect() + shape.width = rect.width() + shape.height = rect.height() + return shape + + def resize_to(self, position: QPointF) -> None: + bounds = self.icon_scene.sceneRect() + width = max(1.0, min(bounds.right(), round(position.x())) - self.pos().x()) + height = max(1.0, min(bounds.bottom(), round(position.y())) - self.pos().y()) + self._set_size(width, height) + + def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget | None = None) -> None: + super().paint(painter, option, widget) + font = QFont() + font.setPixelSize(max(1, round(self.text.size))) + font.setBold(self.text.bold) + font.setItalic(self.text.italic) + painter.setFont(font) + painter.setPen(self.icon_scene._color(self.text.color)) + painter.setClipRect(self.path().boundingRect()) + painter.drawText(self.path().boundingRect(), Qt.AlignmentFlag.AlignCenter | Qt.TextFlag.TextWordWrap, self.text.text) + + def _set_size(self, width: float, height: float) -> None: + self.prepareGeometryChange() + path = QPainterPath() + path.addRect(QRectF(0, 0, width, height)) + self.setPath(path) + + class PortGraphicsItem(QGraphicsRectItem): size = 16.0 @@ -304,9 +354,10 @@ class IconGraphicsScene(QGraphicsScene): super().mouseReleaseEvent(event) def _add_shape_item(self, shape_id: ShapeID, shape: Shape) -> None: - if not isinstance(shape, Rectangle): - return - self.addItem(RectangleGraphicsItem(shape_id, shape, self)) + if isinstance(shape, Rectangle): + self.addItem(RectangleGraphicsItem(shape_id, shape, self)) + elif isinstance(shape, Text): + self.addItem(TextGraphicsItem(shape_id, shape, self)) @staticmethod def _pen(shape: Rectangle) -> QPen: diff --git a/src/bedit_gui/views/shape_options_dialog.py b/src/bedit_gui/views/shape_options_dialog.py index ee547a7..4b89a1e 100644 --- a/src/bedit_gui/views/shape_options_dialog.py +++ b/src/bedit_gui/views/shape_options_dialog.py @@ -3,9 +3,9 @@ from __future__ import annotations from copy import deepcopy from PySide6.QtCore import QRectF -from PySide6.QtWidgets import QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QSpinBox, QVBoxLayout, QWidget +from PySide6.QtWidgets import QCheckBox, QComboBox, QDialog, QDialogButtonBox, QDoubleSpinBox, QFormLayout, QLabel, QLineEdit, QSpinBox, QVBoxLayout, QWidget -from bedit_gui.models import LineType, Rectangle, Shape +from bedit_gui.models import LineType, Rectangle, Shape, Text from bedit_gui.views.color_button import ColorButton @@ -34,6 +34,8 @@ class ShapeOptionsDialog(QDialog): if isinstance(shape, Rectangle): self._add_rectangle_fields(shape) + elif isinstance(shape, Text): + self._add_text_fields(shape) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) buttons.accepted.connect(self.accept) @@ -43,15 +45,18 @@ class ShapeOptionsDialog(QDialog): layout.addWidget(buttons) def shape(self) -> Shape: - if not isinstance(self._shape, Rectangle): - shape = deepcopy(self._shape) - shape.layer = self.layer.value() - shape.pos = (self.x.value(), self.y.value()) - return shape - - width = min(self.width.value(), self._scene_rect.right() - self.x.value()) - height = min(self.height.value(), self._scene_rect.bottom() - self.y.value()) - return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color()) + if isinstance(self._shape, Rectangle): + width = min(self.width.value(), self._scene_rect.right() - self.x.value()) + height = min(self.height.value(), self._scene_rect.bottom() - self.y.value()) + return Rectangle(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, line_type=self.line_type.currentData(), line_thickness=self.line_thickness.value(), corner_radius=self.corner_radius.value(), line_color=self.line_color.color(), fill_color=self.fill_color.color()) + if isinstance(self._shape, Text): + width = min(self.width.value(), self._scene_rect.right() - self.x.value()) + height = min(self.height.value(), self._scene_rect.bottom() - self.y.value()) + return Text(layer=self.layer.value(), pos=(self.x.value(), self.y.value()), width=width, height=height, color=self.color.color(), bold=self.bold.isChecked(), italic=self.italic.isChecked(), size=self.size.value(), text=self.text.text()) + shape = deepcopy(self._shape) + shape.layer = self.layer.value() + shape.pos = (self.x.value(), self.y.value()) + return shape def _add_rectangle_fields(self, shape: Rectangle) -> None: self.width = QSpinBox() @@ -79,3 +84,27 @@ class ShapeOptionsDialog(QDialog): self.form.addRow("Corner radius", self.corner_radius) self.form.addRow("Line color", self.line_color) self.form.addRow("Fill color", self.fill_color) + + def _add_text_fields(self, shape: Text) -> None: + self.width = QSpinBox() + self.width.setRange(1, round(self._scene_rect.width())) + self.width.setValue(round(shape.width)) + self.height = QSpinBox() + self.height.setRange(1, round(self._scene_rect.height())) + self.height.setValue(round(shape.height)) + self.color = ColorButton(shape.color) + self.bold = QCheckBox() + self.bold.setChecked(shape.bold) + self.italic = QCheckBox() + self.italic.setChecked(shape.italic) + self.size = QDoubleSpinBox() + self.size.setRange(1, 1000) + self.size.setValue(shape.size) + self.text = QLineEdit(shape.text) + self.form.addRow("Width", self.width) + self.form.addRow("Height", self.height) + self.form.addRow("Color", self.color) + self.form.addRow("Bold", self.bold) + self.form.addRow("Italic", self.italic) + self.form.addRow("Size", self.size) + self.form.addRow("Text", self.text) diff --git a/untitled.bedit.json b/untitled.bedit.json index fda7e86..6f97a47 100644 --- a/untitled.bedit.json +++ b/untitled.bedit.json @@ -402,21 +402,164 @@ "icons": { "43b3aee6-0b38-429b-9c6d-d38fc097297f": { "shapes": { - "7b93f7ba-d38e-4503-a64f-beb9fc915285": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { "layer": 0, - "type": "rectangle", + "type": "text", "pos": [ - -40, - -40 + -32, + -32 ], - "width": 80, - "height": 80, - "line_type": "solid", - "line_thickness": 1.0, - "corner_radius": 5.0, - "line_color": "#000000ff", - "fill_color": "#c8c8c8ff" + "width": 64.0, + "height": 64.0, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "C" } + }, + "port_positions": { + "c87881f1-23b4-4a69-b586-8e3c7bf6e21e": [ + -8, + -8 + ], + "c62de8eb-e13b-4849-bea5-c8cc5332269e": [ + 16, + -32 + ] + } + }, + "5a8b2e8d-489f-467b-8282-7e344dfad576": { + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "R" + } + }, + "port_positions": { + "0ac7d4ea-77f7-4c0d-8e37-406ef66e4740": [ + -8, + -8 + ], + "d1f5aab2-7bff-4b1e-8697-98fb6c3d8f02": [ + 16, + -32 + ] + } + }, + "77701f68-b14c-4b98-8929-c5fba0261962": { + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64.0, + "height": 64.0, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "R" + } + }, + "port_positions": { + "4a3f69f2-b305-4305-92e3-3998634ff226": [ + -8, + -8 + ] + } + }, + "033b930e-bf79-403a-8b0b-a159f3c81ce9": { + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "I" + } + }, + "port_positions": { + "0b9036b1-e4e4-437e-8c35-f5bb391b6cbd": [ + -8, + -8 + ], + "fb25f35d-4c0c-4cfa-92d4-1a18a71e2c11": [ + 16, + -32 + ] + } + }, + "9ee2f42b-5ed1-446d-8790-c2a1df6b61d3": { + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "1" + } + }, + "port_positions": { + "4306701a-6b1b-4d19-b8ff-45dbfa04f2d3": [ + -8, + -8 + ] + } + }, + "9f392c28-6bef-4b10-9305-93e250747007": { + "shapes": { + "2a5bf670-c821-442b-9d46-1b07faa70722": { + "layer": 0, + "type": "text", + "pos": [ + -32, + -32 + ], + "width": 64, + "height": 64, + "color": "#000000ff", + "bold": true, + "italic": false, + "size": 64.0, + "text": "0" + } + }, + "port_positions": { + "497b1f74-1186-471f-976a-36b07a451caf": [ + -8, + -8 + ] } } }