Text fields and saving Icons to file

This commit is contained in:
2026-07-27 14:30:25 +02:00
parent 13d3825f9b
commit c68e693359
12 changed files with 510 additions and 35 deletions

25
0.icon.json Normal file
View File

@@ -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
]
}
}

25
1.icon.json Normal file
View File

@@ -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
]
}
}

29
C.icon.json Normal file
View File

@@ -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
]
}
}

29
I.icon.json Normal file
View File

@@ -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
]
}
}

29
R.icon.json Normal file
View File

@@ -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
]
}
}

View File

@@ -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)

View File

@@ -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")

View File

@@ -39,10 +39,18 @@
</property>
<addaction name="actionUndo"/>
<addaction name="actionRedo"/>
</widget>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionOpen_from_File"/>
<addaction name="actionSave_to_File"/>
<addaction name="separator"/>
<addaction name="actionSave"/>
<addaction name="actionCancel"/>
</widget>
<addaction name="menuFile"/>
<addaction name="menuEdit"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
@@ -176,6 +184,42 @@
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionSave_to_File">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/document-save-as.png</normaloff>:/icons/icons/document-save-as.png</iconset>
</property>
<property name="text">
<string>Save to File</string>
</property>
<property name="toolTip">
<string>Save icon to a file</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
<action name="actionOpen_from_File">
<property name="icon">
<iconset resource="../../resources/resources.qrc">
<normaloff>:/icons/icons/document-open.png</normaloff>:/icons/icons/document-open.png</iconset>
</property>
<property name="text">
<string>Open from File</string>
</property>
<property name="toolTip">
<string>Open icon from File</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+O</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
</property>
</action>
</widget>
<resources>
<include location="../../resources/resources.qrc"/>

View File

@@ -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)

View File

@@ -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
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:

View File

@@ -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,16 +45,19 @@ class ShapeOptionsDialog(QDialog):
layout.addWidget(buttons)
def shape(self) -> Shape:
if not isinstance(self._shape, Rectangle):
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
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())
def _add_rectangle_fields(self, shape: Rectangle) -> None:
self.width = QSpinBox()
self.width.setRange(1, round(self._scene_rect.width()))
@@ -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)

View File

@@ -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
]
}
}
}