Compare commits

...

3 Commits

Author SHA1 Message Date
495edd42f4 Added array ports and junctions 2026-07-20 15:00:06 +02:00
a067c85994 new text mode editor 2026-07-20 14:24:47 +02:00
bf2512995f labels 2026-07-20 14:02:22 +02:00
24 changed files with 2612 additions and 452 deletions

View File

@@ -60,6 +60,9 @@ Keep the root of `src/bedit` minimal. New UI modules should go into the relevant
- Port removal or reorientation must be rejected when it would invalidate an - Port removal or reorientation must be rejected when it would invalidate an
existing connection. existing connection.
- Connections reference port IDs, never port names. - Connections reference port IDs, never port names.
- Connection junctions are explicit typed graph objects. Splitting a connection
creates one incoming and one outgoing segment; the junction can source further
branches without overlapping full connection paths.
- Graph interaction has separate Pointer and Connect modes. Port hints are only - Graph interaction has separate Pointer and Connect modes. Port hints are only
visible in Connect mode. Connecting two blocks opens the compatible port-pair visible in Connect mode. Connecting two blocks opens the compatible port-pair
chooser; explicit port clicks determine its default selection. Connections chooser; explicit port clicks determine its default selection. Connections
@@ -121,6 +124,9 @@ pyside6-uic --from-imports ui/shape_options_dialog.ui \
pyside6-uic --from-imports ui/icon_editor_dialog.ui \ pyside6-uic --from-imports ui/icon_editor_dialog.ui \
-o src/bedit/gui/generated/ui_icon_editor_dialog.py -o src/bedit/gui/generated/ui_icon_editor_dialog.py
pyside6-uic --from-imports ui/text_definition_editor.ui \
-o src/bedit/gui/generated/ui_text_definition_editor.py
``` ```
When adding a promoted/custom widget in Designer, its header must use the real When adding a promoted/custom widget in Designer, its header must use the real

View File

@@ -13,7 +13,7 @@ class LibraryDocument:
def bundled_library_path() -> Path: def bundled_library_path() -> Path:
return Path(__file__).resolve().parents[1] / "data" / "libraries" / "example.json" return Path(__file__).resolve().parents[1] / "data" / "libraries" / "default.json"
def default_library_paths() -> list[str]: def default_library_paths() -> list[str]:

View File

@@ -16,6 +16,7 @@ class Port:
y: float = 0.0 y: float = 0.0
properties: dict[str, Any] = field(default_factory=dict) properties: dict[str, Any] = field(default_factory=dict)
type: str = "signal" type: str = "signal"
allows_multiple_connections: bool = False
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
@@ -24,6 +25,7 @@ class Port:
"position": {"x": self.x, "y": self.y}, "position": {"x": self.x, "y": self.y},
"properties": self.properties, "properties": self.properties,
"type": self.type, "type": self.type,
"multipleConnections": self.allows_multiple_connections,
} }
@classmethod @classmethod
@@ -36,6 +38,7 @@ class Port:
y=float(position.get("y", 0.0)), y=float(position.get("y", 0.0)),
properties=dict(data.get("properties", {})), properties=dict(data.get("properties", {})),
type=str(data.get("type", "signal")), type=str(data.get("type", "signal")),
allows_multiple_connections=bool(data.get("multipleConnections", False)),
) )
@@ -109,10 +112,13 @@ class Endpoint:
block: str | None = None block: str | None = None
port: str | None = None port: str | None = None
interface: str | None = None interface: str | None = None
junction: str | None = None
def to_dict(self) -> dict[str, str]: def to_dict(self) -> dict[str, str]:
if self.interface is not None: if self.interface is not None:
return {"interface": self.interface} return {"interface": self.interface}
if self.junction is not None:
return {"junction": self.junction}
if self.block is None or self.port is None: if self.block is None or self.port is None:
raise ValueError("A block endpoint requires both block and port") raise ValueError("A block endpoint requires both block and port")
return {"block": self.block, "port": self.port} return {"block": self.block, "port": self.port}
@@ -121,6 +127,8 @@ class Endpoint:
def from_dict(cls, data: dict[str, Any]) -> "Endpoint": def from_dict(cls, data: dict[str, Any]) -> "Endpoint":
if "interface" in data: if "interface" in data:
return cls(interface=str(data["interface"])) return cls(interface=str(data["interface"]))
if "junction" in data:
return cls(junction=str(data["junction"]))
return cls(block=str(data["block"]), port=str(data["port"])) return cls(block=str(data["block"]), port=str(data["port"]))
@@ -152,6 +160,55 @@ class Connection:
) )
@dataclass
class Parameter:
id: str
name: str
type: str = "real"
value: str = "0"
def to_dict(self) -> dict[str, str]:
return {"id": self.id, "name": self.name, "type": self.type, "value": self.value}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Parameter":
if not isinstance(data, dict):
raise ValueError("Each text component parameter must be an object")
if "id" not in data:
raise ValueError("Each text component parameter must have an ID")
return cls(
id=str(data["id"]),
name=str(data.get("name", "")),
type=str(data.get("type", "real")),
value=str(data.get("value", "0")),
)
@dataclass
class Junction:
id: str
x: float
y: float
type: str = "signal"
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"position": {"x": self.x, "y": self.y},
"type": self.type,
}
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "Junction":
position = data.get("position", {})
return cls(
id=str(data["id"]),
x=float(position.get("x", 0)),
y=float(position.get("y", 0)),
type=str(data.get("type", "signal")),
)
@dataclass @dataclass
class Annotation: class Annotation:
id: str id: str
@@ -197,12 +254,14 @@ class Graph:
blocks: dict[str, Component] = field(default_factory=dict) blocks: dict[str, Component] = field(default_factory=dict)
connections: dict[str, Connection] = field(default_factory=dict) connections: dict[str, Connection] = field(default_factory=dict)
annotations: dict[str, Annotation] = field(default_factory=dict) annotations: dict[str, Annotation] = field(default_factory=dict)
junctions: dict[str, Junction] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
"blocks": [block.to_dict() for block in self.blocks.values()], "blocks": [block.to_dict() for block in self.blocks.values()],
"connections": [connection.to_dict() for connection in self.connections.values()], "connections": [connection.to_dict() for connection in self.connections.values()],
"annotations": [item.to_dict() for item in self.annotations.values()], "annotations": [item.to_dict() for item in self.annotations.values()],
"junctions": [junction.to_dict() for junction in self.junctions.values()],
} }
@classmethod @classmethod
@@ -211,18 +270,22 @@ class Graph:
blocks = [Component.from_dict(item) for item in data.get("blocks", [])] blocks = [Component.from_dict(item) for item in data.get("blocks", [])]
connections = [Connection.from_dict(item) for item in data.get("connections", [])] connections = [Connection.from_dict(item) for item in data.get("connections", [])]
annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])] annotations = [Annotation.from_dict(item) for item in data.get("annotations", [])]
junctions = [Junction.from_dict(item) for item in data.get("junctions", [])]
if len({block.id for block in blocks}) != len(blocks): if len({block.id for block in blocks}) != len(blocks):
raise ValueError("A graph contains duplicate component IDs") raise ValueError("A graph contains duplicate component IDs")
if len({connection.id for connection in connections}) != len(connections): if len({connection.id for connection in connections}) != len(connections):
raise ValueError("A graph contains duplicate connection IDs") raise ValueError("A graph contains duplicate connection IDs")
if len({item.id for item in annotations}) != len(annotations): if len({item.id for item in annotations}) != len(annotations):
raise ValueError("A graph contains duplicate annotation IDs") raise ValueError("A graph contains duplicate annotation IDs")
if len({item.id for item in junctions}) != len(junctions):
raise ValueError("A graph contains duplicate junction IDs")
if any(item.kind not in {"box", "line", "text"} for item in annotations): if any(item.kind not in {"box", "line", "text"} for item in annotations):
raise ValueError("A graph contains an unknown annotation kind") raise ValueError("A graph contains an unknown annotation kind")
return cls( return cls(
blocks={block.id: block for block in blocks}, blocks={block.id: block for block in blocks},
connections={connection.id: connection for connection in connections}, connections={connection.id: connection for connection in connections},
annotations={item.id: item for item in annotations}, annotations={item.id: item for item in annotations},
junctions={junction.id: junction for junction in junctions},
) )
@@ -271,6 +334,19 @@ class Component:
kind = str(implementation.get("kind", "graph")) kind = str(implementation.get("kind", "graph"))
if kind not in {"graph", "text"}: if kind not in {"graph", "text"}:
raise ValueError(f"Unknown component implementation kind: {kind}") raise ValueError(f"Unknown component implementation kind: {kind}")
source: dict[str, Any] = {}
if kind == "text":
raw_source = implementation.get("source", {})
equations = raw_source.get("equations", "")
parameters = raw_source.get("parameters", [])
if not isinstance(equations, str):
raise ValueError("Text component equations must be a string")
if not isinstance(parameters, list):
raise ValueError("Text component parameters must be a list")
source = {
"equations": equations,
"parameters": [Parameter.from_dict(item).to_dict() for item in parameters],
}
return cls( return cls(
id=str(data["id"]), id=str(data["id"]),
name=str(data.get("name", "Unnamed")), name=str(data.get("name", "Unnamed")),
@@ -284,7 +360,7 @@ class Component:
show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)), show_subtree_in_library=bool(data.get("library", {}).get("showSubtree", True)),
implementation_kind=kind, implementation_kind=kind,
graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(), graph=Graph.from_dict(implementation.get("graph")) if kind == "graph" else Graph(),
source=dict(implementation.get("source", {})) if kind == "text" else {}, source=source,
) )
@@ -295,7 +371,7 @@ class GraphDocument:
@classmethod @classmethod
def empty(cls) -> "GraphDocument": def empty(cls) -> "GraphDocument":
return cls(metadata={"name": "Untitled"}) return cls(metadata={"name": "Current Document"})
def to_dict(self) -> dict[str, Any]: def to_dict(self) -> dict[str, Any]:
return { return {
@@ -348,6 +424,9 @@ class GraphDocument:
def validate(self) -> None: def validate(self) -> None:
seen: set[str] = set() seen: set[str] = set()
root_names = [component.name for component in self.roots.values()]
if len(set(root_names)) != len(root_names):
raise ValueError("Root component names must be unique")
for component in self.all_components(): for component in self.all_components():
if component.id in seen: if component.id in seen:
raise ValueError(f"Duplicate component ID: {component.id}") raise ValueError(f"Duplicate component ID: {component.id}")
@@ -358,14 +437,29 @@ class GraphDocument:
@staticmethod @staticmethod
def _validate_graph(owner: Component) -> None: def _validate_graph(owner: Component) -> None:
child_names = [component.name for component in owner.graph.blocks.values()]
if len(set(child_names)) != len(child_names):
raise ValueError(
f"Component names inside {owner.name!r} must be unique"
)
input_ids = {port.id for port in owner.inputs} input_ids = {port.id for port in owner.inputs}
output_ids = {port.id for port in owner.outputs} output_ids = {port.id for port in owner.outputs}
if len(input_ids) != len(owner.inputs) or len(output_ids) != len(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") raise ValueError(f"Component {owner.name} contains duplicate port IDs")
for port in (*owner.inputs, *owner.outputs): for port in (*owner.inputs, *owner.outputs):
PortTypeRegistry.get(port.type) PortTypeRegistry.get(port.type)
for junction in owner.graph.junctions.values():
PortTypeRegistry.get(junction.type)
endpoint_counts: dict[tuple[str, str, str], int] = {}
for connection in owner.graph.connections.values(): for connection in owner.graph.connections.values():
if connection.source.interface is not None: if connection.source.junction is not None:
junction = owner.graph.junctions.get(connection.source.junction)
if junction is None:
raise ValueError(
f"Connection {connection.id} uses an unknown source junction"
)
source_port = Port(junction.id, "Junction", type=junction.type)
elif connection.source.interface is not None:
if connection.source.interface not in input_ids: if connection.source.interface not in input_ids:
raise ValueError(f"Connection {connection.id} uses an unknown interface input") 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) source_port = next(p for p in owner.inputs if p.id == connection.source.interface)
@@ -374,7 +468,19 @@ class GraphDocument:
if source is None or connection.source.port not in {p.id for p in source.outputs}: 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") 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) 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.junction is not None:
junction = owner.graph.junctions.get(connection.target.junction)
if junction is None:
raise ValueError(
f"Connection {connection.id} uses an unknown target junction"
)
target_port = Port(
junction.id,
"Junction",
type=junction.type,
allows_multiple_connections=False,
)
elif connection.target.interface is not None:
if connection.target.interface not in output_ids: if connection.target.interface not in output_ids:
raise ValueError(f"Connection {connection.id} uses an unknown interface output") 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) target_port = next(p for p in owner.outputs if p.id == connection.target.interface)
@@ -385,6 +491,40 @@ class GraphDocument:
target_port = next(p for p in target.inputs if p.id == connection.target.port) 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): if not PortTypeRegistry.compatible(source_port.type, target_port.type):
raise ValueError(f"Connection {connection.id} joins incompatible port types") raise ValueError(f"Connection {connection.id} joins incompatible port types")
source_key = (
"source-junction"
if connection.source.junction is not None
else "source-interface"
if connection.source.interface is not None
else "source-block",
connection.source.block or "",
connection.source.junction
or connection.source.interface
or connection.source.port
or "",
)
target_key = (
"target-junction"
if connection.target.junction is not None
else "target-interface"
if connection.target.interface is not None
else "target-block",
connection.target.block or "",
connection.target.junction
or connection.target.interface
or connection.target.port
or "",
)
endpoint_counts[source_key] = endpoint_counts.get(source_key, 0) + 1
endpoint_counts[target_key] = endpoint_counts.get(target_key, 0) + 1
if (
endpoint_counts[target_key] > 1
and not target_port.allows_multiple_connections
):
raise ValueError(
f"Input {target_port.name!r} has multiple incoming connections "
"but does not allow them"
)
def clone_component(source: Component) -> Component: def clone_component(source: Component) -> Component:
@@ -393,10 +533,15 @@ def clone_component(source: Component) -> Component:
def clone(current: Component) -> Component: def clone(current: Component) -> Component:
child_pairs = [(child, clone(child)) for child in current.graph.blocks.values()] child_pairs = [(child, clone(child)) for child in current.graph.blocks.values()]
child_ids = {old.id: new.id for old, new in child_pairs} child_ids = {old.id: new.id for old, new in child_pairs}
junction_ids = {
junction.id: str(uuid4()) for junction in current.graph.junctions.values()
}
def remap(endpoint: Endpoint) -> Endpoint: def remap(endpoint: Endpoint) -> Endpoint:
if endpoint.interface is not None: if endpoint.interface is not None:
return endpoint return endpoint
if endpoint.junction is not None:
return Endpoint(junction=junction_ids[endpoint.junction])
return Endpoint(block=child_ids[endpoint.block or ""], port=endpoint.port) return Endpoint(block=child_ids[endpoint.block or ""], port=endpoint.port)
graph = Graph( graph = Graph(
@@ -427,6 +572,12 @@ def clone_component(source: Component) -> Component:
for annotation in current.graph.annotations.values() for annotation in current.graph.annotations.values()
for new_id in [str(uuid4())] for new_id in [str(uuid4())]
}, },
junctions={
junction_ids[junction.id]: Junction(
junction_ids[junction.id], junction.x, junction.y, junction.type
)
for junction in current.graph.junctions.values()
},
) )
return Component( return Component(
id=str(uuid4()), id=str(uuid4()),
@@ -434,11 +585,27 @@ def clone_component(source: Component) -> Component:
x=current.x, x=current.x,
y=current.y, y=current.y,
inputs=[ inputs=[
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) Port(
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
for port in current.inputs for port in current.inputs
], ],
outputs=[ outputs=[
Port(port.id, port.name, port.x, port.y, deepcopy(port.properties), port.type) Port(
port.id,
port.name,
port.x,
port.y,
deepcopy(port.properties),
port.type,
port.allows_multiple_connections,
)
for port in current.outputs for port in current.outputs
], ],
icon=Icon.from_dict(current.icon.to_dict()), icon=Icon.from_dict(current.icon.to_dict()),

View File

@@ -0,0 +1,435 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Untitled"
},
"roots": [
{
"id": "7ad651ef-6f76-4064-81d3-3fd6dd4d2918",
"name": "gain",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-df34ce84",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-fe9e6486",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "K",
"color": "#00007f",
"fontSize": 24.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"fill": "#ffffff"
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "out = k*in;",
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "1"
}
]
}
}
},
{
"id": "12b21b11-8828-4a90-a561-19491dc9632e",
"name": "differentiate",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-1cbabc8f",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-4eeea4e7",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "d/dt",
"color": "#00007f",
"fontSize": 18.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"fill": "#ffffff"
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "initial out = initial;\nout = der(in);",
"parameters": [
{
"id": "parameter-331e38be",
"name": "initial",
"type": "real",
"value": "0"
}
]
}
}
},
{
"id": "afdc7696-622b-47e4-b2c3-28ec60786bf2",
"name": "integrate",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-12903207",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-b3370b1a",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 64.0,
"y": 40.0,
"width": 28.0,
"height": 48.0,
"text": "dt",
"color": "#00007f",
"fontSize": 18.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"fill": "#ffffff"
},
{
"type": "text",
"x": 40.0,
"y": 32.0,
"width": 28.0,
"height": 54.0,
"fill": "none",
"stroke": "#00007f",
"lineWidth": 1.5,
"lineStyle": "none",
"text": "\u222b",
"fontSize": 32.0,
"color": "#00007f"
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "initial out = initial;\nder(out) = in;",
"parameters": [
{
"id": "parameter-6bdc1c76",
"name": "initial",
"type": "real",
"value": "0"
}
]
}
}
},
{
"id": "f6b8e5c9-4d13-4f7b-b574-e00ca96e42c3",
"name": "add",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-c995845c",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": true
}
],
"outputs": [
{
"id": "port-a2f0dea6",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 80.0,
"y": 48.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "circle",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid"
},
{
"type": "text",
"x": 48.0,
"y": 48.0,
"width": 32.0,
"height": 32.0,
"fill": "none",
"stroke": "#00007f",
"lineWidth": 1.5,
"lineStyle": "none",
"text": "+",
"fontSize": 18.0,
"color": "#00007f"
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "out = sum(in[i] for i in 1:in.N);",
"parameters": []
}
}
}
]
}

