Live patch: handle deletion of node links

This commit is contained in:
Moritz Brückner 2021-07-24 13:36:16 +02:00
parent 256d27e289
commit aa21402221
8 changed files with 312 additions and 80 deletions

View file

@ -4,8 +4,8 @@ package armory.logicnode;
class LogicNode {
var tree: LogicTree;
var inputs: Array<LogicNodeInput> = [];
var outputs: Array<Array<LogicNode>> = [];
var inputs: Array<LogicNodeLink> = [];
var outputs: Array<Array<LogicNodeLink>> = [];
#if (arm_debug || arm_patch)
public var name = "";
@ -22,14 +22,115 @@ class LogicNode {
this.tree = tree;
}
public function addInput(node: LogicNode, from: Int) {
inputs.push(new LogicNodeInput(node, from));
/**
Resize the inputs array to a given size to minimize dynamic
reallocation and over-allocation later.
**/
inline function preallocInputs(amount: Int) {
this.inputs.resize(amount);
}
public function addOutputs(nodes: Array<LogicNode>) {
outputs.push(nodes);
/**
Resize the outputs array to a given size to minimize dynamic
reallocation and over-allocation later.
**/
inline function preallocOutputs(amount: Int) {
this.outputs.resize(amount);
for (i in 0...outputs.length) {
outputs[i] = [];
}
}
/**
Add a link between to nodes to the tree.
**/
public static function addLink(fromNode: LogicNode, toNode: LogicNode, fromIndex: Int, toIndex: Int): LogicNodeLink {
var link = new LogicNodeLink(fromNode, toNode, fromIndex, toIndex);
if (toNode.inputs.length <= toIndex) {
toNode.inputs.resize(toIndex + 1);
}
toNode.inputs[toIndex] = link;
var fromNodeOuts = fromNode.outputs;
var outLen = fromNodeOuts.length;
if (outLen <= fromIndex) {
fromNodeOuts.resize(fromIndex + 1);
// Initialize with empty arrays
for (i in 0...(fromIndex - (outLen - 1))) {
fromNodeOuts[i] = [];
}
}
fromNodeOuts[fromIndex].push(link);
return link;
}
#if arm_patch
/**
Removes a link from the tree.
**/
static function removeLink(link: LogicNodeLink) {
link.fromNode.outputs[link.fromIndex].remove(link);
// Reuse the same link and connect a default input node to it.
// That's why this function is only available in arm_patch mode, we need
// access to the link's type and value.
link.fromNode = LogicNode.createSocketDefaultNode(link.toNode.tree, link.toType, link.toValue);
link.fromIndex = 0;
}
/**
Removes all inputs and their links from this node.
Warning: this function changes the amount of node inputs to 0!
**/
function clearInputs() {
for (link in inputs) {
link.fromNode.outputs[link.fromIndex].remove(link);
}
inputs.resize(0);
}
/**
Removes all outputs and their links from this node.
Warning: this function changes the amount of node inputs to 0!
**/
function clearOutputs() {
for (links in outputs) {
for (link in links) {
var defaultNode = LogicNode.createSocketDefaultNode(tree, link.toType, link.toValue);
link.fromNode = defaultNode;
link.fromIndex = 0;
defaultNode.outputs[0] = [link];
}
}
outputs.resize(0);
}
/**
Creates a default node for a socket so that get() and set() can be
used without null checks.
Loosely equivalent to `make_logic.build_default_node()` in Python.
**/
static inline function createSocketDefaultNode(tree: LogicTree, socketType: String, value: Dynamic): LogicNode {
// Make sure to not add these nodes to the LogicTree.nodes array as they
// won't be garbage collected then if unlinked later.
return switch (socketType) {
case "VECTOR": new armory.logicnode.VectorNode(tree, value[0], value[1], value[2]);
case "RGBA": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2], value[3]);
case "RGB": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2]);
case "VALUE": new armory.logicnode.FloatNode(tree, value);
case "INT": new armory.logicnode.IntegerNode(tree, value);
case "BOOLEAN": new armory.logicnode.BooleanNode(tree, value);
case "STRING": new armory.logicnode.StringNode(tree, value);
case "NONE": new armory.logicnode.NullNode(tree);
case "OBJECT": new armory.logicnode.ObjectNode(tree, value);
default: new armory.logicnode.DynamicNode(tree, value);
}
}
#end
/**
Called when this node is activated.
@param from impulse index
@ -42,42 +143,45 @@ class LogicNode {
**/
function runOutput(i: Int) {
if (i >= outputs.length) return;
for (o in outputs[i]) {
// Check which input activated the node
for (j in 0...o.inputs.length) {
if (o.inputs[j].node == this) {
o.run(j);
break;
}
}
for (outLink in outputs[i]) {
outLink.toNode.run(outLink.toIndex);
}
}
@:allow(armory.logicnode.LogicNodeInput)
@:allow(armory.logicnode.LogicNodeLink)
function get(from: Int): Dynamic { return this; }
@:allow(armory.logicnode.LogicNodeInput)
@:allow(armory.logicnode.LogicNodeLink)
function set(value: Dynamic) {}
}
class LogicNodeInput {
@:allow(armory.logicnode.LogicNode)
@:allow(armory.logicnode.LogicTree)
class LogicNodeLink {
@:allow(armory.logicnode.LogicNode)
var node: LogicNode;
var from: Int; // Socket index
var fromNode: LogicNode;
var toNode: LogicNode;
var fromIndex: Int;
var toIndex: Int;
public function new(node: LogicNode, from: Int) {
this.node = node;
this.from = from;
#if arm_patch
var fromType: String;
var toType: String;
var toValue: Dynamic;
#end
inline function new(fromNode: LogicNode, toNode: LogicNode, fromIndex: Int, toIndex: Int) {
this.fromNode = fromNode;
this.toNode = toNode;
this.fromIndex = fromIndex;
this.toIndex = toIndex;
}
@:allow(armory.logicnode.LogicNode)
function get(): Dynamic {
return node.get(from);
inline function get(): Dynamic {
return fromNode.get(fromIndex);
}
@:allow(armory.logicnode.LogicNode)
function set(value: Dynamic) {
node.set(value);
inline function set(value: Dynamic) {
fromNode.set(value);
}
}

View file

@ -11,10 +11,10 @@ class QuaternionNode extends LogicNode {
super(tree);
if (x != null) {
addInput(new FloatNode(tree, x), 0);
addInput(new FloatNode(tree, y), 0);
addInput(new FloatNode(tree, z), 0);
addInput(new FloatNode(tree, w), 0);
LogicNode.addLink(new FloatNode(tree, x), this, 0, 0);
LogicNode.addLink(new FloatNode(tree, y), this, 0, 1);
LogicNode.addLink(new FloatNode(tree, z), this, 0, 2);
LogicNode.addLink(new FloatNode(tree, w), this, 0, 3);
}
}
@ -27,15 +27,15 @@ class QuaternionNode extends LogicNode {
switch (from){
case 0:
return value;
case 1:
var value1 = new Vec4();
case 1:
var value1 = new Vec4();
value1.x = value.x;
value1.y = value.y;
value1.z = value.z;
value1.w = 0; // use 0 to avoid this vector being translated.
return value1;
case 2:
return value.w;
return value.w;
default:
return null;
}

View file

@ -14,8 +14,8 @@ class SetParentNode extends LogicNode {
var parent: Object;
var isUnparent = false;
if (Std.is(inputs[2].node, ObjectNode)) {
var parentNode = cast(inputs[2].node, ObjectNode);
if (Std.is(inputs[2].fromNode, ObjectNode)) {
var parentNode = cast(inputs[2].fromNode, ObjectNode);
isUnparent = parentNode.objectName == "";
}
if (isUnparent) parent = iron.Scene.active.root;
@ -24,7 +24,7 @@ class SetParentNode extends LogicNode {
if (object == null || parent == null || object.parent == parent) return;
object.parent.removeChild(object, isUnparent); // keepTransform
#if arm_physics
var rigidBody = object.getTrait(RigidBody);
if (rigidBody != null) rigidBody.setActivationState(0);

View file

@ -10,9 +10,9 @@ class VectorNode extends LogicNode {
super(tree);
if (x != null) {
addInput(new FloatNode(tree, x), 0);
addInput(new FloatNode(tree, y), 0);
addInput(new FloatNode(tree, z), 0);
LogicNode.addLink(new FloatNode(tree, x), this, 0, 0);
LogicNode.addLink(new FloatNode(tree, y), this, 0, 1);
LogicNode.addLink(new FloatNode(tree, z), this, 0, 2);
}
}

View file

@ -6,7 +6,7 @@ import armory.logicnode.LogicTree;
#if arm_patch @:expose("LivePatch") #end
@:access(armory.logicnode.LogicNode)
@:access(armory.logicnode.LogicNodeInput)
@:access(armory.logicnode.LogicNodeLink)
class LivePatch extends iron.Trait {
#if !arm_patch
@ -40,11 +40,54 @@ class LivePatch extends iron.Trait {
var toNode = tree.nodes[toNodeName];
if (fromNode == null || toNode == null) return;
// Don't add a connection twice
if (!fromNode.outputs[fromIndex].contains(toNode)) {
fromNode.outputs[fromIndex].push(toNode);
LogicNode.addLink(fromNode, toNode, fromIndex, toIndex);
}
public static function patchSetNodeLinks(treeName: String, nodeName: String, inputDatas: Array<Dynamic>, outputDatas: Array<Array<Dynamic>>) {
var tree = LogicTree.nodeTrees[treeName];
if (tree == null) return;
var node = tree.nodes[nodeName];
if (node == null) return;
node.clearInputs();
node.clearOutputs();
for (inputData in inputDatas) {
var fromNode: LogicNode;
var fromIndex: Int;
if (inputData.isLinked) {
fromNode = tree.nodes[inputData.fromNode];
if (fromNode == null) continue;
fromIndex = inputData.fromIndex;
}
else {
fromNode = LogicNode.createSocketDefaultNode(node.tree, inputData.socketType, inputData.socketValue);
fromIndex = 0;
}
LogicNode.addLink(fromNode, node, fromIndex, inputData.toIndex);
}
for (outputData in outputDatas) {
for (linkData in outputData) {
var toNode: LogicNode;
var toIndex: Int;
if (linkData.isLinked) {
toNode = tree.nodes[linkData.toNode];
if (toNode == null) continue;
toIndex = linkData.toIndex;
}
else {
toNode = LogicNode.createSocketDefaultNode(node.tree, linkData.socketType, linkData.socketValue);
toIndex = 0;
}
LogicNode.addLink(node, toNode, linkData.fromIndex, toIndex);
}
}
toNode.inputs[toIndex] = new LogicNodeInput(fromNode, fromIndex);
}
public static function patchUpdateNodeProp(treeName: String, nodeName: String, propName: String, value: Dynamic) {
@ -123,11 +166,12 @@ class LivePatch extends iron.Trait {
var i = 0;
for (inputData in inputDatas) {
newNode.addInput(createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), i++);
LogicNode.addLink(LogicNode.createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), newNode, 0, i++);
}
i = 0;
for (outputData in outputDatas) {
newNode.addOutputs([createSocketDefaultNode(newNode.tree, outputData[0], outputData[1])]);
LogicNode.addLink(newNode, LogicNode.createSocketDefaultNode(newNode.tree, outputData[0], outputData[1]), i++, 0);
}
}
@ -151,27 +195,14 @@ class LivePatch extends iron.Trait {
var i = 0;
for (inputData in inputDatas) {
newNode.addInput(createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), i++);
LogicNode.addLink(LogicNode.createSocketDefaultNode(newNode.tree, inputData[0], inputData[1]), newNode, 0, i++);
}
i = 0;
for (outputData in outputDatas) {
newNode.addOutputs([createSocketDefaultNode(newNode.tree, outputData[0], outputData[1])]);
LogicNode.addLink(newNode, LogicNode.createSocketDefaultNode(newNode.tree, outputData[0], outputData[1]), i++, 0);
}
}
static inline function createSocketDefaultNode(tree: LogicTree, socketType: String, value: Dynamic): LogicNode {
return switch (socketType) {
case "VECTOR": new armory.logicnode.VectorNode(tree, value[0], value[1], value[2]);
case "RGBA": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2], value[3]);
case "RGB": new armory.logicnode.ColorNode(tree, value[0], value[1], value[2]);
case "VALUE": new armory.logicnode.FloatNode(tree, value);
case "INT": new armory.logicnode.IntegerNode(tree, value);
case "BOOLEAN": new armory.logicnode.BooleanNode(tree, value);
case "STRING": new armory.logicnode.StringNode(tree, value);
case "NONE": new armory.logicnode.NullNode(tree);
case "OBJECT": new armory.logicnode.ObjectNode(tree, value);
default: new armory.logicnode.DynamicNode(tree, value);
}
}
#end
}

View file

@ -269,6 +269,53 @@ def send_event(event_id: str, opt_data: Any = None):
js = f'LivePatch.patchNodeCopy("{tree_name}", "{node_name}", "{newnode_name}", {props_list}, {inp_data}, {out_data});'
write_patch(js)
elif event_id == 'ln_update_sockets':
node: ArmLogicTreeNode = opt_data
tree_name = arm.node_utils.get_export_tree_name(node.get_tree())
node_name = arm.node_utils.get_export_node_name(node)[1:]
inp_data = '['
for idx, inp in enumerate(node.inputs):
inp_data += '{'
# is_linked can be true even if there are no links if the
# user starts dragging a connection away before releasing
# the mouse
if inp.is_linked and len(inp.links) > 0:
inp_data += 'isLinked: true,'
inp_data += f'fromNode: "{arm.node_utils.get_export_node_name(inp.links[0].from_node)[1:]}",'
inp_data += f'fromIndex: {arm.node_utils.get_socket_index(inp.links[0].from_node.outputs, inp.links[0].from_socket)},'
else:
inp_data += 'isLinked: false,'
inp_data += f'socketType: "{inp.arm_socket_type}",'
inp_data += f'socketValue: {arm.node_utils.haxe_format_socket_val(inp.get_default_value())},'
inp_data += f'toIndex: {idx}'
inp_data += '},'
inp_data += ']'
out_data = '['
for idx, out in enumerate(node.outputs):
out_data += '['
for link in out.links:
out_data += '{'
if out.is_linked:
out_data += 'isLinked: true,'
out_data += f'toNode: "{arm.node_utils.get_export_node_name(link.to_node)[1:]}",'
out_data += f'toIndex: {arm.node_utils.get_socket_index(link.to_node.inputs, link.to_socket)},'
else:
out_data += 'isLinked: false,'
out_data += f'socketType: "{out.arm_socket_type}",'
out_data += f'socketValue: {arm.node_utils.haxe_format_socket_val(out.get_default_value())},'
out_data += f'fromIndex: {idx}'
out_data += '},'
out_data += '],'
out_data += ']'
js = f'LivePatch.patchSetNodeLinks("{tree_name}", "{node_name}", {inp_data}, {out_data});'
write_patch(js)
def on_operator(operator_id: str):
"""As long as bpy.msgbus doesn't listen to changes made by

View file

@ -24,6 +24,10 @@ category_items: ODict[str, List['ArmNodeCategory']] = OrderedDict()
array_nodes = dict()
# See ArmLogicTreeNode.update()
# format: [tree pointer => (num inputs, num input links, num outputs, num output links)]
last_node_state: dict[int, tuple[int, int, int, int]] = {}
class ArmLogicTreeNode(bpy.types.Node):
arm_category = PKG_AS_CATEGORY
@ -62,6 +66,40 @@ class ArmLogicTreeNode(bpy.types.Node):
def get_tree(self):
return self.id_data
def update(self):
"""Called if the node was updated in some way, for example
if socket connections change. This callback is not called if
socket values were changed.
"""
def num_connected(sockets):
return sum([socket.is_linked for socket in sockets])
# If a link between sockets is removed, there is currently no
# _reliable_ way in the Blender API to check which connection
# was removed (*).
#
# So instead we just check _if_ the number of links or sockets
# has changed (the update function is called before and after
# each link removal). Because we listen for those updates in
# general, we automatically also listen to link creation events,
# which is more stable than using the dedicated callback for
# that (`insert_link()`), because adding links can remove other
# links and we would need to react to that as well.
#
# (*) https://devtalk.blender.org/t/how-to-detect-which-link-was-deleted-by-user-in-node-editor
self_id = self.as_pointer()
current_state = (len(self.inputs), num_connected(self.inputs), len(self.outputs), num_connected(self.outputs))
if self_id not in last_node_state:
# Lazily initialize the last_node_state dict to also store
# state for nodes that already exist in the tree
last_node_state[self_id] = current_state
if last_node_state[self_id] != current_state:
arm.live_patch.send_event('ln_update_sockets', self)
last_node_state[self_id] = current_state
def free(self):
"""Called before the node is deleted."""
arm.live_patch.send_event('ln_delete', self)
@ -84,7 +122,8 @@ class ArmLogicTreeNode(bpy.types.Node):
def insert_link(self, link: bpy.types.NodeLink):
"""Called on *both* nodes when a link between two nodes is created."""
arm.live_patch.send_event('ln_insert_link', (self, link))
# arm.live_patch.send_event('ln_insert_link', (self, link))
pass
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
# needs to be overridden by individual node classes with arm_version>1

View file

@ -68,6 +68,7 @@ def build_node_tree(node_group: 'arm.nodes_logic.ArmLogicTree'):
with open(file, 'w', encoding="utf-8") as f:
f.write('package ' + pack_path + '.node;\n\n')
f.write('@:access(armory.logicnode.LogicNode)')
f.write('@:keep class ' + group_name + ' extends armory.logicnode.LogicTree {\n\n')
f.write('\tvar functionNodes:Map<String, armory.logicnode.FunctionNode>;\n\n')
f.write('\tvar functionOutputNodes:Map<String, armory.logicnode.FunctionOutputNode>;\n\n')
@ -162,8 +163,12 @@ def build_node(node: bpy.types.Node, f: TextIO) -> Optional[str]:
prop = arm.node_utils.haxe_format_prop_value(node, prop_name)
f.write('\t\t' + name + '.' + prop_name + ' = ' + prop + ';\n')
# Avoid unnecessary input/output array resizes
f.write(f'\t\t{name}.preallocInputs({len(node.inputs)});\n')
f.write(f'\t\t{name}.preallocOutputs({len(node.outputs)});\n')
# Create inputs
for inp in node.inputs:
for idx, inp in enumerate(node.inputs):
# True if the input is connected to a unlinked reroute
# somewhere down the reroute line
unconnected = False
@ -191,34 +196,40 @@ def build_node(node: bpy.types.Node, f: TextIO) -> Optional[str]:
for i in range(0, len(n.outputs)):
if n.outputs[i] == socket:
inp_from = i
from_type = socket.arm_socket_type
break
# Not linked -> create node with default values
else:
inp_name = build_default_node(inp)
inp_from = 0
from_type = inp.arm_socket_type
# The input is linked to a reroute, but the reroute is unlinked
if unconnected:
inp_name = build_default_node(inp)
inp_from = 0
from_type = inp.arm_socket_type
# Add input
f.write('\t\t' + name + '.addInput(' + inp_name + ', ' + str(inp_from) + ');\n')
f.write(f'\t\t{"var __link = " if wrd.arm_live_patch else ""}armory.logicnode.LogicNode.addLink({inp_name}, {name}, {inp_from}, {idx});\n')
if wrd.arm_live_patch:
to_type = inp.arm_socket_type
f.write(f'\t\t__link.fromType = "{from_type}";')
f.write(f'\t\t__link.toType = "{to_type}";')
f.write(f'\t\t__link.toValue = {arm.node_utils.haxe_format_socket_val(inp.get_default_value())};')
# Create outputs
for out in node.outputs:
if out.is_linked:
out_name = ''
for node in collect_nodes_from_output(out, f):
out_name += '[' if len(out_name) == 0 else ', '
out_name += node
out_name += ']'
# Not linked - create node with default values
else:
out_name = '[' + build_default_node(out) + ']'
# Add outputs
f.write('\t\t' + name + '.addOutputs(' + out_name + ');\n')
for idx, out in enumerate(node.outputs):
# Linked outputs are already handled after iterating over inputs
# above, so only unconnected outputs are handled here
if not out.is_linked:
f.write(f'\t\t{"var __link = " if wrd.arm_live_patch else ""}armory.logicnode.LogicNode.addLink({name}, {build_default_node(out)}, {idx}, 0);\n')
if wrd.arm_live_patch:
out_type = out.arm_socket_type
f.write(f'\t\t__link.fromType = "{out_type}";')
f.write(f'\t\t__link.toType = "{out_type}";')
f.write(f'\t\t__link.toValue = {arm.node_utils.haxe_format_socket_val(out.get_default_value())};')
return name