diff --git a/src/ewoksdraw/css_styles/css_link_cubic_bezier.css b/src/ewoksdraw/css_styles/css_link_cubic_bezier.css index 9f42426..f787683 100644 --- a/src/ewoksdraw/css_styles/css_link_cubic_bezier.css +++ b/src/ewoksdraw/css_styles/css_link_cubic_bezier.css @@ -6,3 +6,7 @@ stroke-linejoin: round; stroke-dasharray: none; } + +.link_cubic_bezier[data-import-error=''] { + stroke: var(--error); +} diff --git a/src/ewoksdraw/graph_to_svg.py b/src/ewoksdraw/graph_to_svg.py index bd49bc5..f1a7f2b 100644 --- a/src/ewoksdraw/graph_to_svg.py +++ b/src/ewoksdraw/graph_to_svg.py @@ -64,7 +64,11 @@ def graph_to_svg(graph: TaskGraph, output_path: str | Path) -> None: canvas = SvgCanvas(width=elk_graph["width"], height=elk_graph["height"]) canvas.add_background() canvas.add_element( - build_svg_link_group(elk_graph, group_id=f"{graph.graph_id}-links") + build_svg_link_group( + elk_graph, + task_group.extract_import_errors(), + group_id=f"{graph.graph_id}-links", + ) ) canvas.add_element(task_group) canvas.draw(output_path) diff --git a/src/ewoksdraw/layout/elk_link_group_builder.py b/src/ewoksdraw/layout/elk_link_group_builder.py index a1e525a..7fb1a50 100644 --- a/src/ewoksdraw/layout/elk_link_group_builder.py +++ b/src/ewoksdraw/layout/elk_link_group_builder.py @@ -3,31 +3,48 @@ from ..geometry.cubic_bezier_path import Point from ..svg.svg_group import SvgGroup from ..svg.svg_link_cubic_bezier import SvgLinkCubicBezier +from .elk_converter import ElkEdge from .elk_converter import ElkGraph from .elk_converter import ElkSection def build_svg_link_group( - elk_graph: ElkGraph, group_id: str | None = None + elk_graph: ElkGraph, + task_import_errors: dict[str, bool] | None = None, + group_id: str | None = None, ) -> SvgGroup[SvgLinkCubicBezier]: """Build an SVG link group from the routed edges of an ELK graph.""" link_group: SvgGroup[SvgLinkCubicBezier] = SvgGroup(group_id=group_id) svg_links: list[SvgLinkCubicBezier] = [] for edge in elk_graph["edges"]: + import_error = _edge_has_import_error(edge, task_import_errors) for section in edge.get("sections", list()): points = _section_points(section) cubic_bezier_path = CubicBezierPath.from_points( points=points, radius=LINK_TURN_RADIUS, ) - svg_link = SvgLinkCubicBezier(cubic_bezier_path) + svg_link = SvgLinkCubicBezier(cubic_bezier_path, import_error=import_error) svg_links.append(svg_link) link_group.add_elements(svg_links) return link_group +def _edge_has_import_error( + edge: ElkEdge, task_import_errors: dict[str, bool] | None = None +) -> bool: + if task_import_errors is None: + return False + # FIXME: + source_task_id = edge["sources"][0].split(".output.")[0] + target_task_id = edge["targets"][0].split(".input.")[0] + return task_import_errors.get(source_task_id, False) or task_import_errors.get( + target_task_id, False + ) + + def _section_points(section: ElkSection) -> list[Point]: """Convert an ELK edge section into an ordered list of points. diff --git a/src/ewoksdraw/svg/svg_link_cubic_bezier.py b/src/ewoksdraw/svg/svg_link_cubic_bezier.py index 97d3136..f612bcc 100644 --- a/src/ewoksdraw/svg/svg_link_cubic_bezier.py +++ b/src/ewoksdraw/svg/svg_link_cubic_bezier.py @@ -10,6 +10,7 @@ class SvgLinkCubicBezier(SvgElement): :param color: The stroke color of the link. :param stroke_width: The stroke width of the link. :param stroke_dasharray: The SVG stroke-dasharray value of the link. + :param import_error: Whether the link connects to a task that cannot be imported. """ def __init__( @@ -18,10 +19,13 @@ def __init__( color: str | None = None, stroke_width: float | None = None, stroke_dasharray: str | None = None, + import_error: bool = False, ): string_svg = self._convert_path_data_to_svg_attribute(path) attr = {"d": string_svg} + if import_error: + attr["data-import-error"] = "" styles = [] if color is not None: diff --git a/src/ewoksdraw/svg/svg_task.py b/src/ewoksdraw/svg/svg_task.py index c14a3e5..99372f2 100644 --- a/src/ewoksdraw/svg/svg_task.py +++ b/src/ewoksdraw/svg/svg_task.py @@ -135,6 +135,10 @@ def _scale_vertical(self) -> None: y2=self._title.height - self._title.vertical_margin // 2, ) + @property + def import_error(self) -> bool: + return self._box.get_attr("data-import-error") is not None + @property def width(self) -> float: return self._box.width diff --git a/src/ewoksdraw/svg/svg_task_group.py b/src/ewoksdraw/svg/svg_task_group.py index 7fd8afe..fbdbb90 100644 --- a/src/ewoksdraw/svg/svg_task_group.py +++ b/src/ewoksdraw/svg/svg_task_group.py @@ -59,6 +59,12 @@ def extract_task_sizes(self) -> TaskSizes: for task_id, svg_task in self._svg_tasks.items() } + def extract_import_errors(self) -> dict[str, bool]: + return { + task_id: svg_task.import_error + for task_id, svg_task in self._svg_tasks.items() + } + def extract_input_positions(self) -> TaskInputPositions: return { task_id: svg_task.get_input_positions() diff --git a/src/ewoksdraw/tests/test_graph_to_svg.py b/src/ewoksdraw/tests/test_graph_to_svg.py index 43c5ac2..611071f 100644 --- a/src/ewoksdraw/tests/test_graph_to_svg.py +++ b/src/ewoksdraw/tests/test_graph_to_svg.py @@ -124,3 +124,9 @@ def test_workflow_with_non_importable_task( task_box = element assert task_box is not None assert "data-import-error" in task_box.keys() + + tree_root = ElementTree.parse(output_path).getroot() + link_group = _find_svg_group(tree_root, f"{ewoks_graph.graph_id}-links") + links = [element for element in link_group if element.tag.endswith("path")] + flagged_links = [link for link in links if "data-import-error" in link.keys()] + assert len(flagged_links) == 2