merge upstream

This commit is contained in:
2025-07-16 05:57:15 +00:00
11 changed files with 669 additions and 216 deletions

View File

@ -2843,6 +2843,18 @@ class LeenkxExporter:
body_params['linearDeactivationThreshold'] = deact_lv
body_params['angularDeactivationThrshold'] = deact_av
body_params['deactivationTime'] = deact_time
# New velocity limit properties
body_params['linearVelocityMin'] = bobject.lnx_rb_linear_velocity_min
body_params['linearVelocityMax'] = bobject.lnx_rb_linear_velocity_max
body_params['angularVelocityMin'] = bobject.lnx_rb_angular_velocity_min
body_params['angularVelocityMax'] = bobject.lnx_rb_angular_velocity_max
# New lock properties
body_params['lockTranslationX'] = bobject.lnx_rb_lock_translation_x
body_params['lockTranslationY'] = bobject.lnx_rb_lock_translation_y
body_params['lockTranslationZ'] = bobject.lnx_rb_lock_translation_z
body_params['lockRotationX'] = bobject.lnx_rb_lock_rotation_x
body_params['lockRotationY'] = bobject.lnx_rb_lock_rotation_y
body_params['lockRotationZ'] = bobject.lnx_rb_lock_rotation_z
body_flags = {}
body_flags['animated'] = rb.kinematic
body_flags['trigger'] = bobject.lnx_rb_trigger

View File

