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() navmesh = bpy.ops.import_scene.obj(filepath=mesh_path) 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"})