-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathgetLogicalUpstreamNodes.py
371 lines (293 loc) · 11.5 KB
/
getLogicalUpstreamNodes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
"""
version=2
author=Liam Collod
last_modified=11/03/2022
python=>2.7.1
Script for Foundry's Katana software.
Parse scene to return a list of contributing node connected to the
given source node.
[License]
Copyright 2022 Liam Collod
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
import NodegraphAPI
# error on Python2, for comments only anyway
try:
from typing import Tuple, Optional
except ImportError:
pass
__all__ = [
"SceneParser",
"ParseSettings"
]
class ParseSettings(dict):
"""
A regular dictionary object with a fixed structure. Structure is verified
through ``validate()`` method.
Used to configure the output result of the scene parsing.
[include_groups](bool)
If True, before visiting its content, include the group node in the output
[excluded.asGroupsNodeType](list of str):
list of node type that should not be considered as groups and children
are as such not processed.
[logical](bool):
True to process only logical connections between nodes.
(ex: Switch node only have 1 logical connection)
"""
__default = {
"include_groups": False,
"excluded": {
"asGroupsNodeType": []
},
"logical": True
}
def __init__(self, *args, **kwargs):
if not args and not kwargs:
super(ParseSettings, self).__init__(self.__default)
else:
super(ParseSettings, self).__init__(*args, **kwargs)
self.validate()
return
def __setitem__(self, *args, **kwargs):
super(ParseSettings, self).__setitem__(*args, **kwargs)
self.validate()
# Defined some properties to set/get values. Allow to use autocompletion.
@property
def exluded_asGroupsNodeType(self):
return self["excluded"]["asGroupsNodeType"]
@exluded_asGroupsNodeType.setter
def exluded_asGroupsNodeType(self, value):
self["excluded"]["asGroupsNodeType"] = value
@property
def logical(self):
return self["logical"]
@logical.setter
def logical(self, logical_value):
self["logical"] = logical_value
@property
def include_groups(self):
return self["include_groups"]
@include_groups.setter
def include_groups(self, include_groups_value):
self["include_groups"] = include_groups_value
def validate(self):
"""
Raises:
AssertionError: if self is not built properly.
"""
pre = "[{}] ".format(self.__class__.__name__)
assert self.get("excluded"),\
pre + "Missing key <excluded>"
assert isinstance(self["excluded"].get("asGroupsNodeType"), list),\
pre + "Missing key <excluded.asGroupsNodeType>"
assert isinstance(self.get("logical"), bool),\
pre + "Missing key <logical> or value is not <bool>."
assert isinstance(self.get("include_groups"), bool),\
pre + "Missing key <include_groups> or value is not <bool>."
return
class SceneParser(object):
"""
Attributes:
__buffer(list of NodegraphAPI.Node):
list of visited node in "pseudo-order" after a parsing operation.
Must be returned by the parsing function and reset after.
settings(ParseSettings):
Options for the scene parsing
"""
def __init__(self, source=None):
self.source = source
self.__buffer = list()
self.settings = ParseSettings()
return
def __get_upstream_nodes(self, source, grp_node=None):
"""
From a given node, find all upstream nodes connected.
Groups node themself are not included in the output but their children are
processed (unless contrary specified in settings).
Args:
grp_node (NodegraphAPI.GroupNode or None): GroupNode the source might belongs to.
source(NodegraphAPI.Node or NodegraphAPI.Port):
object to start the parsing from.
Returns:
None:
"""
# we always have at least source_node != None
if isinstance(source, NodegraphAPI.Port):
source_port = source
source_node = source.getNode()
elif isinstance(source, NodegraphAPI.Node):
source_port = None
source_node = source
else:
raise TypeError(
"Submited source argument <{}> is not supported."
"Must be Port or Node."
"".format(source)
)
# We are goint out of a group, its potential inputs have all already
# been processed.
if grp_node == source_node:
if self.settings.include_groups:
self.__buffer.append(source_node)
return
# When we got a groupNode we need to also parse what's inside (unless
# the node it is excluded). To do so we swap the passed inputPort/node
# of the group by the ones from the first children in the group.
# (i): We reach this only when going in a group.
if isinstance(
source_node,
NodegraphAPI.GroupNode
) and (
source_node.getType() not in self.settings.exluded_asGroupsNodeType
):
# if we passed a port we can just find what child node is connected
if source_port:
# at first we assume we a going inside the group = return port
__port = source_node.getReturnPort(source_port.getName())
if not __port:
raise RuntimeError(
"[__get_upstream_nodes][is grp] No Return port found"
" on node <{}> with source port <{}>."
" This should not happens ?!"
"".format(source_node, source_port)
)
source_port = __port.getConnectedPorts()[0]
# instead of continuing we parse directly the upstream node
# of the connected output node. Not returning would create an
# issue when 2 grp are connected and the top one doesnt have inputs.
else:
# if no port supplied we assume the group only have one output
source_port = source_node.getOutputPortByIndex(0)
# and if he doesn't even have an output port ...
if not source_port:
raise TypeError(
"The given source_obj[0] is a GroupNode with no output "
"port which is not currently supported."
)
source_port = source_node.getReturnPort(source_port.getName())
source_port = source_port.getConnectedPorts()[0]
# make sure that if the new node is a group, it is properly
# parsed too.
# the group is added first in the buffer
if self.settings.include_groups:
self.__buffer.append(source_node)
# now parse the node inside the group starting by the
# most downstream one we found
self.__get_upstream_nodes(source_port, grp_node=source_node)
if source_node.getInputPorts():
grp_node = source_node
# we continue the method, by finding connections on the group
pass
else:
# we have already visited it's content so stop here
return
else:
# as not a grp, add to the buffer (grp have already been added)
self.__buffer.append(source_node)
# We need to find a list of port connected to this node
connected_ports = node_get_connections(
node=source_node,
logical=self.settings.logical
)
# Node doesn't have any inputs so return.
if not connected_ports:
return
# now process all the input of this node
for connected_port in connected_ports:
# avoid processing multiples times the same node/port
if connected_port.getNode() in self.__buffer:
continue
self.__get_upstream_nodes(
source=connected_port,
grp_node=grp_node
)
continue
return
def __reset(self):
"""
Operations done after a parsing to reset the instance before the next
parsing.
"""
self.__buffer = list()
self.settings = ParseSettings()
return
def get_upstream_nodes(self, source=None):
"""
Make sure the settings attributes is set accordingly before calling.
Args:
source(NodegraphAPI.Node or NodegraphAPI.Port):
source nodegraph object from where to start the upstream parsing
Returns:
list of NodegraphAPI.Node:
"""
source = source or self.source
if not source:
raise ValueError(
"[get_upstream_nodes] Source argument is nul. Set the class "
"source attribute or pass a source argument to this method."
)
self.__get_upstream_nodes(source=source)
out = self.__buffer # save the buffer before reseting it
self.__reset()
return out
def node_get_connections(node, logical=True):
"""
From a given node return a set of the connected output ports .
If logical is set to True only port's nodes contributing to building
the scene as returned. For example, in the case of a VariableSwitch,
return only the connected port active.
Works for any node type even with only one input or no input.
Args:
logical(bool): True to return only logical connections.
node(NodegraphAPI.Node):
Returns:
set of NodeGraphAPI.Port: set of ports connected to the passed node
"""
output = set()
in_ports = node.getInputPorts()
for in_port in in_ports:
# we assume input port can only have one connection
connected_port = in_port.getConnectedPort(0)
if not connected_port:
continue
if logical:
# Having a GraphState means the node is evaluated.
if connected_port.getNode().getGraphState():
output.add(connected_port)
else:
output.add(connected_port)
return output
def __test():
"""
Example use case of the above functions.
"""
# we avoid visiting GT nodes content.
setting_dict = {
"include_groups": True,
"excluded": {
"asGroupsNodeType": ["GafferThree"]
},
"logical": True
}
excluded_ntype = ["Dot"]
sel = NodegraphAPI.GetAllSelectedNodes() # type: list
scene = SceneParser()
scene.settings = ParseSettings(setting_dict)
result = scene.get_upstream_nodes(sel[0])
# removed nodes of unwanted type
result = filter(lambda node: node.getType() not in excluded_ntype, result)
# convert nodes objects to string
result = map(lambda obj: obj.getName(), result)
# result.sort() # break the visited order ! but nicer for display
import pprint
print("_"*50)
pprint.pprint(result)
return