def __init__(self, parameter): self._tunneled_parameter = parameter super(TunnelParameter, self).__init__(node=parameter.node) self.tunneled_parameterChanged = Signal() self.sink_changed = Signal() self.source_changed = Signal()
def __init__(self, nodegraph, sourceParam, sinkParam, *args, **kwargs): # Infer scene if sourceParam is not None and sourceParam.node.parent is not None: kwargs['scene'] = nodegraph.getNodeUI(sourceParam.node).scene() elif sinkParam is not None and sinkParam.node.parent is not None: kwargs['scene'] = nodegraph.getNodeUI(sinkParam.node).scene() super(Connection, self).__init__(*args, **kwargs) # nodegraph/ramen stuff self.nodegraph = nodegraph self.source = sourceParam self.sink = sinkParam # signals self.updatedGeo = Signal() self.posChanged = Signal() # ui stuff self.setZValue(-1) pen = QtGui.QPen() pen.setWidth(0) pen.setColor(QtGui.QColor(0, 0, 0, 0)) self.setPen(pen) self._curvePath = QtGui.QPainterPath() self._stroker = QtGui.QPainterPathStroker() self._selectStroker = QtGui.QPainterPathStroker() self._path = QtGui.QPainterPath() self._selectPath = QtGui.QPainterPath() self.width = 3 self.registerRamenCallbacks() self.registerNodegraphCallbacks() self.updateGeo()
def __init__(self): self._parent = None self._children = set() self._accepts_children = True self.accepts_children_changed = Signal() self.parent_changed = Signal() self.child_added = Signal() self.child_removed = Signal() self.children_changed = Signal()
def __init__(self, nodegraph, ramenParameter, *args, **kwargs): kwargs['scene'] = nodegraph.getNodeUI(ramenParameter.node).scene() super(Parameter, self).__init__(*args, **kwargs) self._nodegraph = nodegraph self._ramenParameter = ramenParameter self._label = QtGui.QGraphicsTextItem(self) self._connectionsOut = {} # TODO consider wiring the node's updatedGeo to this self.updatedGeo = Signal() self.registerRamenCallbacks() self.syncConnections() self.updateGeo()
def __init__(self, nodegraph, ramenNode, *args, **kwargs): if ramenNode.parent is not None: kwargs['scene'] = nodegraph.getNodeUI(ramenNode.parent).scene super(Node, self).__init__(*args, **kwargs) self._nodegraph = nodegraph self._ramenNode = ramenNode self._parameterUIs = {} self._backdrop = QtGui.QGraphicsRectItem(self) self._label = QtGui.QGraphicsTextItem(self) self.updatedGeo = Signal() self.registerRamenCallbacks() self.syncParameters() self.updateGeo()
class Parameter(QtGui.QGraphicsItem): def __init__(self, nodegraph, ramenParameter, *args, **kwargs): kwargs['scene'] = nodegraph.getNodeUI(ramenParameter.node).scene() super(Parameter, self).__init__(*args, **kwargs) self._nodegraph = nodegraph self._ramenParameter = ramenParameter self._label = QtGui.QGraphicsTextItem(self) self._connectionsOut = {} # TODO consider wiring the node's updatedGeo to this self.updatedGeo = Signal() self.registerRamenCallbacks() self.syncConnections() self.updateGeo() @property def nodegraph(self): return self._nodegraph @property def ramenParameter(self): return self._ramenParameter def registerRamenCallbacks(self): self.ramenParameter.connection_added.connect( self._connectionAddedCallback) self.ramenParameter.connection_removed.connect( self._connectionRemovedCallback) self.ramenParameter.source_changed.connect(self.updateGeo) self.ramenParameter.sink_changed.connect(self.updateGeo) def deregisterRamenCallbacks(self): self.ramenParameter.connection_added.disconnect( self._connectionAddedCallback) self.ramenParameter.connection_removed.disconnect( self._connectionRemovedCallback) self.ramenParameter.source_changed.disconnect(self.updateGeo) self.ramenParameter.sink_changed.disconnect(self.updateGeo) def _connectionAddedCallback(self, source, sink): # Only create the connection if we're the source # (prevent double-creating the connection) if source == self.ramenParameter: return self._addConnectionOut(sink) def _connectionRemovedCallback(self, source, sink): if source == self.ramenParameter: return self._removeConnectionOut(sink) def updateGeo(self): nodeUI = self.nodegraph.getNodeUI(self.ramenParameter.node) boundingRect = QtCore.QRectF() self._label.setDefaultTextColor(QtGui.QColor(255, 255, 255, 150)) if self.ramenParameter.parent is None: self._label.setPlainText('') else: labelText = self.ramenParameter.label # XXX quick way to show sources/sinks; unicode for arrow if self.ramenParameter.sink: labelText = u'\u25b6 ' + labelText if self.ramenParameter.source: labelText = labelText + u' \u25b6' self._label.setPlainText(labelText) boundingRect |= self._label.boundingRect().translated( self._label.pos()) for childParameter in self.ramenParameter.children: childParameterUI = nodeUI.getParameterUI(childParameter) childParameterUI.setPos(0, boundingRect.height()) boundingRect |= childParameterUI.boundingRect().translated( childParameterUI.pos()) self.updatedGeo.emit() def boundingRect(self): return self.childrenBoundingRect() def paint(self, *args): pass def syncConnections(self): connectionsOut = set(self.ramenParameter.connections_out) knownConnectionsOut = set(self._connectionsOut.keys()) connectionsOutToRemove = knownConnectionsOut.difference(connectionsOut) connectionsOutToAdd = connectionsOut.difference(knownConnectionsOut) for sinkParam in connectionsOutToRemove: self._removeConnectionOut(sinkParam) for sinkParam in connectionsOutToAdd: self._addConnectionOut(sinkParam) def _addConnectionOut(self, sinkParam): self._connectionsOut[sinkParam] = Connection(self.nodegraph, self.ramenParameter, sinkParam) def _removeConnectionOut(self, sinkParam): connUI = self.getConnectionOutUI(sinkParam) if connUI is not None: connUI.delete() def getConnectionOutUI(self, sinkParam): return self._connectionsOut.get(sinkParam, None) def getConnectionInUI(self, sourceParam): # We actually need to go to the other UI item for this nodeUI = self.nodegraph.getNodeUI(sourceParam.node) paramUI = nodeUI.getParameterUI(sourceParam) connUI = paramUI.getConnectionOutUI(sourceParam) return connUI
def __init__(self): # ID -> node self._nodes = {} self._root_node_id = None self._selected_nodes = set() # Nodes self.node_added = Signal() self.node_removed = Signal() self.node_parent_changed = Signal() # Parameters self.parameter_added = Signal() self.parameter_removed = Signal() self.parameter_sink_changed = Signal() self.parameter_source_changed = Signal() # Connections self.connection_added = Signal() self.connection_removed = Signal() # node attributes self.node_id_changed = Signal() self.node_label_changed = Signal() self.node_attributes_changed = Signal() self.node_pos_changed = Signal() self.node_selected_changed = Signal() self.node_name_changed = Signal() self.node_added.connect(self._node_added_callback) self.node_removed.connect(self._node_removed_callback) self.node_selected_changed.connect( self._node_selected_changed_callback)
class Graph(object): '''The graph. Contains all our nodes.''' def __init__(self): # ID -> node self._nodes = {} self._root_node_id = None self._selected_nodes = set() # Nodes self.node_added = Signal() self.node_removed = Signal() self.node_parent_changed = Signal() # Parameters self.parameter_added = Signal() self.parameter_removed = Signal() self.parameter_sink_changed = Signal() self.parameter_source_changed = Signal() # Connections self.connection_added = Signal() self.connection_removed = Signal() # node attributes self.node_id_changed = Signal() self.node_label_changed = Signal() self.node_attributes_changed = Signal() self.node_pos_changed = Signal() self.node_selected_changed = Signal() self.node_name_changed = Signal() self.node_added.connect(self._node_added_callback) self.node_removed.connect(self._node_removed_callback) self.node_selected_changed.connect( self._node_selected_changed_callback) @property def nodes(self): return self._nodes.values() @nodes.setter def nodes(self, new_nodes): cur_nodes = set(self._nodes.values()) new_nodes = set(new_nodes) # Don't delete the root node! new_nodes.add(self.root_node) nodes_to_add = new_nodes.difference(cur_nodes) nodes_to_remove = cur_nodes.difference(new_nodes) for node in nodes_to_add: node.graph = self for node in nodes_to_remove: node.graph = None @nodes.deleter def nodes(self): self.clear() def create_node(self, *args, **kwargs): # convenience kwargs['graph'] = self return node.Node(*args, **kwargs) def clear(self): self.nodes = [] @property def selected_nodes(self): return set(self._selected_nodes) def __getitem__(self, node_id): return self._nodes.get(node_id, None) def __contains__(self, node_id): return node_id in self._nodes @property def selected_nodes(self): return set(self._selected_nodes) @selected_nodes.setter def selected_nodes(self, selected_nodes): selected_nodes = set(selected_nodes) nodes_to_select = selected_nodes.difference(self._selected_nodes) nodes_to_unselect = self._selected_nodes.difference(selected_nodes) for node in nodes_to_select: node.selected = True for node in nodes_to_unselect: node.selected = False def clear_selection(self): for node in self.get_selected_nodes(): node.set_selected(False) def _uniquefy_node_id(self, node_id): if node_id not in self: return node_id # The node_id can be either a string or int (or anything else you want, # but we need to be able to make a unique type) if type(node_id) == str: attempt = 0 unique_node_id = node_id while unique_node_id in self: attempt += 1 unique_node_id = node_id + '_' + str(attempt) return unique_node_id elif type(node_id) == int or type(node_id) == float: while node_id in self: node_id += 1 return node_id print('Warning! Unable to unquefy node_id type %s' % str(type(node_id))) return node_id @property def root_node(self): if self._root_node_id is None: self._root_node_id = self._uniquefy_node_id('root') self._root_node = node.SubgraphNode(node_id=self._root_node_id, graph=self) return self._root_node return self[self._root_node_id] def _node_added_callback(self, node): node_id = node.node_id if node_id in self: node_id = self._uniquefy_node_id(node_id) node.node_id = node_id if node.parent is None and node_id != self._root_node_id: node.parent = self.root_node self._nodes[node_id] = node def _node_removed_callback(self, node): if node.node_id not in self: return if node.node_id == self._root_node_id: print('Warning: deleting root node') self._root_node_id = None del self._nodes[node.node_id] def _node_selected_changed_callback(self, node): if not node.selected and node in self.selected_nodes: self._selected_nodes.remove(node) elif node.selected and node not in self.selected_nodes: self._selected_nodes.add(node)
class Connection(QtGui.QGraphicsPathItem): def __init__(self, nodegraph, sourceParam, sinkParam, *args, **kwargs): # Infer scene if sourceParam is not None and sourceParam.node.parent is not None: kwargs['scene'] = nodegraph.getNodeUI(sourceParam.node).scene() elif sinkParam is not None and sinkParam.node.parent is not None: kwargs['scene'] = nodegraph.getNodeUI(sinkParam.node).scene() super(Connection, self).__init__(*args, **kwargs) # nodegraph/ramen stuff self.nodegraph = nodegraph self.source = sourceParam self.sink = sinkParam # signals self.updatedGeo = Signal() self.posChanged = Signal() # ui stuff self.setZValue(-1) pen = QtGui.QPen() pen.setWidth(0) pen.setColor(QtGui.QColor(0, 0, 0, 0)) self.setPen(pen) self._curvePath = QtGui.QPainterPath() self._stroker = QtGui.QPainterPathStroker() self._selectStroker = QtGui.QPainterPathStroker() self._path = QtGui.QPainterPath() self._selectPath = QtGui.QPainterPath() self.width = 3 self.registerRamenCallbacks() self.registerNodegraphCallbacks() self.updateGeo() def registerRamenCallbacks(self): pass def registerNodegraphCallbacks(self): # TODO create faster lighterweight self.updatePos self.sourceNodeUI.updatedGeo.connect(self.updateGeo) self.sinkNodeUI.updatedGeo.connect(self.updateGeo) def deregisterRamenCallbacks(self): pass def deregisterNodegraphCallbacks(self): self.sourceNodeUI.updatedGeo.disconnect(self.updateGeo) self.sinkNodeUI.updatedGeo.disconnect(self.updateGeo) @property def sourceNodeUI(self): return self.nodegraph.getNodeUI(self.source.node) @property def sinkNodeUI(self): return self.nodegraph.getNodeUI(self.sink.node) @property def sourceUI(self): return self.sourceNodeUI.getParameterUI(self.source) @property def sinkUI(self): return self.sinkNodeUI.getParameterUI(self.sink) @property def sourcePos(self): return self.sourceUI.mapToScene( QtCore.QPointF(self.sourceNodeUI.boundingRect().width(), self.sourceUI.boundingRect().height() / 2)) @property def sinkPos(self): return self.sinkUI.mapToScene( QtCore.QPointF(0, self.sinkUI.boundingRect().height() / 2)) @property def sourceColor(self): return QtGui.QColor(255, 255, 255) @property def sinkColor(self): return QtGui.QColor(255, 255, 255) def updateGeo(self): self._stroker.setWidth(self.width) self._selectStroker.setWidth(self.width * 3) gradient = QtGui.QLinearGradient(self.sourcePos, self.sinkPos) gradient.setColorAt(0, self.sourceColor) gradient.setColorAt(1, self.sinkColor) brush = QtGui.QBrush(gradient) self.setBrush(brush) horizDistance = abs(self.sourcePos.x() - self.sinkPos.x()) ctrlPointDelta = QtCore.QPointF(horizDistance / 4.0, 0) self._curvePath = QtGui.QPainterPath(self.sourcePos) self._curvePath.cubicTo(self.sourcePos + ctrlPointDelta, self.sinkPos - ctrlPointDelta, self.sinkPos) path = self._stroker.createStroke(self._curvePath) self.setPath(path) self._selectPath = self._selectStroker.createStroke(self._curvePath) self.updatedGeo.emit() def shape(self): return self._selectPath def delete(self): self.deregisterRamenCallbacks() self.deregisterNodegraphCallbacks() return self.scene().removeItem(self)
class Parentable(object): '''An object with parent, child relationships''' def __init__(self): self._parent = None self._children = set() self._accepts_children = True self.accepts_children_changed = Signal() self.parent_changed = Signal() self.child_added = Signal() self.child_removed = Signal() self.children_changed = Signal() @property def parent(self): return self._parent @parent.setter def parent(self, parent): if parent == self._parent: return if parent is not None and not parent.accepts_children: return if self._parent is not None: self._parent.disown_child(self) self._parent = None self._parent = parent if parent is not None: parent.adopt_child(self) self.parent_changed.emit(parent=parent) @property def ancestors(self): res = [] curItem = self while curItem.parent is not None: res.append(curItem.parent) curItem = curItem.parent return res @ancestors.setter def ancestors(self, new_ancestors): # This is basically calling parent.setter on everything in the list new_ancestors = [None] + new_ancestors + [self] def pairwise(iterable): # TODO: stick this somewhere better a, b = itertools.tee(iterable) next(b, None) return zip(a, b) for parent, child in pairwise(new_ancestors): child.parent = parent @property def children(self): # explicitly copy for now # TODO: make this work with .add, and etc return set(self._children) @children.setter def children(self, new_children): children_to_adopt = new_children.difference(self._children) children_to_disown = self._children.difference(new_children) if self.accepts_children: for child in children_to_adopt: self.adopt_child(child) for child in children_to_disown: self.disown_child(child) @property def accepts_children(self): return self._accepts_children @accepts_children.setter def accepts_children(self, accepts_children): self._accepts_children = accepts_children self.accepts_children_changed.emit(accepts_children=accepts_children) def adopt_child(self, child): if not self.accepts_children: return if child in self._children: return self._children.add(child) child.parent = self self.child_added.emit(child=child) def disown_child(self, child): if child not in self._children: return self._children.remove(child) child.parent = None self.child_removed.emit(child=child)
class Node(QtGui.QGraphicsItem): def __init__(self, nodegraph, ramenNode, *args, **kwargs): if ramenNode.parent is not None: kwargs['scene'] = nodegraph.getNodeUI(ramenNode.parent).scene super(Node, self).__init__(*args, **kwargs) self._nodegraph = nodegraph self._ramenNode = ramenNode self._parameterUIs = {} self._backdrop = QtGui.QGraphicsRectItem(self) self._label = QtGui.QGraphicsTextItem(self) self.updatedGeo = Signal() self.registerRamenCallbacks() self.syncParameters() self.updateGeo() @property def nodegraph(self): return self._nodegraph @property def ramenNode(self): return self._ramenNode def registerRamenCallbacks(self): self._ramenNode.pos_changed.connect(self.updateGeo) self._ramenNode.selected_changed.connect(self.updateGeo) self._ramenNode.label_changed.connect(self.updateGeo) self._ramenNode.parameter_added.connect(self._addParameter) self._ramenNode.parameter_removed.connect(self._removeParameter) def deregisterRamenCallbacks(self): self._ramenNode.pos_changed.disconnect(self.updateGeo) self._ramenNode.selected_changed.disconnect(self.updateGeo) self._ramenNode.label_changed.disconnect(self.updateGeo) self._ramenNode.parameter_added.disconnect(self._addParameter) self._ramenNode.parameter_removed.disconnect(self._removeParameter) def _addParameter(self, parameter): for ancestor in parameter.ancestors: if ancestor not in self._parameterUIs: self._addParameter(ancestor) parentUI = self if parameter.parent is not None: parentUI = self._parameterUIs[parameter.parent] self._parameterUIs[parameter] = Parameter(self.nodegraph, parameter, parentUI) if parameter.parent is not None: self._parameterUIs[parameter.parent].updateGeo() def _removeParameter(self, parameter): self.scene().removeItem(self._parameterUIs[parameter]) del self._parameterUIs[parameter] if parameter.parent is not None: self._parameterUIs[parameter.parent].updateGeo() def getParameterUI(self, parameter): return self._parameterUIs[parameter] def syncParameters(self): ramenParameters = set(self.ramenNode.parameters) knownParameters = set(self._parameterUIs.keys()) parametersToRemove = knownParameters.difference(ramenParameters) parametersToAdd = ramenParameters.difference(knownParameters) for parameter in parametersToRemove: self._removeParameter(parameter) for parameter in parametersToAdd: self._addParameter(parameter) def updateGeo(self): # Temp colors for now -- style later self._label.setDefaultTextColor(QtGui.QColor(255, 255, 255, 200)) if self.ramenNode.selected: self._backdrop.setPen(QtGui.QColor(200, 200, 200, 200)) else: self._backdrop.setPen(QtGui.QColor(100, 100, 100, 200)) self._backdrop.setBrush(QtGui.QColor(30, 30, 30, 200)) backdropRect = QtCore.QRectF(0, 0, 100, 30) self.setPos(*self.ramenNode.pos) if self.ramenNode.label is not None: self._label.setPlainText(self.ramenNode.label) backdropRect |= self._label.boundingRect() if len(self.ramenNode.parameters) > 0: rootParamUI = self._parameterUIs[self.ramenNode.root_parameter] rootParamUIPos = self._label.boundingRect().height() rootParamUI.setPos(0, rootParamUIPos) backdropRect |= rootParamUI.boundingRect().translated( 0, rootParamUIPos) self._backdrop.setRect(backdropRect) self.updatedGeo.emit() def boundingRect(self): return self.childrenBoundingRect() def paint(self, *args): # TODO: figure out what this does pass @property def node(self): return self._ramenNode
def __init__(self, label=None, parameter_id=0, parent=None, index=0, node=None, source=False, sink=False): super(Parameter, self).__init__() if parent is not None: if parent.node is not node and node is not None: print("Warning: Specified node differs from parent's node. " "Using parent's node") node = parent.node # Properties self._parameter_id = parameter_id self._label = label self._inbex = index # Attached node self._node = node # Outward and inward connections self._connections_out = set() self._connections_in = set() # property signals self.parameter_id_changed = Signal() self.label_changed = Signal() self.index_changed = Signal() self.connection_added = Signal() self.connection_removed = Signal() self.sink_changed = Signal() self.source_changed = Signal() self.parent = parent # TODO: this needs to fill in the other sink/source value # self.connectionModeChanged = Signal() # self.sink_changed.connect(self.connectionModeChanged.emit) # self.source_changed.connect(self.connectionModeChanged.emit) self.source = source self.sink = sink self.register_callbacks() if self._node is not None: self._node.parameter_added.emit(parameter=self, param=self)
class Parameter(parentable.Parentable, connectable.Connectable): def __init__(self, label=None, parameter_id=0, parent=None, index=0, node=None, source=False, sink=False): super(Parameter, self).__init__() if parent is not None: if parent.node is not node and node is not None: print("Warning: Specified node differs from parent's node. " "Using parent's node") node = parent.node # Properties self._parameter_id = parameter_id self._label = label self._inbex = index # Attached node self._node = node # Outward and inward connections self._connections_out = set() self._connections_in = set() # property signals self.parameter_id_changed = Signal() self.label_changed = Signal() self.index_changed = Signal() self.connection_added = Signal() self.connection_removed = Signal() self.sink_changed = Signal() self.source_changed = Signal() self.parent = parent # TODO: this needs to fill in the other sink/source value # self.connectionModeChanged = Signal() # self.sink_changed.connect(self.connectionModeChanged.emit) # self.source_changed.connect(self.connectionModeChanged.emit) self.source = source self.sink = sink self.register_callbacks() if self._node is not None: self._node.parameter_added.emit(parameter=self, param=self) def delete(self): self.parent = None self.node = None def register_callbacks(self): if self._node is None: return self.connection_added.connect(self.node.connection_added.emit, parameter=self, param=self) self.connection_removed.connect(self.node.connection_removed.emit, parameter=self, param=self) self.sink_changed.connect(self.node.parameter_sink_changed.emit, parameter=self, param=self) self.source_changed.connect(self.node.parameter_source_changed.emit, parameter=self, param=self) def deregister_callbacks(self): if self._node is None: return self.connection_added.disconnect(self.node.connection_added.emit) self.connection_removed.disconnect(self.node.connection_removed.emit) self.sink_changed.disconnect(self.node.parameter_sink_changed.emit) self.source_changed.disconnect(self.node.parameter_source_changed.emit) def __repr__(self): '''fake convenience repr''' node_str = 'No node' if self.node is not None: node_str = 'Node: %s' % repr(self.node) return '<%s: %s/%s (%s)>' % (self.__class__.__name__, repr(self.parameter_id), self.label, node_str) @property def parameter_id(self): return self._parameter_id @parameter_id.setter def parameter_id(self, new_val): self._parameter_id = new_val self.parameter_id_changed.emit(parameter_id=self._parameter_id) @parameter_id.deleter def parameter_id(self): del self._parameter_id @property def label(self): return self._label @label.setter def label(self, new_val): self._label = new_val self.label_changed.emit(label=self._label) @label.deleter def label(self): del self._label @property def index(self): return self._index @index.setter def index(self, new_val): self._index = new_val self.index_changed.emit(index=self._index) @index.deleter def index(self): del self._index @property def node(self): return self._node @node.setter def node(self, new_node): if self._parent is not None: self.parent = None if self._node is not None: self.deregister_callbacks() self.parameter_removed.emit(parameter=self, param=self) self._node = new_node self.register_callbacks() self.parameter_added.emit(parameter=self, param=self) def is_transitively_connected(self, param): # TODO: two-sided BFS a la path tracing return param in self.connections @property def connection_subgraph(self): return self.node.parent def loft(self): # In ambiguous cases, loft the sink if self.sink: return self.loft_sink() if self.source: return self.loft_source() def loft_sink(self): if not self.sink: return None if self.node.parent is None: return None for tunnel_param in self.node.parent.tunnel_parameters: if self in tunnel_param.connections: return self.node.parent.get_parameter_for_tunnel(tunnel_param) lofted_param = Parameter(node=self.node.parent) lofted_param.sink = True self.node.parent.get_tunnel_parameter(lofted_param).connect(self) return lofted_param def loft_source(self): if not self.source: return None if self.node.parent is None: return None for tunnel_param in self.node.parent.tunnel_parameters: if self in tunnel_param.connections: return self.node.parent.get_parameter_for_tunnel(tunnel_param) lofted_param = Parameter(node=self.node.parent) lofted_param.source = True self.node.parent.get_tunnel_parameter(lofted_param).connect(self) return lofted_param def connect(self, param): # Easy case, connection in the same subgraph if self.connection_subgraph == param.connection_subgraph: return super(Parameter, self).connect(param) # Hard case, different parents # Loft up the one with more ancestors. # (in the case of equal length but different ancestors just choose # one and the other will be lofted in recursion) # Determine directionality # In ambiguous cases, prefer that we are the source self_is_source = True if self.source and param.sink: self_is_source = True elif self.sink and param.source: self_is_source = False else: print('unable to connect') deep_param = self surface_param = param deep_is_source = self_is_source if len(self.node.ancestors) < len(param.node.ancestors): deep_param = param surface_param = self deep_is_source = not self_is_source if deep_is_source: lofted_param = deep_param.loft_source() else: lofted_param = deep_param.loft_sink() if lofted_param is not None: return lofted_param.connect(surface_param)