diff --git a/leenkx/Sources/iron/math/Quat.hx b/leenkx/Sources/iron/math/Quat.hx index 6a8cce9..0d059e2 100644 --- a/leenkx/Sources/iron/math/Quat.hx +++ b/leenkx/Sources/iron/math/Quat.hx @@ -399,17 +399,33 @@ class Quat { @return This quaternion. **/ public inline function fromEulerOrdered(e: Vec4, order: String): Quat { - var c1 = Math.cos(e.x / 2); - var c2 = Math.cos(e.y / 2); - var c3 = Math.cos(e.z / 2); - var s1 = Math.sin(e.x / 2); - var s2 = Math.sin(e.y / 2); - var s3 = Math.sin(e.z / 2); - + + var mappedAngles = new Vec4(); + switch (order) { + case "XYZ": + mappedAngles.set(e.x, e.y, e.z); + case "XZY": + mappedAngles.set(e.x, e.z, e.y); + case "YXZ": + mappedAngles.set(e.y, e.x, e.z); + case "YZX": + mappedAngles.set(e.y, e.z, e.x); + case "ZXY": + mappedAngles.set(e.z, e.x, e.y); + case "ZYX": + mappedAngles.set(e.z, e.y, e.x); + } + var c1 = Math.cos(mappedAngles.x / 2); + var c2 = Math.cos(mappedAngles.y / 2); + var c3 = Math.cos(mappedAngles.z / 2); + var s1 = Math.sin(mappedAngles.x / 2); + var s2 = Math.sin(mappedAngles.y / 2); + var s3 = Math.sin(mappedAngles.z / 2); var qx = new Quat(s1, 0, 0, c1); var qy = new Quat(0, s2, 0, c2); var qz = new Quat(0, 0, s3, c3); + // Original multiplication sequence (implements reverse of 'order') if (order.charAt(2) == 'X') this.setFrom(qx); else if (order.charAt(2) == 'Y') @@ -429,7 +445,7 @@ class Quat { else this.mult(qz); - // TO DO quick fix doesnt make sense. + // TO DO quick fix somethings wrong.. this.x = -this.x; this.y = -this.y; this.z = -this.z; diff --git a/leenkx/Sources/leenkx/logicnode/RotationNode.hx b/leenkx/Sources/leenkx/logicnode/RotationNode.hx index ed80dea..57374ba 100644 --- a/leenkx/Sources/leenkx/logicnode/RotationNode.hx +++ b/leenkx/Sources/leenkx/logicnode/RotationNode.hx @@ -37,6 +37,7 @@ class RotationNode extends LogicNode { value.y = vect.y; value.z = vect.z; value.w = inputs[1].get(); + value.normalize(); case "AxisAngle": var vec: Vec4 = inputs[0].get(); diff --git a/leenkx/blender/lnx/logicnode/lnx_sockets.py b/leenkx/blender/lnx/logicnode/lnx_sockets.py index 3ceab3e..637266d 100644 --- a/leenkx/blender/lnx/logicnode/lnx_sockets.py +++ b/leenkx/blender/lnx/logicnode/lnx_sockets.py @@ -1,4 +1,4 @@ -from math import pi, cos, sin, sqrt +from math import radians, pi, cos, sin, sqrt from typing import Type import bpy @@ -158,70 +158,90 @@ class LnxRotationSocket(LnxCustomSocket): self.do_update_raw(context) @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 ('Quaternion', 'EulerAngles', 'AxisAngle'), - ``param2`` is in ('Rad','Deg') for both EulerAngles and AxisAngle, - ``param3`` is a len-3 string like "XYZ", for EulerAngles """ - if param1=='Quaternion': - 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: - qx, qy, qz, qw = 0.0,0.0,0.0,1.0 + def convert_to_quaternion(vec3_val, scalar_val, mode, unit, order): + '''Converts Euler or Axis-Angle representation to a Quaternion Vector''' + + if mode == 'Quaternion': + qx, qy, qz = vec3_val[0], vec3_val[1], vec3_val[2] + qw = scalar_val + + ql = sqrt(qx**2 + qy**2 + qz**2 + qw**2) + if abs(ql) < 1E-5: + qx, qy, qz, qw = 0.0, 0.0, 0.0, 1.0 else: qx /= ql qy /= ql qz /= ql qw /= ql - return mathutils.Vector((qx,qy,qz,qw)) + return mathutils.Vector((qx, qy, qz, qw)) - elif param1 == 'AxisAngle': - if param2 == 'Deg': - angle = part2 * pi/180 - else: - angle = part2 - cang, sang = cos(angle/2), sin(angle/2) - x,y,z = part1[0], part1[1], part1[2] - veclen = sqrt(x**2+y**2+z**2) - if veclen<1E-5: - return mathutils.Vector((0.0,0.0,0.0,1.0)) - else: - return mathutils.Vector(( - x/veclen * sang, - y/veclen * sang, - z/veclen * sang, - cang - )) - else: # param1 == 'EulerAngles' - x,y,z = part1[0], part1[1], part1[2] - if param2 == 'Deg': - x *= pi/180 - y *= pi/180 - z *= pi/180 - - euler = mathutils.Euler((x, y, z), param3) - quat = euler.to_quaternion() + elif mode == 'EulerAngles': + x, y, z = vec3_val.to_tuple() + + if unit == 'Deg': + x, y, z = radians(x), radians(y), radians(z) + + angles_ordered = [0.0, 0.0, 0.0] + for i, axis in enumerate(order): + if axis == 'X': + angles_ordered[i] = x + elif axis == 'Y': + angles_ordered[i] = y + elif axis == 'Z': + angles_ordered[i] = z + eul = mathutils.Euler(angles_ordered, order) + quat = eul.to_quaternion() return mathutils.Vector((quat.x, quat.y, quat.z, quat.w)) - + + elif mode == 'AxisAngle': + axis = vec3_val.normalized().to_tuple() + angle = scalar_val + if unit == 'Deg': + angle = radians(angle) + quat = mathutils.Quaternion(axis, angle) + return mathutils.Vector((quat.x, quat.y, quat.z, quat.w)) + + print(f"Warning: Invalid mode '{mode}' in convert_to_quaternion") + return mathutils.Vector((0.0, 0.0, 0.0, 1.0)) + + 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 + if self.default_value_mode == 'Quaternion': + # Directly construct the quaternion vector from s0, s1, s2, s3 (x, y, z, w) + vec3_val = mathutils.Vector(( + self.default_value_s0, # X component or Euler X or Axis X + self.default_value_s1, # Y component or Euler Y or Axis Y + self.default_value_s2 # Z component or Euler Z or Axis Z + )) + scalar_val = self.default_value_s3 # W component or Axis Angle - self.default_value_raw = self.convert_to_quaternion( - part1, - self.default_value_s3, - self.default_value_mode, - self.default_value_unit, - self.default_value_order - ) + # Always call the unified conversion function + # The result will be in (x, y, z, w) order + self.default_value_raw = self.convert_to_quaternion( + vec3_val, + scalar_val, + self.default_value_mode, + self.default_value_unit, + self.default_value_order + ) + else: + # Handle EulerAngles and AxisAngle using the conversion helper + vec3_val = mathutils.Vector(( + self.default_value_s0, + self.default_value_s1, + self.default_value_s2 + )) + # s3 is used for AxisAngle angle, irrelevant for Euler + scalar_val = self.default_value_s3 + + self.default_value_raw = self.convert_to_quaternion( + vec3_val, # Vector part (Euler angles X,Y,Z or Axis X,Y,Z) + scalar_val, # Scalar part (Axis angle) + self.default_value_mode, # Mode ('EulerAngles' or 'AxisAngle') + self.default_value_unit, # Unit ('Rad' or 'Deg') + self.default_value_order # Order ('XYZ', 'ZYX', etc. - used only for Euler) + ) def draw(self, context, layout, node, text): @@ -275,9 +295,11 @@ class LnxRotationSocket(LnxCustomSocket): ('YZX','YZX','YZX'), ('ZXY','ZXY','ZXY'), ('ZYX','ZYX','ZYX')], - name='', default='XYZ' + name='', default='XYZ', + update=do_update_raw ) + default_value_s0: FloatProperty(update=do_update_raw) default_value_s1: FloatProperty(update=do_update_raw) default_value_s2: FloatProperty(update=do_update_raw) diff --git a/leenkx/blender/lnx/nodes_logic.py b/leenkx/blender/lnx/nodes_logic.py index 5f43e33..dc125bd 100644 --- a/leenkx/blender/lnx/nodes_logic.py +++ b/leenkx/blender/lnx/nodes_logic.py @@ -344,6 +344,8 @@ class LNX_PT_NodeDevelopment(bpy.types.Panel): layout.separator() layout.operator('lnx.node_replace_all') + layout.operator('lnx.recalculate_rotations') + @staticmethod def _draw_row(col: bpy.types.UILayout, text: str, val: Any): @@ -366,6 +368,40 @@ class LNX_OT_ReplaceNodesOperator(bpy.types.Operator): def poll(cls, context): return context.space_data is not None and context.space_data.type == 'NODE_EDITOR' +class LNX_OT_RecalculateRotations(bpy.types.Operator): + """Recalculates internal rotation values for all rotation sockets in the tree""" + bl_idname = "lnx.recalculate_rotations" + bl_label = "Recalculate Rotations" + bl_description = "Forces recalculation of internal quaternion values for all LnxRotationSockets in the active tree using their current settings. Useful for fixing old files." + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return (context.space_data is not None and + context.space_data.type == 'NODE_EDITOR' and + context.space_data.tree_type == 'LnxLogicTreeType' and + context.space_data.edit_tree is not None) + + def execute(self, context): + tree = context.space_data.edit_tree + if not tree: + self.report({'WARNING'}, "No active Logic Node tree found") + return {'CANCELLED'} + recalculated_count = 0 + for node in tree.nodes: + for socket in list(node.inputs) + list(node.outputs): + if hasattr(socket, 'do_update_raw') and callable(socket.do_update_raw): + try: + socket.do_update_raw(context) + recalculated_count += 1 + except Exception as e: + print(f"Error recalculating socket '{socket.name}' on node '{node.name}': {e}") + + self.report({'INFO'}, f"Recalculated {recalculated_count} rotation sockets in tree '{tree.name}'") + tree.update_tag() + return {'FINISHED'} + + class LNX_UL_InterfaceSockets(bpy.types.UIList): """UI List of input and output sockets""" def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): @@ -421,6 +457,7 @@ __REG_CLASSES = ( LnxOpenNodePythonSource, LnxOpenNodeWikiEntry, LNX_OT_ReplaceNodesOperator, + LNX_OT_RecalculateRotations, LNX_MT_NodeAddOverride, LNX_OT_AddNodeOverride, LNX_UL_InterfaceSockets,