forked from LeenkxTeam/LNXSDK
Merge pull request 'Tangazo - Once Node + Set Object Delayed Location Node [ Additional Clean Handler ]' (#99) from Onek8/LNXSDK:main into main
Reviewed-on: LeenkxTeam/LNXSDK#99
This commit is contained in:
23
leenkx/Sources/leenkx/logicnode/OnceNode.hx
Normal file
23
leenkx/Sources/leenkx/logicnode/OnceNode.hx
Normal file
@ -0,0 +1,23 @@
|
||||
package leenkx.logicnode;
|
||||
|
||||
class OnceNode extends LogicNode {
|
||||
|
||||
var triggered:Bool = false;
|
||||
|
||||
public function new(tree: LogicTree) {
|
||||
super(tree);
|
||||
}
|
||||
|
||||
override function run(from: Int) {
|
||||
if(from == 1){
|
||||
triggered = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!triggered) {
|
||||
triggered = true;
|
||||
runOutput(0);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package leenkx.logicnode;
|
||||
|
||||
import iron.object.Object;
|
||||
import iron.math.Vec4;
|
||||
import iron.math.Mat4;
|
||||
import iron.system.Time;
|
||||
|
||||
class SetObjectDelayedLocationNode extends LogicNode {
|
||||
public var use_local_space: Bool = false;
|
||||
|
||||
private var initialOffset: Vec4 = null;
|
||||
private var targetPos: Vec4 = new Vec4();
|
||||
private var currentPos: Vec4 = new Vec4();
|
||||
private var deltaVec: Vec4 = new Vec4();
|
||||
private var tempVec: Vec4 = new Vec4();
|
||||
|
||||
private var lastParent: Object = null;
|
||||
private var invParentMatrix: Mat4 = null;
|
||||
|
||||
public function new(tree: LogicTree) {
|
||||
super(tree);
|
||||
}
|
||||
|
||||
override function run(from: Int) {
|
||||
var follower: Object = inputs[1].get();
|
||||
var target: Object = inputs[2].get();
|
||||
var delay: Float = inputs[3].get();
|
||||
|
||||
if (follower == null || target == null || delay == null) return runOutput(0);
|
||||
|
||||
if (initialOffset == null) {
|
||||
initialOffset = new Vec4();
|
||||
var followerPos = follower.transform.world.getLoc();
|
||||
var targetPos = target.transform.world.getLoc();
|
||||
initialOffset.setFrom(followerPos);
|
||||
initialOffset.sub(targetPos);
|
||||
}
|
||||
|
||||
targetPos.setFrom(target.transform.world.getLoc());
|
||||
currentPos.setFrom(follower.transform.world.getLoc());
|
||||
|
||||
tempVec.setFrom(targetPos).add(initialOffset);
|
||||
|
||||
deltaVec.setFrom(tempVec).sub(currentPos);
|
||||
|
||||
if (deltaVec.length() < 0.001 && delay < 0.01) {
|
||||
runOutput(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (delay == 0.0) {
|
||||
currentPos.setFrom(tempVec);
|
||||
} else {
|
||||
var smoothFactor = Math.exp(-Time.delta / Math.max(0.0001, delay));
|
||||
currentPos.x = tempVec.x + (currentPos.x - tempVec.x) * smoothFactor;
|
||||
currentPos.y = tempVec.y + (currentPos.y - tempVec.y) * smoothFactor;
|
||||
currentPos.z = tempVec.z + (currentPos.z - tempVec.z) * smoothFactor;
|
||||
}
|
||||
if (use_local_space && follower.parent != null) {
|
||||
if (follower.parent != lastParent || invParentMatrix == null) {
|
||||
lastParent = follower.parent;
|
||||
invParentMatrix = Mat4.identity();
|
||||
invParentMatrix.getInverse(follower.parent.transform.world);
|
||||
}
|
||||
tempVec.setFrom(currentPos);
|
||||
tempVec.applymat(invParentMatrix);
|
||||
follower.transform.loc.set(tempVec.x, tempVec.y, tempVec.z);
|
||||
} else {
|
||||
follower.transform.loc.set(currentPos.x, currentPos.y, currentPos.z);
|
||||
}
|
||||
follower.transform.buildMatrix();
|
||||
runOutput(0);
|
||||
}
|
||||
}
|
@ -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:
|
||||
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.
|
||||
# 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
|
||||
bpy.app.timers.register(poll_threads, first_interval=0.01, persistent=True)
|
||||
# Quickly check if another thread has finished
|
||||
return 0.01
|
||||
|
||||
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] = {}
|
||||
|
15
leenkx/blender/lnx/logicnode/logic/LN_once.py
Normal file
15
leenkx/blender/lnx/logicnode/logic/LN_once.py
Normal 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')
|
||||
|
||||
|
@ -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"
|
@ -613,7 +613,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
|
||||
|
||||
|
Reference in New Issue
Block a user