Implemented the vector icon editor, library previews, and workspace camera controls.

Key changes:
Component Options now has an “Edit Icon…” button instead of direct shape/color fields.
Icon editor supports:Rectangles with configurable corner radius
Circles and ellipses
Lines
Triangles
Text
Movable/selectable shapes
Shape context menus
Line style and width
Solid or transparent fill
Stroke and fill colours through Qt’s colour picker
Editable shape dimensions
Deleting selected shapes

Input anchors are green and output anchors red; both can be dragged around the icon.
Icons use a 120×80 vector coordinate system and are stored as editable elements in document JSON.
Components render their vector icons directly in the workspace.
Library and document trees display 16×16 icon previews.
Added a Camera toolbar:Zoom In
Zoom Out
Center

Mouse-wheel zoom works directly over the workspace.
Icon changes and port positions support undo/redo.
This commit is contained in:
2026-07-19 21:46:44 +02:00
parent 76bd313f79
commit 09824195c9
11 changed files with 498 additions and 464 deletions

View File

@@ -40,24 +40,49 @@ class Icon:
fill: str = "#f4f4f4"
border: str = "#303030"
text: str = ""
width: float = 120.0
height: float = 80.0
elements: list[dict[str, Any]] = field(default_factory=list)
def to_dict(self) -> dict[str, str]:
def __post_init__(self) -> None:
if not self.elements:
self.elements = [{
"type": "ellipse" if self.shape == "ellipse" else "rectangle",
"x": 1.0, "y": 1.0, "width": self.width - 2.0, "height": self.height - 2.0,
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
"lineStyle": "solid", "cornerRadius": 5.0,
}]
if self.text:
self.elements.append({
"type": "text", "x": 8.0, "y": 8.0,
"width": self.width - 16.0, "height": self.height - 16.0,
"text": self.text, "color": "#202020", "fontSize": 12.0,
})
def to_dict(self) -> dict[str, Any]:
return {
"shape": self.shape,
"fill": self.fill,
"border": self.border,
"text": self.text,
"size": {"width": self.width, "height": self.height},
"elements": deepcopy(self.elements),
}
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "Icon":
data = data or {}
return cls(
size = data.get("size", {})
icon = cls(
shape=str(data.get("shape", "rectangle")),
fill=str(data.get("fill", "#f4f4f4")),
border=str(data.get("border", "#303030")),
text=str(data.get("text", "")),
width=float(size.get("width", 120.0)),
height=float(size.get("height", 80.0)),
elements=deepcopy(data.get("elements", [])),
)
return icon
@dataclass(frozen=True)
@@ -318,7 +343,7 @@ def clone_component(source: Component) -> Component:
y=current.y,
inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties)) for port in current.inputs],
outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties)) for port in current.outputs],
icon=Icon(**current.icon.to_dict()),
icon=Icon.from_dict(current.icon.to_dict()),
properties=deepcopy(current.properties),
implementation_kind=current.implementation_kind,
graph=graph if current.implementation_kind == "graph" else Graph(),