Skip to content

Commit 8c9fba6

Browse files
committed
refactored auto node layout logic into graph.
1 parent ded773c commit 8c9fba6

File tree

5 files changed

+199
-247
lines changed

5 files changed

+199
-247
lines changed

NodeGraphQt/base/graph.py

Lines changed: 165 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515
from .factory import NodeFactory
1616
from .menu import NodeGraphMenu, NodesMenu
1717
from .model import NodeGraphModel
18-
from .node import NodeObject, BaseNode
18+
from .node import NodeObject, BaseNode, BackdropNode
1919
from .port import Port
20-
from ..constants import (DRAG_DROP_ID,
21-
PIPE_LAYOUT_CURVED,
22-
PIPE_LAYOUT_STRAIGHT,
23-
PIPE_LAYOUT_ANGLE,
24-
IN_PORT, OUT_PORT,
25-
VIEWER_GRID_LINES)
20+
from ..constants import (
21+
DRAG_DROP_ID,
22+
NODE_LAYOUT_DIRECTION, NODE_LAYOUT_VERTICAL, NODE_LAYOUT_HORIZONTAL,
23+
PIPE_LAYOUT_CURVED, PIPE_LAYOUT_STRAIGHT, PIPE_LAYOUT_ANGLE,
24+
IN_PORT, OUT_PORT,
25+
VIEWER_GRID_LINES
26+
)
2627
from ..widgets.node_space_bar import node_space_bar
2728
from ..widgets.viewer import NodeViewer
2829

@@ -182,15 +183,17 @@ def _wire_signals(self):
182183
self._viewer.moved_nodes.connect(self._on_nodes_moved)
183184
self._viewer.node_double_clicked.connect(self._on_node_double_clicked)
184185
self._viewer.node_name_changed.connect(self._on_node_name_changed)
185-
self._viewer.insert_node.connect(self._insert_node)
186+
self._viewer.node_backdrop_updated.connect(
187+
self._on_node_backdrop_updated)
188+
self._viewer.insert_node.connect(self._on_insert_node)
186189

187190
# pass through translated signals.
188191
self._viewer.node_selected.connect(self._on_node_selected)
189192
self._viewer.node_selection_changed.connect(
190193
self._on_node_selection_changed)
191194
self._viewer.data_dropped.connect(self._on_node_data_dropped)
192195