@ -2,7 +2,10 @@ import importlib
import os
import queue
import sys
import threading
import time
import types
from typing import Dict, Tuple, Callable, Set
import bpy
from bpy.app.handlers import persistent
@ -30,6 +33,10 @@ if lnx.is_reload(__name__):
else:
lnx.enable_reload(__name__)
# Module-level storage for active threads (eliminates re-queuing overhead)
_active_threads: Dict[threading.Thread, Callable] = {}
_last_poll_time = 0.0
_consecutive_empty_polls = 0
@persistent
def on_depsgraph_update_post(self):
@ -135,35 +142,113 @@ def always() -> float:
def poll_threads() -> float:
"""Polls the thread callback queue and if a thread has finished, it
is joined with the main thread and the corresponding callback is
executed in the main thread.
"""
Improved thread polling with:
- No re-queuing overhead
- Batch processing of completed threads
- Adaptive timing based on activity
- Better memory management
- Simplified logic flow
"""
global _last_poll_time, _consecutive_empty_polls
current_time = time.time()
# Process all new threads from queue at once (batch processing)
new_threads_added = 0
try:
thread, callback = make.thread_callback_queue.get(block=False)
while True:
thread, callback = make.thread_callback_queue.get(block=False)
_active_threads[thread] = callback
new_threads_added += 1
except queue.Empty:
pass
# Early return if no active threads
if not _active_threads:
_consecutive_empty_polls += 1
# Adaptive timing: longer intervals when consistently empty
if _consecutive_empty_polls > 10:
return 0.5 # Back off when no activity
return 0.25
if thread.is_alive():
try:
make.thread_callback_queue.put((thread, callback), block=False)
except queue.Full:
return 0.5
return 0.1
# Reset empty poll counter when we have active threads
_consecutive_empty_polls = 0
# Find completed threads (single pass, no re-queuing)
completed_threads = []
for thread in list(_active_threads.keys()):
if not thread.is_alive():
completed_threads.append(thread)
# Batch process all completed threads
if completed_threads:
_process_completed_threads(completed_threads)
# Adaptive timing based on activity level
active_count = len(_active_threads)
if active_count == 0:
return 0.25
elif active_count <= 3:
return 0.05 # Medium frequency for low activity
else:
return 0.01 # High frequency for high activity
def _process_completed_threads(completed_threads: list) -> None:
"""Process a batch of completed threads with robust error handling."""
for thread in completed_threads:
callback = _active_threads.pop(thread) # Remove from tracking
try:
thread.join()
thread.join() # Should be instant since thread is dead
callback()
except Exception as e:
# If there is an exception, we can no longer return the time to
# the next call to this polling function, so to keep it running
# we re-register it and then raise the original exception.
try:
bpy.app.timers.unregister(poll_threads)
except ValueError:
pass
bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True)
# Quickly check if another thread has finished
return 0.01
# Robust error recovery
_handle_callback_error(e)
continue # Continue processing other threads
# Explicit cleanup for better memory management
del thread, callback
def _handle_callback_error(exception: Exception) -> None:
"""Centralized error handling with better recovery."""
try:
# Try to unregister existing timer
bpy.app.timers.unregister(poll_threads)
except ValueError:
pass # Timer wasn't registered, that's fine
# Re-register timer with slightly longer interval for stability
bpy.app.timers.register(poll_threads, first_interval=0.1, persistent=True)
# Re-raise the original exception after ensuring timer continuity
raise exception
def cleanup_polling_system() -> None:
"""Optional cleanup function for proper shutdown."""
global _active_threads, _consecutive_empty_polls
# Wait for remaining threads to complete (with timeout)
for thread in list(_active_threads.keys()):
if thread.is_alive():
thread.join(timeout=1.0) # 1 second timeout
# Clear tracking structures
_active_threads.clear()
_consecutive_empty_polls = 0
# Unregister timer
try:
bpy.app.timers.unregister(poll_threads)
except ValueError:
pass
def get_polling_stats() -> dict:
"""Get statistics about the polling system for monitoring."""
return {
'active_threads': len(_active_threads),
'consecutive_empty_polls': _consecutive_empty_polls,
'thread_ids': [t.ident for t in _active_threads.keys()]
}
loaded_py_libraries: dict[str, types.ModuleType] = {}

View File

@ -2,100 +2,158 @@ from lnx.logicnode.lnx_nodes import *
class MouseLookNode(LnxLogicTreeNode):
"""Controls object rotation based on mouse movement for FPS-style camera control.
"""MouseLookNode - Blender UI interface for FPS-style mouse look camera controller
This class defines the Blender node interface for the MouseLookNode logic node.
It creates the visual node that appears in Blender's logic tree editor and
defines all the properties that configure the mouse look behavior.
The node provides controls for:
- Axis orientation configuration
- Mouse cursor behavior
- Movement inversion options
- Rotation limiting/capping
- Head rotation space behavior
Features:
- Sub-pixel interpolation (always enabled) for optimal precision and smooth low-sensitivity movement
- Resolution-adaptive scaling for consistent feel across different screen resolutions
- Built-in resolution-adaptive scaling for consistent feel across different screen resolutions
- Automatic physics synchronization for rigid bodies
- Support for both single-object and dual-object (body/head) setups
"""
bl_idname = 'LNMouseLookNode'
bl_label = 'Mouse Look'
lnx_section = 'mouse'
lnx_version = 1
# Blender node identification
bl_idname = 'LNMouseLookNode' # Unique identifier for Blender's node system
bl_label = 'Mouse Look' # Display name in node menu and header
lnx_section = 'mouse' # Category section in node add menu
lnx_version = 1 # Node version for compatibility tracking
# Front axis property
# Property 0: Front Axis Configuration
# Determines which 3D axis represents the "forward" direction of the character/camera
# This affects how horizontal and vertical rotations are applied to the objects
property0: HaxeEnumProperty(
'property0',
items=[('X', 'X Axis', 'X Axis as front'),
('Y', 'Y Axis', 'Y Axis as front'),
('Z', 'Z Axis', 'Z Axis as front')],
name='Front', default='Y')
items=[('X', 'X Axis', 'X Axis as front'), # X-forward (side-scrolling, specific orientations)
('Y', 'Y Axis', 'Y Axis as front'), # Y-forward (most common for 3D games)
('Z', 'Z Axis', 'Z Axis as front')], # Z-forward (top-down, specific orientations)
name='Front',
default='Y') # Y-axis is default as it's most common in 3D game development
# Hide Locked property
# Property 1: Automatic Mouse Cursor Management
# When enabled, automatically centers and locks the mouse cursor when mouse input starts
# This is essential for FPS games to prevent cursor from leaving game window
property1: HaxeBoolProperty(
'property1',
name='Hide Locked',
description='Automatically center and lock the mouse cursor',
default=True)
description='Automatically center and lock the mouse cursor when mouse input begins',
default=True) # Enabled by default for typical FPS behavior
# Invert X property
# Property 2: Horizontal Movement Inversion
# Allows users to invert horizontal mouse movement (left becomes right, right becomes left)
# Some players prefer inverted controls for consistency with flight simulators
property2: HaxeBoolProperty(
'property2',
name='Invert X',
description='Invert horizontal mouse movement',
default=False)
description='Invert horizontal mouse movement - moving mouse right turns character left',
default=False) # Most players expect non-inverted horizontal movement
# Invert Y property
# Property 3: Vertical Movement Inversion
# Allows users to invert vertical mouse movement (up becomes down, down becomes up)
# More commonly used than horizontal inversion, especially by flight sim players
property3: HaxeBoolProperty(
'property3',
name='Invert Y',
description='Invert vertical mouse movement',
default=False)
description='Invert vertical mouse movement - moving mouse up looks down',
default=False) # Most players expect non-inverted vertical movement
# Cap Left/Right property
# Property 4: Horizontal Rotation Limiting
# Prevents the character from rotating beyond specified horizontal limits
# Useful for fixed-perspective games or when character shouldn't turn completely around
property4: HaxeBoolProperty(
'property4',
name='Cap Left / Right',
description='Limit horizontal rotation',
default=False)
description='Limit horizontal rotation to prevent full 360-degree turns',
default=False) # Disabled by default - most FPS games allow full horizontal rotation
# Cap Up/Down property
# Property 5: Vertical Rotation Limiting
# Prevents looking too far up or down, simulating human neck movement limitations
# Essential for realistic FPS games to prevent disorienting over-rotation
property5: HaxeBoolProperty(
'property5',
name='Cap Up / Down',
description='Limit vertical rotation',
default=True)
# Strategy toggles
description='Limit vertical rotation to simulate natural neck movement (±90 degrees)',
default=True) # Enabled by default for realistic FPS behavior
# Property 6: Head Rotation Space Mode
# Controls whether head rotation uses local or world space coordinates
# Critical for preventing rotation issues when head object is child of body object
property6: HaxeBoolProperty(
'property6',
name='Resolution Adaptive',
description='Scale sensitivity based on screen resolution',
default=False)
name='Head Local Space',
description='Use local space for head rotation - enable when Head is child of Body to avoid gimbal lock',
default=False) # Disabled by default, enable when using parent-child object relationships
def lnx_init(self, context):
"""Initialize the node's input and output sockets
This method is called when the node is first created in Blender.
It defines all the connection points (sockets) that other nodes can connect to.
Input Sockets:
- Action In: Execution flow input (when this node should run)
- Body: The main character/player object that rotates horizontally
- Head: Optional camera/head object that rotates vertically (can be child of Body)
- Sensitivity: Mouse sensitivity multiplier (0.5 = half sensitivity, 2.0 = double sensitivity)
- Smoothing: Movement smoothing factor (0.0 = no smoothing, higher = more smoothing)
Output Sockets:
- Action Out: Execution flow output (continues to next node after processing)
"""
# Execution flow input - connects from previous logic node
self.add_input('LnxNodeSocketAction', 'In')
self.add_input('LnxNodeSocketObject', 'Body')
self.add_input('LnxNodeSocketObject', 'Head')
self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5)
self.add_input('LnxFloatSocket', 'Smoothing', default_value=0.0)
# Object inputs - require 3D objects from the scene
self.add_input('LnxNodeSocketObject', 'Body') # Main character object (required)
self.add_input('LnxNodeSocketObject', 'Head') # Camera/head object (optional)
# Numeric inputs with sensible defaults
self.add_input('LnxFloatSocket', 'Sensitivity', default_value=0.5) # Medium sensitivity
self.add_input('LnxFloatSocket', 'Smoothing', default_value=0.0) # No smoothing by default
# Execution flow output - connects to next logic node
self.add_output('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'property0', text='Front')
layout.prop(self, 'property1', text='Hide Locked')
"""Draw the node's user interface in Blender's logic tree editor
# Invert XY section
col = layout.column(align=True)
col.label(text="Invert XY:")
row = col.row(align=True)
row.prop(self, 'property2', text='X', toggle=True)
row.prop(self, 'property3', text='Y', toggle=True)
This method creates the visual controls that appear on the node in Blender.
It organizes properties into logical groups for better usability.
# Cap rotations section
col = layout.column(align=True)
col.prop(self, 'property4', text='Cap Left / Right')
col.prop(self, 'property5', text='Cap Up / Down')
Args:
context: Blender context (current scene, selected objects, etc.)
layout: UI layout object for arranging interface elements
"""
# Separator
layout.separator()
# Basic configuration section
layout.prop(self, 'property0', text='Front') # Front axis dropdown
layout.prop(self, 'property1', text='Hide Locked') # Mouse locking checkbox
# Enhancement strategies section
col = layout.column(align=True)
col.label(text="Enhancement Strategies:")
col.prop(self, 'property6', text='Resolution Adaptive')
# Movement inversion controls section
# Group X and Y inversion together for logical organization
col = layout.column(align=True) # Create aligned column
col.label(text="Invert XY:") # Section header
row = col.row(align=True) # Create horizontal row within column
row.prop(self, 'property2', text='X', toggle=True) # X inversion toggle button
row.prop(self, 'property3', text='Y', toggle=True) # Y inversion toggle button
# Rotation limiting controls section
# Group rotation caps together since they're related functionality
col = layout.column(align=True) # Create new aligned column
col.prop(self, 'property4', text='Cap Left / Right') # Horizontal capping checkbox
col.prop(self, 'property5', text='Cap Up / Down') # Vertical capping checkbox
# Advanced head behavior section
# Separate advanced option that affects technical behavior
col = layout.column(align=True) # Create new aligned column
col.prop(self, 'property6', text='Head Local Space') # Head rotation space checkbox

View File

@ -0,0 +1,15 @@
from lnx.logicnode.lnx_nodes import *
import bpy
class OnceNode(LnxLogicTreeNode):
bl_idname = 'LNOnceNode'
bl_label = 'Once'
lnx_version = 1
def lnx_init(self, context):
self.add_input('LnxNodeSocketAction', 'Run Once')
self.add_input('LnxNodeSocketAction', 'Reset')
self.add_output('LnxNodeSocketAction', 'Out')

View File

@ -0,0 +1,28 @@
from lnx.logicnode.lnx_nodes import *
import bpy
from bpy.props import BoolProperty
class LNSetObjectDelayedLocationNode(LnxLogicTreeNode):
bl_idname = 'LNSetObjectDelayedLocationNode'
bl_label = 'Set Object Delayed Location'
lnx_section = 'props'
lnx_version = 1
use_local_space: BoolProperty(
name="Use Local Space",
description="Move follower in local space instead of world space",
default=False
)
def lnx_init(self, context):
self.inputs.new('LnxNodeSocketAction', 'In')
self.inputs.new('LnxNodeSocketObject', 'Source Object')
self.inputs.new('LnxNodeSocketObject', 'Target Object')
self.inputs.new('LnxFloatSocket', 'Delay')
self.outputs.new('LnxNodeSocketAction', 'Out')
def draw_buttons(self, context, layout):
layout.prop(self, 'use_local_space')
def draw_label(self):
return "Set Object Delayed Location"

View File

@ -369,6 +369,26 @@ def init_properties():
default=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False),
size=20,
subtype='LAYER')
# Linear velocity limits
bpy.types.Object.lnx_rb_linear_velocity_min = FloatProperty(name="Linear Velocity Min", description="Minimum linear velocity", default=0.0, min=0.0)
bpy.types.Object.lnx_rb_linear_velocity_max = FloatProperty(name="Linear Velocity Max", description="Maximum linear velocity", default=0.0, min=0.0)
# Angular velocity limits
bpy.types.Object.lnx_rb_angular_velocity_min = FloatProperty(name="Angular Velocity Min", description="Minimum angular velocity", default=0.0, min=0.0)
bpy.types.Object.lnx_rb_angular_velocity_max = FloatProperty(name="Angular Velocity Max", description="Maximum angular velocity", default=0.0, min=0.0)
# Lock translation axes
bpy.types.Object.lnx_rb_lock_translation_x = BoolProperty(name="Lock Translation X", description="Lock movement along X axis", default=False)
bpy.types.Object.lnx_rb_lock_translation_y = BoolProperty(name="Lock Translation Y", description="Lock movement along Y axis", default=False)
bpy.types.Object.lnx_rb_lock_translation_z = BoolProperty(name="Lock Translation Z", description="Lock movement along Z axis", default=False)
# Lock rotation axes
bpy.types.Object.lnx_rb_lock_rotation_x = BoolProperty(name="Lock Rotation X", description="Lock rotation around X axis", default=False)
bpy.types.Object.lnx_rb_lock_rotation_y = BoolProperty(name="Lock Rotation Y", description="Lock rotation around Y axis", default=False)
bpy.types.Object.lnx_rb_lock_rotation_z = BoolProperty(name="Lock Rotation Z", description="Lock rotation around Z axis", default=False)
bpy.types.Object.lnx_relative_physics_constraint = BoolProperty(name="Relative Physics Constraint", description="Add physics constraint relative to the parent object or collection when spawned", default=False)
bpy.types.Object.lnx_animation_enabled = BoolProperty(name="Animation", description="Enable skinning & timeline animation", default=True)
bpy.types.Object.lnx_tilesheet = StringProperty(name="Tilesheet", description="Set tilesheet animation", default='')
@ -594,7 +614,7 @@ def update_leenkx_world():
if bpy.data.filepath != '' and (file_version < sdk_version or wrd.lnx_commit != lnx_commit):
# This allows for seamless migration from earlier versions of Leenkx
for rp in wrd.lnx_rplist: # TODO: deprecated
if rp.rp_gi != 'Off':
if hasattr(rp, 'rp_gi') and rp.rp_gi != 'Off':
rp.rp_gi = 'Off'
rp.rp_voxels = rp.rp_gi