View File

@@ -1,212 +0,0 @@
{
"format": "bedit-document",
"version": 1,
"metadata": {
"name": "Example"
},
"roots": [
{
"id": "bf714517-e7b2-4f4b-8b53-55642e3beb28",
"name": "A",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-5c5d4695",
"name": "Port 1",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 64.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-de109124",
"name": "Port 2",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "A",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": [],
"parameters": {}
}
}
},
{
"id": "73c9d0c0-293d-4a55-8b89-a1f095dfa75f",
"name": "B",
"position": {
"x": 0.0,
"y": 0.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-4f732b3e",
"name": "Port 1",
"position": {
"x": -176.0,
"y": -144.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 48.0
}
},
"type": "signal"
},
{
"id": "port-ef7c4218",
"name": "Port 2",
"position": {
"x": -176.0,
"y": -16.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 80.0
}
},
"type": "signal"
}
],
"outputs": [
{
"id": "port-b68679b2",
"name": "Port 3",
"position": {
"x": 128.0,
"y": -80.0
},
"properties": {
"iconPosition": {
"x": 96.0,
"y": 64.0
}
},
"type": "signal"
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Graph",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"type": "rectangle",
"x": 32.0,
"y": 32.0,
"width": 64.0,
"height": 64.0,
"fill": "#f4f4f4",
"stroke": "#303030",
"lineWidth": 1.5,
"lineStyle": "solid",
"cornerRadius": 5.0
},
{
"type": "text",
"x": 40.0,
"y": 40.0,
"width": 48.0,
"height": 48.0,
"text": "B",
"color": "#303030",
"fontSize": 12.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"fill": "#ffffff"
}
]
},
"properties": {},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "graph",
"graph": {
"blocks": [],
"connections": []
}
}
}
]
}

View File

@@ -1,7 +1,7 @@
from PySide6.QtCore import QPointF from PySide6.QtCore import QPointF
from PySide6.QtGui import QUndoCommand from PySide6.QtGui import QUndoCommand
from bedit.core.model import Annotation, Component, Connection, Port from bedit.core.model import Annotation, Component, Connection, Junction, Port
class AddComponentCommand(QUndoCommand): class AddComponentCommand(QUndoCommand):
@@ -76,6 +76,32 @@ class AddConnectionCommand(QUndoCommand):
self.controller._remove_connection(self.owner_id, self.connection.id) self.controller._remove_connection(self.owner_id, self.connection.id)
class SplitConnectionCommand(QUndoCommand):
def __init__(
self,
controller,
owner_id: str,
original: Connection,
junction: Junction,
first: Connection,
second: Connection,
) -> None:
super().__init__("Add connection junction")
self.controller, self.owner_id = controller, owner_id
self.original, self.junction = original, junction
self.first, self.second = first, second
def redo(self) -> None:
self.controller._split_connection(
self.owner_id, self.original.id, self.junction, self.first, self.second
)
def undo(self) -> None:
self.controller._restore_split_connection(
self.owner_id, self.original, self.junction.id, self.first.id, self.second.id
)
class AddAnnotationCommand(QUndoCommand): class AddAnnotationCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, annotation: Annotation) -> None: def __init__(self, controller, owner_id: str, annotation: Annotation) -> None:
super().__init__(f"Draw {annotation.kind}") super().__init__(f"Draw {annotation.kind}")
@@ -200,6 +226,19 @@ class EditComponentAppearanceCommand(QUndoCommand):
self.controller._set_component_appearance(self.component_id, self.old) self.controller._set_component_appearance(self.component_id, self.old)
class EditComponentPropertiesCommand(QUndoCommand):
def __init__(self, controller, component_id: str, old: dict, new: dict, text: str) -> None:
super().__init__(text)
self.controller, self.component_id = controller, component_id
self.old, self.new = old, new
def redo(self) -> None:
self.controller._set_component_properties(self.component_id, self.new)
def undo(self) -> None:
self.controller._set_component_properties(self.component_id, self.old)
class RenameInterfacePortCommand(QUndoCommand): class RenameInterfacePortCommand(QUndoCommand):
def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None: def __init__(self, controller, owner_id: str, port_id: str, old: str, new: str) -> None:
super().__init__("Rename interface port") super().__init__("Rename interface port")
@@ -309,3 +348,14 @@ class EditTextDefinitionCommand(QUndoCommand):
def undo(self) -> None: def undo(self) -> None:
self.controller._set_text_definition(self.component_id, self.old) self.controller._set_text_definition(self.component_id, self.old)
def id(self) -> int:
return 1001
def mergeWith(self, other: QUndoCommand) -> bool: # noqa: N802
if not isinstance(other, EditTextDefinitionCommand):
return False
if other.component_id != self.component_id:
return False
self.new = other.new
return True

View File

