merge upstream

This commit is contained in:
Onek8 2025-05-20 20:00:31 +00:00
commit 316441b954
33 changed files with 1291 additions and 1086 deletions

View File

@ -80,6 +80,7 @@ extern class Krom {
static function unloadImage(image: kha.Image): Void; static function unloadImage(image: kha.Image): Void;
static function loadSound(file: String): Dynamic; static function loadSound(file: String): Dynamic;
static function writeAudioBuffer(buffer: js.lib.ArrayBuffer, samples: Int): Void; static function writeAudioBuffer(buffer: js.lib.ArrayBuffer, samples: Int): Void;
static function getSamplesPerSecond(): Int;
static function loadBlob(file: String): js.lib.ArrayBuffer; static function loadBlob(file: String): js.lib.ArrayBuffer;
static function init(title: String, width: Int, height: Int, samplesPerPixel: Int, vSync: Bool, windowMode: Int, windowFeatures: Int, kromApi: Int): Void; static function init(title: String, width: Int, height: Int, samplesPerPixel: Int, vSync: Bool, windowMode: Int, windowFeatures: Int, kromApi: Int): Void;
@ -115,6 +116,7 @@ extern class Krom {
static function screenDpi(): Int; static function screenDpi(): Int;
static function systemId(): String; static function systemId(): String;
static function requestShutdown(): Void; static function requestShutdown(): Void;
static function displayFrequency(): Int;
static function displayCount(): Int; static function displayCount(): Int;
static function displayWidth(index: Int): Int; static function displayWidth(index: Int): Int;
static function displayHeight(index: Int): Int; static function displayHeight(index: Int): Int;

View File

@ -79,7 +79,7 @@ class Display {
public var frequency(get, never): Int; public var frequency(get, never): Int;
function get_frequency(): Int { function get_frequency(): Int {
return 60; return Krom.displayFrequency();
} }
public var pixelsPerInch(get, never): Int; public var pixelsPerInch(get, never): Int;

View File

@ -171,8 +171,9 @@ class SystemImpl {
Krom.setGamepadAxisCallback(gamepadAxisCallback); Krom.setGamepadAxisCallback(gamepadAxisCallback);
Krom.setGamepadButtonCallback(gamepadButtonCallback); Krom.setGamepadButtonCallback(gamepadButtonCallback);
kha.audio2.Audio._init(); kha.audio2.Audio.samplesPerSecond = Krom.getSamplesPerSecond();
kha.audio1.Audio._init(); kha.audio1.Audio._init();
kha.audio2.Audio._init();
Krom.setAudioCallback(audioCallback); Krom.setAudioCallback(audioCallback);
Scheduler.start(); Scheduler.start();
@ -207,7 +208,7 @@ class SystemImpl {
} }
public static function getRefreshRate(): Int { public static function getRefreshRate(): Int {
return 60; return Krom.displayFrequency();
} }
public static function getSystemId(): String { public static function getSystemId(): String {

View File

@ -10,8 +10,7 @@ class Audio {
public static function _init() { public static function _init() {
var bufferSize = 1024 * 2; var bufferSize = 1024 * 2;
buffer = new Buffer(bufferSize * 4, 2, 44100); buffer = new Buffer(bufferSize * 4, 2, samplesPerSecond);
Audio.samplesPerSecond = 44100;
} }
public static function _callCallback(samples: Int): Void { public static function _callCallback(samples: Int): Void {
@ -32,11 +31,11 @@ class Audio {
} }
} }
public static function _readSample(): Float { public static function _readSample(): FastFloat {
if (buffer == null) if (buffer == null)
return 0; return 0;
var value = buffer.data.get(buffer.readLocation); var value = buffer.data.get(buffer.readLocation);
buffer.readLocation += 1; ++buffer.readLocation;
if (buffer.readLocation >= buffer.size) { if (buffer.readLocation >= buffer.size) {
buffer.readLocation = 0; buffer.readLocation = 0;
} }

View File

@ -59,7 +59,7 @@ class Graphics implements kha.graphics4.Graphics {
} }
public function refreshRate(): Int { public function refreshRate(): Int {
return 60; return Krom.displayFrequency();
} }
public function clear(?color: Color, ?depth: Float, ?stencil: Int): Void { public function clear(?color: Color, ?depth: Float, ?stencil: Int): Void {

View File

@ -51,7 +51,7 @@ class Scheduler {
static var vsync: Bool; static var vsync: Bool;
// Html5 target can update display frequency after some delay // Html5 target can update display frequency after some delay
#if kha_html5 #if (kha_html5 || kha_debug_html5)
static var onedifhz(get, never): Float; static var onedifhz(get, never): Float;
static inline function get_onedifhz(): Float { static inline function get_onedifhz(): Float {
@ -97,7 +97,7 @@ class Scheduler {
public static function start(restartTimers: Bool = false): Void { public static function start(restartTimers: Bool = false): Void {
vsync = Window.get(0).vSynced; vsync = Window.get(0).vSynced;
#if !kha_html5 #if !(kha_html5 || kha_debug_html5)
var hz = Display.primary != null ? Display.primary.frequency : 60; var hz = Display.primary != null ? Display.primary.frequency : 60;
if (hz >= 57 && hz <= 63) if (hz >= 57 && hz <= 63)
hz = 60; hz = 60;

BIN
Krom/Krom Executable file → Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -310,9 +310,9 @@ class LeenkxAddonPreferences(AddonPreferences):
layout.label(text="Welcome to Leenkx!") layout.label(text="Welcome to Leenkx!")
# Compare version Blender and Leenkx (major, minor) # Compare version Blender and Leenkx (major, minor)
if bpy.app.version[0] != 4 or bpy.app.version[1] != 2: if bpy.app.version[:2] not in [(4, 4), (4, 2), (3, 6), (3, 3)]:
box = layout.box().column() box = layout.box().column()
box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version such as 4.2 | 3.6 | 3.3") box.label(text="Warning: For Leenkx to work correctly, use a Blender LTS version")
layout.prop(self, "sdk_path") layout.prop(self, "sdk_path")
sdk_path = get_sdk_path(context) sdk_path = get_sdk_path(context)

View File

@ -51,6 +51,7 @@ class ParticleSystem {
seed = pref.seed; seed = pref.seed;
particles = []; particles = [];
ready = false; ready = false;
Data.getParticle(sceneName, pref.particle, function(b: ParticleData) { Data.getParticle(sceneName, pref.particle, function(b: ParticleData) {
data = b; data = b;
r = data.raw; r = data.raw;
@ -70,7 +71,13 @@ class ParticleSystem {
lifetime = r.lifetime / frameRate; lifetime = r.lifetime / frameRate;
animtime = (r.frame_end - r.frame_start) / frameRate; animtime = (r.frame_end - r.frame_start) / frameRate;
spawnRate = ((r.frame_end - r.frame_start) / r.count) / frameRate; spawnRate = ((r.frame_end - r.frame_start) / r.count) / frameRate;
for (i in 0...r.count) particles.push(new Particle(i));
for (i in 0...r.count) {
var particle = new Particle(i);
particle.sr = 1 - Math.random() * r.size_random;
particles.push(particle);
}
ready = true; ready = true;
}); });
} }
@ -108,7 +115,7 @@ class ParticleSystem {
} }
// Animate // Animate
time += Time.realDelta * speed; time += Time.delta * speed;
lap = Std.int(time / animtime); lap = Std.int(time / animtime);
lapTime = time - lap * animtime; lapTime = time - lap * animtime;
count = Std.int(lapTime / spawnRate); count = Std.int(lapTime / spawnRate);
@ -143,7 +150,7 @@ class ParticleSystem {
} }
function setupGeomGpu(object: MeshObject, owner: MeshObject) { function setupGeomGpu(object: MeshObject, owner: MeshObject) {
var instancedData = new Float32Array(particles.length * 3); var instancedData = new Float32Array(particles.length * 6);
var i = 0; var i = 0;
var normFactor = 1 / 32767; // pa.values are not normalized var normFactor = 1 / 32767; // pa.values are not normalized
@ -162,6 +169,10 @@ class ParticleSystem {
instancedData.set(i, pa.values[j * pa.size ] * normFactor * scaleFactor.x); i++; instancedData.set(i, pa.values[j * pa.size ] * normFactor * scaleFactor.x); i++;
instancedData.set(i, pa.values[j * pa.size + 1] * normFactor * scaleFactor.y); i++; instancedData.set(i, pa.values[j * pa.size + 1] * normFactor * scaleFactor.y); i++;
instancedData.set(i, pa.values[j * pa.size + 2] * normFactor * scaleFactor.z); i++; instancedData.set(i, pa.values[j * pa.size + 2] * normFactor * scaleFactor.z); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
} }
case 1: // Face case 1: // Face
@ -185,6 +196,10 @@ class ParticleSystem {
instancedData.set(i, pos.x * normFactor * scaleFactor.x); i++; instancedData.set(i, pos.x * normFactor * scaleFactor.x); i++;
instancedData.set(i, pos.y * normFactor * scaleFactor.y); i++; instancedData.set(i, pos.y * normFactor * scaleFactor.y); i++;
instancedData.set(i, pos.z * normFactor * scaleFactor.z); i++; instancedData.set(i, pos.z * normFactor * scaleFactor.z); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
} }
case 2: // Volume case 2: // Volume
@ -195,9 +210,13 @@ class ParticleSystem {
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.x); i++; instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.x); i++;
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y); i++; instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.y); i++;
instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z); i++; instancedData.set(i, (Math.random() * 2.0 - 1.0) * scaleFactorVolume.z); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
instancedData.set(i, p.sr); i++;
} }
} }
object.data.geom.setupInstanced(instancedData, 1, Usage.StaticUsage); object.data.geom.setupInstanced(instancedData, 3, Usage.StaticUsage);
} }
function fhash(n: Int): Float { function fhash(n: Int): Float {
@ -236,9 +255,10 @@ class ParticleSystem {
class Particle { class Particle {
public var i: Int; public var i: Int;
public var x = 0.0; public var px = 0.0;
public var y = 0.0; public var py = 0.0;
public var z = 0.0; public var pz = 0.0;
public var sr = 1.0; // Size random
public var cameraDistance: Float; public var cameraDistance: Float;
public function new(i: Int) { public function new(i: Int) {

View File

@ -2,9 +2,11 @@ package leenkx.logicnode;
import iron.object.Object; import iron.object.Object;
#if lnx_physics #if lnx_bullet
import leenkx.trait.physics.PhysicsConstraint; import leenkx.trait.physics.PhysicsConstraint;
import leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintType; import leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintType;
#elseif lnx_oimo
// TODO
#end #end
class AddPhysicsConstraintNode extends LogicNode { class AddPhysicsConstraintNode extends LogicNode {
@ -25,7 +27,7 @@ class AddPhysicsConstraintNode extends LogicNode {
if (pivotObject == null || rb1 == null || rb2 == null) return; if (pivotObject == null || rb1 == null || rb2 == null) return;
#if lnx_physics #if lnx_bullet
var disableCollisions: Bool = inputs[4].get(); var disableCollisions: Bool = inputs[4].get();
var breakable: Bool = inputs[5].get(); var breakable: Bool = inputs[5].get();
@ -108,6 +110,8 @@ class AddPhysicsConstraintNode extends LogicNode {
} }
pivotObject.addTrait(con); pivotObject.addTrait(con);
} }
#elseif lnx_oimo
// TODO
#end #end
runOutput(0); runOutput(0);
} }

View File

@ -4,7 +4,7 @@ import iron.object.Object;
#if lnx_physics #if lnx_physics
import leenkx.trait.physics.RigidBody; import leenkx.trait.physics.RigidBody;
import leenkx.trait.physics.bullet.RigidBody.Shape; import leenkx.trait.physics.RigidBody.Shape;
#end #end

View File

@ -0,0 +1,26 @@
package leenkx.logicnode;
class ArrayIndexListNode extends LogicNode {
public function new(tree: LogicTree) {
super(tree);
}
override function get(from: Int): Dynamic {
var array: Array<Dynamic> = inputs[0].get();
array = array.map(item -> Std.string(item));
var value: Dynamic = inputs[1].get();
var from: Int = 0;
var arrayList: Array<Int> = [];
var index: Int = array.indexOf(Std.string(value), from);
while(index != -1){
arrayList.push(index);
index = array.indexOf(Std.string(value), index+1);
}
return arrayList;
}
}

View File

@ -1,7 +1,7 @@
package leenkx.logicnode; package leenkx.logicnode;
#if lnx_physics #if lnx_physics
import leenkx.trait.physics.bullet.PhysicsWorld; import leenkx.trait.physics.PhysicsWorld;
#end #end
import leenkx.trait.navigation.Navigation; import leenkx.trait.navigation.Navigation;
import iron.object.Object; import iron.object.Object;

View File

@ -1,7 +1,7 @@
package leenkx.logicnode; package leenkx.logicnode;
#if lnx_physics #if lnx_physics
import leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintAxis; import leenkx.trait.physics.PhysicsConstraint.ConstraintAxis;
#end #end
class PhysicsConstraintNode extends LogicNode { class PhysicsConstraintNode extends LogicNode {

View File

@ -10,6 +10,10 @@ class KinematicCharacterController extends iron.Trait { public function new() {
typedef KinematicCharacterController = leenkx.trait.physics.bullet.KinematicCharacterController; typedef KinematicCharacterController = leenkx.trait.physics.bullet.KinematicCharacterController;
#else
typedef KinematicCharacterController = leenkx.trait.physics.oimo.KinematicCharacterController;
#end #end
#end #end

View File

@ -3,17 +3,16 @@ package leenkx.trait.physics;
#if (!lnx_physics) #if (!lnx_physics)
class PhysicsConstraint extends iron.Trait { public function new() { super(); } } class PhysicsConstraint extends iron.Trait { public function new() { super(); } }
@:enum abstract ConstraintAxis(Int) from Int to Int { }
#else #else
#if lnx_bullet #if lnx_bullet
typedef PhysicsConstraint = leenkx.trait.physics.bullet.PhysicsConstraint; typedef PhysicsConstraint = leenkx.trait.physics.bullet.PhysicsConstraint;
typedef ConstraintAxis = leenkx.trait.physics.bullet.PhysicsConstraint.ConstraintAxis;
#else #else
typedef PhysicsConstraint = leenkx.trait.physics.oimo.PhysicsConstraint; typedef PhysicsConstraint = leenkx.trait.physics.oimo.PhysicsConstraint;
typedef ConstraintAxis = leenkx.trait.physics.oimo.PhysicsConstraint.ConstraintAxis;
#end #end
#end #end

View File

@ -2,6 +2,7 @@ package leenkx.trait.physics;
#if (!lnx_physics) #if (!lnx_physics)
class Hit { }
class PhysicsWorld extends iron.Trait { public function new() { super(); } } class PhysicsWorld extends iron.Trait { public function new() { super(); } }
#else #else
@ -9,11 +10,11 @@ class PhysicsWorld extends iron.Trait { public function new() { super(); } }
#if lnx_bullet #if lnx_bullet
typedef PhysicsWorld = leenkx.trait.physics.bullet.PhysicsWorld; typedef PhysicsWorld = leenkx.trait.physics.bullet.PhysicsWorld;
typedef Hit = leenkx.trait.physics.bullet.PhysicsWorld.Hit;
#else #else
typedef PhysicsWorld = leenkx.trait.physics.oimo.PhysicsWorld; typedef PhysicsWorld = leenkx.trait.physics.oimo.PhysicsWorld;
typedef Hit = leenkx.trait.physics.oimo.PhysicsWorld.Hit;
#end #end
#end #end

View File

@ -3,16 +3,19 @@ package leenkx.trait.physics;
#if (!lnx_physics) #if (!lnx_physics)
class RigidBody extends iron.Trait { public function new() { super(); } } class RigidBody extends iron.Trait { public function new() { super(); } }
@:enum abstract Shape(Int) from Int to Int { }
#else #else
#if lnx_bullet #if lnx_bullet
typedef RigidBody = leenkx.trait.physics.bullet.RigidBody; typedef RigidBody = leenkx.trait.physics.bullet.RigidBody;
typedef Shape = leenkx.trait.physics.bullet.RigidBody.Shape;
#else #else
typedef RigidBody = leenkx.trait.physics.oimo.RigidBody; typedef RigidBody = leenkx.trait.physics.oimo.RigidBody;
typedef Shape = leenkx.trait.physics.oimo.RigidBody.Shape;
#end #end

View File

@ -1,5 +1,8 @@
package leenkx.trait.physics.bullet; package leenkx.trait.physics.bullet;
#if lnx_bullet
import leenkx.trait.physics.bullet.PhysicsWorld.DebugDrawMode;
import bullet.Bt.Vector3; import bullet.Bt.Vector3;
import kha.FastFloat; import kha.FastFloat;
@ -18,15 +21,21 @@ class DebugDrawHelper {
static inline var contactPointNormalColor = 0xffffffff; static inline var contactPointNormalColor = 0xffffffff;
static inline var contactPointDrawLifetime = true; static inline var contactPointDrawLifetime = true;
final rayCastColor: Vec4 = new Vec4(0.0, 1.0, 0.0);
final rayCastHitColor: Vec4 = new Vec4(1.0, 0.0, 0.0);
final rayCastHitPointColor: Vec4 = new Vec4(1.0, 1.0, 0.0);
final physicsWorld: PhysicsWorld; final physicsWorld: PhysicsWorld;
final lines: Array<LineData> = []; final lines: Array<LineData> = [];
final texts: Array<TextData> = []; final texts: Array<TextData> = [];
var font: kha.Font = null; var font: kha.Font = null;
var debugMode: PhysicsWorld.DebugDrawMode = NoDebug; var rayCasts:Array<TRayCastData> = [];
var debugDrawMode: DebugDrawMode = NoDebug;
public function new(physicsWorld: PhysicsWorld) { public function new(physicsWorld: PhysicsWorld, debugDrawMode: DebugDrawMode) {
this.physicsWorld = physicsWorld; this.physicsWorld = physicsWorld;
this.debugDrawMode = debugDrawMode;
#if lnx_ui #if lnx_ui
iron.data.Data.getFont(Canvas.defaultFontName, function(defaultFont: kha.Font) { iron.data.Data.getFont(Canvas.defaultFontName, function(defaultFont: kha.Font) {
@ -35,6 +44,11 @@ class DebugDrawHelper {
#end #end
iron.App.notifyOnRender2D(onRender); iron.App.notifyOnRender2D(onRender);
if (debugDrawMode & DrawRayCast != 0) {
iron.App.notifyOnUpdate(function () {
rayCasts.resize(0);
});
}
} }
public function drawLine(from: bullet.Bt.Vector3, to: bullet.Bt.Vector3, color: bullet.Bt.Vector3) { public function drawLine(from: bullet.Bt.Vector3, to: bullet.Bt.Vector3, color: bullet.Bt.Vector3) {
@ -63,25 +77,6 @@ class DebugDrawHelper {
} }
} }
// Draws raycast in its own function because
// something is conflicting with the btVector3 and JS pointer wrapping
public function drawRayCast(fx:FastFloat, fy:FastFloat, fz:FastFloat, tx:FastFloat, ty:FastFloat, tz:FastFloat, r:FastFloat, g:FastFloat, b:FastFloat) {
final fromScreenSpace = worldToScreenFast(new Vec4(fx, fy, fz, 1.0));
final toScreenSpace = worldToScreenFast(new Vec4(tx, ty, tz, 1.0));
// TO DO: May still go off screen sides.
if (fromScreenSpace.w == 1 || toScreenSpace.w == 1) {
final color = kha.Color.fromFloats(r, g, b, 1.0);
lines.push({
fromX: fromScreenSpace.x,
fromY: fromScreenSpace.y,
toX: toScreenSpace.x,
toY: toScreenSpace.y,
color: color
});
}
}
public function drawContactPoint(pointOnB: Vector3, normalOnB: Vector3, distance: kha.FastFloat, lifeTime: Int, color: Vector3) { public function drawContactPoint(pointOnB: Vector3, normalOnB: Vector3, distance: kha.FastFloat, lifeTime: Int, color: Vector3) {
#if js #if js
pointOnB = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", pointOnB); pointOnB = js.Syntax.code("Ammo.wrapPointer({0}, Ammo.btVector3)", pointOnB);
@ -126,7 +121,62 @@ class DebugDrawHelper {
x: contactPointScreenSpace.x, x: contactPointScreenSpace.x,
y: contactPointScreenSpace.y, y: contactPointScreenSpace.y,
color: color, color: color,
text: Std.string(lifeTime), text: Std.string(lifeTime), // lifeTime: number of frames the contact point existed
});
}
}
}
public function rayCast(rayCastData:TRayCastData) {
rayCasts.push(rayCastData);
}
function drawRayCast(f: Vec4, t: Vec4, hit: Bool) {
final from = worldToScreenFast(f.clone());
final to = worldToScreenFast(t.clone());
var c: kha.Color;
if (from.w == 1 && to.w == 1) {
if (hit) c = kha.Color.fromFloats(rayCastHitColor.x, rayCastHitColor.y, rayCastHitColor.z);
else c = kha.Color.fromFloats(rayCastColor.x, rayCastColor.y, rayCastColor.z);
lines.push({
fromX: from.x,
fromY: from.y,
toX: to.x,
toY: to.y,
color: c
});
}
}
function drawHitPoint(hp: Vec4) {
final hitPoint = worldToScreenFast(hp.clone());
final c = kha.Color.fromFloats(rayCastHitPointColor.x, rayCastHitPointColor.y, rayCastHitPointColor.z);
if (hitPoint.w == 1) {
lines.push({
fromX: hitPoint.x - contactPointSizePx,
fromY: hitPoint.y - contactPointSizePx,
toX: hitPoint.x + contactPointSizePx,
toY: hitPoint.y + contactPointSizePx,
color: c
});
lines.push({
fromX: hitPoint.x - contactPointSizePx,
fromY: hitPoint.y + contactPointSizePx,
toX: hitPoint.x + contactPointSizePx,
toY: hitPoint.y - contactPointSizePx,
color: c
});
if (font != null) {
texts.push({
x: hitPoint.x,
y: hitPoint.y,
color: c,
text: 'RAYCAST HIT'
}); });
} }
} }
@ -155,13 +205,13 @@ class DebugDrawHelper {
}); });
} }
public function setDebugMode(debugMode: PhysicsWorld.DebugDrawMode) { public function setDebugMode(debugDrawMode: DebugDrawMode) {
this.debugMode = debugMode; this.debugDrawMode = debugDrawMode;
} }
public function getDebugMode(): PhysicsWorld.DebugDrawMode { public function getDebugMode(): DebugDrawMode {
#if js #if js
return debugMode; return debugDrawMode;
#elseif hl #elseif hl
return physicsWorld.getDebugDrawMode(); return physicsWorld.getDebugDrawMode();
#else #else
@ -170,7 +220,7 @@ class DebugDrawHelper {
} }
function onRender(g: kha.graphics2.Graphics) { function onRender(g: kha.graphics2.Graphics) {
if (getDebugMode() == NoDebug && !physicsWorld.drawRaycasts) { if (getDebugMode() == NoDebug) {
return; return;
} }
@ -179,7 +229,9 @@ class DebugDrawHelper {
// will cause Bullet to call the btIDebugDraw callbacks), but this way // will cause Bullet to call the btIDebugDraw callbacks), but this way
// we can ensure that--within a frame--the function will not be called // we can ensure that--within a frame--the function will not be called
// before some user-specific physics update, which would result in a // before some user-specific physics update, which would result in a
// one-frame drawing delay... // one-frame drawing delay... Ideally we would ensure that debugDrawWorld()
// is called when all other (late) update callbacks are already executed...
physicsWorld.world.debugDrawWorld(); physicsWorld.world.debugDrawWorld();
g.opacity = 1.0; g.opacity = 1.0;
@ -199,6 +251,17 @@ class DebugDrawHelper {
} }
texts.resize(0); texts.resize(0);
} }
if (debugDrawMode & DrawRayCast != 0) {
for (rayCastData in rayCasts) {
if (rayCastData.hasHit) {
drawRayCast(rayCastData.from, rayCastData.hitPoint, true);
drawHitPoint(rayCastData.hitPoint);
} else {
drawRayCast(rayCastData.from, rayCastData.to, false);
}
}
}
} }
/** /**
@ -241,3 +304,14 @@ class TextData {
public var color: kha.Color; public var color: kha.Color;
public var text: String; public var text: String;
} }
@:structInit
typedef TRayCastData = {
var from: Vec4;
var to: Vec4;
var hasHit: Bool;
@:optional var hitPoint: Vec4;
@:optional var hitNormal: Vec4;
}
#end

View File

@ -71,7 +71,6 @@ class PhysicsWorld extends Trait {
public var convexHitPointWorld = new Vec4(); public var convexHitPointWorld = new Vec4();
public var convexHitNormalWorld = new Vec4(); public var convexHitNormalWorld = new Vec4();
var pairCache: Bool = false; var pairCache: Bool = false;
public var drawRaycasts: Bool = false;
static var nullvec = true; static var nullvec = true;
static var vec1: bullet.Bt.Vector3 = null; static var vec1: bullet.Bt.Vector3 = null;
@ -102,7 +101,7 @@ class PhysicsWorld extends Trait {
public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10, debugDrawMode: DebugDrawMode = NoDebug, drawRaycasts: Bool = false) { public function new(timeScale = 1.0, maxSteps = 10, solverIterations = 10, debugDrawMode: DebugDrawMode = NoDebug) {
super(); super();
if (nullvec) { if (nullvec) {
@ -121,7 +120,6 @@ class PhysicsWorld extends Trait {
this.timeScale = timeScale; this.timeScale = timeScale;
this.maxSteps = maxSteps; this.maxSteps = maxSteps;
this.solverIterations = solverIterations; this.solverIterations = solverIterations;
this.drawRaycasts = drawRaycasts;
// First scene // First scene
if (active == null) { if (active == null) {
@ -408,14 +406,6 @@ class PhysicsWorld extends Trait {
var worldDyn: bullet.Bt.DynamicsWorld = world; var worldDyn: bullet.Bt.DynamicsWorld = world;
var worldCol: bullet.Bt.CollisionWorld = worldDyn; var worldCol: bullet.Bt.CollisionWorld = worldDyn;
if (this.drawRaycasts && this.debugDrawHelper != null) {
this.debugDrawHelper.drawRayCast(
rayFrom.x(), rayFrom.y(), rayFrom.z(),
rayTo.x(), rayTo.y(), rayTo.z(),
0.73, 0.341, 1.0
);
}
worldCol.rayTest(rayFrom, rayTo, rayCallback); worldCol.rayTest(rayFrom, rayTo, rayCallback);
var rb: RigidBody = null; var rb: RigidBody = null;
var hitInfo: Hit = null; var hitInfo: Hit = null;
@ -441,6 +431,16 @@ class PhysicsWorld extends Trait {
#end #end
} }
if (getDebugDrawMode() & DrawRayCast != 0) {
debugDrawHelper.rayCast({
from: from,
to: to,
hasHit: rc.hasHit(),
hitPoint: hitPointWorld,
hitNormal: hitNormalWorld
});
}
#if js #if js
bullet.Bt.Ammo.destroy(rayCallback); bullet.Bt.Ammo.destroy(rayCallback);
#else #else
@ -519,22 +519,14 @@ class PhysicsWorld extends Trait {
public function setDebugDrawMode(debugDrawMode: DebugDrawMode) { public function setDebugDrawMode(debugDrawMode: DebugDrawMode) {
if (debugDrawHelper == null) { if (debugDrawHelper == null) {
// Initialize if helper is null AND (standard debug mode is requested OR our custom raycast drawing is requested) if (debugDrawMode == NoDebug) {
if (debugDrawMode != NoDebug || this.drawRaycasts) {
initDebugDrawing();
}
else {
// Helper is null and no debug drawing needed, so exit
return; return;
} }
initDebugDrawing(debugDrawMode);
} }
// If we reached here, the helper is initialized (or was already)
// Now set the standard Bullet debug mode on the actual drawer
#if js #if js
// Ensure drawer exists before setting mode (might have just been initialized) world.getDebugDrawer().setDebugMode(debugDrawMode);
var drawer = world.getDebugDrawer();
if (drawer != null) drawer.setDebugMode(debugDrawMode);
#elseif hl #elseif hl
hlDebugDrawer_setDebugMode(debugDrawMode); hlDebugDrawer_setDebugMode(debugDrawMode);
#end #end
@ -554,8 +546,8 @@ class PhysicsWorld extends Trait {
#end #end
} }
function initDebugDrawing() { function initDebugDrawing(debugDrawMode: DebugDrawMode) {
debugDrawHelper = new DebugDrawHelper(this); debugDrawHelper = new DebugDrawHelper(this, debugDrawMode);
#if js #if js
final drawer = new bullet.Bt.DebugDrawer(); final drawer = new bullet.Bt.DebugDrawer();
@ -691,6 +683,8 @@ enum abstract DebugDrawMode(Int) from Int to Int {
**/ **/
var DrawFrames = 1 << 15; var DrawFrames = 1 << 15;
var DrawRayCast = 1 << 16;
@:op(~A) public inline function bitwiseNegate(): DebugDrawMode { @:op(~A) public inline function bitwiseNegate(): DebugDrawMode {
return ~this; return ~this;
} }

Binary file not shown.

View File

@ -324,6 +324,20 @@ class LeenkxExporter:
def export_object_transform(self, bobject: bpy.types.Object, o): def export_object_transform(self, bobject: bpy.types.Object, o):
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
# HACK: In Blender 4.2.x, each camera must be selected to ensure its matrix is correctly assigned
if bpy.app.version >= (4, 2, 0) and bobject.type == 'CAMERA' and bobject.users_scene:
current_scene = bpy.context.window.scene
bpy.context.window.scene = bobject.users_scene[0]
bpy.context.view_layer.update()
bobject.select_set(True)
bpy.context.view_layer.update()
bobject.select_set(False)
bpy.context.window.scene = current_scene
bpy.context.view_layer.update()
# Static transform # Static transform
o['transform'] = {'values': LeenkxExporter.write_matrix(bobject.matrix_local)} o['transform'] = {'values': LeenkxExporter.write_matrix(bobject.matrix_local)}
@ -1552,8 +1566,7 @@ class LeenkxExporter:
log.error(e.message) log.error(e.message)
else: else:
# Assume it was caused because of encountering n-gons # Assume it was caused because of encountering n-gons
log.error(f"""object {bobject.name} contains n-gons in its mesh, so it's impossible to compute tanget space for normal mapping. log.error(f"""object {bobject.name} contains n-gons in its mesh, so it's impossible to compute tanget space for normal mapping. Make sure the mesh only has tris/quads.""")
Make sure the mesh only has tris/quads.""")
tangdata = np.empty(num_verts * 3, dtype='<f4') tangdata = np.empty(num_verts * 3, dtype='<f4')
if has_col: if has_col:
@ -3026,16 +3039,16 @@ Make sure the mesh only has tris/quads.""")
if rbw is not None and rbw.enabled: if rbw is not None and rbw.enabled:
out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)] out_trait['parameters'] = [str(rbw.time_scale), str(rbw.substeps_per_frame), str(rbw.solver_iterations)]
if phys_pkg == 'bullet': if phys_pkg == 'bullet' or phys_pkg == 'oimo':
debug_draw_mode = 1 if wrd.lnx_bullet_dbg_draw_wireframe else 0 debug_draw_mode = 1 if wrd.lnx_physics_dbg_draw_wireframe else 0
debug_draw_mode |= 2 if wrd.lnx_bullet_dbg_draw_aabb else 0 debug_draw_mode |= 2 if wrd.lnx_physics_dbg_draw_aabb else 0
debug_draw_mode |= 8 if wrd.lnx_bullet_dbg_draw_contact_points else 0 debug_draw_mode |= 8 if wrd.lnx_physics_dbg_draw_contact_points else 0
debug_draw_mode |= 2048 if wrd.lnx_bullet_dbg_draw_constraints else 0 debug_draw_mode |= 2048 if wrd.lnx_physics_dbg_draw_constraints else 0
debug_draw_mode |= 4096 if wrd.lnx_bullet_dbg_draw_constraint_limits else 0 debug_draw_mode |= 4096 if wrd.lnx_physics_dbg_draw_constraint_limits else 0
debug_draw_mode |= 16384 if wrd.lnx_bullet_dbg_draw_normals else 0 debug_draw_mode |= 16384 if wrd.lnx_physics_dbg_draw_normals else 0
debug_draw_mode |= 32768 if wrd.lnx_bullet_dbg_draw_axis_gizmo else 0 debug_draw_mode |= 32768 if wrd.lnx_physics_dbg_draw_axis_gizmo else 0
debug_draw_mode |= 65536 if wrd.lnx_physics_dbg_draw_raycast else 0
out_trait['parameters'].append(str(debug_draw_mode)) out_trait['parameters'].append(str(debug_draw_mode))
out_trait['parameters'].append(str(wrd.lnx_bullet_dbg_draw_raycast).lower())
self.output['traits'].append(out_trait) self.output['traits'].append(out_trait)

View File

@ -0,0 +1,13 @@
from lnx.logicnode.lnx_nodes import *
class ArrayIndexNode(LnxLogicTreeNode):
"""Returns the array index list of the given value as an array."""
bl_idname = 'LNArrayIndexListNode'
bl_label = 'Array Index List'
lnx_version = 1
def lnx_init(self, context):
self.add_input('LnxNodeSocketArray', 'Array')
self.add_input('LnxDynamicSocket', 'Value')
self.add_output('LnxNodeSocketArray', 'Array')

View File

@ -170,32 +170,64 @@ vec3 random3(const vec3 c) {
r.y = fract(512.0 * j); r.y = fract(512.0 * j);
return r - 0.5; return r - 0.5;
} }
float tex_musgrave_f(const vec3 p) {
float noise_tex(const vec3 p) {
const float F3 = 0.3333333; const float F3 = 0.3333333;
const float G3 = 0.1666667; const float G3 = 0.1666667;
vec3 s = floor(p + dot(p, vec3(F3))); vec3 s = floor(p + dot(p, vec3(F3)));
vec3 x = p - s + dot(s, vec3(G3)); vec3 x = p - s + dot(s, vec3(G3));
vec3 e = step(vec3(0.0), x - x.yzx); vec3 e = step(vec3(0.0), x - x.yzx);
vec3 i1 = e * (1.0 - e.zxy); vec3 i1 = e * (1.0 - e.zxy);
vec3 i2 = 1.0 - e.zxy * (1.0 - e); vec3 i2 = 1.0 - e.zxy * (1.0 - e);
vec3 x1 = x - i1 + G3; vec3 x1 = x - i1 + G3;
vec3 x2 = x - i2 + 2.0 * G3; vec3 x2 = x - i2 + 2.0 * G3;
vec3 x3 = x - 1.0 + 3.0 * G3; vec3 x3 = x - 1.0 + 3.0 * G3;
vec4 w, d;
w.x = dot(x, x); vec4 w;
w.y = dot(x1, x1); w.x = max(0.6 - dot(x, x), 0.0);
w.z = dot(x2, x2); w.y = max(0.6 - dot(x1, x1), 0.0);
w.w = dot(x3, x3); w.z = max(0.6 - dot(x2, x2), 0.0);
w = max(0.6 - w, 0.0); w.w = max(0.6 - dot(x3, x3), 0.0);
w = w * w;
w = w * w;
vec4 d;
d.x = dot(random3(s), x); d.x = dot(random3(s), x);
d.y = dot(random3(s + i1), x1); d.y = dot(random3(s + i1), x1);
d.z = dot(random3(s + i2), x2); d.z = dot(random3(s + i2), x2);
d.w = dot(random3(s + 1.0), x3); d.w = dot(random3(s + 1.0), x3);
w *= w;
w *= w;
d *= w; d *= w;
return clamp(dot(d, vec4(52.0)), 0.0, 1.0); return clamp(dot(d, vec4(52.0)), 0.0, 1.0);
} }
float tex_musgrave_f(const vec3 p, float detail, float distortion) {
// Apply distortion to the input coordinates smoothly with noise_tex
vec3 distorted_p = p + distortion * vec3(
noise_tex(p + vec3(5.2, 1.3, 7.1)),
noise_tex(p + vec3(1.7, 9.2, 3.8)),
noise_tex(p + vec3(8.3, 2.8, 4.5))
);
float value = 0.0;
float amplitude = 1.0;
float frequency = 1.0;
// Use 'detail' as number of octaves, clamped between 1 and 8
int octaves = int(clamp(detail, 1.0, 8.0));
for (int i = 0; i < octaves; i++) {
value += amplitude * noise_tex(distorted_p * frequency);
frequency *= 2.0;
amplitude *= 0.5;
}
return clamp(value, 0.0, 1.0);
}
""" """
# col: the incoming color # col: the incoming color

View File

@ -254,10 +254,10 @@ if bpy.app.version < (4, 1, 0):
co = 'bposition' co = 'bposition'
scale = c.parse_value_input(node.inputs['Scale']) scale = c.parse_value_input(node.inputs['Scale'])
# detail = c.parse_value_input(node.inputs[2]) detail = c.parse_value_input(node.inputs[3])
# distortion = c.parse_value_input(node.inputs[3]) distortion = c.parse_value_input(node.inputs[4])
res = f'tex_musgrave_f({co} * {scale} * 0.5)' res = f'tex_musgrave_f({co} * {scale} * 0.5, {detail}, {distortion})'
return res return res
@ -278,11 +278,11 @@ def parse_tex_noise(node: bpy.types.ShaderNodeTexNoise, out_socket: bpy.types.No
distortion = c.parse_value_input(node.inputs[5]) distortion = c.parse_value_input(node.inputs[5])
if bpy.app.version >= (4, 1, 0): if bpy.app.version >= (4, 1, 0):
if node.noise_type == "FBM": if node.noise_type == "FBM":
if out_socket == node.outputs[1]:
state.curshader.add_function(c_functions.str_tex_musgrave) state.curshader.add_function(c_functions.str_tex_musgrave)
res = 'vec3(tex_musgrave_f({0} * {1}), tex_musgrave_f({0} * {1} + 120.0), tex_musgrave_f({0} * {1} + 168.0))'.format(co, scale, detail, distortion) if out_socket == node.outputs[1]:
res = 'vec3(tex_musgrave_f({0} * {1}, {2}, {3}), tex_musgrave_f({0} * {1} + 120.0, {2}, {3}), tex_musgrave_f({0} * {1} + 168.0, {2}, {3}))'.format(co, scale, detail, distortion)
else: else:
res = f'tex_musgrave_f({co} * {scale} * 1.0)' res = f'tex_musgrave_f({co} * {scale} * 1.0, {detail}, {distortion})'
else: else:
if out_socket == node.outputs[1]: if out_socket == node.outputs[1]:
res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion) res = 'vec3(tex_noise({0} * {1},{2},{3}), tex_noise({0} * {1} + 120.0,{2},{3}), tex_noise({0} * {1} + 168.0,{2},{3}))'.format(co, scale, detail, distortion)

View File

@ -86,7 +86,7 @@ def write(vert, particle_info=None, shadowmap=False):
vert.write('p_fade = sin(min((p_age / 2) * 3.141592, 3.141592));') vert.write('p_fade = sin(min((p_age / 2) * 3.141592, 3.141592));')
if out_index: if out_index:
vert.add_out('float p_index'); vert.add_out('float p_index')
vert.write('p_index = gl_InstanceID;') vert.write('p_index = gl_InstanceID;')
def write_tilesheet(vert): def write_tilesheet(vert):

View File

@ -209,7 +209,8 @@ def make_instancing_and_skinning(mat: Material, mat_users: Dict[Material, List[O
global_elems.append({'name': 'ipos', 'data': 'float3'}) global_elems.append({'name': 'ipos', 'data': 'float3'})
if 'Rot' in inst: if 'Rot' in inst:
global_elems.append({'name': 'irot', 'data': 'float3'}) global_elems.append({'name': 'irot', 'data': 'float3'})
if 'Scale' in inst: #HACK: checking `mat.arm_particle_flag` to force appending 'iscl' to the particle's vertex shader
if 'Scale' in inst or mat.arm_particle_flag:
global_elems.append({'name': 'iscl', 'data': 'float3'}) global_elems.append({'name': 'iscl', 'data': 'float3'})
elif inst == 'Off': elif inst == 'Off':

View File

@ -77,6 +77,25 @@ class LNX_MT_NodeAddOverride(bpy.types.Menu):
layout.separator() layout.separator()
layout.menu(f'LNX_MT_{INTERNAL_GROUPS_MENU_ID}_menu', text=internal_groups_menu_class.bl_label, icon='OUTLINER_OB_GROUP_INSTANCE') layout.menu(f'LNX_MT_{INTERNAL_GROUPS_MENU_ID}_menu', text=internal_groups_menu_class.bl_label, icon='OUTLINER_OB_GROUP_INSTANCE')
elif context.space_data.tree_type == 'ShaderNodeTree':
# TO DO - Recursively gather nodes and draw them to menu
LNX_MT_NodeAddOverride.overridden_draw(self, context)
layout = self.layout
layout.separator()
layout.separator()
col = layout.column()
col.label(text="Custom")
shader_data_op = col.operator("node.add_node", text="Shader Data")
shader_data_op.type = "LnxShaderDataNode"
shader_data_op.use_transform = True
particle_op = col.operator("node.add_node", text="Custom Particle")
particle_op.type = "LnxCustomParticleNode"
particle_op.use_transform = True
else: else:
LNX_MT_NodeAddOverride.overridden_draw(self, context) LNX_MT_NodeAddOverride.overridden_draw(self, context)

View File

@ -197,38 +197,38 @@ def init_properties():
items=[('Bullet', 'Bullet', 'Bullet'), items=[('Bullet', 'Bullet', 'Bullet'),
('Oimo', 'Oimo', 'Oimo')], ('Oimo', 'Oimo', 'Oimo')],
name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache) name="Physics Engine", default='Bullet', update=assets.invalidate_compiler_cache)
bpy.types.World.lnx_bullet_dbg_draw_wireframe = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_wireframe = BoolProperty(
name="Collider Wireframes", default=False, name="Collider Wireframes", default=False,
description="Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations" description="Draw wireframes of the physics collider meshes and suspensions of raycast vehicle simulations"
) )
bpy.types.World.lnx_bullet_dbg_draw_raycast = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_raycast = BoolProperty(
name="Trace Raycast", default=False, name="Raycasts", default=False,
description="Draw raycasts to trace the results" description="Draw raycasts to trace the results"
) )
bpy.types.World.lnx_bullet_dbg_draw_aabb = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_aabb = BoolProperty(
name="Axis-aligned Minimum Bounding Boxes", default=False, name="Axis-aligned Minimum Bounding Boxes", default=False,
description="Draw axis-aligned minimum bounding boxes (AABBs) of the physics collider meshes" description="Draw axis-aligned minimum bounding boxes (AABBs) of the physics collider meshes"
) )
bpy.types.World.lnx_bullet_dbg_draw_contact_points = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_contact_points = BoolProperty(
name="Contact Points", default=False, name="Contact Points", default=False,
description="Visualize contact points of multiple colliders" description="Visualize contact points of multiple colliders"
) )
bpy.types.World.lnx_bullet_dbg_draw_constraints = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_constraints = BoolProperty(
name="Constraints", default=False, name="Constraints", default=False,
description="Draw axis gizmos for important constraint points" description="Draw axis gizmos for important constraint points"
) )
bpy.types.World.lnx_bullet_dbg_draw_constraint_limits = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_constraint_limits = BoolProperty(
name="Constraint Limits", default=False, name="Constraint Limits", default=False,
description="Draw additional constraint information such as distance or angle limits" description="Draw additional constraint information such as distance or angle limits"
) )
bpy.types.World.lnx_bullet_dbg_draw_normals = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_normals = BoolProperty(
name="Face Normals", default=False, name="Face Normals", default=False,
description=( description=(
"Draw the normal vectors of the triangles of the physics collider meshes." "Draw the normal vectors of the triangles of the physics collider meshes."
" This only works for mesh collision shapes" " This only works with Bullet physics, for mesh collision shapes"
) )
) )
bpy.types.World.lnx_bullet_dbg_draw_axis_gizmo = BoolProperty( bpy.types.World.lnx_physics_dbg_draw_axis_gizmo = BoolProperty(
name="Axis Gizmos", default=False, name="Axis Gizmos", default=False,
description=( description=(
"Draw a small axis gizmo at the origin of the collision shape." "Draw a small axis gizmo at the origin of the collision shape."

View File

@ -1907,7 +1907,7 @@ class LNX_PT_RenderPathPostProcessPanel(bpy.types.Panel):
col.prop(rpdat, "rp_bloom") col.prop(rpdat, "rp_bloom")
_col = col.column() _col = col.column()
_col.enabled = rpdat.rp_bloom _col.enabled = rpdat.rp_bloom
if bpy.app.version <= (4, 2, 4): if bpy.app.version < (4, 3, 0):
_col.prop(rpdat, 'lnx_bloom_follow_blender') _col.prop(rpdat, 'lnx_bloom_follow_blender')
if not rpdat.lnx_bloom_follow_blender: if not rpdat.lnx_bloom_follow_blender:
_col.prop(rpdat, 'lnx_bloom_threshold') _col.prop(rpdat, 'lnx_bloom_threshold')
@ -2749,20 +2749,20 @@ class LNX_PT_BulletDebugDrawingPanel(bpy.types.Panel):
layout.use_property_decorate = False layout.use_property_decorate = False
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
if wrd.lnx_physics_engine != 'Bullet': if wrd.lnx_physics_engine != 'Bullet' and wrd.lnx_physics_engine != 'Oimo':
row = layout.row() row = layout.row()
row.alert = True row.alert = True
row.label(text="Physics debug drawing is only supported for the Bullet physics engine") row.label(text="Physics debug drawing is only supported for the Bullet and Oimo physics engines")
col = layout.column(align=False) col = layout.column(align=False)
col.prop(wrd, "lnx_bullet_dbg_draw_wireframe") col.prop(wrd, "lnx_physics_dbg_draw_wireframe")
col.prop(wrd, "lnx_bullet_dbg_draw_raycast") col.prop(wrd, "lnx_physics_dbg_draw_raycast")
col.prop(wrd, "lnx_bullet_dbg_draw_aabb") col.prop(wrd, "lnx_physics_dbg_draw_aabb")
col.prop(wrd, "lnx_bullet_dbg_draw_contact_points") col.prop(wrd, "lnx_physics_dbg_draw_contact_points")
col.prop(wrd, "lnx_bullet_dbg_draw_constraints") col.prop(wrd, "lnx_physics_dbg_draw_constraints")
col.prop(wrd, "lnx_bullet_dbg_draw_constraint_limits") col.prop(wrd, "lnx_physics_dbg_draw_constraint_limits")
col.prop(wrd, "lnx_bullet_dbg_draw_normals") col.prop(wrd, "lnx_physics_dbg_draw_normals")
col.prop(wrd, "lnx_bullet_dbg_draw_axis_gizmo") col.prop(wrd, "lnx_physics_dbg_draw_axis_gizmo")
def draw_custom_node_menu(self, context): def draw_custom_node_menu(self, context):
"""Extension of the node context menu. """Extension of the node context menu.

View File

@ -828,8 +828,8 @@ def check_blender_version(op: bpy.types.Operator):
"""Check whether the Blender version is supported by Leenkx, """Check whether the Blender version is supported by Leenkx,
if not, report in UI. if not, report in UI.
""" """
if bpy.app.version[0] != 4 or bpy.app.version[1] != 2: if bpy.app.version[:2] not in [(4, 4), (4, 2), (3, 6), (3, 3)]:
op.report({'INFO'}, 'INFO: For Leenkx to work correctly, use a Blender LTS version such as 4.2 | 3.6 | 3.3') op.report({'INFO'}, 'INFO: For Leenkx to work correctly, use a Blender LTS version')
def check_saved(self): def check_saved(self):