From 4055c979a1653d1beec499b61b932e8b560d4f61 Mon Sep 17 00:00:00 2001 From: wuaieyo Date: Tue, 24 Jun 2025 19:47:03 +0200 Subject: [PATCH] add new Set Look At Rotation Node for making logic nodes great again --- Krom/Krom | Bin .../leenkx/logicnode/SetLookAtRotationNode.hx | 190 +++++++++ .../transform/LN_set_look_at_rotation.py | 367 ++++++++++++++++++ 3 files changed, 557 insertions(+) mode change 100644 => 100755 Krom/Krom create mode 100644 leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx create mode 100644 leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py diff --git a/Krom/Krom b/Krom/Krom old mode 100644 new mode 100755 diff --git a/leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx b/leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx new file mode 100644 index 0000000..15fb2d7 --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx @@ -0,0 +1,190 @@ +package leenkx.logicnode; + +import iron.math.Vec4; +import iron.math.Quat; +import iron.math.Mat4; +import iron.object.Object; + +class SetLookAtRotationNode extends LogicNode { + + public var property0: String; // Axis to align + public var property1: String; // Use vector for target (true/false) + public var property2: String; // Use vector for source (true/false) + public var property3: String; // Damping value (backward compatibility, now input socket) + public var property4: String; // Disable rotation on aligning axis (true/false) + + // Store the calculated rotation for output + var calculatedRotation: Quat = null; + // Store the previous rotation for smooth interpolation + var previousRotation: Quat = null; + + public function new(tree: LogicTree) { + super(tree); + previousRotation = new Quat(); + } + + override function run(from: Int): Void { + // Determine if we're using a vector or an object as source + var useSourceVector: Bool = property2 == "true"; + var objectToUse: Object = null; + var objectLoc: Vec4 = null; + + if (useSourceVector) { + // Use tree.object as the object to rotate + objectToUse = tree.object; + if (objectToUse == null) { + runOutput(0); + return; + } + + // Get the source location directly + objectLoc = inputs[1].get(); + if (objectLoc == null) { + runOutput(0); + return; + } + } else { + // Get the source object (or fallback to tree.object) + objectToUse = (inputs.length > 1 && inputs[1] != null) ? inputs[1].get() : tree.object; + if (objectToUse == null) { + runOutput(0); + return; + } + + // Get source object's position + objectLoc = objectToUse.transform.loc; + } + + // Determine if we're using a vector or an object as target + var useTargetVector: Bool = property1 == "true"; + var targetLoc: Vec4 = null; + + if (useTargetVector) { + // Get the target location directly + targetLoc = inputs[2].get(); + if (targetLoc == null) { + runOutput(0); + return; + } + } else { + // Get the target object + var targetObject: Object = inputs[2].get(); + if (targetObject == null) { + runOutput(0); + return; + } + + // Get target object's position + targetLoc = targetObject.transform.loc; + } + + // Calculate direction to target + var direction = new Vec4( + targetLoc.x - objectLoc.x, + targetLoc.y - objectLoc.y, + targetLoc.z - objectLoc.z + ); + direction.normalize(); + + // Calculate target rotation based on selected axis + calculatedRotation = new Quat(); + switch (property0) { + case "X": + calculatedRotation.fromTo(new Vec4(1, 0, 0), direction); + case "-X": + calculatedRotation.fromTo(new Vec4(-1, 0, 0), direction); + case "Y": + calculatedRotation.fromTo(new Vec4(0, 1, 0), direction); + case "-Y": + calculatedRotation.fromTo(new Vec4(0, -1, 0), direction); + case "Z": + calculatedRotation.fromTo(new Vec4(0, 0, 1), direction); + case "-Z": + calculatedRotation.fromTo(new Vec4(0, 0, -1), direction); + } + + // If disable rotation on aligning axis is enabled, constrain the target rotation + if (property4 == "true") { + // Apply constraint to the target rotation BEFORE damping to avoid jiggling + var eulerAngles = calculatedRotation.toEulerOrdered("XYZ"); + + // Set the rotation around the selected axis to 0 + switch (property0) { + case "X", "-X": + eulerAngles.x = 0.0; + case "Y", "-Y": + eulerAngles.y = 0.0; + case "Z", "-Z": + eulerAngles.z = 0.0; + } + + // Convert back to quaternion + calculatedRotation.fromEulerOrdered(eulerAngles, "XYZ"); + } + + // Apply rotation with damping + var dampingValue: Float = 0.0; + + // Try to get damping from input socket first (index 3), fallback to property + if (inputs.length > 3 && inputs[3] != null) { + var dampingInput: Dynamic = inputs[3].get(); + if (dampingInput != null) { + dampingValue = dampingInput; + } + } else { + // Fallback to property for backward compatibility + dampingValue = Std.parseFloat(property3); + } + + if (dampingValue > 0.0) { + // Create a fixed interpolation rate that never reaches exactly 1.0 + // Higher damping = slower rotation (smaller step) + var step = Math.max(0.001, (1.0 - dampingValue) * 0.2); // 0.001 to 0.2 range + + // Get current rotation as quaternion + var currentRot = new Quat().setFrom(objectToUse.transform.rot); + + // Calculate the difference between current and target rotation + var diffQuat = new Quat(); + // q1 * inverse(q2) gives the rotation from q2 to q1 + var invCurrent = new Quat().setFrom(currentRot); + invCurrent.x = -invCurrent.x; + invCurrent.y = -invCurrent.y; + invCurrent.z = -invCurrent.z; + diffQuat.multquats(calculatedRotation, invCurrent); + + // Convert to axis-angle representation + var axis = new Vec4(); + var angle = diffQuat.toAxisAngle(axis); + + // Apply only a portion of this rotation (step) + var partialAngle = angle * step; + + // Create partial rotation quaternion + var partialRot = new Quat().fromAxisAngle(axis, partialAngle); + + // Apply this partial rotation to current + var newRot = new Quat(); + newRot.multquats(partialRot, currentRot); + + // Apply the new rotation + objectToUse.transform.rot.setFrom(newRot); + } else { + // No damping, apply instant rotation + objectToUse.transform.rot.setFrom(calculatedRotation); + } + + objectToUse.transform.buildMatrix(); + + runOutput(0); + } + + // Getter method for output sockets + override function get(from: Int): Dynamic { + // Output index 1 is the rotation socket (global rotation) + if (from == 1) { + return calculatedRotation; + } + return null; + } +} diff --git a/leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py b/leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py new file mode 100644 index 0000000..b2d76e8 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/transform/LN_set_look_at_rotation.py @@ -0,0 +1,367 @@ +from lnx.logicnode.lnx_nodes import * +import bpy + +class SetLookAtRotationNode(LnxLogicTreeNode): + """Returns a rotation that makes an object look at a target object or location""" + bl_idname = 'LNSetLookAtRotationNode' + bl_label = 'Set Look At Rotation' + lnx_section = 'rotation' + lnx_version = 1 + + use_vector: bpy.props.BoolProperty( + name='Use Vector for Target Location', + description='Use a vector location instead of a target object', + default=False, + update=lambda self, context: self.update_sockets(context) + ) + + use_source_vector: bpy.props.BoolProperty( + name='Use Vector for Source', + description='Use a vector location instead of a source object', + default=False, + update=lambda self, context: self.update_sockets(context) + ) + + disable_rotation_on_align_axis: bpy.props.BoolProperty( + name='Disable Rotation on Aligning Axis', + description='Zero out the rotation on the aligning axis after look at is applied', + default=False, + update=lambda self, context: self.update_sockets(context) + ) + + damping: bpy.props.FloatProperty( + name='Damping', + description='Amount of damping for rotation (0.0 = instant, 1.0 = no movement)', + default=0.0, + min=0.0, + max=1.0, + step=10, + precision=2, + update=lambda self, context: self.update_damping_socket(context) + ) + + # Store object references as custom properties + source_object_name: bpy.props.StringProperty(default="") + target_object_name: bpy.props.StringProperty(default="") + + property0: HaxeEnumProperty( + 'property0', + items = [('X', ' X', 'X'), + ('-X', '-X', '-X'), + ('Y', ' Y', 'Y'), + ('-Y', '-Y', '-Y'), + ('Z', ' Z', 'Z'), + ('-Z', '-Z', '-Z')], + name='With', default='Z') + + property1: HaxeEnumProperty( + 'property1', + items = [('true', 'True', 'True'), + ('false', 'False', 'False')], + name='Use Vector for Target Location', default='false') + + property2: HaxeEnumProperty( + 'property2', + items = [('true', 'True', 'True'), + ('false', 'False', 'False')], + name='Use Vector for Source', default='false') + + property3: bpy.props.StringProperty(name='Damping', default='0.0') + + property4: HaxeEnumProperty( + 'property4', + items = [('true', 'True', 'True'), + ('false', 'False', 'False')], + name='Disable Rotation on Aligning Axis', default='false') + + def lnx_init(self, context): + # Add inputs in standard order + self.inputs.new('LnxNodeSocketAction', 'In') + + # Add the initial source input + self.inputs.new('LnxNodeSocketObject', 'Source Object') + + # Add the initial target input + self.inputs.new('LnxNodeSocketObject', 'Target Object') + + # Add damping input socket with default value + damping_socket = self.inputs.new('LnxFloatSocket', 'Damping') + damping_socket.default_value_raw = 0.0 + + # Add outputs + self.add_output('LnxNodeSocketAction', 'Out') + # Add rotation output socket + self.add_output('LnxRotationSocket', 'Rotation') + + def draw_buttons(self, context, layout): + # 1. Axis Selector + layout.prop(self, 'property0') + + # 2. 'Use Vector for Source' checkbox + layout.prop(self, 'use_source_vector') + + # 3. 'Use Vector for Target Location' checkbox + layout.prop(self, 'use_vector') + + # 4. 'Disable Rotation on Aligning Axis' checkbox + layout.prop(self, 'disable_rotation_on_align_axis') + + # Note: Damping is now handled by the input socket + + def update_sockets(self, context): + # Update the Haxe properties to match the Python properties + self.property1 = 'true' if self.use_vector else 'false' + self.property2 = 'true' if self.use_source_vector else 'false' + self.property3 = str(self.damping) # Keep for backward compatibility + self.property4 = 'true' if self.disable_rotation_on_align_axis else 'false' + + # Store current object references before changing sockets + self.save_object_references() + + # Helper to find a socket by name + def find_input_socket(name): + for i, s in enumerate(self.inputs): + if s.name == name: + return i, s + return -1, None + + # Ensure we have the 'In' socket at index 0 + in_idx, in_socket = find_input_socket('In') + if in_idx == -1: + # If 'In' socket is missing, add it at index 0 + self.inputs.new('LnxNodeSocketAction', 'In') + + # Fixed indices for our sockets + SOURCE_INDEX = 1 + TARGET_INDEX = 2 + DAMPING_INDEX = 3 + + # Get current socket information + source_names = ['Source Object', 'Source Location'] + target_names = ['Target Object', 'Target Location'] + + # Find current source and target sockets + source_idx = -1 + source_socket = None + for name in source_names: + idx, socket = find_input_socket(name) + if idx != -1: + source_idx = idx + source_socket = socket + break + + target_idx = -1 + target_socket = None + for name in target_names: + idx, socket = find_input_socket(name) + if idx != -1: + target_idx = idx + target_socket = socket + break + + # Expected types based on current settings + expected_source_name = 'Source Location' if self.use_source_vector else 'Source Object' + expected_source_type = 'LnxVectorSocket' if self.use_source_vector else 'LnxNodeSocketObject' + + expected_target_name = 'Target Location' if self.use_vector else 'Target Object' + expected_target_type = 'LnxVectorSocket' if self.use_vector else 'LnxNodeSocketObject' + + # Ensure we have exactly 4 sockets (In, Source, Target, Damping) in the correct order + while len(self.inputs) > 4: + # Remove any extra sockets + self.inputs.remove(self.inputs[-1]) + + # Make sure we have exactly 4 sockets + while len(self.inputs) < 4: + if len(self.inputs) == 0: + self.inputs.new('LnxNodeSocketAction', 'In') + elif len(self.inputs) == 1: + self.inputs.new(expected_source_type, expected_source_name) + elif len(self.inputs) == 2: + self.inputs.new(expected_target_type, expected_target_name) + elif len(self.inputs) == 3: + damping_socket = self.inputs.new('LnxFloatSocket', 'Damping') + damping_socket.default_value_raw = self.damping + + # Now update the source socket if needed + if source_socket and source_socket.name != expected_source_name: + # Store links before removing + links = [] + for link in source_socket.links: + links.append(link.from_socket) + + # Get the index where this socket should be + correct_idx = SOURCE_INDEX + + # Create the new socket at the correct position + self.inputs.remove(source_socket) + new_socket = self.inputs.new(expected_source_type, expected_source_name) + + # Move the new socket to the correct position + if not new_socket.is_linked: # Only move if not linked + # Move the socket to the correct index + if correct_idx < len(self.inputs) - 1: + self.inputs.move(len(self.inputs) - 1, correct_idx) + + # Restore links + if links and hasattr(context, 'space_data') and context.space_data and hasattr(context.space_data, 'edit_tree'): + for link_socket in links: + context.space_data.edit_tree.links.new(link_socket, new_socket) + + # Update the target socket if needed + if target_socket and target_socket.name != expected_target_name: + # Store links before removing + links = [] + for link in target_socket.links: + links.append(link.from_socket) + + # Get the index where this socket should be + correct_idx = TARGET_INDEX + + # Create the new socket at the correct position + self.inputs.remove(target_socket) + new_socket = self.inputs.new(expected_target_type, expected_target_name) + + # Move the new socket to the correct position + if not new_socket.is_linked: # Only move if not linked + # Move the socket to the correct index + if correct_idx < len(self.inputs) - 1: + self.inputs.move(len(self.inputs) - 1, correct_idx) + + # Restore links + if links and hasattr(context, 'space_data') and context.space_data and hasattr(context.space_data, 'edit_tree'): + for link_socket in links: + context.space_data.edit_tree.links.new(link_socket, new_socket) + + # Make a final check to ensure the sockets are in the correct order + # This is a safety measure to ensure consistent UI + in_idx, in_socket = find_input_socket('In') + source_idx = -1 + for name in source_names: + idx, _ = find_input_socket(name) + if idx != -1: + source_idx = idx + break + + target_idx = -1 + for name in target_names: + idx, _ = find_input_socket(name) + if idx != -1: + target_idx = idx + break + + damping_idx, damping_socket = find_input_socket('Damping') + + # If the order is wrong, fix it by recreating the sockets in the correct order + if not (in_idx == 0 and source_idx == 1 and target_idx == 2 and damping_idx == 3): + # Store all links + all_links = {} + + for i, socket in enumerate(self.inputs): + # Store links + links = [] + for link in socket.links: + links.append(link.from_socket) + all_links[socket.name] = links + + # Clear all inputs + while len(self.inputs) > 0: + self.inputs.remove(self.inputs[0]) + + # Recreate in the correct order + in_socket = self.inputs.new('LnxNodeSocketAction', 'In') + source_socket = self.inputs.new(expected_source_type, expected_source_name) + target_socket = self.inputs.new(expected_target_type, expected_target_name) + damping_socket = self.inputs.new('LnxFloatSocket', 'Damping') + damping_socket.default_value_raw = self.damping + + # Restore links + if hasattr(context, 'space_data') and context.space_data and hasattr(context.space_data, 'edit_tree'): + # Restore In links + if 'In' in all_links: + for link_socket in all_links['In']: + context.space_data.edit_tree.links.new(link_socket, in_socket) + + # Restore Source links + source_links = [] + for name in source_names: + if name in all_links: + source_links.extend(all_links[name]) + for link_socket in source_links: + context.space_data.edit_tree.links.new(link_socket, source_socket) + + # Restore Target links + target_links = [] + for name in target_names: + if name in all_links: + target_links.extend(all_links[name]) + for link_socket in target_links: + context.space_data.edit_tree.links.new(link_socket, target_socket) + + # Restore Damping links + if 'Damping' in all_links: + for link_socket in all_links['Damping']: + context.space_data.edit_tree.links.new(link_socket, damping_socket) + + # Restore object references after socket changes + self.restore_object_references() + + def update_damping_socket(self, context): + """Update the damping socket default value when the slider changes""" + for socket in self.inputs: + if socket.name == 'Damping' and hasattr(socket, 'default_value_raw'): + socket.default_value_raw = self.damping + break + + def save_object_references(self): + """Save object references to custom properties""" + # Find source and target object sockets + for socket in self.inputs: + if socket.name == 'Source Object' and socket.is_linked: + for link in socket.links: + if hasattr(link.from_node, 'item') and link.from_node.item: + self.source_object_name = link.from_node.item.name + + if socket.name == 'Target Object' and socket.is_linked: + for link in socket.links: + if hasattr(link.from_node, 'item') and link.from_node.item: + self.target_object_name = link.from_node.item.name + + def restore_object_references(self): + """Restore object references from custom properties""" + # Only restore if we're not using vector inputs + if not self.use_source_vector: + # Find source object socket + for socket in self.inputs: + if socket.name == 'Source Object' and not socket.is_linked: + # Try to find the object in the scene + if self.source_object_name and self.source_object_name in bpy.context.scene.objects: + # Find the appropriate object node in the node tree + if hasattr(self, 'id_data') and hasattr(self.id_data, 'nodes'): + for node in self.id_data.nodes: + if (node.bl_idname == 'LNObjectNode' and + hasattr(node, 'item') and + node.item and + node.item.name == self.source_object_name): + # Create a link between the nodes + if hasattr(self.id_data, 'links'): + self.id_data.links.new(node.outputs[0], socket) + break + + if not self.use_vector: + # Find target object socket + for socket in self.inputs: + if socket.name == 'Target Object' and not socket.is_linked: + # Try to find the object in the scene + if self.target_object_name and self.target_object_name in bpy.context.scene.objects: + # Find the appropriate object node in the node tree + if hasattr(self, 'id_data') and hasattr(self.id_data, 'nodes'): + for node in self.id_data.nodes: + if (node.bl_idname == 'LNObjectNode' and + hasattr(node, 'item') and + node.item and + node.item.name == self.target_object_name): + # Create a link between the nodes + if hasattr(self.id_data, 'links'): + self.id_data.links.new(node.outputs[0], socket) + break