Blender 2.8 - 4.5 Support

This commit is contained in:
2025-09-28 12:44:04 -07:00
parent 8f8d4b1376
commit f97d8fd846
34 changed files with 581 additions and 399 deletions

View File

@ -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)

Binary file not shown.

View File

@ -1,9 +1,17 @@
import importlib
import sys
import types
import bpy
# This gets cleared if this package/the __init__ module is reloaded
_module_cache: dict[str, types.ModuleType] = {}
if bpy.app.version < (2, 92, 0):
from typing import Dict
ModuleCacheType = Dict[str, types.ModuleType]
else:
ModuleCacheType = dict[str, types.ModuleType]
_module_cache: ModuleCacheType = {}
def enable_reload(module_name: str):

View File

@ -15,7 +15,14 @@ from enum import Enum, unique
import math
import os
import time
from typing import Any, Dict, List, Tuple, Union, Optional
from typing import Any, Dict, List, Tuple, Union, Optional, TYPE_CHECKING
import bpy
if bpy.app.version >= (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

View File

@ -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[:]

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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":

View File

@ -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):

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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).
"""

View File

@ -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':

View File

@ -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':

View File

@ -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 == '':

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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) {

View File

@ -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 (

View File

@ -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),

View File

@ -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.
"""

View File

@ -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()

View File

@ -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])

View File

@ -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

View File

@ -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)

View File

@ -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.

View File

@ -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()

View File

@ -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)

View File

@ -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