@@ -15,6 +15,7 @@ from bedit.gui.controllers.commands import (
EditGraphItemCommand, EditGraphItemCommand,
EditTextDefinitionCommand, EditTextDefinitionCommand,
EditComponentAppearanceCommand, EditComponentAppearanceCommand,
EditComponentPropertiesCommand,
MoveComponentCommand, MoveComponentCommand,
MoveInterfacePortCommand, MoveInterfacePortCommand,
PasteSelectionCommand, PasteSelectionCommand,
@@ -22,6 +23,7 @@ from bedit.gui.controllers.commands import (
RenameInterfacePortCommand, RenameInterfacePortCommand,
ReplaceSourceCommand, ReplaceSourceCommand,
RotateComponentsCommand, RotateComponentsCommand,
SplitConnectionCommand,
) )
from bedit.core.model import ( from bedit.core.model import (
Annotation, Annotation,
@@ -30,6 +32,8 @@ from bedit.core.model import (
Endpoint, Endpoint,
GraphDocument, GraphDocument,
Icon, Icon,
Junction,
Parameter,
Port, Port,
clone_component, clone_component,
) )
@@ -45,6 +49,8 @@ class DocumentController(QObject):
componentRemoved = Signal(str) componentRemoved = Signal(str)
componentMoved = Signal(str, QPointF) componentMoved = Signal(str, QPointF)
componentRotated = Signal(str, float) componentRotated = Signal(str, float)
componentPropertiesChanged = Signal(str)
textDefinitionChanged = Signal(str)
connectionAdded = Signal(str) connectionAdded = Signal(str)
connectionRemoved = Signal(str) connectionRemoved = Signal(str)
graphItemChanged = Signal(str, str) graphItemChanged = Signal(str, str)
@@ -151,12 +157,13 @@ class DocumentController(QObject):
if self.document is None: if self.document is None:
raise ValueError("Open or create a document first") raise ValueError("Open or create a document first")
number = len(self.document.roots) + 1 number = len(self.document.roots) + 1
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
component = Component( component = Component(
id=str(uuid4()), id=str(uuid4()),
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}", name=self._available_component_name(base_name, self.document.roots.values(), number),
implementation_kind=kind, implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"), icon=Icon(text="Graph" if kind == "graph" else "Text"),
source={"equations": [], "parameters": {}} if kind == "text" else {}, source={"equations": "", "parameters": []} if kind == "text" else {},
) )
self.undo_stack.push(AddComponentCommand(self, None, component)) self.undo_stack.push(AddComponentCommand(self, None, component))
self.activate_component(component.id) self.activate_component(component.id)
@@ -169,12 +176,13 @@ class DocumentController(QObject):
if owner is None or owner.implementation_kind != "graph": if owner is None or owner.implementation_kind != "graph":
raise ValueError("Children can only be added to graph components") raise ValueError("Children can only be added to graph components")
number = len(owner.graph.blocks) + 1 number = len(owner.graph.blocks) + 1
base_name = f"New {'Graph' if kind == 'graph' else 'Text'} Block "
component = Component( component = Component(
id=str(uuid4()), id=str(uuid4()),
name=f"New {'Graph' if kind == 'graph' else 'Text'} Block {number}", name=self._available_component_name(base_name, owner.graph.blocks.values(), number),
implementation_kind=kind, implementation_kind=kind,
icon=Icon(text="Graph" if kind == "graph" else "Text"), icon=Icon(text="Graph" if kind == "graph" else "Text"),
source={"equations": [], "parameters": {}} if kind == "text" else {}, source={"equations": "", "parameters": []} if kind == "text" else {},
) )
self.undo_stack.push(AddComponentCommand(self, owner_id, component)) self.undo_stack.push(AddComponentCommand(self, owner_id, component))
return component.id return component.id
@@ -209,6 +217,9 @@ class DocumentController(QObject):
if self.active_component is None or self.active_component.implementation_kind != "graph": if self.active_component is None or self.active_component.implementation_kind != "graph":
raise ValueError("Open a graph component before placing components") raise ValueError("Open a graph component before placing components")
component = clone_component(source) component = clone_component(source)
component.name = self._available_component_name(
source.name, self.active_component.graph.blocks.values(), 0
)
component.x, component.y = position.x(), position.y() component.x, component.y = position.x(), position.y()
self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component)) self.undo_stack.push(AddComponentCommand(self, self.active_component_id, component))
return component.id return component.id
@@ -219,6 +230,20 @@ class DocumentController(QObject):
MoveComponentCommand(self, self.active_component_id, component_id, old, new) MoveComponentCommand(self, self.active_component_id, component_id, old, new)
) )
def move_junction(self, junction_id: str, old: QPointF, new: QPointF) -> None:
if old != new and self.active_component_id is not None:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"junction_geometry",
junction_id,
{"x": old.x(), "y": old.y()},
{"x": new.x(), "y": new.y()},
"Move connection junction",
)
)
def rotate_components(self, component_ids: set[str]) -> None: def rotate_components(self, component_ids: set[str]) -> None:
if self.active_component is None or self.active_component_id is None: if self.active_component is None or self.active_component_id is None:
return return
@@ -245,6 +270,14 @@ class DocumentController(QObject):
raise ValueError("A connection endpoint no longer exists") raise ValueError("A connection endpoint no longer exists")
if not PortTypeRegistry.compatible(source_port.type, target_port.type): if not PortTypeRegistry.compatible(source_port.type, target_port.type):
raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}") raise ValueError(f"Cannot connect {source_port.type!r} to {target_port.type!r}")
if not self.endpoint_accepts_connection(source, "source"):
raise ValueError(
f"Port {source_port.name!r} already has a connection; enable multiple connections first"
)
if not self.endpoint_accepts_connection(target, "target"):
raise ValueError(
f"Port {target_port.name!r} already has a connection; enable multiple connections first"
)
connection = Connection( connection = Connection(
str(uuid4()), str(uuid4()),
source, source,
@@ -256,6 +289,53 @@ class DocumentController(QObject):
self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection)) self.undo_stack.push(AddConnectionCommand(self, self.active_component_id, connection))
return connection.id return connection.id
def split_connection(
self,
connection_id: str,
position: QPointF,
first_waypoints: list[QPointF],
second_waypoints: list[QPointF],
) -> str:
if self.active_component_id is None:
raise ValueError("There is no active graph")
original = self.active_graph.connections.get(connection_id)
if original is None:
raise ValueError("The connection no longer exists")
port_type = self.connection_port_type(original)
junction = Junction(str(uuid4()), position.x(), position.y(), port_type)
first_properties = deepcopy(original.properties)
first_properties["waypoints"] = [
{"x": point.x(), "y": point.y()} for point in first_waypoints
]
second_properties = {
"waypoints": [{"x": point.x(), "y": point.y()} for point in second_waypoints]
}
first = Connection(
str(uuid4()),
original.source,
Endpoint(junction=junction.id),
original.name,
first_properties,
)
second = Connection(
str(uuid4()),
Endpoint(junction=junction.id),
original.target,
"",
second_properties,
)
self.undo_stack.push(
SplitConnectionCommand(
self,
self.active_component_id,
original,
junction,
first,
second,
)
)
return junction.id
def add_annotation( def add_annotation(
self, self,
kind: str, kind: str,
@@ -396,6 +476,16 @@ class DocumentController(QObject):
owner = self.active_component owner = self.active_component
if owner is None: if owner is None:
return None return None
if endpoint.junction is not None:
junction = owner.graph.junctions.get(endpoint.junction)
if junction is None:
return None
return Port(
junction.id,
"Junction",
type=junction.type,
allows_multiple_connections=role == "source",
)
if endpoint.interface is not None: if endpoint.interface is not None:
ports = owner.inputs if role == "source" else owner.outputs ports = owner.inputs if role == "source" else owner.outputs
else: else:
@@ -411,6 +501,17 @@ class DocumentController(QObject):
port = self._port_for_endpoint(connection.source, "source") port = self._port_for_endpoint(connection.source, "source")
return port.type if port is not None else "signal" return port.type if port is not None else "signal"
def endpoint_accepts_connection(self, endpoint: Endpoint, role: str) -> bool:
port = self._port_for_endpoint(endpoint, role)
if port is None:
return False
if role == "source" or port.allows_multiple_connections:
return True
return not any(
endpoint == (connection.source if role == "source" else connection.target)
for connection in self.active_graph.connections.values()
)
def add_interface_port(self, direction: str, position: QPointF) -> str: def add_interface_port(self, direction: str, position: QPointF) -> str:
component = self.active_component component = self.active_component
if component is None or component.implementation_kind != "graph": if component is None or component.implementation_kind != "graph":
@@ -468,7 +569,8 @@ class DocumentController(QObject):
self, self,
inputs: list[Port], inputs: list[Port],
outputs: list[Port], outputs: list[Port],
source: dict, equations: str,
parameters: list[Parameter],
) -> None: ) -> None:
component = self.active_component component = self.active_component
if component is None or component.implementation_kind != "text": if component is None or component.implementation_kind != "text":
@@ -477,6 +579,13 @@ class DocumentController(QObject):
output_ids = [port.id for port in outputs] output_ids = [port.id for port in outputs]
if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids): if len(set(input_ids)) != len(input_ids) or len(set(output_ids)) != len(output_ids):
raise ValueError("Input and output IDs must be unique") raise ValueError("Input and output IDs must be unique")
if any(not port.name.strip() for port in (*inputs, *outputs)):
raise ValueError("Every port must have a name")
parameter_ids = [parameter.id for parameter in parameters]
if len(set(parameter_ids)) != len(parameter_ids):
raise ValueError("Parameter IDs must be unique")
if any(not parameter.name.strip() for parameter in parameters):
raise ValueError("Every parameter must have a name")
if self.document is not None: if self.document is not None:
parent = self.document.find_parent(component.id) parent = self.document.find_parent(component.id)
if parent is not None: if parent is not None:
@@ -503,9 +612,18 @@ class DocumentController(QObject):
new = { new = {
"inputs": [port.to_dict() for port in inputs], "inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs], "outputs": [port.to_dict() for port in outputs],
"source": deepcopy(source), "source": {
"equations": equations,
"parameters": [parameter.to_dict() for parameter in parameters],
},
} }
if old != new: if old != new:
candidate = deepcopy(self.document)
candidate_component = candidate.find_component(component.id)
candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs)
candidate_component.source = deepcopy(new["source"])
candidate.validate()
self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new)) self.undo_stack.push(EditTextDefinitionCommand(self, component.id, old, new))
def edit_component_appearance( def edit_component_appearance(
@@ -516,25 +634,36 @@ class DocumentController(QObject):
inputs: list[Port], inputs: list[Port],
outputs: list[Port], outputs: list[Port],
show_subtree: bool, show_subtree: bool,
show_name: bool,
) -> None: ) -> None:
if self.document is None: if self.document is None:
return return
component = self.document.find_component(component_id) component = self.document.find_component(component_id)
if component is None: if component is None:
return return
siblings = self._component_siblings(component_id)
if any(item.id != component_id and item.name == name for item in siblings):
raise ValueError(f"A component named {name!r} already exists at this level")
old = { old = {
"name": component.name, "name": component.name,
"icon": component.icon.to_dict(), "icon": component.icon.to_dict(),
"inputs": [port.to_dict() for port in component.inputs], "inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs], "outputs": [port.to_dict() for port in component.outputs],
"show_subtree": component.show_subtree_in_library, "show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
} }
properties = deepcopy(component.properties)
was_visible = bool(properties.get("showName", False))
properties["showName"] = show_name
if show_name and not was_visible:
properties.pop("nameLabelPosition", None)
new = { new = {
"name": name, "name": name,
"icon": icon.to_dict(), "icon": icon.to_dict(),
"inputs": [port.to_dict() for port in inputs], "inputs": [port.to_dict() for port in inputs],
"outputs": [port.to_dict() for port in outputs], "outputs": [port.to_dict() for port in outputs],
"show_subtree": show_subtree, "show_subtree": show_subtree,
"properties": properties,
} }
if old != new: if old != new:
candidate = deepcopy(self.document) candidate = deepcopy(self.document)
@@ -543,9 +672,75 @@ class DocumentController(QObject):
candidate_component.icon = Icon.from_dict(icon.to_dict()) candidate_component.icon = Icon.from_dict(icon.to_dict())
candidate_component.inputs = deepcopy(inputs) candidate_component.inputs = deepcopy(inputs)
candidate_component.outputs = deepcopy(outputs) candidate_component.outputs = deepcopy(outputs)
candidate_component.properties = deepcopy(properties)
candidate.validate() candidate.validate()
self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new)) self.undo_stack.push(EditComponentAppearanceCommand(self, component_id, old, new))
def move_component_name_label(self, component_id: str, position: QPointF) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is None:
return
old = deepcopy(component.properties)
new = deepcopy(old)
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
if old != new:
self.undo_stack.push(
EditComponentPropertiesCommand(
self, component_id, old, new, "Move component name"
)
)
def edit_connection_options(
self, connection_id: str, name: str, show_name: bool
) -> None:
owner = self.active_component
if owner is None or self.active_component_id is None:
return
connection = owner.graph.connections.get(connection_id)
if connection is None:
return
old = {"name": connection.name, "properties": deepcopy(connection.properties)}
properties = deepcopy(connection.properties)
was_visible = bool(properties.get("showName", False))
properties["showName"] = show_name
if show_name and not was_visible:
properties.pop("nameLabelPosition", None)
new = {"name": name, "properties": properties}
if old != new:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"connection_data",
connection_id,
old,
new,
"Edit connection options",
)
)
def move_connection_name_label(self, connection_id: str, position: QPointF) -> None:
connection = self.active_graph.connections.get(connection_id)
if connection is None or self.active_component_id is None:
return
old = deepcopy(connection.properties)
new = deepcopy(old)
new["nameLabelPosition"] = {"x": position.x(), "y": position.y()}
if old != new:
self.undo_stack.push(
EditGraphItemCommand(
self,
self.active_component_id,
"connection",
connection_id,
old,
new,
"Move connection name",
)
)
def edit_component_ports( def edit_component_ports(
self, component_id: str, inputs: list[Port], outputs: list[Port] self, component_id: str, inputs: list[Port], outputs: list[Port]
) -> None: ) -> None:
@@ -585,6 +780,7 @@ class DocumentController(QObject):
"inputs": [port.to_dict() for port in component.inputs], "inputs": [port.to_dict() for port in component.inputs],
"outputs": [port.to_dict() for port in component.outputs], "outputs": [port.to_dict() for port in component.outputs],
"show_subtree": component.show_subtree_in_library, "show_subtree": component.show_subtree_in_library,
"properties": deepcopy(component.properties),
} }
new = { new = {
**old, **old,
@@ -649,10 +845,13 @@ class DocumentController(QObject):
pairs = [(source, clone_component(source)) for source in source_components] pairs = [(source, clone_component(source)) for source in source_components]
id_map = {source.id: clone.id for source, clone in pairs} id_map = {source.id: clone.id for source, clone in pairs}
blocks = {} blocks = {}
for _source, clone in pairs: used = list(owner.graph.blocks.values())
for source, clone in pairs:
clone.name = self._available_component_name(source.name, used, 0)
clone.x += offset.x() clone.x += offset.x()
clone.y += offset.y() clone.y += offset.y()
blocks[clone.id] = clone blocks[clone.id] = clone
used.append(clone)
connections = {} connections = {}
for source in source_connections: for source in source_connections:
if source.source.block not in id_map or source.target.block not in id_map: if source.source.block not in id_map or source.target.block not in id_map:
@@ -682,6 +881,26 @@ class DocumentController(QObject):
raise ValueError("The containing component is no longer in the document") raise ValueError("The containing component is no longer in the document")
return owner.graph return owner.graph
@staticmethod
def _available_component_name(
base: str, components, start: int = 0
) -> str:
used = {component.name for component in components}
number = start
while f"{base}{number}" in used:
number += 1
return f"{base}{number}"
def _component_siblings(self, component_id: str):
if self.document is None:
return ()
parent = self.document.find_parent(component_id)
return (
parent.graph.blocks.values()
if parent is not None
else self.document.roots.values()
)
def _insert_component(self, owner_id: str | None, component: Component) -> None: def _insert_component(self, owner_id: str | None, component: Component) -> None:
if self.document is None: if self.document is None:
raise ValueError("There is no open document") raise ValueError("There is no open document")
@@ -728,6 +947,36 @@ class DocumentController(QObject):
self.connectionAdded.emit(connection.id) self.connectionAdded.emit(connection.id)
self.documentReset.emit() self.documentReset.emit()
def _split_connection(
self,
owner_id: str,
original_id: str,
junction: Junction,
first: Connection,
second: Connection,
) -> None:
graph = self._graph_for(owner_id)
graph.connections.pop(original_id, None)
graph.junctions[junction.id] = junction
graph.connections[first.id] = first
graph.connections[second.id] = second
self.documentReset.emit()
def _restore_split_connection(
self,
owner_id: str,
original: Connection,
junction_id: str,
first_id: str,
second_id: str,
) -> None:
graph = self._graph_for(owner_id)
graph.connections.pop(first_id, None)
graph.connections.pop(second_id, None)
graph.junctions.pop(junction_id, None)
graph.connections[original.id] = original
self.documentReset.emit()
def _remove_connection(self, owner_id: str, connection_id: str) -> None: def _remove_connection(self, owner_id: str, connection_id: str) -> None:
self._graph_for(owner_id).connections.pop(connection_id, None) self._graph_for(owner_id).connections.pop(connection_id, None)
if owner_id == self.active_component_id: if owner_id == self.active_component_id:
@@ -750,7 +999,16 @@ class DocumentController(QObject):
self, owner_id: str, item_kind: str, item_id: str, values: dict self, owner_id: str, item_kind: str, item_id: str, values: dict
) -> None: ) -> None:
graph = self._graph_for(owner_id) graph = self._graph_for(owner_id)
if item_kind == "connection": if item_kind == "junction_geometry":
item = graph.junctions.get(item_id)
if item is not None:
item.x, item.y = float(values["x"]), float(values["y"])
elif item_kind == "connection_data":
item = graph.connections.get(item_id)
if item is not None:
item.name = values["name"]
item.properties = deepcopy(values["properties"])
elif item_kind == "connection":
item = graph.connections.get(item_id) item = graph.connections.get(item_id)
if item is not None: if item is not None:
item.properties = deepcopy(values) item.properties = deepcopy(values)
@@ -867,10 +1125,19 @@ class DocumentController(QObject):
component.inputs = [Port.from_dict(port) for port in values["inputs"]] component.inputs = [Port.from_dict(port) for port in values["inputs"]]
component.outputs = [Port.from_dict(port) for port in values["outputs"]] component.outputs = [Port.from_dict(port) for port in values["outputs"]]
component.show_subtree_in_library = values["show_subtree"] component.show_subtree_in_library = values["show_subtree"]
component.properties = deepcopy(values["properties"])
self.documentReset.emit() self.documentReset.emit()
if component_id == self.active_component_id: if component_id == self.active_component_id:
self.activeGraphChanged.emit() self.activeGraphChanged.emit()
def _set_component_properties(self, component_id: str, properties: dict) -> None:
if self.document is None:
return
component = self.document.find_component(component_id)
if component is not None:
component.properties = deepcopy(properties)
self.componentPropertiesChanged.emit(component_id)
def _delete_items( def _delete_items(
self, self,
owner_id: str | None, owner_id: str | None,
@@ -948,5 +1215,4 @@ class DocumentController(QObject):
component.source = deepcopy(values["source"]) component.source = deepcopy(values["source"])
self.interfaceChanged.emit() self.interfaceChanged.emit()
self.documentReset.emit() self.documentReset.emit()
if component_id == self.active_component_id: self.textDefinitionChanged.emit(component_id)
self.activeGraphChanged.emit()

View File

@@ -17,6 +17,7 @@ class ComponentOptionsDialog(QDialog):
self.ui.nameEdit.setText(component.name) self.ui.nameEdit.setText(component.name)
self.ui.editIconButton.clicked.connect(self.edit_icon) self.ui.editIconButton.clicked.connect(self.edit_icon)
self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library) self.ui.showSubtreeCheckBox.setChecked(component.show_subtree_in_library)
self.ui.showNameCheckBox.setChecked(bool(component.properties.get("showName", False)))
def edit_icon(self) -> None: def edit_icon(self) -> None:
working = Component.from_dict(self.component.to_dict()) working = Component.from_dict(self.component.to_dict())

View File

@@ -1,6 +1,7 @@
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QDialog, QDialog,
QDialogButtonBox, QDialogButtonBox,
QCheckBox,
QFormLayout, QFormLayout,
QLineEdit, QLineEdit,
QMessageBox, QMessageBox,
@@ -11,7 +12,15 @@ from PySide6.QtWidgets import (
class ItemOptionsDialog(QDialog): class ItemOptionsDialog(QDialog):
"""Small, extensible options dialog shared by ports and connections.""" """Small, extensible options dialog shared by ports and connections."""
def __init__(self, title: str, name: str, parent=None, *, name_required: bool = True) -> None: def __init__(
self,
title: str,
name: str,
parent=None,
*,
name_required: bool = True,
show_name: bool | None = None,
) -> None:
super().__init__(parent) super().__init__(parent)
self.name_required = name_required self.name_required = name_required
self.setWindowTitle(title) self.setWindowTitle(title)
@@ -21,6 +30,11 @@ class ItemOptionsDialog(QDialog):
self.form = QFormLayout() self.form = QFormLayout()
self.name_edit = QLineEdit(name, self) self.name_edit = QLineEdit(name, self)
self.form.addRow("Name:", self.name_edit) self.form.addRow("Name:", self.name_edit)
self.show_name_check = None
if show_name is not None:
self.show_name_check = QCheckBox("Show name below connection", self)
self.show_name_check.setChecked(show_name)
self.form.addRow("", self.show_name_check)
layout.addLayout(self.form) layout.addLayout(self.form)
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
@@ -35,6 +49,10 @@ class ItemOptionsDialog(QDialog):
def name(self) -> str: def name(self) -> str:
return self.name_edit.text().strip() return self.name_edit.text().strip()
@property
def show_name(self) -> bool:
return bool(self.show_name_check and self.show_name_check.isChecked())
def accept(self) -> None: def accept(self) -> None:
if self.name_required and not self.name: if self.name_required and not self.name:
QMessageBox.warning(self, "Invalid name", "The name cannot be empty.") QMessageBox.warning(self, "Invalid name", "The name cannot be empty.")

View File

@@ -36,6 +36,7 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.textEdited.connect(self._store_current) self.ui.nameEdit.textEdited.connect(self._store_current)
self.ui.typeCombo.currentIndexChanged.connect(self._store_current) self.ui.typeCombo.currentIndexChanged.connect(self._store_current)
self.ui.orientationCombo.currentIndexChanged.connect(self._store_current) self.ui.orientationCombo.currentIndexChanged.connect(self._store_current)
self.ui.multipleConnectionsCheckBox.toggled.connect(self._store_current)
self.ui.portSplitter.setSizes([250, 370]) self.ui.portSplitter.setSizes([250, 370])
if read_only: if read_only:
self.ui.addPortButton.setEnabled(False) self.ui.addPortButton.setEnabled(False)
@@ -43,6 +44,7 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.setReadOnly(True) self.ui.nameEdit.setReadOnly(True)
self.ui.typeCombo.setEnabled(False) self.ui.typeCombo.setEnabled(False)
self.ui.orientationCombo.setEnabled(False) self.ui.orientationCombo.setEnabled(False)
self.ui.multipleConnectionsCheckBox.setEnabled(False)
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("Close") self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setText("Close")
self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide() self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Cancel).hide()
self._rebuild_list(0 if self.ports else -1) self._rebuild_list(0 if self.ports else -1)
@@ -72,6 +74,9 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.setText(port.name) self.ui.nameEdit.setText(port.name)
self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type)) self.ui.typeCombo.setCurrentIndex(self.ui.typeCombo.findData(port.type))
self.ui.orientationCombo.setCurrentIndex(self.ui.orientationCombo.findData(orientation)) self.ui.orientationCombo.setCurrentIndex(self.ui.orientationCombo.findData(orientation))
self.ui.multipleConnectionsCheckBox.setChecked(
port.allows_multiple_connections
)
else: else:
self.ui.nameEdit.clear() self.ui.nameEdit.clear()
self._loading = False self._loading = False
@@ -83,6 +88,7 @@ class PortOptionsDialog(QDialog):
self.ui.nameEdit.setEnabled(enabled) self.ui.nameEdit.setEnabled(enabled)
self.ui.typeCombo.setEnabled(enabled) self.ui.typeCombo.setEnabled(enabled)
self.ui.orientationCombo.setEnabled(enabled) self.ui.orientationCombo.setEnabled(enabled)
self.ui.multipleConnectionsCheckBox.setEnabled(enabled)
def _store_current(self) -> None: def _store_current(self) -> None:
row = self.ui.portList.currentRow() row = self.ui.portList.currentRow()
@@ -91,6 +97,7 @@ class PortOptionsDialog(QDialog):
port, _orientation = self.ports[row] port, _orientation = self.ports[row]
port.name = self.ui.nameEdit.text() port.name = self.ui.nameEdit.text()
port.type = self.ui.typeCombo.currentData() port.type = self.ui.typeCombo.currentData()
port.allows_multiple_connections = self.ui.multipleConnectionsCheckBox.isChecked()
self.ports[row] = (port, self.ui.orientationCombo.currentData()) self.ports[row] = (port, self.ui.orientationCombo.currentData())
self.ui.portList.item(row).setText( self.ui.portList.item(row).setText(
f"{port.name} [{self.ports[row][1]}, {port.type}]" f"{port.name} [{self.ports[row][1]}, {port.type}]"

View File

@@ -0,0 +1 @@
"""Reusable editing widgets."""

View File

@@ -0,0 +1,199 @@
from copy import deepcopy
from uuid import uuid4
from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QCheckBox, QComboBox, QHeaderView, QTableWidgetItem, QWidget
from bedit.core.model import Parameter, Port
from bedit.core.port_types import PortTypeRegistry
from bedit.gui.generated.ui_text_definition_editor import Ui_TextDefinitionEditor
ID_ROLE = Qt.ItemDataRole.UserRole
PROPERTIES_ROLE = Qt.ItemDataRole.UserRole + 1
class TextDefinitionEditor(QWidget):
"""Editor for a text component's equations, ports, and parameters."""
modifiedChanged = Signal(bool)
definitionEdited = Signal()
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.ui = Ui_TextDefinitionEditor()
self.ui.setupUi(self)
self._modified = False
self._loading = False
self.ui.portsTable.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch
)
self.ui.parametersTable.horizontalHeader().setSectionResizeMode(
QHeaderView.ResizeMode.Stretch
)
self.ui.columnSplitter.setSizes([560, 340])
self.ui.definitionSplitter.setSizes([300, 300])
self.ui.equationsEdit.textChanged.connect(self._mark_modified)
self.ui.portsTable.cellChanged.connect(self._mark_modified)
self.ui.parametersTable.cellChanged.connect(self._mark_modified)
self.ui.addPortButton.clicked.connect(self.add_port)
self.ui.removePortButton.clicked.connect(self.remove_port)
self.ui.addParameterButton.clicked.connect(self.add_parameter)
self.ui.removeParameterButton.clicked.connect(self.remove_parameter)
self.ui.portsTable.itemSelectionChanged.connect(self._update_buttons)
self.ui.parametersTable.itemSelectionChanged.connect(self._update_buttons)
self._update_buttons()
@property
def is_modified(self) -> bool:
return self._modified
@property
def equations(self) -> str:
return self.ui.equationsEdit.toPlainText()
@property
def ports(self) -> tuple[list[Port], list[Port]]:
inputs: list[Port] = []
outputs: list[Port] = []
for row in range(self.ui.portsTable.rowCount()):
name_item = self.ui.portsTable.item(row, 0)
type_combo = self.ui.portsTable.cellWidget(row, 1)
orientation_combo = self.ui.portsTable.cellWidget(row, 2)
multiple_check = self.ui.portsTable.cellWidget(row, 3)
port = Port(
id=name_item.data(ID_ROLE),
name=name_item.text().strip(),
type=type_combo.currentData(),
properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}),
allows_multiple_connections=multiple_check.isChecked(),
)
target = inputs if orientation_combo.currentData() == "input" else outputs
target.append(port)
return inputs, outputs
@property
def parameters(self) -> list[Parameter]:
table = self.ui.parametersTable
return [
Parameter(
id=table.item(row, 0).data(ID_ROLE),
name=table.item(row, 0).text().strip(),
type=table.item(row, 1).text().strip(),
value=table.item(row, 2).text(),
)
for row in range(table.rowCount())
]
def set_definition(
self,
equations: str,
inputs: list[Port],
outputs: list[Port],
parameters: list[Parameter],
) -> None:
self._loading = True
self.ui.equationsEdit.setPlainText(equations)
self.ui.portsTable.setRowCount(0)
for port in inputs:
self._append_port(port, "input")
for port in outputs:
self._append_port(port, "output")
self.ui.parametersTable.setRowCount(0)
for parameter in parameters:
self._append_parameter(parameter)
self._loading = False
self.set_modified(False)
self._update_buttons()
def set_modified(self, modified: bool) -> None:
if self._modified != modified:
self._modified = modified
self.modifiedChanged.emit(modified)
def _mark_modified(self, *_args) -> None:
if not self._loading:
self.set_modified(True)
self.definitionEdited.emit()
def _new_combo(self, values: list[tuple[str, str]], current: str) -> QComboBox:
combo = QComboBox(self)
for label, value in values:
combo.addItem(label, value)
combo.setCurrentIndex(max(0, combo.findData(current)))
combo.currentIndexChanged.connect(self._mark_modified)
return combo
def _append_port(self, port: Port, orientation: str) -> None:
table = self.ui.portsTable
row = table.rowCount()
table.insertRow(row)
name = QTableWidgetItem(port.name)
name.setData(ID_ROLE, port.id)
name.setData(PROPERTIES_ROLE, deepcopy(port.properties))
table.setItem(row, 0, name)
types = [(item.display_name, item.id) for item in PortTypeRegistry.all()]
table.setCellWidget(row, 1, self._new_combo(types, port.type))
table.setCellWidget(
row,
2,
self._new_combo([("Input", "input"), ("Output", "output")], orientation),
)
multiple = QCheckBox("Any", self)
multiple.setChecked(port.allows_multiple_connections)
multiple.toggled.connect(self._mark_modified)
table.setCellWidget(row, 3, multiple)
def _append_parameter(self, parameter: Parameter) -> None:
table = self.ui.parametersTable
row = table.rowCount()
table.insertRow(row)
name = QTableWidgetItem(parameter.name)
name.setData(ID_ROLE, parameter.id)
table.setItem(row, 0, name)
table.setItem(row, 1, QTableWidgetItem(parameter.type))
table.setItem(row, 2, QTableWidgetItem(parameter.value))
def add_port(self) -> None:
port = Port(
id=f"port-{uuid4().hex[:8]}",
name=f"Port {self.ui.portsTable.rowCount() + 1}",
type="signal",
properties={"iconPosition": {"x": 0.0, "y": 0.0}},
)
self._loading = True
self._append_port(port, "input")
self._loading = False
self.ui.portsTable.selectRow(self.ui.portsTable.rowCount() - 1)
self._mark_modified()
def remove_port(self) -> None:
row = self.ui.portsTable.currentRow()
if row >= 0:
self.ui.portsTable.removeRow(row)
self._mark_modified()
self._update_buttons()
def add_parameter(self) -> None:
parameter = Parameter(
id=f"parameter-{uuid4().hex[:8]}",
name=f"Parameter {self.ui.parametersTable.rowCount() + 1}",
)
self._loading = True
self._append_parameter(parameter)
self._loading = False
self.ui.parametersTable.selectRow(self.ui.parametersTable.rowCount() - 1)
self._mark_modified()
def remove_parameter(self) -> None:
row = self.ui.parametersTable.currentRow()
if row >= 0:
self.ui.parametersTable.removeRow(row)
self._mark_modified()
self._update_buttons()
def _update_buttons(self) -> None:
self.ui.removePortButton.setEnabled(self.ui.portsTable.currentRow() >= 0)
self.ui.removeParameterButton.setEnabled(
self.ui.parametersTable.currentRow() >= 0
)

