Skip to content

Commit 4a4515b

Browse files
author
Nikolas Dahn
committed
Working on more efficient render loop
1 parent 7cdf638 commit 4a4515b

1 file changed

Lines changed: 104 additions & 53 deletions

File tree

hkb_editor/gui/widgets/graph_widget.py

Lines changed: 104 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def __init__(
5454
self.graph = None
5555
self.root: str = None
5656
self.nodes: dict[str, Node] = {}
57+
self.prev_hover: Node = None
5758
self.hovered_node: Node = None
5859
self.selected_node: Node = None
5960
self.default_axis_range = default_axis_range
@@ -66,6 +67,8 @@ def __init__(
6667
self._y_offset = 0.0
6768
# Plot-space positions from the last layout pass; used to transform to pixels
6869
self._plot_positions: dict[str, tuple[float, float]] = {}
70+
self._graph_layers: list[str] = None
71+
self._rendered_nodes: set[str] = set()
6972

7073
# Set when visibility changes; cleared after layout is recomputed
7174
self._layout_dirty: bool = False
@@ -167,6 +170,8 @@ def set_graph(self, graph: nx.DiGraph) -> None:
167170
self.graph = graph
168171

169172
if graph:
173+
# Topological order ensures parents are drawn (and positioned) before children
174+
self._graph_layers = list(nx.topological_sort(graph))
170175
self.root = next(n for n, in_deg in graph.in_degree() if in_deg == 0)
171176

172177
for n, data in graph.nodes.items():
@@ -189,14 +194,14 @@ def clear_highlights(self) -> None:
189194
def highlight_node(self, node: Node | str, color: style.RGBA = style.green) -> None:
190195
if isinstance(node, Node):
191196
node = node.id
192-
197+
193198
self._manual_highlights[node] = color
194199
self._render_required = True
195200

196201
def unhighlight_node(self, node: Node | str) -> None:
197202
if isinstance(node, Node):
198203
node = node.id
199-
204+
200205
del self._manual_highlights[node]
201206
self._render_required = True
202207

@@ -521,7 +526,7 @@ def fold_node(self, node: Node) -> None:
521526
def reveal(self, node: Node | str) -> None:
522527
if not node:
523528
return
524-
529+
525530
if isinstance(node, str):
526531
node = self.nodes[node]
527532

@@ -602,6 +607,7 @@ def _render_graph(self, sender: str, app_data: list, user_data: Any) -> None:
602607
helper_data = app_data[0]
603608
mouse_x = helper_data["MouseX_PixelSpace"]
604609
mouse_y = helper_data["MouseY_PixelSpace"]
610+
self.prev_hover = self.hovered_node
605611
self.hovered_node = None
606612

607613
# One series anchor gives us offset once we know scale.
@@ -622,6 +628,9 @@ def _render_graph(self, sender: str, app_data: list, user_data: Any) -> None:
622628
self._render_required = False
623629

624630
if self._layout_dirty:
631+
dpg.delete_item(sender, children_only=True, slot=2)
632+
self._rendered_nodes.clear()
633+
625634
for node in self.nodes.values():
626635
if node.visible and node.size is None:
627636
# Estimate size in plot space; layout uses plot-space units
@@ -631,49 +640,103 @@ def _render_graph(self, sender: str, app_data: list, user_data: Any) -> None:
631640
self._render_required = True
632641
self._layout_dirty = False
633642

634-
dpg.delete_item(sender, children_only=True, slot=2)
635643
dpg.push_container_stack(sender)
636644
dpg.configure_item(sender, tooltip=False)
637645

638-
# Topological order ensures parents are drawn (and positioned) before children
639-
for n in nx.topological_sort(self.graph):
646+
for n in self._graph_layers:
640647
node = self.nodes[n]
641-
plot_pos = self._plot_positions.get(n)
642-
if not node.visible or plot_pos is None:
643-
continue
644-
645-
px, py = self._to_pixel(*plot_pos)
646-
pw_node, ph_node = (
647-
self._size_to_pixel(*node.size) if node.size else (0.0, 0.0)
648-
)
648+
if n not in self._rendered_nodes:
649+
self._draw_node(node, mouse_x, mouse_y)
650+
self._rendered_nodes.add(n)
651+
else:
652+
self._update_node(node, mouse_x, mouse_y)
649653

650-
# Hit-test with pixel-space mouse coords and pixel-space box
651-
if not self.hovered_node:
652-
x1, y1 = px, py
653-
x2, y2 = px + pw_node, py + ph_node
654-
if x1 <= mouse_x < x2 and y1 <= mouse_y < y2:
655-
self.hovered_node = node
656-
657-
if self.draw_edges:
658-
for child_id in self.graph.successors(node.id):
659-
child_node = self.nodes[child_id]
660-
child_pos = self._plot_positions.get(child_id)
661-
if child_node.visible and child_pos is not None:
662-
cx, cy = self._to_pixel(*child_pos)
663-
self._draw_edge(
664-
node, px, py, pw_node, ph_node, child_node, cx, cy
665-
)
666-
667-
self._draw_node_box(node, px, py, pw_node, ph_node)
654+
if self.prev_hover != self.hovered_node:
655+
self._set_node_hover_style(self.prev_hover)
656+
self._set_node_hover_style(node)
668657

669658
dpg.pop_container_stack()
670659

660+
def _node_tag(self, node: Node, suffix: str = None) -> str:
661+
t = f"{self.tag}_node_{node.id}"
662+
if suffix:
663+
t += "_" + suffix
664+
return t
665+
666+
def _set_node_hover_style(self, node: Node) -> None:
667+
if not node or not dpg.does_item_exist(self._node_tag(node, "box")):
668+
return
669+
670+
color = style.white
671+
thickness = 1
672+
673+
if self.select_enabled and node == self.selected_node:
674+
color = style.blue
675+
thickness = 2
676+
else:
677+
if node.id in self._manual_highlights:
678+
color = self._manual_highlights[node.id]
679+
680+
if self.hover_enabled and node == self.hovered_node:
681+
thickness = 2
682+
683+
dpg.configure_item(self._node_tag(node, "box"), thickness=thickness, color=color)
684+
685+
def _update_node(self, node: Node, mouse_x: float, mouse_y: float) -> None:
686+
plot_pos = self._plot_positions.get(node.id)
687+
if not node.visible or plot_pos is None:
688+
return
689+
690+
px, py = self._to_pixel(*plot_pos)
691+
pw, ph = self._size_to_pixel(*node.size) if node.size else (0.0, 0.0)
692+
margin = self.layout.text_margin
693+
694+
# Hit-test with pixel-space mouse coords and pixel-space box
695+
if not self.hovered_node:
696+
x1, y1 = px, py
697+
x2, y2 = px + pw, py + ph
698+
if x1 <= mouse_x < x2 and y1 <= mouse_y < y2:
699+
self.hovered_node = node
700+
701+
dpg.configure_item(self._node_tag(node, "box"), pmin=(px, py), pmax=(px + pw, py + ph))
702+
703+
for i in range(9):
704+
line_tag = self._node_tag(node, f"text_{i}")
705+
if dpg.does_item_exist(line_tag):
706+
# TODO adjust py
707+
dpg.configure_item(line_tag, pos=(px + margin, py + margin))
708+
709+
# TODO edges
710+
711+
def _draw_node(self, node: Node, mouse_x: float, mouse_y: float) -> str:
712+
plot_pos = self._plot_positions.get(node.id)
713+
if not node.visible or plot_pos is None:
714+
return
715+
716+
px, py = self._to_pixel(*plot_pos)
717+
pw, ph = self._size_to_pixel(*node.size) if node.size else (0.0, 0.0)
718+
719+
# Hit-test with pixel-space mouse coords and pixel-space box
720+
if not self.hovered_node:
721+
x1, y1 = px, py
722+
x2, y2 = px + pw, py + ph
723+
if x1 <= mouse_x < x2 and y1 <= mouse_y < y2:
724+
self.hovered_node = node
725+
726+
if self.draw_edges:
727+
for child_id in self.graph.successors(node.id):
728+
child_node = self.nodes[child_id]
729+
child_pos = self._plot_positions.get(child_id)
730+
if child_node.visible and child_pos is not None:
731+
cx, cy = self._to_pixel(*child_pos)
732+
self._draw_edge(node, px, py, pw, ph, child_node, cx, cy)
733+
734+
return self._draw_node_box(node, px, py, pw, ph)
735+
671736
def _draw_node_box(
672-
self, node: Node, px: float, py: float, pixel_w: float, pixel_h: float
737+
self, node: Node, px: float, py: float, pw: float, ph: float
673738
) -> None:
674-
tag = f"{self.tag}_node_{node.id}"
675-
676-
if dpg.does_item_exist(tag):
739+
if dpg.does_item_exist(self._node_tag(node, "box")):
677740
return
678741

679742
scale = self.zoom_factor
@@ -694,26 +757,13 @@ def _draw_node_box(
694757
max_len = max(len(s) for s in lines)
695758
lines = [s.center(max_len) for s in lines]
696759

697-
edge_color = style.white
698-
thickness = 1
699-
700-
if self.select_enabled and node == self.selected_node:
701-
edge_color = style.blue
702-
thickness = 2
703-
else:
704-
if node.id in self._manual_highlights:
705-
edge_color = self._manual_highlights[node.id]
706-
707-
if self.hover_enabled and node == self.hovered_node:
708-
thickness = 2
709-
710760
dpg.draw_rectangle(
711761
(px, py),
712-
(px + pixel_w, py + pixel_h),
762+
(px + pw, py + ph),
713763
fill=style.dark_grey,
714-
color=edge_color,
715-
thickness=thickness,
716-
tag=f"{tag}_box",
764+
color=style.white,
765+
thickness=1,
766+
tag=self._node_tag(node, "box"),
717767
)
718768

719769
for i, text in enumerate(lines):
@@ -722,6 +772,7 @@ def _draw_node_box(
722772
text,
723773
size=12 * scale,
724774
color=colors[i],
775+
tag=self._node_tag(node, f"text_{i}"),
725776
)
726777

727778
def _draw_edge(

0 commit comments

Comments
 (0)