Start with causality inference

This commit is contained in:
2026-07-22 13:20:49 +02:00
parent 09248421d0
commit 32154386e8
17 changed files with 493 additions and 266 deletions

View File

@@ -458,8 +458,7 @@ class IconEditorDialog(QDialog):
self.ui.setupUi(self)
self.setWindowTitle(f"Icon Editor — {component.name}")
self.icon = Icon.from_dict(component.icon.to_dict())
self.inputs = deepcopy(component.inputs)
self.outputs = deepcopy(component.outputs)
self.ports = deepcopy(component.ports)
self.tool_group = QButtonGroup(self)
self.tool_group.setExclusive(True)
self.tool_group.addButton(self.ui.pointerButton)
@@ -485,8 +484,10 @@ class IconEditorDialog(QDialog):
self.scene.addRect(self.scene.sceneRect(), QPen(QColor("#64748b"), 0)).setZValue(-100)
for element in self.icon.elements:
self.scene.addItem(ShapeItem(element))
self._add_ports(self.inputs, "input", 0.0)
self._add_ports(self.outputs, "output", self.icon.width)
input_side = [port for port in self.ports if port.orientation != "output"]
output_side = [port for port in self.ports if port.orientation == "output"]
self._add_ports(input_side, "input", 0.0)
self._add_ports(output_side, "output", self.icon.width)
self._initial_fit_pending = True
def showEvent(self, event) -> None: # noqa: N802 (Qt API name)

View File