View File

@@ -55,6 +55,11 @@ class Ui_ComponentOptionsDialog(object):
self.optionsForm.setWidget(2, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox) self.optionsForm.setWidget(2, QFormLayout.ItemRole.SpanningRole, self.showSubtreeCheckBox)
self.showNameCheckBox = QCheckBox(ComponentOptionsDialog)
self.showNameCheckBox.setObjectName(u"showNameCheckBox")
self.optionsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.showNameCheckBox)
self.dialogLayout.addLayout(self.optionsForm) self.dialogLayout.addLayout(self.optionsForm)
@@ -85,5 +90,6 @@ class Ui_ComponentOptionsDialog(object):
self.editIconButton.setToolTip(QCoreApplication.translate("ComponentOptionsDialog", u"Open the vector icon and port-position editor", None)) self.editIconButton.setToolTip(QCoreApplication.translate("ComponentOptionsDialog", u"Open the vector icon and port-position editor", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None)) self.showSubtreeCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show contained components in the Libraries tree", None))
self.showNameCheckBox.setText(QCoreApplication.translate("ComponentOptionsDialog", u"Show name below component", None))
# retranslateUi # retranslateUi

View File

@@ -18,10 +18,11 @@ from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
QTransform) QTransform)
from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout, from PySide6.QtWidgets import (QApplication, QDockWidget, QFrame, QHBoxLayout,
QHeaderView, QLabel, QMainWindow, QMenu, QHeaderView, QLabel, QMainWindow, QMenu,
QMenuBar, QPlainTextEdit, QPushButton, QSizePolicy, QMenuBar, QSizePolicy, QSpacerItem, QSplitter,
QSpacerItem, QSplitter, QStackedWidget, QToolBar, QStackedWidget, QToolBar, QToolButton, QTreeView,
QToolButton, QTreeView, QVBoxLayout, QWidget) QVBoxLayout, QWidget)
from bedit.gui.editors.text_definition import TextDefinitionEditor
from bedit.gui.graphics.workspace import GraphWorkspaceView from bedit.gui.graphics.workspace import GraphWorkspaceView
from . import resources_rc from . import resources_rc
@@ -60,6 +61,8 @@ class Ui_MainWindow(object):
icon5 = QIcon() icon5 = QIcon()
icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off) icon5.addFile(u":/icons/icons/document-open.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
self.actionOpen.setIcon(icon5) self.actionOpen.setIcon(icon5)
self.actionReloadLibraries = QAction(MainWindow)
self.actionReloadLibraries.setObjectName(u"actionReloadLibraries")
self.actionSave = QAction(MainWindow) self.actionSave = QAction(MainWindow)
self.actionSave.setObjectName(u"actionSave") self.actionSave.setObjectName(u"actionSave")
icon6 = QIcon() icon6 = QIcon()
@@ -217,12 +220,6 @@ class Ui_MainWindow(object):
self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer) self.workspaceHeaderLayout.addItem(self.workspaceHeaderSpacer)
self.applyJsonButton = QPushButton(self.workspaceHeader)
self.applyJsonButton.setObjectName(u"applyJsonButton")
self.applyJsonButton.setVisible(False)
self.workspaceHeaderLayout.addWidget(self.applyJsonButton)
self.pointerToolButton = QToolButton(self.workspaceHeader) self.pointerToolButton = QToolButton(self.workspaceHeader)
self.pointerToolButton.setObjectName(u"pointerToolButton") self.pointerToolButton.setObjectName(u"pointerToolButton")
icon15 = QIcon() icon15 = QIcon()
@@ -291,18 +288,17 @@ class Ui_MainWindow(object):
self.graphPageLayout.addWidget(self.graphView) self.graphPageLayout.addWidget(self.graphView)
self.workspaceStack.addWidget(self.graphPage) self.workspaceStack.addWidget(self.graphPage)
self.jsonPage = QWidget() self.textPage = QWidget()
self.jsonPage.setObjectName(u"jsonPage") self.textPage.setObjectName(u"textPage")
self.jsonPageLayout = QVBoxLayout(self.jsonPage) self.textPageLayout = QVBoxLayout(self.textPage)
self.jsonPageLayout.setObjectName(u"jsonPageLayout") self.textPageLayout.setObjectName(u"textPageLayout")
self.jsonPageLayout.setContentsMargins(0, 0, 0, 0) self.textPageLayout.setContentsMargins(0, 0, 0, 0)
self.jsonEditor = QPlainTextEdit(self.jsonPage) self.textDefinitionEditor = TextDefinitionEditor(self.textPage)
self.jsonEditor.setObjectName(u"jsonEditor") self.textDefinitionEditor.setObjectName(u"textDefinitionEditor")
self.jsonEditor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.jsonPageLayout.addWidget(self.jsonEditor) self.textPageLayout.addWidget(self.textDefinitionEditor)
self.workspaceStack.addWidget(self.jsonPage) self.workspaceStack.addWidget(self.textPage)
self.emptyPage = QWidget() self.emptyPage = QWidget()
self.emptyPage.setObjectName(u"emptyPage") self.emptyPage.setObjectName(u"emptyPage")
self.emptyPage.setStyleSheet(u"background-color: #9a9a9a;") self.emptyPage.setStyleSheet(u"background-color: #9a9a9a;")
@@ -364,6 +360,7 @@ class Ui_MainWindow(object):
self.menubar.addAction(self.menuHelp.menuAction()) self.menubar.addAction(self.menuHelp.menuAction())
self.menuFile.addAction(self.actionNew) self.menuFile.addAction(self.actionNew)
self.menuFile.addAction(self.actionOpen) self.menuFile.addAction(self.actionOpen)
self.menuFile.addAction(self.actionReloadLibraries)
self.menuFile.addSeparator() self.menuFile.addSeparator()
self.menuFile.addAction(self.actionSave) self.menuFile.addAction(self.actionSave)
self.menuFile.addAction(self.actionSaveAs) self.menuFile.addAction(self.actionSaveAs)
@@ -439,6 +436,13 @@ class Ui_MainWindow(object):
#endif // QT_CONFIG(statustip) #endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut) #if QT_CONFIG(shortcut)
self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None)) self.actionOpen.setShortcut(QCoreApplication.translate("MainWindow", u"Ctrl+O", None))
#endif // QT_CONFIG(shortcut)
self.actionReloadLibraries.setText(QCoreApplication.translate("MainWindow", u"Reload &Libraries", None))
#if QT_CONFIG(statustip)
self.actionReloadLibraries.setStatusTip(QCoreApplication.translate("MainWindow", u"Reload configured library files from disk", None))
#endif // QT_CONFIG(statustip)
#if QT_CONFIG(shortcut)
self.actionReloadLibraries.setShortcut(QCoreApplication.translate("MainWindow", u"F5", None))
#endif // QT_CONFIG(shortcut) #endif // QT_CONFIG(shortcut)
self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None)) self.actionSave.setText(QCoreApplication.translate("MainWindow", u"&Save", None))
#if QT_CONFIG(statustip) #if QT_CONFIG(statustip)
@@ -505,7 +509,6 @@ class Ui_MainWindow(object):
self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None)) self.navigateDownButton.setText(QCoreApplication.translate("MainWindow", u"Down", None))
self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None)) self.graphBreadcrumbLabel.setText(QCoreApplication.translate("MainWindow", u"Untitled", None))
self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None)) self.workspaceModeLabel.setText(QCoreApplication.translate("MainWindow", u"Graph", None))
self.applyJsonButton.setText(QCoreApplication.translate("MainWindow", u"Apply JSON", None))
self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None)) self.pointerToolButton.setText(QCoreApplication.translate("MainWindow", u"Pointer", None))
self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None)) self.connectToolButton.setText(QCoreApplication.translate("MainWindow", u"Connect", None))
#if QT_CONFIG(tooltip) #if QT_CONFIG(tooltip)
@@ -524,7 +527,6 @@ class Ui_MainWindow(object):
self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None)) self.rotateToolButton.setToolTip(QCoreApplication.translate("MainWindow", u"Rotate selected blocks clockwise", None))
#endif // QT_CONFIG(tooltip) #endif // QT_CONFIG(tooltip)
self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None)) self.rotateToolButton.setText(QCoreApplication.translate("MainWindow", u"Rotate", None))
self.jsonEditor.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Component JSON", None))
self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None)) self.emptyWorkspaceLabel.setText(QCoreApplication.translate("MainWindow", u"No document open", None))
self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None)) self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"&File", None))
self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None)) self.menuEdit.setTitle(QCoreApplication.translate("MainWindow", u"&Edit", None))

View File

@@ -15,10 +15,11 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon, QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter, QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QComboBox,
QDialogButtonBox, QFormLayout, QHBoxLayout, QLabel, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout,
QLineEdit, QListWidget, QListWidgetItem, QPushButton, QLabel, QLineEdit, QListWidget, QListWidgetItem,
QSizePolicy, QSplitter, QVBoxLayout, QWidget) QPushButton, QSizePolicy, QSplitter, QVBoxLayout,
QWidget)
class Ui_PortOptionsDialog(object): class Ui_PortOptionsDialog(object):
def setupUi(self, PortOptionsDialog): def setupUi(self, PortOptionsDialog):
@@ -94,11 +95,16 @@ class Ui_PortOptionsDialog(object):
self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo) self.portDetailsForm.setWidget(2, QFormLayout.ItemRole.FieldRole, self.orientationCombo)
self.multipleConnectionsCheckBox = QCheckBox(self.portDetailsPanel)
self.multipleConnectionsCheckBox.setObjectName(u"multipleConnectionsCheckBox")
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.multipleConnectionsCheckBox)
self.positionHintLabel = QLabel(self.portDetailsPanel) self.positionHintLabel = QLabel(self.portDetailsPanel)
self.positionHintLabel.setObjectName(u"positionHintLabel") self.positionHintLabel.setObjectName(u"positionHintLabel")
self.positionHintLabel.setWordWrap(True) self.positionHintLabel.setWordWrap(True)
self.portDetailsForm.setWidget(3, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel) self.portDetailsForm.setWidget(4, QFormLayout.ItemRole.SpanningRole, self.positionHintLabel)
self.portSplitter.addWidget(self.portDetailsPanel) self.portSplitter.addWidget(self.portDetailsPanel)
@@ -130,6 +136,7 @@ class Ui_PortOptionsDialog(object):
self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None)) self.orientationCombo.setItemText(0, QCoreApplication.translate("PortOptionsDialog", u"Input", None))
self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", None)) self.orientationCombo.setItemText(1, QCoreApplication.translate("PortOptionsDialog", u"Output", None))
self.multipleConnectionsCheckBox.setText(QCoreApplication.translate("PortOptionsDialog", u"Allow multiple connections", None))
self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None)) self.positionHintLabel.setText(QCoreApplication.translate("PortOptionsDialog", u"New ports start at (0, 0) in the icon editor.", None))
# retranslateUi # retranslateUi

View File