193-
def _insert_node(self, pipe, node_id, prev_node_pos):
196+
def _on_insert_node(self, pipe, node_id, prev_node_pos):
194197
"""
195198
Slot function triggered when a selected node has collided with a pipe.
196199
@@ -344,6 +347,18 @@ def _on_nodes_moved(self, node_data):
344347
self._undo_stack.push(NodeMovedCmd(node, node.pos(), prev_pos))
345348
self._undo_stack.endMacro()
346349

350+
def _on_node_backdrop_updated(self, node_id, update_property, value):
351+
"""
352+
called when a BackdropNode is updated.
353+
354+
Args:
355+
node_id (str): backdrop node id.
356+
value (str): update type.
357+
"""
358+
backdrop = self.get_node_by_id(node_id)
359+
if backdrop and isinstance(backdrop, BackdropNode):
360+
backdrop.on_backdrop_updated(update_property, value)
361+
347362
def _on_search_triggered(self, node_type, pos):
348363
"""
349364
called when the tab search widget is triggered in the viewer.
@@ -859,7 +874,7 @@ def create_node(self, node_type, name=None, selected=True, color=None,
859874
pos (list[int, int]): initial x, y position for the node (default: ``(0, 0)``).
860875
861876
Returns:
862-
NodeGraphQt.BaseNode: the created instance of the node.
877+
NodeGraphQt.NodeObject: the created instance of the node.
863878
"""
864879
if not self._editable:
865880
return
@@ -903,6 +918,7 @@ def format_color(clr):
903918
else:
904919
node.set_parent(None)
905920

921+
# update the node view from model.
906922
node.update()
907923

908924
undo_cmd = NodeAddedCmd(self, node, node.model.pos)
@@ -1552,6 +1568,143 @@ def disable_nodes(self, nodes, mode=None):
15521568
return
15531569
nodes[0].set_disabled(mode)
15541570

1571+
# auto layout node functions.
1572+
1573+
@staticmethod
1574+
def _update_node_rank(node, nodes_rank, down_stream=True):
1575+
"""
1576+
Recursive function for updating the node ranking.
1577+
1578+
Args:
1579+
node (NodeGraphQt.BaseNode): node to start from.
1580+
nodes_rank (dict): node ranking object to be updated.
1581+
down_stream (bool): true to rank down stram.
1582+
"""
1583+
if down_stream:
1584+
node_values = node.connected_output_nodes().values()
1585+
else:
1586+
node_values = node.connected_input_nodes().values()
1587+
1588+
connected_nodes = set()
1589+
for nodes in node_values:
1590+
connected_nodes.update(nodes)
1591+
1592+
rank = nodes_rank[node] + 1
1593+
for n in connected_nodes:
1594+
if n in nodes_rank:
1595+
nodes_rank[n] = max(nodes_rank[n], rank)
1596+
else:
1597+
nodes_rank[n] = rank
1598+
NodeGraph._update_node_rank(n, nodes_rank, down_stream)
1599+
1600+
@staticmethod
1601+
def _compute_node_rank(nodes, down_stream=True):
1602+
"""
1603+
Compute the ranking of nodes.
1604+
1605+
Args:
1606+
nodes (list[NodeGraphQt.BaseNode]): nodes to start ranking from.
1607+
down_stream (bool): true to compute down stream.
1608+
1609+
Returns:
1610+
dict: {NodeGraphQt.BaseNode: node_rank, ...}
1611+
"""
1612+
nodes_rank = {}
1613+
for node in nodes:
1614+
nodes_rank[node] = 0
1615+
NodeGraph._update_node_rank(node, nodes_rank, down_stream)
1616+
return nodes_rank
1617+
1618+
def auto_layout_nodes(self, nodes=None, down_stream=True):
1619+
"""
1620+
Auto layout the nodes in the node graph.
1621+
1622+
Args:
1623+
nodes (list[NodeGraphQt.BaseNode]): list of nodes to auto layout
1624+
if nodes is None then all nodes is layed out.
1625+
down_stream (bool): false to layout up stream.
1626+
"""
1627+
self.begin_undo('Auto Layout Nodes')
1628+
1629+
nodes = nodes or self.all_nodes()
1630+
1631+
# filter out the backdrops.
1632+
backdrops = {
1633+
n: n.nodes() for n in nodes if isinstance(n, BackdropNode)
1634+
}
1635+
filtered_nodes = [n for n in nodes if not isinstance(n, BackdropNode)]
1636+
1637+
start_nodes = []
1638+
if down_stream:
1639+
start_nodes += [
1640+
n for n in filtered_nodes
1641+
if not any(n.connected_input_nodes().values())
1642+
]
1643+
else:
1644+
start_nodes += [
1645+
n for n in filtered_nodes
1646+
if not any(n.connected_output_nodes().values())
1647+
]
1648+
1649+
if not start_nodes:
1650+
return
1651+
1652+
node_views = [n.view for n in nodes]
1653+
nodes_center_0 = self.viewer().nodes_rect_center(node_views)
1654+
1655+
nodes_rank = NodeGraph._compute_node_rank(start_nodes, down_stream)
1656+
1657+
rank_map = {}
1658+
for node, rank in nodes_rank.items():
1659+
if rank in rank_map:
1660+
rank_map[rank].append(node)
1661+
else:
1662+
rank_map[rank] = [node]
1663+
1664+
if NODE_LAYOUT_DIRECTION is NODE_LAYOUT_HORIZONTAL:
1665+
current_x = 0
1666+
node_height = 120
1667+
for rank in sorted(range(len(rank_map)), reverse=not down_stream):
1668+
ranked_nodes = rank_map[rank]
1669+
max_width = max([node.view.width for node in ranked_nodes])
1670+
current_x += max_width
1671+
current_y = 0
1672+
for idx, node in enumerate(ranked_nodes):
1673+
dy = max(node_height, node.view.height)
1674+
current_y += 0 if idx == 0 else dy
1675+
node.set_pos(current_x, current_y)
1676+
current_y += dy * 0.5 + 10
1677+
1678+
current_x += max_width * 0.5 + 100
1679+
elif NODE_LAYOUT_DIRECTION is NODE_LAYOUT_VERTICAL:
1680+
current_y = 0
1681+
node_width = 250
1682+
for rank in sorted(range(len(rank_map)), reverse=not down_stream):
1683+
ranked_nodes = rank_map[rank]
1684+
max_height = max([node.view.height for node in ranked_nodes])
1685+
current_y += max_height
1686+
current_x = 0
1687+
for idx, node in enumerate(ranked_nodes):
1688+
dx = max(node_width, node.view.width)
1689+
current_x += 0 if idx == 0 else dx
1690+
node.set_pos(current_x, current_y)
1691+
current_x += dx * 0.5 + 10
1692+
1693+
current_y += max_height * 0.5 + 100
1694+
1695+
nodes_center_1 = self.viewer().nodes_rect_center(node_views)
1696+
dx = nodes_center_0[0] - nodes_center_1[0]
1697+
dy = nodes_center_0[1] - nodes_center_1[1]
1698+
[n.set_pos(n.x_pos() + dx, n.y_pos() + dy) for n in nodes]
1699+
1700+
# wrap the backdrop nodes.
1701+
for backdrop, contained_nodes in backdrops.items():
1702+
backdrop.wrap_nodes(contained_nodes)
1703+
1704+
self.end_undo()
1705+
1706+
# prompt dialog functions.
1707+
15551708
def question_dialog(self, text, title='Node Graph'):
15561709
"""
15571710
Prompts a question open dialog with ``"Yes"`` and ``"No"`` buttons in
@@ -1618,6 +1771,8 @@ def save_dialog(self, current_dir=None, ext=None):
16181771
"""
16191772
return self._viewer.save_dialog(current_dir, ext)
16201773

1774+
# hmmm... refactor functions below for "GroupNode" not "SubGraph".
1775+
16211776
def use_OpenGL(self):
16221777
"""
16231778
Use OpenGL to draw the graph.

NodeGraphQt/base/model.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,7 @@ def set_property(self, name, value):
160160
elif name in self._custom_prop.keys():
161161
self._custom_prop[name] = value
162162
else:
163-
self.add_property(name, value)
164-
# raise NodePropertyError('No property "{}"'.format(name))
163+
raise NodePropertyError('No property "{}"'.format(name))
165164

166165
def get_property(self, name):
167166
if name in self.properties.keys():

NodeGraphQt/base/node.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1257,7 +1257,7 @@ def wrap_nodes(self, nodes):
12571257
if not nodes:
12581258
return
12591259
self.graph.begin_undo('"{}" wrap nodes'.format(self.name()))
1260-
size = self.view.calc_backdrop_size()
1260+
size = self.view.calc_backdrop_size([n.view for n in nodes])
12611261
self.set_property('width', size['width'])
12621262
self.set_property('height', size['height'])
12631263
self.set_pos(*size['pos'])

0 commit comments

Comments
 (0)