forked from LeenkxTeam/LNXSDK
add new Set Look At Rotation Node for making logic nodes great again
This commit is contained in:
190
leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx
Normal file
190
leenkx/Sources/leenkx/logicnode/SetLookAtRotationNode.hx
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
Reference in New Issue
Block a user