From 5f5dd599495a84f0995911b7d8b22b51c31779e3 Mon Sep 17 00:00:00 2001 From: Tigar Cyr Date: Sun, 7 Jul 2024 21:23:33 -0700 Subject: [PATCH] Add and remove tips and tip configs with edges --- manim/mobject/graph.py | 139 +++++++----------- .../control_data/graph/digraph_add_edges.npz | Bin 0 -> 13951 bytes tests/test_graphical_units/test_graph.py | 40 +++++ 3 files changed, 95 insertions(+), 84 deletions(-) create mode 100644 tests/test_graphical_units/control_data/graph/digraph_add_edges.npz create mode 100644 tests/test_graphical_units/test_graph.py diff --git a/manim/mobject/graph.py b/manim/mobject/graph.py index 72a26d27db..af9640162d 100644 --- a/manim/mobject/graph.py +++ b/manim/mobject/graph.py @@ -9,7 +9,7 @@ import itertools as it from collections.abc import Hashable, Iterable -from copy import copy +from copy import copy, deepcopy from typing import TYPE_CHECKING, Any, Literal, Protocol, cast import networkx as nx @@ -626,6 +626,7 @@ def __init__( self.vertices = {v: vertex_type(**self._vertex_config[v]) for v in vertices} self.vertices.update(vertex_mobjects) + self.add(*self.vertices.values()) self.change_layout( layout=layout, @@ -635,37 +636,16 @@ def __init__( root_vertex=root_vertex, ) - # build edge_config if edge_config is None: edge_config = {} - default_tip_config = {} - default_edge_config = {} - if edge_config: - default_tip_config = edge_config.pop("tip_config", {}) - default_edge_config = { - k: v - for k, v in edge_config.items() - if not isinstance( - k, tuple - ) # everything that is not an edge is an option - } - self._edge_config = {} - self._tip_config = {} - for e in edges: - if e in edge_config: - self._tip_config[e] = edge_config[e].pop( - "tip_config", copy(default_tip_config) - ) - self._edge_config[e] = edge_config[e] - else: - self._tip_config[e] = copy(default_tip_config) - self._edge_config[e] = copy(default_edge_config) - self.default_edge_config = default_edge_config - self._populate_edge_dict(edges, edge_type) + self.edges = {} + self._edge_config = {} + self.default_edge_config, _ = GenericGraph._split_out_child_configs( + edge_config, lambda k: isinstance(k, tuple) + ) - self.add(*self.vertices.values()) - self.add(*self.edges.values()) + self.add_edges(*edges, edge_type=edge_type, edge_config=edge_config) self.add_updater(self.update_edges) @@ -674,11 +654,11 @@ def _empty_networkx_graph() -> nx.classes.graph.Graph: """Return an empty networkx graph for the given graph type.""" raise NotImplementedError("To be implemented in concrete subclasses") - def _populate_edge_dict( - self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject] - ): - """Helper method for populating the edges of the graph.""" - raise NotImplementedError("To be implemented in concrete subclasses") + @staticmethod + def _split_out_child_configs(config: dict, is_child_key) -> (dict, dict): + parent_config = {k: v for k, v in config.items() if not is_child_key(k)} + child_configs = {k: v for k, v in config.items() if is_child_key(k)} + return parent_config, child_configs def __getitem__(self: Graph, v: Hashable) -> Mobject: return self.vertices[v] @@ -1028,28 +1008,20 @@ def _add_edge( """ if edge_config is None: - edge_config = self.default_edge_config.copy() - added_mobjects = [] - for v in edge: - if v not in self.vertices: - added_mobjects.append(self._add_vertex(v)) - u, v = edge + edge_config = {} + + added_vertices = [self._add_vertex(v) for v in edge if v not in self.vertices] + u, v = edge self._graph.add_edge(u, v) - base_edge_config = self.default_edge_config.copy() - base_edge_config.update(edge_config) - edge_config = base_edge_config - self._edge_config[(u, v)] = edge_config + self._edge_config[edge] = {**self.default_edge_config, **edge_config} + edge_mobject = self._create_edge_mobject(edge, edge_type) - edge_mobject = edge_type( - self[u].get_center(), self[v].get_center(), z_index=-1, **edge_config - ) self.edges[(u, v)] = edge_mobject - self.add(edge_mobject) - added_mobjects.append(edge_mobject) - return self.get_group_class()(*added_mobjects) + + return self.get_group_class()(*added_vertices, edge_mobject) def add_edges( self, @@ -1087,13 +1059,12 @@ def add_edges( """ if edge_config is None: edge_config = {} - non_edge_settings = {k: v for (k, v) in edge_config.items() if k not in edges} - base_edge_config = self.default_edge_config.copy() - base_edge_config.update(non_edge_settings) - base_edge_config = {e: base_edge_config.copy() for e in edges} - for e in edges: - base_edge_config[e].update(edge_config.get(e, {})) - edge_config = base_edge_config + else: + edge_config = deepcopy(edge_config) + + batch_default_config, custom_configs = GenericGraph._split_out_child_configs( + edge_config, lambda k: isinstance(k, tuple) + ) edge_vertices = set(it.chain(*edges)) new_vertices = [v for v in edge_vertices if v not in self.vertices] @@ -1104,7 +1075,7 @@ def add_edges( self._add_edge( edge, edge_type=edge_type, - edge_config=edge_config[edge], + edge_config={**batch_default_config, **custom_configs.get(edge, {})}, ).submobjects for edge in edges ), @@ -1145,7 +1116,7 @@ def _remove_edge(self, edge: tuple[Hashable]): edge_mobject = self.edges.pop(edge) self._graph.remove_edge(*edge) - self._edge_config.pop(edge, None) + self._edge_config.pop(edge) self.remove(edge_mobject) return edge_mobject @@ -1544,18 +1515,14 @@ def construct(self): def _empty_networkx_graph() -> nx.Graph: return nx.Graph() - def _populate_edge_dict( - self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject] - ): - self.edges = { - (u, v): edge_type( - self[u].get_center(), - self[v].get_center(), - z_index=-1, - **self._edge_config[(u, v)], - ) - for (u, v) in edges - } + def _create_edge_mobject(self, edge, edge_type): + u, v = edge + return edge_type( + self[u].get_center(), + self[v].get_center(), + z_index=-1, + **self._edge_config[(u, v)], + ) def update_edges(self, graph): for (u, v), edge in graph.edges.items(): @@ -1751,21 +1718,25 @@ def construct(self): def _empty_networkx_graph() -> nx.DiGraph: return nx.DiGraph() - def _populate_edge_dict( - self, edges: list[tuple[Hashable, Hashable]], edge_type: type[Mobject] - ): - self.edges = { - (u, v): edge_type( - self[u], - self[v], - z_index=-1, - **self._edge_config[(u, v)], - ) - for (u, v) in edges - } + @staticmethod + def _split_out_tip_configs(config: dict) -> (dict, dict): + is_tip_config = lambda k: k == "tip_config" + edge_config, tip_config = GenericGraph._split_out_child_configs( + config, is_tip_config + ) + return edge_config, tip_config.get("tip_config", {}) - for (u, v), edge in self.edges.items(): - edge.add_tip(**self._tip_config[(u, v)]) + def _create_edge_mobject(self, edge, edge_type): + edge_config, tip_config = DiGraph._split_out_tip_configs(self._edge_config[edge]) + u, v = edge + edge_mobject = edge_type( + self[u], + self[v], + z_index=-1, + **edge_config, + ) + edge_mobject.add_tip(**tip_config) + return edge_mobject def update_edges(self, graph): """Updates the edges to stick at their corresponding vertices. diff --git a/tests/test_graphical_units/control_data/graph/digraph_add_edges.npz b/tests/test_graphical_units/control_data/graph/digraph_add_edges.npz new file mode 100644 index 0000000000000000000000000000000000000000..583e741abe330235e6e3a16b832bc4225662db85 GIT binary patch literal 13951 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 N2WA%-d1dtf{ukB2P2T_j literal 0 HcmV?d00001 diff --git a/tests/test_graphical_units/test_graph.py b/tests/test_graphical_units/test_graph.py new file mode 100644 index 0000000000..59733619fa --- /dev/null +++ b/tests/test_graphical_units/test_graph.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from manim import * +from manim.utils.testing.frames_comparison import frames_comparison + +__module_test__ = "graph" + + +@frames_comparison +def test_digraph_add_edges(scene): + vertices = [i for i in range(5)] + edges = [ + (0, 1), + (1, 2), + (3, 2), + (3, 4), + ] + + edge_config = { + "stroke_width": 2, + "tip_config": { + "tip_shape": ArrowSquareTip, + "tip_length": 0.15, + }, + (3, 4): { + "color": RED, + "tip_config": {"tip_length": 0.25, "tip_width": 0.25} + }, + } + + g = DiGraph( + vertices, + [], + labels=True, + layout="circular", + ).scale(1.4) + + g.add_edges(*edges, edge_config=edge_config) + + scene.add(g)