Added array ports and junctions
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"]))
|
||||||
|
|
||||||
|
|
||||||
@@ -176,6 +184,31 @@ class Parameter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
@@ -221,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
|
||||||
@@ -235,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},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -332,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 {
|
||||||
@@ -409,8 +448,18 @@ class GraphDocument:
|
|||||||
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)
|
||||||
@@ -419,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)
|
||||||
@@ -430,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:
|
||||||
@@ -438,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(
|
||||||
@@ -472,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()),
|
||||||
@@ -479,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()),
|
||||||
|
|||||||
@@ -28,7 +28,8 @@
|
|||||||
"y": 64.0
|
"y": 64.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "signal"
|
"type": "signal",
|
||||||
|
"multipleConnections": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -45,7 +46,8 @@
|
|||||||
"y": 40.0
|
"y": 40.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "signal"
|
"type": "signal",
|
||||||
|
"multipleConnections": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -131,7 +133,8 @@
|
|||||||
"y": 64.0
|
"y": 64.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "signal"
|
"type": "signal",
|
||||||
|
"multipleConnections": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputs": [
|
"outputs": [
|
||||||
@@ -148,7 +151,8 @@
|
|||||||
"y": 40.0
|
"y": 40.0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "signal"
|
"type": "signal",
|
||||||
|
"multipleConnections": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -210,6 +214,222 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -23,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,
|
||||||
@@ -31,6 +32,7 @@ from bedit.core.model import (
|
|||||||
Endpoint,
|
Endpoint,
|
||||||
GraphDocument,
|
GraphDocument,
|
||||||
Icon,
|
Icon,
|
||||||
|
Junction,
|
||||||
Parameter,
|
Parameter,
|
||||||
Port,
|
Port,
|
||||||
clone_component,
|
clone_component,
|
||||||
@@ -228,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
|
||||||
@@ -254,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,
|
||||||
@@ -265,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,
|
||||||
@@ -405,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:
|
||||||
@@ -420,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":
|
||||||
@@ -526,6 +618,12 @@ class DocumentController(QObject):
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
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(
|
||||||
@@ -849,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:
|
||||||
@@ -871,7 +999,11 @@ 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_data":
|
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)
|
item = graph.connections.get(item_id)
|
||||||
if item is not None:
|
if item is not None:
|
||||||
item.name = values["name"]
|
item.name = values["name"]
|
||||||
|
|||||||
@@ -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}]"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from copy import deepcopy
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtWidgets import QComboBox, QHeaderView, QTableWidgetItem, QWidget
|
from PySide6.QtWidgets import QCheckBox, QComboBox, QHeaderView, QTableWidgetItem, QWidget
|
||||||
|
|
||||||
from bedit.core.model import Parameter, Port
|
from bedit.core.model import Parameter, Port
|
||||||
from bedit.core.port_types import PortTypeRegistry
|
from bedit.core.port_types import PortTypeRegistry
|
||||||
@@ -60,11 +60,13 @@ class TextDefinitionEditor(QWidget):
|
|||||||
name_item = self.ui.portsTable.item(row, 0)
|
name_item = self.ui.portsTable.item(row, 0)
|
||||||
type_combo = self.ui.portsTable.cellWidget(row, 1)
|
type_combo = self.ui.portsTable.cellWidget(row, 1)
|
||||||
orientation_combo = self.ui.portsTable.cellWidget(row, 2)
|
orientation_combo = self.ui.portsTable.cellWidget(row, 2)
|
||||||
|
multiple_check = self.ui.portsTable.cellWidget(row, 3)
|
||||||
port = Port(
|
port = Port(
|
||||||
id=name_item.data(ID_ROLE),
|
id=name_item.data(ID_ROLE),
|
||||||
name=name_item.text().strip(),
|
name=name_item.text().strip(),
|
||||||
type=type_combo.currentData(),
|
type=type_combo.currentData(),
|
||||||
properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}),
|
properties=deepcopy(name_item.data(PROPERTIES_ROLE) or {}),
|
||||||
|
allows_multiple_connections=multiple_check.isChecked(),
|
||||||
)
|
)
|
||||||
target = inputs if orientation_combo.currentData() == "input" else outputs
|
target = inputs if orientation_combo.currentData() == "input" else outputs
|
||||||
target.append(port)
|
target.append(port)
|
||||||
@@ -137,6 +139,10 @@ class TextDefinitionEditor(QWidget):
|
|||||||
2,
|
2,
|
||||||
self._new_combo([("Input", "input"), ("Output", "output")], orientation),
|
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:
|
def _append_parameter(self, parameter: Parameter) -> None:
|
||||||
table = self.ui.parametersTable
|
table = self.ui.parametersTable
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -52,18 +52,20 @@ class Ui_TextDefinitionEditor(object):
|
|||||||
self.portsLayout = QVBoxLayout(self.portsGroup)
|
self.portsLayout = QVBoxLayout(self.portsGroup)
|
||||||
self.portsLayout.setObjectName(u"portsLayout")
|
self.portsLayout.setObjectName(u"portsLayout")
|
||||||
self.portsTable = QTableWidget(self.portsGroup)
|
self.portsTable = QTableWidget(self.portsGroup)
|
||||||
if (self.portsTable.columnCount() < 3):
|
if (self.portsTable.columnCount() < 4):
|
||||||
self.portsTable.setColumnCount(3)
|
self.portsTable.setColumnCount(4)
|
||||||
__qtablewidgetitem = QTableWidgetItem()
|
__qtablewidgetitem = QTableWidgetItem()
|
||||||
self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
|
self.portsTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
|
||||||
__qtablewidgetitem1 = QTableWidgetItem()
|
__qtablewidgetitem1 = QTableWidgetItem()
|
||||||
self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
|
self.portsTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
|
||||||
__qtablewidgetitem2 = QTableWidgetItem()
|
__qtablewidgetitem2 = QTableWidgetItem()
|
||||||
self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
|
self.portsTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
|
||||||
|
__qtablewidgetitem3 = QTableWidgetItem()
|
||||||
|
self.portsTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
|
||||||
self.portsTable.setObjectName(u"portsTable")
|
self.portsTable.setObjectName(u"portsTable")
|
||||||
self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
self.portsTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
self.portsTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
self.portsTable.setColumnCount(3)
|
self.portsTable.setColumnCount(4)
|
||||||
|
|
||||||
self.portsLayout.addWidget(self.portsTable)
|
self.portsLayout.addWidget(self.portsTable)
|
||||||
|
|
||||||
@@ -94,12 +96,12 @@ class Ui_TextDefinitionEditor(object):
|
|||||||
self.parametersTable = QTableWidget(self.parametersGroup)
|
self.parametersTable = QTableWidget(self.parametersGroup)
|
||||||
if (self.parametersTable.columnCount() < 3):
|
if (self.parametersTable.columnCount() < 3):
|
||||||
self.parametersTable.setColumnCount(3)
|
self.parametersTable.setColumnCount(3)
|
||||||
__qtablewidgetitem3 = QTableWidgetItem()
|
|
||||||
self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem3)
|
|
||||||
__qtablewidgetitem4 = QTableWidgetItem()
|
__qtablewidgetitem4 = QTableWidgetItem()
|
||||||
self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem4)
|
self.parametersTable.setHorizontalHeaderItem(0, __qtablewidgetitem4)
|
||||||
__qtablewidgetitem5 = QTableWidgetItem()
|
__qtablewidgetitem5 = QTableWidgetItem()
|
||||||
self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem5)
|
self.parametersTable.setHorizontalHeaderItem(1, __qtablewidgetitem5)
|
||||||
|
__qtablewidgetitem6 = QTableWidgetItem()
|
||||||
|
self.parametersTable.setHorizontalHeaderItem(2, __qtablewidgetitem6)
|
||||||
self.parametersTable.setObjectName(u"parametersTable")
|
self.parametersTable.setObjectName(u"parametersTable")
|
||||||
self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
self.parametersTable.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||||
self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
self.parametersTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
||||||
@@ -147,15 +149,17 @@ class Ui_TextDefinitionEditor(object):
|
|||||||
___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
|
___qtablewidgetitem1.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
|
||||||
___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2)
|
___qtablewidgetitem2 = self.portsTable.horizontalHeaderItem(2)
|
||||||
___qtablewidgetitem2.setText(QCoreApplication.translate("TextDefinitionEditor", u"Orientation", None))
|
___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.addPortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Port", None))
|
||||||
self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None))
|
self.removePortButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Port", None))
|
||||||
self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None))
|
self.parametersGroup.setTitle(QCoreApplication.translate("TextDefinitionEditor", u"Parameters", None))
|
||||||
___qtablewidgetitem3 = self.parametersTable.horizontalHeaderItem(0)
|
___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(0)
|
||||||
___qtablewidgetitem3.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
|
___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Name", None))
|
||||||
___qtablewidgetitem4 = self.parametersTable.horizontalHeaderItem(1)
|
___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(1)
|
||||||
___qtablewidgetitem4.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
|
___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Type", None))
|
||||||
___qtablewidgetitem5 = self.parametersTable.horizontalHeaderItem(2)
|
___qtablewidgetitem6 = self.parametersTable.horizontalHeaderItem(2)
|
||||||
___qtablewidgetitem5.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None))
|
___qtablewidgetitem6.setText(QCoreApplication.translate("TextDefinitionEditor", u"Value", None))
|
||||||
self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None))
|
self.addParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Add Parameter", None))
|
||||||
self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None))
|
self.removeParameterButton.setText(QCoreApplication.translate("TextDefinitionEditor", u"Remove Parameter", None))
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -33,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
|
||||||
@@ -79,6 +79,42 @@ 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):
|
class NameLabelItem(QGraphicsSimpleTextItem):
|
||||||
"""Movable italic name label whose position is stored by its owner."""
|
"""Movable italic name label whose position is stored by its owner."""
|
||||||
|
|
||||||
@@ -248,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__()
|
||||||
@@ -330,6 +366,8 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.connection_id = connection.id
|
self.connection_id = connection.id
|
||||||
self.name = connection.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.controller = controller
|
||||||
self.style = style or ConnectionStyle()
|
self.style = style or ConnectionStyle()
|
||||||
self.start = QPointF()
|
self.start = QPointF()
|
||||||
@@ -374,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())
|
||||||
@@ -381,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):
|
||||||
@@ -448,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)
|
||||||
)
|
)
|
||||||
@@ -799,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"
|
||||||
@@ -822,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()
|
||||||
@@ -844,6 +891,10 @@ 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,
|
connection,
|
||||||
@@ -892,6 +943,15 @@ class GraphScene(QGraphicsScene):
|
|||||||
if item_kind in {"connection", "connection_data"}:
|
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:
|
||||||
@@ -945,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)
|
||||||
@@ -979,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
|
||||||
|
|
||||||
@@ -988,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
|
||||||
|
|
||||||
@@ -1076,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()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1104,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}",
|
||||||
)
|
)
|
||||||
@@ -1117,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:
|
||||||
@@ -1131,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]))
|
||||||
@@ -1148,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),
|
||||||
)
|
)
|
||||||
@@ -1200,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 = []
|
||||||
@@ -1351,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:
|
||||||
|
|||||||
@@ -315,6 +315,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._applying_text_definition = False
|
self._applying_text_definition = False
|
||||||
except (TypeError, ValueError) as error:
|
except (TypeError, ValueError) as error:
|
||||||
QMessageBox.critical(self, "Invalid text component", str(error))
|
QMessageBox.critical(self, "Invalid text component", str(error))
|
||||||
|
self._load_text_definition()
|
||||||
return False
|
return False
|
||||||
self.ui.textDefinitionEditor.set_modified(False)
|
self.ui.textDefinitionEditor.set_modified(False)
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<widget class="QGroupBox" name="portsGroup">
|
<widget class="QGroupBox" name="portsGroup">
|
||||||
<property name="title"><string>Ports</string></property>
|
<property name="title"><string>Ports</string></property>
|
||||||
<layout class="QVBoxLayout" name="portsLayout">
|
<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>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>Orientation</string></property></column></widget></item>
|
<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>
|
<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>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
|||||||
@@ -6,16 +6,50 @@
|
|||||||
},
|
},
|
||||||
"roots": [
|
"roots": [
|
||||||
{
|
{
|
||||||
"id": "b53c8186-0926-46b1-80ec-51a22ac5e0c6",
|
"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",
|
||||||
@@ -51,16 +85,830 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"properties": {},
|
"properties": {
|
||||||
|
"showName": false
|
||||||
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"showSubtree": true
|
"showSubtree": false
|
||||||
},
|
},
|
||||||
"implementation": {
|
"implementation": {
|
||||||
"kind": "graph",
|
"kind": "graph",
|
||||||
"graph": {
|
"graph": {
|
||||||
"blocks": [],
|
"blocks": [
|
||||||
"connections": [],
|
{
|
||||||
"annotations": []
|
"id": "cbfb5d1f-38cc-412e-b8e3-53aa82e46eb8",
|
||||||
|
"name": "g_D",
|
||||||
|
"position": {
|
||||||
|
"x": -128.0,
|
||||||
|
"y": -160.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": "478e7ff8-baa1-467f-8707-e375b99395f6",
|
||||||
|
"name": "differentiate0",
|
||||||
|
"position": {
|
||||||
|
"x": 0.0,
|
||||||
|
"y": -160.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": [
|
||||||
|
{
|
||||||
|
"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": "d/dt",
|
||||||
|
"type": "text",
|
||||||
|
"width": 48.0,
|
||||||
|
"x": 40.0,
|
||||||
|
"y": 40.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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": "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": [
|
||||||
|
{
|
||||||
|
"id": "f89a8edb-b662-4a55-aa00-6d681b0bf6ae",
|
||||||
|
"source": {
|
||||||
|
"block": "9011e8f6-940a-40ca-8bdf-773f0ed1c9f3",
|
||||||
|
"port": "port-fe9e6486"
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"block": "c7440f4d-f206-4c27-9fd2-5722950f207f",
|
||||||
|
"port": "port-c995845c"
|
||||||
|
},
|
||||||
|
"name": "",
|
||||||
|
"properties": {
|
||||||
|
"waypoints": [
|
||||||
|
{
|
||||||
|
"x": 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": 192.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": -192.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": [],
|
||||||
|
"junctions": [
|
||||||
|
{
|
||||||
|
"id": "7a552e5e-b15d-46ca-a891-37e5129f88d1",
|
||||||
|
"position": {
|
||||||
|
"x": -192.0,
|
||||||
|
"y": -96.0
|
||||||
|
},
|
||||||
|
"type": "signal"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user