Reorganized the application around a clear frontend/backend boundary.
src/bedit/
├── __main__.py
├── core/ # Pure Python, no PySide
│ ├── model.py
│ ├── port_types.py
│ ├── serializer.py
│ └── libraries.py
└── gui/ # All Qt-dependent code
├── app.py
├── main_window.py
├── preferences.py
├── controllers/
├── dialogs/
├── graphics/
├── models/
└── generated/ # Designer/resource output only
Notable improvements:
Domain models, serialization, port types, and library parsing are now Qt-free.
Qt signals and undo infrastructure are explicitly isolated under gui/controllers.
Library parsing is separated from the Qt repository and tree models.
All generated Python is contained in gui/generated.
Designer build tasks now write to the generated directory.
The application entry point and package metadata use the new paths.
README now documents the structure and dependency rules.
Removed the old mixed document, library, and workspace packages.
This commit is contained in:
368
BEdit/src/bedit/core/model.py
Normal file
368
BEdit/src/bedit/core/model.py
Normal file
@@ -0,0 +1,368 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from bedit.core.port_types import PortTypeRegistry
|
||||
|
||||
|
||||
@dataclass
|
||||
class Port:
|
||||
id: str
|
||||
name: str
|
||||
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 {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"position": {"x": self.x, "y": self.y},
|
||||
"properties": self.properties,
|
||||
"type": self.type,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Port":
|
||||
position = data.get("position", {})
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
name=str(data.get("name", data["id"])),
|
||||
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")),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Icon:
|
||||
shape: str = "rectangle"
|
||||
fill: str = "#f4f4f4"
|
||||
border: str = "#303030"
|
||||
text: str = ""
|
||||
width: float = 128.0
|
||||
height: float = 128.0
|
||||
elements: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.elements:
|
||||
self.elements = [{
|
||||
"type": "ellipse" if self.shape == "ellipse" else "rectangle",
|
||||
"x": 32.0, "y": 32.0, "width": 64.0, "height": 64.0,
|
||||
"fill": self.fill, "stroke": self.border, "lineWidth": 1.5,
|
||||
"lineStyle": "solid", "cornerRadius": 5.0,
|
||||
}]
|
||||
if self.text:
|
||||
self.elements.append({
|
||||
"type": "text", "x": 40.0, "y": 40.0,
|
||||
"width": 48.0, "height": 48.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 {}
|
||||
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=128.0,
|
||||
height=128.0,
|
||||
elements=deepcopy(data.get("elements", [])),
|
||||
)
|
||||
return icon
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Endpoint:
|
||||
block: str | None = None
|
||||
port: str | None = None
|
||||
interface: str | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, str]:
|
||||
if self.interface is not None:
|
||||
return {"interface": self.interface}
|
||||
if self.block is None or self.port is None:
|
||||
raise ValueError("A block endpoint requires both block and port")
|
||||
return {"block": self.block, "port": self.port}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Endpoint":
|
||||
if "interface" in data:
|
||||
return cls(interface=str(data["interface"]))
|
||||
return cls(block=str(data["block"]), port=str(data["port"]))
|
||||
|
||||
|
||||
@dataclass
|
||||
class Connection:
|
||||
id: str
|
||||
source: Endpoint
|
||||
target: Endpoint
|
||||
name: str = ""
|
||||
properties: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"source": self.source.to_dict(),
|
||||
"target": self.target.to_dict(),
|
||||
"name": self.name,
|
||||
"properties": self.properties,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Connection":
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
source=Endpoint.from_dict(data["source"]),
|
||||
target=Endpoint.from_dict(data["target"]),
|
||||
name=str(data.get("name", "")),
|
||||
properties=dict(data.get("properties", {})),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Graph:
|
||||
blocks: dict[str, Component] = field(default_factory=dict)
|
||||
connections: dict[str, Connection] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"blocks": [block.to_dict() for block in self.blocks.values()],
|
||||
"connections": [connection.to_dict() for connection in self.connections.values()],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any] | None) -> "Graph":
|
||||
data = data or {}
|
||||
blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
|
||||
connections = [Connection.from_dict(item) for item in data.get("connections", [])]
|
||||
if len({block.id for block in blocks}) != len(blocks):
|
||||
raise ValueError("A graph contains duplicate component IDs")
|
||||
if len({connection.id for connection in connections}) != len(connections):
|
||||
raise ValueError("A graph contains duplicate connection IDs")
|
||||
return cls(
|
||||
blocks={block.id: block for block in blocks},
|
||||
connections={connection.id: connection for connection in connections},
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Component:
|
||||
id: str
|
||||
name: str
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
rotation: float = 0.0
|
||||
inputs: list[Port] = field(default_factory=list)
|
||||
outputs: list[Port] = field(default_factory=list)
|
||||
icon: Icon = field(default_factory=Icon)
|
||||
properties: dict[str, Any] = field(default_factory=dict)
|
||||
implementation_kind: str = "graph"
|
||||
graph: Graph = field(default_factory=Graph)
|
||||
source: dict[str, Any] = field(default_factory=dict)
|
||||
show_subtree_in_library: bool = True
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
implementation = {"kind": self.implementation_kind}
|
||||
if self.implementation_kind == "text":
|
||||
implementation["source"] = self.source
|
||||
else:
|
||||
implementation["graph"] = self.graph.to_dict()
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"position": {"x": self.x, "y": self.y},
|
||||
"rotation": self.rotation,
|
||||
"interface": {
|
||||
"inputs": [port.to_dict() for port in self.inputs],
|
||||
"outputs": [port.to_dict() for port in self.outputs],
|
||||
},
|
||||
"icon": self.icon.to_dict(),
|
||||
"properties": self.properties,
|
||||
"library": {"showSubtree": self.show_subtree_in_library},
|
||||
"implementation": implementation,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "Component":
|
||||
interface = data.get("interface", {})
|
||||
position = data.get("position", {})
|
||||
implementation = data["implementation"]
|
||||
kind = str(implementation.get("kind", "graph"))
|
||||
if kind not in {"graph", "text"}:
|
||||
raise ValueError(f"Unknown component implementation kind: {kind}")
|
||||
return cls(
|
||||
id=str(data["id"]),
|
||||
name=str(data.get("name", "Unnamed")),
|
||||
x=float(position.get("x", 0.0)),
|
||||
y=float(position.get("y", 0.0)),
|
||||
rotation=float(data.get("rotation", 0.0)),
|
||||
inputs=[Port.from_dict(item) for item in interface.get("inputs", [])],
|
||||
outputs=[Port.from_dict(item) for item in interface.get("outputs", [])],
|
||||
icon=Icon.from_dict(data.get("icon")),
|
||||
properties=dict(data.get("properties", {})),
|
||||
show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)),
|
||||
implementation_kind=kind,
|
||||
graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(),
|
||||
source=dict(implementation.get("source", {})) if kind == "text" else {},
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GraphDocument:
|
||||
roots: dict[str, Component] = field(default_factory=dict)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def empty(cls) -> "GraphDocument":
|
||||
return cls(metadata={"name": "Untitled"})
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"format": "bedit-document",
|
||||
"version": 1,
|
||||
"metadata": self.metadata,
|
||||
"roots": [root.to_dict() for root in self.roots.values()],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "GraphDocument":
|
||||
if data.get("format") != "bedit-document":
|
||||
raise ValueError("This is not a BEdit document")
|
||||
if data.get("version") != 1:
|
||||
raise ValueError(f"Unsupported BEdit document version: {data.get('version')}")
|
||||
roots = [Component.from_dict(item) for item in data["roots"]]
|
||||
if len({root.id for root in roots}) != len(roots):
|
||||
raise ValueError("The document contains duplicate root IDs")
|
||||
document = cls(
|
||||
roots={root.id: root for root in roots},
|
||||
metadata=dict(data.get("metadata", {})),
|
||||
)
|
||||
document.validate()
|
||||
return document
|
||||
|
||||
def all_components(self):
|
||||
def walk(component: Component):
|
||||
yield component
|
||||
if component.implementation_kind == "graph":
|
||||
for child in component.graph.blocks.values():
|
||||
yield from walk(child)
|
||||
|
||||
def all_roots():
|
||||
for root in self.roots.values():
|
||||
yield from walk(root)
|
||||
|
||||
return all_roots()
|
||||
|
||||
def find_component(self, component_id: str) -> Component | None:
|
||||
return next(
|
||||
(component for component in self.all_components() if component.id == component_id),
|
||||
None,
|
||||
)
|
||||
|
||||
def find_parent(self, component_id: str) -> Component | None:
|
||||
for component in self.all_components():
|
||||
if component_id in component.graph.blocks:
|
||||
return component
|
||||
return None
|
||||
|
||||
def validate(self) -> None:
|
||||
seen: set[str] = set()
|
||||
for component in self.all_components():
|
||||
if component.id in seen:
|
||||
raise ValueError(f"Duplicate component ID: {component.id}")
|
||||
seen.add(component.id)
|
||||
if component.implementation_kind == "text" and component.graph.blocks:
|
||||
raise ValueError(f"Text component {component.name} cannot contain a graph")
|
||||
self._validate_graph(component)
|
||||
|
||||
@staticmethod
|
||||
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:
|
||||
"""Deep-copy a component tree and remap every owned object ID."""
|
||||
|
||||
def clone(current: Component) -> Component:
|
||||
child_pairs = [(child, clone(child)) for child in current.graph.blocks.values()]
|
||||
child_ids = {old.id: new.id for old, new in child_pairs}
|
||||
|
||||
def remap(endpoint: Endpoint) -> Endpoint:
|
||||
if endpoint.interface is not None:
|
||||
return endpoint
|
||||
return Endpoint(block=child_ids[endpoint.block or ""], port=endpoint.port)
|
||||
|
||||
graph = Graph(
|
||||
blocks={new.id: new for _old, new in child_pairs},
|
||||
connections={
|
||||
new_id: Connection(
|
||||
new_id,
|
||||
remap(connection.source),
|
||||
remap(connection.target),
|
||||
connection.name,
|
||||
deepcopy(connection.properties),
|
||||
)
|
||||
for connection in current.graph.connections.values()
|
||||
for new_id in [str(uuid4())]
|
||||
},
|
||||
)
|
||||
return Component(
|
||||
id=str(uuid4()),
|
||||
name=current.name,
|
||||
x=current.x,
|
||||
y=current.y,
|
||||
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,
|
||||
graph=graph if current.implementation_kind == "graph" else Graph(),
|
||||
source=deepcopy(current.source),
|
||||
show_subtree_in_library=current.show_subtree_in_library,
|
||||
)
|
||||
|
||||
return clone(source)
|
||||
Reference in New Issue
Block a user