@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'text_definition_editor.ui'
##
## Created by: Qt User Interface Compiler version 6.11.1
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QGroupBox, QHBoxLayout,
QHeaderView, QPlainTextEdit, QPushButton, QSizePolicy,
QSpacerItem, QSplitter, QTableWidget, QTableWidgetItem,
QVBoxLayout, QWidget)
class Ui_TextDefinitionEditor(object):
def setupUi(self, TextDefinitionEditor):
if not TextDefinitionEditor.objectName():
TextDefinitionEditor.setObjectName(u"TextDefinitionEditor")
TextDefinitionEditor.resize(900, 600)
self.editorLayout = QHBoxLayout(TextDefinitionEditor)
self.editorLayout.setObjectName(u"editorLayout")
self.editorLayout.setContentsMargins(6, 6, 6, 6)
self.columnSplitter = QSplitter(TextDefinitionEditor)
self.columnSplitter.setObjectName(u"columnSplitter")
self.columnSplitter.setOrientation(Qt.Orientation.Horizontal)
self.columnSplitter.setChildrenCollapsible(False)
self.equationsGroup = QGroupBox(self.columnSplitter)
self.equationsGroup.setObjectName(u"equationsGroup")
self.equationsLayout = QVBoxLayout(self.equationsGroup)
self.equationsLayout.setObjectName(u"equationsLayout")
self.equationsEdit = QPlainTextEdit(self.equationsGroup)
self.equationsEdit.setObjectName(u"equationsEdit")
self.equationsEdit.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.equationsLayout.addWidget(self.equationsEdit)
self.columnSplitter.addWidget(self.equationsGroup)
self.definitionSplitter = QSplitter(self.columnSplitter)
self.definitionSplitter.setObjectName(u"definitionSplitter")
self.definitionSplitter.setOrientation(Qt.Orientation.Vertical)
self.definitionSplitter.setChildrenCollapsible(False)
self.portsGroup = QGroupBox(self.definitionSplitter)
self.portsGroup.setObjectName(u"portsGroup")
self.portsLayout = QVBoxLayout(self.portsGroup)
self.portsLayout.setObjectName(u"portsLayout")
self.portsTable = QTableWidget(self.portsGroup)
if (self.portsTable.columnCount() < 4):
self.portsTable.setColumnCount(4)
__qtablewidgetitem = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__qtablewidgetitem1 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
__qtablewidgetitem2 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem()
self.portsTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
self.portsTable.setObjectName(u"portsTable")
self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.portsTable.setColumnCount(4)
self.portsLayout.addWidget(self.portsTable)
self.portButtonsLayout = QHBoxLayout()
self.portButtonsLayout.setObjectName(u"portButtonsLayout")
self.addPortButton = QPushButton(self.portsGroup)
self.addPortButton.setObjectName(u"addPortButton")
self.portButtonsLayout.addWidget(self.addPortButton)
self.removePortButton = QPushButton(self.portsGroup)
self.removePortButton.setObjectName(u"removePortButton")
self.portButtonsLayout.addWidget(self.removePortButton)
self.portButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.portButtonsLayout.addItem(self.portButtonSpacer)
self.portsLayout.addLayout(self.portButtonsLayout)
self.definitionSplitter.addWidget(self.portsGroup)
self.parametersGroup = QGroupBox(self.definitionSplitter)
self.parametersGroup.setObjectName(u"parametersGroup")
self.parametersLayout = QVBoxLayout(self.parametersGroup)
self.parametersLayout.setObjectName(u"parametersLayout")
self.parametersTable = QTableWidget(self.parametersGroup)
if (self.parametersTable.columnCount() < 3):
self.parametersTable.setColumnCount(3)
__qtablewidgetitem4 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem4)
__qtablewidgetitem5 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem5)
__qtablewidgetitem6 = QTableWidgetItem()
self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem6)
self.parametersTable.setObjectName(u"parametersTable")
self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
self.parametersTable.setColumnCount(3)
self.parametersLayout.addWidget(self.parametersTable)
self.parameterButtonsLayout = QHBoxLayout()
self.parameterButtonsLayout.setObjectName(u"parameterButtonsLayout")
self.addParameterButton = QPushButton(self.parametersGroup)
self.addParameterButton.setObjectName(u"addParameterButton")
self.parameterButtonsLayout.addWidget(self.addParameterButton)
self.removeParameterButton = QPushButton(self.parametersGroup)
self.removeParameterButton.setObjectName(u"removeParameterButton")
self.parameterButtonsLayout.addWidget(self.removeParameterButton)
self.parameterButtonSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum)
self.parameterButtonsLayout.addItem(self.parameterButtonSpacer)
self.parametersLayout.addLayout(self.parameterButtonsLayout)
self.definitionSplitter.addWidget(self.parametersGroup)
self.columnSplitter.addWidget(self.definitionSplitter)
self.editorLayout.addWidget(self.columnSplitter)
self.retranslateUi(TextDefinitionEditor)
QMetaObject.connectSlotsByName(TextDefinitionEditor)
# setupUi
def retranslateUi(self, TextDefinitionEditor):
self.equationsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Equations", None))
self.equationsEdit.setPlaceholderText(QCoreApplication.translate("TextDefinitionEditor", u"Enter equations here\u2026", None))
self.portsGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Ports", None))
___qtablewidgetitem = self.portsTable.horizontalHeaderItem(0)
___qtablewidgetitem.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
___qtablewidgetitem1 = self.portsTable.horizontalHeaderItem(1)
___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2)
___qtablewidgetitem2.setText(QCoreApplication.translate("TextDefinitionEditor", u"Orientation", None))
___qtablewidgetitem3 = self.portsTable.horizontalHeaderItem(3)
___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Multiple", None))
self.addPortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Port", None))
self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None))
self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None))
___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(0)
___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(1)
___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
___qtablewidgetitem6 = self.parametersTable.horizontalHeaderItem(2)
___qtablewidgetitem6.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None))
self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None))
self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None))
pass
# retranslateUi

View File

