Merge branch 'node-versioning' into logic-nodes
This commit is contained in:
commit
cd730249af
|
@ -1,6 +1,6 @@
|
|||
import itertools
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Generator, List, Optional, Type
|
||||
from typing import Any, Generator, List, Optional, Type, Dict
|
||||
from typing import OrderedDict as ODict # Prevent naming conflicts
|
||||
|
||||
import bpy.types
|
||||
|
@ -19,10 +19,41 @@ array_nodes = dict()
|
|||
|
||||
|
||||
class ArmLogicTreeNode(bpy.types.Node):
|
||||
def init(self, context):
|
||||
# make sure a given node knows the version of the NodeClass from when it was created
|
||||
if isinstance(type(self).arm_version, int):
|
||||
self.arm_version = type(self).arm_version
|
||||
else:
|
||||
self.arm_version = 1
|
||||
|
||||
@classmethod
|
||||
def poll(cls, ntree):
|
||||
return ntree.bl_idname == 'ArmLogicTreeType'
|
||||
|
||||
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
|
||||
# needs to be overridden by individual node classes with arm_version>1
|
||||
"""(only called if the node's version is inferior to the node class's version)
|
||||
Help with the node replacement process, by explaining how a node (`self`) should be replaced.
|
||||
This method can either return a NodeReplacement object (see `nodes_logic.py`), or a brand new node.
|
||||
|
||||
If a new node is returned, then the following needs to be already set:
|
||||
- the node's links to the other nodes
|
||||
- the node's properties
|
||||
- the node inputs's default values
|
||||
|
||||
If more than one node need to be created (for example, if an input needs a type conversion after update),
|
||||
please return all the nodes in a list.
|
||||
|
||||
please raise a LookupError specifically when the node's version isn't handled by the function.
|
||||
|
||||
note that the lowest 'defined' version should be 1. if the node's version is 0, it means that it has been saved before versioning was a thing.
|
||||
NODES OF VERSION 1 AND VERSION 0 SHOULD HAVE THE SAME CONTENTS
|
||||
"""
|
||||
if self.arm_version==0 and type(self).arm_version == 1:
|
||||
return NodeReplacement.Identity(self) # in case someone doesn't implement this function, but the node has version 0.
|
||||
else:
|
||||
raise LookupError(f"the current node class, {repr(type(self)):s}, does not implement the getReplacementNode method, even though it has updated")
|
||||
|
||||
def add_input(self, socket_type: str, socket_name: str, default_value: Any = None, is_var: bool = False) -> bpy.types.NodeSocket:
|
||||
"""Adds a new input socket to the node.
|
||||
|
||||
|
@ -58,6 +89,82 @@ class ArmLogicTreeNode(bpy.types.Node):
|
|||
return socket
|
||||
|
||||
|
||||
class NodeReplacement:
|
||||
"""
|
||||
Represents a simple replacement rule, this can replace nodes of one type to nodes of a second type.
|
||||
However, it is fairly limited. For instance, it assumes there are no changes in the type of the inputs or outputs
|
||||
Second, it also assumes that node properties (especially EnumProperties) keep the same possible values.
|
||||
|
||||
- from_node, from_node_version: the type of node to be removed, and its version number
|
||||
- to_node, to_node_version: the type of node which takes from_node's place, and its version number
|
||||
- *_socket_mapping: a map which defines how the sockets of the old node shall be connected to the new node
|
||||
{1: 2} means that anything connected to the socket with index 1 on the original node will be connected to the socket with index 2 on the new node
|
||||
- property_mapping: the mapping used to transfer the values of the old node's properties to the new node's properties.
|
||||
{"property0": "property1"} mean that the value of the new node's property1 should be the old node's property0's value.
|
||||
- input_defaults: a mapping used to give default values to the inputs which aren't overridden otherwise.
|
||||
- property_defaults: a mapping used to define the value of the new node's properties, when they aren't overridden otherwise.
|
||||
"""
|
||||
|
||||
def __init__(self, from_node: str, from_node_version: int, to_node: str, to_node_version: int,
|
||||
in_socket_mapping: Dict[int, int], out_socket_mapping: Dict[int, int], property_mapping: Dict[str, str] = {},
|
||||
input_defaults: Dict[int, any] = {}, property_defaults: Dict[str, any]={}):
|
||||
self.from_node = from_node
|
||||
self.to_node = to_node
|
||||
self.from_node_version = from_node_version
|
||||
self.to_node_version = to_node_version
|
||||
|
||||
self.in_socket_mapping = in_socket_mapping
|
||||
self.out_socket_mapping = out_socket_mapping
|
||||
self.property_mapping = property_mapping
|
||||
|
||||
self.input_defaults = input_defaults
|
||||
self.property_defaults = property_defaults
|
||||
|
||||
@classmethod
|
||||
def Identity(cls, node: ArmLogicTreeNode):
|
||||
"""returns a NodeReplacement that does nothing, while operating on a given node.
|
||||
WARNING: it assumes that all node properties are called "property0", "property1", etc...
|
||||
"""
|
||||
in_socks = {i:i for i in range(len(node.inputs))}
|
||||
out_socks = {i:i for i in range(len(node.outputs))}
|
||||
props = {}
|
||||
i=0
|
||||
while hasattr(node, f'property{i:d}'):
|
||||
props[f'property{i:d}'] = f'property{i:d}'
|
||||
i +=1
|
||||
return NodeReplacement(
|
||||
node.bl_idname, node.arm_version, node.bl_idname, type(node).arm_version,
|
||||
in_socket_mapping=in_socks, out_socket_mapping=out_socks,
|
||||
property_mapping=props
|
||||
)
|
||||
|
||||
def chain_with(self, other):
|
||||
"""modify the current NodeReplacement by "adding" a second replacement after it"""
|
||||
if self.to_node != other.from_node or self.to_node_version != other.from_node_version:
|
||||
raise TypeError('the given NodeReplacement-s could not be chained')
|
||||
self.to_node = other.to_node
|
||||
self.to_node_version = other.to_node_version
|
||||
|
||||
for i1, i2 in self.in_socket_mapping.items():
|
||||
i3 = other.in_socket_mapping[i2]
|
||||
self.in_socket_mapping[i1] = i3
|
||||
for i1, i2 in self.out_socket_mapping.items():
|
||||
i3 = other.out_socket_mapping[i2]
|
||||
self.out_socket_mapping[i1] = i3
|
||||
for p1, p2 in self.property_mapping.items():
|
||||
p3 = other.property_mapping[p2]
|
||||
self.property_mapping[p1] = p3
|
||||
|
||||
old_input_defaults = self.input_defaults
|
||||
self.input_defaults = other.input_defaults.copy()
|
||||
for i, x in old_input_defaults.items():
|
||||
self.input_defaults[ other.in_socket_mapping[i] ] = x
|
||||
|
||||
old_property_defaults = self.property_defaults
|
||||
self.property_defaults = other.property_defaults.copy()
|
||||
for p, x in old_property_defaults.items():
|
||||
self.property_defaults[ other.property_mapping[p] ] = x
|
||||
|
||||
class ArmNodeAddInputButton(bpy.types.Operator):
|
||||
"""Add new input"""
|
||||
bl_idname = 'arm.node_add_input'
|
||||
|
@ -317,7 +424,7 @@ def add_category(category: str, section: str = 'default', icon: str = 'BLANK1',
|
|||
return None
|
||||
|
||||
|
||||
def add_node(node_type: Type[bpy.types.Node], category: str, section: str = 'default') -> None:
|
||||
def add_node(node_type: Type[bpy.types.Node], category: str, section: str = 'default', is_obselete: bool = False) -> None:
|
||||
"""
|
||||
Registers a node to the given category. If no section is given, the
|
||||
node is put into the default section that does always exist.
|
||||
|
@ -330,6 +437,11 @@ def add_node(node_type: Type[bpy.types.Node], category: str, section: str = 'def
|
|||
nodes.append(node_type)
|
||||
node_category = get_category(category)
|
||||
|
||||
if is_obselete:
|
||||
# We need the obselete nodes to be registered in order to have them replaced,
|
||||
# but do not add them to the menu.
|
||||
return
|
||||
|
||||
if node_category is None:
|
||||
node_category = add_category(category)
|
||||
|
||||
|
|
|
@ -5,78 +5,102 @@ from bpy.types import NodeSocket
|
|||
import arm.utils
|
||||
|
||||
|
||||
class ArmActionSocket(NodeSocket):
|
||||
bl_idname = 'ArmNodeSocketAction'
|
||||
bl_label = 'Action Socket'
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
layout.label(text=self.name)
|
||||
|
||||
def draw_color(self, context, node):
|
||||
return 0.8, 0.3, 0.3, 1
|
||||
|
||||
|
||||
class ArmAnimActionSocket(NodeSocket):
|
||||
bl_idname = 'ArmNodeSocketAnimAction'
|
||||
bl_label = 'Action Socket'
|
||||
default_value_get: PointerProperty(name='Action', type=bpy.types.Action)
|
||||
|
||||
def get_default_value(self):
|
||||
if self.default_value_get is None:
|
||||
return ''
|
||||
if self.default_value_get.name not in bpy.data.actions:
|
||||
return self.default_value_get.name
|
||||
name = arm.utils.asset_name(bpy.data.actions[self.default_value_get.name])
|
||||
return arm.utils.safestr(name)
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
if self.is_output:
|
||||
layout.label(text=self.name)
|
||||
elif self.is_linked:
|
||||
layout.label(text=self.name)
|
||||
else:
|
||||
layout.prop_search(self, 'default_value_get', bpy.data, 'actions', icon='NONE', text='')
|
||||
|
||||
def draw_color(self, context, node):
|
||||
return 0.8, 0.8, 0.8, 1
|
||||
|
||||
|
||||
class ArmArraySocket(NodeSocket):
|
||||
bl_idname = 'ArmNodeSocketArray'
|
||||
bl_label = 'Array Socket'
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
layout.label(text=self.name)
|
||||
|
||||
def draw_color(self, context, node):
|
||||
return 0.8, 0.4, 0.0, 1
|
||||
|
||||
|
||||
class ArmCustomSocket(NodeSocket):
|
||||
"""
|
||||
A custom socket that can be used to define more socket types for
|
||||
logic node packs. Do not use this type directly (it is not
|
||||
registered)!
|
||||
"""
|
||||
|
||||
bl_idname = 'ArmCustomSocket'
|
||||
bl_label = 'Custom Socket'
|
||||
# note: trying to use the `type` property will fail. All custom nodes will have "VALUE" as a type, because it is the default.
|
||||
arm_socket_type = 'NONE'
|
||||
# please also declare a property named "default_value_raw" of arm_socket_type isn't "NONE"
|
||||
|
||||
def get_default_value(self):
|
||||
"""Override this for values of unconnected input sockets."""
|
||||
return None
|
||||
|
||||
|
||||
class ArmObjectSocket(NodeSocket):
|
||||
bl_idname = 'ArmNodeSocketObject'
|
||||
bl_label = 'Object Socket'
|
||||
default_value_get: PointerProperty(name='Object', type=bpy.types.Object)
|
||||
class ArmActionSocket(ArmCustomSocket):
|
||||
bl_idname = 'ArmNodeSocketAction'
|
||||
bl_label = 'Action Socket'
|
||||
arm_socket_type = 'NONE'
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
layout.label(text=self.name)
|
||||
|
||||
def draw_color(self, context, node):
|
||||
return 0.8, 0.3, 0.3, 1
|
||||
|
||||
|
||||
class ArmAnimActionSocket(ArmCustomSocket):
|
||||
bl_idname = 'ArmNodeSocketAnimAction'
|
||||
bl_label = 'Action Socket'
|
||||
arm_socket_type = 'STRING'
|
||||
|
||||
default_value_get: PointerProperty(name='Action', type=bpy.types.Action) # legacy version of the line after this one
|
||||
default_value_raw: PointerProperty(name='Action', type=bpy.types.Action)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
if self.default_value_get is not None:
|
||||
self.default_value_raw = self.default_value_get
|
||||
self.default_value_get = None
|
||||
|
||||
def get_default_value(self):
|
||||
if self.default_value_get is None:
|
||||
if self.default_value_raw is None:
|
||||
return ''
|
||||
if self.default_value_get.name not in bpy.data.objects:
|
||||
return self.default_value_get.name
|
||||
return arm.utils.asset_name(bpy.data.objects[self.default_value_get.name])
|
||||
if self.default_value_raw.name not in bpy.data.actions:
|
||||
return self.default_value_raw.name
|
||||
name = arm.utils.asset_name(bpy.data.actions[self.default_value_raw.name])
|
||||
return arm.utils.safestr(name)
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
if self.is_output:
|
||||
layout.label(text=self.name)
|
||||
elif self.is_linked:
|
||||
layout.label(text=self.name)
|
||||
else:
|
||||
layout.prop_search(self, 'default_value_raw', bpy.data, 'actions', icon='NONE', text='')
|
||||
|
||||
def draw_color(self, context, node):
|
||||
return 0.8, 0.8, 0.8, 1
|
||||
|
||||
|
||||
class ArmArraySocket(ArmCustomSocket):
|
||||
bl_idname = 'ArmNodeSocketArray'
|
||||
bl_label = 'Array Socket'
|
||||
arm_socket_type = 'NONE'
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
layout.label(text=self.name)
|
||||
|
||||
def draw_color(self, context, node):
|
||||
return 0.8, 0.4, 0.0, 1
|
||||
|
||||
|
||||
class ArmObjectSocket(ArmCustomSocket):
|
||||
bl_idname = 'ArmNodeSocketObject'
|
||||
bl_label = 'Object Socket'
|
||||
arm_socket_type = 'OBJECT'
|
||||
|
||||
default_value_get: PointerProperty(name='Object', type=bpy.types.Object) # legacy version of the line after this one
|
||||
default_value_raw: PointerProperty(name='Object', type=bpy.types.Object)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
if self.default_value_get is not None:
|
||||
self.default_value_raw = self.default_value_get
|
||||
self.default_value_get = None
|
||||
|
||||
def get_default_value(self):
|
||||
if self.default_value_raw is None:
|
||||
return ''
|
||||
if self.default_value_raw.name not in bpy.data.objects:
|
||||
return self.default_value_raw.name
|
||||
return arm.utils.asset_name(bpy.data.objects[self.default_value_raw.name])
|
||||
|
||||
def draw(self, context, layout, node, text):
|
||||
if self.is_output:
|
||||
|
@ -85,7 +109,7 @@ class ArmObjectSocket(NodeSocket):
|
|||
layout.label(text=self.name)
|
||||
else:
|
||||
row = layout.row(align=True)
|
||||
row.prop_search(self, 'default_value_get', context.scene, 'objects', icon='NONE', text=self.name)
|
||||
row.prop_search(self, 'default_value_raw', context.scene, 'objects', icon='NONE', text=self.name)
|
||||
|
||||
def draw_color(self, context, node):
|
||||
return 0.15, 0.55, 0.75, 1
|
||||
|
|
|
@ -4,8 +4,10 @@ class RotateObjectNode(ArmLogicTreeNode):
|
|||
"""Rotate object node"""
|
||||
bl_idname = 'LNRotateObjectNode'
|
||||
bl_label = 'Rotate Object'
|
||||
arm_version = 1
|
||||
|
||||
def init(self, context):
|
||||
super().init(context)
|
||||
self.add_input('ArmNodeSocketAction', 'In')
|
||||
self.add_input('ArmNodeSocketObject', 'Object')
|
||||
self.add_input('NodeSocketVector', 'Euler Angles')
|
||||
|
|
|
@ -6,16 +6,30 @@ class RotateObjectAroundAxisNode(ArmLogicTreeNode):
|
|||
bl_label = 'Rotate Object Around Axis'
|
||||
bl_description = 'Rotate Object Around Axis (Depreciated: use "Rotate Object")'
|
||||
bl_icon = 'ERROR'
|
||||
arm_version=2
|
||||
|
||||
def init(self, context):
|
||||
super().init(context)
|
||||
print("init() getting called")
|
||||
self.add_input('ArmNodeSocketAction', 'In')
|
||||
self.add_input('ArmNodeSocketObject', 'Object')
|
||||
self.add_input('NodeSocketVector', 'Axis', default_value=[0, 0, 1])
|
||||
self.add_input('NodeSocketFloat', 'Angle')
|
||||
self.add_output('ArmNodeSocketAction', 'Out')
|
||||
|
||||
def get_replacement_node(self, node_tree: bpy.types.NodeTree):
|
||||
if self.arm_version not in (0, 1):
|
||||
raise LookupError()
|
||||
|
||||
return NodeReplacement(
|
||||
'LNRotateObjectAroundAxisNode', self.arm_version, 'LNRotateObjectNode', 1,
|
||||
in_socket_mapping = {0:0, 1:1, 2:2, 3:3}, out_socket_mapping={0:0},
|
||||
property_defaults={'property0': "Angle Axies (Radians)"}
|
||||
)
|
||||
|
||||
def draw_buttons(self, context, layout):
|
||||
row = layout.row(align=True)
|
||||
row.label(text='Depreciated. Consider using "Rotate Object"')
|
||||
|
||||
add_node(RotateObjectAroundAxisNode, category=PKG_AS_CATEGORY, section='rotation')
|
||||
add_node(RotateObjectAroundAxisNode, category=PKG_AS_CATEGORY, section='rotation', is_obselete=True)
|
||||
|
||||
|
|
|
@ -246,40 +246,39 @@ def get_root_nodes(node_group):
|
|||
|
||||
def build_default_node(inp: bpy.types.NodeSocket):
|
||||
"""Creates a new node to give a not connected input socket a value"""
|
||||
null_node = 'new armory.logicnode.NullNode(this)'
|
||||
|
||||
if isinstance(inp, arm.logicnode.arm_sockets.ArmCustomSocket):
|
||||
is_custom_socket = isinstance(inp, arm.logicnode.arm_sockets.ArmCustomSocket)
|
||||
|
||||
if is_custom_socket:
|
||||
# ArmCustomSockets need to implement get_default_value()
|
||||
default_value = inp.get_default_value()
|
||||
if default_value is None:
|
||||
return null_node
|
||||
if isinstance(default_value, str):
|
||||
default_value = f'"{default_value}"'
|
||||
default_value = '"{:s}"'.format( default_value.replace('"', '\\"') )
|
||||
inp_type = inp.arm_socket_type # any custom socket's `type` is "VALUE". might as well have valuable type information for custom nodes as well.
|
||||
else:
|
||||
default_value = inp.default_value
|
||||
inp_type = inp.type
|
||||
|
||||
|
||||
if inp_type == 'VECTOR':
|
||||
return f'new armory.logicnode.VectorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]})'
|
||||
elif inp_type == 'RGBA':
|
||||
return f'new armory.logicnode.ColorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]}, {default_value[3]})'
|
||||
elif inp_type == 'RGB':
|
||||
return f'new armory.logicnode.ColorNode(this, {default_value[0]}, {default_value[1]}, {default_value[2]})'
|
||||
elif inp_type == 'VALUE':
|
||||
return f'new armory.logicnode.FloatNode(this, {default_value})'
|
||||
elif inp_type == 'INT':
|
||||
return f'new armory.logicnode.IntegerNode(this, {default_value})'
|
||||
elif inp_type == 'BOOLEAN':
|
||||
return f'new armory.logicnode.BooleanNode(this, {str(default_value).lower()})'
|
||||
elif inp_type == 'STRING':
|
||||
return f'new armory.logicnode.StringNode(this, {default_value})'
|
||||
elif inp_type == 'NONE':
|
||||
return 'new armory.logicnode.NullNode(this)'
|
||||
elif inp_type == 'OBJECT':
|
||||
return f'new armory.logicnode.ObjectNode(this, {default_value})'
|
||||
elif is_custom_socket:
|
||||
return f'new armory.logicnode.DynamicNode(this, {default_value})'
|
||||
|
||||
if inp.bl_idname == 'ArmNodeSocketAction' or inp.bl_idname == 'ArmNodeSocketArray':
|
||||
return null_node
|
||||
if inp.bl_idname == 'ArmNodeSocketObject':
|
||||
return f'new armory.logicnode.ObjectNode(this, "{inp.get_default_value()}")'
|
||||
if inp.bl_idname == 'ArmNodeSocketAnimAction':
|
||||
# Backslashes are not allowed in f-strings so we need this variable
|
||||
default_value = inp.get_default_value().replace("\"", "\\\"")
|
||||
return f'new armory.logicnode.StringNode(this, "{default_value}")'
|
||||
if inp.type == 'VECTOR':
|
||||
return f'new armory.logicnode.VectorNode(this, {inp.default_value[0]}, {inp.default_value[1]}, {inp.default_value[2]})'
|
||||
elif inp.type == 'RGBA':
|
||||
return f'new armory.logicnode.ColorNode(this, {inp.default_value[0]}, {inp.default_value[1]}, {inp.default_value[2]}, {inp.default_value[3]})'
|
||||
elif inp.type == 'RGB':
|
||||
return f'new armory.logicnode.ColorNode(this, {inp.default_value[0]}, {inp.default_value[1]}, {inp.default_value[2]})'
|
||||
elif inp.type == 'VALUE':
|
||||
return f'new armory.logicnode.FloatNode(this, {inp.default_value})'
|
||||
elif inp.type == 'INT':
|
||||
return f'new armory.logicnode.IntegerNode(this, {inp.default_value})'
|
||||
elif inp.type == 'BOOLEAN':
|
||||
return f'new armory.logicnode.BooleanNode(this, {str(inp.default_value).lower()})'
|
||||
elif inp.type == 'STRING':
|
||||
default_value = inp.default_value.replace("\"", "\\\"")
|
||||
return f'new armory.logicnode.StringNode(this, "{default_value}")'
|
||||
|
||||
return null_node
|
||||
else:
|
||||
return 'new armory.logicnode.NullNode(this)'
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
from typing import Callable
|
||||
import os.path
|
||||
import time
|
||||
import webbrowser
|
||||
|
||||
import bpy
|
||||
from bpy.props import BoolProperty, StringProperty
|
||||
from bpy.types import NodeTree
|
||||
#from bpy.types import NodeTree, Node
|
||||
import nodeitems_utils
|
||||
|
||||
from arm.logicnode import *
|
||||
from arm.logicnode import arm_nodes
|
||||
from arm.logicnode.arm_nodes import ArmNodeCategory
|
||||
from arm.logicnode import arm_sockets
|
||||
from arm.logicnode import *
|
||||
|
||||
registered_nodes = []
|
||||
registered_categories = []
|
||||
|
||||
|
||||
class ArmLogicTree(NodeTree):
|
||||
class ArmLogicTree(bpy.types.NodeTree):
|
||||
"""Logic nodes"""
|
||||
bl_idname = 'ArmLogicTreeType'
|
||||
bl_label = 'Logic Node Editor'
|
||||
|
@ -278,89 +280,138 @@ class ARMAddSetVarNode(bpy.types.Operator):
|
|||
self.setNodeRef = setNode
|
||||
return({'FINISHED'})
|
||||
|
||||
# node replacement code
|
||||
replacements = {}
|
||||
|
||||
def add_replacement(item):
|
||||
replacements[item.from_node] = item
|
||||
def replace(tree: bpy.types.NodeTree, node: bpy.types.Node):
|
||||
"""Replaces the given node with its replacement."""
|
||||
|
||||
def get_replaced_nodes():
|
||||
return replacements.keys()
|
||||
|
||||
def get_replacement_for_node(node):
|
||||
return replacements[node.bl_idname]
|
||||
|
||||
class Replacement:
|
||||
# represents a single replacement rule, this can replace exactly one node with another
|
||||
#
|
||||
# from_node: the node type to be removed
|
||||
# to_node: the node type which takes from_node's place
|
||||
# *SocketMapping: a map which defines how the sockets of the old node shall be connected to the new node
|
||||
# {1: 2} means that anything connected to the socket with index 1 on the original node will be connected to the socket with index 2 on the new node
|
||||
def __init__(self, from_node, to_node, in_socket_mapping, out_socket_mapping, property_mapping):
|
||||
self.from_node = from_node
|
||||
self.to_node = to_node
|
||||
self.in_socket_mapping = in_socket_mapping
|
||||
self.out_socket_mapping = out_socket_mapping
|
||||
self.property_mapping = property_mapping
|
||||
|
||||
# actual replacement code
|
||||
def replace(tree, node):
|
||||
replacement = get_replacement_for_node(node)
|
||||
newnode = tree.nodes.new(replacement.to_node)
|
||||
newnode.location = node.location
|
||||
newnode.parent = node.parent
|
||||
|
||||
parent = node.parent
|
||||
while parent is not None:
|
||||
newnode.location[0] += parent.location[0]
|
||||
newnode.location[1] += parent.location[1]
|
||||
parent = parent.parent
|
||||
# the node can either return a NodeReplacement object (for simple replacements)
|
||||
# or a brand new node, for more complex stuff.
|
||||
response = node.get_replacement_node(tree)
|
||||
|
||||
|
||||
# map properties
|
||||
for prop in replacement.property_mapping.keys():
|
||||
setattr(newnode, replacement.property_mapping.get(prop), getattr(node, prop))
|
||||
if isinstance(response, bpy.types.Node):
|
||||
newnode = response
|
||||
# some misc. properties
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
elif isinstance(response, list): # a list of nodes:
|
||||
for node in response:
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
elif isinstance(response, arm_nodes.NodeReplacement):
|
||||
replacement = response
|
||||
# if the returned object is a NodeReplacement, check that it corresponds to the node (also, create the new node)
|
||||
if node.bl_idname != replacement.from_node or node.arm_version != replacement.from_node_version:
|
||||
raise LookupError("the provided NodeReplacement doesn't seem to correspond to the node needing replacement")
|
||||
newnode = tree.nodes.new(response.to_node)
|
||||
if newnode.arm_version != replacement.to_node_version:
|
||||
raise LookupError("the provided NodeReplacement doesn't seem to correspond to the node needing replacement")
|
||||
|
||||
# map unconnected inputs
|
||||
for in_socket in replacement.in_socket_mapping.keys():
|
||||
if not node.inputs[in_socket].is_linked:
|
||||
newnode.inputs[replacement.in_socket_mapping.get(in_socket)].default_value = node.inputs[in_socket].default_value
|
||||
# some misc. properties
|
||||
newnode.parent = node.parent
|
||||
newnode.location = node.location
|
||||
newnode.select = node.select
|
||||
|
||||
# map connected inputs
|
||||
for link in tree.links:
|
||||
if link.from_node == node:
|
||||
# this is an output link
|
||||
for i in range(0, len(node.outputs)):
|
||||
# check the outputs
|
||||
# i represents the socket index
|
||||
# do we want to remap it & is it the one referenced in the current link
|
||||
if i in replacement.out_socket_mapping.keys() and node.outputs[i] == link.from_socket:
|
||||
tree.links.new(newnode.outputs[replacement.out_socket_mapping.get(i)], link.to_socket)
|
||||
|
||||
if link.to_node == node:
|
||||
# this is an input link
|
||||
for i in range(0, len(node.inputs)):
|
||||
# check the inputs
|
||||
# i represents the socket index
|
||||
# do we want to remap it & is it the one referenced socket in the current link
|
||||
if i in replacement.in_socket_mapping.keys() and node.inputs[i] == link.to_socket:
|
||||
tree.links.new(newnode.inputs[replacement.in_socket_mapping.get(i)], link.from_socket)
|
||||
# now, use the `replacement` to hook up the new node correctly
|
||||
# start by applying defaults
|
||||
for prop_name, prop_value in replacement.property_defaults.items():
|
||||
setattr(newnode, prop_name, prop_value)
|
||||
for input_id, input_value in replacement.input_defaults.items():
|
||||
input_socket = newnode.inputs[input_id]
|
||||
if isinstance(input_socket, arm_sockets.ArmCustomSocket):
|
||||
if input_socket.arm_socket_type != 'NONE':
|
||||
input_socket.default_value_raw = input_value
|
||||
else:
|
||||
input_socket.default_value = input_value
|
||||
|
||||
# map properties
|
||||
for src_prop_name, dest_prop_name in replacement.property_mapping.items():
|
||||
setattr(newnode, dest_prop_name, getattr(node, src_prop_name))
|
||||
|
||||
# map inputs
|
||||
for src_socket_id, dest_socket_id in replacement.in_socket_mapping.items():
|
||||
src_socket = node.inputs[src_socket_id]
|
||||
dest_socket = newnode.inputs[dest_socket_id]
|
||||
if src_socket.is_linked:
|
||||
# an input socket only has one link
|
||||
datasource_socket = src_socket.links[0].from_socket
|
||||
tree.links.new(datasource_socket, dest_socket)
|
||||
else:
|
||||
if isinstance(dest_socket, arm_sockets.ArmCustomSocket):
|
||||
if dest_socket.arm_socket_type != 'NONE':
|
||||
dest_socket.default_value_raw = src_socket.default_value_raw
|
||||
else:
|
||||
dest_socket.default_value = src_socket.default_value
|
||||
|
||||
# map outputs
|
||||
for src_socket_id, dest_socket_id in replacement.out_socket_mapping.items():
|
||||
dest_socket = newnode.outputs[dest_socket_id]
|
||||
for link in node.outputs[src_socket_id].links:
|
||||
tree.links.new(dest_socket, link.to_socket)
|
||||
else:
|
||||
print(response)
|
||||
tree.nodes.remove(node)
|
||||
|
||||
|
||||
def replaceAll():
|
||||
global replacement_errors
|
||||
list_of_errors = set()
|
||||
for tree in bpy.data.node_groups:
|
||||
if tree.bl_idname == "ArmLogicTreeType":
|
||||
for node in tree.nodes:
|
||||
if node.bl_idname in get_replaced_nodes():
|
||||
print("Replacing "+ node.bl_idname+ " in Tree "+tree.name)
|
||||
replace(tree, node)
|
||||
|
||||
|
||||
if not isinstance(type(node).arm_version, int):
|
||||
continue # TODO: that's a line to remove when all node classes will have their own version set.
|
||||
if not node.is_registered_node_type():
|
||||
# node type deleted. That's unusual. Or it has been replaced for a looong time.
|
||||
list_of_errors.add( ('unregistered', None, tree.name) )
|
||||
if node.arm_version < type(node).arm_version:
|
||||
try:
|
||||
replace(tree, node)
|
||||
except LookupError as err:
|
||||
list_of_errors.add( ('update failed', node.bl_idname, tree.name) )
|
||||
except Exception as err:
|
||||
list_of_errors.add( ('misc.', node.bl_idname, tree.name) )
|
||||
elif node.arm_version > type(node).arm_version:
|
||||
list_of_errors.add( ('future version', node.bl_idname, tree.name) )
|
||||
|
||||
# if possible, make a popup about the errors.
|
||||
# also write an error report.
|
||||
if len(list_of_errors) > 0:
|
||||
print('there were errors in node replacement')
|
||||
basedir = os.path.dirname(bpy.data.filepath)
|
||||
reportfile = os.path.join(
|
||||
basedir, 'node_update_failure.{:s}.txt'.format(
|
||||
time.strftime("%Y-%m-%dT%H-%M-%S%z")
|
||||
)
|
||||
)
|
||||
reportf = open(reportfile, 'w')
|
||||
for error_type, node_class, tree_name in list_of_errors:
|
||||
if error_type == 'unregistered':
|
||||
print(f"A node whose class doesn't exist was found in node tree \"{tree_name}\"", file=reportf)
|
||||
elif error_type == 'update failed':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" failed to be updated, "
|
||||
f"because update isn't implemented (anymore?) for this version of the node", file=reportf)
|
||||
elif error_type == 'future version':
|
||||
print(f"A node of type {node_class} in tree \"{tree_name}\" seemingly comes from a future version of armory. "
|
||||
f"Please check whether your version of armory is up to date", file=reportf)
|
||||
elif error_type == 'misc.':
|
||||
print(f"", file=reportf)
|
||||
else:
|
||||
print(f"Whoops, we don't know what this error type (\"{error_type}\") means. You might want to report a bug here. "
|
||||
f"All we know is that it comes form a node of class {node_class} in the node tree called \"{tree_name}\".", file=reportf)
|
||||
reportf.close()
|
||||
|
||||
replacement_errors = list_of_errors
|
||||
bpy.ops.arm.show_node_update_errors()
|
||||
replacement_errors = None
|
||||
|
||||
|
||||
class ReplaceNodesOperator(bpy.types.Operator):
|
||||
'''Automatically replaces deprecated nodes.'''
|
||||
"""Automatically replaces deprecated nodes."""
|
||||
bl_idname = "node.replace"
|
||||
bl_label = "Replace Nodes"
|
||||
bl_description = "Replace deprecated nodes"
|
||||
|
||||
def execute(self, context):
|
||||
replaceAll()
|
||||
|
@ -368,11 +419,12 @@ class ReplaceNodesOperator(bpy.types.Operator):
|
|||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.space_data != None and context.space_data.type == 'NODE_EDITOR'
|
||||
return context.space_data is not None and context.space_data.type == 'NODE_EDITOR'
|
||||
|
||||
# TODO: deprecated
|
||||
# Input Replacement Rules
|
||||
# add_replacement(Replacement("LNOnGamepadNode", "LNMergedGamepadNode", {0: 0}, {0: 0}, {"property0": "property0", "property1": "property1"}))
|
||||
|
||||
# Node Replacement Rules (TODO port them)
|
||||
#add_replacement(NodeReplacement("LNOnGamepadNode", "LNMergedGamepadNode", {0: 0}, {0: 0}, {"property0": "property0", "property1": "property1"}))
|
||||
#add_replacement(NodeReplacement("LNOnKeyboardNode", "LNMergedKeyboardNode", {}, {0: 0}, {"property0": "property0", "property1": "property1"}))
|
||||
|
||||
def register():
|
||||
arm_sockets.register()
|
||||
|
|
|
@ -286,6 +286,7 @@ def init_properties():
|
|||
bpy.types.Node.arm_material_param = BoolProperty(name="Parameter", description="Control this node from script", default=False)
|
||||
bpy.types.Node.arm_logic_id = StringProperty(name="ID", description="Nodes with equal identifier will share data", default='')
|
||||
bpy.types.Node.arm_watch = BoolProperty(name="Watch", description="Watch value of this node in debug console", default=False)
|
||||
bpy.types.Node.arm_version = IntProperty(name="Node Version", description="The version of an instanced node", default=0)
|
||||
# Particles
|
||||
bpy.types.ParticleSettings.arm_count_mult = FloatProperty(name="Multiply Count", description="Multiply particle count when rendering in Armory", default=1.0)
|
||||
bpy.types.ParticleSettings.arm_loop = BoolProperty(name="Loop", description="Loop this particle system", default=False)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
import time
|
||||
|
||||
import bpy
|
||||
|
||||
|
@ -9,6 +10,7 @@ import arm.make as make
|
|||
import arm.make_state as state
|
||||
import arm.props as props
|
||||
import arm.props_properties
|
||||
import arm.nodes_logic
|
||||
import arm.proxy
|
||||
import arm.utils
|
||||
|
||||
|
@ -1773,6 +1775,191 @@ class ARM_PT_MaterialNodePanel(bpy.types.Panel):
|
|||
if n != None and (n.bl_idname == 'ShaderNodeRGB' or n.bl_idname == 'ShaderNodeValue' or n.bl_idname == 'ShaderNodeTexImage'):
|
||||
layout.prop(context.active_node, 'arm_material_param')
|
||||
|
||||
class ARM_OT_ShowFileVersionInfo(bpy.types.Operator):
|
||||
bl_label = 'Show old file version info'
|
||||
bl_idname = 'arm.show_old_file_version_info'
|
||||
bl_description = ('Displays an info panel that warns about opening a file'
|
||||
'which was created in a previous version of Armory')
|
||||
# bl_options = {'INTERNAL'}
|
||||
|
||||
wrd = None
|
||||
|
||||
def draw_message_box(self, context):
|
||||
file_version = ARM_OT_ShowFileVersionInfo.wrd.arm_version
|
||||
current_version = props.arm_version
|
||||
|
||||
|
||||
layout = self.layout
|
||||
layout = layout.column(align=True)
|
||||
layout.alignment = 'EXPAND'
|
||||
|
||||
if current_version == file_version:
|
||||
layout.label('This file was saved in', icon='INFO')
|
||||
layout.label('the current Armory version', icon='BLANK1')
|
||||
layout.separator()
|
||||
layout.label(f'(version: {current_version}')
|
||||
row = layout.row(align=True)
|
||||
row.active_default = True
|
||||
row.operator('arm.discard_popup', text='Ok')
|
||||
|
||||
# this will help order versions better, somewhat.
|
||||
# note: this is NOT complete
|
||||
current_version = tuple( current_version.split('.') )
|
||||
file_version = tuple( file_version.split('.') )
|
||||
|
||||
if current_version > file_version:
|
||||
layout.label(text='Warning: This file was saved in a', icon='ERROR')
|
||||
layout.label(text='previous version of Armory!', icon='BLANK1')
|
||||
layout.separator()
|
||||
|
||||
layout.label(text='Please inform yourself about breaking changes!', icon='BLANK1')
|
||||
layout.label(text=f'File saved in: {file_version}', icon='BLANK1')
|
||||
layout.label(text=f'Current version: {current_version}', icon='BLANK1')
|
||||
layout.separator()
|
||||
layout.separator()
|
||||
layout.label(text='Should Armory try to automatically update', icon='BLANK1')
|
||||
layout.label(text='the file to the current SDK version?', icon='BLANK1')
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.active_default = True
|
||||
row.operator('arm.update_file_sdk', text='Yes')
|
||||
row.active_default = False
|
||||
row.operator('arm.discard_popup', text='No')
|
||||
else:
|
||||
layout.label(text='Warning: This file was saved in a', icon='ERROR')
|
||||
layout.label(text='future version of Armory!', icon='BLANK1')
|
||||
layout.separator()
|
||||
|
||||
layout.label(text='It is impossible to downgrade a file,', icon='BLANK1')
|
||||
layout.label(text='Something will probably be broken here.', icon='BLANK1')
|
||||
layout.label(text=f'File saved in: {file_version}', icon='BLANK1')
|
||||
layout.label(text=f'Current version: {current_version}', icon='BLANK1')
|
||||
layout.separator()
|
||||
layout.separator()
|
||||
layout.label(text='Please check how this file was created', icon='BLANK1')
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.active_default = True
|
||||
row.operator('arm.discard_popup', text='Ok')
|
||||
|
||||
def execute(self, context):
|
||||
ARM_OT_ShowFileVersionInfo.wrd = bpy.data.worlds['Arm']
|
||||
context.window_manager.popover(ARM_OT_ShowFileVersionInfo.draw_message_box, ui_units_x=16)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ARM_OT_ShowNodeUpdateErrors(bpy.types.Operator):
|
||||
bl_label = 'Show upgrade failure details'
|
||||
bl_idname = 'arm.show_node_update_errors'
|
||||
bl_description = ('Displays an info panel that shows the different errors that occurred when upgrading nodes')
|
||||
|
||||
wrd = None # a helper internal variable
|
||||
|
||||
def draw_message_box(self, context):
|
||||
list_of_errors = arm.nodes_logic.replacement_errors.copy()
|
||||
# note: list_of_errors is a set of tuples: `(error_type, node_class, tree_name)`
|
||||
# where `error_type` can be "unregistered", "update failed", "future version" or "misc."
|
||||
|
||||
file_version = ARM_OT_ShowNodeUpdateErrors.wrd.arm_version
|
||||
current_version = props.arm_version
|
||||
|
||||
# this will help order versions better, somewhat.
|
||||
# note: this is NOT complete
|
||||
current_version_2 = tuple( current_version.split('.') )
|
||||
file_version_2 = tuple( file_version.split('.') )
|
||||
is_armory_upgrade = (current_version_2 > file_version_2)
|
||||
|
||||
error_types = set()
|
||||
errored_trees = set()
|
||||
errored_nodes = set()
|
||||
for error_entry in list_of_errors:
|
||||
error_types.add(error_entry[0])
|
||||
errored_nodes.add(error_entry[1])
|
||||
errored_trees.add(error_entry[2])
|
||||
|
||||
layout = self.layout
|
||||
layout = layout.column(align=True)
|
||||
layout.alignment = 'EXPAND'
|
||||
|
||||
layout.label(text="Some nodes failed to be updated to the current armory version", icon="ERROR")
|
||||
if current_version==file_version:
|
||||
layout.label(text="(This might be because you are using a development snapshot, or a homemade version ;) )", icon='BLANK1')
|
||||
elif not is_armory_upgrade:
|
||||
layout.label(text="(Please note that it is not possible do downgrade nodes to a previous version either.", icon='BLANK1')
|
||||
layout.label(text="This might be the cause of your problem.)", icon='BLANK1')
|
||||
|
||||
layout.label(text=f'File saved in: {file_version}', icon='BLANK1')
|
||||
layout.label(text=f'Current version: {current_version}', icon='BLANK1')
|
||||
layout.separator()
|
||||
|
||||
if 'update failed' in error_types:
|
||||
layout.label(text="Some nodes do not have an update procedure to deal with the version saved in this file.", icon='BLANK1')
|
||||
if current_version==file_version:
|
||||
layout.label(text="(if you are a developer, this might be because you didn't implement it yet.)", icon='BLANK1')
|
||||
if 'unregistered' in error_types:
|
||||
if is_armory_upgrade:
|
||||
layout.label(text='Some nodes seem to be too old to be understood by armory anymore', icon='BLANK1')
|
||||
else:
|
||||
layout.label(text="Some nodes are unknown to armory, either because they are too new or too old.", icon='BLANK1')
|
||||
if 'future version' in error_types:
|
||||
if is_armory_upgrade:
|
||||
layout.label(text='Somehow, some nodes seem to have been created with a future version of armory.', icon='BLANK1')
|
||||
else:
|
||||
layout.label(text='Some nodes seem to have been created with a future version of armory.', icon='BLANK1')
|
||||
if 'misc.' in error_types:
|
||||
layout.label(text="Some nodes' update procedure failed to complete")
|
||||
|
||||
layout.separator()
|
||||
layout.label(text='the nodes impacted are the following:', icon='BLANK1')
|
||||
for node in errored_nodes:
|
||||
layout.label(text=f' {node}', icon='BLANK1')
|
||||
layout.separator()
|
||||
layout.label(text='the node trees impacted are the following:', icon='BLANK1')
|
||||
for tree in errored_trees:
|
||||
layout.label(text=f' "{tree}"', icon='BLANK1')
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="A detailed error report has been saved next to the blender file.", icon='BLANK1')
|
||||
layout.label(text="the file name is \"node_update_failure\", followed by the current time.", icon='BLANK1')
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.active_default = False
|
||||
row.operator('arm.discard_popup', text='Ok')
|
||||
|
||||
def execute(self, context):
|
||||
ARM_OT_ShowNodeUpdateErrors.wrd = bpy.data.worlds['Arm']
|
||||
context.window_manager.popover(ARM_OT_ShowNodeUpdateErrors.draw_message_box, ui_units_x=32)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ARM_OT_UpdateFileSDK(bpy.types.Operator):
|
||||
bl_idname = 'arm.update_file_sdk'
|
||||
bl_label = 'Update file to current SDK version'
|
||||
bl_description = bl_label
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
wrd = bpy.data.worlds['Arm']
|
||||
# This allows for seamless migration from ealier versions of Armory
|
||||
for rp in wrd.arm_rplist: # TODO: deprecated
|
||||
if rp.rp_gi != 'Off':
|
||||
rp.rp_gi = 'Off'
|
||||
rp.rp_voxelao = True
|
||||
|
||||
# Replace deprecated nodes
|
||||
arm.nodes_logic.replaceAll()
|
||||
|
||||
wrd.arm_version = props.arm_version
|
||||
wrd.arm_commit = props.arm_commit
|
||||
|
||||
arm.make.clean()
|
||||
print(f'Project updated to SDK {props.arm_version}. Please save the .blend file.')
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
class ARM_OT_DiscardPopup(bpy.types.Operator):
|
||||
"""Empty operator for discarding dialogs."""
|
||||
|
@ -1833,6 +2020,9 @@ def register():
|
|||
bpy.utils.register_class(ArmSyncProxyButton)
|
||||
bpy.utils.register_class(ArmPrintTraitsButton)
|
||||
bpy.utils.register_class(ARM_PT_MaterialNodePanel)
|
||||
bpy.utils.register_class(ARM_OT_UpdateFileSDK)
|
||||
bpy.utils.register_class(ARM_OT_ShowFileVersionInfo)
|
||||
bpy.utils.register_class(ARM_OT_ShowNodeUpdateErrors)
|
||||
bpy.utils.register_class(ARM_OT_DiscardPopup)
|
||||
|
||||
bpy.types.VIEW3D_HT_header.append(draw_view3d_header)
|
||||
|
@ -1844,6 +2034,9 @@ def unregister():
|
|||
bpy.types.VIEW3D_HT_header.remove(draw_view3d_header)
|
||||
|
||||
bpy.utils.unregister_class(ARM_OT_DiscardPopup)
|
||||
bpy.utils.unregister_class(ARM_OT_ShowNodeUpdateErrors)
|
||||
bpy.utils.unregister_class(ARM_OT_ShowFileVersionInfo)
|
||||
bpy.utils.unregister_class(ARM_OT_UpdateFileSDK)
|
||||
bpy.utils.unregister_class(ARM_PT_ObjectPropsPanel)
|
||||
bpy.utils.unregister_class(ARM_PT_ModifiersPropsPanel)
|
||||
bpy.utils.unregister_class(ARM_PT_ParticlesPropsPanel)
|
||||
|
@ -1890,4 +2083,4 @@ def unregister():
|
|||
bpy.utils.unregister_class(ArmProxyApplyAllButton)
|
||||
bpy.utils.unregister_class(ArmSyncProxyButton)
|
||||
bpy.utils.unregister_class(ArmPrintTraitsButton)
|
||||
bpy.utils.unregister_class(ARM_PT_MaterialNodePanel)
|
||||
bpy.utils.unregister_class(ARM_PT_MaterialNodePanel)
|
||||
|
|
Loading…
Reference in a new issue