2025-01-22 16:18:30 +01:00
import json
import os
import shutil
import subprocess
from typing import Union
import webbrowser
from bpy . types import Menu , NodeTree
from bpy . props import *
import bpy . utils . previews
import lnx . make as make
from lnx . props_traits_props import *
import lnx . ui_icons as ui_icons
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 )
from lnx . props_traits_props import *
ui_icons = lnx . reload_module ( ui_icons )
lnx . utils = lnx . reload_module ( lnx . utils )
lnx . write_data = lnx . reload_module ( lnx . write_data )
else :
lnx . enable_reload ( __name__ )
ICON_HAXE = ui_icons . get_id ( ' haxe ' )
ICON_NODES = ' NODETREE '
ICON_CANVAS = ' NODE_COMPOSITING '
ICON_BUNDLED = ui_icons . get_id ( ' bundle ' )
ICON_WASM = ui_icons . get_id ( ' wasm ' )
# Pay attention to the ID number parameter for backward compatibility!
# This is important if the enum is reordered or the string identifier
# is changed as the number is what's stored in the blend file
PROP_TYPES_ENUM = [
( ' Haxe Script ' , ' Haxe ' , ' Haxe script ' , ICON_HAXE , 0 ) ,
( ' Logic Nodes ' , ' Nodes ' , ' Logic nodes (visual scripting) ' , ICON_NODES , 4 ) ,
( ' UI Canvas ' , ' UI ' , ' User interface ' , ICON_CANVAS , 2 ) ,
( ' Bundled Script ' , ' Bundled ' , ' Premade script with common functionality ' , ICON_BUNDLED , 3 ) ,
( ' WebAssembly ' , ' Wasm ' , ' WebAssembly ' , ICON_WASM , 1 )
]
def trigger_recompile ( self , context ) :
wrd = bpy . data . worlds [ ' Lnx ' ]
wrd . lnx_recompile = True
def update_trait_group ( self , context ) :
o = context . object if self . is_object else context . scene
if o == None :
return
i = o . lnx_traitlist_index
if i > = 0 and i < len ( o . lnx_traitlist ) :
t = o . lnx_traitlist [ i ]
if t . type_prop == ' Haxe Script ' or t . type_prop == ' Bundled Script ' :
t . name = t . class_name_prop
elif t . type_prop == ' WebAssembly ' :
t . name = t . webassembly_prop
elif t . type_prop == ' UI Canvas ' :
t . name = t . canvas_name_prop
elif t . type_prop == ' Logic Nodes ' :
if t . node_tree_prop != None :
t . name = t . node_tree_prop . name
# Fetch props
if t . type_prop == ' Bundled Script ' and t . name != ' ' :
file_path = lnx . utils . get_sdk_path ( ) + ' /leenkx/Sources/leenkx/trait/ ' + t . name + ' .hx '
if os . path . exists ( file_path ) :
lnx . utils . fetch_script_props ( file_path )
lnx . utils . fetch_prop ( o )
# Show trait users as collections
if self . is_object :
for col in bpy . data . collections :
if col . name . startswith ( ' Trait| ' ) and o . name in col . objects :
col . objects . unlink ( o )
for t in o . lnx_traitlist :
if ' Trait| ' + t . name not in bpy . data . collections :
col = bpy . data . collections . new ( ' Trait| ' + t . name )
else :
col = bpy . data . collections [ ' Trait| ' + t . name ]
try :
col . objects . link ( o )
except RuntimeError :
# Object is already in that collection. This can
# happen when multiple same traits are copied with
# bpy.ops.lnx.copy_traits_to_active
pass
class LnxTraitListItem ( bpy . types . PropertyGroup ) :
def poll_node_trees ( self , tree : NodeTree ) :
""" Ensure that only logic node trees show up as node traits """
return tree . bl_idname == ' LnxLogicTreeType '
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 )
class_name_prop : StringProperty ( name = " Class " , description = " A name for this item " , default = " " , update = update_trait_group , override = { " LIBRARY_OVERRIDABLE " } )
canvas_name_prop : StringProperty ( name = " Canvas " , description = " A name for this item " , default = " " , update = update_trait_group , override = { " LIBRARY_OVERRIDABLE " } )
webassembly_prop : StringProperty ( name = " Module " , description = " A name for this item " , default = " " , update = update_trait_group , override = { " LIBRARY_OVERRIDABLE " } )
node_tree_prop : PointerProperty ( type = NodeTree , update = update_trait_group , override = { " LIBRARY_OVERRIDABLE " } , poll = poll_node_trees )
lnx_traitpropslist : 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 )
class LNX_UL_TraitList ( bpy . types . UIList ) :
""" List of traits. """
def draw_item ( self , context , layout , data , item , icon , active_data , active_propname , index ) :
layout . use_property_split = False
custom_icon = " NONE "
custom_icon_value = 0
if item . type_prop == " Haxe Script " :
custom_icon_value = ICON_HAXE
elif item . type_prop == " WebAssembly " :
custom_icon_value = ICON_WASM
elif item . type_prop == " UI Canvas " :
custom_icon = " NODE_COMPOSITING "
elif item . type_prop == " Bundled Script " :
custom_icon_value = ICON_BUNDLED
elif item . type_prop == " Logic Nodes " :
custom_icon = ' NODETREE '
if self . layout_type in { ' DEFAULT ' , ' COMPACT ' } :
row = layout . row ( )
row . separator ( factor = 0.1 )
row . prop ( item , " enabled_prop " )
# Display " " for props without a name to right-align the
# fake_user button
row . label ( text = item . name if item . name != " " else " " , icon = custom_icon , icon_value = custom_icon_value )
elif self . layout_type in { ' GRID ' } :
layout . alignment = ' CENTER '
layout . label ( text = " " , icon = custom_icon , icon_value = custom_icon_value )
row = layout . row ( align = True )
row . prop ( item , " fake_user " , text = " " , icon = " FAKE_USER_ON " if item . fake_user else " FAKE_USER_OFF " )
class LnxTraitListNewItem ( bpy . types . Operator ) :
bl_idname = " lnx_traitlist.new_item "
bl_label = " Add Trait "
bl_description = " Add a new trait item to the list "
is_object : BoolProperty ( name = " Is Object Trait " , description = " Whether this trait belongs to an object or a scene " , default = False )
type_prop : EnumProperty ( name = " Type " , items = PROP_TYPES_ENUM )
# Show more options when invoked from the operator search menu
invoked_by_search : BoolProperty ( name = " " , default = True )
def invoke ( self , context , event ) :
wm = context . window_manager
return wm . invoke_props_dialog ( self , width = 400 )
def draw ( self , context ) :
layout = self . layout
if self . invoked_by_search :
row = layout . row ( )
row . prop ( self , " is_object " )
row = layout . row ( )
row . scale_y = 1.3
row . prop ( self , " type_prop " , expand = True )
def execute ( self , context ) :
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
trait = obj . lnx_traitlist . add ( )
trait . is_object = self . is_object
trait . type_prop = self . type_prop
obj . lnx_traitlist_index = len ( obj . lnx_traitlist ) - 1
trigger_recompile ( None , None )
return { ' FINISHED ' }
class LnxTraitListDeleteItem ( bpy . types . Operator ) :
""" Delete the selected item from the list """
bl_idname = " lnx_traitlist.delete_item "
bl_label = " Remove Trait "
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " " , description = " A name for this item " , default = False )
@classmethod
def poll ( self , context ) :
""" Enable if there ' s something in the list """
obj = bpy . context . object
if obj is None :
return False
return len ( obj . lnx_traitlist ) > 0
def execute ( self , context ) :
obj = bpy . context . object
lst = obj . lnx_traitlist
index = obj . lnx_traitlist_index
if len ( lst ) < = index :
return { ' FINISHED ' }
try :
lst . remove ( index )
except TypeError as e :
if obj . override_library is not None :
return { ' CANCELLED ' }
else :
raise e
update_trait_group ( self , context )
if index > 0 :
index = index - 1
obj . lnx_traitlist_index = index
return { ' FINISHED ' }
class LnxTraitListDeleteItemScene ( bpy . types . Operator ) :
""" Delete the selected item from the list """
bl_idname = " lnx_traitlist.delete_item_scene "
bl_label = " Deletes an item "
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " " , description = " A name for this item " , default = False )
@classmethod
def poll ( self , context ) :
""" Enable if there ' s something in the list """
obj = bpy . context . scene
if obj == None :
return False
return len ( obj . lnx_traitlist ) > 0
def execute ( self , context ) :
obj = bpy . context . scene
lst = obj . lnx_traitlist
index = obj . lnx_traitlist_index
if len ( lst ) < = index :
return { ' FINISHED ' }
lst . remove ( index )
if index > 0 :
index = index - 1
obj . lnx_traitlist_index = index
return { ' FINISHED ' }
class LnxTraitListMoveItem ( bpy . types . Operator ) :
""" Move an item in the list """
bl_idname = " lnx_traitlist.move_item "
bl_label = " Move an item in the list "
bl_options = { ' INTERNAL ' }
direction : EnumProperty (
items = (
( ' UP ' , ' Up ' , " " ) ,
( ' DOWN ' , ' Down ' , " " ) , ) )
is_object : BoolProperty ( name = " " , description = " A name for this item " , default = False )
def move_index ( self ) :
# Move index of an item render queue while clamping it
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
index = obj . lnx_traitlist_index
list_length = len ( obj . lnx_traitlist ) - 1
new_index = 0
if self . direction == ' UP ' :
new_index = index - 1
elif self . direction == ' DOWN ' :
new_index = index + 1
new_index = max ( 0 , min ( new_index , list_length ) )
obj . lnx_traitlist . move ( index , new_index )
obj . lnx_traitlist_index = new_index
def execute ( self , context ) :
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
list = obj . lnx_traitlist
index = obj . lnx_traitlist_index
if self . direction == ' DOWN ' :
neighbor = index + 1
self . move_index ( )
elif self . direction == ' UP ' :
neighbor = index - 1
self . move_index ( )
else :
return { ' CANCELLED ' }
return { ' FINISHED ' }
class LnxEditScriptButton ( bpy . types . Operator ) :
bl_idname = ' lnx.edit_script '
bl_label = ' Edit Script '
bl_description = ' Edit script in the text editor '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " " , description = " A name for this item " , default = False )
def execute ( self , context ) :
lnx . utils . check_default_props ( )
if not os . path . exists ( os . path . join ( lnx . utils . get_fp ( ) , " khafile.js " ) ) :
print ( ' Generating Krom project for IDE build configuration ' )
make . build ( ' krom ' )
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
pkg = lnx . utils . safestr ( bpy . data . worlds [ ' Lnx ' ] . lnx_project_package )
# Replace the haxe package syntax with the os-dependent path syntax for opening
hx_path = os . path . join ( lnx . utils . get_fp ( ) , ' Sources ' , pkg , item . class_name_prop . replace ( ' . ' , os . sep ) + ' .hx ' )
lnx . utils . open_editor ( hx_path )
return { ' FINISHED ' }
class LnxEditBundledScriptButton ( bpy . types . Operator ) :
bl_idname = ' lnx.edit_bundled_script '
bl_label = ' Edit Script '
bl_description = ' Copy script to project and edit in the text editor '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( default = False )
def execute ( self , context ) :
if not lnx . utils . check_saved ( self ) :
return { ' CANCELLED ' }
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
sdk_path = lnx . utils . get_sdk_path ( )
project_path = lnx . utils . get_fp ( )
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
pkg = lnx . utils . safestr ( bpy . data . worlds [ ' Lnx ' ] . lnx_project_package )
source_hx_path = os . path . join ( sdk_path , ' leenkx ' , ' Sources ' , ' leenkx ' , ' trait ' , item . class_name_prop + ' .hx ' )
target_dir = os . path . join ( project_path , ' Sources ' , pkg )
target_hx_path = os . path . join ( target_dir , item . class_name_prop + ' .hx ' )
if not os . path . isfile ( target_hx_path ) :
if not os . path . exists ( target_dir ) :
os . makedirs ( target_dir )
# Rewrite package and copy
with open ( source_hx_path , encoding = " utf-8 " ) as sf :
sf . readline ( )
with open ( target_hx_path , ' w ' , encoding = " utf-8 " ) as tf :
tf . write ( ' package ' + pkg + ' ; \n ' )
shutil . copyfileobj ( sf , tf )
lnx . utils . fetch_script_names ( )
# From bundled to script
item . type_prop = ' Haxe Script '
# Open the trait in the code editor
bpy . ops . lnx . edit_script ( ' EXEC_DEFAULT ' , is_object = self . is_object )
return { ' FINISHED ' }
class LnxEditWasmScriptButton ( bpy . types . Operator ) :
bl_idname = ' lnx.edit_wasm_script '
bl_label = ' Edit Script '
bl_description = ' Copy script to project and edit in the text editor '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( default = False )
def execute ( self , context ) :
if not lnx . utils . check_saved ( self ) :
return { ' CANCELLED ' }
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
wasm_path = os . path . join ( lnx . utils . get_fp ( ) , ' Bundled ' , item . webassembly_prop + ' .wasm ' )
lnx . utils . open_editor ( wasm_path )
return { ' FINISHED ' }
class LeenkxGenerateNavmeshButton ( bpy . types . Operator ) :
""" Generate navmesh from selected meshes """
bl_idname = ' lnx.generate_navmesh '
bl_label = ' Generate Navmesh '
def execute ( self , context ) :
obj = context . active_object
if obj . type != ' MESH ' :
return { ' CANCELLED ' }
if not lnx . utils . check_saved ( self ) :
return { " CANCELLED " }
if not lnx . utils . check_sdkpath ( self ) :
return { " CANCELLED " }
print ( " Started visualization generation " )
# Append objects to be included in NavMesh
export_objects = [ ]
# Append Object with trait
export_objects . append ( obj )
# Get NavMesh trait
for trait in obj . lnx_traitlist :
if trait . lnx_traitpropslist and trait . class_name_prop == ' NavMesh ' :
# Check if child objects should be included in NavMesh
prop = trait . lnx_traitpropslist [ ' combineImmidiateChildren ' ]
if ( prop . get_value ( ) ) :
# If yes, check if child is a mesh
for child_obj in obj . children :
if obj . type == ' MESH ' :
# Append child
export_objects . append ( child_obj )
# get dependency graph
depsgraph = bpy . context . evaluated_depsgraph_get ( )
# Get build directory
nav_full_path = lnx . utils . get_fp_build ( ) + ' /compiled/Assets/navigation '
if not os . path . exists ( nav_full_path ) :
os . makedirs ( nav_full_path )
# Get export OBJ name and path
nav_mesh_name = ' nav_ ' + obj . data . name
mesh_path = nav_full_path + ' / ' + nav_mesh_name + ' .obj '
# Max index of objects (vertices) traversed
max_overall_index = 0
# Open to OBJ file
with open ( mesh_path , ' w ' ) as f :
for export_obj in export_objects :
# If armature, apply armature modifier
armature = export_obj . find_armature ( )
apply_modifiers = not armature
obj_eval = export_obj . evaluated_get ( depsgraph ) if apply_modifiers else export_obj
# Get mesh data
export_mesh = obj_eval . to_mesh ( )
# Get world transform
world_matrix = obj_eval . matrix_world
# Iterate over the triangles and get vertices and indices
triangles = export_mesh . loop_triangles
traversed_indices = [ ]
# For each triangle in the object
for triangle in triangles :
# For each index in triangle
for loop_index in triangle . loops :
# Get vertex index
vertex_index = export_mesh . loops [ loop_index ] . vertex_index
# Skip if vertex already appended
if ( vertex_index not in traversed_indices ) :
# If not, append vertex
traversed_indices . append ( vertex_index )
vertex = export_mesh . vertices [ vertex_index ] . co
# Apply world transform
tv = world_matrix @ vertex
# Write to OBJ
f . write ( " v %.4f " % ( tv [ 0 ] ) )
f . write ( " %.4f " % ( tv [ 2 ] ) )
f . write ( " %.4f \n " % ( tv [ 1 ] ) ) # Flipped
# Max index of this object
max_index = 0
# For each triangle in the object
for triangle in triangles :
# Write index to OBJ
f . write ( " f " )
for loop_index in triangle . loops :
# index of this object should be > index of previous objects
curr_index = max_overall_index + loop_index + 1
f . write ( " %d " % ( curr_index ) )
if ( curr_index > max_overall_index ) :
max_index = curr_index
f . write ( " \n " )
# Store max overall index
max_overall_index = max_index
# Get buildnavjs
buildnavjs_path = lnx . utils . get_sdk_path ( ) + ' /lib/haxerecast/buildnavjs '
# append config values
nav_config = { }
for trait in obj . lnx_traitlist :
# check if trait is navmesh here
if trait . lnx_traitpropslist and trait . class_name_prop == ' NavMesh ' :
for prop in trait . lnx_traitpropslist : # Append props
name = prop . name
value = prop . get_value ( )
nav_config [ name ] = value
nav_config_json = json . dumps ( nav_config )
args = [ lnx . utils . get_node_path ( ) , buildnavjs_path , nav_mesh_name , nav_config_json ]
proc = subprocess . Popen ( args , cwd = nav_full_path )
proc . wait ( )
2025-06-20 15:50:09 +00:00
if bpy . app . version [ 0 ] > = 4 :
navmesh = bpy . ops . wm . obj_import ( filepath = mesh_path )
else :
navmesh = bpy . ops . import_scene . obj ( filepath = mesh_path )
2025-01-22 16:18:30 +01:00
navmesh = bpy . context . selected_objects [ 0 ]
# NavMesh preview settings, cleanup
navmesh . name = nav_mesh_name
navmesh . rotation_euler = ( 0 , 0 , 0 )
navmesh . location = ( 0 , 0 , 0 )
navmesh . lnx_export = False
bpy . context . view_layer . objects . active = navmesh
bpy . ops . object . editmode_toggle ( )
bpy . ops . mesh . select_all ( action = ' SELECT ' )
bpy . ops . mesh . remove_doubles ( )
bpy . ops . object . editmode_toggle ( )
obj_eval . to_mesh_clear ( )
print ( " Finished visualization generation " )
return { ' FINISHED ' }
class LnxEditCanvasButton ( bpy . types . Operator ) :
bl_idname = ' lnx.edit_canvas '
bl_label = ' Edit Canvas '
bl_description = ' Edit UI Canvas '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " " , description = " A name for this item " , default = False )
def execute ( self , context ) :
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
project_path = lnx . utils . get_fp ( )
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
canvas_path = project_path + ' /Bundled/canvas/ ' + item . canvas_name_prop + ' .json '
sdk_path = lnx . utils . get_sdk_path ( )
ext = ' d3d11 ' if lnx . utils . get_os ( ) == ' win ' else ' opengl '
leenkx2d_path = sdk_path + ' /lib/leenkx_tools/leenkx2d/ ' + ext
krom_location , krom_path = lnx . utils . krom_paths ( )
os . chdir ( krom_location )
cpath = canvas_path . replace ( ' \\ ' , ' / ' )
uiscale = str ( lnx . utils . get_ui_scale ( ) )
cmd = [ krom_path , leenkx2d_path , leenkx2d_path , cpath , uiscale ]
if lnx . utils . get_os ( ) == ' win ' :
cmd . append ( ' --consolepid ' )
cmd . append ( str ( os . getpid ( ) ) )
subprocess . Popen ( cmd )
return { ' FINISHED ' }
class LnxNewScriptDialog ( bpy . types . Operator ) :
bl_idname = " lnx.new_script "
bl_label = " New Script "
bl_description = ' Create a blank script '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " Object trait " , description = " Is this an object trait? " , default = False )
class_name : StringProperty ( name = " Name " , description = " The class name " )
def execute ( self , context ) :
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
self . class_name = self . class_name . replace ( ' ' , ' ' )
write_data . write_traithx ( self . class_name )
lnx . utils . fetch_script_names ( )
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
item . class_name_prop = self . class_name
return { ' FINISHED ' }
def invoke ( self , context , event ) :
if not lnx . utils . check_saved ( self ) :
return { ' CANCELLED ' }
self . class_name = ' MyTrait '
return context . window_manager . invoke_props_dialog ( self )
def draw ( self , context ) :
self . layout . prop ( self , " class_name " )
class LnxNewTreeNodeDialog ( bpy . types . Operator ) :
bl_idname = " lnx.new_treenode "
bl_label = " New Node Tree "
bl_description = ' Create a blank Node Tree '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " Object Node Tree " , description = " Is this an object Node Tree? " , default = False )
class_name : StringProperty ( name = " Name " , description = " The Node Tree name " )
def execute ( self , context ) :
if self . is_object :
obj = context . object
else :
obj = context . scene
self . class_name = self . class_name . replace ( ' ' , ' ' )
# Create new node tree
node_tree = bpy . data . node_groups . new ( self . class_name , ' LnxLogicTreeType ' )
# Set new node tree
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
if item . node_tree_prop is None :
item . node_tree_prop = node_tree
return { ' FINISHED ' }
def invoke ( self , context , event ) :
if not lnx . utils . check_saved ( self ) :
return { ' CANCELLED ' }
self . class_name = ' MyNodeTree '
return context . window_manager . invoke_props_dialog ( self )
def draw ( self , context ) :
self . layout . prop ( self , " class_name " )
class LnxEditTreeNodeDialog ( bpy . types . Operator ) :
bl_idname = " lnx.edit_treenode "
bl_label = " Edit Node Tree "
bl_description = ' Edit this Node Tree in the Logic Node Editor '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " Object Node Tree " , description = " Is this an object Node Tree? " , default = False )
def execute ( self , context ) :
if self . is_object :
obj = context . object
else :
obj = context . scene
# Check len node tree list
if len ( obj . lnx_traitlist ) > 0 :
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
# Loop for all spaces
context_screen = context . screen
if item is not None and context_screen is not None :
areas = context_screen . areas
for area in areas :
for space in area . spaces :
if space . type == ' NODE_EDITOR ' :
if space . tree_type == ' LnxLogicTreeType ' :
# Set Node Tree
space . node_tree = item . node_tree_prop
return { ' FINISHED ' }
class LnxGetTreeNodeDialog ( bpy . types . Operator ) :
bl_idname = " lnx.get_treenode "
bl_label = " From Node Editor "
bl_description = ' Use the Node Tree from the opened Node Tree Editor for this trait '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " Object Node Tree " , description = " Is this an object Node Tree? " , default = False )
def execute ( self , context ) :
if self . is_object :
obj = context . object
else :
obj = context . scene
# Check len node tree list
if len ( obj . lnx_traitlist ) > 0 :
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
# Loop for all spaces
context_screen = context . screen
if item is not None and context_screen is not None :
areas = context_screen . areas
for area in areas :
for space in area . spaces :
if space . type == ' NODE_EDITOR ' :
if space . tree_type == ' LnxLogicTreeType ' and space . node_tree is not None :
# Set Node Tree in Item
item . node_tree_prop = space . node_tree
return { ' FINISHED ' }
class LnxNewCanvasDialog ( bpy . types . Operator ) :
bl_idname = " lnx.new_canvas "
bl_label = " New Canvas "
bl_description = ' Create a blank canvas '
bl_options = { ' INTERNAL ' }
is_object : BoolProperty ( name = " Object trait " , description = " Is this an object trait? " , default = False )
canvas_name : StringProperty ( name = " Name " , description = " The canvas name " )
def execute ( self , context ) :
if self . is_object :
obj = bpy . context . object
else :
obj = bpy . context . scene
self . canvas_name = self . canvas_name . replace ( ' ' , ' ' )
write_data . write_canvasjson ( self . canvas_name )
lnx . utils . fetch_script_names ( )
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
item . canvas_name_prop = self . canvas_name
return { ' FINISHED ' }
def invoke ( self , context , event ) :
if not lnx . utils . check_saved ( self ) :
return { ' CANCELLED ' }
self . canvas_name = ' MyCanvas '
return context . window_manager . invoke_props_dialog ( self )
def draw ( self , context ) :
self . layout . prop ( self , " canvas_name " )
class LnxNewWasmButton ( bpy . types . Operator ) :
""" Create new WebAssembly module """
bl_idname = ' lnx.new_wasm '
bl_label = ' New Module '
def execute ( self , context ) :
webbrowser . open ( ' https://esmbly.github.io/WebAssemblyStudio/ ' )
return { ' FINISHED ' }
class LnxRefreshScriptsButton ( bpy . types . Operator ) :
""" Fetch all script names and trait properties. """
bl_idname = ' lnx.refresh_scripts '
bl_label = ' Refresh Traits '
poll_msg = (
" Cannot refresh scripts for overrides at the moment due to "
" Blender limitations. Please use the ' Refresh ' operator in "
" the linked file. "
)
def execute ( self , context ) :
lnx . utils . fetch_bundled_script_names ( )
lnx . utils . fetch_bundled_trait_props ( )
lnx . utils . fetch_script_names ( )
lnx . utils . fetch_trait_props ( )
lnx . utils . fetch_wasm_names ( )
return { ' FINISHED ' }
class LnxRefreshObjectScriptsButton ( bpy . types . Operator ) :
""" Fetch all script names and trait properties. """
bl_idname = ' lnx.refresh_object_scripts '
bl_label = ' Refresh Traits '
bl_options = { ' INTERNAL ' }
@classmethod
def poll ( cls , context ) :
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
# can recognize why refreshing doesn't work.
return context . object . override_library is None
def execute ( self , context ) :
return LnxRefreshScriptsButton . execute ( self , context )
class LnxRefreshCanvasListButton ( bpy . types . Operator ) :
""" Fetch all canvas names """
bl_idname = ' lnx.refresh_canvas_list '
bl_label = ' Refresh Canvas Traits '
def execute ( self , context ) :
lnx . utils . fetch_script_names ( )
return { ' FINISHED ' }
class LNX_PT_TraitPanel ( bpy . types . Panel ) :
bl_label = " Leenkx Traits "
bl_space_type = " PROPERTIES "
bl_region_type = " WINDOW "
bl_context = " object "
def draw ( self , context ) :
obj = bpy . context . object
draw_traits_panel ( self . layout , obj , is_object = True )
class LNX_PT_SceneTraitPanel ( bpy . types . Panel ) :
bl_label = " Leenkx Scene Traits "
bl_space_type = " PROPERTIES "
bl_region_type = " WINDOW "
bl_context = " scene "
def draw ( self , context ) :
obj = bpy . context . scene
draw_traits_panel ( self . layout , obj , is_object = False )
class LNX_OT_RemoveTraitsFromActiveObjects ( bpy . types . Operator ) :
bl_label = ' Remove Traits From Selected Objects '
bl_idname = ' lnx.remove_traits_from_active_objects '
bl_description = ' Removes all traits from all selected objects '
@classmethod
def poll ( cls , context ) :
return context . mode != ' SCENE ' and len ( context . selected_objects ) > 0
def execute ( self , context ) :
for obj in bpy . context . selected_objects :
obj . lnx_traitlist . clear ( )
obj . lnx_traitlist_index = 0
return { " FINISHED " }
class LNX_OT_CopyTraitsFromActive ( bpy . types . Operator ) :
bl_label = ' Copy Traits from Active Object '
bl_idname = ' lnx.copy_traits_to_active '
bl_description = ' Copies the traits of the active object to all other selected objects '
overwrite : BoolProperty ( name = " Overwrite " , default = True )
@classmethod
def poll ( cls , context ) :
return context . active_object is not None and len ( context . selected_objects ) > 1
def draw_message_box ( self , context ) :
layout = self . layout
layout = layout . column ( align = True )
layout . alignment = ' EXPAND '
layout . label ( text = ' Warning: At least one target object already has ' , icon = ' ERROR ' )
layout . label ( text = ' traits assigned to it! ' , icon = ' BLANK1 ' )
layout . separator ( )
layout . label ( text = ' Do you want to overwrite the already existing traits ' , icon = ' BLANK1 ' )
layout . label ( text = ' or append to them? ' , icon = ' BLANK1 ' )
layout . separator ( )
row = layout . row ( align = True )
row . active_default = True
row . operator ( ' lnx.copy_traits_to_active ' , text = ' Overwrite ' ) . overwrite = True
row . active_default = False
row . operator ( ' lnx.copy_traits_to_active ' , text = ' Append ' ) . overwrite = False
row . operator ( ' lnx.discard_popup ' , text = ' Cancel ' )
def execute ( self , context ) :
source_obj = bpy . context . active_object
for target_obj in bpy . context . selected_objects :
if source_obj == target_obj :
continue
# Offset for trait iteration when appending traits
offset = 0
if not self . overwrite :
offset = len ( target_obj . lnx_traitlist )
lnx . utils . merge_into_collection (
source_obj . lnx_traitlist , target_obj . lnx_traitlist , clear_dst = self . overwrite )
for i in range ( len ( source_obj . lnx_traitlist ) ) :
lnx . utils . merge_into_collection (
source_obj . lnx_traitlist [ i ] . lnx_traitpropslist ,
target_obj . lnx_traitlist [ i + offset ] . lnx_traitpropslist
)
return { " FINISHED " }
def invoke ( self , context , event ) :
show_dialog = False
# Test if there is a target object which has traits that would
# get overwritten
source_obj = bpy . context . active_object
for target_object in bpy . context . selected_objects :
if source_obj == target_object :
continue
else :
if target_object . lnx_traitlist :
show_dialog = True
if show_dialog :
context . window_manager . popover ( self . __class__ . draw_message_box , ui_units_x = 16 )
else :
bpy . ops . lnx . copy_traits_to_active ( )
return { ' INTERFACE ' }
class LNX_MT_context_menu ( Menu ) :
bl_label = " Trait Specials "
def draw ( self , _context ) :
layout = self . layout
layout . operator ( " lnx.copy_traits_to_active " , icon = ' PASTEDOWN ' )
layout . operator ( " lnx.remove_traits_from_active_objects " , icon = ' REMOVE ' )
layout . operator ( " lnx.print_traits " , icon = ' CONSOLE ' )
def draw_traits_panel ( layout : bpy . types . UILayout , obj : Union [ bpy . types . Object , bpy . types . Scene ] , is_object : bool ) - > None :
layout . use_property_split = True
layout . use_property_decorate = False
# Make the list bigger when there are a few traits
num_rows = 2
if len ( obj . lnx_traitlist ) > 1 :
num_rows = 4
row = layout . row ( )
row . template_list ( " LNX_UL_TraitList " , " The_List " , obj , " lnx_traitlist " , obj , " lnx_traitlist_index " , rows = num_rows )
col = row . column ( align = True )
op = col . operator ( " lnx_traitlist.new_item " , icon = ' ADD ' , text = " " )
op . invoked_by_search = False
op . is_object = is_object
if is_object :
op = col . operator ( " lnx_traitlist.delete_item " , icon = ' REMOVE ' , text = " " )
else :
op = col . operator ( " lnx_traitlist.delete_item_scene " , icon = ' REMOVE ' , text = " " )
op . is_object = is_object
col . separator ( )
col . menu ( " LNX_MT_context_menu " , icon = ' DOWNARROW_HLT ' , text = " " )
if len ( obj . lnx_traitlist ) > 1 :
col . separator ( )
op = col . operator ( " lnx_traitlist.move_item " , icon = ' TRIA_UP ' , text = " " )
op . direction = ' UP '
op . is_object = is_object
op = col . operator ( " lnx_traitlist.move_item " , icon = ' TRIA_DOWN ' , text = " " )
op . direction = ' DOWN '
op . is_object = is_object
# Draw trait specific content
if obj . lnx_traitlist_index > = 0 and len ( obj . lnx_traitlist ) > 0 :
item = obj . lnx_traitlist [ obj . lnx_traitlist_index ]
row = layout . row ( align = True )
row . alignment = ' EXPAND '
row . scale_y = 1.2
if item . type_prop == ' Haxe Script ' or item . type_prop == ' Bundled Script ' :
if item . type_prop == ' Haxe Script ' :
row . operator ( " lnx.new_script " , icon = " FILE_NEW " ) . is_object = is_object
column = row . column ( align = True )
column . enabled = item . class_name_prop != ' '
column . operator ( " lnx.edit_script " , icon_value = ICON_HAXE ) . is_object = is_object
# Bundled scripts
else :
if item . class_name_prop == ' NavMesh ' :
row . operator ( " lnx.generate_navmesh " , icon = " UV_VERTEXSEL " )
else :
row . enabled = item . class_name_prop != ' '
row . operator ( " lnx.edit_bundled_script " , icon_value = ICON_HAXE ) . is_object = is_object
refresh_op = " lnx.refresh_object_scripts " if is_object else " lnx.refresh_scripts "
row . operator ( refresh_op , text = " Refresh " , icon = " FILE_REFRESH " )
# Default props
row = layout . row ( )
if item . type_prop == ' Haxe Script ' :
row . prop_search ( item , " class_name_prop " , bpy . data . worlds [ ' Lnx ' ] , " lnx_scripts_list " , text = " Class " )
else :
row . prop_search ( item , " class_name_prop " , bpy . data . worlds [ ' Lnx ' ] , " lnx_bundled_scripts_list " , text = " Class " )
elif item . type_prop == ' WebAssembly ' :
row . operator ( " lnx.new_wasm " , icon = " FILE_NEW " )
column = row . column ( align = True )
column . enabled = item . webassembly_prop != ' '
column . operator ( " lnx.edit_wasm_script " , icon_value = ICON_WASM ) . is_object = is_object
refresh_op = " lnx.refresh_object_scripts " if is_object else " lnx.refresh_scripts "
row . operator ( refresh_op , text = " Refresh " , icon = " FILE_REFRESH " )
row = layout . row ( )
row . prop_search ( item , " webassembly_prop " , bpy . data . worlds [ ' Lnx ' ] , " lnx_wasm_list " , text = " Module " )
elif item . type_prop == ' UI Canvas ' :
row . operator ( " lnx.new_canvas " , icon = " FILE_NEW " ) . is_object = is_object
column = row . column ( align = True )
column . enabled = item . canvas_name_prop != ' '
column . operator ( " lnx.edit_canvas " , icon = " NODE_COMPOSITING " ) . is_object = is_object
refresh_op = " lnx.refresh_object_scripts " if is_object else " lnx.refresh_scripts "
row . operator ( refresh_op , text = " Refresh " , icon = " FILE_REFRESH " )
row = layout . row ( )
row . prop_search ( item , " canvas_name_prop " , bpy . data . worlds [ ' Lnx ' ] , " lnx_canvas_list " , text = " Canvas " )
elif item . type_prop == ' Logic Nodes ' :
# Check if there is at least one active Logic Node Editor
is_editor_active = False
if bpy . context . screen is not None :
areas = bpy . context . screen . areas
for area in areas :
for space in area . spaces :
if space . type == ' NODE_EDITOR ' :
if space . tree_type == ' LnxLogicTreeType ' and space . node_tree is not None :
is_editor_active = True
break
if is_editor_active :
break
row . operator ( " lnx.new_treenode " , text = " New Tree " , icon = " ADD " ) . is_object = is_object
column = row . column ( align = True )
column . enabled = is_editor_active and item . node_tree_prop is not None
column . operator ( " lnx.edit_treenode " , text = " Edit Tree " , icon = " NODETREE " ) . is_object = is_object
column = row . column ( align = True )
column . enabled = is_editor_active and item is not None
column . operator ( " lnx.get_treenode " , text = " From Editor " , icon = " IMPORT " ) . is_object = is_object
row = layout . row ( )
row . prop_search ( item , " node_tree_prop " , bpy . data , " node_groups " , text = " Tree " )
# =====================
# Draw trait properties
if ( item . type_prop == ' Haxe Script ' or item . type_prop == ' Bundled Script ' ) and item . class_name_prop != ' ' :
if item . lnx_traitpropslist :
layout . label ( text = " Trait Properties: " )
if item . lnx_traitpropswarnings :
box = layout . box ( )
box . label ( text = f " Warnings ( { len ( item . lnx_traitpropswarnings ) } ): " , icon = " ERROR " )
col = box . column ( align = True )
for warning in item . lnx_traitpropswarnings :
col . label ( text = f ' " { warning . propName } " : { warning . warning } ' )
propsrows = max ( len ( item . lnx_traitpropslist ) , 6 )
row = layout . row ( )
row . template_list ( " LNX_UL_PropList " , " The_List " , item , " lnx_traitpropslist " , item , " lnx_traitpropslist_index " , rows = propsrows )
__REG_CLASSES = (
LnxTraitListItem ,
LNX_UL_TraitList ,
LnxTraitListNewItem ,
LnxTraitListDeleteItem ,
LnxTraitListDeleteItemScene ,
LnxTraitListMoveItem ,
LnxEditScriptButton ,
LnxEditBundledScriptButton ,
LnxEditWasmScriptButton ,
LeenkxGenerateNavmeshButton ,
LnxEditCanvasButton ,
LnxNewScriptDialog ,
LnxNewTreeNodeDialog ,
LnxEditTreeNodeDialog ,
LnxGetTreeNodeDialog ,
LnxNewCanvasDialog ,
LnxNewWasmButton ,
LnxRefreshScriptsButton ,
LnxRefreshObjectScriptsButton ,
LnxRefreshCanvasListButton ,
LNX_PT_TraitPanel ,
LNX_PT_SceneTraitPanel ,
LNX_OT_CopyTraitsFromActive ,
LNX_MT_context_menu ,
LNX_OT_RemoveTraitsFromActiveObjects
)
__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 " } )