@@ -23,6 +23,7 @@ from PySide6.QtWidgets import (
QGraphicsScene, QGraphicsScene,
QGraphicsSceneContextMenuEvent, QGraphicsSceneContextMenuEvent,
QGraphicsSceneMouseEvent, QGraphicsSceneMouseEvent,
QGraphicsSimpleTextItem,
QGraphicsView, QGraphicsView,
QApplication, QApplication,
QMenu, QMenu,
@@ -32,7 +33,7 @@ from PySide6.QtWidgets import (
QWidget, QWidget,
) )
from bedit.core.model import Annotation, Component, Connection, Endpoint, Port from bedit.core.model import Annotation, Component, Connection, Endpoint, Junction, Port
from bedit.gui.controllers.document import DocumentController from bedit.gui.controllers.document import DocumentController
from bedit.gui.dialogs.connection_chooser import ConnectionChooserDialog from bedit.gui.dialogs.connection_chooser import ConnectionChooserDialog
from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE from bedit.gui.models.library_tree import COMPONENT_MIME_TYPE
@@ -78,6 +79,84 @@ class ConnectionPortItem(QGraphicsEllipseItem):
self.setToolTip(label) self.setToolTip(label)
class JunctionGraphicsItem(QGraphicsEllipseItem):
def __init__(self, junction: Junction, controller: DocumentController) -> None:
super().__init__(-5, -5, 10, 10)
self.junction = junction
self.controller = controller
self.endpoint = Endpoint(junction=junction.id)
self.drag_start = QPointF(junction.x, junction.y)
self.setPos(junction.x, junction.y)
self.setBrush(QColor("#303030"))
self.setPen(QPen(QColor("#ffffff"), 1))
self.setZValue(3)
self.setToolTip("Connection junction")
self.setCursor(Qt.CursorShape.SizeAllCursor)
self.setFlags(
QGraphicsItem.GraphicsItemFlag.ItemIsMovable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
self.drag_start = self.pos()
super().mousePressEvent(event)
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
super().mouseReleaseEvent(event)
self.controller.move_junction(self.junction.id, self.drag_start, self.pos())
def itemChange(self, change, value): # noqa: N802
if change == QGraphicsItem.GraphicsItemChange.ItemPositionChange:
return _snapped(value)
if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.update_connections_for_junction(self.junction.id)
return super().itemChange(change, value)
class NameLabelItem(QGraphicsSimpleTextItem):
"""Movable italic name label whose position is stored by its owner."""
def __init__(
self,
text: str,
owner_kind: str,
owner_id: str,
controller: DocumentController,
parent: QGraphicsItem | None = None,
) -> None:
super().__init__(text, parent)
self.owner_kind = owner_kind
self.owner_id = owner_id
self.controller = controller
font = self.font()
font.setItalic(True)
self.setFont(font)
self.setBrush(QColor("#303030"))
self.setCursor(Qt.CursorShape.SizeAllCursor)
self.drag_offset = QPointF()
def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
parent = self.parentItem()
point = parent.mapFromScene(event.scenePos()) if parent else event.scenePos()
self.drag_offset = self.pos() - point
event.accept()
def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
parent = self.parentItem()
point = parent.mapFromScene(event.scenePos()) if parent else event.scenePos()
self.setPos(_snapped(point + self.drag_offset))
event.accept()
def mouseReleaseEvent(self, event: QGraphicsSceneMouseEvent) -> None: # noqa: N802
if self.owner_kind == "component":
self.controller.move_component_name_label(self.owner_id, self.pos())
else:
self.controller.move_connection_name_label(self.owner_id, self.pos())
event.accept()
class ComponentGraphicsItem(QGraphicsObject): class ComponentGraphicsItem(QGraphicsObject):
WIDTH = 128.0 WIDTH = 128.0
HEIGHT = 128.0 HEIGHT = 128.0
@@ -98,6 +177,31 @@ class ComponentGraphicsItem(QGraphicsObject):
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH) self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
self.setTransformOriginPoint(self.hitbox.center()) self.setTransformOriginPoint(self.hitbox.center())
self.setRotation(component.rotation) self.setRotation(component.rotation)
self.name_label: NameLabelItem | None = None
self.sync_name_label(component)
def sync_name_label(self, component: Component) -> None:
if component.properties.get("showName", False):
if self.name_label is None:
self.name_label = NameLabelItem(
component.name, "component", component.id, self.controller, self
)
self.name_label.setText(component.name)
position = component.properties.get("nameLabelPosition")
if isinstance(position, dict):
self.name_label.setPos(float(position["x"]), float(position["y"]))
else:
bounds = self.name_label.boundingRect()
self.name_label.setPos(
self.hitbox.center().x() - bounds.width() / 2,
self.hitbox.bottom() + 6,
)
self.name_label.setRotation(-component.rotation)
elif self.name_label is not None:
if self.name_label.scene() is not None:
self.name_label.setParentItem(None)
self.name_label.scene().removeItem(self.name_label)
self.name_label = None
def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]: def _create_ports(self, ports, role: str, x: float) -> dict[str, ConnectionPortItem]:
result = {} result = {}
@@ -180,8 +284,8 @@ class ComponentGraphicsItem(QGraphicsObject):
class InterfaceTerminalItem(QGraphicsObject): class InterfaceTerminalItem(QGraphicsObject):
WIDTH = 110.0 WIDTH = 128.0
HEIGHT = 36.0 HEIGHT = 32.0
def __init__(self, port: Port, direction: str, controller: DocumentController) -> None: def __init__(self, port: Port, direction: str, controller: DocumentController) -> None:
super().__init__() super().__init__()
@@ -255,13 +359,16 @@ class InterfaceTerminalItem(QGraphicsObject):
class ConnectionGraphicsItem(QGraphicsPathItem): class ConnectionGraphicsItem(QGraphicsPathItem):
def __init__( def __init__(
self, self,
connection_id: str, connection: Connection,
name: str = "", controller: DocumentController,
style: ConnectionStyle | None = None, style: ConnectionStyle | None = None,
) -> None: ) -> None:
super().__init__() super().__init__()
self.connection_id = connection_id self.connection_id = connection.id
self.name = name self.name = connection.name
self.source_is_junction = connection.source.junction is not None
self.target_is_junction = connection.target.junction is not None
self.controller = controller
self.style = style or ConnectionStyle() self.style = style or ConnectionStyle()
self.start = QPointF() self.start = QPointF()
self.end = QPointF() self.end = QPointF()
@@ -270,8 +377,34 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable) self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable)
self._update_pen() self._update_pen()
self.setZValue(0) self.setZValue(0)
self.setToolTip(name or "Connection") self.setToolTip(connection.name or "Connection")
self.handles: list[WaypointHandle] = [] self.handles: list[WaypointHandle] = []
self.name_label: NameLabelItem | None = None
self.sync_name_label(connection)
def sync_name_label(self, connection: Connection) -> None:
visible = bool(connection.properties.get("showName", False))
if not visible:
if self.name_label is not None and self.name_label.scene() is not None:
self.name_label.setParentItem(None)
self.name_label.scene().removeItem(self.name_label)
self.name_label = None
return
if self.name_label is None:
self.name_label = NameLabelItem(
connection.name, "connection", connection.id, self.controller, self
)
self.name_label.setText(connection.name)
position = connection.properties.get("nameLabelPosition")
if isinstance(position, dict):
self.name_label.setPos(float(position["x"]), float(position["y"]))
elif not self.path().isEmpty():
bounds = self.path().boundingRect()
label_bounds = self.name_label.boundingRect()
self.name_label.setPos(
bounds.center().x() - label_bounds.width() / 2,
bounds.bottom() + 6,
)
def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802 def contextMenuEvent(self, event: QGraphicsSceneContextMenuEvent) -> None: # noqa: N802
if not self.isSelected(): if not self.isSelected():
@@ -279,6 +412,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.setSelected(True) self.setSelected(True)
menu = QMenu() menu = QMenu()
add_node_action = menu.addAction("Add Node") add_node_action = menu.addAction("Add Node")
add_junction_action = menu.addAction("Add Junction")
menu.addSeparator() menu.addSeparator()
options_action = menu.addAction("Connection Options…") options_action = menu.addAction("Connection Options…")
selected = menu.exec(event.screenPos()) selected = menu.exec(event.screenPos())
@@ -286,6 +420,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
scene = self.scene() scene = self.scene()
if isinstance(scene, GraphScene): if isinstance(scene, GraphScene):
scene.add_route_node("connection", self.connection_id, event.scenePos()) scene.add_route_node("connection", self.connection_id, event.scenePos())
elif selected is add_junction_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.add_connection_junction(self.connection_id, event.scenePos())
elif selected is options_action: elif selected is options_action:
scene = self.scene() scene = self.scene()
if isinstance(scene, GraphScene): if isinstance(scene, GraphScene):
@@ -318,6 +456,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
if len(points) > 1: if len(points) > 1:
self.start_direction = points[1] - points[0] self.start_direction = points[1] - points[0]
self.end_direction = points[-1] - points[-2] self.end_direction = points[-1] - points[-2]
if self.name_label is not None:
connection = self.controller.active_graph.connections.get(self.connection_id)
if connection is not None:
self.sync_name_label(connection)
def set_waypoints(self, points: list[QPointF]) -> None: def set_waypoints(self, points: list[QPointF]) -> None:
for handle in self.handles: for handle in self.handles:
@@ -349,9 +491,9 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
super().paint(painter, option, widget) super().paint(painter, option, widget)
painter.setPen(Qt.PenStyle.NoPen) painter.setPen(Qt.PenStyle.NoPen)
painter.setBrush(self.pen().color()) painter.setBrush(self.pen().color())
if self.style.arrow_at_target: if self.style.arrow_at_target and not self.target_is_junction:
painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size)) painter.drawPolygon(self._arrow(self.end, self.end_direction, self.style.arrow_size))
if self.style.arrow_at_source: if self.style.arrow_at_source and not self.source_is_junction:
painter.drawPolygon( painter.drawPolygon(
self._arrow(self.start, -self.start_direction, self.style.arrow_size) self._arrow(self.start, -self.start_direction, self.style.arrow_size)
) )
@@ -700,8 +842,11 @@ class GraphScene(QGraphicsScene):
self.input_items: dict[str, InterfaceTerminalItem] = {} self.input_items: dict[str, InterfaceTerminalItem] = {}
self.output_items: dict[str, InterfaceTerminalItem] = {} self.output_items: dict[str, InterfaceTerminalItem] = {}
self.connection_items: dict[str, ConnectionGraphicsItem] = {} self.connection_items: dict[str, ConnectionGraphicsItem] = {}
self.junction_items: dict[str, JunctionGraphicsItem] = {}
self.annotation_items: dict[str, QGraphicsItem] = {} self.annotation_items: dict[str, QGraphicsItem] = {}
self.pending_connection_item: ComponentGraphicsItem | ConnectionPortItem | None = None self.pending_connection_item: (
ComponentGraphicsItem | ConnectionPortItem | JunctionGraphicsItem | None
) = None
self.pending_waypoints: list[QPointF] = [] self.pending_waypoints: list[QPointF] = []
self.pending_preview: QGraphicsPathItem | None = None self.pending_preview: QGraphicsPathItem | None = None
self.interaction_mode = "pointer" self.interaction_mode = "pointer"
@@ -713,6 +858,7 @@ class GraphScene(QGraphicsScene):
controller.activeGraphChanged.connect(self.rebuild) controller.activeGraphChanged.connect(self.rebuild)
controller.componentMoved.connect(self.set_component_position) controller.componentMoved.connect(self.set_component_position)
controller.componentRotated.connect(self.set_component_rotation) controller.componentRotated.connect(self.set_component_rotation)
controller.componentPropertiesChanged.connect(self.refresh_component_properties)
controller.graphItemChanged.connect(self.refresh_graph_item) controller.graphItemChanged.connect(self.refresh_graph_item)
self.rebuild() self.rebuild()
@@ -722,6 +868,7 @@ class GraphScene(QGraphicsScene):
self.input_items.clear() self.input_items.clear()
self.output_items.clear() self.output_items.clear()
self.connection_items.clear() self.connection_items.clear()
self.junction_items.clear()
self.annotation_items.clear() self.annotation_items.clear()
self.pending_connection_item = None self.pending_connection_item = None
self.pending_waypoints.clear() self.pending_waypoints.clear()
@@ -744,10 +891,14 @@ class GraphScene(QGraphicsScene):
self.addItem(item) self.addItem(item)
item.setPos(component.x, component.y) item.setPos(component.x, component.y)
self.component_items[component.id] = item self.component_items[component.id] = item
for junction in owner.graph.junctions.values():
item = JunctionGraphicsItem(junction, self.controller)
self.addItem(item)
self.junction_items[junction.id] = item
for connection in owner.graph.connections.values(): for connection in owner.graph.connections.values():
item = ConnectionGraphicsItem( item = ConnectionGraphicsItem(
connection.id, connection,
connection.name, self.controller,
connection_style(self.controller.connection_port_type(connection)), connection_style(self.controller.connection_port_type(connection)),
) )
self.addItem(item) self.addItem(item)
@@ -774,12 +925,33 @@ class GraphScene(QGraphicsScene):
item = self.component_items.get(component_id) item = self.component_items.get(component_id)
if item is not None: if item is not None:
item.setRotation(rotation) item.setRotation(rotation)
if item.name_label is not None:
item.name_label.setRotation(-rotation)
self.update_connections_for_block(component_id) self.update_connections_for_block(component_id)
def refresh_component_properties(self, component_id: str) -> None:
component = (
self.controller.document.find_component(component_id)
if self.controller.document
else None
)
item = self.component_items.get(component_id)
if component is not None and item is not None:
item.sync_name_label(component)
def refresh_graph_item(self, item_kind: str, item_id: str) -> None: def refresh_graph_item(self, item_kind: str, item_id: str) -> None:
if item_kind == "connection": if item_kind in {"connection", "connection_data"}:
self.update_connection(item_id) self.update_connection(item_id)
return return
if item_kind == "junction_geometry":
junction = self.controller.active_graph.junctions.get(item_id)
graphics = self.junction_items.get(item_id)
if junction is not None and graphics is not None:
position = QPointF(junction.x, junction.y)
if graphics.pos() != position:
graphics.setPos(position)
self.update_connections_for_junction(item_id)
return
annotation = self.controller.active_graph.annotations.get(item_id) annotation = self.controller.active_graph.annotations.get(item_id)
graphics = self.annotation_items.get(item_id) graphics = self.annotation_items.get(item_id)
if annotation is None or graphics is None: if annotation is None or graphics is None:
@@ -833,11 +1005,20 @@ class GraphScene(QGraphicsScene):
if port_id in (connection.source.interface, connection.target.interface): if port_id in (connection.source.interface, connection.target.interface):
self.update_connection(connection.id) self.update_connection(connection.id)
def update_connections_for_junction(self, junction_id: str) -> None:
for connection in self.controller.active_graph.connections.values():
if junction_id in (connection.source.junction, connection.target.junction):
self.update_connection(connection.id)
def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802 def drawBackground(self, painter: QPainter, rect: QRectF) -> None: # noqa: N802
# The view paints the grid so it always covers the complete viewport. # The view paints the grid so it always covers the complete viewport.
del painter, rect del painter, rect
def _endpoint_item(self, endpoint: Endpoint, role: str) -> ConnectionPortItem | None: def _endpoint_item(
self, endpoint: Endpoint, role: str
) -> ConnectionPortItem | JunctionGraphicsItem | None:
if endpoint.junction is not None:
return self.junction_items.get(endpoint.junction)
if endpoint.interface is not None: if endpoint.interface is not None:
terminals = self.input_items if role == "source" else self.output_items terminals = self.input_items if role == "source" else self.output_items
terminal = terminals.get(endpoint.interface) terminal = terminals.get(endpoint.interface)
@@ -867,6 +1048,9 @@ class GraphScene(QGraphicsScene):
if endpoint.block is not None: if endpoint.block is not None:
component = self.component_items.get(endpoint.block) component = self.component_items.get(endpoint.block)
return component.mapToScene(component.hitbox.center()) if component else None return component.mapToScene(component.hitbox.center()) if component else None
if endpoint.junction is not None:
junction = self.junction_items.get(endpoint.junction)
return junction.scenePos() if junction else None
port = self._endpoint_item(endpoint, role) port = self._endpoint_item(endpoint, role)
return port.scenePos() if port else None return port.scenePos() if port else None
@@ -876,6 +1060,9 @@ class GraphScene(QGraphicsScene):
if endpoint.block is not None: if endpoint.block is not None:
component = self.component_items.get(endpoint.block) component = self.component_items.get(endpoint.block)
return self._hitbox_intersection(component, reference) if component else None return self._hitbox_intersection(component, reference) if component else None
if endpoint.junction is not None:
junction = self.junction_items.get(endpoint.junction)
return junction.scenePos() if junction else None
port = self._endpoint_item(endpoint, role) port = self._endpoint_item(endpoint, role)
return port.scenePos() if port else None return port.scenePos() if port else None
@@ -908,6 +1095,7 @@ class GraphScene(QGraphicsScene):
start, end = geometry start, end = geometry
path, direction_points = self._route_path(start, end, waypoints) path, direction_points = self._route_path(start, end, waypoints)
graphics.set_connection_path(path, direction_points) graphics.set_connection_path(path, direction_points)
graphics.sync_name_label(connection)
graphics.set_waypoints(waypoints) graphics.set_waypoints(waypoints)
def update_annotation(self, annotation_id: str) -> None: def update_annotation(self, annotation_id: str) -> None:
@@ -963,17 +1151,43 @@ class GraphScene(QGraphicsScene):
return item.parentItem() return item.parentItem()
return None return None
@staticmethod
def _clicked_terminal(item: QGraphicsItem | None) -> InterfaceTerminalItem | None:
if isinstance(item, InterfaceTerminalItem):
return item
if isinstance(item, ConnectionPortItem) and isinstance(
item.parentItem(), InterfaceTerminalItem
):
return item.parentItem()
return None
@classmethod
def _connection_click_item(
cls, item: QGraphicsItem | None
) -> ComponentGraphicsItem | ConnectionPortItem | JunctionGraphicsItem | None:
if isinstance(item, JunctionGraphicsItem):
return item
component = cls._clicked_component(item)
if component is not None:
return item if isinstance(item, ConnectionPortItem) else component
terminal = cls._clicked_terminal(item)
return terminal.connection_port if terminal is not None else None
@staticmethod @staticmethod
def _click_endpoint(item: QGraphicsItem | None) -> Endpoint | None: def _click_endpoint(item: QGraphicsItem | None) -> Endpoint | None:
return item.endpoint if isinstance(item, ConnectionPortItem) else None return (
item.endpoint
if isinstance(item, (ConnectionPortItem, JunctionGraphicsItem))
else None
)
@staticmethod @staticmethod
def _connection_anchor( def _connection_anchor(
item: ComponentGraphicsItem | ConnectionPortItem, item: ComponentGraphicsItem | ConnectionPortItem | JunctionGraphicsItem,
) -> QPointF: ) -> QPointF:
return ( return (
item.scenePos() item.scenePos()
if isinstance(item, ConnectionPortItem) if isinstance(item, (ConnectionPortItem, JunctionGraphicsItem))
else item.sceneBoundingRect().center() else item.sceneBoundingRect().center()
) )
@@ -991,10 +1205,16 @@ class GraphScene(QGraphicsScene):
for input_port in target_item.component.inputs: for input_port in target_item.component.inputs:
if not PortTypeRegistry.compatible(output.type, input_port.type): if not PortTypeRegistry.compatible(output.type, input_port.type):
continue continue
source = Endpoint(block=source_item.component_id, port=output.id)
target = Endpoint(block=target_item.component_id, port=input_port.id)
if not self.controller.endpoint_accepts_connection(source, "source"):
continue
if not self.controller.endpoint_accepts_connection(target, "target"):
continue
choices.append( choices.append(
ConnectionChoice( ConnectionChoice(
Endpoint(block=source_item.component_id, port=output.id), source,
Endpoint(block=target_item.component_id, port=input_port.id), target,
f"{source_item.component.name}.{output.name}" f"{source_item.component.name}.{output.name}"
f"{target_item.component.name}.{input_port.name}", f"{target_item.component.name}.{input_port.name}",
) )
@@ -1004,11 +1224,92 @@ class GraphScene(QGraphicsScene):
add_pairs(second, first) add_pairs(second, first)
return choices return choices
def _interface_connection_choices(
self,
terminal: InterfaceTerminalItem,
component: ComponentGraphicsItem,
) -> list[ConnectionChoice]:
choices: list[ConnectionChoice] = []
interface = Endpoint(interface=terminal.port.id)
if terminal.direction == "input":
for port in component.component.inputs:
target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(terminal.port.type, port.type):
continue
if not self.controller.endpoint_accepts_connection(interface, "source"):
continue
if not self.controller.endpoint_accepts_connection(target, "target"):
continue
choices.append(
ConnectionChoice(
interface,
target,
f"IN.{terminal.port.name}{component.component.name}.{port.name}",
)
)
else:
for port in component.component.outputs:
source = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(port.type, terminal.port.type):
continue
if not self.controller.endpoint_accepts_connection(source, "source"):
continue
if not self.controller.endpoint_accepts_connection(interface, "target"):
continue
choices.append(
ConnectionChoice(
source,
interface,
f"{component.component.name}.{port.name} → OUT.{terminal.port.name}",
)
)
return choices
def _junction_connection_choices(
self,
junction: JunctionGraphicsItem,
component: ComponentGraphicsItem,
) -> list[ConnectionChoice]:
choices: list[ConnectionChoice] = []
source = junction.endpoint
for port in component.component.inputs:
target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
continue
if not self.controller.endpoint_accepts_connection(target, "target"):
continue
choices.append(
ConnectionChoice(
source,
target,
f"Junction → {component.component.name}.{port.name}",
)
)
return choices
def _junction_interface_choices(
self,
junction: JunctionGraphicsItem,
terminal: InterfaceTerminalItem,
) -> list[ConnectionChoice]:
if terminal.direction != "output":
return []
target = Endpoint(interface=terminal.port.id)
if not PortTypeRegistry.compatible(junction.junction.type, terminal.port.type):
return []
if not self.controller.endpoint_accepts_connection(target, "target"):
return []
return [
ConnectionChoice(
junction.endpoint,
target,
f"Junction → OUT.{terminal.port.name}",
)
]
@staticmethod @staticmethod
def _default_choice_index( def _default_choice_index(
choices: list[ConnectionChoice], choices: list[ConnectionChoice],
first_component: ComponentGraphicsItem,
second_component: ComponentGraphicsItem,
first_endpoint: Endpoint | None, first_endpoint: Endpoint | None,
second_endpoint: Endpoint | None, second_endpoint: Endpoint | None,
) -> int: ) -> int:
@@ -1018,10 +1319,6 @@ class GraphScene(QGraphicsScene):
value += 8 value += 8
if second_endpoint is not None and second_endpoint in (choice.source, choice.target): if second_endpoint is not None and second_endpoint in (choice.source, choice.target):
value += 8 value += 8
if choice.source.block == first_component.component_id:
value += 2
if choice.target.block == second_component.component_id:
value += 1
return value return value
return max(range(len(choices)), key=lambda index: score(choices[index])) return max(range(len(choices)), key=lambda index: score(choices[index]))
@@ -1035,27 +1332,41 @@ class GraphScene(QGraphicsScene):
return return
first_component = self._clicked_component(first_item) first_component = self._clicked_component(first_item)
second_component = self._clicked_component(second_item) second_component = self._clicked_component(second_item)
if ( first_terminal = self._clicked_terminal(first_item)
first_component is None second_terminal = self._clicked_terminal(second_item)
or second_component is None first_junction = first_item if isinstance(first_item, JunctionGraphicsItem) else None
or first_component is second_component second_junction = second_item if isinstance(second_item, JunctionGraphicsItem) else None
): if first_component is not None and second_component is not None:
if first_component is second_component:
self._clear_pending_connection()
return
choices = self._connection_choices(first_component, second_component)
elif first_terminal is not None and second_component is not None:
choices = self._interface_connection_choices(first_terminal, second_component)
elif first_component is not None and second_terminal is not None:
choices = self._interface_connection_choices(second_terminal, first_component)
elif first_junction is not None and second_component is not None:
choices = self._junction_connection_choices(first_junction, second_component)
elif first_component is not None and second_junction is not None:
choices = self._junction_connection_choices(second_junction, first_component)
elif first_junction is not None and second_terminal is not None:
choices = self._junction_interface_choices(first_junction, second_terminal)
elif first_terminal is not None and second_junction is not None:
choices = self._junction_interface_choices(second_junction, first_terminal)
else:
self._clear_pending_connection() self._clear_pending_connection()
return return
choices = self._connection_choices(first_component, second_component)
if not choices: if not choices:
QToolTip.showText( QToolTip.showText(
self.views()[0].mapToGlobal(self.views()[0].viewport().rect().center()) self.views()[0].mapToGlobal(self.views()[0].viewport().rect().center())
if self.views() if self.views()
else QPointF().toPoint(), else QPointF().toPoint(),
"These blocks have no compatible input/output pairs", "These items have no available compatible input/output pairs",
) )
self._clear_pending_connection() self._clear_pending_connection()
return return
default = self._default_choice_index( default = self._default_choice_index(
choices, choices,
first_component,
second_component,
self._click_endpoint(first_item), self._click_endpoint(first_item),
self._click_endpoint(second_item), self._click_endpoint(second_item),
) )
@@ -1087,9 +1398,8 @@ class GraphScene(QGraphicsScene):
event.accept() event.accept()
return return
item = self.itemAt(event.scenePos(), QTransform()) item = self.itemAt(event.scenePos(), QTransform())
component = self._clicked_component(item) clicked = self._connection_click_item(item)
if component is not None: if clicked is not None:
clicked = item if isinstance(item, ConnectionPortItem) else component
if self.pending_connection_item is None: if self.pending_connection_item is None:
self.pending_connection_item = clicked self.pending_connection_item = clicked
self.pending_waypoints = [] self.pending_waypoints = []
@@ -1238,6 +1548,44 @@ class GraphScene(QGraphicsScene):
points.insert(insertion, snapped) points.insert(insertion, snapped)
self.controller.set_route_waypoints(item_kind, item_id, points) self.controller.set_route_waypoints(item_kind, item_id, points)
def add_connection_junction(self, connection_id: str, position: QPointF) -> None:
connection, points = self._route_values("connection", connection_id)
graphics = self.connection_items.get(connection_id)
if connection is None or graphics is None:
return
snapped = _snapped(position)
anchors = [graphics.start, *points, graphics.end]
def segment_distance(point: QPointF, first: QPointF, second: QPointF) -> float:
delta = second - first
length_squared = delta.x() ** 2 + delta.y() ** 2
if length_squared == 0:
return (point - first).manhattanLength()
ratio = max(
0.0,
min(
1.0,
((point.x() - first.x()) * delta.x()
+ (point.y() - first.y()) * delta.y())
/ length_squared,
),
)
nearest = first + delta * ratio
return (point.x() - nearest.x()) ** 2 + (point.y() - nearest.y()) ** 2
insertion = min(
range(len(anchors) - 1),
key=lambda index: segment_distance(
snapped, anchors[index], anchors[index + 1]
),
)
self.controller.split_connection(
connection_id,
snapped,
points[:insertion],
points[insertion:],
)
def move_route_node(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None: def move_route_node(self, item_kind: str, item_id: str, index: int, position: QPointF) -> None:
item, points = self._route_values(item_kind, item_id) item, points = self._route_values(item_kind, item_id)
if item is None: if item is None:

View File

@@ -1,4 +1,3 @@
import json
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
@@ -13,7 +12,7 @@ from PySide6.QtWidgets import (
QTabWidget, QTabWidget,
) )
from bedit.core.model import Component, Port from bedit.core.model import Component, Parameter
from bedit.core.serializer import JsonDocumentSerializer from bedit.core.serializer import JsonDocumentSerializer
from bedit.gui.controllers.document import DocumentController from bedit.gui.controllers.document import DocumentController
from bedit.gui.dialogs.component_options import ComponentOptionsDialog from bedit.gui.dialogs.component_options import ComponentOptionsDialog
@@ -40,6 +39,7 @@ class MainWindow(QMainWindow):
self.ui = Ui_MainWindow() self.ui = Ui_MainWindow()
self.ui.setupUi(self) self.ui.setupUi(self)
self.settings = application_settings() self.settings = application_settings()
self._applying_text_definition = False
self.libraries = LibraryRepository(self) self.libraries = LibraryRepository(self)
self.document_controller = DocumentController(self) self.document_controller = DocumentController(self)
@@ -108,12 +108,18 @@ class MainWindow(QMainWindow):
self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line")) self.ui.lineToolButton.clicked.connect(lambda: self.set_graph_tool("line"))
self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text")) self.ui.textToolButton.clicked.connect(lambda: self.set_graph_tool("text"))
self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected) self.ui.rotateToolButton.clicked.connect(self.ui.graphView.rotate_selected)
self.ui.applyJsonButton.clicked.connect(self.apply_json) self.ui.textDefinitionEditor.definitionEdited.connect(
self.apply_text_definition
)
self.document_controller.activeGraphChanged.connect(self._active_graph_changed) self.document_controller.activeGraphChanged.connect(self._active_graph_changed)
self.document_controller.textDefinitionChanged.connect(
self._text_definition_changed
)
def _connect_actions(self) -> None: def _connect_actions(self) -> None:
self.ui.actionNew.triggered.connect(self.new_document) self.ui.actionNew.triggered.connect(self.new_document)
self.ui.actionOpen.triggered.connect(self.open_document) self.ui.actionOpen.triggered.connect(self.open_document)
self.ui.actionReloadLibraries.triggered.connect(self.reload_libraries)
self.ui.actionSave.triggered.connect(self.save_document) self.ui.actionSave.triggered.connect(self.save_document)
self.ui.actionSaveAs.triggered.connect(self.save_document_as) self.ui.actionSaveAs.triggered.connect(self.save_document_as)
self.ui.actionClose.triggered.connect(self.close_document) self.ui.actionClose.triggered.connect(self.close_document)
@@ -131,6 +137,7 @@ class MainWindow(QMainWindow):
self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection) self.ui.actionPaste.triggered.connect(self.ui.graphView.paste_selection)
self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all) self.ui.actionSelectAll.triggered.connect(self.ui.graphView.select_all)
self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected) self.ui.actionRotateClockwise.triggered.connect(self.ui.graphView.rotate_selected)
self.addAction(self.ui.actionRotateClockwise)
self.ui.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in) self.ui.actionZoomIn.triggered.connect(self.ui.graphView.zoom_in)
self.ui.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out) self.ui.actionZoomOut.triggered.connect(self.ui.graphView.zoom_out)
self.ui.actionCenterView.triggered.connect(self.ui.graphView.center_workspace) self.ui.actionCenterView.triggered.connect(self.ui.graphView.center_workspace)
@@ -188,7 +195,6 @@ class MainWindow(QMainWindow):
self.ui.navigateUpButton.setEnabled(False) self.ui.navigateUpButton.setEnabled(False)
self.ui.workspaceModeLabel.setText("") self.ui.workspaceModeLabel.setText("")
self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage) self.ui.workspaceStack.setCurrentWidget(self.ui.emptyPage)
self.ui.applyJsonButton.setVisible(False)
self._set_graph_controls_visible(False) self._set_graph_controls_visible(False)
self._update_edit_actions() self._update_edit_actions()
return return
@@ -198,13 +204,14 @@ class MainWindow(QMainWindow):
) )
is_graph = component.implementation_kind == "graph" is_graph = component.implementation_kind == "graph"
self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text") self.ui.workspaceModeLabel.setText("Graph" if is_graph else "Text")
self.ui.workspaceStack.setCurrentWidget(self.ui.graphPage if is_graph else self.ui.jsonPage) self.ui.workspaceStack.setCurrentWidget(
self.ui.applyJsonButton.setVisible(not is_graph) self.ui.graphPage if is_graph else self.ui.textPage
)
self._set_graph_controls_visible(is_graph) self._set_graph_controls_visible(is_graph)
if is_graph: if is_graph:
self.set_graph_tool("pointer") self.set_graph_tool("pointer")
else: else:
self._load_source_json() self._load_text_definition()
self._update_edit_actions() self._update_edit_actions()
def _update_edit_actions(self) -> None: def _update_edit_actions(self) -> None:
@@ -258,62 +265,71 @@ class MainWindow(QMainWindow):
"text": self.ui.textToolButton, "text": self.ui.textToolButton,
}[mode].setChecked(True) }[mode].setChecked(True)
def _load_source_json(self) -> None: def _load_text_definition(self) -> None:
component = self.document_controller.active_component component = self.document_controller.active_component
if component is None: if component is None:
return return
text = json.dumps( self.ui.textDefinitionEditor.set_definition(
{ component.source.get("equations", ""),
"inputs": [port.to_dict() for port in component.inputs], component.inputs,
"outputs": [port.to_dict() for port in component.outputs], component.outputs,
"source": component.source, [
}, Parameter.from_dict(parameter)
indent=2, for parameter in component.source.get("parameters", [])
],
) )
self.ui.jsonEditor.setPlainText(text)
self.ui.jsonEditor.document().setModified(False)
def _resolve_source_edits(self) -> bool: def _resolve_source_edits(self) -> bool:
component = self.document_controller.active_component component = self.document_controller.active_component
if ( if (
component is None component is None
or component.implementation_kind != "text" or component.implementation_kind != "text"
or not self.ui.jsonEditor.document().isModified() or not self.ui.textDefinitionEditor.is_modified
): ):
return True return True
answer = QMessageBox.question( answer = QMessageBox.question(
self, self,
"Apply text component changes?", "Apply text component changes?",
"The text component has unapplied input, output, or source changes.", "The text component has unapplied equation, port, or parameter changes.",
QMessageBox.StandardButton.Apply QMessageBox.StandardButton.Apply
| QMessageBox.StandardButton.Discard | QMessageBox.StandardButton.Discard
| QMessageBox.StandardButton.Cancel, | QMessageBox.StandardButton.Cancel,
) )
if answer == QMessageBox.StandardButton.Apply: if answer == QMessageBox.StandardButton.Apply:
return self.apply_json() return self.apply_text_definition()
return answer == QMessageBox.StandardButton.Discard return answer == QMessageBox.StandardButton.Discard
@Slot() @Slot()
def apply_json(self) -> bool: def apply_text_definition(self) -> bool:
try: try:
data = json.loads(self.ui.jsonEditor.toPlainText()) inputs, outputs = self.ui.textDefinitionEditor.ports
if not isinstance(data, dict): self._applying_text_definition = True
raise ValueError("The text component JSON must be an object") try:
if not isinstance(data.get("inputs"), list): self.document_controller.replace_active_text_definition(
raise ValueError("'inputs' must be a list") inputs,
if not isinstance(data.get("outputs"), list): outputs,
raise ValueError("'outputs' must be a list") self.ui.textDefinitionEditor.equations,
if not isinstance(data.get("source"), dict): self.ui.textDefinitionEditor.parameters,
raise ValueError("'source' must be an object") )
inputs = [Port.from_dict(item) for item in data["inputs"]] finally:
outputs = [Port.from_dict(item) for item in data["outputs"]] self._applying_text_definition = False
self.document_controller.replace_active_text_definition(inputs, outputs, data["source"]) except (TypeError, ValueError) as error:
except (TypeError, ValueError, json.JSONDecodeError) as error: QMessageBox.critical(self, "Invalid text component", str(error))
QMessageBox.critical(self, "Invalid text component JSON", str(error)) self._load_text_definition()
return False return False
self._load_source_json() self.ui.textDefinitionEditor.set_modified(False)
return True return True
@Slot(str)
def _text_definition_changed(self, component_id: str) -> None:
component = self.document_controller.active_component
if (
component is not None
and component.id == component_id
and not self._applying_text_definition
):
self._load_text_definition()
def _maybe_save(self) -> bool: def _maybe_save(self) -> bool:
if self.document_controller.document is None: if self.document_controller.document is None:
return True return True
@@ -435,9 +451,9 @@ class MainWindow(QMainWindow):
ports_action = menu.addAction("Port Options…") ports_action = menu.addAction("Port Options…")
delete_action = menu.addAction("Delete") delete_action = menu.addAction("Delete")
selected = menu.exec(tree_view.viewport().mapToGlobal(position)) selected = menu.exec(tree_view.viewport().mapToGlobal(position))
if selected is graph_action: if graph_action is not None and selected is graph_action:
self.document_controller.add_child(component_id, "graph") self.document_controller.add_child(component_id, "graph")
elif selected is text_action: elif text_action is not None and selected is text_action:
self.document_controller.add_child(component_id, "text") self.document_controller.add_child(component_id, "text")
elif selected is options_action: elif selected is options_action:
self.show_component_options(component_id) self.show_component_options(component_id)
@@ -520,14 +536,18 @@ class MainWindow(QMainWindow):
return return
dialog = ComponentOptionsDialog(component, self) dialog = ComponentOptionsDialog(component, self)
if dialog.exec() == dialog.DialogCode.Accepted: if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.edit_component_appearance( try:
component_id, self.document_controller.edit_component_appearance(
dialog.ui.nameEdit.text().strip(), component_id,
dialog.edited_icon, dialog.ui.nameEdit.text().strip(),
dialog.edited_inputs, dialog.edited_icon,
dialog.edited_outputs, dialog.edited_inputs,
dialog.ui.showSubtreeCheckBox.isChecked(), dialog.edited_outputs,
) dialog.ui.showSubtreeCheckBox.isChecked(),
dialog.ui.showNameCheckBox.isChecked(),
)
except ValueError as error:
QMessageBox.warning(self, "Cannot rename component", str(error))
@Slot(str, str) @Slot(str, str)
def show_port_options(self, port_id: str, direction: str) -> None: def show_port_options(self, port_id: str, direction: str) -> None:
@@ -550,9 +570,17 @@ class MainWindow(QMainWindow):
connection = owner.graph.connections.get(connection_id) connection = owner.graph.connections.get(connection_id)
if connection is None: if connection is None:
return return
dialog = ItemOptionsDialog("Connection Options", connection.name, self, name_required=False) dialog = ItemOptionsDialog(
"Connection Options",
connection.name,
self,
name_required=False,
show_name=bool(connection.properties.get("showName", False)),
)
if dialog.exec() == dialog.DialogCode.Accepted: if dialog.exec() == dialog.DialogCode.Accepted:
self.document_controller.rename_connection(connection_id, dialog.name) self.document_controller.edit_connection_options(
connection_id, dialog.name, dialog.show_name
)
@Slot() @Slot()
def show_about(self) -> None: def show_about(self) -> None:

View File

@@ -86,7 +86,9 @@ class DocumentTreeModel(LibraryTreeModel):
self.clear() self.clear()
self.setHorizontalHeaderLabels(["Document"]) self.setHorizontalHeaderLabels(["Document"])
if self.controller.document is not None: if self.controller.document is not None:
current_root = QStandardItem("Current Document") current_root = QStandardItem(
str(self.controller.document.metadata.get("name") or "Current Document")
)
current_root.setDragEnabled(False) current_root.setDragEnabled(False)
current_root.setData("current-document", ITEM_KIND_ROLE) current_root.setData("current-document", ITEM_KIND_ROLE)
for component in self.controller.document.roots.values(): for component in self.controller.document.roots.values():

View File

@@ -12,6 +12,7 @@
<item row="1" column="0"><widget class="QLabel" name="iconLabel"><property name="text"><string>Icon:</string></property></widget></item> <item row="1" column="0"><widget class="QLabel" name="iconLabel"><property name="text"><string>Icon:</string></property></widget></item>
<item row="1" column="1"><widget class="QPushButton" name="editIconButton"><property name="text"><string>Edit Icon…</string></property><property name="toolTip"><string>Open the vector icon and port-position editor</string></property></widget></item> <item row="1" column="1"><widget class="QPushButton" name="editIconButton"><property name="text"><string>Edit Icon…</string></property><property name="toolTip"><string>Open the vector icon and port-position editor</string></property></widget></item>
<item row="2" column="0" colspan="2"><widget class="QCheckBox" name="showSubtreeCheckBox"><property name="text"><string>Show contained components in the Libraries tree</string></property><property name="checked"><bool>true</bool></property></widget></item> <item row="2" column="0" colspan="2"><widget class="QCheckBox" name="showSubtreeCheckBox"><property name="text"><string>Show contained components in the Libraries tree</string></property><property name="checked"><bool>true</bool></property></widget></item>
<item row="3" column="0" colspan="2"><widget class="QCheckBox" name="showNameCheckBox"><property name="text"><string>Show name below component</string></property></widget></item>
</layout> </layout>
</item> </item>
<item><spacer name="optionsSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item> <item><spacer name="optionsSpacer"><property name="orientation"><enum>Qt::Orientation::Vertical</enum></property><property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property></spacer></item>

View File

@@ -264,16 +264,6 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QPushButton" name="applyJsonButton">
<property name="visible">
<bool>false</bool>
</property>
<property name="text">
<string>Apply JSON</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QToolButton" name="pointerToolButton"> <widget class="QToolButton" name="pointerToolButton">
<property name="text"> <property name="text">
@@ -397,8 +387,8 @@
</item> </item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="jsonPage"> <widget class="QWidget" name="textPage">
<layout class="QVBoxLayout" name="jsonPageLayout"> <layout class="QVBoxLayout" name="textPageLayout">
<property name="leftMargin"> <property name="leftMargin">
<number>0</number> <number>0</number>
</property> </property>
@@ -412,14 +402,7 @@
<number>0</number> <number>0</number>
</property> </property>
<item> <item>
<widget class="QPlainTextEdit" name="jsonEditor"> <widget class="TextDefinitionEditor" name="textDefinitionEditor" native="true"/>
<property name="lineWrapMode">
<enum>QPlainTextEdit::LineWrapMode::NoWrap</enum>
</property>
<property name="placeholderText">
<string>Component JSON</string>
</property>
</widget>
</item> </item>
</layout> </layout>
</widget> </widget>
@@ -466,6 +449,7 @@
</property> </property>
<addaction name="actionNew"/> <addaction name="actionNew"/>
<addaction name="actionOpen"/> <addaction name="actionOpen"/>
<addaction name="actionReloadLibraries"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSave"/> <addaction name="actionSave"/>
<addaction name="actionSaveAs"/> <addaction name="actionSaveAs"/>
@@ -667,6 +651,17 @@
<string>Ctrl+O</string> <string>Ctrl+O</string>
</property> </property>
</action> </action>
<action name="actionReloadLibraries">
<property name="text">
<string>Reload &amp;Libraries</string>
</property>
<property name="statusTip">
<string>Reload configured library files from disk</string>
</property>
<property name="shortcut">
<string>F5</string>
</property>
</action>
<action name="actionSave"> <action name="actionSave">
<property name="icon"> <property name="icon">
<iconset resource="../resources/resources.qrc"> <iconset resource="../resources/resources.qrc">
@@ -806,6 +801,12 @@
</action> </action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget>
<class>TextDefinitionEditor</class>
<extends>QWidget</extends>
<header>bedit.gui.editors.text_definition</header>
<container>1</container>
</customwidget>
<customwidget> <customwidget>
<class>GraphWorkspaceView</class> <class>GraphWorkspaceView</class>
<extends>QGraphicsView</extends> <extends>QGraphicsView</extends>

View File