@@ -173,8 +173,16 @@ class ComponentGraphicsItem(QGraphicsObject):
| QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
| QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges
)
self.input_ports = self._create_ports(component.inputs, "target", 0.0)
self.output_ports = self._create_ports(component.outputs, "source", self.WIDTH)
self.input_ports = self._create_ports(
[port for port in component.ports if port.orientation != "output"],
"target",
0.0,
)
self.output_ports = self._create_ports(
[port for port in component.ports if port.orientation == "output"],
"source",
self.WIDTH,
)
self.setTransformOriginPoint(self.hitbox.center())
self.setRotation(component.rotation)
self.name_label: NameLabelItem | None = None
@@ -371,8 +379,10 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
super().__init__()
self.connection_id = connection.id
self.name = connection.name
self.connection_type = connection.type
self.source_is_junction = connection.source.junction is not None
self.target_is_junction = connection.target.junction is not None
self.causality = connection.causality
self.controller = controller
self.style = style or ConnectionStyle()
self.start = QPointF()
@@ -417,7 +427,11 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
self.setSelected(True)
menu = QMenu()
add_node_action = menu.addAction("Add Node")
add_junction_action = menu.addAction("Add Junction")
add_junction_action = (
menu.addAction("Add Junction")
if self.connection_type != "power"
else None
)
menu.addSeparator()
options_action = menu.addAction("Connection Options…")
selected = menu.exec(event.screenPos())
@@ -425,7 +439,7 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
scene = self.scene()
if isinstance(scene, GraphScene):
scene.add_route_node("connection", self.connection_id, event.scenePos())
elif selected is add_junction_action:
elif add_junction_action is not None and selected is add_junction_action:
scene = self.scene()
if isinstance(scene, GraphScene):
scene.add_connection_junction(self.connection_id, event.scenePos())
@@ -512,12 +526,36 @@ class ConnectionGraphicsItem(QGraphicsPathItem):
if self.style.arrow_style == "open":
painter.drawLine(tip, right)
def _draw_causality_mark(
self, painter: QPainter, point: QPointF, direction: QPointF, *, warning: bool
) -> None:
length = max(0.001, (direction.x() ** 2 + direction.y() ** 2) ** 0.5)
normal = QPointF(-direction.y() / length, direction.x() / length)
half_length = 6.0
color = QColor("#c25a00") if warning else self.pen().color()
painter.setPen(QPen(color, max(2.0, self.pen().widthF()), Qt.PenStyle.SolidLine))
painter.drawLine(point - normal * half_length, point + normal * half_length)
def paint(self, painter: QPainter, option, widget=None) -> None:
super().paint(painter, option, widget)
if self.style.arrow_at_target and not self.target_is_junction:
self._draw_arrow(painter, self.end, self.end_direction)
if self.style.arrow_at_source and not self.source_is_junction:
self._draw_arrow(painter, self.start, -self.start_direction)
if self.causality in {"source", "warn_source"}:
self._draw_causality_mark(
painter,
self.start,
self.start_direction,
warning=self.causality == "warn_source",
)
elif self.causality in {"target", "warn_target"}:
self._draw_causality_mark(
painter,
self.end,
self.end_direction,
warning=self.causality == "warn_target",
)
class WaypointHandle(QGraphicsEllipseItem):
@@ -898,12 +936,12 @@ class GraphScene(QGraphicsScene):
owner = self.controller.active_component
if owner is None or owner.implementation_kind != "graph":
return
for port in owner.inputs:
for port in (port for port in owner.ports if port.orientation != "output"):
item = InterfaceTerminalItem(port, "input", self.controller)
self.addItem(item)
item.setPos(port.x, port.y)
self.input_items[port.id] = item
for port in owner.outputs:
for port in (port for port in owner.ports if port.orientation == "output"):
item = InterfaceTerminalItem(port, "output", self.controller)
self.addItem(item)
item.setPos(port.x, port.y)
@@ -1224,15 +1262,16 @@ class GraphScene(QGraphicsScene):
source_item: ComponentGraphicsItem, target_item: ComponentGraphicsItem
) -> None:
source_ports = [
*source_item.component.outputs,
*(
port
for port in source_item.component.inputs
if port.orientation == "indifferent"
),
port
for port in source_item.component.ports
if port.orientation in {"output", "indifferent"}
]
for output in source_ports:
for input_port in target_item.component.inputs:
for input_port in (
port
for port in target_item.component.ports
if port.orientation in {"input", "indifferent"}
):
if not PortTypeRegistry.compatible(
output.type,
input_port.type,
@@ -1267,7 +1306,11 @@ class GraphScene(QGraphicsScene):
choices: list[ConnectionChoice] = []
interface = Endpoint(interface=terminal.port.id)
if terminal.direction == "input":
for port in component.component.inputs:
for port in (
port
for port in component.component.ports
if port.orientation in {"input", "indifferent"}
):
target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(
terminal.port.type, port.type, terminal.port.domain, port.domain
@@ -1286,12 +1329,9 @@ class GraphScene(QGraphicsScene):
)
else:
ports = [
*component.component.outputs,
*(
port
for port in component.component.inputs
if port.orientation == "indifferent"
),
port
for port in component.component.ports
if port.orientation in {"output", "indifferent"}
]
for port in ports:
source = Endpoint(block=component.component_id, port=port.id)
@@ -1319,7 +1359,11 @@ class GraphScene(QGraphicsScene):
) -> list[ConnectionChoice]:
choices: list[ConnectionChoice] = []
source = junction.endpoint
for port in component.component.inputs:
for port in (
port
for port in component.component.ports
if port.orientation in {"input", "indifferent"}
):
target = Endpoint(block=component.component_id, port=port.id)
if not PortTypeRegistry.compatible(junction.junction.type, port.type):
continue
@@ -1600,6 +1644,8 @@ class GraphScene(QGraphicsScene):
graphics = self.connection_items.get(connection_id)
if connection is None or graphics is None:
return
if connection.type == "power":
return
snapped = _snapped(position)
anchors = [graphics.start, *points, graphics.end]
@@ -1850,8 +1896,7 @@ class GraphWorkspaceView(QGraphicsView):
return
blocks: set[str] = set()
connections: set[str] = set()
inputs: set[str] = set()
outputs: set[str] = set()
ports: set[str] = set()
annotations: set[str] = set()
for item in self.scene().selectedItems():
if isinstance(item, ComponentGraphicsItem):
@@ -1859,10 +1904,10 @@ class GraphWorkspaceView(QGraphicsView):
elif isinstance(item, ConnectionGraphicsItem):
connections.add(item.connection_id)
elif isinstance(item, InterfaceTerminalItem):
(inputs if item.direction == "input" else outputs).add(item.port.id)
ports.add(item.port.id)
elif isinstance(item, (AnnotationGraphicsItem, LineAnnotationGraphicsItem)):
annotations.add(item.annotation_id)
self.controller.delete_selection(blocks, connections, inputs, outputs)
self.controller.delete_selection(blocks, connections, ports)
self.controller.delete_annotations(annotations)
def has_selected_components(self) -> bool: