import json import os import sys import shutil import textwrap import mathutils import bpy from bpy.props import * from lnx.lightmapper.panels import scene import lnx.api import lnx.assets as assets from lnx.exporter import LeenkxExporter import lnx.log as log import lnx.logicnode.replacement import lnx.make as make import lnx.make_state as state import lnx.props as props import lnx.props_properties import lnx.props_traits import lnx.nodes_logic import lnx.ui_icons as ui_icons import lnx.utils import lnx.utils_vs import lnx.write_probes if lnx.is_reload(__name__): lnx.api = lnx.reload_module(lnx.api) assets = lnx.reload_module(assets) lnx.exporter = lnx.reload_module(lnx.exporter) from lnx.exporter import LeenkxExporter log = lnx.reload_module(log) lnx.logicnode.replacement = lnx.reload_module(lnx.logicnode.replacement) make = lnx.reload_module(make) state = lnx.reload_module(state) props = lnx.reload_module(props) lnx.props_properties = lnx.reload_module(lnx.props_properties) lnx.props_traits = lnx.reload_module(lnx.props_traits) lnx.nodes_logic = lnx.reload_module(lnx.nodes_logic) ui_icons = lnx.reload_module(ui_icons) lnx.utils = lnx.reload_module(lnx.utils) lnx.utils_vs = lnx.reload_module(lnx.utils_vs) lnx.write_probes = lnx.reload_module(lnx.write_probes) else: lnx.enable_reload(__name__) class LNX_PT_ObjectPropsPanel(bpy.types.Panel): """Menu in object region.""" bl_label = "Leenkx Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.object if obj == None: return col = layout.column() col.prop(obj, 'lnx_export') if not obj.lnx_export: return col.prop(obj, 'lnx_spawn') col.prop(obj, 'lnx_mobile') col.prop(obj, 'lnx_animation_enabled') col.prop(obj, 'lnx_visible_shadow') if obj.type == 'MESH': layout.prop(obj, 'lnx_instanced') wrd = bpy.data.worlds['Lnx'] layout.prop_search(obj, "lnx_tilesheet", wrd, "lnx_tilesheetlist", text="Tilesheet") if obj.lnx_tilesheet != '': selected_ts = None for ts in wrd.lnx_tilesheetlist: if ts.name == obj.lnx_tilesheet: selected_ts = ts break layout.prop_search(obj, "lnx_tilesheet_action", selected_ts, "lnx_tilesheetactionlist", text="Action") layout.prop(obj, "lnx_use_custom_tilesheet_node") # Properties list lnx.props_properties.draw_properties(layout, obj) # Lightmapping props if obj.type == "MESH": row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_use") if obj.TLM_ObjectProperties.tlm_mesh_lightmap_use: row = layout.row() row.prop(obj.TLM_ObjectProperties, "tlm_use_default_channel") if not obj.TLM_ObjectProperties.tlm_use_default_channel: row = layout.row() row.prop_search(obj.TLM_ObjectProperties, "tlm_uv_channel", obj.data, "uv_layers", text='UV Channel') row = layout.row() row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_resolution") if obj.TLM_ObjectProperties.tlm_use_default_channel: row = layout.row() row.prop(obj.TLM_ObjectProperties, "tlm_mesh_lightmap_unwrap_mode") row = layout.row() if obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode == "AtlasGroupA": if scene.TLM_AtlasListItem >= 0 and len(scene.TLM_AtlasList) > 0: row = layout.row() item = scene.TLM_AtlasList[scene.TLM_AtlasListItem] row.prop_search(obj.TLM_ObjectProperties, "tlm_atlas_pointer", scene, "TLM_AtlasList", text='Atlas Group') row = layout.row() else: row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") row = layout.row() else: row = layout.row() row.prop(obj.TLM_ObjectProperties, "tlm_postpack_object") row = layout.row() if obj.TLM_ObjectProperties.tlm_postpack_object and obj.TLM_ObjectProperties.tlm_mesh_lightmap_unwrap_mode != "AtlasGroupA": if scene.TLM_PostAtlasListItem >= 0 and len(scene.TLM_PostAtlasList) > 0: row = layout.row() item = scene.TLM_PostAtlasList[scene.TLM_PostAtlasListItem] row.prop_search(obj.TLM_ObjectProperties, "tlm_postatlas_pointer", scene, "TLM_PostAtlasList", text='Atlas Group') row = layout.row() else: row = layout.label(text="Add Atlas Groups from the scene lightmapping settings.") row = layout.row() row.prop(obj.TLM_ObjectProperties, "tlm_mesh_unwrap_margin") row = layout.row() row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filter_override") row = layout.row() if obj.TLM_ObjectProperties.tlm_mesh_filter_override: row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_mode") row = layout.row(align=True) if obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Gaussian": row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_gaussian_strength") row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Box": row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_box_strength") row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") elif obj.TLM_ObjectProperties.tlm_mesh_filtering_mode == "Bilateral": row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_diameter") row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_color_deviation") row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_bilateral_coordinate_deviation") row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") else: row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_median_kernel", expand=True) row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_mesh_filtering_iterations") #If UV Packer installed if "UV-Packer" in bpy.context.preferences.addons.keys(): row.prop(obj.TLM_ObjectProperties, "tlm_use_uv_packer") if obj.TLM_ObjectProperties.tlm_use_uv_packer: row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_uv_packer_padding") row = layout.row(align=True) row.prop(obj.TLM_ObjectProperties, "tlm_uv_packer_packing_engine") class LNX_PT_ModifiersPropsPanel(bpy.types.Panel): bl_label = "Leenkx Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "modifier" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.object if obj == None: return layout.operator("lnx.invalidate_cache") class LNX_PT_ParticlesPropsPanel(bpy.types.Panel): bl_label = "Leenkx Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "particle" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.particle_system if obj == None: return layout.prop(obj.settings, 'lnx_loop') layout.prop(obj.settings, 'lnx_count_mult') class LNX_PT_PhysicsPropsPanel(bpy.types.Panel): bl_label = "Leenkx Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "physics" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.object if obj == None: return rb = obj.rigid_body if rb is not None: col = layout.column() row = col.row() row.alignment = 'RIGHT' rb_type = 'Dynamic' if LeenkxExporter.rigid_body_static(rb): rb_type = 'Static' if rb.kinematic: rb_type = 'Kinematic' row.label(text=(f'Rigid Body Export Type: {rb_type}'), icon='AUTO') layout.prop(obj, 'lnx_rb_linear_factor') layout.prop(obj, 'lnx_rb_angular_factor') layout.prop(obj, 'lnx_rb_angular_friction') layout.prop(obj, 'lnx_rb_trigger') layout.prop(obj, 'lnx_rb_ccd') if obj.soft_body is not None: layout.prop(obj, 'lnx_soft_body_margin') if obj.rigid_body_constraint is not None: layout.prop(obj, 'lnx_relative_physics_constraint') class LNX_OT_AddArmatureRootMotion(bpy.types.Operator): bl_idname = "lnx.add_root_motion_bone" bl_label = "Add root bone" bl_description = "Add a new bone and set it as the root bone. This may be used for root motion." @classmethod def poll(cls, context): obj = context.object if obj.mode == 'EDIT': return False armature = obj.data if armature.bones: if armature.bones.active: return True def execute(self, context): obj = context.object current_mode = obj.mode bpy.ops.object.mode_set(mode='EDIT') armature = obj.data edit_bones = armature.edit_bones current_root_pos = edit_bones.active.head current_root_len = edit_bones.active.length new_root = edit_bones.new("LeenkxRoot") new_root.head = current_root_pos new_root.tail = new_root.head + mathutils.Vector((0.0, 0.0, current_root_len)) bpy.ops.object.mode_set(mode=current_mode) return{'FINISHED'} # Menu in data region class LNX_PT_DataPropsPanel(bpy.types.Panel): bl_label = "Leenkx Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "data" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.object if obj == None: return wrd = bpy.data.worlds['Lnx'] if obj.type == 'CAMERA': layout.prop(obj.data, 'lnx_frustum_culling') elif obj.type == 'MESH' or obj.type == 'FONT' or obj.type == 'META': layout.prop(obj.data, 'lnx_dynamic_usage') layout.operator("lnx.invalidate_cache") elif obj.type == 'LIGHT': layout.prop(obj.data, 'lnx_clip_start') layout.prop(obj.data, 'lnx_clip_end') layout.prop(obj.data, 'lnx_fov') layout.prop(obj.data, 'lnx_shadows_bias') layout.prop(wrd, 'lnx_light_ies_texture') layout.prop(wrd, 'lnx_light_clouds_texture') elif obj.type == 'SPEAKER': layout.prop(obj.data, 'lnx_play_on_start') layout.prop(obj.data, 'lnx_loop') layout.prop(obj.data, 'lnx_stream') elif obj.type == 'ARMATURE': layout.prop(obj.data, 'lnx_autobake') layout.prop(obj.data, 'lnx_relative_bone_constraints') if obj.data.bones: if obj.data.bones.active: layout.label(text='Current Root: ' + obj.data.bones.active.name) layout.operator('lnx.add_root_motion_bone', text='Add new root bone', icon='ADD') pass class LNX_PT_WorldPropsPanel(bpy.types.Panel): bl_label = "Leenkx World Properties" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "world" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False world = context.world if world is None: return layout.prop(world, 'lnx_use_clouds') col = layout.column(align=True) col.enabled = world.lnx_use_clouds col.prop(world, 'lnx_darken_clouds') col.prop(world, 'lnx_clouds_lower') col.prop(world, 'lnx_clouds_upper') col.prop(world, 'lnx_clouds_precipitation') col.prop(world, 'lnx_clouds_secondary') col.prop(world, 'lnx_clouds_wind') col.prop(world, 'lnx_clouds_steps') class LNX_PT_ScenePropsPanel(bpy.types.Panel): bl_label = "Leenkx Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False scene = bpy.context.scene if scene == None: return row = layout.row() column = row.column() row.prop(scene, 'lnx_export') class InvalidateCacheButton(bpy.types.Operator): """Delete cached mesh data""" bl_idname = "lnx.invalidate_cache" bl_label = "Invalidate Cache" def execute(self, context): context.object.data.lnx_cached = False return{'FINISHED'} class InvalidateMaterialCacheButton(bpy.types.Operator): """Delete cached material data""" bl_idname = "lnx.invalidate_material_cache" bl_label = "Invalidate Cache" def execute(self, context): context.material.lnx_cached = False context.material.signature = '' return{'FINISHED'} class LNX_OT_NewCustomMaterial(bpy.types.Operator): bl_idname = "lnx.new_custom_material" bl_label = "New Custom Material" bl_description = "Add a new custom material. This will create all the necessary files and folders" def poll_mat_name(self, context): project_dir = lnx.utils.get_fp() shader_dir_dst = os.path.join(project_dir, 'Shaders') mat_name = lnx.utils.safestr(self.mat_name) self.mat_exists = os.path.isdir(os.path.join(project_dir, 'Bundled', mat_name)) vert_exists = os.path.isfile(os.path.join(shader_dir_dst, f'{mat_name}.vert.glsl')) frag_exists = os.path.isfile(os.path.join(shader_dir_dst, f'{mat_name}.frag.glsl')) self.shader_exists = vert_exists or frag_exists mat_name: StringProperty( name='Material Name', description='The name of the new material', default='MyCustomMaterial', update=poll_mat_name) mode: EnumProperty( name='Target RP', description='Choose for which render path mode the new material is created', default='deferred', items=[('deferred', 'Deferred', 'Create the material for a deferred render path'), ('forward', 'Forward', 'Create the material for a forward render path')]) mat_exists: BoolProperty( name='Material Already Exists', default=False, options={'HIDDEN', 'SKIP_SAVE'}) shader_exists: BoolProperty( name='Shaders Already Exist', default=False, options={'HIDDEN', 'SKIP_SAVE'}) def invoke(self, context, event): if not bpy.data.is_saved: self.report({'INFO'}, "Please save your file first") return {"CANCELLED"} # Try to set deferred/forward based on the selected render path try: self.mode = 'forward' if lnx.utils.get_rp().rp_renderer == 'Forward' else 'deferred' except IndexError: # No render path, use default (deferred) pass self.poll_mat_name(context) wm = context.window_manager return wm.invoke_props_dialog(self, width=300) def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False layout.prop(self, 'mat_name') layout.prop(self, 'mode', expand=True) if self.mat_exists: box = layout.box() box.alert = True col = box.column(align=True) col.label(text='A custom material with that name already exists,', icon='ERROR') col.label(text='clicking on \'OK\' will override the material!', icon='BLANK1') if self.shader_exists: box = layout.box() box.alert = True col = box.column(align=True) col.label(text='Shader file(s) with that name already exists,', icon='ERROR') col.label(text='clicking on \'OK\' will override the shader(s)!', icon='BLANK1') def execute(self, context): if self.mat_name == '': return {'CANCELLED'} project_dir = lnx.utils.get_fp() shader_dir_src = os.path.join(lnx.utils.get_sdk_path(), 'leenkx', 'Shaders', 'custom_mat_presets') shader_dir_dst = os.path.join(project_dir, 'Shaders') mat_name = lnx.utils.safestr(self.mat_name) mat_dir = os.path.join(project_dir, 'Bundled', mat_name) os.makedirs(mat_dir, exist_ok=True) os.makedirs(shader_dir_dst, exist_ok=True) # Shader data if self.mode == 'forward': col_attachments = ['RGBA64'] constants = [{'link': '_worldViewProjectionMatrix', 'name': 'WVP', 'type': 'mat4'}] vertex_elems = [{'name': 'pos', 'data': 'short4norm'}] else: col_attachments = ['RGBA64', 'RGBA64'] constants = [ {'link': '_worldViewProjectionMatrix', 'name': 'WVP', 'type': 'mat4'}, {'link': '_normalMatrix', 'name': 'N', 'type': 'mat3'} ] vertex_elems = [ {'name': 'pos', 'data': 'short4norm'}, {'name': 'nor', 'data': 'short2norm'} ] con = { 'color_attachments': col_attachments, 'compare_mode': 'less', 'constants': constants, 'cull_mode': 'clockwise', 'depth_write': True, 'fragment_shader': f'{mat_name}.frag', 'name': 'mesh', 'texture_units': [], 'vertex_shader': f'{mat_name}.vert', 'vertex_elements': vertex_elems } data = { 'shader_datas': [{ 'contexts': [con], 'name': f'{mat_name}' }] } # Save shader data file with open(os.path.join(mat_dir, f'{mat_name}.json'), 'w') as datafile: json.dump(data, datafile, indent=4, sort_keys=True) # Copy preset shaders to project if self.mode == 'forward': shutil.copy(os.path.join(shader_dir_src, 'custom_mat_forward.frag.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.frag.glsl')) shutil.copy(os.path.join(shader_dir_src, 'custom_mat_forward.vert.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.vert.glsl')) else: shutil.copy(os.path.join(shader_dir_src, 'custom_mat_deferred.frag.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.frag.glsl')) shutil.copy(os.path.join(shader_dir_src, 'custom_mat_deferred.vert.glsl'), os.path.join(shader_dir_dst, f'{mat_name}.vert.glsl')) # True if called from the material properties tab, else it was called from the search menu if hasattr(context, 'material') and context.material is not None: context.material.lnx_custom_material = mat_name return{'FINISHED'} class LNX_PG_BindTexturesListItem(bpy.types.PropertyGroup): uniform_name: StringProperty( name='Uniform Name', description='The name of the sampler uniform as used in the shader', default='ImageTexture', ) image: PointerProperty( name='Image', type=bpy.types.Image, description='The image to attach to the texture unit', ) class LNX_UL_BindTexturesList(bpy.types.UIList): def draw_item(self, context, layout, data, item: LNX_PG_BindTexturesListItem, icon, active_data, active_propname, index): row = layout.row(align=True) if item.image is not None: row.label(text=item.uniform_name, icon_value=item.image.preview.icon_id) else: row.label(text='', icon='ERROR') class LNX_OT_BindTexturesListNewItem(bpy.types.Operator): bl_idname = "lnx_bind_textures_list.new_item" bl_label = "Add Texture Binding" bl_description = "Add a new texture binding to the list" bl_options = {'INTERNAL'} @classmethod def poll(cls, context): mat = context.material if mat is None: return False return True def execute(self, context): mat = context.material mat.lnx_bind_textures_list.add() mat.lnx_bind_textures_list_index = len(mat.lnx_bind_textures_list) - 1 return{'FINISHED'} class LNX_OT_BindTexturesListDeleteItem(bpy.types.Operator): bl_idname = "lnx_bind_textures_list.delete_item" bl_label = "Remove Texture Binding" bl_description = "Delete the selected texture binding from the list" bl_options = {'INTERNAL'} @classmethod def poll(cls, context): mat = context.material if mat is None: return False return len(mat.lnx_bind_textures_list) > 0 def execute(self, context): mat = context.material lst = mat.lnx_bind_textures_list index = mat.lnx_bind_textures_list_index if len(lst) <= index: return{'FINISHED'} lst.remove(index) if index > 0: index = index - 1 mat.lnx_bind_textures_list_index = index return{'FINISHED'} class LNX_PT_MaterialPropsPanel(bpy.types.Panel): bl_label = "Leenkx Props" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False mat = bpy.context.material if mat is None: return layout.prop(mat, 'lnx_cast_shadow') columnb = layout.column() wrd = bpy.data.worlds['Lnx'] columnb.enabled = len(wrd.lnx_rplist) > 0 and lnx.utils.get_rp().rp_renderer == 'Forward' columnb.prop(mat, 'lnx_receive_shadow') layout.prop(mat, 'lnx_ignore_irradiance') layout.prop(mat, 'lnx_two_sided') columnb = layout.column() columnb.enabled = not mat.lnx_two_sided columnb.prop(mat, 'lnx_cull_mode') layout.prop(mat, 'lnx_material_id') layout.prop(mat, 'lnx_depth_read') layout.prop(mat, 'lnx_overlay') layout.prop(mat, 'lnx_decal') layout.prop(mat, 'lnx_discard') columnb = layout.column() columnb.enabled = mat.lnx_discard columnb.prop(mat, 'lnx_discard_opacity') columnb.prop(mat, 'lnx_discard_opacity_shadows') row = layout.row(align=True) row.prop(mat, 'lnx_custom_material') row.operator('lnx.new_custom_material', text='', icon='ADD') layout.prop(mat, 'lnx_skip_context') layout.prop(mat, 'lnx_particle_fade') layout.prop(mat, 'lnx_billboard') layout.operator("lnx.invalidate_material_cache") class LNX_PT_BindTexturesPropsPanel(bpy.types.Panel): bl_label = "Bind Textures" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_MaterialPropsPanel" @classmethod def poll(cls, context): mat = context.material if mat is None: return False return mat.lnx_custom_material != '' def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False mat = bpy.context.material if mat is None: return row = layout.row(align=True) col = row.column(align=True) col.template_list('LNX_UL_BindTexturesList', '', mat, 'lnx_bind_textures_list', mat, 'lnx_bind_textures_list_index') if mat.lnx_bind_textures_list_index >= 0 and len(mat.lnx_bind_textures_list) > 0: item = mat.lnx_bind_textures_list[mat.lnx_bind_textures_list_index] box = col.box() if item.image is None: _row = box.row() _row.alert = True _row.alignment = 'RIGHT' _row.label(text="No image selected, skipping export") box.prop(item, 'uniform_name') box.prop(item, 'image') col = row.column(align=True) col.operator("lnx_bind_textures_list.new_item", icon='ADD', text="") col.operator("lnx_bind_textures_list.delete_item", icon='REMOVE', text="") class LNX_PT_MaterialDriverPropsPanel(bpy.types.Panel): """Per-material properties for custom render path drivers""" bl_label = "Leenkx Driver Properties" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" @classmethod def poll(cls, context): mat = context.material if mat is None: return False wrd = bpy.data.worlds['Lnx'] if wrd.lnx_rplist_index < 0 or len(wrd.lnx_rplist) == 0: return False if len(lnx.api.drivers) == 0: return False rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] return rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['draw_mat_props'] is not None def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] lnx.api.drivers[rpdat.rp_driver]['draw_mat_props'](layout, context.material) class LNX_PT_MaterialBlendingPropsPanel(bpy.types.Panel): bl_label = "Blending" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_MaterialPropsPanel" def draw_header(self, context): if context.material is None: return self.layout.prop(context.material, 'lnx_blending', text="") def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False mat = bpy.context.material if mat is None: return flow = layout.grid_flow() flow.enabled = mat.lnx_blending col = flow.column(align=True) col.prop(mat, 'lnx_blending_source') col.prop(mat, 'lnx_blending_destination') col.prop(mat, 'lnx_blending_operation') flow.separator() col = flow.column(align=True) col.prop(mat, 'lnx_blending_source_alpha') col.prop(mat, 'lnx_blending_destination_alpha') col.prop(mat, 'lnx_blending_operation_alpha') class LNX_PT_LeenkxPlayerPanel(bpy.types.Panel): bl_label = "Leenkx Player" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] row = layout.row(align=True) row.alignment = 'EXPAND' row.scale_y = 1.3 if state.proc_play is None and state.proc_build is None: row.operator("lnx.play", icon="PLAY") else: if bpy.app.version < (3, 0, 0): row.operator("lnx.stop", icon="CANCEL", text="") elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 3, 2): row.operator("lnx.stop", icon="SEQUENCE_COLOR_01", text="") else: row.operator("lnx.stop", icon="EVENT_MEDIASTOP", text="") row.operator("lnx.clean_menu", icon="BRUSH_DATA") col = layout.box().column() col.prop(wrd, 'lnx_runtime') col.prop(wrd, 'lnx_play_camera') col.prop(wrd, 'lnx_play_scene') col.prop_search(wrd, 'lnx_play_renderpath', wrd, 'lnx_rplist', text='Render Path') if log.num_warnings > 0: box = layout.box() box.alert = True col = box.column(align=True) warnings = 'warnings' if log.num_warnings > 1 else 'warning' col.label(text=f'{log.num_warnings} {warnings} occurred during compilation!', icon='ERROR') # Blank icon to achieve the same indentation as the line before # prevent showing "open console" twice: if log.num_errors == 0: col.label(text='Please open the console to get more information.', icon='BLANK1') if log.num_errors > 0: box = layout.box() box.alert = True # Less spacing between lines col = box.column(align=True) errors = 'errors' if log.num_errors > 1 else 'error' col.label(text=f'{log.num_errors} {errors} occurred during compilation!', icon='CANCEL') # Blank icon to achieve the same indentation as the line before col.label(text='Please open the console to get more information.', icon='BLANK1') class LNX_PT_LeenkxExporterPanel(bpy.types.Panel): bl_label = "Leenkx Exporter" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] row = layout.row(align=True) row.alignment = 'EXPAND' row.scale_y = 1.3 row.operator("lnx.build_project", icon="MOD_BUILD") # row.operator("lnx.patch_project") row.operator("lnx.publish_project", icon="EXPORT") rows = 2 if len(wrd.lnx_exporterlist) > 1: rows = 4 row = layout.row() row.template_list("LNX_UL_ExporterList", "The_List", wrd, "lnx_exporterlist", wrd, "lnx_exporterlist_index", rows=rows) col = row.column(align=True) col.operator("lnx_exporterlist.new_item", icon='ADD', text="") col.operator("lnx_exporterlist.delete_item", icon='REMOVE', text="") col.menu("LNX_MT_ExporterListSpecials", icon='DOWNARROW_HLT', text="") if len(wrd.lnx_exporterlist) > 1: col.separator() op = col.operator("lnx_exporterlist.move_item", icon='TRIA_UP', text="") op.direction = 'UP' op = col.operator("lnx_exporterlist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' if wrd.lnx_exporterlist_index >= 0 and len(wrd.lnx_exporterlist) > 0: item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] box = layout.box().column() box.prop(item, 'lnx_project_target') box.prop(item, 'lnx_project_khamake') box.prop(item, lnx.utils.target_to_gapi(item.lnx_project_target)) box.prop_search(item, "lnx_project_rp", wrd, "lnx_rplist", text="Render Path") box.prop_search(item, 'lnx_project_scene', bpy.data, 'scenes', text='Scene') layout.separator() col = layout.column(align=True) col.prop(wrd, 'lnx_project_name') col.prop(wrd, 'lnx_project_package') col.prop(wrd, 'lnx_project_bundle') col = layout.column(align=True) col.prop(wrd, 'lnx_project_version') col.prop(wrd, 'lnx_project_version_autoinc') col = layout.column() col.prop(wrd, 'lnx_project_icon') col = layout.column(heading='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.prop(wrd, 'lnx_minimize') col.prop(wrd, 'lnx_optimize_data') col.prop(wrd, 'lnx_asset_compression') col.prop(wrd, 'lnx_single_data_file') class ExporterTargetSettingsMixin: """Mixin for common exporter setting subpanel functionality. Panels that inherit from this mixin need to have a lnx_target variable for polling.""" bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' bl_context = 'render' bl_parent_id = 'LNX_PT_LeenkxExporterPanel' # Override this in sub classes lnx_panel = '' @classmethod def poll(cls, context): wrd = bpy.data.worlds['Lnx'] if (len(wrd.lnx_exporterlist) > 0) and (wrd.lnx_exporterlist_index >= 0): item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] return item.lnx_project_target == cls.lnx_target return False def draw_header(self, context): self.layout.label(text='', icon='SETTINGS') class LNX_PT_LeenkxExporterAndroidSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): bl_label = "Android Settings" lnx_target = 'android-hl' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] col = layout.column() col.prop(wrd, 'lnx_winorient') col.prop(wrd, 'lnx_project_android_sdk_min') col.prop(wrd, 'lnx_project_android_sdk_target') col.prop(wrd, 'lnx_project_android_sdk_compile') class LNX_PT_LeenkxExporterAndroidPermissionsPanel(bpy.types.Panel): bl_label = "Permissions" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_LeenkxExporterAndroidSettingsPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] # Permission row = layout.row() rows = 2 if len(wrd.lnx_exporter_android_permission_list) > 1: rows = 4 row.template_list("LNX_UL_Exporter_AndroidPermissionList", "The_List", wrd, "lnx_exporter_android_permission_list", wrd, "lnx_exporter_android_permission_list_index", rows=rows) col = row.column(align=True) col.operator("lnx_exporter_android_permission_list.new_item", icon='ADD', text="") col.operator("lnx_exporter_android_permission_list.delete_item", icon='REMOVE', text="") row = layout.row() if wrd.lnx_exporter_android_permission_list_index >= 0 and len(wrd.lnx_exporter_android_permission_list) > 0: item = wrd.lnx_exporter_android_permission_list[wrd.lnx_exporter_android_permission_list_index] row = layout.row() row.prop(item, 'lnx_android_permissions') class LNX_PT_LeenkxExporterAndroidAbiPanel(bpy.types.Panel): bl_label = "Android ABI Filters" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = { 'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_LeenkxExporterAndroidSettingsPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] # ABIs row = layout.row() rows = 2 if len(wrd.lnx_exporter_android_abi_list) > 1: rows = 4 row.template_list("LNX_UL_Exporter_AndroidAbiList", "The_List", wrd, "lnx_exporter_android_abi_list", wrd, "lnx_exporter_android_abi_list_index", rows=rows) col = row.column(align=True) col.operator("lnx_exporter_android_abi_list.new_item", icon='ADD', text="") col.operator("lnx_exporter_android_abi_list.delete_item", icon='REMOVE', text="") row = layout.row() if wrd.lnx_exporter_android_abi_list_index >= 0 and len(wrd.lnx_exporter_android_abi_list) > 0: item = wrd.lnx_exporter_android_abi_list[wrd.lnx_exporter_android_abi_list_index] row = layout.row() row.prop(item, 'lnx_android_abi') class LNX_PT_LeenkxExporterAndroidBuildAPKPanel(bpy.types.Panel): bl_label = "Building APK" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_LeenkxExporterAndroidSettingsPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] path = lnx.utils.get_android_sdk_root_path() col = layout.column() row = col.row() row.enabled = len(path) > 0 row.prop(wrd, 'lnx_project_android_build_apk') row = col.row() row.enabled = wrd.lnx_project_android_build_apk row.prop(wrd, 'lnx_project_android_rename_apk') row = col.row() row.enabled = wrd.lnx_project_android_build_apk and len(lnx.utils.get_android_apk_copy_path()) > 0 row.prop(wrd, 'lnx_project_android_copy_apk') row = col.row(align=True) row.prop(wrd, 'lnx_project_android_list_avd') sub = row.column(align=True) sub.enabled = len(path) > 0 sub.operator('lnx.update_list_android_emulator', text='', icon='FILE_REFRESH') sub = row.column(align=True) sub.enabled = len(path) > 0 and len(lnx.utils.get_android_emulator_name()) > 0 sub.operator('lnx.run_android_emulator', text='', icon='PLAY') row = col.row() row.enabled = lnx.utils.get_project_android_build_apk() and len(lnx.utils.get_android_emulator_name()) > 0 row.prop(wrd, 'lnx_project_android_run_avd') class LNX_PT_LeenkxExporterHTML5SettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): bl_label = "HTML5 Settings" lnx_target = 'html5' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] col = layout.column() col.prop(wrd, 'lnx_project_html5_popupmenu_in_browser') row = col.row() row.enabled = len(lnx.utils.get_html5_copy_path()) > 0 row.prop(wrd, 'lnx_project_html5_copy') row = col.row() row.enabled = len(lnx.utils.get_html5_copy_path()) > 0 and wrd.lnx_project_html5_copy and len(lnx.utils.get_link_web_server()) > 0 row.prop(wrd, 'lnx_project_html5_start_browser') class LNX_PT_LeenkxExporterWindowsSettingsPanel(ExporterTargetSettingsMixin, bpy.types.Panel): bl_label = "Windows Settings" lnx_target = 'windows-hl' # See ExporterTargetSettingsMixin def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] is_windows = lnx.utils.get_os_is_windows() col = layout.column() col.prop(wrd, 'lnx_project_win_list_vs') row = col.row() row.enabled = is_windows row.prop(wrd, 'lnx_project_win_build', text='After Publish') layout = layout.column() layout.enabled = is_windows if is_windows and wrd.lnx_project_win_build != 'nothing' and not lnx.utils_vs.is_version_installed(wrd.lnx_project_win_list_vs): box = draw_error_box( layout, 'The selected version of Visual Studio could not be found and' ' may not be installed. The "After Publish" action may not work' ' as intended.' ) box.operator('lnx.update_list_installed_vs', icon='FILE_REFRESH') layout.separator() col = layout.column() col.enabled = wrd.lnx_project_win_build.startswith('compile') col.prop(wrd, 'lnx_project_win_build_mode') col.prop(wrd, 'lnx_project_win_build_arch') col.prop(wrd, 'lnx_project_win_build_log') col.prop(wrd, 'lnx_project_win_build_cpu') col.prop(wrd, 'lnx_project_win_build_open') class LNX_PT_LeenkxProjectPanel(bpy.types.Panel): bl_label = "Leenkx Project" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False row = layout.row(align=True) row.operator("lnx.open_editor", icon="DESKTOP") row.operator("lnx.open_project_folder", icon="FILE_FOLDER") class LNX_PT_ProjectFlagsPanel(bpy.types.Panel): bl_label = "Flags" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_parent_id = "LNX_PT_LeenkxProjectPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] col = layout.column(heading='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.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.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') 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.prop(wrd, 'lnx_copy_override') col.operator('lnx.copy_to_bundled', icon='IMAGE_DATA') class LNX_PT_ProjectFlagsDebugConsolePanel(bpy.types.Panel): bl_label = "Debug Console" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_ProjectFlagsPanel" def draw_header(self, context): wrd = bpy.data.worlds['Lnx'] self.layout.prop(wrd, 'lnx_debug_console', text='') def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] col = layout.column() col.enabled = wrd.lnx_debug_console col.prop(wrd, 'lnx_debug_console_position') col.prop(wrd, 'lnx_debug_console_scale') col.prop(wrd, 'lnx_debug_console_visible') col.prop(wrd, 'lnx_debug_console_trace_pos') class LNX_PT_ProjectWindowPanel(bpy.types.Panel): bl_label = "Window" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_LeenkxProjectPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] layout.prop(wrd, 'lnx_winmode') col = layout.column(align=True) col.prop(wrd, 'lnx_winresize') sub = col.column() sub.enabled = wrd.lnx_winresize sub.prop(wrd, 'lnx_winmaximize') col.enabled = True col.prop(wrd, 'lnx_winminimize') layout.prop(wrd, 'lnx_vsync') class LNX_PT_ProjectModulesPanel(bpy.types.Panel): bl_label = "Modules" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_LeenkxProjectPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] layout.prop(wrd, 'lnx_audio') layout.prop(wrd, 'lnx_physics') if wrd.lnx_physics != 'Disabled': layout.prop(wrd, 'lnx_physics_engine') layout.prop(wrd, 'lnx_navigation') if wrd.lnx_navigation != 'Disabled': layout.prop(wrd, 'lnx_navigation_engine') layout.prop(wrd, 'lnx_ui') layout.prop(wrd, 'lnx_network') layout.prop_search(wrd, 'lnx_khafile', bpy.data, 'texts') layout.prop(wrd, 'lnx_project_root') class LnxVirtualInputPanel(bpy.types.Panel): bl_label = "Leenkx Virtual Input" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False class LeenkxPlayButton(bpy.types.Operator): '''Launch player in new window''' bl_idname = 'lnx.play' bl_label = 'Play' def invoke(self, context, event): if event.shift: state.is_play = True make.build_success() return{'FINISHED'} return self.execute(context) def execute(self, context): wrd = bpy.data.worlds['Lnx'] if state.proc_build is not None: return {"CANCELLED"} lnx.utils.check_blender_version(self) if not lnx.utils.check_saved(self): return {"CANCELLED"} if not lnx.utils.check_sdkpath(self): return {"CANCELLED"} lnx.utils.check_projectpath(None) lnx.utils.check_default_props() assets.invalidate_enabled = False if wrd.lnx_clear_on_compile: os.system("cls") make.play() assets.invalidate_enabled = True return{'FINISHED'} class LeenkxStopButton(bpy.types.Operator): '''Stop currently running player''' bl_idname = 'lnx.stop' bl_label = 'Stop' def execute(self, context): if state.proc_play != None: state.proc_play.terminate() state.proc_play = None elif state.proc_build != None: state.proc_build.terminate() state.proc_build = None lnx.write_probes.check_last_cmft_time() return {'FINISHED'} class LeenkxBuildProjectButton(bpy.types.Operator): """Build and compile project""" bl_idname = 'lnx.build_project' bl_label = 'Build' @classmethod def poll(cls, context): wrd = bpy.data.worlds['Lnx'] return wrd.lnx_exporterlist_index >= 0 and len(wrd.lnx_exporterlist) > 0 def execute(self, context): lnx.utils.check_blender_version(self) if not lnx.utils.check_saved(self): return {"CANCELLED"} if not lnx.utils.check_sdkpath(self): return {"CANCELLED"} lnx.utils.check_projectpath(self) lnx.utils.check_default_props() wrd = bpy.data.worlds['Lnx'] item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] if item.lnx_project_rp == '': item.lnx_project_rp = wrd.lnx_rplist[wrd.lnx_rplist_index].name if item.lnx_project_scene == None: item.lnx_project_scene = context.scene # Assume unique rp names rplist_index = wrd.lnx_rplist_index for i in range(0, len(wrd.lnx_rplist)): if wrd.lnx_rplist[i].name == item.lnx_project_rp: wrd.lnx_rplist_index = i break assets.invalidate_shader_cache(None, None) assets.invalidate_enabled = False if wrd.lnx_clear_on_compile: os.system("cls") make.build(item.lnx_project_target, is_export=True) make.compile() wrd.lnx_rplist_index = rplist_index assets.invalidate_enabled = True return{'FINISHED'} class LeenkxPublishProjectButton(bpy.types.Operator): """Build project ready for publishing.""" bl_idname = 'lnx.publish_project' bl_label = 'Publish' @classmethod def poll(cls, context): wrd = bpy.data.worlds['Lnx'] return wrd.lnx_exporterlist_index >= 0 and len(wrd.lnx_exporterlist) > 0 def execute(self, context): lnx.utils.check_blender_version(self) if not lnx.utils.check_saved(self): return {"CANCELLED"} if not lnx.utils.check_sdkpath(self): return {"CANCELLED"} self.report({'INFO'}, 'Publishing project, check console for details.') lnx.utils.check_projectpath(self) lnx.utils.check_default_props() wrd = bpy.data.worlds['Lnx'] item = wrd.lnx_exporterlist[wrd.lnx_exporterlist_index] if item.lnx_project_rp == '': item.lnx_project_rp = wrd.lnx_rplist[wrd.lnx_rplist_index].name if item.lnx_project_scene == None: item.lnx_project_scene = context.scene # Assume unique rp names rplist_index = wrd.lnx_rplist_index for i in range(0, len(wrd.lnx_rplist)): if wrd.lnx_rplist[i].name == item.lnx_project_rp: wrd.lnx_rplist_index = i break make.clean() assets.invalidate_enabled = False if wrd.lnx_clear_on_compile: os.system("cls") make.build(item.lnx_project_target, is_publish=True, is_export=True) make.compile() wrd.lnx_rplist_index = rplist_index assets.invalidate_enabled = True return{'FINISHED'} class LeenkxOpenProjectFolderButton(bpy.types.Operator): '''Open project folder''' bl_idname = 'lnx.open_project_folder' bl_label = 'Project Folder' def execute(self, context): if not lnx.utils.check_saved(self): return {"CANCELLED"} lnx.utils.open_folder(lnx.utils.get_fp()) return{'FINISHED'} class LeenkxOpenEditorButton(bpy.types.Operator): '''Launch this project in the IDE''' bl_idname = 'lnx.open_editor' bl_label = 'Code Editor' bl_description = 'Open Project in IDE' def execute(self, context): if not lnx.utils.check_saved(self): return {"CANCELLED"} lnx.utils.check_default_props() if not os.path.exists(lnx.utils.get_fp() + "/khafile.js"): print('Generating Krom project for IDE build configuration') make.build('krom') lnx.utils.open_editor() return{'FINISHED'} class CleanMenu(bpy.types.Menu): bl_label = "Ok?" bl_idname = "OBJECT_MT_clean_menu" def draw(self, context): layout = self.layout layout.operator("lnx.clean_project") class CleanButtonMenu(bpy.types.Operator): '''Clean cached data''' bl_label = "Clean" bl_idname = "lnx.clean_menu" def execute(self, context): bpy.ops.wm.call_menu(name=CleanMenu.bl_idname) return {"FINISHED"} class LeenkxCleanProjectButton(bpy.types.Operator): '''Delete all cached project data''' bl_idname = 'lnx.clean_project' bl_label = 'Clean Project' def execute(self, context): if not lnx.utils.check_saved(self): return {"CANCELLED"} make.clean() return{'FINISHED'} def draw_view3d_header(self, context): if state.proc_build is not None: self.layout.label(text='Compiling..') elif log.info_text != '': self.layout.label(text=log.info_text) def draw_view3d_object_menu(self, context): self.layout.separator() self.layout.operator_context = 'INVOKE_DEFAULT' self.layout.operator('lnx.copy_traits_to_active') class LNX_PT_TopbarPanel(bpy.types.Panel): bl_label = "Leenkx Player" bl_space_type = "VIEW_3D" bl_region_type = "WINDOW" bl_options = {'INSTANCED'} def draw_header(self, context): row = self.layout.row(align=True) if state.proc_play is None and state.proc_build is None: row.operator("lnx.play", icon="PLAY", text="") else: if bpy.app.version < (3, 0, 0): row.operator("lnx.stop", icon="CANCEL", text="") elif bpy.app.version > (3, 0, 0) and bpy.app.version < (4, 3, 2): row.operator("lnx.stop", icon="SEQUENCE_COLOR_01", text="") else: row.operator("lnx.stop", icon="EVENT_MEDIASTOP", text="") row.operator("lnx.clean_menu", icon="BRUSH_DATA", text="") row.operator("lnx.open_editor", icon="DESKTOP", text="") row.operator("lnx.open_project_folder", icon="FILE_FOLDER", text="") def draw(self, context): col = self.layout.column() wrd = bpy.data.worlds['Lnx'] col.label(text="Leenkx Launch") col.separator() col.prop(wrd, 'lnx_runtime') col.prop(wrd, 'lnx_play_camera') col.prop(wrd, 'lnx_play_scene') col.prop_search(wrd, 'lnx_play_renderpath', wrd, 'lnx_rplist', text='Render Path') col.prop(wrd, 'lnx_debug_console', text="Debug Console") def draw_space_topbar(self, context): # for some blender reasons, topbar is instanced twice. this avoids doubling the panel if context.region.alignment == 'RIGHT': self.layout.popover(panel="LNX_PT_TopbarPanel", text="") class LNX_PT_RenderPathPanel(bpy.types.Panel): bl_label = "Leenkx Render Path" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] rows = 2 if len(wrd.lnx_rplist) > 1: rows = 4 row = layout.row() row.template_list("LNX_UL_RPList", "The_List", wrd, "lnx_rplist", wrd, "lnx_rplist_index", rows=rows) col = row.column(align=True) col.operator("lnx_rplist.new_item", icon='ADD', text="") col.operator("lnx_rplist.delete_item", icon='REMOVE', text="") if len(wrd.lnx_rplist) > 1: col.separator() op = col.operator("lnx_rplist.move_item", icon='TRIA_UP', text="") op.direction = 'UP' op = col.operator("lnx_rplist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' if wrd.lnx_rplist_index < 0 or len(wrd.lnx_rplist) == 0: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] if len(lnx.api.drivers) > 0: layout.prop_search(rpdat, "rp_driver", wrd, "rp_driver_list", text="Driver") layout.separator() if rpdat.rp_driver != 'Leenkx' and lnx.api.drivers[rpdat.rp_driver]['draw_props'] != None: lnx.api.drivers[rpdat.rp_driver]['draw_props'](layout) return class LNX_PT_RenderPathRendererPanel(bpy.types.Panel): bl_label = "Renderer" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_RenderPathPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] layout.prop(rpdat, 'rp_renderer') if rpdat.rp_renderer == 'Forward': layout.prop(rpdat, 'rp_depthprepass') layout.prop(rpdat, 'lnx_material_model') layout.prop(rpdat, 'rp_translucency_state') layout.prop(rpdat, 'rp_overlays_state') layout.prop(rpdat, 'rp_decals_state') layout.prop(rpdat, 'rp_blending_state') layout.prop(rpdat, 'rp_depth_texture_state') layout.prop(rpdat, 'rp_draw_order') layout.prop(rpdat, 'lnx_samples_per_pixel') layout.prop(rpdat, 'lnx_texture_filter') layout.prop(rpdat, 'rp_sss_state') col = layout.column() col.enabled = rpdat.rp_sss_state != 'Off' col.prop(rpdat, 'lnx_sss_width') layout.prop(rpdat, 'lnx_rp_displacement') if rpdat.lnx_rp_displacement == 'Tessellation': layout.label(text='Mesh') layout.prop(rpdat, 'lnx_tess_mesh_inner') layout.prop(rpdat, 'lnx_tess_mesh_outer') layout.label(text='Shadow') layout.prop(rpdat, 'lnx_tess_shadows_inner') layout.prop(rpdat, 'lnx_tess_shadows_outer') layout.prop(rpdat, 'lnx_particles') layout.separator(factor=0.1) col = layout.column() col.prop(rpdat, 'lnx_skin') col = col.column() col.enabled = rpdat.lnx_skin == 'On' col.prop(rpdat, 'lnx_use_armature_deform_only') col.prop(rpdat, 'lnx_skin_max_bones_auto') row = col.row() row.enabled = not rpdat.lnx_skin_max_bones_auto row.prop(rpdat, 'lnx_skin_max_bones') layout.separator(factor=0.1) col = layout.column() col.prop(rpdat, 'lnx_morph_target') col = col.column() col.enabled = rpdat.lnx_morph_target == 'On' layout.separator(factor=0.1) col = layout.column() col.prop(rpdat, "rp_hdr") col.prop(rpdat, "rp_stereo") col.prop(rpdat, 'lnx_culling') col.prop(rpdat, 'rp_pp') class LNX_PT_RenderPathShadowsPanel(bpy.types.Panel): bl_label = "Shadows" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_RenderPathPanel" def draw_header(self, context): wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] self.layout.prop(rpdat, "rp_shadows", text="") def compute_subdivs(self, max, subdivs): l = [max] for i in range(subdivs - 1): l.append(int(max / 2)) max = max / 2 return l def tiles_per_light_type(self, rpdat: lnx.props_renderpath.LnxRPListItem, light_type: str) -> int: if light_type == 'point': return 6 elif light_type == 'spot': return 1 else: return int(rpdat.rp_shadowmap_cascades) def lights_number_atlas(self, rpdat: lnx.props_renderpath.LnxRPListItem, atlas_size: int, shadowmap_size: int, light_type: str) -> int: '''Compute number lights that could fit in an atlas''' lights = atlas_size / shadowmap_size lights *= lights / self.tiles_per_light_type(rpdat, light_type) return int(lights) def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] layout.enabled = rpdat.rp_shadows col = layout.column() col.enabled = not rpdat.rp_shadowmap_atlas_single_map or not rpdat.rp_shadowmap_atlas col.prop(rpdat, 'rp_shadowmap_cube') layout.prop(rpdat, 'rp_shadowmap_cascade') layout.prop(rpdat, 'rp_shadowmap_cascades') col = layout.column() col2 = col.column() col2.enabled = rpdat.rp_shadowmap_cascades != '1' col2.prop(rpdat, 'lnx_shadowmap_split') col.prop(rpdat, 'lnx_shadowmap_bounds') col.prop(rpdat, 'lnx_pcfsize') layout.separator() layout.prop(rpdat, 'rp_shadowmap_atlas') colatlas = layout.column() colatlas.enabled = rpdat.rp_shadowmap_atlas colatlas.prop(rpdat, 'rp_max_lights') colatlas.prop(rpdat, 'rp_max_lights_cluster') colatlas.prop(rpdat, 'rp_shadowmap_atlas_lod') colatlas_lod = colatlas.column() colatlas_lod.enabled = rpdat.rp_shadowmap_atlas_lod colatlas_lod.prop(rpdat, 'rp_shadowmap_atlas_lod_subdivisions') colatlas_lod_info = colatlas_lod.row() colatlas_lod_info.alignment = 'RIGHT' subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cascade), int(rpdat.rp_shadowmap_atlas_lod_subdivisions)) subdiv_text = "Subdivisions for spot lights: " + ', '.join(map(str, subdivs_list)) colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH") if not rpdat.rp_shadowmap_atlas_single_map: colatlas_lod_info = colatlas_lod.row() colatlas_lod_info.alignment = 'RIGHT' subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cube), int(rpdat.rp_shadowmap_atlas_lod_subdivisions)) subdiv_text = "Subdivisions for point lights: " + ', '.join(map(str, subdivs_list)) colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH") size_warning = int(rpdat.rp_shadowmap_cascade) > 2048 or int(rpdat.rp_shadowmap_cube) > 2048 colatlas.prop(rpdat, 'rp_shadowmap_atlas_single_map') # show size for single texture if rpdat.rp_shadowmap_atlas_single_map: colatlas_single = colatlas.column() colatlas_single.prop(rpdat, 'rp_shadowmap_atlas_max_size') if rpdat.rp_shadowmap_atlas_max_size != '': atlas_size = int(rpdat.rp_shadowmap_atlas_max_size) shadowmap_size = int(rpdat.rp_shadowmap_cascade) if shadowmap_size > 2048: size_warning = True point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point') spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot') dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun') col = colatlas_single.row() col.alignment = 'RIGHT' col.label(text=f'Enough space for { point_lights } point lights or { spot_lights } spot lights or { dir_lights } directional lights.') else: # show size for all types colatlas_mixed = colatlas.column() colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_spot') if rpdat.rp_shadowmap_atlas_max_size_spot != '': atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_spot) shadowmap_size = int(rpdat.rp_shadowmap_cascade) spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot') if shadowmap_size > 2048: size_warning = True col = colatlas_mixed.row() col.alignment = 'RIGHT' col.label(text=f'Enough space for {spot_lights} spot lights.') colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_point') if rpdat.rp_shadowmap_atlas_max_size_point != '': atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_point) shadowmap_size = int(rpdat.rp_shadowmap_cube) point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point') if shadowmap_size > 2048: size_warning = True col = colatlas_mixed.row() col.alignment = 'RIGHT' col.label(text=f'Enough space for {point_lights} point lights.') colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_sun') if rpdat.rp_shadowmap_atlas_max_size_sun != '': atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_sun) shadowmap_size = int(rpdat.rp_shadowmap_cascade) dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun') if shadowmap_size > 2048: size_warning = True col = colatlas_mixed.row() col.alignment = 'RIGHT' col.label(text=f'Enough space for {dir_lights} directional lights.') # show warning when user picks a size higher than 2048 (arbitrary number). if size_warning: col = layout.column() row = col.row() row.alignment = 'RIGHT' row.label(text='Warning: Game will crash if texture size is higher than max texture size allowed by target.', icon='ERROR') class LNX_PT_RenderPathVoxelsPanel(bpy.types.Panel): bl_label = "Voxels" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_RenderPathPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] layout.prop(rpdat, 'rp_voxels') col = layout.column() col.enabled = rpdat.rp_voxels != 'Off' col2 = col.column() col2.enabled = rpdat.rp_voxels == 'Voxel GI' col3 = col.column() col3.enabled = rpdat.rp_voxels == 'Voxel AO' col.prop(rpdat, 'lnx_voxelgi_shadows', text='Shadows') col2.prop(rpdat, 'lnx_voxelgi_refract', text='Refraction') col.prop(rpdat, 'lnx_voxelgi_clipmap_count') #col.prop(rpdat, 'lnx_voxelgi_cones') col.prop(rpdat, 'rp_voxelgi_resolution') col.prop(rpdat, 'lnx_voxelgi_size') #col.prop(rpdat, 'rp_voxelgi_resolution_z') col2.enabled = rpdat.rp_voxels == 'Voxel GI' #col.prop(rpdat, 'lnx_voxelgi_temporal') col.label(text="Light") col2 = col.column() col2.enabled = rpdat.rp_voxels == 'Voxel GI' col2.prop(rpdat, 'lnx_voxelgi_diff') col2.prop(rpdat, 'lnx_voxelgi_spec') col2.prop(rpdat, 'lnx_voxelgi_refr') col.prop(rpdat, 'lnx_voxelgi_shad') col.prop(rpdat, 'lnx_voxelgi_occ') col.label(text="Ray") col.prop(rpdat, 'lnx_voxelgi_offset') col.prop(rpdat, 'lnx_voxelgi_step') col.prop(rpdat, 'lnx_voxelgi_range') #col.prop(rpdat, 'lnx_voxelgi_aperture') class LNX_PT_RenderPathWorldPanel(bpy.types.Panel): bl_label = "World" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_RenderPathPanel" def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] layout.prop(rpdat, "rp_background") col = layout.column() col.prop(rpdat, 'lnx_irradiance') colb = col.column() colb.enabled = rpdat.lnx_irradiance colb.prop(rpdat, 'lnx_radiance') sub = colb.row() sub.enabled = rpdat.lnx_radiance sub.prop(rpdat, 'lnx_radiance_size') layout.separator() layout.prop(rpdat, 'lnx_clouds') col = layout.column(align=True) col.prop(rpdat, "rp_water") col = col.column(align=True) col.enabled = rpdat.rp_water col.prop(rpdat, 'lnx_water_level') col.prop(rpdat, 'lnx_water_density') col.prop(rpdat, 'lnx_water_displace') col.prop(rpdat, 'lnx_water_speed') col.prop(rpdat, 'lnx_water_freq') col.prop(rpdat, 'lnx_water_refract') col.prop(rpdat, 'lnx_water_reflect') col.prop(rpdat, 'lnx_water_color') class LNX_PT_RenderPathPostProcessPanel(bpy.types.Panel): bl_label = "Post Process" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_RenderPathPanel" def draw_header(self, context): wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] self.layout.prop(rpdat, "rp_render_to_texture", text="") def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] layout.enabled = rpdat.rp_render_to_texture col = layout.column() col.prop(rpdat, "rp_antialiasing") col.prop(rpdat, "rp_supersampling") col = layout.column() col.prop(rpdat, 'lnx_rp_resolution') if rpdat.lnx_rp_resolution == 'Custom': col.prop(rpdat, 'lnx_rp_resolution_size') col.prop(rpdat, 'lnx_rp_resolution_filter') col.prop(rpdat, 'rp_dynres') layout.separator() col = layout.column() col.prop(rpdat, "rp_ssgi") sub = col.column() sub.enabled = rpdat.rp_ssgi != 'Off' sub.prop(rpdat, 'lnx_ssgi_half_res') sub.prop(rpdat, 'lnx_ssgi_rays') sub.prop(rpdat, 'lnx_ssgi_radius') sub.prop(rpdat, 'lnx_ssgi_strength') sub.prop(rpdat, 'lnx_ssgi_max_steps') layout.separator() row = layout.row() row.enabled = rpdat.lnx_material_model == 'Full' row.prop(rpdat, 'lnx_micro_shadowing') layout.separator() col = layout.column() col.prop(rpdat, "rp_ssr") col.prop(rpdat, 'lnx_ssr_half_res') col = col.column() col.enabled = rpdat.rp_ssr col.prop(rpdat, 'lnx_ssr_ray_step') col.prop(rpdat, 'lnx_ssr_search_dist') col.prop(rpdat, 'lnx_ssr_falloff_exp') col.prop(rpdat, 'lnx_ssr_jitter') layout.separator() col = layout.column() col.prop(rpdat, "rp_ss_refraction") col = col.column() col.enabled = rpdat.rp_ss_refraction col.prop(rpdat, 'lnx_ss_refraction_ray_step') col.prop(rpdat, 'lnx_ss_refraction_search_dist') col.prop(rpdat, 'lnx_ss_refraction_falloff_exp') col.prop(rpdat, 'lnx_ss_refraction_jitter') layout.separator() col = layout.column() col.prop(rpdat, 'lnx_ssrs') col = col.column() col.enabled = rpdat.lnx_ssrs col.prop(rpdat, 'lnx_ssrs_ray_step') layout.separator() col = layout.column() col.prop(rpdat, "rp_bloom") _col = col.column() _col.enabled = rpdat.rp_bloom if bpy.app.version <= (4, 2, 4): _col.prop(rpdat, 'lnx_bloom_follow_blender') if not rpdat.lnx_bloom_follow_blender: _col.prop(rpdat, 'lnx_bloom_threshold') _col.prop(rpdat, 'lnx_bloom_knee') _col.prop(rpdat, 'lnx_bloom_radius') _col.prop(rpdat, 'lnx_bloom_strength') else: _col.prop(rpdat, 'lnx_bloom_threshold') _col.prop(rpdat, 'lnx_bloom_knee') _col.prop(rpdat, 'lnx_bloom_radius') _col.prop(rpdat, 'lnx_bloom_strength') _col.prop(rpdat, 'lnx_bloom_quality') _col.prop(rpdat, 'lnx_bloom_anti_flicker') layout.separator() col = layout.column() col.prop(rpdat, "rp_motionblur") col = col.column() col.enabled = rpdat.rp_motionblur != 'Off' col.prop(rpdat, 'lnx_motion_blur_intensity') layout.separator() col = layout.column() col.prop(rpdat, "rp_volumetriclight") col = col.column() col.enabled = rpdat.rp_volumetriclight col.prop(rpdat, 'lnx_volumetric_light_air_color') col.prop(rpdat, 'lnx_volumetric_light_air_turbidity') col.prop(rpdat, 'lnx_volumetric_light_steps') layout.separator() col = layout.column() col.prop(rpdat, "rp_chromatic_aberration") col = col.column() col.enabled = rpdat.rp_chromatic_aberration col.prop(rpdat, 'lnx_chromatic_aberration_type') col.prop(rpdat, 'lnx_chromatic_aberration_strength') if rpdat.lnx_chromatic_aberration_type == "Spectral": col.prop(rpdat, 'lnx_chromatic_aberration_samples') class LNX_PT_RenderPathCompositorPanel(bpy.types.Panel): bl_label = "Compositor" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "LNX_PT_RenderPathPanel" def draw_header(self, context): wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] self.layout.prop(rpdat, "rp_compositornodes", text="") def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] if len(wrd.lnx_rplist) <= wrd.lnx_rplist_index: return rpdat = wrd.lnx_rplist[wrd.lnx_rplist_index] layout.enabled = rpdat.rp_compositornodes layout.prop(rpdat, 'lnx_tonemap') layout.separator() col = layout.column() col.prop(rpdat, 'lnx_letterbox') col = col.column(align=True) col.enabled = rpdat.lnx_letterbox col.prop(rpdat, 'lnx_letterbox_color') col.prop(rpdat, 'lnx_letterbox_size') layout.separator() col = layout.column() draw_conditional_prop(col, 'Distort', rpdat, 'lnx_distort', 'lnx_distort_strength') draw_conditional_prop(col, 'Film Grain', rpdat, 'lnx_grain', 'lnx_grain_strength') draw_conditional_prop(col, 'Sharpen', rpdat, 'lnx_sharpen', 'lnx_sharpen_strength') draw_conditional_prop(col, 'Vignette', rpdat, 'lnx_vignette', 'lnx_vignette_strength') layout.separator() col = layout.column() col.prop(rpdat, 'lnx_fog') col = col.column(align=True) col.enabled = rpdat.lnx_fog col.prop(rpdat, 'lnx_fog_color') col.prop(rpdat, 'lnx_fog_amounta') col.prop(rpdat, 'lnx_fog_amountb') layout.separator() col = layout.column() col.prop(rpdat, "rp_autoexposure") sub = col.column(align=True) sub.enabled = rpdat.rp_autoexposure sub.prop(rpdat, 'lnx_autoexposure_strength', text='Strength') sub.prop(rpdat, 'lnx_autoexposure_speed', text='Speed') layout.separator() col = layout.column() col.prop(rpdat, 'lnx_fisheye') col.prop(rpdat, 'lnx_lensflare') layout.separator() col = layout.column() col.prop(rpdat, 'lnx_lens') col = col.column(align=True) col.enabled = rpdat.lnx_lens col.prop(rpdat, 'lnx_lens_texture') if rpdat.lnx_lens_texture != "": col.prop(rpdat, 'lnx_lens_texture_masking') if rpdat.lnx_lens_texture_masking: sub = col.column(align=True) sub.prop(rpdat, 'lnx_lens_texture_masking_centerMinClip') sub.prop(rpdat, 'lnx_lens_texture_masking_centerMaxClip') sub = col.column(align=True) sub.prop(rpdat, 'lnx_lens_texture_masking_luminanceMin') sub.prop(rpdat, 'lnx_lens_texture_masking_luminanceMax') col.prop(rpdat, 'lnx_lens_texture_masking_brightnessExp') layout.separator() layout.separator() col = layout.column() col.prop(rpdat, 'lnx_lut') col = col.column(align=True) col.enabled = rpdat.lnx_lut col.prop(rpdat, 'lnx_lut_texture') layout.separator() class LNX_PT_BakePanel(bpy.types.Panel): bl_label = "Leenkx Bake" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "render" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False scn = bpy.data.scenes[context.scene.name] row = layout.row(align=True) row.prop(scn, "lnx_bakemode", expand=True) if scn.lnx_bakemode == "Static Map": row = layout.row(align=True) row.alignment = 'EXPAND' row.operator("lnx.bake_textures", icon="RENDER_STILL") row.operator("lnx.bake_apply") col = layout.column() col.prop(scn, 'lnx_bakelist_scale') col.prop(scn.cycles, "samples") layout.prop(scn, 'lnx_bakelist_unwrap') rows = 2 if len(scn.lnx_bakelist) > 1: rows = 4 row = layout.row() row.template_list("LNX_UL_BakeList", "The_List", scn, "lnx_bakelist", scn, "lnx_bakelist_index", rows=rows) col = row.column(align=True) col.operator("lnx_bakelist.new_item", icon='ADD', text="") col.operator("lnx_bakelist.delete_item", icon='REMOVE', text="") col.menu("LNX_MT_BakeListSpecials", icon='DOWNARROW_HLT', text="") if len(scn.lnx_bakelist) > 1: col.separator() op = col.operator("lnx_bakelist.move_item", icon='TRIA_UP', text="") op.direction = 'UP' op = col.operator("lnx_bakelist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' if scn.lnx_bakelist_index >= 0 and len(scn.lnx_bakelist) > 0: item = scn.lnx_bakelist[scn.lnx_bakelist_index] layout.prop_search(item, "obj", bpy.data, "objects", text="Object") layout.prop(item, "res_x") layout.prop(item, "res_y") class LnxGenLodButton(bpy.types.Operator): """Automatically generate LoD levels.""" bl_idname = 'lnx.generate_lod' bl_label = 'Auto Generate' def lod_name(self, name, level): return name + '_LOD' + str(level + 1) def execute(self, context): obj = context.object if obj == None: return{'CANCELLED'} # Clear mdata = context.object.data mdata.lnx_lodlist_index = 0 mdata.lnx_lodlist.clear() # Lod levels wrd = bpy.data.worlds['Lnx'] ratio = wrd.lnx_lod_gen_ratio num_levels = wrd.lnx_lod_gen_levels for level in range(0, num_levels): new_obj = obj.copy() for i in range(0, 3): new_obj.location[i] = 0 new_obj.rotation_euler[i] = 0 new_obj.scale[i] = 1 new_obj.data = obj.data.copy() new_obj.name = self.lod_name(obj.name, level) new_obj.parent = obj new_obj.hide_viewport = True new_obj.hide_render = True mod = new_obj.modifiers.new('Decimate', 'DECIMATE') mod.ratio = ratio ratio *= wrd.lnx_lod_gen_ratio context.scene.collection.objects.link(new_obj) # Screen sizes for level in range(0, num_levels): mdata.lnx_lodlist.add() mdata.lnx_lodlist[-1].name = self.lod_name(obj.name, level) mdata.lnx_lodlist[-1].screen_size_prop = (1 - (1 / (num_levels + 1)) * level) - (1 / (num_levels + 1)) return{'FINISHED'} class LNX_PT_LodPanel(bpy.types.Panel): bl_label = "Leenkx Lod" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "object" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False obj = bpy.context.object # Mesh only for now if obj.type != 'MESH': return mdata = obj.data rows = 2 if len(mdata.lnx_lodlist) > 1: rows = 4 row = layout.row() row.template_list("LNX_UL_LodList", "The_List", mdata, "lnx_lodlist", mdata, "lnx_lodlist_index", rows=rows) col = row.column(align=True) col.operator("lnx_lodlist.new_item", icon='ADD', text="") col.operator("lnx_lodlist.delete_item", icon='REMOVE', text="") if len(mdata.lnx_lodlist) > 1: col.separator() op = col.operator("lnx_lodlist.move_item", icon='TRIA_UP', text="") op.direction = 'UP' op = col.operator("lnx_lodlist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' if mdata.lnx_lodlist_index >= 0 and len(mdata.lnx_lodlist) > 0: item = mdata.lnx_lodlist[mdata.lnx_lodlist_index] layout.prop_search(item, "name", bpy.data, "objects", text="Object") layout.prop(item, "screen_size_prop") layout.prop(mdata, "lnx_lod_material") # Auto lod for meshes if obj.type == 'MESH': layout.separator() layout.operator("lnx.generate_lod") wrd = bpy.data.worlds['Lnx'] layout.prop(wrd, 'lnx_lod_gen_levels') layout.prop(wrd, 'lnx_lod_gen_ratio') class LnxGenTerrainButton(bpy.types.Operator): '''Generate terrain sectors''' bl_idname = 'lnx.generate_terrain' bl_label = 'Generate' def execute(self, context): scn = context.scene if scn == None: return{'CANCELLED'} sectors = scn.lnx_terrain_sectors size = scn.lnx_terrain_sector_size height_scale = scn.lnx_terrain_height_scale # Create material mat = bpy.data.materials.new(name="Terrain") mat.use_nodes = True nodes = mat.node_tree.nodes links = mat.node_tree.links node = nodes.new('ShaderNodeDisplacement') node.location = (-200, 100) node.inputs[2].default_value = height_scale node.space = 'WORLD' links.new(nodes['Material Output'].inputs[2], node.outputs[0]) node = nodes.new('ShaderNodeTexImage') node.location = (-600, 100) node.interpolation = 'Closest' node.extension = 'EXTEND' node.lnx_material_param = True node.name = '_TerrainHeight' node.label = '_TerrainHeight' # Height-map texture link for this sector links.new(nodes['Displacement'].inputs[0], nodes['_TerrainHeight'].outputs[0]) node = nodes.new('ShaderNodeBump') node.location = (-200, -200) node.inputs[0].default_value = 5.0 links.new(nodes['Bump'].inputs[2], nodes['_TerrainHeight'].outputs[0]) links.new(nodes['Principled BSDF'].inputs[20], nodes['Bump'].outputs[0]) # Create sectors root_obj = bpy.data.objects.new("Terrain", None) root_obj.location[0] = 0 root_obj.location[1] = 0 root_obj.location[2] = 0 root_obj.lnx_export = False scn.collection.objects.link(root_obj) scn.lnx_terrain_object = root_obj for i in range(sectors[0] * sectors[1]): j = str(i + 1).zfill(2) x = i % sectors[0] y = int(i / sectors[0]) bpy.ops.mesh.primitive_plane_add(location=(x * size, -y * size, 0)) slice_obj = bpy.context.active_object slice_obj.scale[0] = size / 2 slice_obj.scale[1] = -(size / 2) slice_obj.scale[2] = height_scale slice_obj.data.materials.append(mat) for p in slice_obj.data.polygons: p.use_smooth = True slice_obj.name = 'Terrain.' + j slice_obj.parent = root_obj sub_mod = slice_obj.modifiers.new('Subdivision', 'SUBSURF') sub_mod.subdivision_type = 'SIMPLE' disp_mod = slice_obj.modifiers.new('Displace', 'DISPLACE') disp_mod.texture_coords = 'UV' disp_mod.texture = bpy.data.textures.new(name='Terrain.' + j, type='IMAGE') disp_mod.texture.extension = 'EXTEND' disp_mod.texture.use_interpolation = False disp_mod.texture.use_mipmap = False disp_mod.texture.image = bpy.data.images.load(filepath=scn.lnx_terrain_textures+'/heightmap_' + j + '.png') f = 1 levels = 0 while f < disp_mod.texture.image.size[0]: f *= 2 levels += 1 sub_mod.levels = sub_mod.render_levels = levels return{'FINISHED'} class LNX_PT_TerrainPanel(bpy.types.Panel): bl_label = "Leenkx Terrain" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False scn = bpy.context.scene if scn == None: return layout.prop(scn, 'lnx_terrain_textures') layout.prop(scn, 'lnx_terrain_sectors') layout.prop(scn, 'lnx_terrain_sector_size') layout.prop(scn, 'lnx_terrain_height_scale') layout.operator('lnx.generate_terrain') layout.prop(scn, 'lnx_terrain_object') class LNX_PT_TilesheetPanel(bpy.types.Panel): bl_label = "Leenkx Tilesheet" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "material" bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] rows = 2 if len(wrd.lnx_tilesheetlist) > 1: rows = 4 row = layout.row() row.template_list("LNX_UL_TilesheetList", "The_List", wrd, "lnx_tilesheetlist", wrd, "lnx_tilesheetlist_index", rows=rows) col = row.column(align=True) col.operator("lnx_tilesheetlist.new_item", icon='ADD', text="") col.operator("lnx_tilesheetlist.delete_item", icon='REMOVE', text="") if len(wrd.lnx_tilesheetlist) > 1: col.separator() op = col.operator("lnx_tilesheetlist.move_item", icon='TRIA_UP', text="") op.direction = 'UP' op = col.operator("lnx_tilesheetlist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' if wrd.lnx_tilesheetlist_index >= 0 and len(wrd.lnx_tilesheetlist) > 0: dat = wrd.lnx_tilesheetlist[wrd.lnx_tilesheetlist_index] layout.prop(dat, "tilesx_prop") layout.prop(dat, "tilesy_prop") layout.prop(dat, "framerate_prop") layout.label(text='Actions') rows = 2 if len(dat.lnx_tilesheetactionlist) > 1: rows = 4 row = layout.row() row.template_list("LNX_UL_TilesheetList", "The_List", dat, "lnx_tilesheetactionlist", dat, "lnx_tilesheetactionlist_index", rows=rows) col = row.column(align=True) col.operator("lnx_tilesheetactionlist.new_item", icon='ADD', text="") col.operator("lnx_tilesheetactionlist.delete_item", icon='REMOVE', text="") if len(dat.lnx_tilesheetactionlist) > 1: col.separator() op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_UP', text="") op.direction = 'UP' op = col.operator("lnx_tilesheetactionlist.move_item", icon='TRIA_DOWN', text="") op.direction = 'DOWN' if dat.lnx_tilesheetactionlist_index >= 0 and len(dat.lnx_tilesheetactionlist) > 0: adat = dat.lnx_tilesheetactionlist[dat.lnx_tilesheetactionlist_index] layout.prop(adat, "start_prop") layout.prop(adat, "end_prop") layout.prop(adat, "loop_prop") class LnxPrintTraitsButton(bpy.types.Operator): bl_idname = 'lnx.print_traits' bl_label = 'Print All Traits' bl_description = 'Returns all traits in current blend' def execute(self, context): for s in bpy.data.scenes: print('Scene: {0}'.format(s.name)) for t in s.lnx_traitlist: if not t.enabled_prop: continue tname = "undefined" if t.type_prop == 'Haxe Script' or "Bundled": tname = t.class_name_prop if t.type_prop == 'Logic Nodes': tname = t.node_tree_prop.name if t.type_prop == 'UI Canvas': tname = t.canvas_name_prop if t.type_prop == 'WebAssembly': tname = t.webassembly_prop print('Scene Trait: {0} ("{1}")'.format(s.name, tname)) for o in s.objects: for t in o.lnx_traitlist: if not t.enabled_prop: continue tname = "undefined" if t.type_prop == 'Haxe Script' or "Bundled": tname = t.class_name_prop if t.type_prop == 'Logic Nodes': tname = t.node_tree_prop.name if t.type_prop == 'UI Canvas': tname = t.canvas_name_prop if t.type_prop == 'WebAssembly': tname = t.webassembly_prop print(' Object Trait: {0} ("{1}")'.format(o.name, tname)) return{'FINISHED'} class LNX_PT_MaterialNodePanel(bpy.types.Panel): bl_label = 'Leenkx Material Node' bl_idname = 'LNX_PT_MaterialNodePanel' bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_category = 'Leenkx' @classmethod def poll(cls, context): return (context.space_data.tree_type == 'ShaderNodeTree' and context.space_data.edit_tree and context.space_data.shader_type == 'OBJECT') def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False n = context.active_node if n != None and (n.bl_idname == 'ShaderNodeRGB' or n.bl_idname == 'ShaderNodeValue' or n.bl_idname == 'ShaderNodeTexImage'): layout.prop(context.active_node, 'lnx_material_param') class LNX_OT_CopyToBundled(bpy.types.Operator): bl_label = 'Copy To Bundled' bl_idname = 'lnx.copy_to_bundled' bl_description = ('Copies and repaths external image assets to project Bundled folder') def execute(self, context): self.copy_to_bundled(bpy.data.images) return {'FINISHED'} @classmethod def copy_to_bundled(self, data): wrd = bpy.data.worlds['Lnx'] project_path = lnx.utils.get_fp() # Blend - Images for asset in data: # File is saved if asset.filepath_from_user() != '': bundled_filepath = project_path + '/Bundled/' + asset.name try: # Exists -> Yes if os.path.isfile(bundled_filepath): # Override -> Yes if (wrd.lnx_copy_override): # Valid file if asset.has_data: asset.filepath_raw = bundled_filepath asset.save() asset.reload() # Syntax - Yellow print(log.colorize(f'Asset name "{asset.name}" already exists, overriding the original', 33), file=sys.stderr) # Invalid file or corrupted else: # Syntax - Red log.error(f'Asset name "{asset.name}" has no data to save or copy, skipping') continue # Override -> No else: # Syntax - Yellow print(log.colorize(f'Asset name "{asset.name}" already exists, skipping', 33), file=sys.stderr) continue # Exists -> No else: # Valid file if asset.has_data: asset.filepath_raw = bundled_filepath asset.save() asset.reload() # Syntax - Green print(log.colorize(f'Asset name "{asset.name}" was successfully copied', 32), file=sys.stderr) # Invalid file or corrupted else: # Syntax - Red log.error(f'Asset name "{asset.name}" has no data to save or copy, skipping') continue except: # Syntax - Red log.error(f'Insufficient write permissions or other issues occurred') continue # File is unsaved else: # Syntax - Purple log.warn(f'Asset name "{asset.name}" is either packed or unsaved, skipping') continue class LNX_OT_ShowFileVersionInfo(bpy.types.Operator): bl_label = 'Show old file version info' bl_idname = 'lnx.show_old_file_version_info' bl_description = ('Displays an info panel that warns about opening a file' 'which was created in a previous version of Leenkx') bl_options = {'INTERNAL'} wrd = None def draw_message_box(self, context): file_version = LNX_OT_ShowFileVersionInfo.wrd.lnx_version current_version = props.lnx_version layout = self.layout layout = layout.column(align=True) layout.alignment = 'EXPAND' if current_version == file_version: layout.label('This file was saved in', icon='INFO') layout.label('the current Leenkx version', icon='BLANK1') layout.separator() layout.label(f'(version: {current_version}') row = layout.row(align=True) row.active_default = True row.operator('lnx.discard_popup', text='Ok') # this will help order versions better, somewhat. # note: this is NOT complete current_version = tuple( current_version.split('.') ) file_version = tuple( file_version.split('.') ) if current_version > file_version: layout.label(text='Warning: This file was saved in a', icon='ERROR') layout.label(text='previous version of Leenkx!', icon='BLANK1') layout.separator() layout.label(text='Please inform yourself about breaking changes!', icon='BLANK1') layout.label(text=f'File saved in: {file_version}', icon='BLANK1') layout.label(text=f'Current version: {current_version}', icon='BLANK1') layout.separator() layout.separator() layout.label(text='Should Leenkx try to automatically update', icon='BLANK1') layout.label(text='the file to the current SDK version?', icon='BLANK1') layout.separator() row = layout.row(align=True) row.active_default = True row.operator('lnx.update_file_sdk', text='Yes') row.active_default = False row.operator('lnx.discard_popup', text='No') else: layout.label(text='Warning: This file was saved in a', icon='ERROR') layout.label(text='future version of Leenkx!', icon='BLANK1') layout.separator() layout.label(text='It is impossible to downgrade a file,', icon='BLANK1') layout.label(text='Something will probably be broken here.', icon='BLANK1') layout.label(text=f'File saved in: {file_version}', icon='BLANK1') layout.label(text=f'Current version: {current_version}', icon='BLANK1') layout.separator() layout.separator() layout.label(text='Please check how this file was created', icon='BLANK1') layout.separator() row = layout.row(align=True) row.active_default = True row.operator('lnx.discard_popup', text='Ok') def execute(self, context): LNX_OT_ShowFileVersionInfo.wrd = bpy.data.worlds['Lnx'] context.window_manager.popover(LNX_OT_ShowFileVersionInfo.draw_message_box, ui_units_x=16) return {"FINISHED"} class LNX_OT_ShowNodeUpdateErrors(bpy.types.Operator): bl_label = 'Show upgrade failure details' bl_idname = 'lnx.show_node_update_errors' bl_description = ('Displays an info panel that shows the different errors that occurred when upgrading nodes') wrd = None # a helper internal variable def draw_message_box(self, context): list_of_errors = lnx.logicnode.replacement.replacement_errors.copy() # note: list_of_errors is a set of tuples: `(error_type, node_class, tree_name)` # where `error_type` can be "unregistered", "update failed", "future version", "bad version", or "misc." file_version = LNX_OT_ShowNodeUpdateErrors.wrd.lnx_version current_version = props.lnx_version # this will help order versions better, somewhat. # note: this is NOT complete current_version_2 = tuple(current_version.split('.')) file_version_2 = tuple(file_version.split('.')) is_leenkx_upgrade = (current_version_2 > file_version_2) error_types = set() errored_trees = set() errored_nodes = set() for error_entry in list_of_errors: error_types.add(error_entry[0]) errored_nodes.add(error_entry[1]) errored_trees.add(error_entry[2]) layout = self.layout layout = layout.column(align=True) layout.alignment = 'EXPAND' layout.label(text="Some nodes failed to be updated to the current Leenkx version", icon="ERROR") if current_version == file_version: layout.label(text="(This might be because you are using a development snapshot, or a homemade version ;) )", icon='BLANK1') elif not is_leenkx_upgrade: layout.label(text="(Please note that it is not possible do downgrade nodes to a previous version either.", icon='BLANK1') layout.label(text="This might be the cause of your problem.)", icon='BLANK1') layout.label(text=f'File saved in: {file_version}', icon='BLANK1') layout.label(text=f'Current version: {current_version}', icon='BLANK1') layout.separator() if 'update failed' in error_types: layout.label(text="Some nodes do not have an update procedure to deal with the version saved in this file.", icon='BLANK1') if current_version == file_version: layout.label(text="(if you are a developer, this might be because you didn't implement it yet.)", icon='BLANK1') if 'bad version' in error_types: layout.label(text="Some nodes do not have version information attached to them.", icon='BLANK1') if 'unregistered' in error_types: if is_leenkx_upgrade: layout.label(text='Some nodes seem to be too old to be understood by leenkx anymore', icon='BLANK1') else: layout.label(text="Some nodes are unknown to leenkx, either because they are too new or too old.", icon='BLANK1') if 'future version' in error_types: if is_leenkx_upgrade: layout.label(text='Somehow, some nodes seem to have been created with a future version of leenkx.', icon='BLANK1') else: layout.label(text='Some nodes seem to have been created with a future version of leenkx.', icon='BLANK1') if 'misc.' in error_types: layout.label(text="Some nodes' update procedure failed to complete") layout.separator() layout.label(text='the nodes impacted are the following:', icon='BLANK1') for node in errored_nodes: layout.label(text=f' {node}', icon='BLANK1') layout.separator() layout.label(text='the node trees impacted are the following:', icon='BLANK1') for tree in errored_trees: layout.label(text=f' "{tree}"', icon='BLANK1') layout.separator() layout.label(text="A detailed error report has been saved next to the blender file.", icon='BLANK1') layout.label(text="the file name is \"node_update_failure\", followed by the current time.", icon='BLANK1') layout.separator() row = layout.row(align=True) row.active_default = False row.operator('lnx.discard_popup', text='Ok') row.operator('lnx.open_project_folder', text='Open Project Folder', icon="FILE_FOLDER") def execute(self, context): LNX_OT_ShowNodeUpdateErrors.wrd = bpy.data.worlds['Lnx'] context.window_manager.popover(LNX_OT_ShowNodeUpdateErrors.draw_message_box, ui_units_x=32) return {"FINISHED"} class LNX_OT_UpdateFileSDK(bpy.types.Operator): bl_idname = 'lnx.update_file_sdk' bl_label = 'Update file to current SDK version' bl_description = bl_label bl_options = {'INTERNAL'} def execute(self, context): wrd = bpy.data.worlds['Lnx'] # This allows for seamless migration from earlier versions of Leenkx for rp in wrd.lnx_rplist: # TODO: deprecated if rp.rp_gi != 'Off': rp.rp_gi = 'Off' rp.rp_voxelao = True # Replace deprecated nodes lnx.logicnode.replacement.replace_all() wrd.lnx_version = props.lnx_version wrd.lnx_commit = props.lnx_commit lnx.make.clean() print(f'Project updated to SDK {props.lnx_version}. Please save the .blend file.') return {'FINISHED'} class LNX_OT_DiscardPopup(bpy.types.Operator): """Empty operator for discarding dialogs.""" bl_idname = 'lnx.discard_popup' bl_label = 'OK' bl_description = 'Discard' bl_options = {'INTERNAL'} def execute(self, context): return {'FINISHED'} class LeenkxUpdateListAndroidEmulatorButton(bpy.types.Operator): '''Updating the list of emulators for the Android platform''' bl_idname = 'lnx.update_list_android_emulator' bl_label = 'Update List Emulators' def execute(self, context): if not lnx.utils.check_saved(self): return {"CANCELLED"} if not lnx.utils.check_sdkpath(self): return {"CANCELLED"} if len(lnx.utils.get_android_sdk_root_path()) == 0: return {"CANCELLED"} os.environ['ANDROID_SDK_ROOT'] = lnx.utils.get_android_sdk_root_path() items, err = lnx.utils.get_android_emulators_list() if len(err) > 0: print('Update List Emulators Warning: File "'+ lnx.utils.get_android_emulator_file() +'" not found. Check that the variable ANDROID_SDK_ROOT is correct in environment variables or in "Android SDK Path" setting: \n- If you specify an environment variable ANDROID_SDK_ROOT, then you need to restart Blender;\n- If you specify the setting "Android SDK Path", then repeat operation "Publish"') return{'FINISHED'} if len(items) > 0: items_enum = [] for i in items: items_enum.append((i, i, i)) bpy.types.World.lnx_project_android_list_avd = EnumProperty(items=items_enum, name="Emulator", update=assets.invalidate_compiler_cache) return{'FINISHED'} class LeenkxUpdateListAndroidEmulatorRunButton(bpy.types.Operator): '''Launch Android emulator selected from the list''' bl_idname = 'lnx.run_android_emulator' bl_label = 'Launch Emulator' def execute(self, context): if not lnx.utils.check_saved(self): return {"CANCELLED"} if not lnx.utils.check_sdkpath(self): return {"CANCELLED"} if len(lnx.utils.get_android_sdk_root_path()) == 0: return {"CANCELLED"} make.run_android_emulators(lnx.utils.get_android_emulator_name()) return{'FINISHED'} class LeenkxUpdateListInstalledVSButton(bpy.types.Operator): """Update the list of installed Visual Studio versions for the Windows platform""" bl_idname = 'lnx.update_list_installed_vs' bl_label = '(Re-)Fetch Installed Visual Studio Versions' def execute(self, context): if not lnx.utils.get_os_is_windows(): return {"CANCELLED"} success = lnx.utils_vs.fetch_installed_vs() if not success: self.report({"ERROR"}, "Could not fetch installed Visual Studio versions, check console for details.") return {'CANCELLED'} return {'FINISHED'} class LNX_PT_BulletDebugDrawingPanel(bpy.types.Panel): bl_label = "Leenkx Debug Drawing" bl_space_type = "PROPERTIES" bl_region_type = "WINDOW" bl_context = "scene" bl_options = {'DEFAULT_CLOSED'} bl_parent_id = "SCENE_PT_rigid_body_world" @classmethod def poll(cls, context): return context.scene.rigidbody_world is not None def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False wrd = bpy.data.worlds['Lnx'] if wrd.lnx_physics_engine != 'Bullet': row = layout.row() row.alert = True row.label(text="Physics debug drawing is only supported for the Bullet physics engine") col = layout.column(align=False) col.prop(wrd, "lnx_bullet_dbg_draw_wireframe") col.prop(wrd, "lnx_bullet_dbg_draw_aabb") col.prop(wrd, "lnx_bullet_dbg_draw_contact_points") col.prop(wrd, "lnx_bullet_dbg_draw_constraints") col.prop(wrd, "lnx_bullet_dbg_draw_constraint_limits") col.prop(wrd, "lnx_bullet_dbg_draw_normals") col.prop(wrd, "lnx_bullet_dbg_draw_axis_gizmo") def draw_custom_node_menu(self, context): """Extension of the node context menu. https://blender.stackexchange.com/questions/150101/python-how-to-add-items-in-context-menu-in-2-8 """ if context.selected_nodes is None or len(context.selected_nodes) != 1: return if context.space_data.tree_type == 'LnxLogicTreeType': if context.selected_nodes[0].bl_idname.startswith('LN'): layout = self.layout layout.separator() layout.operator("lnx.open_node_documentation", text="Show documentation for this node", icon='HELP') layout.operator("lnx.open_node_haxe_source", text="Open .hx source in the browser", icon_value=ui_icons.get_id("haxe")) layout.operator("lnx.open_node_python_source", text="Open .py source in the browser", icon='FILE_SCRIPT') elif context.space_data.tree_type == 'ShaderNodeTree': if context.active_node.bl_idname in ('ShaderNodeRGB', 'ShaderNodeValue', 'ShaderNodeTexImage'): layout = self.layout layout.separator() layout.prop(context.active_node, 'lnx_material_param', text='Leenkx: Material Parameter') def draw_conditional_prop(layout: bpy.types.UILayout, heading: str, data: bpy.types.AnyType, prop_condition: str, prop_value: str) -> None: """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) row = col.row() row.prop(data, prop_condition, text='') sub = row.row() sub.enabled = getattr(data, prop_condition) sub.prop(data, prop_value, expand=True) def draw_error_box(layout: bpy.types.UILayout, text: str) -> bpy.types.UILayout: """Draw an error box in the given UILayout and return it for further optional modification. The text is wrapped automatically according to the current region's width. """ textwrap_width = max(0, int((bpy.context.region.width - 25) / 6)) lines = textwrap.wrap(text, width=textwrap_width, break_long_words=True) box = layout.box() col = box.column(align=True) col.alert = True for idx, line in enumerate(lines): col.label(text=line, icon='ERROR' if idx == 0 else 'BLANK1') return box def draw_multiline_with_icon(layout: bpy.types.UILayout, layout_width_px: int, icon: str, text: str) -> bpy.types.UILayout: """Draw a multiline string with an icon in the given UILayout and return it for further optional modification. The text is wrapped according to the given layout width. """ textwrap_width = max(0, layout_width_px // 6) lines = textwrap.wrap(text, width=textwrap_width, break_long_words=True) col = layout.column(align=True) col.scale_y = 0.8 for idx, line in enumerate(lines): col.label(text=line, icon=icon if idx == 0 else 'BLANK1') return col __REG_CLASSES = ( LNX_PT_ObjectPropsPanel, LNX_PT_ModifiersPropsPanel, LNX_PT_ParticlesPropsPanel, LNX_PT_PhysicsPropsPanel, LNX_PT_DataPropsPanel, LNX_PT_ScenePropsPanel, LNX_PT_WorldPropsPanel, InvalidateCacheButton, InvalidateMaterialCacheButton, LNX_OT_NewCustomMaterial, LNX_PG_BindTexturesListItem, LNX_UL_BindTexturesList, LNX_OT_BindTexturesListNewItem, LNX_OT_BindTexturesListDeleteItem, LNX_PT_MaterialPropsPanel, LNX_PT_BindTexturesPropsPanel, LNX_PT_MaterialBlendingPropsPanel, LNX_PT_MaterialDriverPropsPanel, LNX_PT_LeenkxPlayerPanel, LNX_PT_TopbarPanel, LNX_PT_LeenkxExporterPanel, LNX_PT_LeenkxExporterAndroidSettingsPanel, LNX_PT_LeenkxExporterAndroidPermissionsPanel, LNX_PT_LeenkxExporterAndroidAbiPanel, LNX_PT_LeenkxExporterAndroidBuildAPKPanel, LNX_PT_LeenkxExporterHTML5SettingsPanel, LNX_PT_LeenkxExporterWindowsSettingsPanel, LNX_PT_LeenkxProjectPanel, LNX_PT_ProjectFlagsPanel, LNX_PT_ProjectFlagsDebugConsolePanel, LNX_PT_ProjectWindowPanel, LNX_PT_ProjectModulesPanel, LNX_PT_RenderPathPanel, LNX_PT_RenderPathRendererPanel, LNX_PT_RenderPathShadowsPanel, LNX_PT_RenderPathVoxelsPanel, LNX_PT_RenderPathWorldPanel, LNX_PT_RenderPathPostProcessPanel, LNX_PT_RenderPathCompositorPanel, LNX_PT_BakePanel, # LnxVirtualInputPanel, LeenkxPlayButton, LeenkxStopButton, LeenkxBuildProjectButton, LeenkxOpenProjectFolderButton, LeenkxOpenEditorButton, CleanMenu, CleanButtonMenu, LeenkxCleanProjectButton, LeenkxPublishProjectButton, LnxGenLodButton, LNX_PT_LodPanel, LnxGenTerrainButton, LNX_PT_TerrainPanel, LNX_PT_TilesheetPanel, LnxPrintTraitsButton, LNX_PT_MaterialNodePanel, LNX_OT_UpdateFileSDK, LNX_OT_CopyToBundled, LNX_OT_ShowFileVersionInfo, LNX_OT_ShowNodeUpdateErrors, LNX_OT_DiscardPopup, LeenkxUpdateListAndroidEmulatorButton, LeenkxUpdateListAndroidEmulatorRunButton, LeenkxUpdateListInstalledVSButton, LNX_PT_BulletDebugDrawingPanel, LNX_OT_AddArmatureRootMotion, scene.TLM_PT_Settings, scene.TLM_PT_Denoise, scene.TLM_PT_Filtering, scene.TLM_PT_Encoding, scene.TLM_PT_Utility, scene.TLM_PT_Additional, ) __reg_classes, __unreg_classes = bpy.utils.register_classes_factory(__REG_CLASSES) def register(): __reg_classes() bpy.types.VIEW3D_HT_header.append(draw_view3d_header) bpy.types.VIEW3D_MT_object.append(draw_view3d_object_menu) bpy.types.NODE_MT_context_menu.append(draw_custom_node_menu) bpy.types.TOPBAR_HT_upper_bar.prepend(draw_space_topbar) bpy.types.Material.lnx_bind_textures_list = CollectionProperty(type=LNX_PG_BindTexturesListItem) bpy.types.Material.lnx_bind_textures_list_index = IntProperty(name='Index for lnx_bind_textures_list', default=0) def unregister(): bpy.types.NODE_MT_context_menu.remove(draw_custom_node_menu) bpy.types.VIEW3D_MT_object.remove(draw_view3d_object_menu) bpy.types.VIEW3D_HT_header.remove(draw_view3d_header) bpy.types.TOPBAR_HT_upper_bar.remove(draw_space_topbar) __unreg_classes()