@@ -27,7 +27,8 @@
<item row="1" column="1"><widget class="QComboBox" name="typeCombo"><item><property name="text"><string>Signal</string></property></item></widget></item> <item row="1" column="1"><widget class="QComboBox" name="typeCombo"><item><property name="text"><string>Signal</string></property></item></widget></item>
<item row="2" column="0"><widget class="QLabel" name="orientationLabel"><property name="text"><string>Orientation:</string></property></widget></item> <item row="2" column="0"><widget class="QLabel" name="orientationLabel"><property name="text"><string>Orientation:</string></property></widget></item>
<item row="2" column="1"><widget class="QComboBox" name="orientationCombo"><item><property name="text"><string>Input</string></property></item><item><property name="text"><string>Output</string></property></item></widget></item> <item row="2" column="1"><widget class="QComboBox" name="orientationCombo"><item><property name="text"><string>Input</string></property></item><item><property name="text"><string>Output</string></property></item></widget></item>
<item row="3" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item> <item row="3" column="0" colspan="2"><widget class="QCheckBox" name="multipleConnectionsCheckBox"><property name="text"><string>Allow multiple connections</string></property></widget></item>
<item row="4" column="0" colspan="2"><widget class="QLabel" name="positionHintLabel"><property name="text"><string>New ports start at (0, 0) in the icon editor.</string></property><property name="wordWrap"><bool>true</bool></property></widget></item>
</layout> </layout>
</widget> </widget>
</widget> </widget>

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TextDefinitionEditor</class>
<widget class="QWidget" name="TextDefinitionEditor">
<property name="geometry"><rect><x>0</x><y>0</y><width>900</width><height>600</height></rect></property>
<layout class="QHBoxLayout" name="editorLayout">
<property name="leftMargin"><number>6</number></property>
<property name="topMargin"><number>6</number></property>
<property name="rightMargin"><number>6</number></property>
<property name="bottomMargin"><number>6</number></property>
<item>
<widget class="QSplitter" name="columnSplitter">
<property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QGroupBox" name="equationsGroup">
<property name="title"><string>Equations</string></property>
<layout class="QVBoxLayout" name="equationsLayout">
<item><widget class="QPlainTextEdit" name="equationsEdit"><property name="lineWrapMode"><enum>QPlainTextEdit::LineWrapMode::NoWrap</enum></property><property name="placeholderText"><string>Enter equations here…</string></property></widget></item>
</layout>
</widget>
<widget class="QSplitter" name="definitionSplitter">
<property name="orientation"><enum>Qt::Orientation::Vertical</enum></property>
<property name="childrenCollapsible"><bool>false</bool></property>
<widget class="QGroupBox" name="portsGroup">
<property name="title"><string>Ports</string></property>
<layout class="QVBoxLayout" name="portsLayout">
<item><widget class="QTableWidget" name="portsTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>4</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Orientation</string></property></column><column><property name="text"><string>Multiple</string></property></column></widget></item>
<item><layout class="QHBoxLayout" name="portButtonsLayout"><item><widget class="QPushButton" name="addPortButton"><property name="text"><string>Add Port</string></property></widget></item><item><widget class="QPushButton" name="removePortButton"><property name="text"><string>Remove Port</string></property></widget></item><item><spacer name="portButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
</layout>
</widget>
<widget class="QGroupBox" name="parametersGroup">
<property name="title"><string>Parameters</string></property>
<layout class="QVBoxLayout" name="parametersLayout">
<item><widget class="QTableWidget" name="parametersTable"><property name="selectionBehavior"><enum>QAbstractItemView::SelectionBehavior::SelectRows</enum></property><property name="selectionMode"><enum>QAbstractItemView::SelectionMode::SingleSelection</enum></property><property name="columnCount"><number>3</number></property><column><property name="text"><string>Name</string></property></column><column><property name="text"><string>Type</string></property></column><column><property name="text"><string>Value</string></property></column></widget></item>
<item><layout class="QHBoxLayout" name="parameterButtonsLayout"><item><widget class="QPushButton" name="addParameterButton"><property name="text"><string>Add Parameter</string></property></widget></item><item><widget class="QPushButton" name="removeParameterButton"><property name="text"><string>Remove Parameter</string></property></widget></item><item><spacer name="parameterButtonSpacer"><property name="orientation"><enum>Qt::Orientation::Horizontal</enum></property><property name="sizeHint" stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -6,16 +6,50 @@
}, },
"roots": [ "roots": [
{ {
"id": "f4a9c769-d49a-46e1-90d8-a8175bf42e9c", "id": "57fe8127-ea9d-4a8b-8d09-da430ffbbd92",
"name": "New Graph Block 1", "name": "PID",
"position": { "position": {
"x": 0.0, "x": 0.0,
"y": 0.0 "y": 0.0
}, },
"rotation": 0.0, "rotation": 0.0,
"interface": { "interface": {
"inputs": [], "inputs": [
"outputs": [] {
"id": "port-1342716d",
"name": "in",
"position": {
"x": -384.0,
"y": -112.0
},
"properties": {
"iconPosition": {
"x": 0.0,
"y": 0.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-c75d0f16",
"name": "out",
"position": {
"x": 320.0,
"y": -112.0
},
"properties": {
"iconPosition": {
"x": 0.0,
"y": 0.0
}
},
"type": "signal",
"multipleConnections": false
}
]
}, },
"icon": { "icon": {
"shape": "rectangle", "shape": "rectangle",
@@ -31,7 +65,7 @@
"type": "rectangle", "type": "rectangle",
"x": 32.0, "x": 32.0,
"y": 32.0, "y": 32.0,
"width": 88.0, "width": 64.0,
"height": 64.0, "height": 64.0,
"fill": "#f4f4f4", "fill": "#f4f4f4",
"stroke": "#303030", "stroke": "#303030",
@@ -43,63 +77,67 @@
"type": "text", "type": "text",
"x": 40.0, "x": 40.0,
"y": 40.0, "y": 40.0,
"width": 72.0, "width": 48.0,
"height": 40.0, "height": 48.0,
"text": "Graph", "text": "Graph",
"color": "#202020", "color": "#202020",
"fontSize": 12.0 "fontSize": 12.0
} }
] ]
}, },
"properties": {}, "properties": {
"showName": false
},
"library": { "library": {
"showSubtree": true "showSubtree": false
}, },
"implementation": { "implementation": {
"kind": "graph", "kind": "graph",
"graph": { "graph": {
"blocks": [ "blocks": [
{ {
"id": "af73487a-4a2d-4908-b919-d0a2eaa75ce2", "id": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
"name": "A", "name": "g_D",
"position": { "position": {
"x": -224.0, "x": -128.0,
"y": -168.0 "y": -160.0
}, },
"rotation": 0.0, "rotation": 0.0,
"interface": { "interface": {
"inputs": [ "inputs": [
{ {
"id": "port-5c5d4695", "id": "port-df34ce84",
"name": "Port 1", "name": "in",
"position": { "position": {
"x": 0.0, "x": 0.0,
"y": 0.0 "y": 0.0
}, },
"properties": { "properties": {
"iconPosition": { "iconPosition": {
"x": 32.0, "x": 64.0,
"y": 64.0 "y": 64.0
} }
}, },
"type": "signal" "type": "signal",
"multipleConnections": false
} }
], ],
"outputs": [ "outputs": [
{ {
"id": "port-de109124", "id": "port-fe9e6486",
"name": "Port 2", "name": "out",
"position": { "position": {
"x": 0.0, "x": 0.0,
"y": 0.0 "y": 0.0
}, },
"properties": { "properties": {
"iconPosition": { "iconPosition": {
"x": 96.0, "x": 88.0,
"y": 64.0 "y": 40.0
} }
}, },
"type": "signal" "type": "signal",
"multipleConnections": false
} }
] ]
}, },
@@ -126,14 +164,14 @@
"y": 32.0 "y": 32.0
}, },
{ {
"color": "#303030", "color": "#00007f",
"fill": "#ffffff", "fill": "#ffffff",
"fontSize": 12.0, "fontSize": 24.0,
"height": 48.0, "height": 48.0,
"lineStyle": "solid", "lineStyle": "solid",
"lineWidth": 1.5, "lineWidth": 1.5,
"stroke": "#303030", "stroke": "#00007f",
"text": "A", "text": "K",
"type": "text", "type": "text",
"width": 48.0, "width": 48.0,
"x": 40.0, "x": 40.0,
@@ -141,74 +179,70 @@
} }
] ]
}, },
"properties": {}, "properties": {
"showName": true
},
"library": { "library": {
"showSubtree": true "showSubtree": true
}, },
"implementation": { "implementation": {
"kind": "text", "kind": "text",
"source": { "source": {
"equations": [], "equations": "out = k*in;",
"parameters": {} "parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "1"
}
]
} }
} }
}, },
{ {
"id": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2", "id": "478e7ff8-baa1-467f-8707-e375b99395f6",
"name": "B", "name": "differentiate0",
"position": { "position": {
"x": 96.0, "x": 0.0,
"y": -288.0 "y": -160.0
}, },
"rotation": 0.0, "rotation": 0.0,
"interface": { "interface": {
"inputs": [ "inputs": [
{ {
"id": "port-4f732b3e", "id": "port-1cbabc8f",
"name": "Port 1", "name": "in",
"position": { "position": {
"x": -176.0, "x": 0.0,
"y": -144.0 "y": 0.0
}, },
"properties": { "properties": {
"iconPosition": { "iconPosition": {
"x": 32.0, "x": 64.0,
"y": 48.0 "y": 64.0
} }
}, },
"type": "signal" "type": "signal",
}, "multipleConnections": false
{
"id": "port-ef7c4218",
"name": "Port 2",
"position": {
"x": -176.0,
"y": -16.0
},
"properties": {
"iconPosition": {
"x": 32.0,
"y": 80.0
}
},
"type": "signal"
} }
], ],
"outputs": [ "outputs": [
{ {
"id": "port-b68679b2", "id": "port-4eeea4e7",
"name": "Port 3", "name": "out",
"position": { "position": {
"x": 128.0, "x": 0.0,
"y": -80.0 "y": 0.0
}, },
"properties": { "properties": {
"iconPosition": { "iconPosition": {
"x": 96.0, "x": 88.0,
"y": 64.0 "y": 40.0
} }
}, },
"type": "signal" "type": "signal",
"multipleConnections": false
} }
] ]
}, },
@@ -216,7 +250,7 @@
"shape": "rectangle", "shape": "rectangle",
"fill": "#f4f4f4", "fill": "#f4f4f4",
"border": "#303030", "border": "#303030",
"text": "Graph", "text": "Text",
"size": { "size": {
"width": 128.0, "width": 128.0,
"height": 128.0 "height": 128.0
@@ -235,14 +269,14 @@
"y": 32.0 "y": 32.0
}, },
{ {
"color": "#303030", "color": "#00007f",
"fill": "#ffffff", "fill": "#ffffff",
"fontSize": 12.0, "fontSize": 18.0,
"height": 48.0, "height": 48.0,
"lineStyle": "solid", "lineStyle": "solid",
"lineWidth": 1.5, "lineWidth": 1.5,
"stroke": "#303030", "stroke": "#00007f",
"text": "B", "text": "d/dt",
"type": "text", "type": "text",
"width": 48.0, "width": 48.0,
"x": 40.0, "x": 40.0,
@@ -250,51 +284,631 @@
} }
] ]
}, },
"properties": {}, "properties": {
"showName": false
},
"library": { "library": {
"showSubtree": true "showSubtree": true
}, },
"implementation": { "implementation": {
"kind": "graph", "kind": "text",
"graph": { "source": {
"blocks": [], "equations": "initial out = initial;\nout = der(in);",
"connections": [], "parameters": [
"annotations": [] {
"id": "parameter-331e38be",
"name": "initial",
"type": "real",
"value": "0"
}
]
}
}
},
{
"id": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
"name": "g_P",
"position": {
"x": -64.0,
"y": -288.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-df34ce84",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-fe9e6486",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#00007f",
"fill": "#ffffff",
"fontSize": 24.0,
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "K",
"type": "text",
"width": 48.0,
"x": 40.0,
"y": 40.0
}
]
},
"properties": {
"showName": true
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "out = k*in;",
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "1"
}
]
}
}
},
{
"id": "1995074c-4580-4ca3-8691-6923aedc5833",
"name": "g_I",
"position": {
"x": -128.0,
"y": -32.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-df34ce84",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-fe9e6486",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#00007f",
"fill": "#ffffff",
"fontSize": 24.0,
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "K",
"type": "text",
"width": 48.0,
"x": 40.0,
"y": 40.0
}
]
},
"properties": {
"showName": true
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "out = k*in;",
"parameters": [
{
"id": "parameter-61a43a86",
"name": "k",
"type": "real",
"value": "1"
}
]
}
}
},
{
"id": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"name": "add0",
"position": {
"x": 128.0,
"y": -160.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-c995845c",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": true
}
],
"outputs": [
{
"id": "port-a2f0dea6",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 80.0,
"y": 48.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"fill": "#f4f4f4",
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "circle",
"width": 48.0,
"x": 40.0,
"y": 40.0
},
{
"color": "#00007f",
"fill": "none",
"fontSize": 18.0,
"height": 32.0,
"lineStyle": "none",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "+",
"type": "text",
"width": 32.0,
"x": 48.0,
"y": 48.0
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "out = sum(in[i] for i in 1:in.N);",
"parameters": []
}
}
},
{
"id": "48af4110-839a-45f7-a105-7e7dbc51676b",
"name": "integrate0",
"position": {
"x": 0.0,
"y": -32.0
},
"rotation": 0.0,
"interface": {
"inputs": [
{
"id": "port-12903207",
"name": "in",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 64.0,
"y": 64.0
}
},
"type": "signal",
"multipleConnections": false
}
],
"outputs": [
{
"id": "port-b3370b1a",
"name": "out",
"position": {
"x": 0.0,
"y": 0.0
},
"properties": {
"iconPosition": {
"x": 88.0,
"y": 40.0
}
},
"type": "signal",
"multipleConnections": false
}
]
},
"icon": {
"shape": "rectangle",
"fill": "#f4f4f4",
"border": "#303030",
"text": "Text",
"size": {
"width": 128.0,
"height": 128.0
},
"elements": [
{
"cornerRadius": 5.0,
"fill": "#f4f4f4",
"height": 64.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#303030",
"type": "rectangle",
"width": 64.0,
"x": 32.0,
"y": 32.0
},
{
"color": "#00007f",
"fill": "#ffffff",
"fontSize": 18.0,
"height": 48.0,
"lineStyle": "solid",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "dt",
"type": "text",
"width": 28.0,
"x": 64.0,
"y": 40.0
},
{
"color": "#00007f",
"fill": "none",
"fontSize": 32.0,
"height": 54.0,
"lineStyle": "none",
"lineWidth": 1.5,
"stroke": "#00007f",
"text": "\u222b",
"type": "text",
"width": 28.0,
"x": 40.0,
"y": 32.0
}
]
},
"properties": {
"showName": false
},
"library": {
"showSubtree": true
},
"implementation": {
"kind": "text",
"source": {
"equations": "initial out = initial;\nder(out) = in;",
"parameters": [
{
"id": "parameter-6bdc1c76",
"name": "initial",
"type": "real",
"value": "0"
}
]
} }
} }
} }
], ],
"connections": [ "connections": [
{ {
"id": "87490296-e353-43ea-8b59-55329950fdce", "id": "f89a8edb-b662-4a55-aa00-6d681b0bf6ae",
"source": { "source": {
"block": "af73487a-4a2d-4908-b919-d0a2eaa75ce2", "block": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
"port": "port-de109124" "port": "port-fe9e6486"
}, },
"target": { "target": {
"block": "b89a77a1-d773-4ee3-8c3f-74377c29b4b2", "block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-4f732b3e" "port": "port-c995845c"
}, },
"name": "", "name": "",
"properties": { "properties": {
"waypoints": [ "waypoints": [
{ {
"x": -64.0, "x": 192.0,
"y": -192.0 "y": -224.0
}, }
]
}
},
{
"id": "c1a2f8e2-b98f-45ee-9ef6-fad8cd42b71b",
"source": {
"block": "478e7ff8-baa1-467f-8707-e375b99395f6",
"port": "port-4eeea4e7"
},
"target": {
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-c995845c"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "e889f7bd-c684-4e59-8e9d-1693bd2ba87f",
"source": {
"block": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
"port": "port-fe9e6486"
},
"target": {
"block": "478e7ff8-baa1-467f-8707-e375b99395f6",
"port": "port-1cbabc8f"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "1cebb01c-7da4-48c7-b7ea-5585f64cd524",
"source": {
"block": "1995074c-4580-4ca3-8691-6923aedc5833",
"port": "port-fe9e6486"
},
"target": {
"block": "48af4110-839a-45f7-a105-7e7dbc51676b",
"port": "port-12903207"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "6d16540e-d902-4ebf-a6a1-dcdda0cc8458",
"source": {
"block": "48af4110-839a-45f7-a105-7e7dbc51676b",
"port": "port-b3370b1a"
},
"target": {
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-c995845c"
},
"name": "",
"properties": {
"waypoints": [
{ {
"x": -64.0, "x": 192.0,
"y": -256.0 "y": 32.0
}, }
]
}
},
{
"id": "206f1501-4509-4ba7-a28e-156e35de51ea",
"source": {
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
"port": "port-a2f0dea6"
},
"target": {
"interface": "port-c75d0f16"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "5d234bec-8754-46e8-b9e8-1c802bb9f9ba",
"source": {
"interface": "port-1342716d"
},
"target": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "33ba6f27-6f3a-4290-baac-b3cddcaa6b50",
"source": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
},
"target": {
"block": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
"port": "port-df34ce84"
},
"name": "",
"properties": {
"waypoints": []
}
},
{
"id": "ee4a3409-fc46-485c-b475-b456dddb93b5",
"source": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
},
"target": {
"block": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
"port": "port-df34ce84"
},
"name": "",
"properties": {
"waypoints": [
{ {
"x": 64.0, "x": -192.0,
"y": -256.0 "y": -224.0
}
]
}
},
{
"id": "8d1a5f3c-b706-4671-873a-2121f55f748b",
"source": {
"junction": "7a552e5e-b15d-46ca-a891-37e5129f88d1"
},
"target": {
"block": "1995074c-4580-4ca3-8691-6923aedc5833",
"port": "port-df34ce84"
},
"name": "",
"properties": {
"waypoints": [
{
"x": -192.0,
"y": 32.0
} }
] ]
} }
} }
], ],
"annotations": [] "annotations": [],
"junctions": [
{
"id": "7a552e5e-b15d-46ca-a891-37e5129f88d1",
"position": {
"x": -192.0,
"y": -96.0
},
"type": "signal"
}
]
} }
} }
} }