Implemented the new port-management and grid workflow.

Right-click blocks in the graph, Document tree, or Libraries tree and choose “Port Options…”.
Unified port list with:Add/remove port
Name
Type (Signal currently)
Input/output orientation

New ports start at (0, 0) in the icon editor.
Connected ports cannot be removed, reoriented, or changed incompatibly.
Added a PortType registry; connections require matching port types.
Removed/hid the separate Add Input and Add Output workspace tools.
Library port changes are written back to the library JSON.
Added independent graph and icon grid-size settings.
Components, interface terminals, icon shapes, resize handles, and icon ports snap to their corresponding grid.
Icon canvases and component hitboxes are now fixed at 128×128.
Selected icon shapes show a bottom-right resize handle.
Circles preserve equal width and height while resizing.
Shapes and ports are constrained to the icon hitbox.

Fixed the icon-editor crash and grid behavior.
Renamed the resize handle’s shape attribute, which was overriding Qt’s required shape() method.
Icon shapes, resize handles, and port anchors now snap while dragging.
Graph blocks and interface terminals also snap while dragging.
Replaced the nearly invisible dotted graph grid with higher-contrast grid lines.
Retained final release-time snapping as a safety check.
Python compilation and diff validation pass.
This commit is contained in:
2026-07-19 22:06:36 +02:00
parent 09824195c9
commit dbca41640f
10 changed files with 551 additions and 59 deletions

View File

@@ -5,6 +5,8 @@ from dataclasses import dataclass, field
from typing import Any
from uuid import uuid4
from bedit.document.port_types import PortTypeRegistry
@dataclass
class Port:
@@ -13,6 +15,7 @@ class Port:
x: float = 0.0
y: float = 0.0
properties: dict[str, Any] = field(default_factory=dict)
type: str = "signal"
def to_dict(self) -> dict[str, Any]:
return {
@@ -20,6 +23,7 @@ class Port:
"name": self.name,
"position": {"x": self.x, "y": self.y},
"properties": self.properties,
"type": self.type,
}
@classmethod
@@ -31,6 +35,7 @@ class Port:
x=float(position.get("x", 0.0)),
y=float(position.get("y", 0.0)),
properties=dict(data.get("properties", {})),
type=str(data.get("type", "signal")),
)
@@ -40,8 +45,8 @@ class Icon:
fill: str = "#f4f4f4"
border: str = "#303030"
text: str = ""
width: float = 120.0
height: float = 80.0
width: float = 128.0
height: float = 128.0
elements: list[dict[str, Any]] = field(default_factory=list)
def __post_init__(self) -> None:
@@ -72,14 +77,13 @@ class Icon:
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "Icon":
data = data or {}
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)),
width=128.0,
height=128.0,
elements=deepcopy(data.get("elements", [])),
)
return icon
@@ -293,21 +297,31 @@ class GraphDocument:
def _validate_graph(owner: Component) -> None:
input_ids = {port.id for port in owner.inputs}
output_ids = {port.id for port in owner.outputs}
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(owner.outputs):
raise ValueError(f"Component {owner.name} contains duplicate port IDs")
for port in (*owner.inputs, *owner.outputs):
PortTypeRegistry.get(port.type)
for connection in owner.graph.connections.values():
if connection.source.interface is not None:
if connection.source.interface not in input_ids:
raise ValueError(f"Connection {connection.id} uses an unknown interface input")
source_port = next(p for p in owner.inputs if p.id == connection.source.interface)
else:
source = owner.graph.blocks.get(connection.source.block or "")
if source is None or connection.source.port not in {p.id for p in source.outputs}:
raise ValueError(f"Connection {connection.id} uses an unknown block output")
source_port = next(p for p in source.outputs if p.id == connection.source.port)
if connection.target.interface is not None:
if connection.target.interface not in output_ids:
raise ValueError(f"Connection {connection.id} uses an unknown interface output")
target_port = next(p for p in owner.outputs if p.id == connection.target.interface)
else:
target = owner.graph.blocks.get(connection.target.block or "")
if target is None or connection.target.port not in {p.id for p in target.inputs}:
raise ValueError(f"Connection {connection.id} uses an unknown block input")
target_port = next(p for p in target.inputs if p.id == connection.target.port)
if not PortTypeRegistry.compatible(source_port.type, target_port.type):
raise ValueError(f"Connection {connection.id} joins incompatible port types")
def clone_component(source: Component) -> Component:
@@ -341,8 +355,8 @@ def clone_component(source: Component) -> Component:
name=current.name,
x=current.x,
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],
inputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.inputs],
outputs=[Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) for port in current.outputs],
icon=Icon.from_dict(current.icon.to_dict()),
properties=deepcopy(current.properties),
implementation_kind=current.implementation_kind,