View File

@ -240,6 +240,36 @@ class LNX_PT_PhysicsPropsPanel(bpy.types.Panel):
layout.prop(obj, 'lnx_rb_linear_factor')
layout.prop(obj, 'lnx_rb_angular_factor')
layout.prop(obj, 'lnx_rb_angular_friction')
# Linear Velocity section
layout.separator()
layout.label(text="Linear Velocity:")
layout.prop(obj, 'lnx_rb_linear_velocity_min')
layout.prop(obj, 'lnx_rb_linear_velocity_max')
# Angular Velocity section
layout.separator()
layout.label(text="Angular Velocity:")
layout.prop(obj, 'lnx_rb_angular_velocity_min')
layout.prop(obj, 'lnx_rb_angular_velocity_max')
# Lock Translation section
layout.separator()
layout.label(text="Lock Translation:")
row = layout.row(align=True)
row.prop(obj, 'lnx_rb_lock_translation_x', text="X")
row.prop(obj, 'lnx_rb_lock_translation_y', text="Y")
row.prop(obj, 'lnx_rb_lock_translation_z', text="Z")
# Lock Rotation section
layout.separator()
layout.label(text="Lock Rotation:")
row = layout.row(align=True)
row.prop(obj, 'lnx_rb_lock_rotation_x', text="X")
row.prop(obj, 'lnx_rb_lock_rotation_y', text="Y")
row.prop(obj, 'lnx_rb_lock_rotation_z', text="Z")
layout.prop(obj, 'lnx_rb_trigger')
layout.prop(obj, 'lnx_rb_ccd')
layout.prop(obj, 'lnx_rb_interpolate')