Blender 2.8 - 4.5 Support
This commit is contained in:
58
leenkx.py
58
leenkx.py
@ -24,7 +24,7 @@ import textwrap
|
||||
import threading
|
||||
import traceback
|
||||
import typing
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable, Optional, List
|
||||
import webbrowser
|
||||
|
||||
import bpy
|
||||
@ -33,6 +33,12 @@ from bpy.props import *
|
||||
from bpy.types import Operator, AddonPreferences
|
||||
|
||||
|
||||
if bpy.app.version < (2, 90, 0):
|
||||
ListType = List
|
||||
else:
|
||||
ListType = list
|
||||
|
||||
|
||||
class SDKSource(IntEnum):
|
||||
PREFS = 0
|
||||
LOCAL = 1
|
||||
@ -58,8 +64,45 @@ def get_os():
|
||||
else:
|
||||
return 'linux'
|
||||
|
||||
|
||||
def detect_sdk_path():
|
||||
"""Auto-detect the SDK path after Leenkx installation."""
|
||||
preferences = bpy.context.preferences
|
||||
addon_prefs = preferences.addons["leenkx"].preferences
|
||||
|
||||
# Don't overwrite if already set
|
||||
if addon_prefs.sdk_path:
|
||||
return
|
||||
|
||||
# For all versions, try to get the path from the current file location first
|
||||
current_file = os.path.realpath(__file__)
|
||||
if os.path.exists(current_file):
|
||||
# Go up one level from the current file's directory to get the SDK root
|
||||
sdk_path = os.path.dirname(os.path.dirname(current_file))
|
||||
if os.path.exists(os.path.join(sdk_path, "leenkx")):
|
||||
addon_prefs.sdk_path = sdk_path
|
||||
return
|
||||
|
||||
# Fallback for Blender 2.92+ with the original method
|
||||
if bpy.app.version >= (2, 92, 0):
|
||||
try:
|
||||
win = bpy.context.window_manager.windows[0]
|
||||
area = win.screen.areas[0]
|
||||
area_type = area.type
|
||||
area.type = "INFO"
|
||||
|
||||
with bpy.context.temp_override(window=win, screen=win.screen, area=area):
|
||||
bpy.ops.info.select_all(action='SELECT')
|
||||
bpy.ops.info.report_copy()
|
||||
|
||||
clipboard = bpy.context.window_manager.clipboard
|
||||
match = re.findall(r"^Modules Installed .* from '(.*leenkx.py)' into",
|
||||
clipboard, re.MULTILINE)
|
||||
if match:
|
||||
addon_prefs.sdk_path = os.path.dirname(match[-1])
|
||||
finally:
|
||||
area.type = area_type
|
||||
|
||||
def detect_sdk_path22():
|
||||
"""Auto-detect the SDK path after Leenkx installation."""
|
||||
# Do not overwrite the SDK path (this method gets
|
||||
# called after each registration, not after
|
||||
@ -73,6 +116,7 @@ def detect_sdk_path():
|
||||
area = win.screen.areas[0]
|
||||
area_type = area.type
|
||||
area.type = "INFO"
|
||||
|
||||
with bpy.context.temp_override(window=win, screen=win.screen, area=area):
|
||||
bpy.ops.info.select_all(action='SELECT')
|
||||
bpy.ops.info.report_copy()
|
||||
@ -558,7 +602,7 @@ def remove_readonly(func, path, excinfo):
|
||||
func(path)
|
||||
|
||||
|
||||
def run_proc(cmd: list[str], done: Optional[Callable[[bool], None]] = None):
|
||||
def run_proc(cmd: ListType[str], done: Optional[Callable[[bool], None]] = None):
|
||||
def fn(p, done):
|
||||
p.wait()
|
||||
if done is not None:
|
||||
@ -840,7 +884,13 @@ def update_leenkx_py(sdk_path: str, force_relink=False):
|
||||
else:
|
||||
raise err
|
||||
else:
|
||||
lnx_module_file.unlink(missing_ok=True)
|
||||
if bpy.app.version < (2, 92, 0):
|
||||
try:
|
||||
lnx_module_file.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
else:
|
||||
lnx_module_file.unlink(missing_ok=True)
|
||||
shutil.copy(Path(sdk_path) / 'leenkx.py', lnx_module_file)
|
||||
|
||||
|
||||
|
BIN
leenkx/blender/data/lnx_data_2.blend
Normal file
BIN
leenkx/blender/data/lnx_data_2.blend
Normal file
Binary file not shown.
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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[:]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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":
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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).
|
||||
"""
|
||||
|
@ -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':
|
||||
|
@ -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':
|
||||
|
@ -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 == '':
|
||||
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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 (
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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()
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user