diff --git a/blender/arm/logicnode/arm_nodes.py b/blender/arm/logicnode/arm_nodes.py index 64c23121..877c01b3 100644 --- a/blender/arm/logicnode/arm_nodes.py +++ b/blender/arm/logicnode/arm_nodes.py @@ -121,7 +121,14 @@ class ArmNodeAddInputButton(bpy.types.Operator): def execute(self, context): global array_nodes inps = array_nodes[self.node_index].inputs - inps.new(self.socket_type, self.name_format.format(str(len(inps) + self.index_name_offset))) + + socket_types = self.socket_type.split(';') + name_formats = self.name_format.split(';') + assert len(socket_types)==len(name_formats) + + format_index = (len(inps) + self.index_name_offset) //len(socket_types) + for socket_type, name_format in zip(socket_types, name_formats): + inps.new(socket_type, name_format.format(str(format_index))) # Reset to default again for subsequent calls of this operator self.node_index = '' @@ -132,31 +139,34 @@ class ArmNodeAddInputButton(bpy.types.Operator): return{'FINISHED'} class ArmNodeAddInputValueButton(bpy.types.Operator): - """Add new input""" - bl_idname = 'arm.node_add_input_value' - bl_label = 'Add Input' - node_index: StringProperty(name='Node Index', default='') - socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') + """Add new input""" + bl_idname = 'arm.node_add_input_value' + bl_label = 'Add Input' + node_index: StringProperty(name='Node Index', default='') + socket_type: StringProperty(name='Socket Type', default='NodeSocketShader') - def execute(self, context): - global array_nodes - inps = array_nodes[self.node_index].inputs - inps.new(self.socket_type, 'Value') - return{'FINISHED'} + def execute(self, context): + global array_nodes + inps = array_nodes[self.node_index].inputs + inps.new(self.socket_type, 'Value') + return{'FINISHED'} class ArmNodeRemoveInputButton(bpy.types.Operator): """Remove last input""" bl_idname = 'arm.node_remove_input' bl_label = 'Remove Input' node_index: StringProperty(name='Node Index', default='') + count: IntProperty(name='Number of inputs to remove', default=1, min=1) + min_inputs: IntProperty(name='Number of inputs to keep', default=0, min=0) def execute(self, context): global array_nodes node = array_nodes[self.node_index] inps = node.inputs - min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs - if len(inps) > min_inps: - inps.remove(inps.values()[-1]) + min_inps = self.min_inputs if not hasattr(node, 'min_inputs') else node.min_inputs + if len(inps) >= min_inps + self.count: + for _ in range(self.count): + inps.remove(inps.values()[-1]) return{'FINISHED'} class ArmNodeRemoveInputValueButton(bpy.types.Operator): @@ -164,13 +174,14 @@ class ArmNodeRemoveInputValueButton(bpy.types.Operator): bl_idname = 'arm.node_remove_input_value' bl_label = 'Remove Input' node_index: StringProperty(name='Node Index', default='') + target_name: StringProperty(name='Name of socket to remove', default='Value') def execute(self, context): global array_nodes node = array_nodes[self.node_index] inps = node.inputs min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs - if len(inps) > min_inps and inps[-1].name == 'Value': + if len(inps) > min_inps and inps[-1].name == self.target_name: inps.remove(inps.values()[-1]) return{'FINISHED'} @@ -187,7 +198,14 @@ class ArmNodeAddOutputButton(bpy.types.Operator): def execute(self, context): global array_nodes outs = array_nodes[self.node_index].outputs - outs.new(self.socket_type, self.name_format.format(str(len(outs) + self.index_name_offset))) + + socket_types = self.socket_type.split(';') + name_formats = self.name_format.split(';') + assert len(socket_types)==len(name_formats) + + format_index = (len(outs) + self.index_name_offset) //len(socket_types) + for socket_type, name_format in zip(socket_types, name_formats): + outs.new(socket_type, name_format.format(str(format_index))) # Reset to default again for subsequent calls of this operator self.node_index = '' @@ -202,14 +220,16 @@ class ArmNodeRemoveOutputButton(bpy.types.Operator): bl_idname = 'arm.node_remove_output' bl_label = 'Remove Output' node_index: StringProperty(name='Node Index', default='') + count: IntProperty(name='Number of outputs to remove', default=1, min=1) def execute(self, context): global array_nodes node = array_nodes[self.node_index] outs = node.outputs min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs - if len(outs) > min_outs: - outs.remove(outs.values()[-1]) + if len(outs) >= min_outs + self.count: + for _ in range(self.count): + outs.remove(outs.values()[-1]) return{'FINISHED'} class ArmNodeAddInputOutputButton(bpy.types.Operator): @@ -229,8 +249,21 @@ class ArmNodeAddInputOutputButton(bpy.types.Operator): node = array_nodes[self.node_index] inps = node.inputs outs = node.outputs - inps.new(self.in_socket_type, self.in_name_format.format(str(len(inps) + self.in_index_name_offset))) - outs.new(self.out_socket_type, self.out_name_format.format(str(len(outs)))) + + in_socket_types = self.in_socket_type.split(';') + in_name_formats = self.in_name_format.split(';') + assert len(in_socket_types)==len(in_name_formats) + + out_socket_types = self.out_socket_type.split(';') + out_name_formats = self.out_name_format.split(';') + assert len(out_socket_types)==len(out_name_formats) + + in_format_index = (len(outs) + self.index_name_offset) // len(in_socket_types) + out_format_index = len(outs) // len(out_socket_types) + for socket_type, name_format in zip(in_socket_types, in_name_formats): + inps.new(socket_type, name_format.format(str(in_format_index))) + for socket_type, name_format in zip(out_socket_types, out_name_formats): + outs.new(socket_type, name_format.format(str(out_format_index))) # Reset to default again for subsequent calls of this operator self.node_index = '' @@ -247,7 +280,9 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): bl_idname = 'arm.node_remove_input_output' bl_label = 'Remove Input Output' node_index: StringProperty(name='Node Index', default='') - + in_count: IntProperty(name='Number of inputs to remove', default=1, min=1) + out_count: IntProperty(name='Number of inputs to remove', default=1, min=1) + def execute(self, context): global array_nodes node = array_nodes[self.node_index] @@ -255,10 +290,12 @@ class ArmNodeRemoveInputOutputButton(bpy.types.Operator): outs = node.outputs min_inps = 0 if not hasattr(node, 'min_inputs') else node.min_inputs min_outs = 0 if not hasattr(node, 'min_outputs') else node.min_outputs - if len(inps) > min_inps: - inps.remove(inps.values()[-1]) - if len(outs) > min_outs: - outs.remove(outs.values()[-1]) + if len(inps) >= min_inps + self.in_count: + for _ in range(self.in_count): + inps.remove(inps.values()[-1]) + if len(outs) >= min_outs + self.out_count: + for _ in range(self.out_count): + outs.remove(outs.values()[-1]) return{'FINISHED'} @@ -428,7 +465,10 @@ def deprecated(*alternatives: str, message=""): def wrapper(cls: ArmLogicTreeNode) -> ArmLogicTreeNode: cls.bl_label += ' (Deprecated)' - cls.bl_description = f'Deprecated. {cls.bl_description}' + if hasattr(cls, 'bl_description'): + cls.bl_description = f'Deprecated. {cls.bl_description}' + else: + cls.bl_description = 'Deprecated.' cls.bl_icon = 'ERROR' cls.arm_is_obsolete = True diff --git a/blender/arm/logicnode/arm_sockets.py b/blender/arm/logicnode/arm_sockets.py index b1dce886..a0f702fd 100644 --- a/blender/arm/logicnode/arm_sockets.py +++ b/blender/arm/logicnode/arm_sockets.py @@ -112,12 +112,16 @@ class ArmRotationSocket(ArmCustomSocket): self.default_value_s3 = 0.0 self.do_update_raw(context) - def do_update_raw(self, context): - if self.default_value_mode == 'Quat': - qx = self.default_value_s0 - qy = self.default_value_s1 - qz = self.default_value_s2 - qw = self.default_value_s3 + @staticmethod + def convert_to_quaternion(part1,part2,param1,param2,param3): + """converts a representation of rotation into a quaternion. + ``part1`` is a vector, ``part2`` is a scalar or None, + ``param1`` is in ('Quat', 'EulerAngles', 'AxisAngle'), + ``param2`` is in ('Rad','Deg') for both EulerAngles and AxisAngle, + ``param3`` is a len-3 string like "XYZ", for EulerAngles """ + if param1 == 'Quat': + qx, qy, qz = part1[0], part1[1], part1[2] + qw = part2 # need to normalize the quaternion for a rotation (having it be 0 is not an option) ql = sqrt(qx**2+qy**2+qz**2+qw**2) if abs(ql)<1E-5: @@ -127,49 +131,61 @@ class ArmRotationSocket(ArmCustomSocket): qy /= ql qz /= ql qw /= ql - self.default_value_raw = mathutils.Vector((qx,qy,qz,qw)) - - elif self.default_value_mode == 'AxisAngle': - if self.default_value_unit == 'Deg': - angle = self.default_value_s3 * pi/180 + return mathutils.Vector((qx,qy,qz,qw)) + + elif param1 == 'AxisAngle': + if param2 == 'Deg': + angle = part2 * pi/180 else: - angle = self.default_value_s3 + angle = part2 cang, sang = cos(angle/2), sin(angle/2) - x = self.default_value_s0 - y = self.default_value_s1 - z = self.default_value_s2 + x,y,z = part1[0], part1[1], part1[2] veclen = sqrt(x**2+y**2+z**2) if veclen<1E-5: - self.default_value_raw = mathutils.Vector((0.0,0.0,0.0,1.0)) + return mathutils.Vector((0.0,0.0,0.0,1.0)) else: - self.default_value_raw = mathutils.Vector(( + return mathutils.Vector(( x/veclen * sang, y/veclen * sang, z/veclen * sang, cang )) - else: - if self.default_value_unit == 'Deg': - x = self.default_value_s0 * pi/180 - y = self.default_value_s1 * pi/180 - z = self.default_value_s2 * pi/180 - else: - x = self.default_value_s0 - y = self.default_value_s1 - z = self.default_value_s2 + else: # param1 == 'EulerAngles' + x,y,z = part1[0], part1[1], part1[2] + if param2 == 'Deg': + x *= pi/180 + y *= pi/180 + z *= pi/180 cx, sx = cos(x/2), sin(x/2) cy, sy = cos(y/2), sin(y/2) cz, sz = cos(z/2), sin(z/2) qw, qx, qy, qz = 1.0,0.0,0.0,0.0 - for direction in self.default_value_order[::-1]: + for direction in param3[::-1]: qwi, qxi,qyi,qzi = {'X': (cx,sx,0,0), 'Y': (cy,0,sy,0), 'Z': (cz,0,0,sz)}[direction] qw = qw*qwi -qx*qxi -qy*qyi -qz*qzi qx = qx*qwi +qw*qxi +qy*qzi -qz*qyi qy = qy*qwi +qw*qyi +qz*qxi -qx*qzi qz = qz*qwi +qw*qzi +qx*qyi -qy*qxi - self.default_value_raw = mathutils.Vector((qx,qy,qz,qw)) + return mathutils.Vector((qx,qy,qz,qw)) + + + def do_update_raw(self, context): + part1 = mathutils.Vector(( + self.default_value_s0, + self.default_value_s1, + self.default_value_s2, 1 + )) + part2 = self.default_value_s3 + + self.default_value_raw = self.convert_to_quaternion( + part1, + self.default_value_s3, + self.default_value_mode, + self.defualt_value_unit, + self.default_value_order + ) def draw(self, context, layout, node, text): diff --git a/blender/arm/logicnode/math/LN_quaternion_math.py b/blender/arm/logicnode/math/LN_quaternion_math.py index d1f91abc..10f13832 100644 --- a/blender/arm/logicnode/math/LN_quaternion_math.py +++ b/blender/arm/logicnode/math/LN_quaternion_math.py @@ -4,44 +4,46 @@ class QuaternionMathNode(ArmLogicTreeNode): """Mathematical operations on quaternions.""" bl_idname = 'LNQuaternionMathNode' bl_label = 'Quaternion Math' + bl_description = 'Mathematical operations that can be performed on rotations, when represented as quaternions specifically' arm_section = 'quaternions' - arm_version = 1 + arm_version = 2 - def get_bool(self): - return self.get('property1', False) - def set_bool(self, value): - self['property1'] = value - if value: - if ((self.property0 == 'Module') or (self.property0 == 'DotProduct') or (self.property0 == 'ToAxisAngle')) and (len(self.outputs) > 1): - self.outputs.remove(self.outputs.values()[-1]) # Module/DotProduct/ToAxisAngle - self.add_output('NodeSocketFloat', 'X') # Result X - self.add_output('NodeSocketFloat', 'Y') # Result Y - self.add_output('NodeSocketFloat', 'Z') # Result Z - self.add_output('NodeSocketFloat', 'W') # Result W - if (self.property0 == 'Module'): - self.add_output('NodeSocketFloat', 'Module') # Module - if (self.property0 == 'DotProduct'): - self.add_output('NodeSocketFloat', 'Scalar') # DotProduct - if (self.property0 == 'ToAxisAngle'): - self.add_output('NodeSocketFloat', 'To Axis Angle') # ToAxisAngle + def ensure_input_socket(self, socket_number, newclass, newname, default_value=None): + while len(self.inputs) < socket_number: + self.inputs.new('NodeSocketFloat', 'BOGUS') + if len(self.inputs) > socket_number: + if len(self.inputs[socket_number].links) == 1: + source_socket = self.inputs[socket_number].links[0].from_socket + else: + source_socket = None + if self.inputs[socket_number].bl_idname == newclass: + default_value = self.inputs[socket_number].default_value + self.inputs.remove(self.inputs[socket_number]) else: - if ((self.property0 == 'Module') or (self.property0 == 'DotProduct') or (self.property0 == 'ToAxisAngle')) and (len(self.outputs) > 1): - self.outputs.remove(self.outputs.values()[-1]) # Module/DotProduct/ToAxisAngle - # Remove X, Y, Z, W - for i in range(4): - if len(self.outputs) > 1: - self.outputs.remove(self.outputs.values()[-1]) - else: - break - if (self.property0 == 'Module'): - self.add_output('NodeSocketFloat', 'Module') # Module - if (self.property0 == 'DotProduct'): - self.add_output('NodeSocketFloat', 'Scalar') # DotProduct - if (self.property0 == 'ToAxisAngle'): - self.add_output('NodeSocketFloat', 'To Axis Angle') # ToAxisAngle + source_socket = None + + + self.inputs.new(newclass, newname) + if default_value != None: + self.inputs[-1].default_value = default_value + self.inputs.move(len(self.inputs)-1, socket_number) + if source_socket is not None: + self.id_data.links.new(source_socket, self.inputs[socket_number]) + + def ensure_output_socket(self, socket_number, newclass, newname): + sink_sockets = [] + while len(self.outputs) < socket_number: + self.outputs.new('NodeSocketFloat', 'BOGUS') + if len(self.outputs) > socket_number: + for link in self.inputs[socket_number].links: + sink_sockets.append(link.to_socket) + self.inputs.remove(self.inputs[socket_number]) - property1: BoolProperty(name='Separator Out', default=False, set=set_bool, get=get_bool) + self.inputs.new(newclass, newname) + self.inputs.move(len(self.inputs)-1, socket_number) + for socket in sink_sockets: + self.id_data.links.new(self.inputs[socket_number], socket) @staticmethod def get_enum_id_value(obj, prop_name, value): @@ -75,62 +77,40 @@ class QuaternionMathNode(ArmLogicTreeNode): # Checking the selection of another operation select_current = self.get_enum_id_value(self, 'property0', value) select_prev = self.property0 - if select_prev != select_current: - # Remove - count = 0 - if ((select_prev == 'Add') or (select_prev == 'Subtract') or (select_prev == 'Multiply') or (select_prev == 'DotProduct')) and ((select_current == 'Add') or (select_current == 'Subtract') or (select_current == 'Multiply') or (select_current == 'DotProduct')) or (((select_current == 'Lerp') or (select_current == 'Slerp')) and ((select_prev == 'Lerp') or (select_prev == 'Slerp'))): - count = len(self.inputs) - while (len(self.inputs) > count): - self.inputs.remove(self.inputs.values()[-1]) - if (select_prev == 'DotProduct') or (select_prev == 'ToAxisAngle') or (select_prev == 'Module'): - self.outputs.remove(self.outputs.values()[-1]) - - # Many arguments: Add, Subtract, DotProduct, Multiply, MultiplyFloat - if (self.get_count_in(select_current) == 0): - if (select_current == "MultiplyFloats"): - self.add_input('NodeSocketVector', 'Quaternion ' + str(len(self.inputs))) - self.add_input('NodeSocketFloat', 'Value ' + str(len(self.inputs))) - else: - while (len(self.inputs) < 2): - self.add_input('NodeSocketVector', 'Quaternion ' + str(len(self.inputs))) - if (select_current == 'DotProduct'): - self.add_output('NodeSocketFloat', 'Scalar') - - # 3 arguments: Lerp, Slerp, FromAxisAngle, FromEuler - if (self.get_count_in(select_current) == 3): - if (select_current == 'Lerp') or (select_current == 'Slerp'): - while (len(self.inputs) < 3): - self.add_input('NodeSocketVector', 'From') - self.add_input('NodeSocketVector', 'To') - self.add_input('NodeSocketFloat', 'T') - if (select_current == 'FromAxisAngle'): - self.add_input('NodeSocketVector', 'Quaternion') - self.add_input('NodeSocketVector', 'Axis') - self.add_input('NodeSocketFloat', 'Angle') - if (select_current == 'FromEuler'): - self.add_input('NodeSocketFloat', 'X') - self.add_input('NodeSocketFloat', 'Y') - self.add_input('NodeSocketFloat', 'Z') - - # 2 arguments: FromTo, FromMat, FromRotationMat, ToAxisAngle - if (self.get_count_in(select_current) == 2): - if (select_current == 'FromTo'): - self.add_input('NodeSocketVector', 'Vector ' + str(len(self.inputs))) - self.add_input('NodeSocketVector', 'Vector ' + str(len(self.inputs))) - if (select_current == 'FromMat') or (select_current == 'FromRotationMat'): - self.add_input('NodeSocketVector', 'Quaternion') - self.add_input('NodeSocketShader', 'Matrix') - if (select_current == 'ToAxisAngle'): - self.add_input('NodeSocketVector', 'Quaternion') - self.add_input('NodeSocketVector', 'Axis') - self.add_output('NodeSocketFloat', 'Angle') - # 1 argument: Module, Normalize, GetEuler - if (self.get_count_in(select_current) == 1): - self.add_input('NodeSocketVector', 'Quaternion') - if (select_current == 'Module'): - self.add_output('NodeSocketFloat', 'Module') + + if select_current in ('Add','Subtract','Multiply','DotProduct') \ + and select_prev in ('Add','Subtract','Multiply','DotProduct'): + pass # same as select_current==select_prev for the sockets + elif select_prev != select_current: + if select_current in ('Add','Subtract','Multiply','DotProduct'): + for i in range( max(len(self.inputs)//2 ,2) ): + self.ensure_input_socket(2*i, 'NodeSocketVector', 'Quaternion %d XYZ'%i) + self.ensure_input_socket(2*i+1, 'NodeSocketFloat', 'Quaternion %d W'%i, default_value=1.0) + if len(self.inputs)%1: + self.inputs.remove(self.inputs[len(self.inputs)-1]) + elif select_current == 'MultiplyFloats': + self.ensure_input_socket(0, 'NodeSocketVector', 'Quaternion XYZ') + self.ensure_input_socket(1, 'NodeSocketFloat', 'Quaternion W', default_value=1.0) + for i in range( max(len(self.inputs)-2 ,1) ): + self.ensure_input_socket(i+2, 'NodeSocketFloat', 'Value %d'%i) + elif select_current in ('Module', 'Normalize'): + self.ensure_input_socket(0, 'NodeSocketVector', 'Quaternion XYZ') + self.ensure_input_socket(1, 'NodeSocketFloat', 'Quaternion W', default_value=1.0) + while len(self.inputs)>2: + self.inputs.remove(self.inputs[2]) + else: + raise ValueError('Internal code of LNQuaternionMathNode failed to deal correctly with math operation "%s". Please report this to the developers.' %select_current) + + if select_current in ('Add','Subtract','Multiply','MultiplyFloats','Normalize'): + self.outputs[0].name = 'XYZ Out' + self.outputs[1].name = 'W Out' + else: + self.outputs[0].name = '[unused]' + self.outputs[1].name = 'Value Out' + self['property0'] = value + self['property0_proxy'] = value property0: EnumProperty( items = [('Add', 'Add', 'Add'), @@ -139,30 +119,35 @@ class QuaternionMathNode(ArmLogicTreeNode): ('Multiply', 'Multiply', 'Multiply'), ('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'), ('Module', 'Module', 'Module'), - ('Normalize', 'Normalize', 'Normalize'), - ('Lerp', 'Lerp', 'Linearly interpolate'), - ('Slerp', 'Slerp', 'Spherical linear interpolation'), - ('FromTo', 'From To', 'From To'), - ('FromMat', 'From Mat', 'From Mat'), - ('FromRotationMat', 'From Rotation Mat', 'From Rotation Mat'), - ('ToAxisAngle', 'To Axis Angle', 'To Axis Angle'), - ('FromAxisAngle', 'From Axis Angle', 'From Axis Angle'), - ('FromEuler', 'From Euler', 'From Euler'), - ('GetEuler', 'To Euler', 'To Euler')], - name='', default='Add', set=set_enum, get=get_enum) + ('Normalize', 'Normalize', 'Normalize'), #], + # NOTE: the unused parts need to exist to be read from an old version from the node. + # this is so dumb… + ('Lerp', 'DO NOT USE',''), + ('Slerp', 'DO NOT USE',''), + ('FromTo', 'DO NOT USE',''), + ('FromMat', 'DO NOT USE',''), + ('FromRotationMat', 'DO NOT USE',''), + ('ToAxisAngle', 'DO NOT USE',''), + ('FromAxisAngle', 'DO NOT USE',''), + ('FromEuler', 'DO NOT USE',''), + ('GetEuler', 'DO NOT USE','')], + name='', default='Add') #, set=set_enum, get=get_enum) def __init__(self): + super(QuaternionMathNode, self).__init__() array_nodes[str(id(self))] = self def init(self, context): super(QuaternionMathNode, self).init(context) - self.add_input('NodeSocketVector', 'Quaternion 0', default_value=[0.0, 0.0, 0.0]) - self.add_input('NodeSocketVector', 'Quaternion 1', default_value=[0.0, 0.0, 0.0]) - self.add_output('NodeSocketVector', 'Result') + self.add_input('NodeSocketVector', 'Quaternion 0 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('NodeSocketFloat', 'Quaternion 0 W', default_value=1) + self.add_input('NodeSocketVector', 'Quaternion 1 XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_input('NodeSocketFloat', 'Quaternion 1 W', default_value=1) + self.add_output('NodeSocketVector', 'Result XYZ', default_value=[0.0, 0.0, 0.0]) + self.add_output('NodeSocketFloat', 'Result W', default_value=1) def draw_buttons(self, context, layout): - layout.prop(self, 'property1') # Separator Out - layout.prop(self, 'property0') # Operation + layout.prop(self, 'property0_proxy') # Operation # Buttons if (self.get_count_in(self.property0) == 0): row = layout.row(align=True) @@ -170,15 +155,201 @@ class QuaternionMathNode(ArmLogicTreeNode): op = column.operator('arm.node_add_input', text='Add Value', icon='PLUS', emboss=True) op.node_index = str(id(self)) if (self.property0 == 'Add') or (self.property0 == 'Subtract') or (self.property0 == 'Multiply') or (self.property0 == 'DotProduct'): - op.name_format = 'Quaternion {0}' + op.name_format = 'Quaternion {0} XYZ;Quaternion {0} W' else: op.name_format = 'Value {0}' if (self.property0 == "MultiplyFloats"): op.socket_type = 'NodeSocketFloat' else: - op.socket_type = 'NodeSocketVector' + op.socket_type = 'NodeSocketVector;NodeSocketFloat' column = row.column(align=True) op = column.operator('arm.node_remove_input', text='', icon='X', emboss=True) op.node_index = str(id(self)) + if self.property0 != "MultiplyFloats": + op.count = 2 + op.min_inputs = 4 + else: + op.min_inputs = 2 if len(self.inputs) == 2: column.enabled = False + + + + + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + ret=[] + if self.property0 == 'GetEuler': + newself = node_tree.nodes.new('LNSeparateRotationNode') + ret.append(newself) + newself.property0='EulerAngles' + newself.property2='XZY' + newself.property1='Rad' + + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_socket, newself.inputs[0]) + elif self.property0 == 'ToEuler': + newself = node_tree.nodes.new('LNRotationNode') + ret.append(newself) + preconv = node_tree.nodes.new('LNVectorNode') + ret.append(preconv) + newself.property0='EulerAngles' + newself.property2='XZY' + newself.property1='Rad' + node_tree.links.new(preconv.outputs[0], newself.inputs[0]) + + preconv.inputs[0].default_value = self.inputs[0].default_value + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[0]) + preconv.inputs[1].default_value = self.inputs[1].default_value + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[1]) + preconv.inputs[2].default_value = self.inputs[2].default_value + for link in self.inputs[2].links: # 0 or 1 + node_tree.links.new(link.from_socket, preconv.inputs[2]) + elif self.property0 == 'ToAxisAngle': + newself = node_tree.nodes.new('LNSeparateRotationNode') + ret.append(newself) + newself.property0='AxisAngle' + newself.property1='Rad' + + for link in self.inputs[0].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[0]) + elif self.property0 == 'FromAxisAngle': + newself = node_tree.nodes.new('LNRotationNode') + ret.append(newself) + newself.property0='AxisAngle' + newself.property1='Rad' + + newself.inputs[0].default_value = self.inputs[1].default_value + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[0]) + newself.inputs[1].default_value = self.inputs[2].default_value + for link in self.inputs[2].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[1]) + elif self.property0 in ('FromMat','FromRotationMat'): + newself = node_tree.nodes.new('LNSeparateTransformNode') + ret.append(newself) + for link in self.inputs[1].links: # 0 or 1 + node_tree.links.new(link.from_node, newself.inputs[0]) + + elif self.property0 in ('Lerp','Slerp','FromTo'): + newself = node_tree.nodes.new('LNRotationMathNode') + ret.append(newself) + newself.property0 = self.property0 + for in1, in2 in zip(self.inputs, newself.inputs): + if in1.bl_idname in ('NodeSocketFloat', 'NodeSocketVector'): + in2.default_value = in1.default_value + for link in in1.links: + node_tree.links.new(link.from_socket, in2) + + else: + newself = node_tree.nodes.new('LNQuaternionMathNode') + ret.append(newself) + newself.property0 = self.property0 + + # convert the inputs… this is going to be hard lmao. + i_in_1 = 0 + i_in_2 = 0 + while i_in_1 < len(self.inputs): + in1 = self.inputs[i_in_1] + if in1.bl_idname == 'NodeSocketVector': + # quaternion input: now two sockets, not one. + convnode = node_tree.nodes.new('LNSeparateRotationNode') + convnode.property0 = 'Quaternion' + ret.append(convnode) + node_tree.links.new(convnode.outputs[0], newself.inputs[i_in_2]) + node_tree.links.new(convnode.outputs[1], newself.inputs[i_in_2+1]) + for link in in1.links: + node_tree.links.new(link.from_socket, convnode.inputs[0]) + i_in_2 +=1 + i_in_1 +=2 + elif in1.bl_idname == 'NodeSocketfloat': + for link in in1.links: + node_tree.links.new(link.from_socket, newself.inputs[i_in_2]) + else: + raise ValueError('get_replacement_node() for is not LNQuaternionMathNode V1->V2 is not prepared to deal with an input socket of type %s. This is a bug to report to the developers' %in1.bl_idname) + + # #### now that the input has been dealt with, let's deal with the output. + + if self.property0 in ('FromEuler','FromMat','FromRotationMat','FromAxisAngle','Lerp','Slerp','FromTo'): + # the new self returns a rotation + for link in self.outputs[0].links: + node_tree.links.new(newself.outputs[0], link.to_socket) + elif self.property0 in ('DotProduct','Module'): + # new self returns a float + for link in self.outputs[1 + 4*int(self.property1)].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + elif self.property0 in ('GetEuler', 'ToAxisAngle'): + # new self returns misc. + for link in self.outputs[0].links: + node_tree.links.new(newself.outputs[0], link.to_socket) + if self.property0 == 'ToAxisAngle': + for link in self.outputs[1 + 4*int(self.property1)].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + if self.property1: + xlinks = self.outputs[1].links + ylinks = self.outputs[2].links + zlinks = self.outputs[3].links + if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0: + conv = node_tree.nodes.new('LNSeparateVectorNode') + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + for link in xlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + for link in ylinks: + node_tree.links.new(conv.outputs[1], link.to_socket) + for link in zlinks: + node_tree.links.new(conv.outputs[2], link.to_socket) + else: + # new self returns a proper quaternion XYZ/W + outlinks = self.outputs[0].links + if len(outlinks)>0: + conv = node_tree.nodes.new('LNRotationNode') + conv.property0='Quaternion' + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + node_tree.links.new(newself.outputs[1], conv.inputs[1]) + for link in outlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + if self.property1: + for link in self.outputs[4].links: # for W + node_tree.links.new(newself.outputs[1], link.to_socket) + xlinks = self.outputs[1].links + ylinks = self.outputs[2].links + zlinks = self.outputs[3].links + if len(xlinks)>0 or len(ylinks)>0 or len(zlinks)>0: + conv = node_tree.nodes.new('LNSeparateVectorNode') + ret.append(conv) + node_tree.links.new(newself.outputs[0], conv.inputs[0]) + for link in xlinks: + node_tree.links.new(conv.outputs[0], link.to_socket) + for link in ylinks: + node_tree.links.new(conv.outputs[1], link.to_socket) + for link in zlinks: + node_tree.links.new(conv.outputs[2], link.to_socket) + + return ret + + + # note: keep property1, so that it is actually readable for node conversion. + property1: BoolProperty(name='DEPRECATED', default=False) + + # this is the version of property0 that is shown in the interface, + # even though the real property0 is the one used elsewhere. + # NOTE FOR FUTURE MAINTAINERS: the value of this proxy property does **not** matter, only the value of property0 does. + property0_proxy: EnumProperty( + items = [('Add', 'Add', 'Add'), + ('Subtract', 'Subtract', 'Subtract'), + ('DotProduct', 'Dot Product', 'Dot Product'), + ('Multiply', 'Multiply', 'Multiply'), + ('MultiplyFloats', 'Multiply (Floats)', 'Multiply (Floats)'), + ('Module', 'Module', 'Module'), + ('Normalize', 'Normalize', 'Normalize')], + name='', default='Add', set=set_enum, get=get_enum) diff --git a/blender/arm/logicnode/math/LN_rotation_math.py b/blender/arm/logicnode/math/LN_rotation_math.py index 50d2abd3..dcf59883 100644 --- a/blender/arm/logicnode/math/LN_rotation_math.py +++ b/blender/arm/logicnode/math/LN_rotation_math.py @@ -4,6 +4,7 @@ class QuaternionMathNode(ArmLogicTreeNode): """Mathematical operations on rotations.""" bl_idname = 'LNRotationMathNode' bl_label = 'Rotation Math' + bl_description = 'Mathematical operations that can be performed on rotations, no matter their internal representation' arm_section = 'quaternions' arm_version = 1 @@ -97,7 +98,7 @@ class QuaternionMathNode(ArmLogicTreeNode): property0: EnumProperty( items = [('Compose', 'Compose (multiply)', 'compose (multiply) two rotations. Note that order of the composition matters.'), ('Amplify', 'Amplify (multiply by float)', 'Amplify or diminish the effect of a rotation'), - ('Normalize', 'Normalize', 'Normalize'), + #('Normalize', 'Normalize', 'Normalize'), ('Inverse', 'Get Inverse', 'from r, get the rotation r2 so that " r×r2=r2×r= " '), ('Lerp', 'Lerp', 'Linearly interpolation'), ('Slerp', 'Slerp', 'Spherical linear interpolation'), diff --git a/blender/arm/logicnode/math/LN_separate_quaternion.py b/blender/arm/logicnode/math/LN_separate_quaternion.py index fcd9abf6..21f18946 100644 --- a/blender/arm/logicnode/math/LN_separate_quaternion.py +++ b/blender/arm/logicnode/math/LN_separate_quaternion.py @@ -1,11 +1,14 @@ from arm.logicnode.arm_nodes import * +@deprecated(message='Do not use quaternion sockets') class SeparateQuaternionNode(ArmLogicTreeNode): """Splits the given quaternion into X, Y, Z and W.""" bl_idname = 'LNSeparateQuaternionNode' - bl_label = "Separate Quaternion" + bl_label = "Separate Quaternion (do not use: quaternions sockets have been phased out entirely)" + bl_description = "Separate a quaternion object (transported through a vector socket) into its four compoents." arm_section = 'quaternions' - arm_version = 1 + arm_version = 2 # deprecate + def init(self, context): super(SeparateQuaternionNode, self).init(context) @@ -15,3 +18,29 @@ class SeparateQuaternionNode(ArmLogicTreeNode): self.add_output('NodeSocketFloat', 'Y') self.add_output('NodeSocketFloat', 'Z') self.add_output('NodeSocketFloat', 'W') + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + # transition from version 1 to version 2[deprecated] + newself = node_tree.nodes.new('LNSeparateRotationNode') + separator = node_tree.nodes.new('LNSeparateVectorNode') + + newself.property0 = 'Quaternion' + newself.property1 = 'Rad' # bogus + newself.property2 = 'XYZ' # bogus + + for link in self.inputs[0].links: + node_tree.links.new(link.from_socket, newself.inputs[0]) + node_tree.links.new(newself.outputs[0], separator.inputs[0]) + for link in self.outputs[0].links: + node_tree.links.new(separator.outputs[0], link.to_socket) + for link in self.outputs[1].links: + node_tree.links.new(separator.outputs[1], link.to_socket) + for link in self.outputs[2].links: + node_tree.links.new(separator.outputs[2], link.to_socket) + for link in self.outputs[3].links: + node_tree.links.new(newself.outputs[1], link.to_socket) + return [newself, separator] diff --git a/blender/arm/logicnode/transform/LN_get_object_rotation.py b/blender/arm/logicnode/transform/LN_get_object_rotation.py index 9ba23553..86cce2f4 100644 --- a/blender/arm/logicnode/transform/LN_get_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_get_object_rotation.py @@ -5,7 +5,7 @@ class GetRotationNode(ArmLogicTreeNode): bl_idname = 'LNGetRotationNode' bl_label = 'Get Object Rotation' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 def init(self, context): super(GetRotationNode, self).init(context) @@ -19,3 +19,72 @@ class GetRotationNode(ArmLogicTreeNode): items = [('Local', 'Local', 'Local'), ('Global', 'Global', 'Global')], name='', default='Local') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + newself = self.id_data.nodes.new('LNGetRotationNode') + newself.property0 = 'Local' + newnodes = [newself] + + if len(self.outputs[0].links)>0: + # euler (radians) needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "EulerAngles" + converter.property1 = "Rad" + converter.property2 = "XZY" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[0].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + if len(self.outputs[4].links)>0 or len(self.outputs[5].links)>0: + # quaternion needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "Quaternion" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[4].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + for link in self.outputs[5].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + + if len(self.outputs[1].links)>0 or len(self.outputs[2].links)>0 or len(self.outputs[3].links)>0: + # axis/angle needed + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "AxisAngle" + converter.property1 = "Rad" + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[1].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + if len(self.outputs[3].links)==0 and len(self.outputs[2].links)==0: + pass + elif len(self.outputs[3].links)==0: + for link in self.outputs[2].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + elif len(self.outputs[2].links)==0: + converter.property1 = 'Deg' + for link in self.outputs[3].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + else: + for link in self.outputs[2].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + converter = self.id_data.nodes.new('LNSeparateRotationNode') + converter.property0 = "AxisAngle" + converter.property1 = "Deg" + converter.property2 = "XYZ" # bogus + newnodes.append(converter) + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + for link in self.outputs[3].links: + self.id_data.links.new(converter.outputs[1], link.to_socket) + + return newnodes diff --git a/blender/arm/logicnode/transform/LN_look_at.py b/blender/arm/logicnode/transform/LN_look_at.py index 87fdf03f..669d2f8a 100644 --- a/blender/arm/logicnode/transform/LN_look_at.py +++ b/blender/arm/logicnode/transform/LN_look_at.py @@ -5,7 +5,7 @@ class LookAtNode(ArmLogicTreeNode): bl_idname = 'LNLookAtNode' bl_label = 'Look At' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 property0: EnumProperty( items = [('X', ' X', 'X'), @@ -25,3 +25,23 @@ class LookAtNode(ArmLogicTreeNode): def draw_buttons(self, context, layout): layout.prop(self, 'property0') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNLookAtNode') + converter = self.id_data.nodes.new('LNSeparateRotationNode') + self.id_data.links.new(newself.outputs[0], converter.inputs[0]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + for link in self.outputs[0].links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + return [newself, converter] diff --git a/blender/arm/logicnode/transform/LN_rotate_object.py b/blender/arm/logicnode/transform/LN_rotate_object.py index c49f68f0..b43cafc0 100644 --- a/blender/arm/logicnode/transform/LN_rotate_object.py +++ b/blender/arm/logicnode/transform/LN_rotate_object.py @@ -1,11 +1,12 @@ from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation class RotateObjectNode(ArmLogicTreeNode): """Rotates the given object.""" bl_idname = 'LNRotateObjectNode' bl_label = 'Rotate Object' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 def init(self, context): super().init(context) @@ -20,5 +21,59 @@ class RotateObjectNode(ArmLogicTreeNode): property0: EnumProperty( items = [('Local', 'Local F.O.R.', 'Frame of reference oriented with the object'), - ('Global', 'Global/Parent F.O.R.', 'Frame of reference oriented with the object\'s parent or the world')], + ('Global', 'Global/Parent F.O.R.', + 'Frame of reference oriented with the object\'s parent or the world')], name='', default='Local') + + + + + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + newself = self.id_data.nodes.new('LNRotateObjectNode') + inputnode = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(inputnode.outputs[0], newself.inputs[2]) + newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw + inputnode.inputs[0].default_value = self.inputs[2].default_value + inputnode.inputs[1].default_value = self.inputs[3].default_value + + if len(self.inputs[0].links) >0: + self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0]) + if len(self.inputs[1].links) >0: + self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1]) + if len(self.inputs[2].links) >0: + self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0]) + if len(self.inputs[3].links) >0: + self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1]) + + # first, convert the default value + if self.property0 == 'Quaternion': + inputnode.property0 = 'Quaternion' + elif self.property0 == 'Euler Angles': + inputnode.property0 = 'EulerAngles' + inputnode.property1 = 'Rad' + inputnode.property2 = 'XZY' # legacy order + else: # starts with "Angle Axies" + inputnode.property0 = 'AxisAngle' + if 'Degrees' in self.property0: + inputnode.property1 = 'Deg' + else: + inputnode.property1 = 'Rad' + quat = Rotation.convert_to_quaternion( + self.inputs[2].default_value, + self.inputs[3].default_value, + inputnode.property0, + inputnode.property1, + inputnode.property2 + ) + newself.inputs[2].default_value_raw = quat + return [newself, inputnode] diff --git a/blender/arm/logicnode/transform/LN_separate_rotation.py b/blender/arm/logicnode/transform/LN_separate_rotation.py index 0111d5a0..2085951a 100644 --- a/blender/arm/logicnode/transform/LN_separate_rotation.py +++ b/blender/arm/logicnode/transform/LN_separate_rotation.py @@ -40,7 +40,7 @@ class SeparateRotationNode(ArmLogicTreeNode): property0: EnumProperty( items = [('EulerAngles', 'Euler Angles', 'Euler Angles'), ('AxisAngle', 'Axis/Angle', 'Axis/Angle'), - ('Quat', 'Quaternion', 'Quaternion')], + ('Quaternion', 'Quaternion', 'Quaternion')], name='', default='EulerAngles', update=on_property_update) diff --git a/blender/arm/logicnode/transform/LN_separate_transform.py b/blender/arm/logicnode/transform/LN_separate_transform.py index 6ab0ade8..0a96747b 100644 --- a/blender/arm/logicnode/transform/LN_separate_transform.py +++ b/blender/arm/logicnode/transform/LN_separate_transform.py @@ -4,7 +4,7 @@ class SeparateTransformNode(ArmLogicTreeNode): """Separates the transform of the given object.""" bl_idname = 'LNSeparateTransformNode' bl_label = 'Separate Transform' - arm_version = 1 + arm_version = 2 def init(self, context): super(SeparateTransformNode, self).init(context) @@ -13,3 +13,34 @@ class SeparateTransformNode(ArmLogicTreeNode): self.add_output('NodeSocketVector', 'Location') self.add_output('ArmNodeSocketRotation', 'Rotation') self.add_output('NodeSocketVector', 'Scale') + + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNSeparateTransformNode') + for link in self.outputs[0].links: + self.id_data.links.new(newself.outputs[0], link.to_socket) + for link in self.outputs[2].links: + self.id_data.links.new(newself.outputs[2], link.to_socket) + for link in self.inputs[0].links: + self.id_data.links.new(link.from_socket, newself.inputs[0]) + + ret = [newself] + rot_links = self.outputs[1].links + if len(rot_links) >0: + converter = self.id_data.nodes.new('LNSeparateRotationNode') + ret.append(converter) + self.id_data.links.new(newself.outputs[1], converter.inputs[0]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + for link in rot_links: + self.id_data.links.new(converter.outputs[0], link.to_socket) + + return ret diff --git a/blender/arm/logicnode/transform/LN_set_object_rotation.py b/blender/arm/logicnode/transform/LN_set_object_rotation.py index 555e3229..1223cdf8 100644 --- a/blender/arm/logicnode/transform/LN_set_object_rotation.py +++ b/blender/arm/logicnode/transform/LN_set_object_rotation.py @@ -1,11 +1,12 @@ from arm.logicnode.arm_nodes import * +from arm.logicnode.arm_sockets import ArmRotationSocket as Rotation class SetRotationNode(ArmLogicTreeNode): """Sets the rotation of the given object.""" bl_idname = 'LNSetRotationNode' bl_label = 'Set Object Rotation' arm_section = 'rotation' - arm_version = 1 + arm_version = 2 def init(self, context): @@ -16,10 +17,61 @@ class SetRotationNode(ArmLogicTreeNode): self.add_output('ArmNodeSocketAction', 'Out') -# def draw_buttons(self, context, layout): -# layout.prop(self, 'property0') -# property0: EnumProperty( -# items = [('Local', 'Local', 'Local'), -# ('Global', 'Global', 'Global')], -# name='', default='Local') + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + + + newself = self.id_data.nodes.new('LNRotateObjectNode') + inputnode = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(inputnode.outputs[0], newself.inputs[2]) + newself.inputs[1].default_value_raw = self.inputs[1].default_value_raw + inputnode.inputs[0].default_value = self.inputs[2].default_value + inputnode.inputs[1].default_value = self.inputs[3].default_value + + if len(self.inputs[0].links) >0: + self.id_data.links.new(self.inputs[0].links[0].from_socket, newself.inputs[0]) + if len(self.inputs[1].links) >0: + self.id_data.links.new(self.inputs[1].links[0].from_socket, newself.inputs[1]) + if len(self.inputs[2].links) >0: + self.id_data.links.new(self.inputs[2].links[0].from_socket, inputnode.inputs[0]) + if len(self.inputs[3].links) >0: + self.id_data.links.new(self.inputs[3].links[0].from_socket, inputnode.inputs[1]) + + # first, convert the default value + if self.property0 == 'Quaternion': + inputnode.property0 = 'Quaternion' + elif self.property0 == 'Euler Angles': + inputnode.property0 = 'EulerAngles' + inputnode.property1 = 'Rad' + inputnode.property2 = 'XZY' # legacy order + elif self.property0.startswith("Angle Axies "): + inputnode.property0 = 'AxisAngle' + if 'Degrees' in self.property0: + inputnode.property1 = 'Deg' + else: + inputnode.property1 = 'Rad' + else: + raise ValueError('nonsensical value {:s} for property0 in SetObjectRotationNode/V1. please report this to the devs.'.format(self.property0)) + quat = Rotation.convert_to_quaternion( + self.inputs[2].default_value, + self.inputs[3].default_value, + inputnode.property0, + inputnode.property1, + inputnode.property2 + ) + newself.inputs[2].default_value_raw = quat + return [newself, inputnode] + + # note: this is unused, but kept here so that the 'property0' field can be read during node replacement + property0: EnumProperty( + items = [('Euler Angles', '',''), + ('Angle Axies (Radians)', '', ''), + ('Angle Axies (Degrees)', '', ''), + ('Quaternion', '', '')], + name='', default='Euler Angles') diff --git a/blender/arm/logicnode/transform/LN_transform.py b/blender/arm/logicnode/transform/LN_transform.py index 1087caba..a4302b97 100644 --- a/blender/arm/logicnode/transform/LN_transform.py +++ b/blender/arm/logicnode/transform/LN_transform.py @@ -4,7 +4,7 @@ class TransformNode(ArmLogicTreeNode): """Stores the location, rotation and scale values as a transform.""" bl_idname = 'LNTransformNode' bl_label = 'Transform' - arm_version = 1 + arm_version = 2 def init(self, context): super(TransformNode, self).init(context) @@ -13,3 +13,34 @@ class TransformNode(ArmLogicTreeNode): self.add_input('NodeSocketVector', 'Scale', default_value=[1.0, 1.0, 1.0]) self.add_output('NodeSocketShader', 'Transform', is_var=True) + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + + # transition from version 1 to version 2: make rotations their own sockets + # this transition is a mess, I know. + newself = self.id_data.nodes.new('LNTransformNode') + ret = [newself] + + for link in self.inputs[0].links: + self.id_data.links.new(link.from_socket, newself.inputs[0]) + for link in self.inputs[2].links: + self.id_data.links.new(link.from_socket, newself.inputs[2]) + for link in self.outputs[0].links: + self.id_data.links.new(newself.outputs[0], link.to_socket) + + links_rot = self.inputs[1].links + if len(links_rot) > 0: + converter = self.id_data.nodes.new('LNRotationNode') + self.id_data.links.new(converter.outputs[0], newself.inputs[1]) + converter.property0 = 'EulerAngles' + converter.property1 = 'Rad' + converter.property2 = 'XZY' + ret.append(converter) + for link in links_rot: + self.id_data.links.new(link.from_socket, converter.inputs[0]) + + return ret diff --git a/blender/arm/logicnode/variable/LN_quaternion.py b/blender/arm/logicnode/variable/LN_quaternion.py index 8f91465d..25a23a8f 100644 --- a/blender/arm/logicnode/variable/LN_quaternion.py +++ b/blender/arm/logicnode/variable/LN_quaternion.py @@ -1,11 +1,14 @@ from arm.logicnode.arm_nodes import * +from mathutils import Vector +@deprecated(message='Do not use quaternion sockets') class QuaternionNode(ArmLogicTreeNode): """TO DO.""" bl_idname = 'LNQuaternionNode' bl_label = 'Quaternion' + bl_label = 'Create a quaternion variable (transported through a vector socket)' arm_section = 'quaternions' - arm_version = 1 + arm_version = 2 # deprecate def init(self, context): super(QuaternionNode, self).init(context) @@ -17,3 +20,57 @@ class QuaternionNode(ArmLogicTreeNode): self.add_output('NodeSocketVector', 'Quaternion') self.add_output('NodeSocketVector', 'XYZ') self.add_output('NodeSocketFloat', 'W') + + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.arm_version not in (0, 1): + raise LookupError() + + # transition from version 1 to version 2[deprecated] + + + newnodes = [] + + rawlinks = self.outputs[0].links + xyzlinks = self.outputs[1].links + wlinks = self.outputs[2].links + if len(rawlinks)>0 or len(xyzlinks)>0: + xyzcomb = node_tree.nodes.new('LNVectorNode') + newnodes.append(xyzcomb) + + xyzcomb.inputs[0].default_value = self.inputs[0].default_value + xyzcomb.inputs[1].default_value = self.inputs[1].default_value + xyzcomb.inputs[2].default_value = self.inputs[2].default_value + for link in self.inputs[0].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[0]) + for link in self.inputs[1].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[1]) + for link in self.inputs[2].links: + node_tree.links.new(link.from_socket, xyzcomb.inputs[2]) + + for link in xyzlinks: + node_tree.links.new(xyzcomb.outputs[0], link.to_socket) + if len(rawlinks)>0: + rotnode = node_tree.nodes.new('LNRotationNode') + newnodes.append(rotnode) + rotnode.property0 = 'Quaternion' + rotnode.inputs[0].default_value = Vector( + (self.inputs[0].default_value, + self.inputs[1].default_value, + self.inputs[2].default_value)) + rotnode.inputs[1].default_value = self.inputs[3].default_value + node_tree.links.new(xyzcomb.outputs[0], rotnode.inputs[0]) + for link in self.inputs[3].links: # 0 or 1 + node_tree.links.new(link.from_socket, rotnode.inputs[1]) + for link in rawlinks: + node_tree.links.new(rotnode.outputs[0], link.to_socket) + + if len(self.inputs[3].links)>0: + fromval = self.inputs[3].links[0].from_socket + for link in self.outputs[2].links: + node_tree.links.new(fromval, link.to_socket) + else: + for link in self.outputs[2].links: + link.to_socket.default_value = self.inputs[3].default_value + + return newnodes diff --git a/blender/arm/logicnode/variable/LN_rotation.py b/blender/arm/logicnode/variable/LN_rotation.py index ac832cea..a5ee0a2d 100644 --- a/blender/arm/logicnode/variable/LN_rotation.py +++ b/blender/arm/logicnode/variable/LN_rotation.py @@ -4,6 +4,7 @@ class RotationNode(ArmLogicTreeNode): """A rotation, created from one of its possible mathematical representations""" bl_idname = 'LNRotationNode' bl_label = 'Rotation' + bl_description = 'Create a Rotation object, describing the difference between two orientations (internally represented as a quaternion for efficiency)' #arm_section = 'rotation' arm_version = 1