"""EdgeModel: a connector between two ports, with style and properties.""" from __future__ import annotations from enum import Enum from typing import Optional from .properties import PropertyBag class EndpointDecoration(str, Enum): """Head/tail decorations drawn at an edge endpoint.""" NONE = "none" ARROW = "arrow" CIRCLE = "circle" DIAMOND = "diamond" BAR = "bar" class LineStyle(str, Enum): SOLID = "solid" DASHED = "dashed" DOTTED = "dotted" DASH_DOT = "dash_dot" class EdgeStyle: """Visual style of an edge.""" __slots__ = ("line_style", "width", "color", "tail", "head") def __init__( self, *, line_style: LineStyle = LineStyle.SOLID, width: float = 2.0, color: Optional[str] = None, tail: EndpointDecoration = EndpointDecoration.NONE, head: EndpointDecoration = EndpointDecoration.ARROW, ) -> None: self.line_style = LineStyle(line_style) self.width = float(width) self.color = color # None => derive from relation self.tail = EndpointDecoration(tail) self.head = EndpointDecoration(head) def to_dict(self) -> dict: return { "line_style": self.line_style.value, "width": self.width, "color": self.color, "tail": self.tail.value, "head": self.head.value, } @classmethod def from_dict(cls, data: dict) -> "EdgeStyle": return cls( line_style=LineStyle(data.get("line_style", "solid")), width=data.get("width", 2.0), color=data.get("color"), tail=EndpointDecoration(data.get("tail", "none")), head=EndpointDecoration(data.get("head", "arrow")), ) def clone(self) -> "EdgeStyle": return EdgeStyle.from_dict(self.to_dict()) class EdgeModel: """A connector between (source_node.source_port) and (target_node.target_port). ``relation`` records the media the edge carries (usually inherited from the connected ports) and drives default coloring. ``waypoints`` optionally pins intermediate routing points; when empty the router computes an orthogonal path. """ def __init__( self, edge_id: str, source_node: str, source_port: str, target_node: str, target_port: str, *, relation: str = "", style: Optional[EdgeStyle] = None, properties: Optional[PropertyBag] = None, waypoints: Optional[list[tuple[float, float]]] = None, ) -> None: self.edge_id = edge_id self.source_node = source_node self.source_port = source_port self.target_node = target_node self.target_port = target_port self.relation = relation self.style = style if style is not None else EdgeStyle() self.properties: PropertyBag = properties if properties is not None else PropertyBag() self.waypoints: list[tuple[float, float]] = list(waypoints or []) # Populated by the flow solver (mock calculation); not persisted. self.flow: float = 0.0 # signed magnitude along source->target self.flow_direction: int = 0 # +1 s->t, -1 t->s, 0 none @property def title(self) -> str: return self.properties.value("title", "") or "" def endpoints(self) -> tuple[tuple[str, str], tuple[str, str]]: return (self.source_node, self.source_port), (self.target_node, self.target_port) def to_dict(self) -> dict: return { "id": self.edge_id, "source_node": self.source_node, "source_port": self.source_port, "target_node": self.target_node, "target_port": self.target_port, "relation": self.relation, "style": self.style.to_dict(), "properties": self.properties.to_dict(), "waypoints": [list(w) for w in self.waypoints], } @classmethod def from_dict(cls, data: dict) -> "EdgeModel": return cls( data["id"], data["source_node"], data["source_port"], data["target_node"], data["target_port"], relation=data.get("relation", ""), style=EdgeStyle.from_dict(data.get("style", {})), properties=PropertyBag.from_dict(data.get("properties", {})), waypoints=[tuple(w) for w in data.get("waypoints", [])], ) def clone(self, new_id: str) -> "EdgeModel": d = self.to_dict() d["id"] = new_id return EdgeModel.from_dict(d)