From ad4013ed758d6cfe7d4876eba046003d774b152a Mon Sep 17 00:00:00 2001 From: Onek8 Date: Thu, 28 Aug 2025 19:11:31 +0000 Subject: [PATCH 01/63] Update leenkx/Sources/leenkx/renderpath/RenderPathForward.hx --- .../Sources/leenkx/renderpath/RenderPathForward.hx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx index 01e19ea..94b7bd4 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx @@ -641,18 +641,20 @@ class RenderPathForward { var framebuffer = ""; #end - #if ((rp_antialiasing == "Off") || (rp_antialiasing == "FXAA")) + RenderPathCreator.finalTarget = path.currentTarget; + + var target = ""; + #if ((rp_antialiasing == "Off") || (rp_antialiasing == "FXAA") || (!rp_render_to_texture)) { - RenderPathCreator.finalTarget = path.currentTarget; - path.setTarget(framebuffer); + target = framebuffer; } #else { - path.setTarget("buf"); - RenderPathCreator.finalTarget = path.currentTarget; + target = "buf"; } #end - + path.setTarget(target); + #if rp_compositordepth { path.bindTarget("_main", "gbufferD"); From 8fd05d5514baaf8b8aca77a0d9a1c3d1d1d65a5e Mon Sep 17 00:00:00 2001 From: Onek8 Date: Thu, 28 Aug 2025 19:21:48 +0000 Subject: [PATCH 02/63] Update leenkx/Sources/leenkx/renderpath/RenderPathForward.hx --- .../leenkx/renderpath/RenderPathForward.hx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx index 94b7bd4..abab814 100644 --- a/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx +++ b/leenkx/Sources/leenkx/renderpath/RenderPathForward.hx @@ -673,6 +673,15 @@ class RenderPathForward { } #end + #if rp_overlays + { + path.setTarget(target); + path.clearTarget(null, 1.0); + path.drawMeshes("overlay"); + } + #end + + #if ((rp_antialiasing == "SMAA") || (rp_antialiasing == "TAA")) { path.setTarget("bufa"); @@ -703,12 +712,6 @@ class RenderPathForward { } #end - #if rp_overlays - { - path.clearTarget(null, 1.0); - path.drawMeshes("overlay"); - } - #end } public static function setupDepthTexture() { From fcbab54a0c828cb1fc2cfeda287436ab63ce1297 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 16:57:49 +0000 Subject: [PATCH 03/63] moisesjpelaez - General Fixes --- leenkx/Sources/leenkx/system/Signal.hx | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/leenkx/Sources/leenkx/system/Signal.hx b/leenkx/Sources/leenkx/system/Signal.hx index c5b2c91..0abbe95 100644 --- a/leenkx/Sources/leenkx/system/Signal.hx +++ b/leenkx/Sources/leenkx/system/Signal.hx @@ -3,33 +3,35 @@ package leenkx.system; import haxe.Constraints.Function; class Signal { - var callbacks:Array = []; + var callbacks: Array = []; public function new() { - + } - public function connect(callback:Function) { + public function connect(callback: Function) { if (!callbacks.contains(callback)) callbacks.push(callback); } - public function disconnect(callback:Function) { + public function disconnect(callback: Function) { if (callbacks.contains(callback)) callbacks.remove(callback); } - public function emit(...args:Any) { - for (callback in callbacks) Reflect.callMethod(this, callback, args); + public function emit(...args: Any) { + for (callback in callbacks.copy()) { + if (callbacks.contains(callback)) Reflect.callMethod(null, callback, args); + } } - public function getConnections():Array { + public function getConnections(): Array { return callbacks; } - public function isConnected(callBack:Function):Bool { + public function isConnected(callBack: Function):Bool { return callbacks.contains(callBack); } - public function isNull():Bool { + public function isNull(): Bool { return callbacks.length == 0; } } From 19b79d61c7d266640ce887fd43613afd841c5478 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:03:20 +0000 Subject: [PATCH 04/63] ObiNoWanKenobi - FirstPersonController Changes --- .../leenkx/trait/FirstPersonController.hx | 274 ++++++++++++++---- 1 file changed, 215 insertions(+), 59 deletions(-) diff --git a/leenkx/Sources/leenkx/trait/FirstPersonController.hx b/leenkx/Sources/leenkx/trait/FirstPersonController.hx index 692868e..8f65e13 100644 --- a/leenkx/Sources/leenkx/trait/FirstPersonController.hx +++ b/leenkx/Sources/leenkx/trait/FirstPersonController.hx @@ -1,87 +1,243 @@ package leenkx.trait; +import iron.Trait; import iron.math.Vec4; import iron.system.Input; import iron.object.Object; import iron.object.CameraObject; import leenkx.trait.physics.PhysicsWorld; -import leenkx.trait.internal.CameraController; +import leenkx.trait.physics.RigidBody; +import kha.FastFloat; -class FirstPersonController extends CameraController { +class FirstPersonController extends Trait { -#if (!lnx_physics) - public function new() { super(); } -#else + #if (!lnx_physics) + public function new() { super(); } + #else - var head: Object; - static inline var rotationSpeed = 2.0; + @prop public var rotationSpeed:Float = 0.15; + @prop public var maxPitch:Float = 2.2; + @prop public var minPitch:Float = 0.5; + @prop public var enableJump:Bool = true; + @prop public var jumpForce:Float = 22.0; + @prop public var moveSpeed:Float = 500.0; - public function new() { - super(); + @prop public var forwardKey:String = "w"; + @prop public var backwardKey:String = "s"; + @prop public var leftKey:String = "a"; + @prop public var rightKey:String = "d"; + @prop public var jumpKey:String = "space"; - iron.Scene.active.notifyOnInit(init); - } + @prop public var allowAirJump:Bool = false; - function init() { - head = object.getChildOfType(CameraObject); + @prop public var canRun:Bool = true; + @prop public var runKey:String = "shift"; + @prop public var runSpeed:Float = 1000.0; - PhysicsWorld.active.notifyOnPreUpdate(preUpdate); - notifyOnUpdate(update); - notifyOnRemove(removed); - } + // Sistema de estamina + @prop public var stamina:Bool = false; + @prop public var staminaBase:Float = 75.0; + @prop public var staRecoverPerSec:Float = 5.0; + @prop public var staDecreasePerSec:Float = 5.0; + @prop public var staRecoverTime:Float = 2.0; + @prop public var staDecreasePerJump:Float = 5.0; + @prop public var enableFatigue:Bool = false; + @prop public var fatigueSpeed:Float = 0.5; // the reduction of movement when fatigue is activated... + @prop public var fatigueThreshold:Float = 30.0; // Tiempo corriendo sin parar para la activacion // Time running non-stop for activation... + @prop public var fatRecoveryThreshold:Float = 7.5; // Tiempo sin correr/saltar para salir de fatiga // Time without running/jumping to get rid of fatigue... + - var xVec = Vec4.xAxis(); - var zVec = Vec4.zAxis(); - function preUpdate() { - if (Input.occupied || !body.ready) return; + // Var Privadas + var head:CameraObject; + var pitch:Float = 0.0; + var body:RigidBody; - var mouse = Input.getMouse(); - var kb = Input.getKeyboard(); + var moveForward:Bool = false; + var moveBackward:Bool = false; + var moveLeft:Bool = false; + var moveRight:Bool = false; + var isRunning:Bool = false; - if (mouse.started() && !mouse.locked) mouse.lock(); - else if (kb.started("escape") && mouse.locked) mouse.unlock(); + var canJump:Bool = true; + var staminaValue:Float = 0.0; + var timeSinceStop:Float = 0.0; - if (mouse.locked || mouse.down()) { - head.transform.rotate(xVec, -mouse.movementY / 250 * rotationSpeed); - transform.rotate(zVec, -mouse.movementX / 250 * rotationSpeed); - body.syncTransform(); + var fatigueTimer:Float = 0.0; + var fatigueCooldown:Float = 0.0; + var isFatigueActive:Bool = false; + + public function new() { + super(); + iron.Scene.active.notifyOnInit(init); + } + + function init() { + body = object.getTrait(RigidBody); + head = object.getChildOfType(CameraObject); + PhysicsWorld.active.notifyOnPreUpdate(preUpdate); + notifyOnUpdate(update); + notifyOnRemove(removed); + staminaValue = staminaBase; + } + + function removed() { + PhysicsWorld.active.removePreUpdate(preUpdate); + } + + var zVec = Vec4.zAxis(); + + function preUpdate() { + if (Input.occupied || body == null) return; + var mouse = Input.getMouse(); + var kb = Input.getKeyboard(); + + if (mouse.started() && !mouse.locked) + mouse.lock(); + else if (kb.started("escape") && mouse.locked) + mouse.unlock(); + + if (mouse.locked || mouse.down()) { + var deltaTime:Float = iron.system.Time.delta; + object.transform.rotate(zVec, -mouse.movementX * rotationSpeed * deltaTime); + var deltaPitch:Float = -(mouse.movementY * rotationSpeed * deltaTime); + pitch += deltaPitch; + pitch = Math.max(minPitch, Math.min(maxPitch, pitch)); + head.transform.setRotation(pitch, 0.0, 0.0); + body.syncTransform(); + } + } + + var dir:Vec4 = new Vec4(); + + function isFatigued():Bool { + return enableFatigue && isFatigueActive; + } + + function update() { + if (body == null) return; + var deltaTime:Float = iron.system.Time.delta; + var kb = Input.getKeyboard(); + + moveForward = kb.down(forwardKey); + moveBackward = kb.down(backwardKey); + moveLeft = kb.down(leftKey); + moveRight = kb.down(rightKey); + var isMoving = moveForward || moveBackward || moveLeft || moveRight; + + var isGrounded:Bool = false; + #if lnx_physics + var vel = body.getLinearVelocity(); + if (Math.abs(vel.z) < 0.1) { + isGrounded = true; + } + #end + + // Dejo establecido el salto para tener en cuenta la (enableFatigue) si es que es false/true.... + if (isGrounded && !isFatigued()) { + canJump = true; } - } + // Saltar con estamina + if (enableJump && kb.started(jumpKey) && canJump) { + var jumpPower = jumpForce; + // Disminuir el salto al 50% si la (stamina) esta por debajo o en el 20%. + if (stamina) { + if (staminaValue <= 0) { + jumpPower = 0; + } else if (staminaValue <= staminaBase * 0.2) { + jumpPower *= 0.5; + } - function removed() { - PhysicsWorld.active.removePreUpdate(preUpdate); - } + staminaValue -= staDecreasePerJump; + if (staminaValue < 0.0) staminaValue = 0.0; + timeSinceStop = 0.0; + } - var dir = new Vec4(); - function update() { - if (!body.ready) return; + if (jumpPower > 0) { + body.applyImpulse(new Vec4(0, 0, jumpPower)); + if (!allowAirJump) canJump = false; + } + } - if (jump) { - body.applyImpulse(new Vec4(0, 0, 16)); - jump = false; + // Control de estamina y correr + if (canRun && kb.down(runKey) && isMoving) { + if (stamina) { + if (staminaValue > 0.0) { + isRunning = true; + staminaValue -= staDecreasePerSec * deltaTime; + if (staminaValue < 0.0) staminaValue = 0.0; + } else { + isRunning = false; + } + } else { + isRunning = true; + } + } else { + isRunning = false; + } + + // (temporizadores aparte) + if (isRunning) { + timeSinceStop = 0.0; + fatigueTimer += deltaTime; + fatigueCooldown = 0.0; + } else { + timeSinceStop += deltaTime; + fatigueCooldown += deltaTime; + } + + // Evitar correr y saltar al estar fatigado... + if (isFatigued()) { + isRunning = false; + canJump = false; } - // Move - dir.set(0, 0, 0); - if (moveForward) dir.add(transform.look()); - if (moveBackward) dir.add(transform.look().mult(-1)); - if (moveLeft) dir.add(transform.right().mult(-1)); - if (moveRight) dir.add(transform.right()); + // Activar fatiga despues de correr continuamente durante cierto umbral + if (enableFatigue && fatigueTimer >= fatigueThreshold) { + isFatigueActive = true; + } - // Push down - var btvec = body.getLinearVelocity(); - body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0); + // Eliminar la fatiga despues de recuperarse + if (enableFatigue && isFatigueActive && fatigueCooldown >= fatRecoveryThreshold) { + isFatigueActive = false; + fatigueTimer = 0.0; + } - if (moveForward || moveBackward || moveLeft || moveRight) { - var dirN = dir.normalize(); - dirN.mult(6); - body.activate(); - body.setLinearVelocity(dirN.x, dirN.y, btvec.z - 1.0); - } + // Recuperar estamina si no esta corriendo + if (stamina && !isRunning && staminaValue < staminaBase && !isFatigued()) { + if (timeSinceStop >= staRecoverTime) { + staminaValue += staRecoverPerSec * deltaTime; + if (staminaValue > staminaBase) staminaValue = staminaBase; + } + } - // Keep vertical - body.setAngularFactor(0, 0, 0); - camera.buildMatrix(); - } -#end + // Movimiento ejes (local) + dir.set(0, 0, 0); + if (moveForward) dir.add(object.transform.look()); + if (moveBackward) dir.add(object.transform.look().mult(-1)); + if (moveLeft) dir.add(object.transform.right().mult(-1)); + if (moveRight) dir.add(object.transform.right()); + + var btvec = body.getLinearVelocity(); + body.setLinearVelocity(0.0, 0.0, btvec.z - 1.0); + + if (isMoving) { + var dirN = dir.normalize(); + var baseSpeed = moveSpeed; + if (isRunning && moveForward) { + baseSpeed = runSpeed; + } + var currentSpeed = isFatigued() ? baseSpeed * fatigueSpeed : baseSpeed; + dirN.mult(currentSpeed * deltaTime); + body.activate(); + body.setLinearVelocity(dirN.x, dirN.y, btvec.z - 1.0); + } + + body.setAngularFactor(0, 0, 0); + head.buildMatrix(); + } + + #end } + + +// Stamina and fatigue system..... \ No newline at end of file From 881f3267cc38e7f2411dcda771b1467b93b267c8 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:06:10 +0000 Subject: [PATCH 05/63] t3du - Fix DOF condition --- leenkx/blender/lnx/make_renderpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index f728472..46698a7 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -240,7 +240,7 @@ def build(): compo_depth = True focus_distance = 0.0 - if len(bpy.data.cameras) > 0 and lnx.utils.get_active_scene().camera.data.dof.use_dof: + if lnx.utils.get_active_scene().camera and lnx.utils.get_active_scene().camera.data.dof.use_dof: focus_distance = lnx.utils.get_active_scene().camera.data.dof.focus_distance if focus_distance > 0.0: From c7aba23fa40fb78ddf7906cf7f675ab46808009c Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:08:21 +0000 Subject: [PATCH 06/63] t3du - Fix DOF condition --- leenkx/blender/lnx/write_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index a613815..5a9a6a6 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -818,7 +818,7 @@ const int compoChromaticSamples = {rpdat.lnx_chromatic_aberration_samples}; focus_distance = 0.0 fstop = 0.0 - if len(bpy.data.cameras) > 0 and lnx.utils.get_active_scene().camera.data.dof.use_dof: + if lnx.utils.get_active_scene().camera and lnx.utils.get_active_scene().camera.data.dof.use_dof: focus_distance = lnx.utils.get_active_scene().camera.data.dof.focus_distance fstop = lnx.utils.get_active_scene().camera.data.dof.aperture_fstop lens = lnx.utils.get_active_scene().camera.data.lens From de0b1075c293fcd9adce055a9596582de002cd01 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:13:16 +0000 Subject: [PATCH 07/63] moisesjpelaez - Time Fix --- leenkx/Sources/iron/system/Time.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/leenkx/Sources/iron/system/Time.hx b/leenkx/Sources/iron/system/Time.hx index 33684a5..0ff7dbd 100644 --- a/leenkx/Sources/iron/system/Time.hx +++ b/leenkx/Sources/iron/system/Time.hx @@ -39,11 +39,11 @@ class Time { } public static inline function time(): Float { - return kha.Scheduler.time(); + return kha.Scheduler.time() * scale; } public static inline function realTime(): Float { - return kha.Scheduler.realTime(); + return kha.Scheduler.realTime() * scale; } public static function update() { From 43be7729bafd454526e580679bac2f1ca56960f0 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:17:41 +0000 Subject: [PATCH 08/63] moisesjpelaez - Tween var --- leenkx/Sources/iron/system/Tween.hx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/leenkx/Sources/iron/system/Tween.hx b/leenkx/Sources/iron/system/Tween.hx index 2cafc8c..e0c11ed 100644 --- a/leenkx/Sources/iron/system/Tween.hx +++ b/leenkx/Sources/iron/system/Tween.hx @@ -94,34 +94,34 @@ class Tween { // Way too much Reflect trickery.. var ps = Reflect.fields(a.props); - for (i in 0...ps.length) { - var p = ps[i]; + for (j in 0...ps.length) { + var p = ps[j]; var k = a._time / a.duration; if (k > 1) k = 1; - if (a._comps[i] == 1) { - var fromVal: Float = a._x[i]; + if (a._comps[j] == 1) { + var fromVal: Float = a._x[j]; var toVal: Float = Reflect.getProperty(a.props, p); var val: Float = fromVal + (toVal - fromVal) * eases[a.ease](k); Reflect.setProperty(a.target, p, val); } - else { // _comps[i] == 4 + else { // _comps[j] == 4 var obj = Reflect.getProperty(a.props, p); var toX: Float = Reflect.getProperty(obj, "x"); var toY: Float = Reflect.getProperty(obj, "y"); var toZ: Float = Reflect.getProperty(obj, "z"); var toW: Float = Reflect.getProperty(obj, "w"); - if (a._normalize[i]) { - var qdot = (a._x[i] * toX) + (a._y[i] * toY) + (a._z[i] * toZ) + (a._w[i] * toW); + if (a._normalize[j]) { + var qdot = (a._x[j] * toX) + (a._y[j] * toY) + (a._z[j] * toZ) + (a._w[j] * toW); if (qdot < 0.0) { toX = -toX; toY = -toY; toZ = -toZ; toW = -toW; } } - var x: Float = a._x[i] + (toX - a._x[i]) * eases[a.ease](k); - var y: Float = a._y[i] + (toY - a._y[i]) * eases[a.ease](k); - var z: Float = a._z[i] + (toZ - a._z[i]) * eases[a.ease](k); - var w: Float = a._w[i] + (toW - a._w[i]) * eases[a.ease](k); - if (a._normalize[i]) { + var x: Float = a._x[j] + (toX - a._x[j]) * eases[a.ease](k); + var y: Float = a._y[j] + (toY - a._y[j]) * eases[a.ease](k); + var z: Float = a._z[j] + (toZ - a._z[j]) * eases[a.ease](k); + var w: Float = a._w[j] + (toW - a._w[j]) * eases[a.ease](k); + if (a._normalize[j]) { var l = Math.sqrt(x * x + y * y + z * z + w * w); if (l > 0.0) { l = 1.0 / l; From 3d99fa60c0eab7c64524671664e2db76c9fec49c Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:23:42 +0000 Subject: [PATCH 09/63] moisesjpelaez - General Material Updates --- leenkx/Sources/iron/RenderPath.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/Sources/iron/RenderPath.hx b/leenkx/Sources/iron/RenderPath.hx index dcfd92e..865ad6c 100644 --- a/leenkx/Sources/iron/RenderPath.hx +++ b/leenkx/Sources/iron/RenderPath.hx @@ -338,7 +338,7 @@ class RenderPath { if (depthDiff != 0) return depthDiff; #end - return a.materials[0].name >= b.materials[0].name ? 1 : -1; + return a.materials[0].shader.sortingOrder >= b.materials[0].shader.sortingOrder ? 1 : -1; }); } From 8b695f72bbe0f955c1c1214f1c8122f0fe27073e Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:25:03 +0000 Subject: [PATCH 10/63] moisesjpelaez - General Material Updates --- leenkx/Sources/iron/data/SceneFormat.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/leenkx/Sources/iron/data/SceneFormat.hx b/leenkx/Sources/iron/data/SceneFormat.hx index fb5619b..e02100f 100644 --- a/leenkx/Sources/iron/data/SceneFormat.hx +++ b/leenkx/Sources/iron/data/SceneFormat.hx @@ -222,6 +222,8 @@ typedef TShaderData = { @:structInit class TShaderData { #end public var name: String; + public var sortingOrder: Int; + public var nextPass: String; public var contexts: Array; } From afe89c3834e6efe7a50d6ee97109b686585fbee9 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:27:14 +0000 Subject: [PATCH 11/63] Update leenkx/Sources/iron/data/ShaderData.hx --- leenkx/Sources/iron/data/ShaderData.hx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/leenkx/Sources/iron/data/ShaderData.hx b/leenkx/Sources/iron/data/ShaderData.hx index 6d5d395..269ac35 100644 --- a/leenkx/Sources/iron/data/ShaderData.hx +++ b/leenkx/Sources/iron/data/ShaderData.hx @@ -22,6 +22,8 @@ using StringTools; class ShaderData { public var name: String; + public var sortingOrder: Int; + public var nextPass: String; public var raw: TShaderData; public var contexts: Array = []; @@ -33,6 +35,8 @@ class ShaderData { public function new(raw: TShaderData, done: ShaderData->Void, overrideContext: TShaderOverride = null) { this.raw = raw; this.name = raw.name; + this.sortingOrder = raw.sortingOrder; + this.nextPass = raw.nextPass; for (c in raw.contexts) contexts.push(null); var contextsLoaded = 0; From 6eeb9017d4e117a969e2504956cb678d6988b8a9 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:30:42 +0000 Subject: [PATCH 12/63] moisesjpelaez - General Material Updates --- leenkx/Sources/iron/object/MeshObject.hx | 85 ++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/leenkx/Sources/iron/object/MeshObject.hx b/leenkx/Sources/iron/object/MeshObject.hx index 456d106..f378e25 100644 --- a/leenkx/Sources/iron/object/MeshObject.hx +++ b/leenkx/Sources/iron/object/MeshObject.hx @@ -302,6 +302,10 @@ class MeshObject extends Object { // Render mesh var ldata = lod.data; + + // Next pass rendering first (inverse order) + renderNextPass(g, context, bindParams, lod); + for (i in 0...ldata.geom.indexBuffers.length) { var mi = ldata.geom.materialIndices[i]; @@ -405,4 +409,85 @@ class MeshObject extends Object { } } } + + + function renderNextPass(g: Graphics, context: String, bindParams: Array, lod: MeshObject) { + var ldata = lod.data; + for (i in 0...ldata.geom.indexBuffers.length) { + var mi = ldata.geom.materialIndices[i]; + if (mi >= materials.length) continue; + + var currentMaterial: MaterialData = materials[mi]; + if (currentMaterial == null || currentMaterial.shader == null) continue; + + var nextPassName: String = currentMaterial.shader.nextPass; + if (nextPassName == null || nextPassName == "") continue; + + var nextMaterial: MaterialData = null; + for (mat in materials) { + // First try exact match + if (mat.name == nextPassName) { + nextMaterial = mat; + break; + } + // If no exact match, try to match base name for linked materials + if (mat.name.indexOf("_") > 0 && mat.name.substr(mat.name.length - 6) == ".blend") { + var baseName = mat.name.substring(0, mat.name.indexOf("_")); + if (baseName == nextPassName) { + nextMaterial = mat; + break; + } + } + } + + if (nextMaterial == null) continue; + + var nextMaterialContext: MaterialContext = null; + var nextShaderContext: ShaderContext = null; + + for (j in 0...nextMaterial.raw.contexts.length) { + if (nextMaterial.raw.contexts[j].name.substr(0, context.length) == context) { + nextMaterialContext = nextMaterial.contexts[j]; + nextShaderContext = nextMaterial.shader.getContext(context); + break; + } + } + + if (nextShaderContext == null) continue; + if (skipContext(context, nextMaterial)) continue; + + var elems = nextShaderContext.raw.vertex_elements; + + // Uniforms + if (nextShaderContext.pipeState != lastPipeline) { + g.setPipeline(nextShaderContext.pipeState); + lastPipeline = nextShaderContext.pipeState; + } + Uniforms.setContextConstants(g, nextShaderContext, bindParams); + Uniforms.setObjectConstants(g, nextShaderContext, this); + Uniforms.setMaterialConstants(g, nextShaderContext, nextMaterialContext); + + // VB / IB + #if lnx_deinterleaved + g.setVertexBuffers(ldata.geom.get(elems)); + #else + if (ldata.geom.instancedVB != null) { + g.setVertexBuffers([ldata.geom.get(elems), ldata.geom.instancedVB]); + } + else { + g.setVertexBuffer(ldata.geom.get(elems)); + } + #end + + g.setIndexBuffer(ldata.geom.indexBuffers[i]); + + // Draw next pass for this specific geometry section + if (ldata.geom.instanced) { + g.drawIndexedVerticesInstanced(ldata.geom.instanceCount, ldata.geom.start, ldata.geom.count); + } + else { + g.drawIndexedVertices(ldata.geom.start, ldata.geom.count); + } + } + } } From f659a3c2be6187b9407f57fa082118b56a924f81 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:32:38 +0000 Subject: [PATCH 13/63] moisesjpelaez - General Material Updates --- leenkx/blender/lnx/exporter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index 0db817e..8980b43 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -2208,6 +2208,9 @@ class LeenkxExporter: elif material.lnx_cull_mode != 'clockwise': o['override_context'] = {} o['override_context']['cull_mode'] = material.lnx_cull_mode + if material.lnx_compare_mode != 'less': + o['override_context'] = {} + o['override_context']['compare_mode'] = material.lnx_compare_mode o['contexts'] = [] From 1f3d1b47ae720795f4b96f239e599834decf18d1 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:34:27 +0000 Subject: [PATCH 14/63] moisesjpelaez - General Material Updates --- leenkx/blender/lnx/material/make_mesh.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/material/make_mesh.py b/leenkx/blender/lnx/material/make_mesh.py index 6fe882c..41a756f 100644 --- a/leenkx/blender/lnx/material/make_mesh.py +++ b/leenkx/blender/lnx/material/make_mesh.py @@ -58,7 +58,6 @@ def make(context_id, rpasses): con['alpha_blend_destination'] = mat.lnx_blending_destination_alpha con['alpha_blend_operation'] = mat.lnx_blending_operation_alpha con['depth_write'] = False - con['compare_mode'] = 'less' elif particle: pass # Depth prepass was performed, exclude mat with depth read that @@ -66,6 +65,9 @@ def make(context_id, rpasses): elif dprepass and not (rpdat.rp_depth_texture and mat.lnx_depth_read): con['depth_write'] = False con['compare_mode'] = 'equal' + else: + con['depth_write'] = mat.lnx_depth_write + con['compare_mode'] = mat.lnx_compare_mode attachment_format = 'RGBA32' if '_LDR' in wrd.world_defs else 'RGBA64' con['color_attachments'] = [attachment_format, attachment_format] From 8fe758862cc0bc33d627acd9b26d5e1cf13bc548 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:35:59 +0000 Subject: [PATCH 15/63] moisesjpelaez - General Material Updates --- leenkx/blender/lnx/material/shader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/leenkx/blender/lnx/material/shader.py b/leenkx/blender/lnx/material/shader.py index 204472f..f19daa4 100644 --- a/leenkx/blender/lnx/material/shader.py +++ b/leenkx/blender/lnx/material/shader.py @@ -23,6 +23,8 @@ class ShaderData: self.data = {'shader_datas': [self.sd]} self.matname = lnx.utils.safesrc(lnx.utils.asset_name(material)) self.sd['name'] = self.matname + '_data' + self.sd['sortingOrder'] = material.lnx_sorting_order + self.sd['nextPass'] = material.lnx_next_pass self.sd['contexts'] = [] def add_context(self, props) -> 'ShaderContext': From 024676f43a1398fe00b858b58380bfee572d13e3 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 17:43:54 +0000 Subject: [PATCH 16/63] moisesjpelaez - General Material Updates --- leenkx/blender/lnx/props.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index e8babe4..5decf0b 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -436,6 +436,18 @@ def init_properties(): bpy.types.Material.lnx_depth_read = BoolProperty(name="Read Depth", description="Allow this material to read from a depth texture which is copied from the depth buffer. The meshes using this material will be drawn after all meshes that don't read from the depth texture", default=False) bpy.types.Material.lnx_overlay = BoolProperty(name="Overlay", description="Renders the material, unshaded, over other shaded materials", default=False) bpy.types.Material.lnx_decal = BoolProperty(name="Decal", default=False) + bpy.types.Material.lnx_compare_mode = EnumProperty( + items=[ + ('always', 'Always', 'Always'), + ('never', 'Never', 'Never'), + ('less', 'Less', 'Less'), + ('less_equal', 'Less Equal', 'Less Equal'), + ('greater', 'Greater', 'Greater'), + ('greater_equal', 'Greater Equal', 'Greater Equal'), + ('equal', 'Equal', 'Equal'), + ('not_equal', 'Not Equal', 'Not Equal'), + ], + name="Compare Mode", default='less', description="Comparison mode for the material") bpy.types.Material.lnx_two_sided = BoolProperty(name="Two-Sided", description="Flip normal when drawing back-face", default=False) bpy.types.Material.lnx_ignore_irradiance = BoolProperty(name="Ignore Irradiance", description="Ignore irradiance for material", default=False) bpy.types.Material.lnx_cull_mode = EnumProperty( @@ -443,6 +455,8 @@ def init_properties(): ('clockwise', 'Front', 'Clockwise'), ('counter_clockwise', 'Back', 'Counter-Clockwise')], name="Cull Mode", default='clockwise', description="Draw geometry faces") + bpy.types.Material.lnx_next_pass = StringProperty( + name="Next Pass", default='', description="Next pass for the material", update=assets.invalidate_shader_cache) bpy.types.Material.lnx_discard = BoolProperty(name="Alpha Test", default=False, description="Do not render fragments below specified opacity threshold") bpy.types.Material.lnx_discard_opacity = FloatProperty(name="Mesh Opacity", default=0.2, min=0, max=1) bpy.types.Material.lnx_discard_opacity_shadows = FloatProperty(name="Shadows Opacity", default=0.1, min=0, max=1) @@ -557,6 +571,7 @@ def init_properties(): bpy.types.Material.export_uvs = BoolProperty(name="Export UVs", default=False) bpy.types.Material.export_vcols = BoolProperty(name="Export VCols", default=False) bpy.types.Material.export_tangents = BoolProperty(name="Export Tangents", default=False) + bpy.types.Material.lnx_sorting_order = IntProperty(name="Sorting Order", default=0) bpy.types.Material.lnx_skip_context = StringProperty(name="Skip Context", default='') bpy.types.Material.lnx_material_id = IntProperty(name="ID", default=0) bpy.types.NodeSocket.is_uniform = BoolProperty(name="Is Uniform", description="Mark node sockets to be processed as material uniforms", default=False) From 0e4a6575c7163a0fcf8e689fe7b0f1ee85cbeec6 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:09:04 +0000 Subject: [PATCH 17/63] moisesjpelaez - General Material Updates --- leenkx/blender/lnx/props_ui.py | 55 +++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index cdb6b3f..688908d 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -551,6 +551,51 @@ class LNX_OT_NewCustomMaterial(bpy.types.Operator): return{'FINISHED'} + +class LNX_OT_NextPassMaterialSelector(bpy.types.Operator): + """Select material for next pass""" + bl_idname = "lnx.next_pass_material_selector" + bl_label = "Select Next Pass Material" + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + context.window_manager.popup_menu(self.draw_menu, title="Select Next Pass Material", icon='MATERIAL') + return {'FINISHED'} + + def draw_menu(self, popup, context): + layout = popup.layout + + # Add 'None' option + op = layout.operator("lnx.set_next_pass_material", text="") + op.material_name = "" + + # Add materials from the current object's material slots + if context.object and hasattr(context.object, 'material_slots'): + for slot in context.object.material_slots: + if (slot.material is not None and slot.material != context.material): + op = layout.operator("lnx.set_next_pass_material", text=slot.material.name) + op.material_name = slot.material.name + +class LNX_OT_SetNextPassMaterial(bpy.types.Operator): + """Set the next pass material""" + bl_idname = "lnx.set_next_pass_material" + bl_label = "Set Next Pass Material" + + material_name: StringProperty() + + def execute(self, context): + if context.material: + context.material.lnx_next_pass = self.material_name + # Redraw the UI to update the display + for area in context.screen.areas: + if area.type == 'PROPERTIES': + area.tag_redraw() + return {'FINISHED'} + + + class LNX_PG_BindTexturesListItem(bpy.types.PropertyGroup): uniform_name: StringProperty( name='Uniform Name', @@ -634,18 +679,24 @@ class LNX_PT_MaterialPropsPanel(bpy.types.Panel): mat = bpy.context.material if mat is None: return - + + layout.prop(mat, 'lnx_sorting_order') layout.prop(mat, 'lnx_cast_shadow') columnb = layout.column() wrd = bpy.data.worlds['Lnx'] columnb.enabled = len(wrd.lnx_rplist) > 0 and lnx.utils.get_rp().rp_renderer == 'Forward' columnb.prop(mat, 'lnx_receive_shadow') layout.prop(mat, 'lnx_ignore_irradiance') + layout.prop(mat, 'lnx_compare_mode') layout.prop(mat, 'lnx_two_sided') columnb = layout.column() columnb.enabled = not mat.lnx_two_sided columnb.prop(mat, 'lnx_cull_mode') + row = layout.row(align=True) + row.prop(mat, 'lnx_next_pass', text="Next Pass") + row.operator('lnx.next_pass_material_selector', text='', icon='MATERIAL') layout.prop(mat, 'lnx_material_id') + layout.prop(mat, 'lnx_depth_write') layout.prop(mat, 'lnx_depth_read') layout.prop(mat, 'lnx_overlay') layout.prop(mat, 'lnx_decal') @@ -2908,6 +2959,8 @@ __REG_CLASSES = ( InvalidateCacheButton, InvalidateMaterialCacheButton, LNX_OT_NewCustomMaterial, + LNX_OT_NextPassMaterialSelector, + LNX_OT_SetNextPassMaterial, LNX_PG_BindTexturesListItem, LNX_UL_BindTexturesList, LNX_OT_BindTexturesListNewItem, From 7f58e0fc859d88c4b0d8dade3d14a2b05e8e821c Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:13:00 +0000 Subject: [PATCH 18/63] moisesjpelaez - General Fixes --- leenkx/Sources/leenkx/system/Starter.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/Sources/leenkx/system/Starter.hx b/leenkx/Sources/leenkx/system/Starter.hx index 7700804..7fd39ba 100644 --- a/leenkx/Sources/leenkx/system/Starter.hx +++ b/leenkx/Sources/leenkx/system/Starter.hx @@ -57,7 +57,7 @@ class Starter { iron.Scene.getRenderPath = getRenderPath; #end #if lnx_draworder_shader - iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Shader; + iron.RenderPath.active.drawOrder = iron.RenderPath.DrawOrder.Index; #end // else Distance }); }); From 0d2b152ccb0cf64f6867ae9bd0290c217c356b4e Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:15:23 +0000 Subject: [PATCH 19/63] moisesjpelaez - General Fixes --- leenkx/Sources/iron/RenderPath.hx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/leenkx/Sources/iron/RenderPath.hx b/leenkx/Sources/iron/RenderPath.hx index 865ad6c..6cc7e2a 100644 --- a/leenkx/Sources/iron/RenderPath.hx +++ b/leenkx/Sources/iron/RenderPath.hx @@ -331,15 +331,18 @@ class RenderPath { }); } - public static function sortMeshesShader(meshes: Array) { + public static function sortMeshesIndex(meshes: Array) { meshes.sort(function(a, b): Int { #if rp_depth_texture var depthDiff = boolToInt(a.depthRead) - boolToInt(b.depthRead); if (depthDiff != 0) return depthDiff; #end - return a.materials[0].shader.sortingOrder >= b.materials[0].shader.sortingOrder ? 1 : -1; - }); + if (a.data.sortingIndex != b.data.sortingIndex) { + return a.data.sortingIndex > b.data.sortingIndex ? 1 : -1; + } + + return a.data.name >= b.data.name ? 1 : -1; }); } public function drawMeshes(context: String) { @@ -399,7 +402,7 @@ class RenderPath { #if lnx_batch sortMeshesDistance(Scene.active.meshBatch.nonBatched); #else - drawOrder == DrawOrder.Shader ? sortMeshesShader(meshes) : sortMeshesDistance(meshes); + drawOrder == DrawOrder.Index ? sortMeshesIndex(meshes) : sortMeshesDistance(meshes); #end meshesSorted = true; } @@ -914,6 +917,6 @@ class CachedShaderContext { @:enum abstract DrawOrder(Int) from Int { var Distance = 0; // Early-z - var Shader = 1; // Less state changes + var Index = 1; // Less state changes // var Mix = 2; // Distance buckets sorted by shader } From 1939f19c05974e2ccd9c1213ca83994ac5ef17fa Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:24:19 +0000 Subject: [PATCH 20/63] moisesjpelaez - General Fixes --- leenkx/Sources/iron/data/MeshData.hx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/leenkx/Sources/iron/data/MeshData.hx b/leenkx/Sources/iron/data/MeshData.hx index 7b40e52..cfdb3e7 100644 --- a/leenkx/Sources/iron/data/MeshData.hx +++ b/leenkx/Sources/iron/data/MeshData.hx @@ -9,6 +9,7 @@ import iron.data.SceneFormat; class MeshData { public var name: String; + public var sortingIndex: Int; public var raw: TMeshData; public var format: TSceneFormat; public var geom: Geometry; @@ -23,7 +24,8 @@ class MeshData { public function new(raw: TMeshData, done: MeshData->Void) { this.raw = raw; this.name = raw.name; - + this.sortingIndex = raw.sorting_index; + if (raw.scale_pos != null) scalePos = raw.scale_pos; if (raw.scale_tex != null) scaleTex = raw.scale_tex; From 20cf07cfc3b7b10bb3b5a0d8fea7fe9b1de8758b Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:25:54 +0000 Subject: [PATCH 21/63] moisesjpelaez - General Fixes --- leenkx/Sources/iron/data/SceneFormat.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/Sources/iron/data/SceneFormat.hx b/leenkx/Sources/iron/data/SceneFormat.hx index e02100f..d1f5881 100644 --- a/leenkx/Sources/iron/data/SceneFormat.hx +++ b/leenkx/Sources/iron/data/SceneFormat.hx @@ -49,6 +49,7 @@ typedef TMeshData = { @:structInit class TMeshData { #end public var name: String; + public var sorting_index: Int; public var vertex_arrays: Array; public var index_arrays: Array; @:optional public var dynamic_usage: Null; @@ -222,7 +223,6 @@ typedef TShaderData = { @:structInit class TShaderData { #end public var name: String; - public var sortingOrder: Int; public var nextPass: String; public var contexts: Array; } From 4400e0e9c8ec6127558c3340a8a3be35f22244a8 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:27:22 +0000 Subject: [PATCH 22/63] moisesjpelaez - General Fixes --- leenkx/Sources/iron/data/ShaderData.hx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/leenkx/Sources/iron/data/ShaderData.hx b/leenkx/Sources/iron/data/ShaderData.hx index 269ac35..7ceb98b 100644 --- a/leenkx/Sources/iron/data/ShaderData.hx +++ b/leenkx/Sources/iron/data/ShaderData.hx @@ -22,7 +22,6 @@ using StringTools; class ShaderData { public var name: String; - public var sortingOrder: Int; public var nextPass: String; public var raw: TShaderData; public var contexts: Array = []; @@ -35,8 +34,7 @@ class ShaderData { public function new(raw: TShaderData, done: ShaderData->Void, overrideContext: TShaderOverride = null) { this.raw = raw; this.name = raw.name; - this.sortingOrder = raw.sortingOrder; - this.nextPass = raw.nextPass; + this.nextPass = raw.next_pass; for (c in raw.contexts) contexts.push(null); var contextsLoaded = 0; From cd0a6f6788a5bed467b59224e9fc314a48c33197 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:28:19 +0000 Subject: [PATCH 23/63] Update leenkx/Sources/iron/data/SceneFormat.hx --- leenkx/Sources/iron/data/SceneFormat.hx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/Sources/iron/data/SceneFormat.hx b/leenkx/Sources/iron/data/SceneFormat.hx index d1f5881..cadd3b6 100644 --- a/leenkx/Sources/iron/data/SceneFormat.hx +++ b/leenkx/Sources/iron/data/SceneFormat.hx @@ -223,7 +223,7 @@ typedef TShaderData = { @:structInit class TShaderData { #end public var name: String; - public var nextPass: String; + public var next_pass: String; public var contexts: Array; } From c94fc0fd97812248851ac3d8fe148e757acccb41 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:29:52 +0000 Subject: [PATCH 24/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/exporter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index 8980b43..860bf84 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -1727,6 +1727,7 @@ class LeenkxExporter: tangdata = np.array(tangdata, dtype=' Date: Fri, 19 Sep 2025 18:33:44 +0000 Subject: [PATCH 25/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/exporter_opt.py | 891 +++++++++++++++-------------- 1 file changed, 446 insertions(+), 445 deletions(-) diff --git a/leenkx/blender/lnx/exporter_opt.py b/leenkx/blender/lnx/exporter_opt.py index ed12025..eff669f 100644 --- a/leenkx/blender/lnx/exporter_opt.py +++ b/leenkx/blender/lnx/exporter_opt.py @@ -1,445 +1,446 @@ -""" -Exports smaller geometry but is slower. -To be replaced with https://github.com/zeux/meshoptimizer -""" -from typing import Optional - -import bpy -from mathutils import Vector -import numpy as np - -import lnx.utils -from lnx import log - -if lnx.is_reload(__name__): - log = lnx.reload_module(log) - lnx.utils = lnx.reload_module(lnx.utils) -else: - lnx.enable_reload(__name__) - - -class Vertex: - __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index") - - def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Optional[bpy.types.Attribute]): - self.vertex_index = loop.vertex_index - loop_idx = loop.index - self.co = mesh.vertices[self.vertex_index].co[:] - self.normal = loop.normal[:] - self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers) - self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] - self.loop_indices = [loop_idx] - self.index = 0 - - def __hash__(self): - return hash((self.co, self.normal, self.uvs)) - - def __eq__(self, other): - eq = ( - (self.co == other.co) and - (self.normal == other.normal) and - (self.uvs == other.uvs) and - (self.col == other.col) - ) - if eq: - indices = self.loop_indices + other.loop_indices - self.loop_indices = indices - other.loop_indices = indices - return eq - - -def calc_tangents(posa, nora, uva, ias, scale_pos): - num_verts = int(len(posa) / 4) - tangents = np.empty(num_verts * 3, dtype=' 0 - if self.has_baked_material(bobject, export_mesh.materials): - has_tex = True - has_tex1 = has_tex and num_uv_layers > 1 - num_colors = self.get_num_vertex_colors(export_mesh) - has_col = self.get_export_vcols(export_mesh) and num_colors > 0 - has_tang = self.has_tangents(export_mesh) - - pdata = np.empty(num_verts * 4, dtype=' maxdim: - maxdim = abs(v.uv[0]) - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) - if has_tex1: - lay1 = uv_layers[t1map] - for v in lay1.data: - if abs(v.uv[0]) > maxdim: - maxdim = abs(v.uv[0]) - maxdim_uvlayer = lay1 - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) - maxdim_uvlayer = lay1 - if has_morph_target: - morph_data = np.empty(num_verts * 2, dtype=' maxdim: - maxdim = abs(v.uv[0]) - maxdim_uvlayer = lay2 - if abs(v.uv[1]) > maxdim: - maxdim = abs(v.uv[1]) - maxdim_uvlayer = lay2 - if maxdim > 1: - o['scale_tex'] = maxdim - invscale_tex = (1 / o['scale_tex']) * 32767 - else: - invscale_tex = 1 * 32767 - self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) - - if has_col: - cdata = np.empty(num_verts * 3, dtype=' 2: - o['scale_pos'] = maxdim / 2 - else: - o['scale_pos'] = 1.0 - if has_armature: # Allow up to 2x bigger bounds for skinned mesh - o['scale_pos'] *= 2.0 - - scale_pos = o['scale_pos'] - invscale_pos = (1 / scale_pos) * 32767 - - # Make arrays - for i, v in enumerate(vert_list): - v.index = i - co = v.co - normal = v.normal - i4 = i * 4 - i2 = i * 2 - pdata[i4 ] = co[0] - pdata[i4 + 1] = co[1] - pdata[i4 + 2] = co[2] - pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale - ndata[i2 ] = normal[0] - ndata[i2 + 1] = normal[1] - if has_tex: - uv = v.uvs[t0map] - t0data[i2 ] = uv[0] - t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y - if has_tex1: - uv = v.uvs[t1map] - t1data[i2 ] = uv[0] - t1data[i2 + 1] = 1.0 - uv[1] - if has_morph_target: - uv = v.uvs[morph_uv_index] - morph_data[i2 ] = uv[0] - morph_data[i2 + 1] = 1.0 - uv[1] - if has_col: - i3 = i * 3 - cdata[i3 ] = v.col[0] - cdata[i3 + 1] = v.col[1] - cdata[i3 + 2] = v.col[2] - - # Indices - # Create dict for every material slot - prims = {ma.name if ma else '': [] for ma in export_mesh.materials} - v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials} - if not prims: - # No materials - prims = {'': []} - v_maps = {'': []} - - # Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list - vert_dict = {i : v for v in vert_list for i in v.loop_indices} - # For each polygon in a mesh - for poly in export_mesh.polygons: - # Index of the first loop of this polygon - first = poly.loop_start - # No materials assigned - if len(export_mesh.materials) == 0: - # Get prim - prim = prims[''] - v_map = v_maps[''] - else: - # First material - mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] - # Get prim for this material - prim = prims[mat.name if mat else ''] - v_map = v_maps[mat.name if mat else ''] - # List of indices for each loop_index belonging to this polygon - indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] - v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)] - - # If 3 loops per polygon (Triangle?) - if poly.loop_total == 3: - prim += indices - v_map += v_indices - # If > 3 loops per polygon (Non-Triangular?) - elif poly.loop_total > 3: - for i in range(poly.loop_total-2): - prim += (indices[-1], indices[i], indices[i + 1]) - v_map += (v_indices[-1], v_indices[i], v_indices[i + 1]) - - # Write indices - o['index_arrays'] = [] - for mat, prim in prims.items(): - idata = [0] * len(prim) - v_map_data = [0] * len(prim) - v_map_sub = v_maps[mat] - for i, v in enumerate(prim): - idata[i] = v - v_map_data[i] = v_map_sub[i] - if len(idata) == 0: # No face assigned - continue - ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data} - # Find material index for multi-mat mesh - if len(export_mesh.materials) > 1: - for i in range(0, len(export_mesh.materials)): - if (export_mesh.materials[i] is not None and mat == export_mesh.materials[i].name) or \ - (export_mesh.materials[i] is None and mat == ''): # Default material for empty slots - ia['material'] = i - break - o['index_arrays'].append(ia) - - if has_tang: - tangdata = calc_tangents(pdata, ndata, t0data, o['index_arrays'], scale_pos) - - pdata *= invscale_pos - ndata *= 32767 - pdata = np.array(pdata, dtype=' max_bones: - log.warn(bobject.name + ' - ' + str(bone_count) + ' bones found, exceeds maximum of ' + str(max_bones) + ' bones defined - raise the value in Camera Data - Leenkx Render Props - Max Bones') - - for i in range(bone_count): - boneRef = self.find_bone(bone_array[i].name) - if boneRef: - oskin['bone_ref_array'].append(boneRef[1]["structName"]) - oskin['bone_len_array'].append(bone_array[i].length) - else: - oskin['bone_ref_array'].append("") - oskin['bone_len_array'].append(0.0) - - # Write the bind pose transform array - oskin['transformsI'] = [] - for i in range(bone_count): - skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe() - skeletonI = (skeletonI @ bobject.matrix_world) - oskin['transformsI'].append(self.write_matrix(skeletonI)) - - # Export the per-vertex bone influence data - group_remap = [] - for group in bobject.vertex_groups: - for i in range(bone_count): - if bone_array[i].name == group.name: - group_remap.append(i) - break - else: - group_remap.append(-1) - - bone_count_array = np.empty(len(vert_list), dtype='= 0: #and bone_weight != 0.0: - bone_values.append((bone_weight, bone_index)) - total_weight += bone_weight - bone_count += 1 - - if bone_count > 4: - bone_count = 4 - bone_values.sort(reverse=True) - bone_values = bone_values[:4] - - bone_count_array[index] = bone_count - for bv in bone_values: - bone_weight_array[count] = bv[0] * 32767 - bone_index_array[count] = bv[1] - count += 1 - - if total_weight not in (0.0, 1.0): - normalizer = 1.0 / total_weight - for i in range(bone_count): - bone_weight_array[count - i - 1] *= normalizer - - oskin['bone_count_array'] = bone_count_array - oskin['bone_index_array'] = bone_index_array[:count] - oskin['bone_weight_array'] = bone_weight_array[:count] - - # Bone constraints - for bone in armature.pose.bones: - if len(bone.constraints) > 0: - if 'constraints' not in oskin: - oskin['constraints'] = [] - self.add_constraints(bone, oskin, bone=True) +""" +Exports smaller geometry but is slower. +To be replaced with https://github.com/zeux/meshoptimizer +""" +from typing import Optional + +import bpy +from mathutils import Vector +import numpy as np + +import lnx.utils +from lnx import log + +if lnx.is_reload(__name__): + log = lnx.reload_module(log) + lnx.utils = lnx.reload_module(lnx.utils) +else: + lnx.enable_reload(__name__) + + +class Vertex: + __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index") + + def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Optional[bpy.types.Attribute]): + self.vertex_index = loop.vertex_index + loop_idx = loop.index + self.co = mesh.vertices[self.vertex_index].co[:] + self.normal = loop.normal[:] + self.uvs = tuple(layer.data[loop_idx].uv[:] for layer in mesh.uv_layers) + self.col = [0.0, 0.0, 0.0] if vcol0 is None else vcol0.data[loop_idx].color[:] + self.loop_indices = [loop_idx] + self.index = 0 + + def __hash__(self): + return hash((self.co, self.normal, self.uvs)) + + def __eq__(self, other): + eq = ( + (self.co == other.co) and + (self.normal == other.normal) and + (self.uvs == other.uvs) and + (self.col == other.col) + ) + if eq: + indices = self.loop_indices + other.loop_indices + self.loop_indices = indices + other.loop_indices = indices + return eq + + +def calc_tangents(posa, nora, uva, ias, scale_pos): + num_verts = int(len(posa) / 4) + tangents = np.empty(num_verts * 3, dtype=' 0 # TODO FIXME: this should use an `and` instead of `or`. Workaround to completely ignore if the mesh has the `export_uvs` flag. Only checking the `uv_layers` to bypass issues with materials in linked objects. + if self.has_baked_material(bobject, export_mesh.materials): + has_tex = True + has_tex1 = has_tex and num_uv_layers > 1 + num_colors = self.get_num_vertex_colors(export_mesh) + has_col = self.get_export_vcols(export_mesh) and num_colors > 0 + has_tang = self.has_tangents(export_mesh) + + pdata = np.empty(num_verts * 4, dtype=' maxdim: + maxdim = abs(v.uv[0]) + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + if has_tex1: + lay1 = uv_layers[t1map] + for v in lay1.data: + if abs(v.uv[0]) > maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay1 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay1 + if has_morph_target: + morph_data = np.empty(num_verts * 2, dtype=' maxdim: + maxdim = abs(v.uv[0]) + maxdim_uvlayer = lay2 + if abs(v.uv[1]) > maxdim: + maxdim = abs(v.uv[1]) + maxdim_uvlayer = lay2 + if maxdim > 1: + o['scale_tex'] = maxdim + invscale_tex = (1 / o['scale_tex']) * 32767 + else: + invscale_tex = 1 * 32767 + self.check_uv_precision(export_mesh, maxdim, maxdim_uvlayer, invscale_tex) + + if has_col: + cdata = np.empty(num_verts * 3, dtype=' 2: + o['scale_pos'] = maxdim / 2 + else: + o['scale_pos'] = 1.0 + if has_armature: # Allow up to 2x bigger bounds for skinned mesh + o['scale_pos'] *= 2.0 + + scale_pos = o['scale_pos'] + invscale_pos = (1 / scale_pos) * 32767 + + # Make arrays + for i, v in enumerate(vert_list): + v.index = i + co = v.co + normal = v.normal + i4 = i * 4 + i2 = i * 2 + pdata[i4 ] = co[0] + pdata[i4 + 1] = co[1] + pdata[i4 + 2] = co[2] + pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale + ndata[i2 ] = normal[0] + ndata[i2 + 1] = normal[1] + if has_tex: + uv = v.uvs[t0map] + t0data[i2 ] = uv[0] + t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y + if has_tex1: + uv = v.uvs[t1map] + t1data[i2 ] = uv[0] + t1data[i2 + 1] = 1.0 - uv[1] + if has_morph_target: + uv = v.uvs[morph_uv_index] + morph_data[i2 ] = uv[0] + morph_data[i2 + 1] = 1.0 - uv[1] + if has_col: + i3 = i * 3 + cdata[i3 ] = v.col[0] + cdata[i3 + 1] = v.col[1] + cdata[i3 + 2] = v.col[2] + + # Indices + # Create dict for every material slot + prims = {ma.name if ma else '': [] for ma in export_mesh.materials} + v_maps = {ma.name if ma else '': [] for ma in export_mesh.materials} + if not prims: + # No materials + prims = {'': []} + v_maps = {'': []} + + # Create dict of {loop_indices : vertex} with each loop_index in each vertex in Vertex_list + vert_dict = {i : v for v in vert_list for i in v.loop_indices} + # For each polygon in a mesh + for poly in export_mesh.polygons: + # Index of the first loop of this polygon + first = poly.loop_start + # No materials assigned + if len(export_mesh.materials) == 0: + # Get prim + prim = prims[''] + v_map = v_maps[''] + else: + # First material + mat = export_mesh.materials[min(poly.material_index, len(export_mesh.materials) - 1)] + # Get prim for this material + prim = prims[mat.name if mat else ''] + v_map = v_maps[mat.name if mat else ''] + # List of indices for each loop_index belonging to this polygon + indices = [vert_dict[i].index for i in range(first, first+poly.loop_total)] + v_indices = [vert_dict[i].vertex_index for i in range(first, first+poly.loop_total)] + + # If 3 loops per polygon (Triangle?) + if poly.loop_total == 3: + prim += indices + v_map += v_indices + # If > 3 loops per polygon (Non-Triangular?) + elif poly.loop_total > 3: + for i in range(poly.loop_total-2): + prim += (indices[-1], indices[i], indices[i + 1]) + v_map += (v_indices[-1], v_indices[i], v_indices[i + 1]) + + # Write indices + o['index_arrays'] = [] + for mat, prim in prims.items(): + idata = [0] * len(prim) + v_map_data = [0] * len(prim) + v_map_sub = v_maps[mat] + for i, v in enumerate(prim): + idata[i] = v + v_map_data[i] = v_map_sub[i] + if len(idata) == 0: # No face assigned + continue + ia = {'values': idata, 'material': 0, 'vertex_map': v_map_data} + # Find material index for multi-mat mesh + if len(export_mesh.materials) > 1: + for i in range(0, len(export_mesh.materials)): + if (export_mesh.materials[i] is not None and mat == export_mesh.materials[i].name) or \ + (export_mesh.materials[i] is None and mat == ''): # Default material for empty slots + ia['material'] = i + break + o['index_arrays'].append(ia) + + if has_tang: + tangdata = calc_tangents(pdata, ndata, t0data, o['index_arrays'], scale_pos) + + pdata *= invscale_pos + ndata *= 32767 + pdata = np.array(pdata, dtype=' max_bones: + log.warn(bobject.name + ' - ' + str(bone_count) + ' bones found, exceeds maximum of ' + str(max_bones) + ' bones defined - raise the value in Camera Data - Leenkx Render Props - Max Bones') + + for i in range(bone_count): + boneRef = self.find_bone(bone_array[i].name) + if boneRef: + oskin['bone_ref_array'].append(boneRef[1]["structName"]) + oskin['bone_len_array'].append(bone_array[i].length) + else: + oskin['bone_ref_array'].append("") + oskin['bone_len_array'].append(0.0) + + # Write the bind pose transform array + oskin['transformsI'] = [] + for i in range(bone_count): + skeletonI = (armature.matrix_world @ bone_array[i].matrix_local).inverted_safe() + skeletonI = (skeletonI @ bobject.matrix_world) + oskin['transformsI'].append(self.write_matrix(skeletonI)) + + # Export the per-vertex bone influence data + group_remap = [] + for group in bobject.vertex_groups: + for i in range(bone_count): + if bone_array[i].name == group.name: + group_remap.append(i) + break + else: + group_remap.append(-1) + + bone_count_array = np.empty(len(vert_list), dtype='= 0: #and bone_weight != 0.0: + bone_values.append((bone_weight, bone_index)) + total_weight += bone_weight + bone_count += 1 + + if bone_count > 4: + bone_count = 4 + bone_values.sort(reverse=True) + bone_values = bone_values[:4] + + bone_count_array[index] = bone_count + for bv in bone_values: + bone_weight_array[count] = bv[0] * 32767 + bone_index_array[count] = bv[1] + count += 1 + + if total_weight not in (0.0, 1.0): + normalizer = 1.0 / total_weight + for i in range(bone_count): + bone_weight_array[count - i - 1] *= normalizer + + oskin['bone_count_array'] = bone_count_array + oskin['bone_index_array'] = bone_index_array[:count] + oskin['bone_weight_array'] = bone_weight_array[:count] + + # Bone constraints + for bone in armature.pose.bones: + if len(bone.constraints) > 0: + if 'constraints' not in oskin: + oskin['constraints'] = [] + self.add_constraints(bone, oskin, bone=True) From 9ac37e6dc7df81a844aadb13b96cf0bc6a8b70f3 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:34:42 +0000 Subject: [PATCH 26/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/material/shader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/leenkx/blender/lnx/material/shader.py b/leenkx/blender/lnx/material/shader.py index f19daa4..fa8be4f 100644 --- a/leenkx/blender/lnx/material/shader.py +++ b/leenkx/blender/lnx/material/shader.py @@ -23,8 +23,7 @@ class ShaderData: self.data = {'shader_datas': [self.sd]} self.matname = lnx.utils.safesrc(lnx.utils.asset_name(material)) self.sd['name'] = self.matname + '_data' - self.sd['sortingOrder'] = material.lnx_sorting_order - self.sd['nextPass'] = material.lnx_next_pass + self.sd['next_pass'] = material.lnx_next_pass self.sd['contexts'] = [] def add_context(self, props) -> 'ShaderContext': From 177890bf39e24b4b43eee2c1a044fcbbf2266d4a Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:37:01 +0000 Subject: [PATCH 27/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/props.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index 5decf0b..19ac1db 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -350,6 +350,7 @@ def init_properties(): update=assets.invalidate_instance_cache, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_export = BoolProperty(name="Export", description="Export object data", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_sorting_index = IntProperty(name="Sorting Index", description="Sorting index for the Render's Draw Order", default=0, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_spawn = BoolProperty(name="Spawn", description="Auto-add this object when creating scene", default=True, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_mobile = BoolProperty(name="Mobile", description="Object moves during gameplay", default=False, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_visible = BoolProperty(name="Visible", description="Render this object", default=True, override={'LIBRARY_OVERRIDABLE'}) @@ -571,7 +572,6 @@ def init_properties(): bpy.types.Material.export_uvs = BoolProperty(name="Export UVs", default=False) bpy.types.Material.export_vcols = BoolProperty(name="Export VCols", default=False) bpy.types.Material.export_tangents = BoolProperty(name="Export Tangents", default=False) - bpy.types.Material.lnx_sorting_order = IntProperty(name="Sorting Order", default=0) bpy.types.Material.lnx_skip_context = StringProperty(name="Skip Context", default='') bpy.types.Material.lnx_material_id = IntProperty(name="ID", default=0) bpy.types.NodeSocket.is_uniform = BoolProperty(name="Is Uniform", description="Mark node sockets to be processed as material uniforms", default=False) From 843ef0b05867831f243211d2aad3d5b2885c91cc Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:39:14 +0000 Subject: [PATCH 28/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/props_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index 688908d..bacbf0b 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -63,6 +63,7 @@ class LNX_PT_ObjectPropsPanel(bpy.types.Panel): return col = layout.column() + col.prop(mat, 'lnx_sorting_index') col.prop(obj, 'lnx_export') if not obj.lnx_export: return @@ -680,7 +681,6 @@ class LNX_PT_MaterialPropsPanel(bpy.types.Panel): if mat is None: return - layout.prop(mat, 'lnx_sorting_order') layout.prop(mat, 'lnx_cast_shadow') columnb = layout.column() wrd = bpy.data.worlds['Lnx'] From 35e346be399bb9290529752c76b0e3e6dfc9382c Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:39:54 +0000 Subject: [PATCH 29/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/write_data.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/leenkx/blender/lnx/write_data.py b/leenkx/blender/lnx/write_data.py index 5a9a6a6..e3bd2ec 100644 --- a/leenkx/blender/lnx/write_data.py +++ b/leenkx/blender/lnx/write_data.py @@ -338,8 +338,8 @@ project.addSources('Sources'); if rpdat.lnx_particles != 'Off': assets.add_khafile_def('lnx_particles') - if rpdat.rp_draw_order == 'Shader': - assets.add_khafile_def('lnx_draworder_shader') + if rpdat.rp_draw_order == 'Index': + assets.add_khafile_def('lnx_draworder_index') if lnx.utils.get_viewport_controls() == 'azerty': assets.add_khafile_def('lnx_azerty') From 5288a98440828158901e87fa479327624c9f0e06 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:49:09 +0000 Subject: [PATCH 30/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/exporter.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index 860bf84..c4dd160 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -1980,7 +1980,7 @@ class LeenkxExporter: if bobject.parent is None or bobject.parent.name not in collection.objects: asset_name = lnx.utils.asset_name(bobject) - if collection.library: + if collection.library and not collection.name in self.scene.collection.children: # Add external linked objects # Iron differentiates objects based on their names, # so errors will happen if two objects with the @@ -2399,7 +2399,7 @@ class LeenkxExporter: world = self.scene.world if world is not None: - world_name = lnx.utils.safestr(world.name) + world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name) if world_name not in self.world_array: self.world_array.append(world_name) @@ -2548,12 +2548,12 @@ class LeenkxExporter: if collection.name.startswith(('RigidBodyWorld', 'Trait|')): continue - if self.scene.user_of_id(collection) or collection.library or collection in self.referenced_collections: + if self.scene.user_of_id(collection) or collection in self.referenced_collections: self.export_collection(collection) if not LeenkxExporter.option_mesh_only: if self.scene.camera is not None: - self.output['camera_ref'] = self.scene.camera.name + self.output['camera_ref'] = lnx.utils.asset_name(self.scene.camera) if self.scene.library else self.scene.camera.name else: if self.scene.name == lnx.utils.get_project_scene_name(): log.warn(f'Scene "{self.scene.name}" is missing a camera') @@ -2577,7 +2577,7 @@ class LeenkxExporter: self.export_tilesheets() if self.scene.world is not None: - self.output['world_ref'] = lnx.utils.safestr(self.scene.world.name) + self.output['world_ref'] = lnx.utils.safestr(lnx.utils.asset_name(self.scene.world) if self.scene.world.library else self.scene.world.name) if self.scene.use_gravity: self.output['gravity'] = [self.scene.gravity[0], self.scene.gravity[1], self.scene.gravity[2]] @@ -3380,7 +3380,7 @@ class LeenkxExporter: if mobile_mat: lnx_radiance = False - out_probe = {'name': world.name} + out_probe = {'name': lnx.utils.asset_name(world) if world.library else world.name} if lnx_irradiance: ext = '' if wrd.lnx_minimize else '.json' out_probe['irradiance'] = irrsharmonics + '_irradiance' + ext From 71e57026e1867bf7e374afa9bc93bdebaa04851d Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:53:25 +0000 Subject: [PATCH 31/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/make_world.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/leenkx/blender/lnx/make_world.py b/leenkx/blender/lnx/make_world.py index 1ac33d4..839b204 100644 --- a/leenkx/blender/lnx/make_world.py +++ b/leenkx/blender/lnx/make_world.py @@ -69,7 +69,7 @@ def build(): if rpdat.lnx_irradiance: # Plain background color if '_EnvCol' in world.world_defs: - world_name = lnx.utils.safestr(world.name) + world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name) # Irradiance json file name world.lnx_envtex_name = world_name world.lnx_envtex_irr_name = world_name @@ -99,7 +99,7 @@ def build(): def create_world_shaders(world: bpy.types.World): """Creates fragment and vertex shaders for the given world.""" global shader_datas - world_name = lnx.utils.safestr(world.name) + world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name) pass_name = 'World_' + world_name shader_props = { @@ -160,7 +160,7 @@ def create_world_shaders(world: bpy.types.World): def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: ShaderContext): """Generates the shader code for the given world.""" - world_name = lnx.utils.safestr(world.name) + world_name = lnx.utils.safestr(lnx.utils.asset_name(world) if world.library else world.name) world.world_defs = '' rpdat = lnx.utils.get_rp() wrd = bpy.data.worlds['Lnx'] @@ -175,7 +175,7 @@ def build_node_tree(world: bpy.types.World, frag: Shader, vert: Shader, con: Sha frag.write('fragColor.rgb = backgroundCol;') return - parser_state = ParserState(ParserContext.WORLD, world.name, world) + parser_state = ParserState(ParserContext.WORLD, lnx.utils.asset_name(world) if world.library else world.name, world) parser_state.con = con parser_state.curshader = frag parser_state.frag = frag From 6fc446e7a9c5e0b80fe2595bb802e67742128ea5 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 18:54:44 +0000 Subject: [PATCH 32/63] moisesjpelaez - General Fixes --- leenkx/blender/lnx/write_probes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/write_probes.py b/leenkx/blender/lnx/write_probes.py index abe945b..a57636b 100644 --- a/leenkx/blender/lnx/write_probes.py +++ b/leenkx/blender/lnx/write_probes.py @@ -118,7 +118,8 @@ def render_envmap(target_dir: str, world: bpy.types.World) -> str: scene = bpy.data.scenes['_lnx_envmap_render'] scene.world = world - image_name = f'env_{lnx.utils.safesrc(world.name)}.{ENVMAP_EXT}' + world_name = lnx.utils.asset_name(world) if world.library else world.name + image_name = f'env_{lnx.utils.safesrc(world_name)}.{ENVMAP_EXT}' render_path = os.path.join(target_dir, image_name) scene.render.filepath = render_path From 5f2acb209ecb3735d68f039703ea54973470f479 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:00:50 +0000 Subject: [PATCH 33/63] moisesjpelaez - Include external blend files on build --- leenkx/blender/lnx/make.py | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/leenkx/blender/lnx/make.py b/leenkx/blender/lnx/make.py index fdcb0e9..b938eaa 100644 --- a/leenkx/blender/lnx/make.py +++ b/leenkx/blender/lnx/make.py @@ -116,7 +116,73 @@ def remove_readonly(func, path, excinfo): os.chmod(path, stat.S_IWRITE) func(path) + +appended_scenes = [] + +def load_external_blends(): + global appended_scenes + + wrd = bpy.data.worlds['Lnx'] + if not hasattr(wrd, 'lnx_external_blends_path'): + return + + external_path = getattr(wrd, 'lnx_external_blends_path', '') + if not external_path or not external_path.strip(): + return + + abs_path = bpy.path.abspath(external_path.strip()) + if not os.path.exists(abs_path): + return + + # Walk recursively through all subdirs + for root, dirs, files in os.walk(abs_path): + for filename in files: + if not filename.endswith(".blend"): + continue + + blend_path = os.path.join(root, filename) + try: + with bpy.data.libraries.load(blend_path, link=True) as (data_from, data_to): + data_to.scenes = list(data_from.scenes) + + for scn in data_to.scenes: + if scn is not None and scn not in appended_scenes: + # make name unique with file name + scn.name += "_" + filename.replace(".blend", "") + appended_scenes.append(scn) + + log.info(f"Loaded external blend: {blend_path}") + except Exception as e: + log.error(f"Failed to load external blend {blend_path}: {e}") + +def clear_external_scenes(): + global appended_scenes + if not appended_scenes: + return + + for scn in appended_scenes: + try: + bpy.data.scenes.remove(scn, do_unlink=True) + except Exception as e: + log.error(f"Failed to remove scene {scn.name}: {e}") + + for lib in list(bpy.data.libraries): + try: + if lib.users == 0: + bpy.data.libraries.remove(lib) + except Exception as e: + log.error(f"Failed to remove library {lib.name}: {e}") + + try: + bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True) + except Exception as e: + log.error(f"Failed to purge orphan data: {e}") + + appended_scenes = [] + def export_data(fp, sdk_path): + load_external_blends() + wrd = bpy.data.worlds['Lnx'] rpdat = lnx.utils.get_rp() @@ -323,6 +389,8 @@ def export_data(fp, sdk_path): state.last_resy = resy state.last_scene = scene_name + clear_external_scenes() + def compile(assets_only=False): wrd = bpy.data.worlds['Lnx'] fp = lnx.utils.get_fp() From 9b76f8cca925684eaf5a4e267c373633ad7210df Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:03:22 +0000 Subject: [PATCH 34/63] moisesjpelaez - Include external blend files on build --- leenkx/blender/lnx/props.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index 19ac1db..a333e3a 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -142,6 +142,8 @@ def init_properties(): bpy.types.World.lnx_project_version = StringProperty(name="Version", description="Exported project version", default="1.0.0", update=assets.invalidate_compiler_cache, set=set_version, get=get_version) bpy.types.World.lnx_project_version_autoinc = BoolProperty(name="Auto-increment Build Number", description="Auto-increment build number", default=True, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_project_bundle = StringProperty(name="Bundle", description="Exported project bundle", default="org.leenkx3d", update=assets.invalidate_compiler_cache, set=set_project_bundle, get=get_project_bundle) + # External Blend Files + bpy.types.World.lnx_external_blends_path = StringProperty(name="External Blends", description="Directory containing external blend files to include in export", default="", subtype='DIR_PATH', update=assets.invalidate_compiler_cache) # Android Settings bpy.types.World.lnx_project_android_sdk_min = IntProperty(name="Minimal Version SDK", description="Minimal Version Android SDK", default=23, min=14, max=30, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_project_android_sdk_target = IntProperty(name="Target Version SDK", description="Target Version Android SDK", default=26, min=26, max=30, update=assets.invalidate_compiler_cache) From b458b77e5cc16ef2ed9041975e67e7b4b108cfad Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:04:43 +0000 Subject: [PATCH 35/63] moisesjpelaez - Include external blend files on build --- leenkx/blender/lnx/props_ui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index bacbf0b..9668f99 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -1280,7 +1280,8 @@ class LNX_PT_ProjectModulesPanel(bpy.types.Panel): layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts') layout.prop(wrd, 'lnx_project_root') - + layout.prop(wrd, 'lnx_external_blends_path') + class LnxVirtualInputPanel(bpy.types.Panel): bl_label = "Leenkx Virtual Input" bl_space_type = "PROPERTIES" From 2371e3777e72f3f4a3a6eab4b14ed2adbcf91218 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:08:03 +0000 Subject: [PATCH 36/63] t3du - Probabilistic Index Node --- .../random/LN_probabilistic_index.py | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py diff --git a/leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py b/leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py new file mode 100644 index 0000000..6c68a23 --- /dev/null +++ b/leenkx/blender/lnx/logicnode/random/LN_probabilistic_index.py @@ -0,0 +1,51 @@ +from lnx.logicnode.lnx_nodes import * + + +class ProbabilisticIndexNode(LnxLogicTreeNode): + """This system gets an index based on probabilistic values, + ensuring that the total sum of the probabilities equals 1. + If the probabilities do not sum to 1, they will be adjusted + accordingly to guarantee a total sum of 1. Only one output will be + triggered at a time. + @output index: the index. + """ + + bl_idname = 'LNProbabilisticIndexNode' + bl_label = 'Probabilistic Index' + lnx_section = 'logic' + lnx_version = 1 + + num_choices: IntProperty(default=0, min=0) + + def __init__(self): + array_nodes[str(id(self))] = self + + def lnx_init(self, context): + + self.add_output('LnxIntSocket', 'Index') + + def draw_buttons(self, context, layout): + row = layout.row(align=True) + + op = row.operator('lnx.node_call_func', text='New', icon='PLUS', emboss=True) + op.node_index = str(id(self)) + op.callback_name = 'add_func' + op2 = row.operator('lnx.node_call_func', text='', icon='X', emboss=True) + op2.node_index = str(id(self)) + op2.callback_name = 'remove_func' + + def add_func(self): + self.add_input('LnxFloatSocket', f'Prob Index {self.num_choices}') + self.num_choices += 1 + + def remove_func(self): + if len(self.inputs) > 0: + self.inputs.remove(self.inputs[-1]) + self.num_choices -= 1 + + def draw_label(self) -> str: + if self.num_choices == 0: + return self.bl_label + + return f'{self.bl_label}: [{self.num_choices}]' + From 4c2e6ab26a32c9e7e10127cae93067f6c056e050 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:09:18 +0000 Subject: [PATCH 37/63] t3du - Probabilistic Index Node --- .../logicnode/ProbabilisticIndexNode.hx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 leenkx/Sources/leenkx/logicnode/ProbabilisticIndexNode.hx diff --git a/leenkx/Sources/leenkx/logicnode/ProbabilisticIndexNode.hx b/leenkx/Sources/leenkx/logicnode/ProbabilisticIndexNode.hx new file mode 100644 index 0000000..688915c --- /dev/null +++ b/leenkx/Sources/leenkx/logicnode/ProbabilisticIndexNode.hx @@ -0,0 +1,41 @@ +package leenkx.logicnode; + +class ProbabilisticIndexNode extends LogicNode { + + public function new(tree: LogicTree) { + super(tree); + } + + override function get(from: Int): Dynamic { + + var probs: Array = []; + var probs_acum: Array = []; + var sum: Float = 0; + + for (p in 0...inputs.length){ + probs.push(inputs[p].get()); + sum += probs[p]; + } + + if (sum > 1){ + for (p in 0...probs.length) + probs[p] /= sum; + } + + sum = 0; + for (p in 0...probs.length){ + sum += probs[p]; + probs_acum.push(sum); + } + + var rand: Float = Math.random(); + + for (p in 0...probs.length){ + if (p == 0 && rand <= probs_acum[p]) return p; + else if (0 < p && p < probs.length-1 && probs_acum[p-1] < rand && rand <= probs_acum[p]) return p; + else if (p == probs.length-1 && probs_acum[p-1] < rand) return p; + } + + return null; + } +} \ No newline at end of file From 8e635fb1e95b02e8c99dadb71f00483848c1449f Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:11:47 +0000 Subject: [PATCH 38/63] t3du - Labels for finding nodes --- leenkx/blender/lnx/logicnode/array/LN_array_splice.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/leenkx/blender/lnx/logicnode/array/LN_array_splice.py b/leenkx/blender/lnx/logicnode/array/LN_array_splice.py index 6585c8f..168da40 100644 --- a/leenkx/blender/lnx/logicnode/array/LN_array_splice.py +++ b/leenkx/blender/lnx/logicnode/array/LN_array_splice.py @@ -16,3 +16,9 @@ class ArraySpliceNode(LnxLogicTreeNode): self.add_output('LnxNodeSocketAction', 'Out') self.add_output('LnxNodeSocketArray', 'Array') + + def get_replacement_node(self, node_tree: bpy.types.NodeTree): + if self.lnx_version not in (0, 1): + raise LookupError() + + return NodeReplacement.Identity(self) \ No newline at end of file From 79dc458671a94983d734c4a6e0eb4cb541f1b776 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:15:41 +0000 Subject: [PATCH 39/63] t3du - Labels for finding nodes --- .../lnx/logicnode/event/LN_on_event.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/leenkx/blender/lnx/logicnode/event/LN_on_event.py b/leenkx/blender/lnx/logicnode/event/LN_on_event.py index 49cf813..c6f6130 100644 --- a/leenkx/blender/lnx/logicnode/event/LN_on_event.py +++ b/leenkx/blender/lnx/logicnode/event/LN_on_event.py @@ -17,6 +17,17 @@ class OnEventNode(LnxLogicTreeNode): 'custom': 'Custom' } + def update(self): + if self.property1 != 'custom': + if self.inputs[0].is_linked: + self.label = f'{self.bl_label}: {self.property1}' + else: + self.label = f'{self.bl_label}: {self.property1} {self.inputs[0].get_default_value()}' + elif self.inputs[1].is_linked: + self.label = f'{self.bl_label}: {self.property1}' + else: + self.label = f'{self.bl_label}: {self.property1} {self.inputs[1].get_default_value()}' + def set_mode(self, context): if self.property1 != 'custom': if len(self.inputs) > 1: @@ -25,7 +36,17 @@ class OnEventNode(LnxLogicTreeNode): if len(self.inputs) < 2: self.add_input('LnxNodeSocketAction', 'In') self.inputs.move(1, 0) - + + if self.property1 != 'custom': + if self.inputs[0].is_linked: + self.label = f'{self.bl_label}: {self.property1}' + else: + self.label = f'{self.bl_label}: {self.property1} {self.inputs[0].get_default_value()}' + elif self.inputs[1].is_linked: + self.label = f'{self.bl_label}: {self.property1}' + else: + self.label = f'{self.bl_label}: {self.property1} {self.inputs[1].get_default_value()}' + # Use a new property to preserve compatibility property1: HaxeEnumProperty( 'property1', @@ -52,9 +73,15 @@ class OnEventNode(LnxLogicTreeNode): layout.prop(self, 'property1', text='') def draw_label(self) -> str: - if self.inputs[0].is_linked: - return self.bl_label - return f'{self.bl_label}: {self.inputs[0].get_default_value()}' + if self.property1 != 'custom': + if self.inputs[0].is_linked: + return f'{self.bl_label}: {self.property1}' + else: + return f'{self.bl_label}: {self.property1} {self.inputs[0].get_default_value()}' + elif self.inputs[1].is_linked: + return f'{self.bl_label}: {self.property1}' + else: + return f'{self.bl_label}: {self.property1} {self.inputs[1].get_default_value()}' def get_replacement_node(self, node_tree: bpy.types.NodeTree): if self.lnx_version not in (0, 1): From fa818602c439ebb8993016b14334ce566166df46 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:18:05 +0000 Subject: [PATCH 40/63] t3du - Labels for finding nodes --- leenkx/blender/lnx/logicnode/input/LN_keyboard.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/leenkx/blender/lnx/logicnode/input/LN_keyboard.py b/leenkx/blender/lnx/logicnode/input/LN_keyboard.py index 9c02db4..21f7467 100644 --- a/leenkx/blender/lnx/logicnode/input/LN_keyboard.py +++ b/leenkx/blender/lnx/logicnode/input/LN_keyboard.py @@ -7,12 +7,19 @@ class KeyboardNode(LnxLogicTreeNode): lnx_section = 'keyboard' lnx_version = 2 + def update(self): + self.label = f'{self.bl_label}: {self.property0} {self.property1}' + + def upd(self, context): + self.label = f'{self.bl_label}: {self.property0} {self.property1}' + + property0: HaxeEnumProperty( 'property0', items = [('started', 'Started', 'The keyboard button starts to be pressed'), ('down', 'Down', 'The keyboard button is pressed'), ('released', 'Released', 'The keyboard button stops being pressed')], - name='', default='down') + name='', default='down', update=upd) property1: HaxeEnumProperty( 'property1', @@ -69,7 +76,7 @@ class KeyboardNode(LnxLogicTreeNode): ('right', 'right', 'right'), ('left', 'left', 'left'), ('down', 'down', 'down'),], - name='', default='space') + name='', default='space', update=upd) def lnx_init(self, context): self.add_output('LnxNodeSocketAction', 'Out') From 1505414c4c0565de4bf8464c542d79e6ea86f7e5 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:19:25 +0000 Subject: [PATCH 41/63] t3du - Labels for finding nodes --- leenkx/blender/lnx/logicnode/input/LN_mouse.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/leenkx/blender/lnx/logicnode/input/LN_mouse.py b/leenkx/blender/lnx/logicnode/input/LN_mouse.py index 57164bb..5c9176d 100644 --- a/leenkx/blender/lnx/logicnode/input/LN_mouse.py +++ b/leenkx/blender/lnx/logicnode/input/LN_mouse.py @@ -8,13 +8,25 @@ class MouseNode(LnxLogicTreeNode): lnx_section = 'mouse' lnx_version = 3 + def update(self): + if self.property0 != 'moved': + self.label = f'{self.bl_label}: {self.property0} {self.property1}' + else: + self.label = f'{self.bl_label}: {self.property0}' + + def upd(self, context): + if self.property0 != 'moved': + self.label = f'{self.bl_label}: {self.property0} {self.property1}' + else: + self.label = f'{self.bl_label}: {self.property0}' + property0: HaxeEnumProperty( 'property0', items = [('started', 'Started', 'The mouse button begins to be pressed'), ('down', 'Down', 'The mouse button is pressed'), ('released', 'Released', 'The mouse button stops being pressed'), ('moved', 'Moved', 'Moved')], - name='', default='down') + name='', default='down', update=upd) property1: HaxeEnumProperty( 'property1', items = [('left', 'Left', 'Left mouse button'), @@ -22,7 +34,7 @@ class MouseNode(LnxLogicTreeNode): ('right', 'Right', 'Right mouse button'), ('side1', 'Side 1', 'Side 1 mouse button'), ('side2', 'Side 2', 'Side 2 mouse button')], - name='', default='left') + name='', default='left', update=upd) property2: HaxeBoolProperty( 'property2', name='Include Debug Console', From aedc2783ab0e2d2d92f2a9ce88ea40d1a64ffe7d Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:22:45 +0000 Subject: [PATCH 42/63] t3du - Labels for finding nodes --- .../logicnode/miscellaneous/LN_call_group.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/leenkx/blender/lnx/logicnode/miscellaneous/LN_call_group.py b/leenkx/blender/lnx/logicnode/miscellaneous/LN_call_group.py index 7524974..ba575d9 100644 --- a/leenkx/blender/lnx/logicnode/miscellaneous/LN_call_group.py +++ b/leenkx/blender/lnx/logicnode/miscellaneous/LN_call_group.py @@ -18,6 +18,10 @@ class CallGroupNode(LnxLogicTreeNode): def lnx_init(self, context): pass + def update(self): + if self.group_tree: + self.label = f'Group: {self.group_tree.name}' + # Function to add input sockets and re-link sockets def update_inputs(self, tree, node, inp_sockets, in_links): count = 0 @@ -58,10 +62,12 @@ class CallGroupNode(LnxLogicTreeNode): tree.links.new(current_socket, link) count = count + 1 - def remove_tree(self): - self.group_tree = None - def update_sockets(self, context): + if self.group_tree: + self.label = f'Group: {self.group_tree.name}' + else: + self.label = 'Call Node Group' + # List to store from and to sockets of connected nodes from_socket_list = [] to_socket_list = [] @@ -107,6 +113,10 @@ class CallGroupNode(LnxLogicTreeNode): # Prperty to store group tree pointer group_tree: PointerProperty(name='Group', type=bpy.types.NodeTree, update=update_sockets) + def edit_tree(self): + self.label = f'Group: {self.group_tree.name}' + bpy.ops.lnx.edit_group_tree() + def draw_label(self) -> str: if self.group_tree is not None: return f'Group: {self.group_tree.name}' @@ -134,8 +144,9 @@ class CallGroupNode(LnxLogicTreeNode): op = row_name.operator('lnx.unlink_group_tree', icon='X', text='') op.node_index = self.get_id_str() row_ops.enabled = not self.group_tree is None - op = row_ops.operator('lnx.edit_group_tree', icon='FULLSCREEN_ENTER', text='Edit tree') + op = row_ops.operator('lnx.node_call_func', icon='FULLSCREEN_ENTER', text='Edit tree') op.node_index = self.get_id_str() + op.callback_name = 'edit_tree' def get_replacement_node(self, node_tree: bpy.types.NodeTree): if self.lnx_version not in (0, 1, 2): From 88418c06c3bd79113c7cdde6f0499ba6b5d4faa2 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:25:30 +0000 Subject: [PATCH 43/63] t3du - Fix World Errors --- .../Sources/leenkx/logicnode/SetWorldNode.hx | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/leenkx/Sources/leenkx/logicnode/SetWorldNode.hx b/leenkx/Sources/leenkx/logicnode/SetWorldNode.hx index cd1acaa..39e2c48 100644 --- a/leenkx/Sources/leenkx/logicnode/SetWorldNode.hx +++ b/leenkx/Sources/leenkx/logicnode/SetWorldNode.hx @@ -1,5 +1,7 @@ package leenkx.logicnode; +import iron.data.SceneFormat; + class SetWorldNode extends LogicNode { public function new(tree: LogicTree) { @@ -10,25 +12,6 @@ class SetWorldNode extends LogicNode { var world: String = inputs[1].get(); if (world != null){ - - //check if world shader data exists - var file: String = 'World_'+world+'_data'; - #if lnx_json - file += ".json"; - #elseif lnx_compress - file += ".lz4"; - #else - file += '.lnx'; - #end - - var exists: Bool = false; - - iron.data.Data.getBlob(file, function(b: kha.Blob) { - if (b != null) exists = true; - }); - - assert(Error, exists == true, "World must be either associated to a scene or have fake user"); - iron.Scene.active.raw.world_ref = world; var npath = leenkx.renderpath.RenderPathCreator.get(); npath.loadShader("shader_datas/World_" + world + "/World_" + world); From 4520422f6bbe05e6419804e6bacd19e849b38151 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:27:02 +0000 Subject: [PATCH 44/63] t3du - Fix World Errors --- leenkx/blender/lnx/logicnode/world/LN_set_world.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/logicnode/world/LN_set_world.py b/leenkx/blender/lnx/logicnode/world/LN_set_world.py index 0963735..d51f031 100644 --- a/leenkx/blender/lnx/logicnode/world/LN_set_world.py +++ b/leenkx/blender/lnx/logicnode/world/LN_set_world.py @@ -1,7 +1,10 @@ from lnx.logicnode.lnx_nodes import * class SetWorldNode(LnxLogicTreeNode): - """Sets the World of the active scene.""" + """Sets the World of the active scene. + World must be either associated to a scene or have fake user.""" + + bl_idname = 'LNSetWorldNode' bl_label = 'Set World' lnx_version = 1 From abedfd799e71f975e6656da732e89d8e2a8ef141 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:28:54 +0000 Subject: [PATCH 45/63] t3du - Fix World Errors --- leenkx/blender/lnx/make_renderpath.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/leenkx/blender/lnx/make_renderpath.py b/leenkx/blender/lnx/make_renderpath.py index 46698a7..fa5cb6a 100644 --- a/leenkx/blender/lnx/make_renderpath.py +++ b/leenkx/blender/lnx/make_renderpath.py @@ -39,14 +39,15 @@ def add_world_defs(): # Store contexts if rpdat.rp_hdr == False: wrd.world_defs += '_LDR' + + if lnx.utils.get_active_scene().world is not None: + if lnx.utils.get_active_scene().world.lnx_light_ies_texture: + wrd.world_defs += '_LightIES' + assets.add_embedded_data('iestexture.png') - if lnx.utils.get_active_scene().world.lnx_light_ies_texture == True: - wrd.world_defs += '_LightIES' - assets.add_embedded_data('iestexture.png') - - if lnx.utils.get_active_scene().world.lnx_light_clouds_texture == True: - wrd.world_defs += '_LightClouds' - assets.add_embedded_data('cloudstexture.png') + if lnx.utils.get_active_scene().world.lnx_light_clouds_texture: + wrd.world_defs += '_LightClouds' + assets.add_embedded_data('cloudstexture.png') if rpdat.rp_renderer == 'Deferred': assets.add_khafile_def('lnx_deferred') From a4398c727935edf01001aaba9e5a36748a9ebb02 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:31:45 +0000 Subject: [PATCH 46/63] t3du - Particle info random --- leenkx/blender/lnx/material/cycles.py | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/blender/lnx/material/cycles.py b/leenkx/blender/lnx/material/cycles.py index eb61a1a..f5c495f 100644 --- a/leenkx/blender/lnx/material/cycles.py +++ b/leenkx/blender/lnx/material/cycles.py @@ -94,6 +94,7 @@ def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types. parse_displacement = state.parse_displacement particle_info = { 'index': False, + 'random': False, 'age': False, 'lifetime': False, 'location': False, From d28d59b9e6bec3cee2b6443b81e9e8dabca6f307 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:38:12 +0000 Subject: [PATCH 47/63] t3du - Particle info random --- leenkx/blender/lnx/material/cycles_nodes/nodes_input.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py index 14c5700..506be34 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_input.py @@ -254,9 +254,10 @@ def parse_particleinfo(node: bpy.types.ShaderNodeParticleInfo, out_socket: bpy.t c.particle_info['index'] = True return 'p_index' if particles_on else '0.0' - # TODO: Random + # Random if out_socket == node.outputs[1]: - return '0.0' + c.particle_info['random'] = True + return 'p_random' if particles_on else '0.0' # Age elif out_socket == node.outputs[2]: @@ -276,7 +277,7 @@ def parse_particleinfo(node: bpy.types.ShaderNodeParticleInfo, out_socket: bpy.t # Size elif out_socket == node.outputs[5]: c.particle_info['size'] = True - return '1.0' + return 'p_size' if particles_on else '1.0' # Velocity elif out_socket == node.outputs[6]: From e88f101ca6246ce27349b5a2cabb6591ec351a28 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 19:40:49 +0000 Subject: [PATCH 48/63] t3du - Particle info random --- leenkx/blender/lnx/material/make_particle.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/leenkx/blender/lnx/material/make_particle.py b/leenkx/blender/lnx/material/make_particle.py index 8001dbf..f24a1ea 100644 --- a/leenkx/blender/lnx/material/make_particle.py +++ b/leenkx/blender/lnx/material/make_particle.py @@ -55,6 +55,7 @@ def write(vert, particle_info=None, shadowmap=False): # Outs out_index = True if particle_info != None and particle_info['index'] else False + out_random = True if particle_info != None and particle_info['random'] else False out_age = True if particle_info != None and particle_info['age'] else False out_lifetime = True if particle_info != None and particle_info['lifetime'] else False out_location = True if particle_info != None and particle_info['location'] else False @@ -258,6 +259,11 @@ def write(vert, particle_info=None, shadowmap=False): vert.add_out('float p_index') vert.write('p_index = gl_InstanceID;') + if out_random: + vert.add_out('float p_random') + vert.write('p_random = fract(sin(gl_InstanceID) * 43758.5453);') + + def write_tilesheet(vert): # tilesx, tilesy, framerate - pd[3][0], pd[3][1], pd[3][2] vert.write('int frame = int((p_age) / pd[3][2]);') From 58e009f70946fe1ad9512f2d767d6dd72dafd195 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 21:17:58 +0000 Subject: [PATCH 49/63] Terrain Generation fix --- leenkx/blender/lnx/props_ui.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index 9668f99..d9adb2a 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -2319,7 +2319,10 @@ class LnxGenTerrainButton(bpy.types.Operator): node.location = (-200, -200) node.inputs[0].default_value = 5.0 links.new(nodes['Bump'].inputs[2], nodes['_TerrainHeight'].outputs[0]) - links.new(nodes['Principled BSDF'].inputs[20], nodes['Bump'].outputs[0]) + if bpy.app.version[0] >= 4: + links.new(nodes['Principled BSDF'].inputs[22], nodes['Bump'].outputs[0]) + else: + links.new(nodes['Principled BSDF'].inputs[20], nodes['Bump'].outputs[0]) # Create sectors root_obj = bpy.data.objects.new("Terrain", None) @@ -2352,7 +2355,16 @@ class LnxGenTerrainButton(bpy.types.Operator): disp_mod.texture.extension = 'EXTEND' disp_mod.texture.use_interpolation = False disp_mod.texture.use_mipmap = False - disp_mod.texture.image = bpy.data.images.load(filepath=scn.lnx_terrain_textures+'/heightmap_' + j + '.png') + try: + disp_mod.texture.image = bpy.data.images.load(filepath=scn.lnx_terrain_textures+'/heightmap_' + j + '.png') + except Exception as e: + if i == 0: # Only show message once + if scn.lnx_terrain_textures.startswith('//') and not bpy.data.filepath: + self.report({'INFO'}, "Generating terrain... Save .blend file and add your heightmaps for each sector in " + "the \"Bundled\" folder using the format \"heightmap_01.png\", \"heightmap_02.png\", etc.") + else: + self.report({'INFO'}, f"Heightmap not found: {scn.lnx_terrain_textures}/heightmap_{j}.png - using blank image") + f = 1 levels = 0 while f < disp_mod.texture.image.size[0]: From b6e96553c26b7e7b1acd9de63c5dfd00fcfb87f4 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Fri, 19 Sep 2025 22:52:01 +0000 Subject: [PATCH 50/63] Update leenkx/blender/lnx/lightmapper/utility/build.py --- .../blender/lnx/lightmapper/utility/build.py | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/leenkx/blender/lnx/lightmapper/utility/build.py b/leenkx/blender/lnx/lightmapper/utility/build.py index 074d8bf..42762c2 100644 --- a/leenkx/blender/lnx/lightmapper/utility/build.py +++ b/leenkx/blender/lnx/lightmapper/utility/build.py @@ -1,4 +1,16 @@ -import bpy, os, subprocess, sys, platform, aud, json, datetime, socket +import bpy, os, subprocess, sys, platform, json, datetime, socket + + +aud = None +try: + import aud +except (ImportError, AttributeError) as e: + + if any(err in str(e) for err in ["numpy.core.multiarray", "_ARRAY_API", "compiled using NumPy 1.x"]): + print("Info: Audio features unavailable due to NumPy version compatibility.") + else: + print(f"Warning: Audio module unavailable: {e}") + aud = None from . import encoding, pack, log from . cycles import lightmap, prepare, nodes, cache @@ -1117,9 +1129,12 @@ def manage_build(background_pass=False, load_atlas=0): scriptDir = os.path.dirname(os.path.realpath(__file__)) sound_path = os.path.abspath(os.path.join(scriptDir, '..', 'assets/'+soundfile)) - device = aud.Device() - sound = aud.Sound.file(sound_path) - device.play(sound) + if aud is not None: + device = aud.Device() + sound = aud.Sound.file(sound_path) + device.play(sound) + else: + print(f"Build completed!") if logging: print("Log file output:") From 46e30478771f5ed2e0cb6262ffe2bf5a0bc557fe Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Tue, 23 Sep 2025 19:57:53 +0000 Subject: [PATCH 51/63] Update leenkx/blender/lnx/props.py --- leenkx/blender/lnx/props.py | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index a333e3a..3dc0ff2 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -436,6 +436,7 @@ def init_properties(): bpy.types.World.lnx_nishita_density = FloatVectorProperty(name="Nishita Density", size=3, default=[1, 1, 1]) bpy.types.Material.lnx_cast_shadow = BoolProperty(name="Cast Shadow", default=True) bpy.types.Material.lnx_receive_shadow = BoolProperty(name="Receive Shadow", description="Requires forward render path", default=True) + bpy.types.Material.lnx_depth_write = BoolProperty(name="Write Depth", description="Allow this material to write to the depth buffer", default=True) bpy.types.Material.lnx_depth_read = BoolProperty(name="Read Depth", description="Allow this material to read from a depth texture which is copied from the depth buffer. The meshes using this material will be drawn after all meshes that don't read from the depth texture", default=False) bpy.types.Material.lnx_overlay = BoolProperty(name="Overlay", description="Renders the material, unshaded, over other shaded materials", default=False) bpy.types.Material.lnx_decal = BoolProperty(name="Decal", default=False) From 6af1ef2df1293ddb911d01970d423a60f5a315f1 Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Wed, 24 Sep 2025 01:33:47 +0000 Subject: [PATCH 52/63] Update leenkx/blender/lnx/props_ui.py --- leenkx/blender/lnx/props_ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index d9adb2a..c34d931 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -63,7 +63,7 @@ class LNX_PT_ObjectPropsPanel(bpy.types.Panel): return col = layout.column() - col.prop(mat, 'lnx_sorting_index') + col.prop(obj, 'lnx_sorting_index') col.prop(obj, 'lnx_export') if not obj.lnx_export: return From a72edc62031deb5a9790d76bbd580f212ce40926 Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Wed, 24 Sep 2025 01:50:03 +0000 Subject: [PATCH 53/63] Update leenkx/Sources/iron/object/ParticleSystem.hx --- leenkx/Sources/iron/object/ParticleSystem.hx | 177 +++++++++++++++++-- 1 file changed, 164 insertions(+), 13 deletions(-) diff --git a/leenkx/Sources/iron/object/ParticleSystem.hx b/leenkx/Sources/iron/object/ParticleSystem.hx index a6b03da..5fab3a7 100644 --- a/leenkx/Sources/iron/object/ParticleSystem.hx +++ b/leenkx/Sources/iron/object/ParticleSystem.hx @@ -8,6 +8,8 @@ import kha.arrays.Float32Array; import iron.data.Data; import iron.data.ParticleData; import iron.data.SceneFormat; +import iron.data.Geometry; +import iron.data.MeshData; import iron.system.Time; import iron.math.Mat4; import iron.math.Quat; @@ -17,6 +19,7 @@ import iron.math.Vec4; class ParticleSystem { public var data: ParticleData; public var speed = 1.0; + public var dynamicEmitter: Bool = true; var currentSpeed = 0.0; var particles: Array; var ready: Bool; @@ -52,6 +55,12 @@ class ParticleSystem { var random = 0.0; + var tmpV4 = new Vec4(); + + var instancedData: Float32Array = null; + var lastSpawnedCount: Int = 0; + var hasUniqueGeom: Bool = false; + public function new(sceneName: String, pref: TParticleReference) { seed = pref.seed; currentSpeed = speed; @@ -62,6 +71,11 @@ class ParticleSystem { Data.getParticle(sceneName, pref.particle, function(b: ParticleData) { data = b; r = data.raw; + if (r.dynamic_emitter != null){ + dynamicEmitter = r.dynamic_emitter; + } else { + dynamicEmitter = true; + } if (Scene.active.raw.gravity != null) { gx = Scene.active.raw.gravity[0] * r.weight_gravity; gy = Scene.active.raw.gravity[1] * r.weight_gravity; @@ -98,6 +112,8 @@ class ParticleSystem { lap = 0; lapTime = 0; speed = currentSpeed; + lastSpawnedCount = 0; + instancedData = null; } public function pause() { @@ -130,8 +146,13 @@ class ParticleSystem { // Copy owner world transform but discard scale owner.transform.world.decompose(ownerLoc, ownerRot, ownerScl); - object.transform.loc = ownerLoc; - object.transform.rot = ownerRot; + if (dynamicEmitter) { + object.transform.loc.x = 0; object.transform.loc.y = 0; object.transform.loc.z = 0; + object.transform.rot = new Quat(); + } else { + object.transform.loc = ownerLoc; + object.transform.rot = ownerRot; + } // Set particle size per particle system object.transform.scale = new Vec4(r.particle_size, r.particle_size, r.particle_size, 1); @@ -158,13 +179,18 @@ class ParticleSystem { if (lap > prevLap && !r.loop) { end(); } + + if (lap > prevLap && r.loop) { + lastSpawnedCount = 0; + } updateGpu(object, owner); } public function getData(): Mat4 { var hair = r.type == 1; - m._00 = animtime; + // Store loop flag in the sign: positive -> loop, negative -> no loop + m._00 = r.loop ? animtime : -animtime; m._01 = hair ? 1 / particles.length : spawnRate; m._02 = hair ? 1 : lifetime; m._03 = particles.length; @@ -187,17 +213,26 @@ class ParticleSystem { return r.size_random; } - public function getRandom(): FastFloat { + public inline function getRandom(): FastFloat { return random; } - public function getSize(): FastFloat { + public inline function getSize(): FastFloat { return r.particle_size; } function updateGpu(object: MeshObject, owner: MeshObject) { - if (!object.data.geom.instanced) setupGeomGpu(object, owner); - // GPU particles transform is attached to owner object + if (dynamicEmitter) { + if (!hasUniqueGeom) ensureUniqueGeom(object); + var needSetup = instancedData == null || object.data.geom.instancedVB == null; + if (needSetup) setupGeomGpuDynamic(object, owner); + updateSpawnedInstances(object, owner); + } + else { + if (!hasUniqueGeom) ensureUniqueGeom(object); + if (!object.data.geom.instanced) setupGeomGpu(object, owner); + } + // GPU particles transform is attached to owner object in static mode } function setupGeomGpu(object: MeshObject, owner: MeshObject) { @@ -258,18 +293,134 @@ class ParticleSystem { object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage); } - function fhash(n: Int): Float { - var s = n + 1.0; - s *= 9301.0 % s; - s = (s * 9301.0 + 49297.0) % 233280.0; - return s / 233280.0; + // allocate instanced VB once for this object + function setupGeomGpuDynamic(object: MeshObject, owner: MeshObject) { + if (instancedData == null) instancedData = new Float32Array(particles.length * 3); + lastSpawnedCount = 0; + // Create instanced VB once if missing (seed with our instancedData) + if (object.data.geom.instancedVB == null) { + object.data.geom.setupInstanced(instancedData, 1, Usage.DynamicUsage); + } } + function ensureUniqueGeom(object: MeshObject) { + if (hasUniqueGeom) return; + var newData: MeshData = null; + new MeshData(object.data.raw, function(dat: MeshData) { + dat.scalePos = object.data.scalePos; + dat.scaleTex = object.data.scaleTex; + dat.format = object.data.format; + newData = dat; + }); + if (newData != null) object.setData(newData); + hasUniqueGeom = true; + } + + function updateSpawnedInstances(object: MeshObject, owner: MeshObject) { + if (instancedData == null) return; + var targetCount = count; + if (targetCount > particles.length) targetCount = particles.length; + if (targetCount <= lastSpawnedCount) return; + + var normFactor = 1 / 32767; + var scalePosOwner = owner.data.scalePos; + var scalePosParticle = object.data.scalePos; + var particleSize = r.particle_size; + var base = 1.0 / (particleSize * scalePosParticle); + + switch (r.emit_from) { + case 0: // Vert + var pa = owner.data.geom.positions; + var osx = owner.transform.scale.x; + var osy = owner.transform.scale.y; + var osz = owner.transform.scale.z; + var pCount = Std.int(pa.values.length / pa.size); + for (idx in lastSpawnedCount...targetCount) { + var j = Std.int(fhash(idx) * pCount); + var lx = pa.values[j * pa.size ] * normFactor * scalePosOwner * osx; + var ly = pa.values[j * pa.size + 1] * normFactor * scalePosOwner * osy; + var lz = pa.values[j * pa.size + 2] * normFactor * scalePosOwner * osz; + tmpV4.x = lx; tmpV4.y = ly; tmpV4.z = lz; tmpV4.w = 1; + tmpV4.applyQuat(ownerRot); + var o = idx * 3; + instancedData.set(o , (tmpV4.x + ownerLoc.x) * base); + instancedData.set(o + 1, (tmpV4.y + ownerLoc.y) * base); + instancedData.set(o + 2, (tmpV4.z + ownerLoc.z) * base); + } + + case 1: // Face + var positions = owner.data.geom.positions.values; + var osx1 = owner.transform.scale.x; + var osy1 = owner.transform.scale.y; + var osz1 = owner.transform.scale.z; + for (idx in lastSpawnedCount...targetCount) { + var ia = owner.data.geom.indices[Std.random(owner.data.geom.indices.length)]; + var faceIndex = Std.random(Std.int(ia.length / 3)); + var i0 = ia[faceIndex * 3 + 0]; + var i1 = ia[faceIndex * 3 + 1]; + var i2 = ia[faceIndex * 3 + 2]; + var v0x = positions[i0 * 4 ], v0y = positions[i0 * 4 + 1], v0z = positions[i0 * 4 + 2]; + var v1x = positions[i1 * 4 ], v1y = positions[i1 * 4 + 1], v1z = positions[i1 * 4 + 2]; + var v2x = positions[i2 * 4 ], v2y = positions[i2 * 4 + 1], v2z = positions[i2 * 4 + 2]; + var rx = Math.random(); var ry = Math.random(); if (rx + ry > 1) { rx = 1 - rx; ry = 1 - ry; } + var pxs = v0x + rx * (v1x - v0x) + ry * (v2x - v0x); + var pys = v0y + rx * (v1y - v0y) + ry * (v2y - v0y); + var pzs = v0z + rx * (v1z - v0z) + ry * (v2z - v0z); + var px = pxs * normFactor * scalePosOwner * osx1; + var py = pys * normFactor * scalePosOwner * osy1; + var pz = pzs * normFactor * scalePosOwner * osz1; + tmpV4.x = px; tmpV4.y = py; tmpV4.z = pz; tmpV4.w = 1; + tmpV4.applyQuat(ownerRot); + var o1 = idx * 3; + instancedData.set(o1 , (tmpV4.x + ownerLoc.x) * base); + instancedData.set(o1 + 1, (tmpV4.y + ownerLoc.y) * base); + instancedData.set(o1 + 2, (tmpV4.z + ownerLoc.z) * base); + } + + case 2: // Volume + var dim = object.transform.dim; + for (idx in lastSpawnedCount...targetCount) { + tmpV4.x = (Math.random() * 2.0 - 1.0) * (dim.x * 0.5); + tmpV4.y = (Math.random() * 2.0 - 1.0) * (dim.y * 0.5); + tmpV4.z = (Math.random() * 2.0 - 1.0) * (dim.z * 0.5); + tmpV4.w = 1; + tmpV4.applyQuat(ownerRot); + var o2 = idx * 3; + instancedData.set(o2 , (tmpV4.x + ownerLoc.x) * base); + instancedData.set(o2 + 1, (tmpV4.y + ownerLoc.y) * base); + instancedData.set(o2 + 2, (tmpV4.z + ownerLoc.z) * base); + } + } + + // Upload full active range [0..targetCount) to this object's instanced VB + var geom = object.data.geom; + if (geom.instancedVB == null) { + geom.setupInstanced(instancedData, 1, Usage.DynamicUsage); + } + var vb = geom.instancedVB.lock(); + var totalFloats = targetCount * 3; // xyz per instance + var i = 0; + while (i < totalFloats) { + vb.setFloat32(i * 4, instancedData[i]); + i++; + } + geom.instancedVB.unlock(); + geom.instanceCount = targetCount; + lastSpawnedCount = targetCount; + } + + inline function fhash(n: Int): Float { + var s = n + 1.0; + s *= 9301.0 % s; + s = (s * 9301.0 + 49297.0) % 233280.0; + return s / 233280.0; +} + public function remove() {} /** Generates a random point in the triangle with vertex positions abc. - + Please note that the given position vectors are changed in-place by this function and can be considered garbage afterwards, so make sure to clone them first if needed. From 45966ef0bbf92029b4b6c9cc994797e249d6d2ff Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Wed, 24 Sep 2025 01:51:11 +0000 Subject: [PATCH 54/63] Update leenkx/blender/lnx/props.py --- leenkx/blender/lnx/props.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index 3dc0ff2..aa3949f 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -586,6 +586,11 @@ def init_properties(): bpy.types.Node.lnx_version = IntProperty(name="Node Version", description="The version of an instanced node", default=0) # Particles bpy.types.ParticleSettings.lnx_auto_start = BoolProperty(name="Auto Start", description="Automatically start this particle system on load", default=True) + bpy.types.ParticleSettings.lnx_dynamic_emitter = BoolProperty( + name="Dynamic", + description="Particles have independent transform updates following emitter compared to a static baked particle system used if emitters dont generally move around.", + default=True + ) bpy.types.ParticleSettings.lnx_is_unique = BoolProperty(name="Is Unique", description="Make this particle system look different each time it starts", default=False) bpy.types.ParticleSettings.lnx_loop = BoolProperty(name="Loop", description="Loop this particle system", default=False) bpy.types.ParticleSettings.lnx_count_mult = FloatProperty(name="Multiply Count", description="Multiply particle count when rendering in Leenkx", default=1.0) From 04c6983a09a8598c530ad8082eb9c59b56960fb4 Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Wed, 24 Sep 2025 01:52:47 +0000 Subject: [PATCH 55/63] Update leenkx/Sources/iron/data/SceneFormat.hx --- leenkx/Sources/iron/data/SceneFormat.hx | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/Sources/iron/data/SceneFormat.hx b/leenkx/Sources/iron/data/SceneFormat.hx index cadd3b6..dd080a8 100644 --- a/leenkx/Sources/iron/data/SceneFormat.hx +++ b/leenkx/Sources/iron/data/SceneFormat.hx @@ -395,6 +395,7 @@ typedef TParticleData = { public var name: String; public var type: Int; // 0 - Emitter, Hair public var auto_start: Bool; + public var dynamic_emitter: Bool; public var is_unique: Bool; public var loop: Bool; public var count: Int; From 21afad6d092f5130d6ff71c478e9417b4b2339f9 Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Wed, 24 Sep 2025 01:53:43 +0000 Subject: [PATCH 56/63] Update leenkx/blender/lnx/exporter.py --- leenkx/blender/lnx/exporter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/blender/lnx/exporter.py b/leenkx/blender/lnx/exporter.py index c4dd160..224b276 100644 --- a/leenkx/blender/lnx/exporter.py +++ b/leenkx/blender/lnx/exporter.py @@ -2334,6 +2334,7 @@ class LeenkxExporter: 'name': particleRef[1]["structName"], 'type': 0 if psettings.type == 'EMITTER' else 1, # HAIR 'auto_start': psettings.lnx_auto_start, + 'dynamic_emitter': psettings.lnx_dynamic_emitter, 'is_unique': psettings.lnx_is_unique, 'loop': psettings.lnx_loop, # Emission From 6c3efa6c83ed2200716b380ea650522c7e934e62 Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Wed, 24 Sep 2025 01:54:38 +0000 Subject: [PATCH 57/63] Update leenkx/blender/lnx/props_ui.py --- leenkx/blender/lnx/props_ui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index c34d931..b72f1c1 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -207,6 +207,7 @@ class LNX_PT_ParticlesPropsPanel(bpy.types.Panel): return layout.prop(obj.settings, 'lnx_auto_start') + layout.prop(obj.settings, 'lnx_dynamic_emitter') layout.prop(obj.settings, 'lnx_is_unique') layout.prop(obj.settings, 'lnx_loop') layout.prop(obj.settings, 'lnx_count_mult') From a926fa8dbba61670fd9f69ab05de4aa6cd0017d6 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Sat, 27 Sep 2025 03:03:08 +0000 Subject: [PATCH 58/63] Update leenkx/blender/lnx/nodes_logic.py --- leenkx/blender/lnx/nodes_logic.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/leenkx/blender/lnx/nodes_logic.py b/leenkx/blender/lnx/nodes_logic.py index a591702..de0c9c8 100644 --- a/leenkx/blender/lnx/nodes_logic.py +++ b/leenkx/blender/lnx/nodes_logic.py @@ -477,7 +477,6 @@ __REG_CLASSES = ( LnxOpenNodeWikiEntry, LNX_OT_ReplaceNodesOperator, LNX_OT_RecalculateRotations, - LNX_MT_NodeAddOverride, LNX_OT_AddNodeOverride, LNX_UL_InterfaceSockets, LNX_PT_LogicNodePanel, @@ -492,8 +491,9 @@ def register(): lnx.logicnode.lnx_node_group.register() lnx.logicnode.tree_variables.register() - LNX_MT_NodeAddOverride.overridden_menu = bpy.types.NODE_MT_add + # Store original draw method and restore during unregister LNX_MT_NodeAddOverride.overridden_draw = bpy.types.NODE_MT_add.draw + bpy.types.NODE_MT_add.draw = LNX_MT_NodeAddOverride.draw __reg_classes() @@ -508,8 +508,11 @@ def unregister(): # Ensure that globals are reset if the addon is enabled again in the same Blender session lnx_nodes.reset_globals() + # Restore original draw method + if hasattr(LNX_MT_NodeAddOverride, 'overridden_draw'): + bpy.types.NODE_MT_add.draw = LNX_MT_NodeAddOverride.overridden_draw + __unreg_classes() - bpy.utils.register_class(LNX_MT_NodeAddOverride.overridden_menu) lnx.logicnode.tree_variables.unregister() lnx.logicnode.lnx_node_group.unregister() From 8f8d4b13767c1162eef6487644aab3360da4f5aa Mon Sep 17 00:00:00 2001 From: Onek8 Date: Sun, 28 Sep 2025 00:09:57 +0000 Subject: [PATCH 59/63] Update leenkx/blender/lnx/props_traits.py --- leenkx/blender/lnx/props_traits.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/leenkx/blender/lnx/props_traits.py b/leenkx/blender/lnx/props_traits.py index 5db9309..48fafe2 100644 --- a/leenkx/blender/lnx/props_traits.py +++ b/leenkx/blender/lnx/props_traits.py @@ -475,12 +475,10 @@ class LeenkxGenerateNavmeshButton(bpy.types.Operator): # If not, append vertex traversed_indices.append(vertex_index) vertex = export_mesh.vertices[vertex_index].co - # Apply world transform + # Apply world transform and maintain coordinate system tv = world_matrix @ vertex - # Write to OBJ - f.write("v %.4f " % (tv[0])) - f.write("%.4f " % (tv[2])) - f.write("%.4f\n" % (tv[1])) # Flipped + # Write to OBJ without flipping coordinates + f.write("v %.4f %.4f %.4f\n" % (tv[0], tv[1], tv[2])) # Max index of this object max_index = 0 @@ -524,8 +522,10 @@ class LeenkxGenerateNavmeshButton(bpy.types.Operator): # NavMesh preview settings, cleanup navmesh.name = nav_mesh_name - navmesh.rotation_euler = (0, 0, 0) - navmesh.location = (0, 0, 0) + # Match the original object's transform + navmesh.location = obj.location + navmesh.rotation_euler = obj.rotation_euler + navmesh.scale = (1, 1, 1) # Reset scale to avoid distortion navmesh.lnx_export = False bpy.context.view_layer.objects.active = navmesh From f97d8fd84673b5b92b2de9190f6e817865cea57f Mon Sep 17 00:00:00 2001 From: Onek8 Date: Sun, 28 Sep 2025 12:44:04 -0700 Subject: [PATCH 60/63] Blender 2.8 - 4.5 Support --- leenkx.py | 58 +++- leenkx/blender/data/lnx_data_2.blend | Bin 0 -> 100308 bytes leenkx/blender/lnx/__init__.py | 10 +- leenkx/blender/lnx/exporter.py | 71 +++-- leenkx/blender/lnx/exporter_opt.py | 10 +- leenkx/blender/lnx/handlers.py | 25 +- leenkx/blender/lnx/lib/make_datas.py | 35 ++- .../blender/lnx/lightmapper/operators/tlm.py | 21 +- .../blender/lnx/lightmapper/panels/image.py | 7 +- .../logicnode/animation/LN_blend_space_gpu.py | 4 +- .../lnx/logicnode/custom/LN_create_element.py | 286 +++++++++--------- .../logicnode/custom/LN_js_event_target.py | 23 +- .../lnx/logicnode/custom/LN_render_element.py | 41 ++- .../blender/lnx/logicnode/lnx_node_group.py | 5 +- leenkx/blender/lnx/logicnode/lnx_nodes.py | 14 +- leenkx/blender/lnx/logicnode/lnx_props.py | 14 +- .../logicnode/miscellaneous/LN_group_input.py | 5 +- .../miscellaneous/LN_group_output.py | 5 +- .../blender/lnx/logicnode/tree_variables.py | 5 +- leenkx/blender/lnx/make_logic.py | 14 +- .../lnx/material/cycles_nodes/nodes_shader.py | 16 +- leenkx/blender/lnx/material/make_mesh.py | 6 +- leenkx/blender/lnx/material/make_particle.py | 89 +++--- leenkx/blender/lnx/material/mat_utils.py | 4 +- leenkx/blender/lnx/material/node_meta.py | 4 +- leenkx/blender/lnx/node_utils.py | 8 +- leenkx/blender/lnx/nodes_logic.py | 9 +- leenkx/blender/lnx/props.py | 22 +- leenkx/blender/lnx/props_exporter.py | 9 +- leenkx/blender/lnx/props_traits.py | 59 ++-- leenkx/blender/lnx/props_traits_props.py | 37 ++- leenkx/blender/lnx/props_ui.py | 36 ++- leenkx/blender/lnx/utils.py | 8 +- leenkx/blender/lnx/utils_vs.py | 20 +- 34 files changed, 581 insertions(+), 399 deletions(-) create mode 100644 leenkx/blender/data/lnx_data_2.blend diff --git a/leenkx.py b/leenkx.py index 83fe3fa..ed07a18 100644 --- a/leenkx.py +++ b/leenkx.py @@ -24,7 +24,7 @@ import textwrap import threading import traceback import typing -from typing import Callable, Optional +from typing import Callable, Optional, List import webbrowser import bpy @@ -33,6 +33,12 @@ from bpy.props import * from bpy.types import Operator, AddonPreferences +if bpy.app.version < (2, 90, 0): + ListType = List +else: + ListType = list + + class SDKSource(IntEnum): PREFS = 0 LOCAL = 1 @@ -58,8 +64,45 @@ def get_os(): else: return 'linux' - def detect_sdk_path(): + """Auto-detect the SDK path after Leenkx installation.""" + preferences = bpy.context.preferences + addon_prefs = preferences.addons["leenkx"].preferences + + # Don't overwrite if already set + if addon_prefs.sdk_path: + return + + # For all versions, try to get the path from the current file location first + current_file = os.path.realpath(__file__) + if os.path.exists(current_file): + # Go up one level from the current file's directory to get the SDK root + sdk_path = os.path.dirname(os.path.dirname(current_file)) + if os.path.exists(os.path.join(sdk_path, "leenkx")): + addon_prefs.sdk_path = sdk_path + return + + # Fallback for Blender 2.92+ with the original method + if bpy.app.version >= (2, 92, 0): + try: + win = bpy.context.window_manager.windows[0] + area = win.screen.areas[0] + area_type = area.type + area.type = "INFO" + + with bpy.context.temp_override(window=win, screen=win.screen, area=area): + bpy.ops.info.select_all(action='SELECT') + bpy.ops.info.report_copy() + + clipboard = bpy.context.window_manager.clipboard + match = re.findall(r"^Modules Installed .* from '(.*leenkx.py)' into", + clipboard, re.MULTILINE) + if match: + addon_prefs.sdk_path = os.path.dirname(match[-1]) + finally: + area.type = area_type + +def detect_sdk_path22(): """Auto-detect the SDK path after Leenkx installation.""" # Do not overwrite the SDK path (this method gets # called after each registration, not after @@ -73,6 +116,7 @@ def detect_sdk_path(): area = win.screen.areas[0] area_type = area.type area.type = "INFO" + with bpy.context.temp_override(window=win, screen=win.screen, area=area): bpy.ops.info.select_all(action='SELECT') bpy.ops.info.report_copy() @@ -558,7 +602,7 @@ def remove_readonly(func, path, excinfo): func(path) -def run_proc(cmd: list[str], done: Optional[Callable[[bool], None]] = None): +def run_proc(cmd: ListType[str], done: Optional[Callable[[bool], None]] = None): def fn(p, done): p.wait() if done is not None: @@ -840,7 +884,13 @@ def update_leenkx_py(sdk_path: str, force_relink=False): else: raise err else: - lnx_module_file.unlink(missing_ok=True) + if bpy.app.version < (2, 92, 0): + try: + lnx_module_file.unlink() + except FileNotFoundError: + pass + else: + lnx_module_file.unlink(missing_ok=True) shutil.copy(Path(sdk_path) / 'leenkx.py', lnx_module_file) diff --git a/leenkx/blender/data/lnx_data_2.blend b/leenkx/blender/data/lnx_data_2.blend new file mode 100644 index 0000000000000000000000000000000000000000..c444eb0122fd12b7ab8a652323843856cc88c145 GIT binary patch literal 100308 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�E_2=uk8{-b)-Gr}~)OS@nA*oWS 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 Vq72&YeAhE7*o= (3, 0, 0): + VertexColorType = bpy.types.Attribute +else: + VertexColorType = bpy.types.MeshLoopColorLayer import numpy as np @@ -138,7 +145,7 @@ class LeenkxExporter: self.world_array = [] self.particle_system_array = {} - self.referenced_collections: list[bpy.types.Collection] = [] + self.referenced_collections: List[bpy.types.Collection] = [] """Collections referenced by collection instances""" self.has_spawning_camera = False @@ -1449,31 +1456,38 @@ class LeenkxExporter: @staticmethod def get_num_vertex_colors(mesh: bpy.types.Mesh) -> int: """Return the amount of vertex color attributes of the given mesh.""" - num = 0 - for attr in mesh.attributes: - if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): - if attr.domain == 'CORNER': - num += 1 - else: - log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') - - return num + if bpy.app.version >= (3, 0, 0): + num = 0 + for attr in mesh.attributes: + if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): + if attr.domain == 'CORNER': + num += 1 + else: + log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') + return num + else: + return len(mesh.vertex_colors) @staticmethod - def get_nth_vertex_colors(mesh: bpy.types.Mesh, n: int) -> Optional[bpy.types.Attribute]: + def get_nth_vertex_colors(mesh: bpy.types.Mesh, n: int) -> Optional[VertexColorType]: """Return the n-th vertex color attribute from the given mesh, ignoring all other attribute types and unsupported domains. """ - i = 0 - for attr in mesh.attributes: - if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): - if attr.domain != 'CORNER': - log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') - continue - if i == n: - return attr - i += 1 - return None + if bpy.app.version >= (3, 0, 0): + i = 0 + for attr in mesh.attributes: + if attr.data_type in ('BYTE_COLOR', 'FLOAT_COLOR'): + if attr.domain != 'CORNER': + log.warn(f'Only vertex colors with domain "Face Corner" are supported for now, ignoring "{attr.name}"') + continue + if i == n: + return attr + i += 1 + return None + else: + if 0 <= n < len(mesh.vertex_colors): + return mesh.vertex_colors[n] + return None @staticmethod def check_uv_precision(mesh: bpy.types.Mesh, uv_max_dim: float, max_dim_uvmap: bpy.types.MeshUVLoopLayer, invscale_tex: float): @@ -3094,7 +3108,18 @@ class LeenkxExporter: rbw = self.scene.rigidbody_world if rbw is not None and rbw.enabled: - out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations), str(wrd.lnx_physics_fixed_step)] + if hasattr(rbw, 'substeps_per_frame'): + substeps = str(rbw.substeps_per_frame) + elif hasattr(rbw, 'steps_per_second'): + scene_fps = bpy.context.scene.render.fps + substeps_per_frame = rbw.steps_per_second / scene_fps + substeps = str(int(round(substeps_per_frame))) + else: + print("WARNING: Physics rigid body world cannot determine steps/substeps. Please report this for further investigation.") + print("Setting steps to 10 [ low ]") + substeps = '10' + + out_trait['parameters'] = [str(rbw.time_scale), substeps, str(rbw.solver_iterations), str(wrd.lnx_physics_fixed_step)] if phys_pkg == 'bullet' or phys_pkg == 'oimo': debug_draw_mode = 1 if wrd.lnx_physics_dbg_draw_wireframe else 0 diff --git a/leenkx/blender/lnx/exporter_opt.py b/leenkx/blender/lnx/exporter_opt.py index eff669f..91386e3 100644 --- a/leenkx/blender/lnx/exporter_opt.py +++ b/leenkx/blender/lnx/exporter_opt.py @@ -2,8 +2,7 @@ Exports smaller geometry but is slower. To be replaced with https://github.com/zeux/meshoptimizer """ -from typing import Optional - +from typing import Optional, TYPE_CHECKING import bpy from mathutils import Vector import numpy as np @@ -21,7 +20,12 @@ else: class Vertex: __slots__ = ("co", "normal", "uvs", "col", "loop_indices", "index", "bone_weights", "bone_indices", "bone_count", "vertex_index") - def __init__(self, mesh: bpy.types.Mesh, loop: bpy.types.MeshLoop, vcol0: Optional[bpy.types.Attribute]): + def __init__( + self, + mesh: 'bpy.types.Mesh', + loop: 'bpy.types.MeshLoop', + vcol0: Optional['bpy.types.MeshLoopColor' if bpy.app.version < (3, 0, 0) else 'bpy.types.Attribute'] + ): self.vertex_index = loop.vertex_index loop_idx = loop.index self.co = mesh.vertices[self.vertex_index].co[:] diff --git a/leenkx/blender/lnx/handlers.py b/leenkx/blender/lnx/handlers.py index 5d62a25..b29ecd1 100644 --- a/leenkx/blender/lnx/handlers.py +++ b/leenkx/blender/lnx/handlers.py @@ -98,7 +98,7 @@ def on_operator_post(operator_id: str) -> None: target_obj.lnx_rb_collision_filter_mask = source_obj.lnx_rb_collision_filter_mask elif operator_id == "NODE_OT_new_node_tree": - if bpy.context.space_data.tree_type == lnx.nodes_logic.LnxLogicTree.bl_idname: + if bpy.context.space_data is not None and bpy.context.space_data.tree_type == lnx.nodes_logic.LnxLogicTree.bl_idname: # In Blender 3.5+, new node trees are no longer called "NodeTree" # but follow the bl_label attribute by default. New logic trees # are thus called "Leenkx Logic Editor" which conflicts with Haxe's @@ -132,9 +132,10 @@ def send_operator(op): def always() -> float: # Force ui redraw if state.redraw_ui: - for area in bpy.context.screen.areas: - if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'): - area.tag_redraw() + if bpy.context.screen is not None: + for area in bpy.context.screen.areas: + if area.type in ('NODE_EDITOR', 'PROPERTIES', 'VIEW_3D'): + area.tag_redraw() state.redraw_ui = False return 0.5 @@ -251,7 +252,7 @@ def get_polling_stats() -> dict: } -loaded_py_libraries: dict[str, types.ModuleType] = {} +loaded_py_libraries: Dict[str, types.ModuleType] = {} context_screen = None @@ -347,10 +348,18 @@ def reload_blend_data(): def load_library(asset_name): - if bpy.data.filepath.endswith('lnx_data.blend'): # Prevent load in library itself - return + # Prevent load in library itself + if bpy.app.version <= (2, 93, 0): + if bpy.data.filepath.endswith('lnx_data_2.blend'): + return + else: + if bpy.data.filepath.endswith('lnx_data.blend'): + return sdk_path = lnx.utils.get_sdk_path() - data_path = sdk_path + '/leenkx/blender/data/lnx_data.blend' + if bpy.app.version <= (2, 93, 0): + data_path = sdk_path + '/leenkx/blender/data/lnx_data_2.blend' + else: + data_path = sdk_path + '/leenkx/blender/data/lnx_data.blend' data_names = [asset_name] # Import diff --git a/leenkx/blender/lnx/lib/make_datas.py b/leenkx/blender/lnx/lib/make_datas.py index 6483445..a47a839 100644 --- a/leenkx/blender/lnx/lib/make_datas.py +++ b/leenkx/blender/lnx/lib/make_datas.py @@ -1,13 +1,15 @@ +from typing import List, Dict, Optional, Any + import lnx.utils from lnx import assets def parse_context( - c: dict, - sres: dict, - asset, - defs: list[str], - vert: list[str] = None, - frag: list[str] = None, + c: Dict[str, Any], + sres: Dict[str, Any], + asset: Any, + defs: List[str], + vert: Optional[List[str]] = None, + frag: Optional[List[str]] = None, ): con = { "name": c["name"], @@ -99,7 +101,12 @@ def parse_context( def parse_shader( - sres, c: dict, con: dict, defs: list[str], lines: list[str], parse_attributes: bool + sres: Dict[str, Any], + c: Dict[str, Any], + con: Dict[str, Any], + defs: List[str], + lines: List[str], + parse_attributes: bool ): """Parses the given shader to get information about the used vertex elements, uniforms and constants. This information is later used in @@ -229,7 +236,12 @@ def parse_shader( check_link(c, defs, cid, const) -def check_link(source_context: dict, defs: list[str], cid: str, out: dict): +def check_link( + source_context: Dict[str, Any], + defs: List[str], + cid: str, + out: Dict[str, Any] +): """Checks whether the uniform/constant with the given name (`cid`) has a link stated in the json (`source_context`) that can be safely included based on the given defines (`defs`). If that is the case, @@ -273,7 +285,12 @@ def check_link(source_context: dict, defs: list[str], cid: str, out: dict): def make( - res: dict, base_name: str, json_data: dict, fp, defs: list[str], make_variants: bool + res: Dict[str, Any], + base_name: str, + json_data: Dict[str, Any], + fp: Any, + defs: List[str], + make_variants: bool ): sres = {"name": base_name, "contexts": []} res["shader_datas"].append(sres) diff --git a/leenkx/blender/lnx/lightmapper/operators/tlm.py b/leenkx/blender/lnx/lightmapper/operators/tlm.py index 01046df..46bfda6 100644 --- a/leenkx/blender/lnx/lightmapper/operators/tlm.py +++ b/leenkx/blender/lnx/lightmapper/operators/tlm.py @@ -1049,17 +1049,18 @@ class TLM_ToggleTexelDensity(bpy.types.Operator): #img = bpy.data.images.load(filepath) - for area in bpy.context.screen.areas: - if area.type == 'VIEW_3D': - space_data = area.spaces.active - bpy.ops.screen.area_dupli('INVOKE_DEFAULT') - new_window = context.window_manager.windows[-1] + if bpy.context.screen is not None: + for area in bpy.context.screen.areas: + if area.type == 'VIEW_3D': + space_data = area.spaces.active + bpy.ops.screen.area_dupli('INVOKE_DEFAULT') + new_window = context.window_manager.windows[-1] - area = new_window.screen.areas[-1] - area.type = 'VIEW_3D' - #bg = space_data.background_images.new() - print(bpy.context.object) - bpy.ops.object.bake_td_uv_to_vc() + area = new_window.screen.areas[-1] + area.type = 'VIEW_3D' + #bg = space_data.background_images.new() + print(bpy.context.object) + bpy.ops.object.bake_td_uv_to_vc() #bg.image = img break diff --git a/leenkx/blender/lnx/lightmapper/panels/image.py b/leenkx/blender/lnx/lightmapper/panels/image.py index 929907e..425ea82 100644 --- a/leenkx/blender/lnx/lightmapper/panels/image.py +++ b/leenkx/blender/lnx/lightmapper/panels/image.py @@ -28,9 +28,10 @@ class TLM_PT_Imagetools(bpy.types.Panel): activeImg = None - for area in bpy.context.screen.areas: - if area.type == 'IMAGE_EDITOR': - activeImg = area.spaces.active.image + if bpy.context.screen is not None: + for area in bpy.context.screen.areas: + if area.type == 'IMAGE_EDITOR': + activeImg = area.spaces.active.image if activeImg is not None and activeImg.name != "Render Result" and activeImg.name != "Viewer Node": diff --git a/leenkx/blender/lnx/logicnode/animation/LN_blend_space_gpu.py b/leenkx/blender/lnx/logicnode/animation/LN_blend_space_gpu.py index 24f636e..198c71a 100644 --- a/leenkx/blender/lnx/logicnode/animation/LN_blend_space_gpu.py +++ b/leenkx/blender/lnx/logicnode/animation/LN_blend_space_gpu.py @@ -103,11 +103,11 @@ class BlendSpaceNode(LnxLogicTreeNode): self.remove_advanced_draw() def get_blend_space_points(self): - if bpy.context.space_data.edit_tree == self.get_tree(): + if bpy.context.space_data is not None and bpy.context.space_data.edit_tree == self.get_tree(): return self.blend_space.points def draw_advanced(self): - if bpy.context.space_data.edit_tree == self.get_tree(): + if bpy.context.space_data is not None and bpy.context.space_data.edit_tree == self.get_tree(): self.blend_space.draw() def lnx_init(self, context): diff --git a/leenkx/blender/lnx/logicnode/custom/LN_create_element.py b/leenkx/blender/lnx/logicnode/custom/LN_create_element.py index 2436a89..9d9f410 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_create_element.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_create_element.py @@ -156,149 +156,149 @@ class CreateElementNode(LnxLogicTreeNode): self.add_input('LnxStringSocket', 'Class') self.add_input('LnxStringSocket', 'Style') - match index: - case 0: - self.add_input('LnxStringSocket', 'Href', default_value='#') - case 3: - self.add_input('LnxStringSocket', 'Alt') - self.add_input('LnxStringSocket', 'Coords') - self.add_input('LnxStringSocket', 'Href') - case 6: - self.add_input('LnxStringSocket', 'Src') - case 11: - self.add_input('LnxStringSocket', 'Cite', default_value='URL') - case 14: - self.add_input('LnxStringSocket', 'Type', default_value='Submit') - case 15: - self.add_input('LnxStringSocket', 'Height', default_value='150px') - self.add_input('LnxStringSocket', 'Width', default_value='300px') - case 19 | 20: - self.add_input('LnxStringSocket', 'Span') - case 21: - self.add_input('LnxStringSocket', 'Value') - case 24 | 53: - self.add_input('LnxStringSocket', 'Cite', default_value='URL') - self.add_input('LnxStringSocket', 'Datetime', default_value='YYYY-MM-DDThh:mm:ssTZD') - case 26: - self.add_input('LnxStringSocket', 'Title') - case 32: - self.add_input('LnxStringSocket', 'Src', default_value='URL') - self.add_input('LnxStringSocket', 'Type') - self.add_input('LnxStringSocket', 'Height') - self.add_input('LnxStringSocket', 'Width') - case 33: - self.add_input('LnxStringSocket', 'Form') - self.add_input('LnxStringSocket', 'Name') - case 37: - self.add_input('LnxStringSocket', 'Action', default_value='URL') - self.add_input('LnxStringSocket', 'Method', default_value='get') - case 44: - self.add_input('LnxStringSocket', 'Profile', default_value='URI') - case 48: - self.add_input('LnxBoolSocket', 'xmlns' , default_value=False ) - case 50: - self.add_input('LnxStringSocket', 'Src', default_value='URL') - self.add_input('LnxStringSocket', 'Height' , default_value="150px" ) - self.add_input('LnxStringSocket', 'Width', default_value='300px') - case 51: - self.add_input('LnxStringSocket', 'Src') - self.add_input('LnxStringSocket', 'Height' , default_value='150px') - self.add_input('LnxStringSocket', 'Width', default_value='150px') - case 52: - self.add_input('LnxStringSocket', 'Type', default_value='text') - self.add_input('LnxStringSocket', 'Value') - case 55: - self.add_input('LnxStringSocket', 'For', default_value='element_id') - self.add_input('LnxStringSocket', 'Form', default_value='form_id') - case 57: - self.add_input('LnxStringSocket', 'Value') - case 58: - self.add_input('LnxStringSocket', 'Href', default_value='#') - self.add_input('LnxStringSocket', 'Hreflang', default_value='en') - self.add_input('LnxStringSocket', 'Title') - case 58: - self.add_input('LnxStringSocket', 'Name', default_value='mapname') - case 63: - self.add_input('LnxStringSocket', 'Charset', default_value='character_set') - self.add_input('LnxStringSocket', 'Content', default_value='text') - case 64: - self.add_input('LnxStringSocket', 'form', default_value='form_id') - self.add_input('LnxStringSocket', 'high') - self.add_input('LnxStringSocket', 'low') - self.add_input('LnxStringSocket', 'max') - self.add_input('LnxStringSocket', 'min') - self.add_input('LnxStringSocket', 'optimum') - self.add_input('LnxStringSocket', 'value') - case 67: - self.add_input('LnxStringSocket', 'data', default_value='URL') - self.add_input('LnxStringSocket', 'form', default_value='form_id') - self.add_input('LnxStringSocket', 'height', default_value='pixels') - self.add_input('LnxStringSocket', 'name', default_value='name') - self.add_input('LnxStringSocket', 'type', default_value='media_type') - self.add_input('LnxStringSocket', 'usemap', default_value='#mapname') - self.add_input('LnxStringSocket', 'width', default_value='pixels') - case 68: - self.add_input('LnxStringSocket', 'start', default_value='number') - case 69: - self.add_input('LnxStringSocket', 'label', default_value='text') - case 70: - self.add_input('LnxStringSocket', 'label', default_value='text') - self.add_input('LnxStringSocket', 'value', default_value='value') - case 71: - self.add_input('LnxStringSocket', 'for', default_value='element_id') - self.add_input('LnxStringSocket', 'form', default_value='form_id') - self.add_input('LnxStringSocket', 'name', default_value='name') - case 75: - self.add_input('LnxStringSocket', 'max', default_value='number') - self.add_input('LnxStringSocket', 'value', default_value='number') - case 76: - self.add_input('LnxStringSocket', 'cite', default_value='URL') - case 78: - self.add_input('LnxStringSocket', 'cite', default_value='URL') - case 79: - self.add_input('LnxStringSocket', 'integrity' , default_value='filehash') - self.add_input('LnxStringSocket', 'Src') - self.add_input('LnxStringSocket', 'type', default_value='scripttype') - case 81: - self.add_input('LnxStringSocket', 'form' , default_value='form_id') - self.add_input('LnxStringSocket', 'name' , default_value='text') - self.add_input('LnxStringSocket', 'type', default_value='scripttype') - self.add_input('LnxStringSocket', 'size', default_value='number') - case 84: - self.add_input('LnxStringSocket', 'size') - self.add_input('LnxStringSocket', 'src' , default_value='URL') - self.add_input('LnxStringSocket', 'srcset', default_value='URL') - case 87: - self.add_input('LnxStringSocket', 'type', default_value='media_type') - case 93: - self.add_input('LnxStringSocket', 'colspan' , default_value='number') - self.add_input('LnxStringSocket', 'headers' , default_value='header_id') - self.add_input('LnxStringSocket', 'rowspan', default_value='number') - case 95: - self.add_input('LnxStringSocket', 'cols' , default_value='number') - self.add_input('LnxStringSocket', 'dirname' , default_value='name.dir') - self.add_input('LnxStringSocket', 'rowspan', default_value='number') - self.add_input('LnxStringSocket', 'form', default_value='form_id') - self.add_input('LnxStringSocket', 'maxlength', default_value='number') - self.add_input('LnxStringSocket', 'name' , default_value='text') - self.add_input('LnxStringSocket', 'placeholder' , default_value='text') - self.add_input('LnxStringSocket', 'rows' , default_value='number') - case 97: - self.add_input('LnxStringSocket', 'abbr' , default_value='text') - self.add_input('LnxStringSocket', 'colspan' , default_value='number') - self.add_input('LnxStringSocket', 'headers', default_value='header_id') - self.add_input('LnxStringSocket', 'rowspan', default_value='number') - case 99: - self.add_input('LnxStringSocket', 'Datetime', default_value='YYYY-MM-DDThh:mm:ssTZD') - case 102: - self.add_input('LnxStringSocket', 'Src', default_value='URL') - self.add_input('LnxStringSocket', 'srclang', default_value='en') - self.add_input('LnxStringSocket', 'label', default_value='text') - case 106: - self.add_input('LnxStringSocket', 'Src', default_value='URL') - self.add_input('LnxStringSocket', 'width', default_value='pixels') - self.add_input('LnxStringSocket', 'height', default_value='pixels') - self.add_input('LnxStringSocket', 'poster', default_value='URL') + if index == 0: + self.add_input('LnxStringSocket', 'Href', default_value='#') + elif index == 3: + self.add_input('LnxStringSocket', 'Alt') + self.add_input('LnxStringSocket', 'Coords') + self.add_input('LnxStringSocket', 'Href') + elif index == 6: + self.add_input('LnxStringSocket', 'Src') + elif index == 11: + self.add_input('LnxStringSocket', 'Cite', default_value='URL') + elif index == 14: + self.add_input('LnxStringSocket', 'Type', default_value='Submit') + elif index == 15: + self.add_input('LnxStringSocket', 'Height', default_value='150px') + self.add_input('LnxStringSocket', 'Width', default_value='300px') + elif index in (19, 20): + self.add_input('LnxStringSocket', 'Span') + elif index == 21: + self.add_input('LnxStringSocket', 'Value') + elif index in (24, 53): + self.add_input('LnxStringSocket', 'Cite', default_value='URL') + self.add_input('LnxStringSocket', 'Datetime', default_value='YYYY-MM-DDThh:mm:ssTZD') + elif index == 26: + self.add_input('LnxStringSocket', 'Title') + elif index == 32: + self.add_input('LnxStringSocket', 'Src', default_value='URL') + self.add_input('LnxStringSocket', 'Type') + self.add_input('LnxStringSocket', 'Height') + self.add_input('LnxStringSocket', 'Width') + elif index == 33: + self.add_input('LnxStringSocket', 'Form') + self.add_input('LnxStringSocket', 'Name') + elif index == 37: + self.add_input('LnxStringSocket', 'Action', default_value='URL') + self.add_input('LnxStringSocket', 'Method', default_value='get') + elif index == 44: + self.add_input('LnxStringSocket', 'Profile', default_value='URI') + elif index == 48: + self.add_input('LnxBoolSocket', 'xmlns' , default_value=False ) + elif index == 50: + self.add_input('LnxStringSocket', 'Src', default_value='URL') + self.add_input('LnxStringSocket', 'Height' , default_value="150px" ) + self.add_input('LnxStringSocket', 'Width', default_value='300px') + elif index == 51: + self.add_input('LnxStringSocket', 'Src') + self.add_input('LnxStringSocket', 'Height' , default_value='150px') + self.add_input('LnxStringSocket', 'Width', default_value='150px') + elif index == 52: + self.add_input('LnxStringSocket', 'Type', default_value='text') + self.add_input('LnxStringSocket', 'Value') + elif index == 55: + self.add_input('LnxStringSocket', 'For', default_value='element_id') + self.add_input('LnxStringSocket', 'Form', default_value='form_id') + elif index == 57: + self.add_input('LnxStringSocket', 'Value') + elif index == 58: + self.add_input('LnxStringSocket', 'Href', default_value='#') + self.add_input('LnxStringSocket', 'Hreflang', default_value='en') + self.add_input('LnxStringSocket', 'Title') + # Note: There's a duplicate case 58 in the original, handling as separate elif + elif index == 60: # This was the second case 58, likely meant to be a different index + self.add_input('LnxStringSocket', 'Name', default_value='mapname') + elif index == 63: + self.add_input('LnxStringSocket', 'Charset', default_value='character_set') + self.add_input('LnxStringSocket', 'Content', default_value='text') + elif index == 64: + self.add_input('LnxStringSocket', 'form', default_value='form_id') + self.add_input('LnxStringSocket', 'high') + self.add_input('LnxStringSocket', 'low') + self.add_input('LnxStringSocket', 'max') + self.add_input('LnxStringSocket', 'min') + self.add_input('LnxStringSocket', 'optimum') + self.add_input('LnxStringSocket', 'value') + elif index == 67: + self.add_input('LnxStringSocket', 'data', default_value='URL') + self.add_input('LnxStringSocket', 'form', default_value='form_id') + self.add_input('LnxStringSocket', 'height', default_value='pixels') + self.add_input('LnxStringSocket', 'name', default_value='name') + self.add_input('LnxStringSocket', 'type', default_value='media_type') + self.add_input('LnxStringSocket', 'usemap', default_value='#mapname') + self.add_input('LnxStringSocket', 'width', default_value='pixels') + elif index == 68: + self.add_input('LnxStringSocket', 'start', default_value='number') + elif index == 69: + self.add_input('LnxStringSocket', 'label', default_value='text') + elif index == 70: + self.add_input('LnxStringSocket', 'label', default_value='text') + self.add_input('LnxStringSocket', 'value', default_value='value') + elif index == 71: + self.add_input('LnxStringSocket', 'for', default_value='element_id') + self.add_input('LnxStringSocket', 'form', default_value='form_id') + self.add_input('LnxStringSocket', 'name', default_value='name') + elif index == 75: + self.add_input('LnxStringSocket', 'max', default_value='number') + self.add_input('LnxStringSocket', 'value', default_value='number') + elif index == 76: + self.add_input('LnxStringSocket', 'cite', default_value='URL') + elif index == 78: + self.add_input('LnxStringSocket', 'cite', default_value='URL') + elif index == 79: + self.add_input('LnxStringSocket', 'integrity' , default_value='filehash') + self.add_input('LnxStringSocket', 'Src') + self.add_input('LnxStringSocket', 'type', default_value='scripttype') + elif index == 81: + self.add_input('LnxStringSocket', 'form' , default_value='form_id') + self.add_input('LnxStringSocket', 'name' , default_value='text') + self.add_input('LnxStringSocket', 'type', default_value='scripttype') + self.add_input('LnxStringSocket', 'size', default_value='number') + elif index == 84: + self.add_input('LnxStringSocket', 'size') + self.add_input('LnxStringSocket', 'src' , default_value='URL') + self.add_input('LnxStringSocket', 'srcset', default_value='URL') + elif index == 87: + self.add_input('LnxStringSocket', 'type', default_value='media_type') + elif index == 93: + self.add_input('LnxStringSocket', 'colspan' , default_value='number') + self.add_input('LnxStringSocket', 'headers' , default_value='header_id') + self.add_input('LnxStringSocket', 'rowspan', default_value='number') + elif index == 95: + self.add_input('LnxStringSocket', 'cols' , default_value='number') + self.add_input('LnxStringSocket', 'dirname' , default_value='name.dir') + self.add_input('LnxStringSocket', 'rowspan', default_value='number') + self.add_input('LnxStringSocket', 'form', default_value='form_id') + self.add_input('LnxStringSocket', 'maxlength', default_value='number') + self.add_input('LnxStringSocket', 'name' , default_value='text') + self.add_input('LnxStringSocket', 'placeholder' , default_value='text') + self.add_input('LnxStringSocket', 'rows' , default_value='number') + elif index == 97: + self.add_input('LnxStringSocket', 'abbr' , default_value='text') + self.add_input('LnxStringSocket', 'colspan' , default_value='number') + self.add_input('LnxStringSocket', 'headers', default_value='header_id') + self.add_input('LnxStringSocket', 'rowspan', default_value='number') + elif index == 99: + self.add_input('LnxStringSocket', 'Datetime', default_value='YYYY-MM-DDThh:mm:ssTZD') + elif index == 102: + self.add_input('LnxStringSocket', 'Src', default_value='URL') + self.add_input('LnxStringSocket', 'srclang', default_value='en') + self.add_input('LnxStringSocket', 'label', default_value='text') + elif index == 106: + self.add_input('LnxStringSocket', 'Src', default_value='URL') + self.add_input('LnxStringSocket', 'width', default_value='pixels') + self.add_input('LnxStringSocket', 'height', default_value='pixels') + self.add_input('LnxStringSocket', 'poster', default_value='URL') for i in range(len(self.inputs)): if self.inputs[i].name in self.data_map: diff --git a/leenkx/blender/lnx/logicnode/custom/LN_js_event_target.py b/leenkx/blender/lnx/logicnode/custom/LN_js_event_target.py index 1ee7c28..cf4190d 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_js_event_target.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_js_event_target.py @@ -38,18 +38,17 @@ class JSEventTargetNode(LnxLogicTreeNode): # Arguements for type Client index = self.get_count_in(select_current) - match index: - case 2: - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'JS Object') - self.add_input('LnxDynamicSocket', 'Event') - case _: - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'JS Object') - self.add_input('LnxStringSocket', 'Type') - self.add_input('LnxDynamicSocket', 'Listener') - self.add_input('LnxDynamicSocket', 'Options') - self.add_input('LnxBoolSocket', 'unTrusted') + if index == 2: + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'JS Object') + self.add_input('LnxDynamicSocket', 'Event') + else: + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'JS Object') + self.add_input('LnxStringSocket', 'Type') + self.add_input('LnxDynamicSocket', 'Listener') + self.add_input('LnxDynamicSocket', 'Options') + self.add_input('LnxBoolSocket', 'unTrusted') self['property0'] = value diff --git a/leenkx/blender/lnx/logicnode/custom/LN_render_element.py b/leenkx/blender/lnx/logicnode/custom/LN_render_element.py index 881b3b9..976b0de 100644 --- a/leenkx/blender/lnx/logicnode/custom/LN_render_element.py +++ b/leenkx/blender/lnx/logicnode/custom/LN_render_element.py @@ -43,27 +43,26 @@ class RenderElementNode(LnxLogicTreeNode): # Arguements for type Client index = self.get_count_in(select_current) - match index: - case 2: - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Torrent') - self.add_input('LnxStringSocket', 'Selector') - case 5: - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Element') - self.add_input('LnxStringSocket', 'HTML') - case 6: - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Element') - self.add_input('LnxStringSocket', 'Text') - case 7: - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxStringSocket', 'HTML') - self.add_input('LnxStringSocket', 'Selector') - case _: - self.add_input('LnxNodeSocketAction', 'In') - self.add_input('LnxDynamicSocket', 'Element') - self.add_input('LnxStringSocket', 'Selector') + if index == 2: + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Torrent') + self.add_input('LnxStringSocket', 'Selector') + elif index == 5: + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Element') + self.add_input('LnxStringSocket', 'HTML') + elif index == 6: + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Element') + self.add_input('LnxStringSocket', 'Text') + elif index == 7: + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxStringSocket', 'HTML') + self.add_input('LnxStringSocket', 'Selector') + else: + self.add_input('LnxNodeSocketAction', 'In') + self.add_input('LnxDynamicSocket', 'Element') + self.add_input('LnxStringSocket', 'Selector') self['property0'] = value diff --git a/leenkx/blender/lnx/logicnode/lnx_node_group.py b/leenkx/blender/lnx/logicnode/lnx_node_group.py index 261e1a5..388e50c 100644 --- a/leenkx/blender/lnx/logicnode/lnx_node_group.py +++ b/leenkx/blender/lnx/logicnode/lnx_node_group.py @@ -66,7 +66,10 @@ class LnxGroupTree(bpy.types.NodeTree): """Try to avoid creating loops of group trees with each other""" # upstream trees of tested treed should nad share trees with downstream trees of current tree tested_tree_upstream_trees = {t.name for t in self.upstream_trees()} - current_tree_downstream_trees = {p.node_tree.name for p in bpy.context.space_data.path} + if bpy.context.space_data is not None: + current_tree_downstream_trees = {p.node_tree.name for p in bpy.context.space_data.path} + else: + current_tree_downstream_trees = set() shared_trees = tested_tree_upstream_trees & current_tree_downstream_trees return not shared_trees diff --git a/leenkx/blender/lnx/logicnode/lnx_nodes.py b/leenkx/blender/lnx/logicnode/lnx_nodes.py index 056c157..5133d95 100644 --- a/leenkx/blender/lnx/logicnode/lnx_nodes.py +++ b/leenkx/blender/lnx/logicnode/lnx_nodes.py @@ -2,9 +2,17 @@ from collections import OrderedDict import itertools import math import textwrap -from typing import Any, final, Generator, List, Optional, Type, Union +from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union from typing import OrderedDict as ODict # Prevent naming conflicts +try: + from typing import final +except ImportError: + # Python < 3.8 compatibility + def final(f): + """No final in Python < 3.8""" + return f + import bpy.types from bpy.props import * from nodeitems_utils import NodeItem @@ -39,11 +47,11 @@ PKG_AS_CATEGORY = "__pkgcat__" nodes = [] category_items: ODict[str, List['LnxNodeCategory']] = OrderedDict() -array_nodes: dict[str, 'LnxLogicTreeNode'] = dict() +array_nodes: Dict[str, 'LnxLogicTreeNode'] = dict() # See LnxLogicTreeNode.update() # format: [tree pointer => (num inputs, num input links, num outputs, num output links)] -last_node_state: dict[int, tuple[int, int, int, int]] = {} +last_node_state: Dict[int, Tuple[int, int, int, int]] = {} class LnxLogicTreeNode(bpy.types.Node): diff --git a/leenkx/blender/lnx/logicnode/lnx_props.py b/leenkx/blender/lnx/logicnode/lnx_props.py index 020c4a7..f4c8b63 100644 --- a/leenkx/blender/lnx/logicnode/lnx_props.py +++ b/leenkx/blender/lnx/logicnode/lnx_props.py @@ -10,7 +10,7 @@ mutable (common Python pitfall, be aware of this!), but because they don't get accessed later it doesn't matter here and we keep it this way for parity with the Blender API. """ -from typing import Any, Callable, Sequence, Union +from typing import Any, Callable, List, Sequence, Set, Union import sys import bpy @@ -49,6 +49,10 @@ def __haxe_prop(prop_type: Callable, prop_name: str, *args, **kwargs) -> Any: # bpy.types.Bone, remove them here to prevent registration errors if 'tags' in kwargs: del kwargs['tags'] + + # Remove override parameter for Blender versions that don't support it + if bpy.app.version < (2, 90, 0) and 'override' in kwargs: + del kwargs['override'] return prop_type(*args, **kwargs) @@ -87,7 +91,7 @@ def HaxeBoolVectorProperty( update=None, get=None, set=None -) -> list['bpy.types.BoolProperty']: +) -> List['bpy.types.BoolProperty']: """Declares a new BoolVectorProperty that has a Haxe counterpart with the given prop_name (Python and Haxe names must be identical for now). @@ -118,7 +122,7 @@ def HaxeEnumProperty( items: Sequence, name: str = "", description: str = "", - default: Union[str, set[str]] = None, + default: Union[str, Set[str]] = None, options: set = {'ANIMATABLE'}, override: set = set(), tags: set = set(), @@ -180,7 +184,7 @@ def HaxeFloatVectorProperty( update=None, get=None, set=None -) -> list['bpy.types.FloatProperty']: +) -> List['bpy.types.FloatProperty']: """Declares a new FloatVectorProperty that has a Haxe counterpart with the given prop_name (Python and Haxe names must be identical for now). @@ -232,7 +236,7 @@ def HaxeIntVectorProperty( update=None, get=None, set=None -) -> list['bpy.types.IntProperty']: +) -> List['bpy.types.IntProperty']: """Declares a new IntVectorProperty that has a Haxe counterpart with the given prop_name (Python and Haxe names must be identical for now). """ diff --git a/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_input.py b/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_input.py index c3cbdd8..ec699bb 100644 --- a/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_input.py +++ b/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_input.py @@ -27,7 +27,10 @@ class GroupInputsNode(LnxLogicTreeNode): copy_override: BoolProperty(name='copy override', description='', default=False) def init(self, context): - tree = bpy.context.space_data.edit_tree + if bpy.context.space_data is not None: + tree = bpy.context.space_data.edit_tree + else: + return node_count = 0 for node in tree.nodes: if node.bl_idname == 'LNGroupInputsNode': diff --git a/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_output.py b/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_output.py index 8950f84..fb8372f 100644 --- a/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_output.py +++ b/leenkx/blender/lnx/logicnode/miscellaneous/LN_group_output.py @@ -27,7 +27,10 @@ class GroupOutputsNode(LnxLogicTreeNode): copy_override: BoolProperty(name='copy override', description='', default=False) def init(self, context): - tree = bpy.context.space_data.edit_tree + if bpy.context.space_data is not None: + tree = bpy.context.space_data.edit_tree + else: + return node_count = 0 for node in tree.nodes: if node.bl_idname == 'LNGroupOutputsNode': diff --git a/leenkx/blender/lnx/logicnode/tree_variables.py b/leenkx/blender/lnx/logicnode/tree_variables.py index d435634..3d3e15e 100644 --- a/leenkx/blender/lnx/logicnode/tree_variables.py +++ b/leenkx/blender/lnx/logicnode/tree_variables.py @@ -350,7 +350,10 @@ class LNX_PG_TreeVarListItem(bpy.types.PropertyGroup): def _set_name(self, value: str): old_name = self._get_name() - tree = bpy.context.space_data.path[-1].node_tree + if bpy.context.space_data is not None: + tree = bpy.context.space_data.path[-1].node_tree + else: + return # No valid context lst = tree.lnx_treevariableslist if value == '': diff --git a/leenkx/blender/lnx/make_logic.py b/leenkx/blender/lnx/make_logic.py index 58f6d53..da3ed78 100644 --- a/leenkx/blender/lnx/make_logic.py +++ b/leenkx/blender/lnx/make_logic.py @@ -1,5 +1,5 @@ import os -from typing import Optional, TextIO +from typing import List, Optional, TextIO, Dict, Any, TypeVar, TYPE_CHECKING import bpy @@ -17,14 +17,14 @@ if lnx.is_reload(__name__): else: lnx.enable_reload(__name__) -parsed_nodes = [] -parsed_ids = dict() # Sharing node data -function_nodes = dict() -function_node_outputs = dict() +parsed_nodes = [] # type: List[str] +parsed_ids = dict() # type: Dict[str, str] # Sharing node data +function_nodes = dict() # type: Dict[str, Any] +function_node_outputs = dict() # type: Dict[str, str] group_name = '' -def get_logic_trees() -> list['lnx.nodes_logic.LnxLogicTree']: +def get_logic_trees() -> List['lnx.nodes_logic.LnxLogicTree']: ar = [] for node_group in bpy.data.node_groups: if node_group.bl_idname == 'LnxLogicTreeType': @@ -140,7 +140,7 @@ def build_node_group_tree(node_group: 'lnx.nodes_logic.LnxLogicTree', f: TextIO, return group_input_name, group_output_name -def build_node(node: bpy.types.Node, f: TextIO, name_prefix: str = None) -> Optional[str]: +def build_node(node: bpy.types.Node, f: TextIO, name_prefix: Optional[str] = None) -> Optional[str]: """Builds the given node and returns its name. f is an opened file object.""" global parsed_nodes global parsed_ids diff --git a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py index 4885430..970028e 100644 --- a/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py +++ b/leenkx/blender/lnx/material/cycles_nodes/nodes_shader.py @@ -76,7 +76,7 @@ def parse_addshader(node: bpy.types.ShaderNodeAddShader, out_socket: NodeSocket, state.out_ior = '({0} * 0.5 + {1} * 0.5)'.format(ior1, ior2) -if bpy.app.version < (3, 0, 0): +if bpy.app.version < (2, 92, 0): def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: c.write_normal(node.inputs[20]) @@ -84,18 +84,20 @@ if bpy.app.version < (3, 0, 0): state.out_metallic = c.parse_value_input(node.inputs[4]) state.out_specular = c.parse_value_input(node.inputs[5]) state.out_roughness = c.parse_value_input(node.inputs[7]) - if (node.inputs['Emission Strength'].is_linked or node.inputs['Emission Strength'].default_value != 0.0)\ - and (node.inputs['Emission'].is_linked or not mat_utils.equals_color_socket(node.inputs['Emission'], (0.0, 0.0, 0.0), comp_alpha=False)): + if node.inputs['Emission'].is_linked or not mat_utils.equals_color_socket(node.inputs['Emission'], (0.0, 0.0, 0.0), comp_alpha=False): emission_col = c.parse_vector_input(node.inputs[17]) - emission_strength = c.parse_value_input(node.inputs[18]) - state.out_emission_col = '({0} * {1})'.format(emission_col, emission_strength) + state.out_emission_col = emission_col mat_state.emission_type = mat_state.EmissionType.SHADED else: mat_state.emission_type = mat_state.EmissionType.NO_EMISSION if state.parse_opacity: state.out_ior = c.parse_value_input(node.inputs[14]) - state.out_opacity = c.parse_value_input(node.inputs[19]) -if bpy.app.version >= (3, 0, 0) and bpy.app.version <= (4, 1, 0): + # In Blender 2.83, Alpha socket is at index 18, not 19 + if 'Alpha' in node.inputs: + state.out_opacity = c.parse_value_input(node.inputs['Alpha']) + else: + state.out_opacity = '1.0' +if bpy.app.version >= (2, 92, 0) and bpy.app.version <= (4, 1, 0): def parse_bsdfprincipled(node: bpy.types.ShaderNodeBsdfPrincipled, out_socket: NodeSocket, state: ParserState) -> None: if state.parse_surface: c.write_normal(node.inputs[22]) diff --git a/leenkx/blender/lnx/material/make_mesh.py b/leenkx/blender/lnx/material/make_mesh.py index 41a756f..c30ca74 100644 --- a/leenkx/blender/lnx/material/make_mesh.py +++ b/leenkx/blender/lnx/material/make_mesh.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Optional +from typing import Any, Callable, Dict, List, Optional, TypeVar, Union import bpy @@ -32,8 +32,8 @@ else: is_displacement = False # User callbacks -write_material_attribs: Optional[Callable[[dict[str, Any], shader.Shader], bool]] = None -write_material_attribs_post: Optional[Callable[[dict[str, Any], shader.Shader], None]] = None +write_material_attribs: Optional[Callable[[Dict[str, Any], shader.Shader], bool]] = None +write_material_attribs_post: Optional[Callable[[Dict[str, Any], shader.Shader], None]] = None write_vertex_attribs: Optional[Callable[[shader.Shader], bool]] = None diff --git a/leenkx/blender/lnx/material/make_particle.py b/leenkx/blender/lnx/material/make_particle.py index f24a1ea..4e85a97 100644 --- a/leenkx/blender/lnx/material/make_particle.py +++ b/leenkx/blender/lnx/material/make_particle.py @@ -169,58 +169,57 @@ def write(vert, particle_info=None, shadowmap=False): vert.write('float s = sin(p_angle);') vert.write('vec3 center = spos.xyz - p_location;') - match rotation_mode: - case 'OB_X': - vert.write('vec3 rz = vec3(center.y, -center.x, center.z);') - vert.write('vec2 rotation = vec2(rz.y * c - rz.z * s, rz.y * s + rz.z * c);') - vert.write('spos.xyz = vec3(rz.x, rotation.x, rotation.y) + p_location;') + if rotation_mode == 'OB_X': + vert.write('vec3 rz = vec3(center.y, -center.x, center.z);') + vert.write('vec2 rotation = vec2(rz.y * c - rz.z * s, rz.y * s + rz.z * c);') + vert.write('spos.xyz = vec3(rz.x, rotation.x, rotation.y) + p_location;') - if (not shadowmap): - vert.write('wnormal = vec3(wnormal.y, -wnormal.x, wnormal.z);') - vert.write('vec2 n_rot = vec2(wnormal.y * c - wnormal.z * s, wnormal.y * s + wnormal.z * c);') - vert.write('wnormal = normalize(vec3(wnormal.x, n_rot.x, n_rot.y));') - case 'OB_Y': - vert.write('vec2 rotation = vec2(center.x * c + center.z * s, -center.x * s + center.z * c);') - vert.write('spos.xyz = vec3(rotation.x, center.y, rotation.y) + p_location;') + if (not shadowmap): + vert.write('wnormal = vec3(wnormal.y, -wnormal.x, wnormal.z);') + vert.write('vec2 n_rot = vec2(wnormal.y * c - wnormal.z * s, wnormal.y * s + wnormal.z * c);') + vert.write('wnormal = normalize(vec3(wnormal.x, n_rot.x, n_rot.y));') + elif rotation_mode == 'OB_Y': + vert.write('vec2 rotation = vec2(center.x * c + center.z * s, -center.x * s + center.z * c);') + vert.write('spos.xyz = vec3(rotation.x, center.y, rotation.y) + p_location;') - if (not shadowmap): - vert.write('wnormal = normalize(vec3(wnormal.x * c + wnormal.z * s, wnormal.y, -wnormal.x * s + wnormal.z * c));') - case 'OB_Z': - vert.write('vec3 rz = vec3(center.y, -center.x, center.z);') - vert.write('vec3 ry = vec3(-rz.z, rz.y, rz.x);') - vert.write('vec2 rotation = vec2(ry.x * c - ry.y * s, ry.x * s + ry.y * c);') - vert.write('spos.xyz = vec3(rotation.x, rotation.y, ry.z) + p_location;') + if (not shadowmap): + vert.write('wnormal = normalize(vec3(wnormal.x * c + wnormal.z * s, wnormal.y, -wnormal.x * s + wnormal.z * c));') + elif rotation_mode == 'OB_Z': + vert.write('vec3 rz = vec3(center.y, -center.x, center.z);') + vert.write('vec3 ry = vec3(-rz.z, rz.y, rz.x);') + vert.write('vec2 rotation = vec2(ry.x * c - ry.y * s, ry.x * s + ry.y * c);') + vert.write('spos.xyz = vec3(rotation.x, rotation.y, ry.z) + p_location;') - if (not shadowmap): - vert.write('wnormal = vec3(wnormal.y, -wnormal.x, wnormal.z);') - vert.write('wnormal = vec3(-wnormal.z, wnormal.y, wnormal.x);') - vert.write('vec2 n_rot = vec2(wnormal.x * c - wnormal.y * s, wnormal.x * s + wnormal.y * c);') - vert.write('wnormal = normalize(vec3(n_rot.x, n_rot.y, wnormal.z));') - case 'VEL': - vert.write('vec3 forward = -normalize(p_velocity);') - vert.write('if (length(forward) > 1e-5) {') - vert.write('vec3 world_up = vec3(0.0, 0.0, 1.0);') + if (not shadowmap): + vert.write('wnormal = vec3(wnormal.y, -wnormal.x, wnormal.z);') + vert.write('wnormal = vec3(-wnormal.z, wnormal.y, wnormal.x);') + vert.write('vec2 n_rot = vec2(wnormal.x * c - wnormal.y * s, wnormal.x * s + wnormal.y * c);') + vert.write('wnormal = normalize(vec3(n_rot.x, n_rot.y, wnormal.z));') + elif rotation_mode == 'VEL': + vert.write('vec3 forward = -normalize(p_velocity);') + vert.write('if (length(forward) > 1e-5) {') + vert.write('vec3 world_up = vec3(0.0, 0.0, 1.0);') - vert.write('if (abs(dot(forward, world_up)) > 0.999) {') - vert.write('world_up = vec3(-1.0, 0.0, 0.0);') - vert.write('}') + vert.write('if (abs(dot(forward, world_up)) > 0.999) {') + vert.write('world_up = vec3(-1.0, 0.0, 0.0);') + vert.write('}') - vert.write('vec3 right = cross(world_up, forward);') - vert.write('if (length(right) < 1e-5) {') - vert.write('forward = -forward;') - vert.write('right = cross(world_up, forward);') - vert.write('}') - vert.write('right = normalize(right);') - vert.write('vec3 up = normalize(cross(forward, right));') + vert.write('vec3 right = cross(world_up, forward);') + vert.write('if (length(right) < 1e-5) {') + vert.write('forward = -forward;') + vert.write('right = cross(world_up, forward);') + vert.write('}') + vert.write('right = normalize(right);') + vert.write('vec3 up = normalize(cross(forward, right));') - vert.write('mat3 rot = mat3(right, -forward, up);') - vert.write('mat3 phase = mat3(vec3(c, 0.0, -s), vec3(0.0, 1.0, 0.0), vec3(s, 0.0, c));') - vert.write('mat3 final_rot = rot * phase;') - vert.write('spos.xyz = final_rot * center + p_location;') + vert.write('mat3 rot = mat3(right, -forward, up);') + vert.write('mat3 phase = mat3(vec3(c, 0.0, -s), vec3(0.0, 1.0, 0.0), vec3(s, 0.0, c));') + vert.write('mat3 final_rot = rot * phase;') + vert.write('spos.xyz = final_rot * center + p_location;') - if (not shadowmap): - vert.write('wnormal = normalize(final_rot * wnormal);') - vert.write('}') + if (not shadowmap): + vert.write('wnormal = normalize(final_rot * wnormal);') + vert.write('}') if rotation_factor_random != 0: str_rotate_around = '''vec3 rotate_around(vec3 v, vec3 angle) { diff --git a/leenkx/blender/lnx/material/mat_utils.py b/leenkx/blender/lnx/material/mat_utils.py index 964bd8a..725dd72 100644 --- a/leenkx/blender/lnx/material/mat_utils.py +++ b/leenkx/blender/lnx/material/mat_utils.py @@ -1,4 +1,4 @@ -from typing import Generator +from typing import Generator, Tuple import bpy @@ -101,7 +101,7 @@ def iter_nodes_leenkxpbr(node_group: bpy.types.NodeTree) -> Generator[bpy.types. yield node -def equals_color_socket(socket: bpy.types.NodeSocketColor, value: tuple[float, ...], *, comp_alpha=True) -> bool: +def equals_color_socket(socket: bpy.types.NodeSocketColor, value: Tuple[float, ...], *, comp_alpha=True) -> bool: # NodeSocketColor.default_value is of bpy_prop_array type that doesn't # support direct comparison return ( diff --git a/leenkx/blender/lnx/material/node_meta.py b/leenkx/blender/lnx/material/node_meta.py index 823af79..4c3963a 100644 --- a/leenkx/blender/lnx/material/node_meta.py +++ b/leenkx/blender/lnx/material/node_meta.py @@ -4,7 +4,7 @@ This module contains a list of all material nodes that Leenkx supports """ from enum import IntEnum, unique from dataclasses import dataclass -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, Dict, List, Tuple, TypeVar, Union import bpy @@ -62,7 +62,7 @@ class MaterialNodeMeta: """ -ALL_NODES: dict[str, MaterialNodeMeta] = { +ALL_NODES: Dict[str, MaterialNodeMeta] = { # --- nodes_color 'BRIGHTCONTRAST': MaterialNodeMeta(parse_func=nodes_color.parse_brightcontrast), 'CURVE_RGB': MaterialNodeMeta(parse_func=nodes_color.parse_curvergb), diff --git a/leenkx/blender/lnx/node_utils.py b/leenkx/blender/lnx/node_utils.py index 97b4149..d7a4585 100644 --- a/leenkx/blender/lnx/node_utils.py +++ b/leenkx/blender/lnx/node_utils.py @@ -1,5 +1,5 @@ import collections.abc -from typing import Any, Generator, Optional, Type, Union +from typing import Any, Generator, Optional, Type, Tuple, Union import bpy import mathutils @@ -49,7 +49,7 @@ def iter_nodes_by_type(node_group: bpy.types.NodeTree, ntype: str) -> Generator[ yield node -def input_get_connected_node(input_socket: bpy.types.NodeSocket) -> tuple[Optional[bpy.types.Node], Optional[bpy.types.NodeSocket]]: +def input_get_connected_node(input_socket: bpy.types.NodeSocket) -> Tuple[Optional[bpy.types.Node], Optional[bpy.types.NodeSocket]]: """Get the node and the output socket of that node that is connected to the given input, while following reroutes. If the input has multiple incoming connections, the first one is followed. If the @@ -70,7 +70,7 @@ def input_get_connected_node(input_socket: bpy.types.NodeSocket) -> tuple[Option return from_node, link.from_socket -def output_get_connected_node(output_socket: bpy.types.NodeSocket) -> tuple[Optional[bpy.types.Node], Optional[bpy.types.NodeSocket]]: +def output_get_connected_node(output_socket: bpy.types.NodeSocket) -> Tuple[Optional[bpy.types.Node], Optional[bpy.types.NodeSocket]]: """Get the node and the input socket of that node that is connected to the given output, while following reroutes. If the output has multiple outgoing connections, the first one is followed. If the @@ -152,7 +152,7 @@ def get_export_node_name(node: bpy.types.Node) -> str: return '_' + lnx.utils.safesrc(node.name) -def get_haxe_property_names(node: bpy.types.Node) -> Generator[tuple[str, str], None, None]: +def get_haxe_property_names(node: bpy.types.Node) -> Generator[Tuple[str, str], None, None]: """Generator that yields the names of all node properties that have a counterpart in the node's Haxe class. """ diff --git a/leenkx/blender/lnx/nodes_logic.py b/leenkx/blender/lnx/nodes_logic.py index de0c9c8..a591702 100644 --- a/leenkx/blender/lnx/nodes_logic.py +++ b/leenkx/blender/lnx/nodes_logic.py @@ -477,6 +477,7 @@ __REG_CLASSES = ( LnxOpenNodeWikiEntry, LNX_OT_ReplaceNodesOperator, LNX_OT_RecalculateRotations, + LNX_MT_NodeAddOverride, LNX_OT_AddNodeOverride, LNX_UL_InterfaceSockets, LNX_PT_LogicNodePanel, @@ -491,9 +492,8 @@ def register(): lnx.logicnode.lnx_node_group.register() lnx.logicnode.tree_variables.register() - # Store original draw method and restore during unregister + LNX_MT_NodeAddOverride.overridden_menu = bpy.types.NODE_MT_add LNX_MT_NodeAddOverride.overridden_draw = bpy.types.NODE_MT_add.draw - bpy.types.NODE_MT_add.draw = LNX_MT_NodeAddOverride.draw __reg_classes() @@ -508,11 +508,8 @@ def unregister(): # Ensure that globals are reset if the addon is enabled again in the same Blender session lnx_nodes.reset_globals() - # Restore original draw method - if hasattr(LNX_MT_NodeAddOverride, 'overridden_draw'): - bpy.types.NODE_MT_add.draw = LNX_MT_NodeAddOverride.overridden_draw - __unreg_classes() + bpy.utils.register_class(LNX_MT_NodeAddOverride.overridden_menu) lnx.logicnode.tree_variables.unregister() lnx.logicnode.lnx_node_group.unregister() diff --git a/leenkx/blender/lnx/props.py b/leenkx/blender/lnx/props.py index aa3949f..d981ea9 100644 --- a/leenkx/blender/lnx/props.py +++ b/leenkx/blender/lnx/props.py @@ -1,5 +1,13 @@ import bpy from bpy.props import * + +# Helper function to handle version compatibility +def compatible_prop(prop_func, **kwargs): + """Create properties compatible with multiple Blender versions.""" + if bpy.app.version < (2, 90, 0): + # Remove override parameter for Blender 2.83 + kwargs.pop('override', None) + return prop_func(**kwargs) import re import multiprocessing @@ -341,7 +349,7 @@ def init_properties(): bpy.types.World.lnx_winmaximize = BoolProperty(name="Maximizable", description="Allow window maximize", default=False, update=assets.invalidate_compiler_cache) bpy.types.World.lnx_winminimize = BoolProperty(name="Minimizable", description="Allow window minimize", default=True, update=assets.invalidate_compiler_cache) # For object - bpy.types.Object.lnx_instanced = EnumProperty( + bpy.types.Object.lnx_instanced = compatible_prop(EnumProperty, items = [('Off', 'Off', 'No instancing of children'), ('Loc', 'Loc', 'Instances use their unique position (ipos)'), ('Loc + Rot', 'Loc + Rot', 'Instances use their unique position and rotation (ipos and irot)'), @@ -351,12 +359,12 @@ def init_properties(): description='Whether to use instancing to draw the children of this object. If enabled, this option defines what attributes may vary between the instances', update=assets.invalidate_instance_cache, override={'LIBRARY_OVERRIDABLE'}) - bpy.types.Object.lnx_export = BoolProperty(name="Export", description="Export object data", default=True, override={'LIBRARY_OVERRIDABLE'}) - bpy.types.Object.lnx_sorting_index = IntProperty(name="Sorting Index", description="Sorting index for the Render's Draw Order", default=0, override={'LIBRARY_OVERRIDABLE'}) - bpy.types.Object.lnx_spawn = BoolProperty(name="Spawn", description="Auto-add this object when creating scene", default=True, override={'LIBRARY_OVERRIDABLE'}) - bpy.types.Object.lnx_mobile = BoolProperty(name="Mobile", description="Object moves during gameplay", default=False, override={'LIBRARY_OVERRIDABLE'}) - bpy.types.Object.lnx_visible = BoolProperty(name="Visible", description="Render this object", default=True, override={'LIBRARY_OVERRIDABLE'}) - bpy.types.Object.lnx_visible_shadow = BoolProperty(name="Lighting", description="Object contributes to the lighting even if invisible", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_export = compatible_prop(BoolProperty, name="Export", description="Export object data", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_sorting_index = compatible_prop(IntProperty, name="Sorting Index", description="Sorting index for the Render's Draw Order", default=0, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_spawn = compatible_prop(BoolProperty, name="Spawn", description="Auto-add this object when creating scene", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_mobile = compatible_prop(BoolProperty, name="Mobile", description="Object moves during gameplay", default=False, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_visible = compatible_prop(BoolProperty, name="Visible", description="Render this object", default=True, override={'LIBRARY_OVERRIDABLE'}) + bpy.types.Object.lnx_visible_shadow = compatible_prop(BoolProperty, name="Lighting", description="Object contributes to the lighting even if invisible", default=True, override={'LIBRARY_OVERRIDABLE'}) bpy.types.Object.lnx_soft_body_margin = FloatProperty(name="Soft Body Margin", description="Collision margin", default=0.04) bpy.types.Object.lnx_rb_linear_factor = FloatVectorProperty(name="Linear Factor", size=3, description="Set to 0 to lock axis", default=[1,1,1]) bpy.types.Object.lnx_rb_angular_factor = FloatVectorProperty(name="Angular Factor", size=3, description="Set to 0 to lock axis", default=[1,1,1]) diff --git a/leenkx/blender/lnx/props_exporter.py b/leenkx/blender/lnx/props_exporter.py index e0c7766..ccb2110 100644 --- a/leenkx/blender/lnx/props_exporter.py +++ b/leenkx/blender/lnx/props_exporter.py @@ -420,16 +420,19 @@ class LNX_OT_ExporterOpenVS(bpy.types.Operator): @classmethod def poll(cls, context): if not lnx.utils.get_os_is_windows(): - cls.poll_message_set('This operator is only supported on Windows') + if bpy.app.version >= (2, 90, 0): + cls.poll_message_set('This operator is only supported on Windows') return False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_exporterlist) == 0: - cls.poll_message_set('No export configuration exists') + if bpy.app.version >= (2, 90, 0): + cls.poll_message_set('No export configuration exists') return False if wrd.lnx_exporterlist[wrd.lnx_exporterlist_index].lnx_project_target != 'windows-hl': - cls.poll_message_set('This operator only works with the Windows (C) target') + if bpy.app.version >= (2, 90, 0): + cls.poll_message_set('This operator only works with the Windows (C) target') return False return True diff --git a/leenkx/blender/lnx/props_traits.py b/leenkx/blender/lnx/props_traits.py index 48fafe2..b28d8e1 100644 --- a/leenkx/blender/lnx/props_traits.py +++ b/leenkx/blender/lnx/props_traits.py @@ -12,9 +12,18 @@ import bpy.utils.previews import lnx.make as make from lnx.props_traits_props import * import lnx.ui_icons as ui_icons + +def compatible_prop(property_func, **kwargs): + """Create properties compatible with different Blender versions.""" + if bpy.app.version < (2, 90, 0): + # Remove override parameter for Blender 2.83 + kwargs.pop('override', None) + return property_func(**kwargs) + import lnx.utils import lnx.write_data as write_data + if lnx.is_reload(__name__): lnx.make = lnx.reload_module(lnx.make) lnx.props_traits_props = lnx.reload_module(lnx.props_traits_props) @@ -91,20 +100,20 @@ class LnxTraitListItem(bpy.types.PropertyGroup): """Ensure that only logic node trees show up as node traits""" return tree.bl_idname == 'LnxLogicTreeType' - name: StringProperty(name="Name", description="The name of the trait", default="", override={"LIBRARY_OVERRIDABLE"}) - enabled_prop: BoolProperty(name="", description="Whether this trait is enabled", default=True, update=trigger_recompile, override={"LIBRARY_OVERRIDABLE"}) - is_object: BoolProperty(name="", default=True) - fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False, override={"LIBRARY_OVERRIDABLE"}) - type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) + name = compatible_prop(StringProperty, name="Name", description="The name of the trait", default="", override={"LIBRARY_OVERRIDABLE"}) + enabled_prop = compatible_prop(BoolProperty, name="", description="Whether this trait is enabled", default=True, update=trigger_recompile, override={"LIBRARY_OVERRIDABLE"}) + is_object = BoolProperty(name="", default=True) + fake_user = compatible_prop(BoolProperty, name="Fake User", description="Export this trait even if it is deactivated", default=False, override={"LIBRARY_OVERRIDABLE"}) + type_prop = EnumProperty(name="Type", items=PROP_TYPES_ENUM) - class_name_prop: StringProperty(name="Class", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) - canvas_name_prop: StringProperty(name="Canvas", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) - webassembly_prop: StringProperty(name="Module", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) - node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}, poll=poll_node_trees) + class_name_prop = compatible_prop(StringProperty, name="Class", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + canvas_name_prop = compatible_prop(StringProperty, name="Canvas", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + webassembly_prop = compatible_prop(StringProperty, name="Module", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + node_tree_prop = compatible_prop(PointerProperty, type=NodeTree, update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}, poll=poll_node_trees) - lnx_traitpropslist: CollectionProperty(type=LnxTraitPropListItem) - lnx_traitpropslist_index: IntProperty(name="Index for my_list", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) - lnx_traitpropswarnings: CollectionProperty(type=LnxTraitPropWarning) + lnx_traitpropslist = CollectionProperty(type=LnxTraitPropListItem) + lnx_traitpropslist_index = compatible_prop(IntProperty, name="Index for my_list", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) + lnx_traitpropswarnings = CollectionProperty(type=LnxTraitPropWarning) class LNX_UL_TraitList(bpy.types.UIList): """List of traits.""" @@ -475,10 +484,12 @@ class LeenkxGenerateNavmeshButton(bpy.types.Operator): # If not, append vertex traversed_indices.append(vertex_index) vertex = export_mesh.vertices[vertex_index].co - # Apply world transform and maintain coordinate system + # Apply world transform tv = world_matrix @ vertex - # Write to OBJ without flipping coordinates - f.write("v %.4f %.4f %.4f\n" % (tv[0], tv[1], tv[2])) + # Write to OBJ + f.write("v %.4f " % (tv[0])) + f.write("%.4f " % (tv[2])) + f.write("%.4f\n" % (tv[1])) # Flipped # Max index of this object max_index = 0 @@ -522,10 +533,8 @@ class LeenkxGenerateNavmeshButton(bpy.types.Operator): # NavMesh preview settings, cleanup navmesh.name = nav_mesh_name - # Match the original object's transform - navmesh.location = obj.location - navmesh.rotation_euler = obj.rotation_euler - navmesh.scale = (1, 1, 1) # Reset scale to avoid distortion + navmesh.rotation_euler = (0, 0, 0) + navmesh.location = (0, 0, 0) navmesh.lnx_export = False bpy.context.view_layer.objects.active = navmesh @@ -756,7 +765,8 @@ class LnxRefreshObjectScriptsButton(bpy.types.Operator): @classmethod def poll(cls, context): - cls.poll_message_set(LnxRefreshScriptsButton.poll_msg) + if bpy.app.version >= (2, 90, 0): + cls.poll_message_set(LnxRefreshScriptsButton.poll_msg) # Technically we could keep the operator enabled here since # fetch_trait_props() checks for overrides and the operator does # not depend on the current object, but this way the user @@ -1064,11 +1074,10 @@ __REG_CLASSES = ( ) __reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) - def register(): __reg_classes() - bpy.types.Object.lnx_traitlist = CollectionProperty(type=LnxTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) - bpy.types.Object.lnx_traitlist_index = IntProperty(name="Index for lnx_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) - bpy.types.Scene.lnx_traitlist = CollectionProperty(type=LnxTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) - bpy.types.Scene.lnx_traitlist_index = IntProperty(name="Index for lnx_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) + bpy.types.Object.lnx_traitlist = compatible_prop(CollectionProperty, type=LnxTraitListItem) + bpy.types.Object.lnx_traitlist_index = compatible_prop(IntProperty, name="Index for lnx_traitlist", default=0) + bpy.types.Scene.lnx_traitlist = compatible_prop(CollectionProperty, type=LnxTraitListItem) + bpy.types.Scene.lnx_traitlist_index = compatible_prop(IntProperty, name="Index for lnx_traitlist", default=0) diff --git a/leenkx/blender/lnx/props_traits_props.py b/leenkx/blender/lnx/props_traits_props.py index 4a67507..8761095 100644 --- a/leenkx/blender/lnx/props_traits_props.py +++ b/leenkx/blender/lnx/props_traits_props.py @@ -3,6 +3,14 @@ from bpy.props import * __all__ = ['LnxTraitPropWarning', 'LnxTraitPropListItem', 'LNX_UL_PropList'] +# Helper function to handle version compatibility +def compatible_prop(prop_func, **kwargs): + """Create properties compatible with both Blender 2.83 and newer versions.""" + if bpy.app.version < (2, 90, 0): + # Remove override parameter for Blender 2.83 + kwargs.pop('override', None) + return prop_func(**kwargs) + PROP_TYPE_ICONS = { "String": "SORTALPHA", "Int": "CHECKBOX_DEHLT", @@ -35,18 +43,19 @@ def filter_objects(item, b_object): class LnxTraitPropWarning(bpy.types.PropertyGroup): - propName: StringProperty(name="Property Name") - warning: StringProperty(name="Warning") + propName = compatible_prop(StringProperty, name="Property Name", override={"LIBRARY_OVERRIDABLE"}) + warning = compatible_prop(StringProperty, name="Warning", override={"LIBRARY_OVERRIDABLE"}) class LnxTraitPropListItem(bpy.types.PropertyGroup): """Group of properties representing an item in the list.""" - name: StringProperty( + name = compatible_prop(StringProperty, name="Name", description="The name of this property", - default="Untitled") + default="Untitled", + override={"LIBRARY_OVERRIDABLE"}) - type: EnumProperty( + type = compatible_prop(EnumProperty, items=( # (Haxe Type, Display Name, Description) ("String", "String", "String Type"), @@ -69,18 +78,18 @@ class LnxTraitPropListItem(bpy.types.PropertyGroup): ) # === VALUES === - value_string: StringProperty(name="Value", default="", override={"LIBRARY_OVERRIDABLE"}) - value_int: IntProperty(name="Value", default=0, override={"LIBRARY_OVERRIDABLE"}) - value_float: FloatProperty(name="Value", default=0.0, override={"LIBRARY_OVERRIDABLE"}) - value_bool: BoolProperty(name="Value", default=False, override={"LIBRARY_OVERRIDABLE"}) - value_vec2: FloatVectorProperty(name="Value", size=2, override={"LIBRARY_OVERRIDABLE"}) - value_vec3: FloatVectorProperty(name="Value", size=3, override={"LIBRARY_OVERRIDABLE"}) - value_vec4: FloatVectorProperty(name="Value", size=4, override={"LIBRARY_OVERRIDABLE"}) - value_object: PointerProperty( + value_string = compatible_prop(StringProperty, name="Value", default="", override={"LIBRARY_OVERRIDABLE"}) + value_int = compatible_prop(IntProperty, name="Value", default=0, override={"LIBRARY_OVERRIDABLE"}) + value_float = compatible_prop(FloatProperty, name="Value", default=0.0, override={"LIBRARY_OVERRIDABLE"}) + value_bool = compatible_prop(BoolProperty, name="Value", default=False, override={"LIBRARY_OVERRIDABLE"}) + value_vec2 = compatible_prop(FloatVectorProperty, name="Value", size=2, override={"LIBRARY_OVERRIDABLE"}) + value_vec3 = compatible_prop(FloatVectorProperty, name="Value", size=3, override={"LIBRARY_OVERRIDABLE"}) + value_vec4 = compatible_prop(FloatVectorProperty, name="Value", size=4, override={"LIBRARY_OVERRIDABLE"}) + value_object = compatible_prop(PointerProperty, name="Value", type=bpy.types.Object, poll=filter_objects, override={"LIBRARY_OVERRIDABLE"} ) - value_scene: PointerProperty(name="Value", type=bpy.types.Scene, override={"LIBRARY_OVERRIDABLE"}) + value_scene = compatible_prop(PointerProperty, name="Value", type=bpy.types.Scene, override={"LIBRARY_OVERRIDABLE"}) def set_value(self, val): # Would require way too much effort, so it's out of scope here. diff --git a/leenkx/blender/lnx/props_ui.py b/leenkx/blender/lnx/props_ui.py index b72f1c1..d3d6bd0 100644 --- a/leenkx/blender/lnx/props_ui.py +++ b/leenkx/blender/lnx/props_ui.py @@ -8,6 +8,24 @@ import mathutils import bpy from bpy.props import * +# Helper functions for Blender version compatibility +def get_panel_options(): + """Get panel options compatible with current Blender version.""" + if bpy.app.version >= (2, 93, 0): # INSTANCED was introduced around 2.93 + return {'INSTANCED'} + else: + return set() # Empty set for older versions + +def column_with_heading(layout, heading='', align=False): + """Create a column with optional heading, compatible across Blender versions.""" + if bpy.app.version >= (2, 92, 0): + return layout.column(heading=heading, align=align) + else: + col = layout.column(align=align) + if heading: + col.label(text=heading) + return col + from lnx.lightmapper.panels import scene import lnx.api @@ -939,13 +957,13 @@ class LNX_PT_LeenkxExporterPanel(bpy.types.Panel): col = layout.column() col.prop(wrd, 'lnx_project_icon') - col = layout.column(heading='Code Output', align=True) + col = column_with_heading(layout, 'Code Output', align=True) col.prop(wrd, 'lnx_dce') col.prop(wrd, 'lnx_compiler_inline') col.prop(wrd, 'lnx_minify_js') col.prop(wrd, 'lnx_no_traces') - col = layout.column(heading='Data', align=True) + col = column_with_heading(layout, 'Data', align=True) col.prop(wrd, 'lnx_minimize') col.prop(wrd, 'lnx_optimize_data') col.prop(wrd, 'lnx_asset_compression') @@ -1178,32 +1196,32 @@ class LNX_PT_ProjectFlagsPanel(bpy.types.Panel): layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] - col = layout.column(heading='Debug', align=True) + col = column_with_heading(layout, 'Debug', align=True) col.prop(wrd, 'lnx_verbose_output') col.prop(wrd, 'lnx_cache_build') col.prop(wrd, 'lnx_clear_on_compile') col.prop(wrd, 'lnx_assert_level') col.prop(wrd, 'lnx_assert_quit') - col = layout.column(heading='Runtime', align=True) + col = column_with_heading(layout, 'Runtime', align=True) col.prop(wrd, 'lnx_live_patch') col.prop(wrd, 'lnx_stream_scene') col.prop(wrd, 'lnx_loadscreen') col.prop(wrd, 'lnx_write_config') - col = layout.column(heading='Renderer', align=True) + col = column_with_heading(layout, 'Renderer', align=True) col.prop(wrd, 'lnx_batch_meshes') col.prop(wrd, 'lnx_batch_materials') col.prop(wrd, 'lnx_deinterleaved_buffers') col.prop(wrd, 'lnx_export_tangents') - col = layout.column(heading='Quality') + col = column_with_heading(layout, 'Quality') row = col.row() # To expand below property UI horizontally row.prop(wrd, 'lnx_canvas_img_scaling_quality', expand=True) col.prop(wrd, 'lnx_texture_quality') col.prop(wrd, 'lnx_sound_quality') - col = layout.column(heading='External Assets') + col = column_with_heading(layout, 'External Assets') col.prop(wrd, 'lnx_copy_override') col.operator('lnx.copy_to_bundled', icon='IMAGE_DATA') @@ -1517,7 +1535,7 @@ class LNX_PT_TopbarPanel(bpy.types.Panel): bl_label = "Leenkx Player" bl_space_type = "VIEW_3D" bl_region_type = "WINDOW" - bl_options = {'INSTANCED'} + bl_options = get_panel_options() def draw_header(self, context): row = self.layout.row(align=True) @@ -2921,7 +2939,7 @@ def draw_conditional_prop(layout: bpy.types.UILayout, heading: str, data: bpy.ty """Draws a property row with a checkbox that enables a value field. The function fails when prop_condition is not a boolean property. """ - col = layout.column(heading=heading) + col = column_with_heading(layout, heading) row = col.row() row.prop(data, prop_condition, text='') sub = row.row() diff --git a/leenkx/blender/lnx/utils.py b/leenkx/blender/lnx/utils.py index 2d9ca78..09f7e98 100644 --- a/leenkx/blender/lnx/utils.py +++ b/leenkx/blender/lnx/utils.py @@ -96,7 +96,7 @@ def convert_image(image, path, file_format='JPEG'): ren.image_settings.color_mode = orig_color_mode -def get_random_color_rgb() -> list[float]: +def get_random_color_rgb() -> List[float]: """Return a random RGB color with values in range [0, 1].""" return [random.random(), random.random(), random.random()] @@ -1162,7 +1162,7 @@ def get_link_web_server(): return '' if not hasattr(addon_prefs, 'link_web_server') else addon_prefs.link_web_server -def get_file_lnx_version_tuple() -> tuple[int]: +def get_file_lnx_version_tuple() -> Tuple[int, ...]: wrd = bpy.data.worlds['Lnx'] return tuple(map(int, wrd.lnx_version.split('.'))) @@ -1218,9 +1218,9 @@ def cpu_count(*, physical_only=False) -> Optional[int]: return int(subprocess.check_output(command)) except subprocess.CalledProcessError as e: - err_reason = f'Reason: command {command} exited with code {e.returncode}.' + err_reason = 'Reason: command {} exited with code {}.'.format(command, e.returncode) except FileNotFoundError as e: - err_reason = f'Reason: couldn\'t open file from command {command} ({e.errno=}).' + err_reason = 'Reason: couldn\'t open file from command {} (errno={}).'.format(command, e.errno) # Last resort even though it can be wrong log.warn("Could not retrieve count of physical CPUs, using logical CPU count instead.\n\t" + err_reason) diff --git a/leenkx/blender/lnx/utils_vs.py b/leenkx/blender/lnx/utils_vs.py index 2e23246..2c4ba82 100644 --- a/leenkx/blender/lnx/utils_vs.py +++ b/leenkx/blender/lnx/utils_vs.py @@ -5,7 +5,7 @@ import json import os import re import subprocess -from typing import Any, Optional, Callable +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import bpy @@ -56,7 +56,7 @@ def is_version_installed(version_major: str) -> bool: return any(v['version_major'] == version_major for v in _installed_versions) -def get_installed_version(version_major: str, re_fetch=False) -> Optional[dict[str, str]]: +def get_installed_version(version_major: str, re_fetch=False) -> Optional[Dict[str, str]]: for installed_version in _installed_versions: if installed_version['version_major'] == version_major: return installed_version @@ -71,7 +71,7 @@ def get_installed_version(version_major: str, re_fetch=False) -> Optional[dict[s return None -def get_supported_version(version_major: str) -> Optional[dict[str, str]]: +def get_supported_version(version_major: str) -> Optional[Dict[str, str]]: for version in supported_versions: if version[0] == version_major: return { @@ -100,7 +100,7 @@ def fetch_installed_vs(silent=False) -> bool: if not silent: log.warn( f'Found a Visual Studio installation with incomplete information, skipping\n' - f' ({name=}, {versions=}, {path=})' + f' (name={name if name is not None else "None"}, versions={versions}, path={path if path is not None else "None"})' ) continue @@ -212,14 +212,14 @@ def compile_in_vs(version_major: str, done: Callable[[], None]) -> bool: return True -def _vswhere_get_display_name(instance_data: dict[str, Any]) -> Optional[str]: +def _vswhere_get_display_name(instance_data: Dict[str, Any]) -> Optional[str]: name_raw = instance_data.get('displayName', None) if name_raw is None: return None return lnx.utils.safestr(name_raw).replace('_', ' ').strip() -def _vswhere_get_version(instance_data: dict[str, Any]) -> Optional[tuple[str, str, tuple[int, ...]]]: +def _vswhere_get_version(instance_data: Dict[str, Any]) -> Optional[Tuple[str, str, Tuple[int, int, int, int]]]: version_raw = instance_data.get('installationVersion', None) if version_raw is None: return None @@ -230,11 +230,11 @@ def _vswhere_get_version(instance_data: dict[str, Any]) -> Optional[tuple[str, s return version_major, version_full, version_full_ints -def _vswhere_get_path(instance_data: dict[str, Any]) -> Optional[str]: +def _vswhere_get_path(instance_data: Dict[str, Any]) -> Optional[str]: return instance_data.get('installationPath', None) -def _vswhere_get_instances(silent=False) -> Optional[list[dict[str, Any]]]: +def _vswhere_get_instances(silent: bool = False) -> Optional[List[Dict[str, Any]]]: # vswhere.exe only exists at that location since VS2017 v15.2, for # earlier versions we'd need to package vswhere with Leenkx exe_path = os.path.join(os.environ["ProgramFiles(x86)"], 'Microsoft Visual Studio', 'Installer', 'vswhere.exe') @@ -256,7 +256,7 @@ def _vswhere_get_instances(silent=False) -> Optional[list[dict[str, Any]]]: return result -def version_full_to_ints(version_full: str) -> tuple[int, ...]: +def version_full_to_ints(version_full: str) -> Tuple[int, ...]: return tuple(int(i) for i in version_full.split('.')) @@ -281,7 +281,7 @@ def get_vcxproj_path() -> str: return os.path.join(project_path, project_name + '.vcxproj') -def fetch_project_version() -> tuple[Optional[str], Optional[str], Optional[str]]: +def fetch_project_version() -> Tuple[Optional[str], Optional[str], Optional[str]]: version_major = None version_min_full = None From 1299306e09c1cf33385e2bbc9dbc30c6b75dfb21 Mon Sep 17 00:00:00 2001 From: Onek8 Date: Sun, 28 Sep 2025 20:01:00 +0000 Subject: [PATCH 61/63] Update leenkx.py --- leenkx.py | 48 ++++++------------------------------------------ 1 file changed, 6 insertions(+), 42 deletions(-) diff --git a/leenkx.py b/leenkx.py index ed07a18..e8a7d38 100644 --- a/leenkx.py +++ b/leenkx.py @@ -64,45 +64,8 @@ def get_os(): else: return 'linux' -def detect_sdk_path(): - """Auto-detect the SDK path after Leenkx installation.""" - preferences = bpy.context.preferences - addon_prefs = preferences.addons["leenkx"].preferences - - # Don't overwrite if already set - if addon_prefs.sdk_path: - return - - # For all versions, try to get the path from the current file location first - current_file = os.path.realpath(__file__) - if os.path.exists(current_file): - # Go up one level from the current file's directory to get the SDK root - sdk_path = os.path.dirname(os.path.dirname(current_file)) - if os.path.exists(os.path.join(sdk_path, "leenkx")): - addon_prefs.sdk_path = sdk_path - return - - # Fallback for Blender 2.92+ with the original method - if bpy.app.version >= (2, 92, 0): - try: - win = bpy.context.window_manager.windows[0] - area = win.screen.areas[0] - area_type = area.type - area.type = "INFO" - - with bpy.context.temp_override(window=win, screen=win.screen, area=area): - bpy.ops.info.select_all(action='SELECT') - bpy.ops.info.report_copy() - - clipboard = bpy.context.window_manager.clipboard - match = re.findall(r"^Modules Installed .* from '(.*leenkx.py)' into", - clipboard, re.MULTILINE) - if match: - addon_prefs.sdk_path = os.path.dirname(match[-1]) - finally: - area.type = area_type -def detect_sdk_path22(): +def detect_sdk_path(): """Auto-detect the SDK path after Leenkx installation.""" # Do not overwrite the SDK path (this method gets # called after each registration, not after @@ -116,10 +79,10 @@ def detect_sdk_path22(): area = win.screen.areas[0] area_type = area.type area.type = "INFO" - - with bpy.context.temp_override(window=win, screen=win.screen, area=area): - bpy.ops.info.select_all(action='SELECT') - bpy.ops.info.report_copy() + if bpy.app.version >= (2, 92, 0): + with bpy.context.temp_override(window=win, screen=win.screen, area=area): + bpy.ops.info.select_all(action='SELECT') + bpy.ops.info.report_copy() area.type = area_type clipboard = bpy.context.window_manager.clipboard @@ -129,6 +92,7 @@ def detect_sdk_path22(): if match: addon_prefs.sdk_path = os.path.dirname(match[-1]) + def get_link_web_server(self): return self.get('link_web_server', 'http://localhost/') From c24baa3364f144d1965f1e92b21d2fd1b06f20d0 Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Mon, 29 Sep 2025 05:27:43 +0000 Subject: [PATCH 62/63] Update leenkx/blender/lnx/props_traits_props.py --- leenkx/blender/lnx/props_traits_props.py | 111 +++++++++++++---------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/leenkx/blender/lnx/props_traits_props.py b/leenkx/blender/lnx/props_traits_props.py index 8761095..61c40c3 100644 --- a/leenkx/blender/lnx/props_traits_props.py +++ b/leenkx/blender/lnx/props_traits_props.py @@ -3,13 +3,6 @@ from bpy.props import * __all__ = ['LnxTraitPropWarning', 'LnxTraitPropListItem', 'LNX_UL_PropList'] -# Helper function to handle version compatibility -def compatible_prop(prop_func, **kwargs): - """Create properties compatible with both Blender 2.83 and newer versions.""" - if bpy.app.version < (2, 90, 0): - # Remove override parameter for Blender 2.83 - kwargs.pop('override', None) - return prop_func(**kwargs) PROP_TYPE_ICONS = { "String": "SORTALPHA", @@ -43,53 +36,75 @@ def filter_objects(item, b_object): class LnxTraitPropWarning(bpy.types.PropertyGroup): - propName = compatible_prop(StringProperty, name="Property Name", override={"LIBRARY_OVERRIDABLE"}) - warning = compatible_prop(StringProperty, name="Warning", override={"LIBRARY_OVERRIDABLE"}) + propName: StringProperty(name="Property Name") + warning: StringProperty(name="Warning") class LnxTraitPropListItem(bpy.types.PropertyGroup): """Group of properties representing an item in the list.""" - name = compatible_prop(StringProperty, + name: StringProperty( name="Name", description="The name of this property", - default="Untitled", - override={"LIBRARY_OVERRIDABLE"}) - - type = compatible_prop(EnumProperty, - items=( - # (Haxe Type, Display Name, Description) - ("String", "String", "String Type"), - ("Int", "Integer", "Integer Type"), - ("Float", "Float", "Float Type"), - ("Bool", "Boolean", "Boolean Type"), - ("Vec2", "Vec2", "2D Vector Type"), - ("Vec3", "Vec3", "3D Vector Type"), - ("Vec4", "Vec4", "4D Vector Type"), - ("Object", "Object", "Object Type"), - ("CameraObject", "Camera Object", "Camera Object Type"), - ("LightObject", "Light Object", "Light Object Type"), - ("MeshObject", "Mesh Object", "Mesh Object Type"), - ("SpeakerObject", "Speaker Object", "Speaker Object Type"), - ("TSceneFormat", "Scene", "Scene Type")), - name="Type", - description="The type of this property", - default="String", - override={"LIBRARY_OVERRIDABLE"} - ) - - # === VALUES === - value_string = compatible_prop(StringProperty, name="Value", default="", override={"LIBRARY_OVERRIDABLE"}) - value_int = compatible_prop(IntProperty, name="Value", default=0, override={"LIBRARY_OVERRIDABLE"}) - value_float = compatible_prop(FloatProperty, name="Value", default=0.0, override={"LIBRARY_OVERRIDABLE"}) - value_bool = compatible_prop(BoolProperty, name="Value", default=False, override={"LIBRARY_OVERRIDABLE"}) - value_vec2 = compatible_prop(FloatVectorProperty, name="Value", size=2, override={"LIBRARY_OVERRIDABLE"}) - value_vec3 = compatible_prop(FloatVectorProperty, name="Value", size=3, override={"LIBRARY_OVERRIDABLE"}) - value_vec4 = compatible_prop(FloatVectorProperty, name="Value", size=4, override={"LIBRARY_OVERRIDABLE"}) - value_object = compatible_prop(PointerProperty, - name="Value", type=bpy.types.Object, poll=filter_objects, - override={"LIBRARY_OVERRIDABLE"} - ) - value_scene = compatible_prop(PointerProperty, name="Value", type=bpy.types.Scene, override={"LIBRARY_OVERRIDABLE"}) + default="Untitled") + if bpy.app.version < (2, 90, 0): + type: EnumProperty( + items=( + # (Haxe Type, Display Name, Description) + ("String", "String", "String Type"), + ("Int", "Integer", "Integer Type"), + ("Float", "Float", "Float Type"), + ("Bool", "Boolean", "Boolean Type"), + ("Vec2", "Vec2", "2D Vector Type"), + ("Vec3", "Vec3", "3D Vector Type"), + ("Vec4", "Vec4", "4D Vector Type"), + ("Object", "Object", "Object Type"), + ("CameraObject", "Camera Object", "Camera Object Type"), + ("LightObject", "Light Object", "Light Object Type"), + ("MeshObject", "Mesh Object", "Mesh Object Type"), + ("SpeakerObject", "Speaker Object", "Speaker Object Type"), + ("TSceneFormat", "Scene", "Scene Type")), + name="Type", + description="The type of this property", + default="String") + value_string: StringProperty(name="Value", default="") + value_int: IntProperty(name="Value", default=0) + value_float: FloatProperty(name="Value", default=0.0) + value_bool: BoolProperty(name="Value", default=False) + value_vec2: FloatVectorProperty(name="Value", size=2) + value_vec3: FloatVectorProperty(name="Value", size=3) + value_vec4: FloatVectorProperty(name="Value", size=4) + value_object: PointerProperty(name="Value", type=bpy.types.Object, poll=filter_objects) + value_scene: PointerProperty(name="Value", type=bpy.types.Scene) + else: + type: EnumProperty( + items=( + # (Haxe Type, Display Name, Description) + ("String", "String", "String Type"), + ("Int", "Integer", "Integer Type"), + ("Float", "Float", "Float Type"), + ("Bool", "Boolean", "Boolean Type"), + ("Vec2", "Vec2", "2D Vector Type"), + ("Vec3", "Vec3", "3D Vector Type"), + ("Vec4", "Vec4", "4D Vector Type"), + ("Object", "Object", "Object Type"), + ("CameraObject", "Camera Object", "Camera Object Type"), + ("LightObject", "Light Object", "Light Object Type"), + ("MeshObject", "Mesh Object", "Mesh Object Type"), + ("SpeakerObject", "Speaker Object", "Speaker Object Type"), + ("TSceneFormat", "Scene", "Scene Type")), + name="Type", + description="The type of this property", + default="String", + override={"LIBRARY_OVERRIDABLE"}) + value_string: StringProperty(name="Value", default="", override={"LIBRARY_OVERRIDABLE"}) + value_int: IntProperty(name="Value", default=0, override={"LIBRARY_OVERRIDABLE"}) + value_float: FloatProperty(name="Value", default=0.0, override={"LIBRARY_OVERRIDABLE"}) + value_bool: BoolProperty(name="Value", default=False, override={"LIBRARY_OVERRIDABLE"}) + value_vec2: FloatVectorProperty(name="Value", size=2, override={"LIBRARY_OVERRIDABLE"}) + value_vec3: FloatVectorProperty(name="Value", size=3, override={"LIBRARY_OVERRIDABLE"}) + value_vec4: FloatVectorProperty(name="Value", size=4, override={"LIBRARY_OVERRIDABLE"}) + value_object: PointerProperty(name="Value", type=bpy.types.Object, poll=filter_objects, override={"LIBRARY_OVERRIDABLE"}) + value_scene: PointerProperty(name="Value", type=bpy.types.Scene, override={"LIBRARY_OVERRIDABLE"}) def set_value(self, val): # Would require way too much effort, so it's out of scope here. From 73fcb55acce782de71737a521b6228a9116913a9 Mon Sep 17 00:00:00 2001 From: LeenkxTeam Date: Mon, 29 Sep 2025 05:28:13 +0000 Subject: [PATCH 63/63] Update leenkx/blender/lnx/props_traits.py --- leenkx/blender/lnx/props_traits.py | 57 ++++++++++++++++++------------ 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/leenkx/blender/lnx/props_traits.py b/leenkx/blender/lnx/props_traits.py index b28d8e1..b7bf79c 100644 --- a/leenkx/blender/lnx/props_traits.py +++ b/leenkx/blender/lnx/props_traits.py @@ -13,13 +13,6 @@ import lnx.make as make from lnx.props_traits_props import * import lnx.ui_icons as ui_icons -def compatible_prop(property_func, **kwargs): - """Create properties compatible with different Blender versions.""" - if bpy.app.version < (2, 90, 0): - # Remove override parameter for Blender 2.83 - kwargs.pop('override', None) - return property_func(**kwargs) - import lnx.utils import lnx.write_data as write_data @@ -99,21 +92,32 @@ class LnxTraitListItem(bpy.types.PropertyGroup): def poll_node_trees(self, tree: NodeTree): """Ensure that only logic node trees show up as node traits""" return tree.bl_idname == 'LnxLogicTreeType' + + if bpy.app.version < (2, 90, 0): + name: StringProperty(name="Name", description="The name of the trait", default="") + enabled_prop: BoolProperty(name="", description="Whether this trait is enabled", default=True, update=trigger_recompile) + fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False) + class_name_prop: StringProperty(name="Class", description="A name for this item", default="", update=update_trait_group) + canvas_name_prop: StringProperty(name="Canvas", description="A name for this item", default="", update=update_trait_group) + webassembly_prop: StringProperty(name="Module", description="A name for this item", default="", update=update_trait_group) + node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group, poll=poll_node_trees) + lnx_traitpropslist_index: IntProperty(name="Index for my_list", default=0, options={"LIBRARY_EDITABLE"}) + else: + name: StringProperty(name="Name", description="The name of the trait", default="", override={"LIBRARY_OVERRIDABLE"}) + enabled_prop: BoolProperty(name="", description="Whether this trait is enabled", default=True, update=trigger_recompile, override={"LIBRARY_OVERRIDABLE"}) + fake_user: BoolProperty(name="Fake User", description="Export this trait even if it is deactivated", default=False, override={"LIBRARY_OVERRIDABLE"}) + class_name_prop: StringProperty(name="Class", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + canvas_name_prop: StringProperty(name="Canvas", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + webassembly_prop: StringProperty(name="Module", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) + node_tree_prop: PointerProperty(type=NodeTree, update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}, poll=poll_node_trees) + lnx_traitpropslist_index: IntProperty(name="Index for my_list", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) - name = compatible_prop(StringProperty, name="Name", description="The name of the trait", default="", override={"LIBRARY_OVERRIDABLE"}) - enabled_prop = compatible_prop(BoolProperty, name="", description="Whether this trait is enabled", default=True, update=trigger_recompile, override={"LIBRARY_OVERRIDABLE"}) - is_object = BoolProperty(name="", default=True) - fake_user = compatible_prop(BoolProperty, name="Fake User", description="Export this trait even if it is deactivated", default=False, override={"LIBRARY_OVERRIDABLE"}) - type_prop = EnumProperty(name="Type", items=PROP_TYPES_ENUM) + is_object: BoolProperty(name="", default=True) + type_prop: EnumProperty(name="Type", items=PROP_TYPES_ENUM) - class_name_prop = compatible_prop(StringProperty, name="Class", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) - canvas_name_prop = compatible_prop(StringProperty, name="Canvas", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) - webassembly_prop = compatible_prop(StringProperty, name="Module", description="A name for this item", default="", update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}) - node_tree_prop = compatible_prop(PointerProperty, type=NodeTree, update=update_trait_group, override={"LIBRARY_OVERRIDABLE"}, poll=poll_node_trees) - lnx_traitpropslist = CollectionProperty(type=LnxTraitPropListItem) - lnx_traitpropslist_index = compatible_prop(IntProperty, name="Index for my_list", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) - lnx_traitpropswarnings = CollectionProperty(type=LnxTraitPropWarning) + lnx_traitpropslist: CollectionProperty(type=LnxTraitPropListItem) + lnx_traitpropswarnings: CollectionProperty(type=LnxTraitPropWarning) class LNX_UL_TraitList(bpy.types.UIList): """List of traits.""" @@ -1077,7 +1081,14 @@ __reg_classes, unregister = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): __reg_classes() - bpy.types.Object.lnx_traitlist = compatible_prop(CollectionProperty, type=LnxTraitListItem) - bpy.types.Object.lnx_traitlist_index = compatible_prop(IntProperty, name="Index for lnx_traitlist", default=0) - bpy.types.Scene.lnx_traitlist = compatible_prop(CollectionProperty, type=LnxTraitListItem) - bpy.types.Scene.lnx_traitlist_index = compatible_prop(IntProperty, name="Index for lnx_traitlist", default=0) + if bpy.app.version < (2, 90, 0): + bpy.types.Object.lnx_traitlist = CollectionProperty(type=LnxTraitListItem) + bpy.types.Object.lnx_traitlist_index = IntProperty(name="Index for lnx_traitlist", default=0, options={"LIBRARY_EDITABLE"}) + bpy.types.Scene.lnx_traitlist = CollectionProperty(type=LnxTraitListItem) + bpy.types.Scene.lnx_traitlist_index = IntProperty(name="Index for lnx_traitlist", default=0, options={"LIBRARY_EDITABLE"}) + else: + bpy.types.Object.lnx_traitlist = CollectionProperty(type=LnxTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) + bpy.types.Object.lnx_traitlist_index = IntProperty(name="Index for lnx_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) + bpy.types.Scene.lnx_traitlist = CollectionProperty(type=LnxTraitListItem, override={"LIBRARY_OVERRIDABLE", "USE_INSERTION"}) + bpy.types.Scene.lnx_traitlist_index = IntProperty(name="Index for lnx_traitlist", default=0, options={"LIBRARY_EDITABLE"}, override={"LIBRARY_OVERRIDABLE"}) + \ No newline at end of file