diff --git a/.gitignore b/.gitignore index 885bf479..47349ee9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ -# Blender-For-UnrealEngine-Addons +# Docs exported files +ExportedFbx/ + +# BleuRaven Blender Addons +/generated_builds **.zip -**.blend[1-9]* +**.blend[1-32]* .git/ .vscode/ .vs/ -ExportedFbx/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/ReleaseLogs/Version_4.3.2.md b/ReleaseLogs/Version_4.3.2.md new file mode 100644 index 00000000..0fa87b4d --- /dev/null +++ b/ReleaseLogs/Version_4.3.2.md @@ -0,0 +1,6 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.2 + +- New: Addon renamed from "Blender For Unreal Engine" To "Unreal Engine Assets Exporter". This was needed to follow [Blender Extensions Terms of Service](https://extensions.blender.org/terms-of-service/) (Branding) diff --git a/ReleaseLogs/Version_4.3.3.md b/ReleaseLogs/Version_4.3.3.md new file mode 100644 index 00000000..a2f36c65 --- /dev/null +++ b/ReleaseLogs/Version_4.3.3.md @@ -0,0 +1,7 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.3 + +- Update import scripts to avoid sys.path manipulations durring BFU module imports. +- Fixed: If export folder is relative empty "//" addons export assets at the disc root. diff --git a/ReleaseLogs/Version_4.3.4.md b/ReleaseLogs/Version_4.3.4.md new file mode 100644 index 00000000..4bdcc237 --- /dev/null +++ b/ReleaseLogs/Version_4.3.4.md @@ -0,0 +1,9 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.4 + +- Update import scripts to avoid sys.argv manipulations durring BFU module imports. +- Removed useless fbxio folders depending Blender Version. +- Fixed: If export folder is relative empty "//" addons export import scripts at the disc root. + diff --git a/ReleaseLogs/Version_4.3.5.md b/ReleaseLogs/Version_4.3.5.md new file mode 100644 index 00000000..4fef1226 --- /dev/null +++ b/ReleaseLogs/Version_4.3.5.md @@ -0,0 +1,7 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.5 + +- Fix copyright. +- Fix import script fail. (import 'importlib.util' and sys.modules manipulation) \ No newline at end of file diff --git a/ReleaseLogs/Version_4.3.6.md b/ReleaseLogs/Version_4.3.6.md new file mode 100644 index 00000000..5efcf550 --- /dev/null +++ b/ReleaseLogs/Version_4.3.6.md @@ -0,0 +1,6 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.6 + +- Fixed: Names in data_path are not escaped. \ No newline at end of file diff --git a/ReleaseLogs/Version_4.3.7.md b/ReleaseLogs/Version_4.3.7.md new file mode 100644 index 00000000..043bc8df --- /dev/null +++ b/ReleaseLogs/Version_4.3.7.md @@ -0,0 +1,14 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.7 + +- New: Support for Unreal Engine 5.5. +- New: Correct Extrem UV Scale: Added the "Use Positive Pos" option to keep UV islands in positive positions. +- New: Correct Extrem UV Scale: Added the "Step Scale" option for export. +- New: Correct Extrem UV Scale: Added the "Use Positive Pos" option for export. +- Changes: The addon now uses the new Interchange Assets pipeline for importing assets into Unreal Engine 5.5. +- Fixed: Correct Extrem UV Scale: UV changes are now applied to the entire asset. +- Fixed: Animations were not exported to subfolders on disk. +- Fixed: NLA lost animated_influence FCurve after export. +- Cleanup: Removed debug logs. diff --git a/ReleaseLogs/Version_4.3.8.md b/ReleaseLogs/Version_4.3.8.md new file mode 100644 index 00000000..8b9c0b14 --- /dev/null +++ b/ReleaseLogs/Version_4.3.8.md @@ -0,0 +1,12 @@ +# Unreal Engine Assets Exporter - Release Log +Release Logs: https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Release-Logs + +### Version 4.3.8 + +- Full addon refactoring for better properties and UI management and organization. +- Fixed: Custom Skeletal Mesh Ref not well used. +- Fixed: Skeleton search use only loaded assets. +- Fixed: Export curve as static mesh do export fail. +- Fixed: Script fail at import for Unreal Engine 4.27 +- Fixed: Script fail at import in Unreal Engine when python plugins is disabled. +- Fixed: Import mesh with multiple lods may create new material slots. \ No newline at end of file diff --git a/blender-for-unrealengine/__init__.py b/blender-for-unrealengine/__init__.py index e34e5dd7..6bb0b904 100644 --- a/blender-for-unrealengine/__init__.py +++ b/blender-for-unrealengine/__init__.py @@ -32,13 +32,22 @@ import bpy import importlib -from . import bps +from . import bpl from . import bbpl from . import bfu_assets_manager from . import bfu_propertys +from . import bfu_base_object +from . import bfu_adv_object +from . import bfu_base_collection from . import bfu_static_mesh from . import bfu_skeletal_mesh +from . import bfu_modular_skeletal_mesh from . import bfu_alembic_animation +from . import bfu_anim_base +from . import bfu_anim_action +from . import bfu_anim_action_adv +from . import bfu_anim_nla +from . import bfu_anim_nla_adv from . import bfu_groom from . import bfu_camera from . import bfu_spline @@ -47,8 +56,14 @@ from . import bfu_material from . import bfu_vertex_color from . import bfu_lod +from . import bfu_uv_map +from . import bfu_light_map +from . import bfu_assets_references from . import bfu_custom_property from . import bfu_addon_parts +from . import bfu_export_nomenclature +from . import bfu_export_filter +from . import bfu_export_process from . import bfu_export_procedure from . import bfu_addon_pref from . import bfu_export_logs @@ -68,20 +83,39 @@ from . import bfu_cached_asset_list -if "bps" in locals(): - importlib.reload(bps) + +if "bpl" in locals(): + importlib.reload(bpl) if "bbpl" in locals(): importlib.reload(bbpl) if "bfu_assets_manager" in locals(): importlib.reload(bfu_assets_manager) if "bfu_propertys" in locals(): importlib.reload(bfu_propertys) +if "bfu_base_object" in locals(): + importlib.reload(bfu_base_object) +if "bfu_adv_object" in locals(): + importlib.reload(bfu_adv_object) +if "bfu_base_collection" in locals(): + importlib.reload(bfu_base_collection) if "bfu_static_mesh" in locals(): importlib.reload(bfu_static_mesh) if "bfu_skeletal_mesh" in locals(): importlib.reload(bfu_skeletal_mesh) +if "bfu_modular_skeletal_mesh" in locals(): + importlib.reload(bfu_modular_skeletal_mesh) if "bfu_alembic_animation" in locals(): importlib.reload(bfu_alembic_animation) +if "bfu_anim_base" in locals(): + importlib.reload(bfu_anim_base) +if "bfu_anim_action" in locals(): + importlib.reload(bfu_anim_action) +if "bfu_anim_action_adv" in locals(): + importlib.reload(bfu_anim_action_adv) +if "bfu_anim_nla" in locals(): + importlib.reload(bfu_anim_nla) +if "bfu_anim_nla_adv" in locals(): + importlib.reload(bfu_anim_nla_adv) if "bfu_groom" in locals(): importlib.reload(bfu_groom) if "bfu_camera" in locals(): @@ -98,10 +132,22 @@ importlib.reload(bfu_vertex_color) if "bfu_lod" in locals(): importlib.reload(bfu_lod) +if "bfu_uv_map" in locals(): + importlib.reload(bfu_uv_map) +if "bfu_light_map" in locals(): + importlib.reload(bfu_light_map) +if "bfu_assets_references" in locals(): + importlib.reload(bfu_assets_references) if "bfu_custom_property" in locals(): importlib.reload(bfu_custom_property) if "bfu_addon_parts" in locals(): importlib.reload(bfu_addon_parts) +if "bfu_export_nomenclature" in locals(): + importlib.reload(bfu_export_nomenclature) +if "bfu_export_filter" in locals(): + importlib.reload(bfu_export_filter) +if "bfu_export_process" in locals(): + importlib.reload(bfu_export_process) if "bfu_export_procedure" in locals(): importlib.reload(bfu_export_procedure) if "bfu_addon_pref" in locals(): @@ -137,19 +183,7 @@ if "bfu_cached_asset_list" in locals(): importlib.reload(bfu_cached_asset_list) -bl_info = { - 'name': 'Blender for UnrealEngine', - 'author': 'Loux Xavier (BleuRaven)', - 'version': (4, 3, 1), - 'blender': (2, 80, 0), - 'location': 'View3D > UI > Unreal Engine', - 'description': "This add-ons allows to easily export several objects at the same time and import in Unreal Engine.", - 'warning': '', - "wiki_url": "https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki", - 'tracker_url': 'https://github.com/xavier150/Blender-For-UnrealEngine-Addons/issues', - 'support': 'COMMUNITY', - 'category': 'Import-Export'} - +bl_info = {} class BFUCachedAction(bpy.types.PropertyGroup): """ @@ -168,9 +202,18 @@ def register(): bbpl.register() bfu_assets_manager.register() bfu_propertys.register() + bfu_base_object.register() + bfu_adv_object.register() + bfu_base_collection.register() bfu_static_mesh.register() bfu_skeletal_mesh.register() + bfu_modular_skeletal_mesh.register() bfu_alembic_animation.register() + bfu_anim_base.register() + bfu_anim_action.register() + bfu_anim_action_adv.register() + bfu_anim_nla.register() + bfu_anim_nla_adv.register() bfu_groom.register() bfu_camera.register() bfu_spline.register() @@ -179,8 +222,14 @@ def register(): bfu_material.register() bfu_vertex_color.register() bfu_lod.register() + bfu_uv_map.register() + bfu_light_map.register() + bfu_assets_references.register() bfu_custom_property.register() bfu_addon_parts.register() + bfu_export_nomenclature.register() + bfu_export_filter.register() + bfu_export_process.register() bfu_export_procedure.register() bfu_addon_pref.register() bfu_export_logs.register() @@ -200,8 +249,14 @@ def unregister(): bfu_export_logs.unregister() bfu_addon_pref.unregister() bfu_export_procedure.unregister() + bfu_export_process.unregister() + bfu_export_filter.unregister() + bfu_export_nomenclature.unregister() bfu_addon_parts.unregister() bfu_custom_property.unregister() + bfu_assets_references.unregister() + bfu_light_map.unregister() + bfu_uv_map.unregister() bfu_lod.unregister() bfu_vertex_color.unregister() bfu_material.unregister() @@ -209,10 +264,19 @@ def unregister(): bfu_collision.unregister() bfu_spline.unregister() bfu_camera.unregister() + bfu_anim_nla_adv.unregister() + bfu_anim_nla.unregister() + bfu_anim_action_adv.unregister() + bfu_anim_action.unregister() + bfu_anim_base.unregister() bfu_alembic_animation.unregister() bfu_groom.unregister() + bfu_modular_skeletal_mesh.unregister() bfu_skeletal_mesh.unregister() bfu_static_mesh.unregister() + bfu_base_collection.unregister() + bfu_adv_object.unregister() + bfu_base_object.unregister() bfu_propertys.unregister() bfu_assets_manager.unregister() bbpl.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/addon_generate_config.json b/blender-for-unrealengine/addon_generate_config.json new file mode 100644 index 00000000..e67ebe10 --- /dev/null +++ b/blender-for-unrealengine/addon_generate_config.json @@ -0,0 +1,89 @@ +{ + "schema_version": [1,0,0], + "blender_manifest": { + "id": "unrealengine_assets_exporter", + "version": [4,3,8], + "name": "Unreal Engine Assets Exporter", + "tagline": "Allows to batch export and import in Unreal Engine", + "maintainer": "Loux Xavier (BleuRaven) xavierloux.loux@gmail.com", + + "website_url": "https://github.com/xavier150/Blender-For-UnrealEngine-Addons/", + "report_issue_url": "https://github.com/xavier150/Blender-For-UnrealEngine-Addons/issues", + "support": "COMMUNITY", + + "type": "add-on", + "tags": ["Import-Export"], + "category": "Import-Export", + "license": ["SPDX:GPL-3.0-or-later"], + + "copyright": [ + "2024 Xavier Loux", + "2013 Blender Foundation", + "2006-2012 assimp team", + "2013 Campbell Barton", + "2014 Bastien Montagne" + ], + + "permissions": { + "files": "Import/export FBX from/to disk", + "clipboard": "Copy generated script paths" + } + }, + + "builds": { + "unrealengine_assets_exporter_4.3": { + "generate_method": "EXTENTION_COMMAND", + "auto_install_range": [[4,3,0], [4,3,0]], + "naming": "{Name}_{Version}-blender_4.3.zip", + "module": "blender-for-unrealengine", + "pkg_id": "unrealengine_assets_exporter", + "exclude_paths": [ + "fbxio/" + ], + "include_paths": [ + "fbxio/__init__.py/", + "fbxio/io_scene_fbx_4_3/" + ], + "blender_version_min": [4,3,0] + }, + "unrealengine_assets_exporter_4.2": { + "generate_method": "EXTENTION_COMMAND", + "auto_install_range": [[4,2,0], [4,2,3]], + "naming": "{Name}_{Version}-blender_4.2.zip", + "module": "blender-for-unrealengine", + "pkg_id": "unrealengine_assets_exporter", + "exclude_paths": [ + "fbxio/" + ], + "include_paths": [ + "fbxio/__init__.py/", + "fbxio/io_scene_fbx_4_2/" + ], + "blender_version_min": [4,2,0] + }, + "unrealengine_assets_exporter_2.8": { + "generate_method": "SIMPLE_ZIP", + "auto_install_range": [[2,80,0], [4,1,0]], + "naming": "{Name}_{Version}-blender_2.8-4.1.zip", + "module": "blender-for-unrealengine", + "pkg_id": "unrealengine_assets_exporter", + "exclude_paths": [ + "fbxio/" + ], + "include_paths": [ + "fbxio/__init__.py/", + "fbxio/io_scene_fbx_4_1/", + "fbxio/io_scene_fbx_4_0/", + "fbxio/io_scene_fbx_3_6/", + "fbxio/io_scene_fbx_3_5/", + "fbxio/io_scene_fbx_3_4/", + "fbxio/io_scene_fbx_3_3/", + "fbxio/io_scene_fbx_3_2/", + "fbxio/io_scene_fbx_3_1/", + "fbxio/io_scene_fbx_2_93/", + "fbxio/io_scene_fbx_2_83/" + ], + "blender_version_min": [2,80,0] + } + } +} \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/__init__.py b/blender-for-unrealengine/bbam/__init__.py new file mode 100644 index 00000000..7994211d --- /dev/null +++ b/blender-for-unrealengine/bbam/__init__.py @@ -0,0 +1,110 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import os +import json +import importlib + +from . import config +from . import manifest_generate +from . import bl_info_generate +from . import addon_file_management +from . import utils +from . import blender_exec +from . import blender_utils + +# Reloading modules if they're already loaded +if "config" in locals(): + importlib.reload(config) +if "manifest_generate" in locals(): + importlib.reload(manifest_generate) +if "bl_info_generate" in locals(): + importlib.reload(bl_info_generate) +if "addon_file_management" in locals(): + importlib.reload(addon_file_management) +if "utils" in locals(): + importlib.reload(utils) +if "blender_exec" in locals(): + importlib.reload(blender_exec) +if "blender_utils" in locals(): + importlib.reload(blender_utils) + +def install_from_blender(): + """ + Loads the addon's configuration file to retrieve its manifest data and initiates + the installation process within Blender. + """ + # Get the path of the current addon's configuration file from `config` + addon_manifest = config.addon_generate_config + + # Construct absolute paths for addon and manifest file + addon_path = os.path.abspath(os.path.join(__file__, '..', '..')) + search_addon_folder = os.path.abspath(os.path.join(addon_path, addon_manifest)) + + # Load the manifest file data if it exists + if os.path.isfile(search_addon_folder): + with open(search_addon_folder, 'r', encoding='utf-8') as file: + data = json.load(file) + install_from_blender_with_build_data(addon_path, data) + else: + print(f"Error: '{addon_manifest}' was not found in '{search_addon_folder}'.") + +def install_from_blender_with_build_data(addon_path, addon_manifest_data): + """ + Manages the addon installation in Blender based on the build data from the manifest. + + Parameters: + addon_path (str): The path to the addon's root directory. + addon_manifest_data (dict): The data structure containing build specifications. + """ + # Import bpy lib here when exec from Blender. + import bpy + + # Get Blender executable path from bpy + blender_executable_path = bpy.app.binary_path + + # Process each build specified in the manifest data + for target_build_name in addon_manifest_data["builds"]: + # Create temporary addon folder + temp_addon_path = addon_file_management.create_temp_addon_folder( + addon_path, addon_manifest_data, target_build_name + ) + # Zip the addon folder for installation + zip_file = addon_file_management.zip_addon_folder( + temp_addon_path, addon_path, addon_manifest_data, target_build_name, blender_executable_path + ) + + build_data = addon_manifest_data["builds"][target_build_name] + pkg_id = build_data.get("pkg_id") + module = build_data.get("module") + + # Check if the addon should be installed based on Blender's version + auto_install_range = utils.get_tuple_range_version(build_data.get("auto_install_range")) + should_install = utils.get_version_in_range(bpy.app.version, auto_install_range) + if should_install: + + # Uninstall previous versions if they exist + blender_utils.uninstall_addon_from_blender(bpy, pkg_id, module) + blender_utils.install_zip_addon_from_blender(bpy, zip_file, module) \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/addon_file_management.py b/blender-for-unrealengine/bbam/addon_file_management.py new file mode 100644 index 00000000..bd2add4a --- /dev/null +++ b/blender-for-unrealengine/bbam/addon_file_management.py @@ -0,0 +1,188 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import shutil +import tempfile +import sys +import os +from . import manifest_generate +from . import bl_info_generate +from . import config +from . import utils +from . import blender_exec +from . import blender_utils + + +def copy_addon_folder(src, dst, exclude_paths=[], include_paths=[]): + """ + Copies the addon folder from 'src' to 'dst' while excluding specified files and folders. + + Parameters: + src (str): Source path of the addon. + dst (str): Destination path for the copied addon. + exclude_paths (list): List of file or folder paths to exclude during the copy process. + """ + # Normalize paths for comparison + exclude_paths = [os.path.normpath(path) for path in exclude_paths] + include_paths = [os.path.normpath(path) for path in include_paths] + + # Ignore function to exclude specific files/folders during the copy + def ignore_files(dir, files): + ignore_list = [] + for file in files: + file_path = os.path.join(dir, file) + relative_path = os.path.normpath(os.path.relpath(file_path, src)) + + # Skip directories if only files should be ignored + if os.path.isdir(file_path): + continue + + # Check if the file path should be included + if any(relative_path.startswith(os.path.normpath(path)) for path in include_paths): + continue # Skip excluding this file or folder + + # Check if the file path should be excluded + if any(relative_path.startswith(os.path.normpath(path)) for path in exclude_paths): + ignore_list.append(file) + return set(ignore_list) + + shutil.copytree(src, dst, ignore=ignore_files) + + +def create_temp_addon_folder(addon_path, addon_manifest_data, target_build_name, show_debug=True): + """ + Creates a temporary folder for the addon, copies relevant files, and generates the manifest. + + Parameters: + addon_path (str): Root path of the addon. + addon_manifest_data (dict): Manifest data containing build specifications. + target_build_name (str): Name of the target build configuration. + show_debug (bool): If True, debug information is displayed. + + Returns: + str: Path to the temporary addon folder. + """ + build_data = addon_manifest_data["builds"][target_build_name] + generate_method = build_data["generate_method"] + + # Step 1: Create a temporary directory for the addon + temp_dir = tempfile.mkdtemp(prefix="blender_addon_") + temp_addon_path = os.path.join(temp_dir, os.path.basename(addon_path)) + + # Step 2: Copy addon folder to temporary directory, excluding specified paths + exclude_paths = build_data.get("exclude_paths", []) + include_paths = build_data.get("include_paths", []) + exclude_paths.append("bbam/") # Exclude addon manager from the final build + copy_addon_folder(addon_path, temp_addon_path, exclude_paths, include_paths) + print(f"Copied build '{target_build_name}' to temporary location: {temp_addon_path}") + + # Step 3: Generate addon manifest based on generation method + if generate_method == "EXTENTION_COMMAND": + new_manifest = manifest_generate.generate_new_manifest(addon_manifest_data, target_build_name) + manifest_generate.save_addon_manifest(temp_addon_path, new_manifest, show_debug) + elif generate_method == "SIMPLE_ZIP": + new_manifest = bl_info_generate.generate_new_bl_info(addon_manifest_data, target_build_name) + bl_info_generate.update_file_bl_info(temp_addon_path, new_manifest, show_debug) + + return temp_addon_path + +def get_zip_output_filename(addon_path, addon_manifest_data, target_build_name): + """ + Generates the output filename for the ZIP file based on naming conventions in the manifest. + + Parameters: + addon_path (str): Root path of the addon. + addon_manifest_data (dict): Manifest data containing build specifications. + target_build_name (str): Name of the target build configuration. + + Returns: + str: Full path of the output ZIP file. + """ + manifest_data = addon_manifest_data["blender_manifest"] + build_data = addon_manifest_data["builds"][target_build_name] + version = utils.get_str_version(manifest_data["version"]) + + # Formatting output filename + output_folder_path = os.path.abspath(os.path.join(addon_path, '..', config.build_output_folder)) + formatted_file_name = build_data["naming"].replace("{Name}", build_data["pkg_id"]).replace("{Version}", version) + output_filepath = os.path.join(output_folder_path, formatted_file_name) + return output_filepath + +def zip_addon_folder(src, addon_path, addon_manifest_data, target_build_name, blender_executable_path): + """ + Creates a ZIP archive of the addon folder, either through Blender's extension command + or by using a simple ZIP method. + + Parameters: + src (str): Path to the source addon folder. + addon_path (str): Root path of the addon. + addon_manifest_data (dict): Manifest data containing build specifications. + target_build_name (str): Name of the target build configuration. + blender_executable_path (str): Path to the Blender executable for running commands. + + Returns: + str: Path to the created ZIP file. + """ + build_data = addon_manifest_data["builds"][target_build_name] + generate_method = build_data["generate_method"] + + # Define output file path and ensure the output directory exists + output_filepath = get_zip_output_filename(addon_path, addon_manifest_data, target_build_name) + output_dir = os.path.dirname(output_filepath) + os.makedirs(output_dir, exist_ok=True) + + # Run addon zip process based on the specified generation method + if generate_method == "EXTENTION_COMMAND": + print("Start build with extension command") + result = blender_exec.build_extension(src, output_filepath, blender_executable_path) + print(result.stdout) + print(result.stderr, file=sys.stderr) + + created_filename = blender_exec.get_build_file(result) + if created_filename: + print("Start Validate") + blender_exec.validate_extension(created_filename, blender_executable_path) + print("End Validate") + + return created_filename + + elif generate_method == "SIMPLE_ZIP": + print("Start creating simple ZIP file with root folder using shutil") + + # Specify the root folder name inside the ZIP file + root_folder_name = "my_addon_root_folder" + + # Create a temporary directory + with tempfile.TemporaryDirectory() as temp_dir: + # Copy the source folder to a temporary root folder + temp_root = os.path.join(temp_dir, root_folder_name) + shutil.copytree(src, temp_root) + + # Use shutil to create the ZIP archive from the temporary directory + base_name = os.path.splitext(output_filepath)[0] # Path without .zip extension + shutil.make_archive(base_name, 'zip', temp_dir, root_folder_name) + + print(f"SIMPLE_ZIP created successfully at {output_filepath}") + return output_filepath \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/bl_info_generate.py b/blender-for-unrealengine/bbam/bl_info_generate.py new file mode 100644 index 00000000..da3dfde5 --- /dev/null +++ b/blender-for-unrealengine/bbam/bl_info_generate.py @@ -0,0 +1,101 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import os +import re +from . import config + +def generate_new_bl_info(addon_generate_config_data, target_build_name): + """ + Generates a new `bl_info` dictionary for the addon based on the configuration data. + + Parameters: + addon_generate_config_data (dict): Data containing addon configurations. + target_build_name (str): The name of the target build configuration. + + Returns: + dict: A dictionary representing the new `bl_info` for the addon. + """ + # Check if the target build data exists + if target_build_name not in addon_generate_config_data["builds"]: + print(f"Error: Build data for '{target_build_name}' not found!") + return {} + + manifest_data = addon_generate_config_data["blender_manifest"] + build_data = addon_generate_config_data["builds"][target_build_name] + + # Populate `bl_info` with addon details + data = { + 'name': manifest_data["name"], + 'author': manifest_data["maintainer"], + 'version': tuple(manifest_data["version"]), + 'blender': tuple(build_data["blender_version_min"]), + 'location': 'View3D > UI > Unreal Engine', + 'description': manifest_data["tagline"], + 'warning': '', + "wiki_url": manifest_data["website_url"], + 'tracker_url': manifest_data["report_issue_url"], + 'support': manifest_data["support"], + 'category': manifest_data["category"] + } + + return data + +def update_file_bl_info(addon_path, data, show_debug=False): + """ + Updates the `bl_info` dictionary in the addon's __init__.py file with new data. + + Parameters: + addon_path (str): Path to the addon's root folder. + data (dict): New `bl_info` dictionary to update in the file. + show_debug (bool): If True, displays debug information about the update process. + """ + addon_init_file_path = os.path.join(addon_path, "__init__.py") + + # Format the new `bl_info` dictionary with line breaks and indentation + new_bl_info_lines = ["bl_info = {\n"] + for key, value in data.items(): + new_bl_info_lines.append(f" '{key}': {repr(value)},\n") + new_bl_info_lines.append("}\n\n") # Close `bl_info` and add an extra line break for readability + + # Read the existing lines of the __init__.py file + with open(addon_init_file_path, 'r') as file: + lines = file.readlines() + + # Write the updated lines, replacing the old `bl_info` if it exists + with open(addon_init_file_path, "w") as file: + in_bl_info = False + for line in lines: + # Detect the start of `bl_info` + if line.strip().startswith("bl_info = {") and not in_bl_info: + in_bl_info = True + file.writelines(new_bl_info_lines) # Write the new `bl_info` dictionary + elif in_bl_info and line.strip() == "}": + in_bl_info = False # End of `bl_info`, but skip this closing brace line + elif not in_bl_info: + file.write(line) # Write all other lines unchanged + + if show_debug: + print(f"Addon bl_info successfully updated at: {addon_init_file_path}") \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/blender_exec.py b/blender-for-unrealengine/bbam/blender_exec.py new file mode 100644 index 00000000..f48d12ad --- /dev/null +++ b/blender-for-unrealengine/bbam/blender_exec.py @@ -0,0 +1,85 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import sys +import re +import subprocess + +def build_extension(src, dst, blender_executable_path): + """ + Builds an extension using Blender's executable with specified source and destination paths. + + Parameters: + src (str): Path to the source directory of the extension. + dst (str): Destination path for the built extension. + blender_executable_path (str): Path to the Blender executable. + + Returns: + subprocess.CompletedProcess: The result of the subprocess command execution. + """ + command = [ + blender_executable_path, + '--command', 'extension', 'build', + '--source-dir', src, + '--output-filepath', dst, + ] + result = subprocess.run(command, capture_output=True, text=True) + return result + +def get_build_file(build_result): + """ + Extracts the path of the created build file from the build result output. + + Parameters: + build_result (subprocess.CompletedProcess): The result of the build command. + + Returns: + str: The path of the created build file, if found; otherwise, None. + """ + match = re.search(r'created: "([^"]+)"', build_result.stdout) + if match: + return match.group(1) + return None + +def validate_extension(path, blender_executable_path): + """ + Validates the built extension using Blender's executable. + + Parameters: + path (str): Path to the extension file to validate. + blender_executable_path (str): Path to the Blender executable. + """ + validate_command = [ + blender_executable_path, + '--command', 'extension', 'validate', + path, + ] + result = subprocess.run(validate_command, capture_output=True, text=True) + + # Output results for debugging purposes + if result.returncode == 0: + print("Validation successful.") + else: + print(f"Validation failed. Error: {result.stderr}", file=sys.stderr) \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/blender_utils.py b/blender-for-unrealengine/bbam/blender_utils.py new file mode 100644 index 00000000..59f85338 --- /dev/null +++ b/blender-for-unrealengine/bbam/blender_utils.py @@ -0,0 +1,66 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import os + +def uninstall_addon_from_blender(bpy, pkg_id, module): + """ + Uninstalls an addon from Blender, using the correct method based on Blender's version. + + Parameters: + bpy (module): Blender's Python API module. + pkg_id (str): Package ID for the extension (used in newer Blender versions). + module (str): Name of the addon module to uninstall. + """ + # For Blender version 4.2.0 and above, use `package_uninstall` + if bpy.app.version >= (4, 2, 0): + print(f"Uninstalling extension '{pkg_id}'...") + bpy.ops.extensions.package_uninstall(repo_index=1, pkg_id=pkg_id) + bpy.ops.preferences.addon_remove(module=module) + else: + # For earlier versions, directly remove the addon using `addon_remove` + print(f"Uninstalling add-on '{module}'...") + bpy.ops.preferences.addon_remove(module=module) + +def install_zip_addon_from_blender(bpy, zip_file, module): + """ + Installs a ZIP addon file in Blender, using the correct method based on Blender's version. + + Parameters: + bpy (module): Blender's Python API module. + zip_file (str): Path to the ZIP file containing the addon. + module (str): Name of the addon module to enable after installation. + """ + if bpy.app.version >= (4, 2, 0): + # For Blender version 4.2.0 and above, install as an extension + print("Installing as extension...", zip_file) + bpy.ops.extensions.package_install_files(repo="user_default", filepath=zip_file, enable_on_install=True) + print("Extension installation complete.") + else: + # For earlier versions, install and enable as an addon + print("Installing as add-on...", zip_file) + bpy.ops.preferences.addon_install(overwrite=True, filepath=zip_file) + bpy.ops.preferences.addon_enable(module=module) + print("Add-on installation complete.") diff --git a/blender-for-unrealengine/bbam/config.py b/blender-for-unrealengine/bbam/config.py new file mode 100644 index 00000000..04235c04 --- /dev/null +++ b/blender-for-unrealengine/bbam/config.py @@ -0,0 +1,38 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +# Configuration file names and paths for the addon build process + +# JSON file containing addon generation configurations +addon_generate_config = "addon_generate_config.json" + +# TOML file containing the Blender manifest details +blender_manifest = "blender_manifest.toml" + +# Version of the manifest schema being used +manifest_schema_version = "1.0.0" + +# Folder where the generated build files will be stored +build_output_folder = "generated_builds" diff --git a/blender-for-unrealengine/bbam/exec/install_from_blender.py b/blender-for-unrealengine/bbam/exec/install_from_blender.py new file mode 100644 index 00000000..3c091d05 --- /dev/null +++ b/blender-for-unrealengine/bbam/exec/install_from_blender.py @@ -0,0 +1,65 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import os +import sys +import importlib.util + +# get bbam __init__.py file +bbam_path = os.path.abspath(os.path.join(__file__, '..', '..', '__init__.py')) +module_name = "bbam" + +# Load and run bbam +spec = importlib.util.spec_from_file_location(module_name, bbam_path) +module = importlib.util.module_from_spec(spec) +sys.modules[module_name] = module +spec.loader.exec_module(module) +module.install_from_blender() + + +# Instructions for running this script from Blender +''' +# Run this script in Blender to generate and install the addon build. +# Ensure the paths in `addon_directories` point to the correct addon directories. +# For more details, visit the GitHub repository: https://github.com/xavier150/BBAM + +import os +import importlib.util + +# List of addon paths using BBAM +addon_directories = [ + # Uncomment and adjust paths as needed + r"P:/GitHubBlenderAddon/Blender-For-UnrealEngine-Addons/blender-for-unrealengine", + # r"M:/MMVS_ProjectFiles/Other/BlenderForMMVS", + # r"P:/GitHubBlenderAddon/Modular-Auto-Rig/modular-auto-rig", +] + +for dir in addon_directories: + # Install or reinstall the addon + script_path = os.path.join(dir, "bbam/exec/install_from_blender.py") + spec = importlib.util.spec_from_file_location("install_from_blender", script_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) +''' \ No newline at end of file diff --git a/blender-for-unrealengine/bbam/manifest_generate.py b/blender-for-unrealengine/bbam/manifest_generate.py new file mode 100644 index 00000000..4d9b8e9a --- /dev/null +++ b/blender-for-unrealengine/bbam/manifest_generate.py @@ -0,0 +1,106 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import os +import toml +from toml.encoder import TomlEncoder + +from . import config +from . import utils +from . import blender_utils + +class MultiLineTomlEncoder(TomlEncoder): + """ + Custom TOML encoder that formats lists to display on multiple lines in the output. + """ + def dump_list(self, v): + """ + Formats a list to display each item on a new line in the TOML output. + + Parameters: + v (list): The list to format for multi-line display. + + Returns: + str: A formatted multi-line string representation of the list. + """ + output = "[\n" + for item in v: + output += f" {toml.encoder._dump_str(item)},\n" # Indent each item + output += "]" + return output + +def generate_new_manifest(addon_generate_config_data, target_build_name): + """ + Generates a new manifest dictionary for the addon based on the configuration data. + + Parameters: + addon_generate_config_data (dict): The configuration data for the addon. + target_build_name (str): The name of the target build. + + Returns: + dict: A dictionary representing the new manifest for the addon. + """ + # Check if the target build data exists + if target_build_name not in addon_generate_config_data["builds"]: + print(f"Error: Build data for '{target_build_name}' not found!") + return {} + + data = {} + manifest_data = addon_generate_config_data["blender_manifest"] + build_data = addon_generate_config_data["builds"][target_build_name] + + # Populate generic information + data["schema_version"] = config.manifest_schema_version + data["id"] = manifest_data["id"] + data["version"] = utils.get_str_version(manifest_data["version"]) + data["name"] = manifest_data["name"] + data["maintainer"] = manifest_data["maintainer"] + data["tagline"] = manifest_data["tagline"] + data["website"] = manifest_data["website_url"] + data["type"] = manifest_data["type"] + data["tags"] = manifest_data["tags"] + data["permissions"] = manifest_data["permissions"] + data["blender_version_min"] = utils.get_str_version(build_data["blender_version_min"]) + data["license"] = manifest_data["license"] + data["copyright"] = manifest_data["copyright"] + return data + +def save_addon_manifest(addon_path, data, show_debug=False): + """ + Saves the addon manifest as a TOML file using the MultiLineTomlEncoder for custom formatting. + + Parameters: + addon_path (str): Path to the addon's root folder. + data (dict): Manifest data to save as a TOML file. + show_debug (bool): If True, displays debug information about the save process. + """ + addon_manifest_path = os.path.join(addon_path, config.blender_manifest) + + # Save the manifest as a TOML file with custom encoder for multi-line list formatting + with open(addon_manifest_path, "w") as file: + toml.dump(data, file, encoder=MultiLineTomlEncoder()) + + if show_debug: + print(f"Addon manifest saved successfully at: {addon_manifest_path}") diff --git a/blender-for-unrealengine/bbam/utils.py b/blender-for-unrealengine/bbam/utils.py new file mode 100644 index 00000000..cbb1a226 --- /dev/null +++ b/blender-for-unrealengine/bbam/utils.py @@ -0,0 +1,67 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBAM -> BleuRaven Blender Addon Manager +# https://github.com/xavier150/BBAM +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +def get_str_version(data): + """ + Converts a list of version components into a version string. + + Parameters: + data (list): A list of integers representing the version, e.g., [1, 2, 3]. + + Returns: + str: A string representation of the version, e.g., "1.2.3". + """ + return f'{data[0]}.{data[1]}.{data[2]}' + + +def get_tuple_range_version(data): + """ + Converts version range data into a list of version tuples. + + Parameters: + data (list): A list of two lists, each representing a version range, + e.g., [[1, 0, 0], [2, 0, 0]]. + + Returns: + list: A list of tuples representing the version range, + e.g., [(1, 0, 0), (2, 0, 0)]. + """ + return [tuple(data[0]), tuple(data[1])] + + +def get_version_in_range(version, range): + """ + Checks if a given version is within a specified version range. + + Parameters: + version (tuple): A tuple representing the current version, e.g., (1, 2, 0). + range (list): A list of two tuples representing the minimum and maximum versions, + e.g., [(1, 0, 0), (2, 0, 0)]. + + Returns: + bool: True if the version is within the specified range; False otherwise. + """ + min_version, max_version = range + return min_version <= version <= max_version diff --git a/blender-for-unrealengine/bbpl/__init__.py b/blender-for-unrealengine/bbpl/__init__.py index 64a28bcc..94793df2 100644 --- a/blender-for-unrealengine/bbpl/__init__.py +++ b/blender-for-unrealengine/bbpl/__init__.py @@ -30,6 +30,7 @@ from . import backward_compatibility from . import blender_rig from . import blender_addon +from . import save_data from . import blender_extension from . import basics from . import utils @@ -51,6 +52,8 @@ importlib.reload(blender_addon) if "blender_extension" in locals(): importlib.reload(blender_extension) +if "save_data" in locals(): + importlib.reload(save_data) if "basics" in locals(): importlib.reload(basics) if "utils" in locals(): diff --git a/blender-for-unrealengine/bbpl/__internal__/utils.py b/blender-for-unrealengine/bbpl/__internal__/utils.py index eea63982..bb4f5a06 100644 --- a/blender-for-unrealengine/bbpl/__internal__/utils.py +++ b/blender-for-unrealengine/bbpl/__internal__/utils.py @@ -23,23 +23,35 @@ # ---------------------------------------------- import bpy +from .. import __package__ as base_package def get_package_name(): - package_name = __package__ - package_name = package_name.split(".")[0] # Isolating 'addon name' + # Before 4.2 __package__ will look like that: + # my_blender_addon.bbpl.__internal__ + + # After 4.2 __package__ will look like that: + # bl_ext.user_default.my_blender_addon.bbpl.__internal__ + + package_name = base_package.split(".")[-1] return package_name def get_reduced_package_name(): package_name = get_package_name() - # From blender-for-unrealengine - # To bdfunr - parts = package_name.split("-") + # blender-for-unrealengine -> bdfunr + # unrealengine_assets_exporter -> unrassexp + + separators = ["-", "_", "."] + for sep in separators: + package_name = package_name.replace(sep, " ") + parts = package_name.split() special_reductions = { "blender": "bd", "for": "f", "to": "t", + "assets": "ass", + "asset": "as", } reduced_parts = [] @@ -50,7 +62,6 @@ def get_reduced_package_name(): reduced_name = ''.join(reduced_parts).lower()[:12] # Max length is 12 return reduced_name - def get_operator_class_name(name): package_name = get_reduced_package_name() return f"BBPL_OT_{package_name}_{name}" diff --git a/blender-for-unrealengine/bbpl/anim_utils.py b/blender-for-unrealengine/bbpl/anim_utils.py index 0cd494f6..736fbe55 100644 --- a/blender-for-unrealengine/bbpl/anim_utils.py +++ b/blender-for-unrealengine/bbpl/anim_utils.py @@ -146,7 +146,7 @@ def __init__(self, nla_strip: bpy.types.NlaStrip): self.fcurves = [] # Since 3.5 interact to a NlaStripFCurves not linked to an object produce Blender Crash. for fcurve in nla_strip.fcurves: - self.fcurves.append(ProxyCopy_FCurve(fcurve)) + self.fcurves.append(ProxyCopy_StripFCurve(fcurve)) self.frame_end = nla_strip.frame_end if bpy.app.version >= (3, 3, 0): self.frame_end_ui = nla_strip.frame_end_ui @@ -181,15 +181,14 @@ def paste_data_on(self, nla_strip: bpy.types.NlaStrip): nla_strip.blend_type = self.blend_type nla_strip.extrapolation = self.extrapolation for fcurve in self.fcurves: - new_fcurve = nla_strip.fcurves.find(fcurve.data_path) # Can't create so use find - fcurve.paste_data_on(new_fcurve) + fcurve.paste_data_on(nla_strip) nla_strip.frame_end = self.frame_end if bpy.app.version >= (3, 3, 0): nla_strip.frame_end_ui = self.frame_end_ui nla_strip.frame_start = self.frame_start if bpy.app.version >= (3, 3, 0): nla_strip.frame_start_ui = self.frame_start_ui - nla_strip.influence = self.influence + nla_strip.influence = self.influence # nla_strip.modifiers = self.modifiers #TO DO nla_strip.mute = self.mute # nla_strip.name = self.name @@ -208,18 +207,65 @@ def paste_data_on(self, nla_strip: bpy.types.NlaStrip): nla_strip.use_sync_length = self.use_sync_length +class ProxyCopy_StripFCurve(): + """ + Proxy class for copying bpy.types.NlaStripFCurves. (NLA Strip only) + + It is used to safely copy the bpy.types.NlaStripFCurves struct. + """ + + def __init__(self, fcurve: bpy.types.NlaStripFCurves): + self.data_path = fcurve.data_path + self.keyframe_points = [] + for keyframe_point in fcurve.keyframe_points: + self.keyframe_points.append(ProxyCopy_Keyframe(keyframe_point)) + + def paste_data_on(self, strips: bpy.types.NlaStrips): + if self.data_path == "influence": + # Create the curve with use_animated_influence + strips.use_animated_influence = True + + for key in self.keyframe_points: + strips.influence = key.co[1] + strips.keyframe_insert(data_path="influence", frame=key.co[0], keytype=key.type) + + + class ProxyCopy_FCurve(): """ - Proxy class for copying bpy.types.FCurve. + Proxy class for copying bpy.types.FCurve. It is used to safely copy the bpy.types.FCurve struct. """ def __init__(self, fcurve: bpy.types.FCurve): self.data_path = fcurve.data_path + self.keyframe_points = [] + for keyframe_point in fcurve.keyframe_points: + self.keyframe_points.append(ProxyCopy_Keyframe(keyframe_point)) def paste_data_on(self, fcurve: bpy.types.FCurve): - pass + fcurve.data_path = self.data_path + for keyframe_point in self.keyframe_points: + pass + #TODO + + +class ProxyCopy_Keyframe(): + """ + Proxy class for copying bpy.types.Keyframe. (NLA Strip only) + + It is used to safely copy the bpy.types.Keyframe struct. + """ + + def __init__(self, keyframe: bpy.types.Keyframe): + self.co = keyframe.co + self.type = keyframe.type + + def paste_data_on(self, keyframe: bpy.types.Keyframe): + keyframe.co = self.co + keyframe.type = self.type + def copy_attributes(a, b): diff --git a/blender-for-unrealengine/bbpl/backward_compatibility/__init__.py b/blender-for-unrealengine/bbpl/backward_compatibility/__init__.py index 6214b06c..9b8145c9 100644 --- a/blender-for-unrealengine/bbpl/backward_compatibility/__init__.py +++ b/blender-for-unrealengine/bbpl/backward_compatibility/__init__.py @@ -23,6 +23,7 @@ # ---------------------------------------------- import bpy +from typing import List import importlib classes = ( @@ -63,7 +64,7 @@ def __init__(self): self.remove_fcurve = 0 self.print_log = False - def update_action_curve_data_path(self, action, old_data_paths, new_data_path, remove_if_already_exists=False): + def update_action_curve_data_path(self, action: bpy.types.Action, old_data_paths: List[str], new_data_path: str, remove_if_already_exists=False, show_debug=False): """ Update the data paths of FCurves in a given action by replacing old data paths with a new one. @@ -77,36 +78,40 @@ def update_action_curve_data_path(self, action, old_data_paths, new_data_path, r Returns: None """ - cache_action_fcurves = [] - cache_data_paths = [] + cache_action_fcurves: List[bpy.types.FCurve] = [] + cache_data_paths: List[str] = [] for fcurve in action.fcurves: cache_action_fcurves.append(fcurve) cache_data_paths.append(fcurve.data_path) - for action_fcurve in cache_action_fcurves: for old_data_path in old_data_paths: current_target = action_fcurve.data_path - if old_data_path in current_target: + if old_data_path in current_target: # --- + if show_debug: + print(f"{old_data_path} found in {current_target} for action {action.name}.") new_target = current_target.replace(old_data_path, new_data_path) if new_target not in cache_data_paths: action_fcurve.data_path = new_target - if self.print_log: + if self.print_log or show_debug: print(f'"{current_target}" updated to "{new_target}" in {action.name} action.') self.update_fcurve += 1 else: if remove_if_already_exists: action.fcurves.remove(action_fcurve) - if self.print_log: + if self.print_log or show_debug: print(f'"{current_target}" can not be updated to "{new_target}" in {action.name} action. (Alredy exist!) It was removed in {action.name} action.') self.remove_fcurve += 1 break #FCurve removed so no neew to test the other old_var_names else: - if self.print_log: + if self.print_log or show_debug: print(f'"{current_target}" can not be updated to "{new_target}" in {action.name} action. (Alredy exist!)') + else: + if show_debug: + print(f"{old_data_path} not found in {current_target} for action {action.name}.") def remove_action_curve_by_data_path(self, action, data_paths): """ diff --git a/blender-for-unrealengine/bbpl/basics.py b/blender-for-unrealengine/bbpl/basics.py index f03628c1..35cffb27 100644 --- a/blender-for-unrealengine/bbpl/basics.py +++ b/blender-for-unrealengine/bbpl/basics.py @@ -29,21 +29,7 @@ import bmesh import addon_utils import pathlib - -def is_deleted(obj): - """ - Checks if the specified Blender object has been deleted. - - Args: - obj (bpy.types.Object): The Blender object to check. - - Returns: - bool: True if the object has been deleted, False otherwise. - """ - if obj and obj is not None: - return obj.name not in bpy.data.objects - else: - return True +from typing import Optional def check_plugin_is_activated(plugin_name): @@ -104,8 +90,9 @@ def get_childs(obj): Returns: list: A list of direct children objects. """ + scene = bpy.context.scene childs_obj = [] - for child_obj in bpy.data.objects: + for child_obj in scene.objects: if child_obj.library is None: parent = child_obj.parent if parent is not None: @@ -115,56 +102,76 @@ def get_childs(obj): return childs_obj -def get_root_bone_parent(bone): +def get_armature_root_bone(obj): """ - Retrieves the root bone parent of a given bone. + Retrieves the root bone of an armature object. Args: - bone (bpy.types.Bone): The bone to find the root bone parent for. + obj (bpy.types.Object): The armature object to find the root bone for. Returns: - bpy.types.Bone: The root bone parent. + bpy.types.Bone: The root bone of the armature, or None if not found. """ - if bone.parent is not None: - return get_root_bone_parent(bone.parent) - return bone + # Vérifie si l'objet est une armature et s'il a des données d'armature + if obj.type == 'ARMATURE' and obj.data: + armature = obj.data + + # Parcours tous les os de l'armature pour trouver le(s) root(s) + for bone in armature.bones: + if bone.parent is None: + return bone + return None -def get_first_deform_bone_parent(bone): +def get_armature_root_bone(obj: bpy.types.Object) -> Optional[bpy.types.Bone]: """ - Retrieves the first deform bone parent of a given bone. + Retrieves the root bone of an armature object. Args: - bone (bpy.types.Bone): The bone to find the first deform bone parent for. + obj (bpy.types.Object): The armature object to find the root bone for. Returns: - bpy.types.Bone: The first deform bone parent. + bpy.types.Bone: The root bone of the armature, or None if not found. """ - if bone.parent is not None: - if bone.use_deform is True: - return bone - else: - return get_first_deform_bone_parent(bone.parent) + if obj.type == 'ARMATURE' and obj.data: + armature = obj.data + + for bone in armature.bones: + if bone.parent is None: + return bone + return None + + +def get_root_bone_parent(bone: bpy.types.Bone) -> bpy.types.Bone: + """ + Retrieves the root bone parent of a given bone by traversing the bone's parents. + + Args: + bone (bpy.types.Bone): The bone to find the root bone parent for. + + Returns: + bpy.types.Bone: The root bone parent. + """ + while bone.parent: + bone = bone.parent return bone -def set_collection_use(collection): +def get_first_deform_bone_parent(bone: bpy.types.Bone) -> Optional[bpy.types.Bone]: """ - Sets the visibility and selectability of a collection. + Retrieves the first deform bone parent of a given bone by traversing the bone's parents. Args: - collection (bpy.types.Collection): The collection to modify. + bone (bpy.types.Bone): The bone to find the first deform bone parent for. Returns: - None + bpy.types.Bone: The first deform bone parent, or None if not found. """ - collection.hide_viewport = False - collection.hide_select = False - layer_collection = bpy.context.view_layer.layer_collection - if collection.name in layer_collection.children: - layer_collection.children[collection.name].hide_viewport = False - else: - print(collection.name, "not found in view_layer.layer_collection") + while bone.parent: + if bone.use_deform: + return bone + bone = bone.parent + return bone if bone.use_deform else None def get_recursive_childs(target_obj): @@ -186,8 +193,9 @@ def get_recursive_parent(parent, start_obj): return True return False + scene = bpy.context.scene save_objs = [] - for obj in bpy.data.objects: + for obj in scene.objects: if get_recursive_parent(target_obj, obj): save_objs.append(obj) return save_objs @@ -313,15 +321,16 @@ def set_windows_clipboard(text): def get_obj_childs(obj): # Get all direct childs of a object - ChildsObj = [] - for childObj in bpy.data.objects: + scene = bpy.context.scene + childs_obj = [] + for childObj in scene.objects: if childObj.library is None: pare = childObj.parent if pare is not None: if pare.name == obj.name: - ChildsObj.append(childObj) + childs_obj.append(childObj) - return ChildsObj + return childs_obj def get_recursive_obj_childs(obj, include_self = False): # Get all recursive childs of a object diff --git a/blender-for-unrealengine/bbpl/blender_addon/addon_utils.py b/blender-for-unrealengine/bbpl/blender_addon/addon_utils.py index 4997bfa0..61d3389f 100644 --- a/blender-for-unrealengine/bbpl/blender_addon/addon_utils.py +++ b/blender-for-unrealengine/bbpl/blender_addon/addon_utils.py @@ -26,6 +26,7 @@ import os import bpy import addon_utils +from .. import __internal__ def get_addon_version(addon_name): diff --git a/blender-for-unrealengine/bbpl/blender_extension/extension_utils.py b/blender-for-unrealengine/bbpl/blender_extension/extension_utils.py index f91aa745..2ebed6e2 100644 --- a/blender-for-unrealengine/bbpl/blender_extension/extension_utils.py +++ b/blender-for-unrealengine/bbpl/blender_extension/extension_utils.py @@ -25,23 +25,43 @@ import os import bpy +from ... import __package__ as base_package - -def get_package_version(pkg_id): - +def get_package_version(pkg_idname = None, repo_module = 'user_default'): + if bpy.app.version < (4, 2, 0): + print("Blender extensions are not supported under 4.2. Please use bbpl.blender_addon.addon_utils instead.") + return None + + manifest_filename = "blender_manifest.toml" + + if pkg_idname: + file_path = os.path.join(bpy.utils.user_resource('EXTENSIONS'), repo_module, pkg_idname, manifest_filename) + else: + from addon_utils import _extension_module_name_decompose + repo_module, pkg_idname = _extension_module_name_decompose(base_package) + file_path = os.path.join(bpy.utils.user_resource('EXTENSIONS'), repo_module, pkg_idname, manifest_filename) + version = None + if os.path.isfile(file_path): + with open(file_path, 'r') as file: + for line in file: + if line.startswith("version"): + version = line.split('=')[1].strip().strip('"') + break + else: + print(f"File {file_path} does not exist.") + + return version - # @TODO this look like a bad way to do this. Need found how use bpy.ops.extensions. - file_path = os.path.join(bpy.utils.user_resource('EXTENSIONS'), 'user_default', pkg_id, "blender_manifest.toml") - with open(file_path, 'r') as file: - for line in file: - if line.startswith("version"): - # Extraire la partie droite de la ligne après le signe '=' et enlever les espaces et guillemets - version = line.split('=')[1].strip().strip('"') - break +def get_package_path(pkg_idname = None, repo_module = 'user_default'): + if bpy.app.version < (4, 2, 0): + print("Blender extensions are not supported under 4.2. Please use bbpl.blender_addon.addon_utils instead.") + return None - return version + if pkg_idname: + return os.path.join(bpy.utils.user_resource('EXTENSIONS'), repo_module, pkg_idname) + else: + from addon_utils import _extension_module_name_decompose + repo_module, pkg_idname = _extension_module_name_decompose(base_package) + return os.path.join(bpy.utils.user_resource('EXTENSIONS'), repo_module, pkg_idname) -def get_package_path(pkg_id): - # @TODO this look like a bad way to do this. Need found how use bpy.ops.extensions. - return os.path.join(bpy.utils.user_resource('EXTENSIONS'), 'user_default', pkg_id) \ No newline at end of file diff --git a/blender-for-unrealengine/bbpl/blender_layout/layout_selector/functions.py b/blender-for-unrealengine/bbpl/blender_layout/layout_selector/functions.py index 741ecb43..10170f0e 100644 --- a/blender-for-unrealengine/bbpl/blender_layout/layout_selector/functions.py +++ b/blender-for-unrealengine/bbpl/blender_layout/layout_selector/functions.py @@ -30,12 +30,13 @@ -def add_string_selector(property_name, property_selector_name, default: str="", name: str="", description: str="", items=[]) -> types.StringSelector: +def add_string_selector(property_name, property_selector_name, default: str="", name: str="", description: str="", items=[], update=None) -> types.StringSelector: my_string_selector = types.StringSelector(property_name, property_selector_name) my_string_selector.name = name my_string_selector.default = default my_string_selector.description = description my_string_selector.items = items + my_string_selector.update = update my_string_selector.create_propertys() return my_string_selector diff --git a/blender-for-unrealengine/bbpl/blender_layout/layout_selector/types.py b/blender-for-unrealengine/bbpl/blender_layout/layout_selector/types.py index ad4be687..55bc2586 100644 --- a/blender-for-unrealengine/bbpl/blender_layout/layout_selector/types.py +++ b/blender-for-unrealengine/bbpl/blender_layout/layout_selector/types.py @@ -26,23 +26,19 @@ from . import utils from ... import __internal__ -def update_string_from_selector(self, context, string_selector): - if context.region.type != "UI": - return +def update_string_from_selector(self, string_selector): string_name = string_selector.property_name selector_name = string_selector.property_selector_name if getattr(self, string_name) != getattr(self, selector_name): setattr(self, string_name, getattr(self, selector_name)) - print("Selector update...") + #print("Selector update...") -def update_selector_from_string(self, context, string_selector): - if context.region.type != "UI": - return +def update_selector_from_string(self, string_selector): string_name = string_selector.property_name selector_name = string_selector.property_selector_name if getattr(self, selector_name) != getattr(self, string_name): setattr(self, selector_name, getattr(self, string_name)) - print("Selector update...") + #print("Selector update...") class StringSelector(): @@ -53,6 +49,7 @@ def __init__(self, property_name, property_selector_name): self.default = "" self.description = "" self.items = [] + self.update = None self.string_property = None self.enum_selector = None @@ -60,10 +57,15 @@ def __init__(self, property_name, property_selector_name): def create_propertys(self): string_selector = self def string_update_wrapper(self, context): - update_selector_from_string(self, context, string_selector) + update_selector_from_string(self, string_selector) + if string_selector.update: + string_selector.update() def selector_update_wrapper(self, context): - update_string_from_selector(self, context, string_selector) + update_string_from_selector(self, string_selector) + if string_selector.update: + string_selector.update() + self.string_property = bpy.props.StringProperty( default=self.default, diff --git a/blender-for-unrealengine/bbpl/blender_layout/layout_template_list/types.py b/blender-for-unrealengine/bbpl/blender_layout/layout_template_list/types.py index db7b4f61..ac332d10 100644 --- a/blender-for-unrealengine/bbpl/blender_layout/layout_template_list/types.py +++ b/blender-for-unrealengine/bbpl/blender_layout/layout_template_list/types.py @@ -70,7 +70,7 @@ class BBPL_UI_TemplateItem(bpy.types.PropertyGroup): return BBPL_UI_TemplateItem def create_template_item_draw_class(): - class BBPL_UI_TemplateItemDraw(bpy.types.UIList): + class BBPL_UL_TemplateItemDraw(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): prop_line = layout @@ -92,15 +92,14 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn prop_data.prop(item, "name", text="") prop_data.enabled = item.use - BBPL_UI_TemplateItemDraw.__name__ = utils.get_operator_class_name("TemplateItemDraw") - return BBPL_UI_TemplateItemDraw + BBPL_UL_TemplateItemDraw.__name__ = utils.get_operator_class_name("TemplateItemDraw") + return BBPL_UL_TemplateItemDraw def create_template_list_class(TemplateItem, TemplateItemDraw): class BBPL_UI_TemplateList(bpy.types.PropertyGroup): template_collection: bpy.props.CollectionProperty(type = TemplateItem) - template_collection_uilist_class: bpy.props.StringProperty(default = TemplateItemDraw.__name__) - #template_collection_uilist_class: bpy.props.StringProperty(default = "BBPL_UI_TemplateItemDraw") + template_collection_uilist_class_name = "" active_template_property: bpy.props.IntProperty(default = 0) rows: bpy.props.IntProperty(default = 6) maxrows: bpy.props.IntProperty(default = 6) @@ -146,13 +145,17 @@ def get_name(self): def draw(self, layout: bpy.types.UILayout): template_row = layout.row() - template_row.template_list( - self.template_collection_uilist_class, "", # type and unique id - self, "template_collection", # pointer to the CollectionProperty - self, "active_template_property", # pointer to the active identifier - rows=self.rows, - maxrows=self.maxrows, - ) + if self.template_collection_uilist_class_name == "": + print("template_collection_uilist_class_name was not set!") + + else: + template_row.template_list( + self.template_collection_uilist_class_name, "", # type and unique id + self, "template_collection", # pointer to the CollectionProperty + self, "active_template_property", # pointer to the active identifier + rows=self.rows, + maxrows=self.maxrows, + ) template_column = template_row.column(align=True) diff --git a/blender-for-unrealengine/bbpl/blender_rig/rig_utils.py b/blender-for-unrealengine/bbpl/blender_rig/rig_utils.py index 2906f20a..566adf5f 100644 --- a/blender-for-unrealengine/bbpl/blender_rig/rig_utils.py +++ b/blender-for-unrealengine/bbpl/blender_rig/rig_utils.py @@ -55,51 +55,35 @@ def create_safe_bone(arm, bone_name, context_id=None): return bone - def get_mirror_bone_name(original_bones): """ Get the mirror bone name for the given bone(s). """ - bones = [] - new_bones = [] - if not isinstance(original_bones, list): bones = [original_bones] # Convert to list else: bones = original_bones def try_to_invert_bones(bone): - def invert(bone, old, new): - if bone.endswith(old): - new_bone_name = bone[:-len(old)] - new_bone_name = new_bone_name + new - return new_bone_name - return None - change = [ - ["_l", "_r"], - ["_L", "_R"] + ("_l", "_r"), + ("_L", "_R") ] - for c in change: - a = invert(bone, c[0], c[1]) - if a: - return a - b = invert(bone, c[1], c[0]) - if b: - return b - - # Return original If no invert found. - return bone - for bone in bones: - new_bones.append(try_to_invert_bones(bone)) + for old, new in change: + if bone.endswith(old): + return bone[:-len(old)] + new + elif bone.endswith(new): + return bone[:-len(new)] + old - # Can return same bone when don't found mirror - if not isinstance(original_bones, list): - return new_bones[0] - else: - return new_bones + # Return original if no invert found + return bone + + # Using list comprehension for performance + new_bones = [try_to_invert_bones(bone) for bone in bones] + # Return a single element if the input was not a list + return new_bones[0] if not isinstance(original_bones, list) else new_bones def get_name_with_new_prefix(name, old_prefix, new_prefix): """ @@ -114,7 +98,6 @@ def get_name_with_new_prefix(name, old_prefix, new_prefix): raise TypeError('"' + old_prefix + '" not found as prefix in "' + name + '".') return new_bone_name - def get_name_list_with_new_prefix(name_list, old_prefix, new_prefix): """ Replace a prefix and add a new prefix to each name in a list. @@ -125,7 +108,6 @@ def get_name_list_with_new_prefix(name_list, old_prefix, new_prefix): new_list.append(get_name_with_new_prefix(name, old_prefix, new_prefix)) return new_list - def no_num(name): """ Remove the number index from a bone name. @@ -181,7 +163,6 @@ def change_current_layer(layer, source): if i != layer: source.layers[i] = False - def change_select_layer(layer): """ Change the active bone layer in the armature to the specified layer. @@ -189,14 +170,12 @@ def change_select_layer(layer): layer_values = [layer == i for i in range(32)] bpy.ops.armature.bone_layers(layers=layer_values) - def change_user_view_layer(layer): """ Change the active layer in the user view to the specified layer. """ change_current_layer(layer, bpy.context.object.data) - def duplicate_rig_layer(armature, original_layer, new_layer, old_prefix, new_prefix): """ Duplicates the bones in the specified original layer of the armature and moves them to the new layer. @@ -235,6 +214,7 @@ def duplicate_rig_layer(armature, original_layer, new_layer, old_prefix, new_pre armature.data.pose_position = 'POSE' # Set the pose position back to pose mode return new_bone_names + class Orig_prefixhanBone(): """ Create a new Orig_prefixhanBone instance. @@ -268,7 +248,6 @@ def set_bone_orientation(armature, bone_name, vector, roll): bone.tail = bone.head + vector * length bone.roll = roll - def get_bone_with_length(armature, bone_name, new_length, apply_tail=True): """ Evaluate the edit_bone tail position with specific length @@ -280,8 +259,6 @@ def get_bone_with_length(armature, bone_name, new_length, apply_tail=True): new_tail = bone.head + (vector * new_length) return new_tail - - def set_bone_length(armature, bone_name, new_length): """ Définit la longueur d'un os dans l'armature. @@ -303,7 +280,6 @@ def get_bone_vector(armature, bone_name): tail = armature.data.edit_bones[bone_name].tail return head - tail - def set_bone_scale(armature, bone_name, new_scale, apply_tail=True): """ Définit l'échelle d'un os dans l'armature. @@ -336,7 +312,6 @@ def get_first_parent(bone): else: return bone - def create_simple_stretch(armature, bone, target_bone_name, name): """ Create a simple stretch constraint for a bone in an armature. @@ -390,9 +365,9 @@ def apply_driver(self, bone_name): description=self.description, overridable=True) - bone_const = self.bone_const_name - constraints = self.constraint_name - driver_value = 'pose.bones["' + bone_const + '"].constraints["' + constraints + '"].influence' + escaped_bone_const = bpy.utils.escape_identifier(self.bone_const_name) + escaped_constraints = bpy.utils.escape_identifier(self.constraint_name) + driver_value = f'pose.bones["{escaped_bone_const}"].constraints["{escaped_constraints}"].influence' driver = self.armature.driver_add(driver_value).driver set_driver(self.armature, driver, bone_name, self.property_name) @@ -412,10 +387,11 @@ def create_bone_custom_property(armature, property_bone_name, property_name, def soft_max=value_max, description=description ) - property_bone.property_overridable_library_set('["' + property_name + '"]', overridable) - - return 'pose.bones["' + property_bone_name + '"]["' + property_name + '"]' - + escaped_property_name = bpy.utils.escape_identifier(property_name) + escaped_property_bone_name = bpy.utils.escape_identifier(property_bone_name) + property_bone.property_overridable_library_set(f'["{escaped_property_name}"]', overridable) + data_path = f'pose.bones["{escaped_property_bone_name}"]["{escaped_property_name}"]' + return data_path def set_driver(armature, driver, bone_name, driver_name, clean_previous=True): """ @@ -423,14 +399,18 @@ def set_driver(armature, driver, bone_name, driver_name, clean_previous=True): """ if clean_previous: utils.clear_driver_var(driver) + + # Échapper les noms de bone et driver pour les utiliser en toute sécurité dans data_path + escaped_bone_name = bpy.utils.escape_identifier(bone_name) + escaped_driver_name = bpy.utils.escape_identifier(driver_name) + v = driver.variables.new() v.name = driver_name.replace(" ", "_") v.targets[0].id = armature - v.targets[0].data_path = 'pose.bones["' + bone_name + '"]["' + driver_name + '"]' - driver.expression = driver_name.replace(" ", "_") + v.targets[0].data_path = f'pose.bones["{escaped_bone_name}"]["{escaped_driver_name}"]' + driver.expression = v.name return v - def subdivise_one_bone(armature, bone_name, subdivise_prefix_name="Subdivise_", split_number=2, keep_parent=True, ): """ Subdivides a bone into multiple segments. @@ -484,7 +464,6 @@ def subdivise_one_bone(armature, bone_name, subdivise_prefix_name="Subdivise_", # Final reparenting return chain - def duplicate_bone(arm, bone_name, new_name=None): """ Creates a duplicate bone in the armature. @@ -497,6 +476,7 @@ def duplicate_bone(arm, bone_name, new_name=None): Returns: str: The name of the created bone. """ + edit_bone = arm.data.edit_bones[bone_name] if new_name is None: new_name = edit_bone.name + "_dup" @@ -520,7 +500,6 @@ def duplicate_bone(arm, bone_name, new_name=None): return new_bone.name - def copy_constraint(armature, copy_bone_name, paste_bone_name, clear=True): """ Copies constraints from one bone to another in the armature. @@ -547,13 +526,10 @@ def copy_constraint(armature, copy_bone_name, paste_bone_name, clear=True): # armature.pose.bones[paste_bone].constraints = armature.pose.bones[copy_bone].constraints - - def set_bones_lock(armature, bone_names, lock): for bone_name in bone_names: set_bone_lock(armature, bone_name, lock) - def set_bone_lock(armature, bone_name, lock): # Check if we are in Pose mode if armature.mode != 'POSE': diff --git a/blender-for-unrealengine/bbpl/rig_bone_visual.py b/blender-for-unrealengine/bbpl/rig_bone_visual.py index ce8517dd..f4a0f18a 100644 --- a/blender-for-unrealengine/bbpl/rig_bone_visual.py +++ b/blender-for-unrealengine/bbpl/rig_bone_visual.py @@ -156,7 +156,7 @@ def generate_bone_shape_from_prop( bone = armature.data.bones.get(bone_name) new_shape_name = "Shape_CustomGeneratedShape_" + bone_name - # Vérifier si la forme existe et la supprimer le cas échéant + # Check in all blender objs the form exists and delete it if so if new_shape_name in bpy.data.objects: bpy.data.objects.remove(bpy.data.objects[new_shape_name], do_unlink=True) diff --git a/blender-for-unrealengine/bbpl/save_data/__init__.py b/blender-for-unrealengine/bbpl/save_data/__init__.py new file mode 100644 index 00000000..feb7cb80 --- /dev/null +++ b/blender-for-unrealengine/bbpl/save_data/__init__.py @@ -0,0 +1,34 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBPL -> BleuRaven Blender Python Library +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import bpy +import importlib + +from . import scene_save +from . import select_save + +if "scene_save" in locals(): + importlib.reload(scene_save) +if "select_save" in locals(): + importlib.reload(select_save) \ No newline at end of file diff --git a/blender-for-unrealengine/bbpl/save_data/scene_save.py b/blender-for-unrealengine/bbpl/save_data/scene_save.py new file mode 100644 index 00000000..e9442653 --- /dev/null +++ b/blender-for-unrealengine/bbpl/save_data/scene_save.py @@ -0,0 +1,262 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBPL -> BleuRaven Blender Python Library +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import bpy +from typing import List, TYPE_CHECKING +from . import select_save +from .. import utils + +class SavedObject(): + """ + Saved data from a blender object. + """ + + def __init__(self, obj: bpy.types.Object): + if obj: + self.ref = obj + self.name = obj.name + self.select = obj.select_get() + self.hide = obj.hide_get() + self.hide_select = obj.hide_select + self.hide_viewport = obj.hide_viewport + + def get_obj(self, use_names: bool = False)-> bpy.types.Object: + scene = bpy.context.scene + if use_names: + if self.name != "": + if self.name in scene.objects: + if self.name in bpy.context.view_layer.objects: + return scene.objects[self.name] + return None + else: + return self.ref + + + + +class SavedBones(): + """ + Saved data from a blender armature bone. + """ + + def __init__(self, bone): + if bone: + self.name = bone.name + self.select = bone.select + self.hide = bone.hide + + +class SavedCollection(): + """ + Saved data from a blender collection. + """ + + def __init__(self, col :bpy.types.Collection): + if col: + self.ref:bpy.types.Collection = col + self.name = col.name + self.hide_select = col.hide_select + self.hide_viewport = col.hide_viewport + + def get_col(self, use_names: bool = False)-> bpy.types.Collection: + if use_names: + if self.name != "": + if self.name in bpy.data.collections: + return bpy.data.collections[self.name] + return None + else: + return self.ref + +class SavedViewLayerChildren(): + """ + Saved data from a blender ViewLayerChildren. + """ + + def __init__(self, vlayer :bpy.types.ViewLayer, child_col :bpy.types.LayerCollection): + if child_col: + self.vlayer_name = vlayer.name + self.name = child_col.name + self.exclude = child_col.exclude + self.hide_viewport = child_col.hide_viewport + + +class UserSceneSave(): + """ + Manager for saving and resetting the user scene. + """ + + def __init__(self): + # Select + self.user_select_class = select_save.UserSelectSave() + + self.user_bone_active = None + self.user_bone_active_name = "" + + # Stats + self.user_mode = None + self.use_simplify = False + + # Data + self.objects: List[SavedObject] = [] + self.object_bones: List[SavedBones] = [] + self.collections: List[SavedCollection] = [] + self.view_layer_collections: List[SavedViewLayerChildren] = [] + self.action_names: List[str] = [] + self.collection_names: List[str] = [] + + def save_current_scene(self): + """ + Save the current scene data. + """ + # Save data (This can take time) + scene = bpy.context.scene + + # Select + self.user_select_class.save_current_select() + + # Stats + if self.user_select_class.user_active: + if bpy.ops.object.mode_set.poll(): + self.user_mode = self.user_select_class.user_active.mode # Save current mode + self.use_simplify = bpy.context.scene.render.use_simplify + + # Data + for obj in scene.objects: + self.objects.append(SavedObject(obj)) + for col in bpy.data.collections: + self.collections.append(SavedCollection(col)) + for vlayer in scene.view_layers: + layer_collections = utils.get_layer_collections_recursive(vlayer.layer_collection) + for layer_collection in layer_collections: + self.view_layer_collections.append(SavedViewLayerChildren(vlayer, layer_collection)) + for action in bpy.data.actions: + self.action_names.append(action.name) + for collection in bpy.data.collections: + self.collection_names.append(collection.name) + + # Data for armature + if self.user_select_class.user_active: + if self.user_select_class.user_active.type == "ARMATURE": + if self.user_select_class.user_active.data.bones.active: + self.user_bone_active = self.user_select_class.user_active.data.bones.active + self.user_bone_active_name = self.user_select_class.user_active.data.bones.active.name + for bone in self.user_select_class.user_active.data.bones: + self.object_bones.append(SavedBones(bone)) + + def reset_select(self, use_names: bool = False): + """ + Reset the user selection based on object references. + """ + self.user_select_class.reset_select(use_names) + self.reset_bones_select(use_names) + + def reset_bones_select(self, use_names: bool = False): + """ + Reset bone selection by name (works only in pose mode). + """ + # Work only in pose mode! + if len(self.object_bones) > 0: + user_active = self.user_select_class.get_user_active(use_names) + if user_active: + if bpy.ops.object.mode_set.poll(): + if user_active.mode == "POSE": + bpy.ops.pose.select_all(action='DESELECT') + for bone in self.object_bones: + if bone.select: + if bone.name in user_active.data.bones: + user_active.data.bones[bone.name].select = True + + if self.user_bone_active_name is not None: + if self.user_bone_active_name in user_active.data.bones: + new_active = user_active.data.bones[self.user_bone_active_name] + user_active.data.bones.active = new_active + + def reset_mode_at_save(self): + """ + Reset the user mode at the last save. + """ + if self.user_mode: + utils.safe_mode_set(self.user_mode, bpy.ops.object) + + def reset_scene_at_save(self, print_removed_items = False, use_names: bool = False): + """ + Reset the user scene to at the last save. + """ + scene = bpy.context.scene + self.reset_mode_at_save() + + bpy.context.scene.render.use_simplify = self.use_simplify + + # Reset hide and select + for obj in self.objects: + try: + obj_ref = obj.get_obj(use_names) + if obj_ref: + if obj_ref.hide_select != obj.hide_select: + obj_ref.hide_select = obj.hide_select + if obj_ref.hide_viewport != obj.hide_viewport: + obj_ref.hide_viewport = obj.hide_viewport + if obj_ref.hide_get() != obj.hide: + obj_ref.hide_set(obj.hide) + else: + if print_removed_items: + print(f"/!\\ {obj.name} not found.") + except ReferenceError: + if print_removed_items: + print(f"/!\\ object {obj.name} has been removed.") + + # Reset hide and select (bpy.data.collections) + for col in self.collections: + try: + col_ref = col.get_col(use_names) + if col_ref: + if col_ref.hide_select != col.hide_select: + col_ref.hide_select = col.hide_select + if col_ref.hide_viewport != col.hide_viewport: + col_ref.hide_viewport = col.hide_viewport + else: + if print_removed_items: + print(f"/!\\ {col.name} not found.") + except ReferenceError: + if print_removed_items: + print(f"/!\\ collection {col.name} has been removed.") + + # Reset hide and viewport (collections from view_layers) + for vlayer in scene.view_layers: + layer_collections = utils.get_layer_collections_recursive(vlayer.layer_collection) + + def get_layer_collection_in_list(name, collections) -> bpy.types.LayerCollection: + for layer_collection in collections: + if layer_collection.name == name: + return layer_collection + + for view_layer_collection in self.view_layer_collections: + if view_layer_collection.vlayer_name == vlayer.name: + layer_collection = get_layer_collection_in_list(view_layer_collection.name, layer_collections) + if layer_collection: + if layer_collection.exclude != view_layer_collection.exclude: + layer_collection.exclude = view_layer_collection.exclude + if layer_collection.hide_viewport != view_layer_collection.hide_viewport: + layer_collection.hide_viewport = view_layer_collection.hide_viewport + diff --git a/blender-for-unrealengine/bbpl/save_data/select_save.py b/blender-for-unrealengine/bbpl/save_data/select_save.py new file mode 100644 index 00000000..7732c866 --- /dev/null +++ b/blender-for-unrealengine/bbpl/save_data/select_save.py @@ -0,0 +1,116 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BBPL -> BleuRaven Blender Python Library +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import bpy +from typing import List +from .. import utils + +class UserSelectSave(): + """ + Manager for user selection. + """ + + def __init__(self): + # Select + self.user_active = None + self.user_active_name = "" + self.user_selecteds: List[bpy.types.Object] = [] + self.user_selected_names: List[str] = [] + + # Stats + self.user_mode = None + + def save_current_select(self): + """ + Save user selection. + """ + + # Save data (This can take time) + + # Select + self.user_active = bpy.context.active_object # Save current active object + if self.user_active: + self.user_active_name = self.user_active.name + + self.user_selecteds = bpy.context.selected_objects # Save current selected objects + self.user_selected_names = [obj.name for obj in bpy.context.selected_objects] + + def reset_select(self, use_names: bool = False): + """ + Reset user selection at the last save. + """ + + scene = bpy.context.scene + self.save_mode(use_names) + utils.safe_mode_set("OBJECT", bpy.ops.object) + bpy.ops.object.select_all(action='DESELECT') + + if use_names: + for obj in scene.objects: + if obj.name in self.user_selected_names: + if obj.name in bpy.context.view_layer.objects: + scene.objects.get(obj.name).select_set(True) # Use the name because can be duplicated name + + if self.user_active_name != "": + if self.user_active_name in scene.objects: + if self.user_active_name in bpy.context.view_layer.objects: + bpy.context.view_layer.objects.active = scene.objects.get(self.user_active_name) + + + else: + for obj in scene.objects: # Resets previous selected object if still exist + if obj in self.user_selecteds: + obj.select_set(True) + + bpy.context.view_layer.objects.active = self.user_active + + self.reset_mode_at_save() + + def save_mode(self, use_names: bool = False): + """ + Save user mode. + """ + + user_active = self.get_user_active(use_names) + if user_active: + if bpy.ops.object.mode_set.poll(): + self.user_mode = user_active.mode # Save current mode + + def reset_mode_at_save(self): + """ + Reset user mode at the last save. + """ + if self.user_mode: + utils.safe_mode_set(self.user_mode, bpy.ops.object) + + def get_user_active(self, use_names: bool = False): + scene = bpy.context.scene + if use_names: + if self.user_active_name != "": + if self.user_active_name in scene.objects: + if self.user_active_name in bpy.context.view_layer.objects: + return scene.objects.get(self.user_active_name) + return None + else: + return self.user_active \ No newline at end of file diff --git a/blender-for-unrealengine/bbpl/skin_utils.py b/blender-for-unrealengine/bbpl/skin_utils.py index 489e0853..ab996f24 100644 --- a/blender-for-unrealengine/bbpl/skin_utils.py +++ b/blender-for-unrealengine/bbpl/skin_utils.py @@ -97,33 +97,19 @@ def copy_rig_group(obj, source): bpy.ops.object.modifier_apply(modifier=mod_name) -def apply_auto_rig_parent(armature, target_objects, parent_type='ARMATURE_AUTO', use_only_bone_white_list=False, white_list_bones=None, black_list_bones=None): +def apply_auto_rig_parent(armature, target_objects, parent_type='ARMATURE_AUTO', white_list_bones=[], black_list_bones=[]): """ Apply an automatic rig parent to the target object using the armature. Optionally, specify a white list or black list of bones to control the deform flag. """ - if white_list_bones is None: - white_list_bones = [] - if black_list_bones is None: - black_list_bones = [] - # Exit current mode - #if bpy.ops.object.mode_set.poll(): - # bpy.ops.object.mode_set(mode='OBJECT') + save_defom = save_defoms_bones(armature) - #bpy.ops.object.select_all(action='DESELECT') - #armature.select_set(state=True) - #target.select_set(state=True) - #bpy.context.view_layer.objects.active = armature + if len(white_list_bones) > 0: + set_all_bones_deforms(armature, False) - if len(white_list_bones) > 0 or len(black_list_bones) > 0: - save_defom = save_defoms_bones(armature) - - if use_only_bone_white_list: - set_all_bones_deforms(armature, False) - - set_bones_deforms(armature, white_list_bones, True) - set_bones_deforms(armature, black_list_bones, False) + set_bones_deforms(armature, white_list_bones, True) + set_bones_deforms(armature, black_list_bones, False) for obj in target_objects: for modifier in obj.modifiers: @@ -146,5 +132,4 @@ def apply_auto_rig_parent(armature, target_objects, parent_type='ARMATURE_AUTO', bpy.ops.object.parent_set(override_context, type=parent_type) - if len(white_list_bones) > 0 or len(black_list_bones) > 0: - reset_deform_bones(armature, save_defom) \ No newline at end of file + reset_deform_bones(armature, save_defom) \ No newline at end of file diff --git a/blender-for-unrealengine/bbpl/utils.py b/blender-for-unrealengine/bbpl/utils.py index 74014af1..c1d962f4 100644 --- a/blender-for-unrealengine/bbpl/utils.py +++ b/blender-for-unrealengine/bbpl/utils.py @@ -27,142 +27,6 @@ import bpy import mathutils - -class SavedObject(): - """ - Saved data from a blender object. - """ - - def __init__(self, obj: bpy.types.Object): - if obj: - self.ref = obj - self.name = obj.name - self.select = obj.select_get() - self.hide = obj.hide_get() - self.hide_select = obj.hide_select - self.hide_viewport = obj.hide_viewport - - -class SavedBones(): - """ - Saved data from a blender armature bone. - """ - - def __init__(self, bone): - if bone: - self.name = bone.name - self.select = bone.select - self.hide = bone.hide - - -class SavedCollection(): - """ - Saved data from a blender collection. - """ - - def __init__(self, col): - if col: - self.ref = col - self.name = col.name - self.hide_select = col.hide_select - self.hide_viewport = col.hide_viewport - - -class SavedViewLayerChildren(): - """ - Saved data from a blender ViewLayerChildren. - """ - - def __init__(self, vlayer, childCol): - if childCol: - self.vlayer_name = vlayer.name - self.name = childCol.name - self.exclude = childCol.exclude - self.hide_viewport = childCol.hide_viewport - - -class UserSelectSave(): - """ - Manager for user selection. - """ - - def __init__(self): - # Select - self.user_active = None - self.user_active_name = "" - self.user_selecteds = [] - self.user_selected_names = [] - - # Stats - self.user_mode = None - - def save_current_select(self): - """ - Save user selection. - """ - - # Save data (This can take time) - - # Select - self.user_active = bpy.context.active_object # Save current active object - if self.user_active: - self.user_active_name = self.user_active.name - - self.user_selecteds = bpy.context.selected_objects # Save current selected objects - self.user_selected_names = [obj.name for obj in bpy.context.selected_objects] - - - def reset_select_by_ref(self): - """ - Reset user selection at the last save. (Use objects refs) - """ - - self.save_mode() - safe_mode_set("OBJECT", bpy.ops.object) - bpy.ops.object.select_all(action='DESELECT') - for obj in bpy.data.objects: # Resets previous selected object if still exist - if obj in self.user_selecteds: - obj.select_set(True) - - bpy.context.view_layer.objects.active = self.user_active - - self.reset_mode_at_save() - - def reset_select_by_name(self): - """ - Reset user selection at the last save. (Use objects names) - """ - - self.save_mode() - safe_mode_set("OBJECT", bpy.ops.object) - bpy.ops.object.select_all(action='DESELECT') - for obj in bpy.data.objects: - if obj.name in self.user_selected_names: - if obj.name in bpy.context.view_layer.objects: - bpy.data.objects[obj.name].select_set(True) # Use the name because can be duplicated name - - if self.user_active_name != "": - if self.user_active_name in bpy.data.objects: - if self.user_active_name in bpy.context.view_layer.objects: - bpy.context.view_layer.objects.active = bpy.data.objects[self.user_active_name] - - self.reset_mode_at_save() - - def save_mode(self): - """ - Save user mode. - """ - if self.user_active: - if bpy.ops.object.mode_set.poll(): - self.user_mode = self.user_active.mode # Save current mode - - def reset_mode_at_save(self): - """ - Reset user mode at the last save. - """ - if self.user_mode: - safe_mode_set(self.user_mode, bpy.ops.object) - def select_specific_object(obj: bpy.types.Object): """ Selects a specific object in Blender. @@ -179,170 +43,6 @@ def select_specific_object(obj: bpy.types.Object): obj.select_set(True) bpy.context.view_layer.objects.active = obj - -class UserSceneSave(): - """ - Manager for saving and resetting the user scene. - """ - - def __init__(self): - # Select - self.user_select_class = UserSelectSave() - - self.user_bone_active = None - self.user_bone_active_name = "" - - # Stats - self.user_mode = None - self.use_simplify = False - - # Data - self.objects = [] - self.object_bones = [] - self.collections = [] - self.view_layer_collections = [] - self.action_names = [] - self.collection_names = [] - - def save_current_scene(self): - """ - Save the current scene data. - """ - # Save data (This can take time) - c = bpy.context - # Select - self.user_select_class.save_current_select() - - # Stats - if self.user_select_class.user_active: - if bpy.ops.object.mode_set.poll(): - self.user_mode = self.user_select_class.user_active.mode # Save current mode - self.use_simplify = bpy.context.scene.render.use_simplify - - # Data - for obj in bpy.data.objects: - self.objects.append(SavedObject(obj)) - for col in bpy.data.collections: - self.collections.append(SavedCollection(col)) - for vlayer in c.scene.view_layers: - layer_collections = get_layer_collections_recursive(vlayer.layer_collection) - for layer_collection in layer_collections: - self.view_layer_collections.append(SavedViewLayerChildren(vlayer, layer_collection)) - for action in bpy.data.actions: - self.action_names.append(action.name) - for collection in bpy.data.collections: - self.collection_names.append(collection.name) - - # Data for armature - if self.user_select_class.user_active: - if self.user_select_class.user_active.type == "ARMATURE": - if self.user_select_class.user_active.data.bones.active: - self.user_bone_active = self.user_select_class.user_active.data.bones.active - self.user_bone_active_name = self.user_select_class.user_active.data.bones.active.name - for bone in self.user_select_class.user_active.data.bones: - self.object_bones.append(SavedBones(bone)) - - def reset_select_by_ref(self): - """ - Reset the user selection based on object references. - """ - self.user_select_class.reset_select_by_ref() - self.reset_bones_select_by_name() - - def reset_select_by_name(self): - """ - Reset the user selection based on object names. - """ - self.user_select_class.reset_select_by_name() - self.reset_bones_select_by_name() - - def reset_bones_select_by_name(self): - """ - Reset bone selection by name (works only in pose mode). - """ - # Work only in pose mode! - if len(self.object_bones) > 0: - if self.user_select_class.user_active: - if bpy.ops.object.mode_set.poll(): - if self.user_select_class.user_active.mode == "POSE": - bpy.ops.pose.select_all(action='DESELECT') - for bone in self.object_bones: - if bone.select: - if bone.name in self.user_select_class.user_active.data.bones: - self.user_select_class.user_active.data.bones[bone.name].select = True - - if self.user_bone_active_name is not None: - if self.user_bone_active_name in self.user_select_class.user_active.data.bones: - new_active = self.user_select_class.user_active.data.bones[self.user_bone_active_name] - self.user_select_class.user_active.data.bones.active = new_active - - def reset_mode_at_save(self): - """ - Reset the user mode at the last save. - """ - if self.user_mode: - safe_mode_set(self.user_mode, bpy.ops.object) - - def reset_scene_at_save(self, print_removed_items = False): - """ - Reset the user scene to at the last save. - """ - scene = bpy.context.scene - self.reset_mode_at_save() - - bpy.context.scene.render.use_simplify = self.use_simplify - - # Reset hide and select (bpy.data.objects) - for obj in self.objects: - try: - if obj.ref: - if obj.ref.hide_select != obj.hide_select: - obj.ref.hide_select = obj.hide_select - if obj.ref.hide_viewport != obj.hide_viewport: - obj.ref.hide_viewport = obj.hide_viewport - if obj.ref.hide_get() != obj.hide: - obj.ref.hide_set(obj.hide) - else: - if print_removed_items: - print(f"/!\\ {obj.name} not found.") - except ReferenceError: - if print_removed_items: - print(f"/!\\ {obj.name} has been removed.") - - # Reset hide and select (bpy.data.collections) - for col in self.collections: - try: - if col.ref.name in bpy.data.collections: - if col.ref.hide_select != col.hide_select: - col.ref.hide_select = col.hide_select - if col.ref.hide_viewport != col.hide_viewport: - col.ref.hide_viewport = col.hide_viewport - else: - if print_removed_items: - print(f"/!\\ {col.name} not found.") - except ReferenceError: - if print_removed_items: - print(f"/!\\ {col.name} has been removed.") - - # Reset hide and viewport (collections from view_layers) - for vlayer in scene.view_layers: - layer_collections = get_layer_collections_recursive(vlayer.layer_collection) - - def get_layer_collection_in_list(name, collections): - for layer_collection in collections: - if layer_collection.name == name: - return layer_collection - - for view_layer_collection in self.view_layer_collections: - if view_layer_collection.vlayer_name == vlayer.name: - layer_collection = get_layer_collection_in_list(view_layer_collection.name, layer_collections) - if layer_collection: - if layer_collection.exclude != view_layer_collection.exclude: - layer_collection.exclude = view_layer_collection.exclude - if layer_collection.hide_viewport != view_layer_collection.hide_viewport: - layer_collection.hide_viewport = view_layer_collection.hide_viewport - - class UserArmatureDataSave(): """ Manager for saving and resetting an armature. @@ -712,7 +412,6 @@ def __init__(self): def LoadUserRenderSimplify(self): bpy.context.scene.render.use_simplify = self.use_simplify - class SaveObjectReferanceUser(): """ This class is used to save and update references to an object in constraints @@ -725,39 +424,41 @@ def __init__(self): """ self.using_constraints = [] - def save_refs_from_object(self, obj: bpy.types.Object): + def save_refs_from_object(self, targe_obj: bpy.types.Object): """ Scans all objects in the Blender scene to find and save constraints in armature bones that reference the specified object. :param obj: The target bpy.types.Object to find references to. """ - for objet in bpy.data.objects: - if objet.type == 'ARMATURE': - for bone in objet.pose.bones: + scene = bpy.context.scene + for obj in scene.objects: + if obj.type == 'ARMATURE': + for bone in obj.pose.bones: for contrainte in bone.constraints: - if hasattr(contrainte, 'target') and contrainte.target and contrainte.target.name == obj.name: + if hasattr(contrainte, 'target') and contrainte.target and contrainte.target.name == targe_obj.name: constraint_info = { - 'armature_object': objet.name, + 'armature_object': obj.name, 'bone': bone.name, 'constraint': contrainte.name } self.using_constraints.append(constraint_info) - def update_refs_with_object(self, obj: bpy.types.Object): + def update_refs_with_object(self, targe_obj: bpy.types.Object): """ Updates all previously found constraints to reference a new object. :param obj: The new bpy.types.Object to be used as the target for the saved constraints. """ + scene = bpy.context.scene for info in self.using_constraints: - if info['armature_object'] in bpy.data.objects: - armature_object = bpy.data.objects[info['armature_object']] + if info['armature_object'] in scene.objects: + armature_object = scene.objects.get(info['armature_object']) if info['bone'] in armature_object.pose.bones: bone = armature_object.pose.bones[info['bone']] if info['constraint'] in bone.constraints: constraint = bone.constraints[info['constraint']] - constraint.target = obj + constraint.target = targe_obj def active_mode_is(targetMode): # Return True is active obj mode == targetMode @@ -794,4 +495,105 @@ def found_type_in_selection(targetType, include_active=True): for obj in select: if obj.type == targetType: return True - return False \ No newline at end of file + return False + +def get_bone_path(armature: bpy.types.Object, start_bone_name: str, end_bone_name: str): + """ + Returns a list of bone names between start_bone and end_bone in an armature. + + :param armature: The armature object. + :param start_bone_name: The name of the starting bone. + :param end_bone_name: The name of the ending bone. + :return: List of bone names between start_bone and end_bone, or an empty list if no path is found. + """ + + # Access bones directly. + if armature.mode == 'EDIT': + bones = armature.data.edit_bones + else: + bones = armature.data.bones + + # Initialize the bones + start_bone = bones[start_bone_name] + end_bone = bones[end_bone_name] + + # Depth-First Search to find the path from start_bone to end_bone + def find_path(current_bone, path): + path.append(current_bone.name) + + # Check if we've reached the end bone + if current_bone == end_bone: + return path + + # Explore each child recursively + for child in current_bone.children: + result = find_path(child, path[:]) # Use a copy of the current path + if result: # If a valid path is found, return it + return result + + return None # Return None if no path is found from this branch + + # Start the recursive search + all_bones = find_path(start_bone, []) + return all_bones + + +def get_bone_path_to_end(armature: bpy.types.Object, start_bone_name: str): + """ + Returns a list of bone names from the start_bone to the last child in a chain. + + :param armature: The armature object. + :param start_bone_name: The name of the starting bone. + :return: List of bone names from start_bone to the last child. + """ + + # Access bones directly. + if armature.mode == 'EDIT': + bones = armature.data.edit_bones + else: + bones = armature.data.bones + + # Initialize the bones + start_bone = bones[start_bone_name] + + # Traverse bones from start_bone to the last child in the chain + current_bone = start_bone + bone_path = [current_bone.name] + + while current_bone.children: + # Use first child only + current_bone = current_bone.children[0] + bone_path.append(current_bone.name) + + return bone_path + +def get_bone_and_children(armature: bpy.types.Object, start_bone_name: str): + """ + Returns a list of all descendant bones of the specified start_bone, including all children recursively. + + :param armature: The armature object. + :param start_bone_name: The name of the starting bone. + :return: List of bone names, including the start bone and all its descendants. + """ + + # Access bones directly. + if armature.mode == 'EDIT': + bones = armature.data.edit_bones + else: + bones = armature.data.bones + + # Initialize the bones + bones = armature.data.edit_bones + start_bone = bones.get(start_bone_name) + + + # Recursive function to collect all children bones + def collect_children(bone): + bone_list = [bone.name] + for child in bone.children: + bone_list.extend(collect_children(child)) + return bone_list + + # Get all bones starting from the start_bone + all_bones = collect_children(start_bone) + return all_bones diff --git a/blender-for-unrealengine/bfu_addon_parts/__init__.py b/blender-for-unrealengine/bfu_addon_parts/__init__.py index 5aea252d..14d8272a 100644 --- a/blender-for-unrealengine/bfu_addon_parts/__init__.py +++ b/blender-for-unrealengine/bfu_addon_parts/__init__.py @@ -1,29 +1,20 @@ import bpy import importlib -from . import bfu_modular_skeletal_specified_parts_meshs -from . import bfu_object_ui_and_props -from . import bfu_tool_ui_and_props -from . import bfu_export_ui_and_props -from . import bfu_unreal_engine_refs_props +from . import bfu_panel_object +from . import bfu_panel_tools +from . import bfu_panel_export from . import bfu_export_correct_and_improv_panel -from . import bfu_debug_ui_and_props_panel - - -if "bfu_modular_skeletal_specified_parts_meshs" in locals(): - importlib.reload(bfu_modular_skeletal_specified_parts_meshs) -if "bfu_object_ui_and_props" in locals(): - importlib.reload(bfu_object_ui_and_props) -if "bfu_tool_ui_and_props" in locals(): - importlib.reload(bfu_tool_ui_and_props) -if "bfu_export_ui_and_props" in locals(): - importlib.reload(bfu_export_ui_and_props) -if "bfu_unreal_engine_refs_props" in locals(): - importlib.reload(bfu_unreal_engine_refs_props) + + +if "bfu_panel_object" in locals(): + importlib.reload(bfu_panel_object) +if "bfu_panel_tools" in locals(): + importlib.reload(bfu_panel_tools) +if "bfu_panel_export" in locals(): + importlib.reload(bfu_panel_export) if "bfu_export_correct_and_improv_panel" in locals(): importlib.reload(bfu_export_correct_and_improv_panel) -if "bfu_debug_ui_and_props_panel" in locals(): - importlib.reload(bfu_debug_ui_and_props_panel) classes = ( ) @@ -33,22 +24,16 @@ def register(): for cls in classes: bpy.utils.register_class(cls) - bfu_modular_skeletal_specified_parts_meshs.register() - bfu_object_ui_and_props.register() - bfu_tool_ui_and_props.register() - bfu_export_ui_and_props.register() - bfu_unreal_engine_refs_props.register() + bfu_panel_object.register() + bfu_panel_tools.register() + bfu_panel_export.register() bfu_export_correct_and_improv_panel.register() - bfu_debug_ui_and_props_panel.register() def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) - bfu_modular_skeletal_specified_parts_meshs.unregister() - bfu_object_ui_and_props.unregister() - bfu_tool_ui_and_props.unregister() - bfu_unreal_engine_refs_props.unregister() - bfu_export_ui_and_props.unregister() bfu_export_correct_and_improv_panel.unregister() - bfu_debug_ui_and_props_panel.unregister() \ No newline at end of file + bfu_panel_export.unregister() + bfu_panel_tools.unregister() + bfu_panel_object.unregister() diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_debug_ui_and_props_panel.py b/blender-for-unrealengine/bfu_addon_parts/bfu_debug_ui_and_props_panel.py deleted file mode 100644 index ce88b356..00000000 --- a/blender-for-unrealengine/bfu_addon_parts/bfu_debug_ui_and_props_panel.py +++ /dev/null @@ -1,72 +0,0 @@ -import bpy - -from .. import bfu_utils -from .. import bfu_assets_manager - -class BFU_PT_BlenderForUnrealDebug(bpy.types.Panel): - # Debug panel for get dev info and test - - bl_idname = "BFU_PT_BlenderForUnrealDebug" - bl_label = "Debug" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "Unreal Engine" - - bpy.types.Object.bfu_use_socket_custom_Name = bpy.props.BoolProperty( - name="Socket custom name", - description='Use a custom name in Unreal Engine for this socket?', - default=False - ) - - bpy.types.Object.bfu_socket_custom_Name = bpy.props.StringProperty( - name="", - description='', - default="MySocket" - ) - - def draw(self, context): - - layout = self.layout - obj = context.object - layout.label(text="This panel is only for Debug", icon='INFO') - - if obj: - layout.label(text="Full path name as Static Mesh:") - asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) - if asset_class: - obj_export_name = asset_class.get_obj_export_name(obj) - obj_export_dir = asset_class.get_obj_export_directory_path(obj) - obj_export_abs_dir = asset_class.get_obj_export_abs_directory_path(obj) - - else: - obj_export_name = "XXX" - obj_export_dir = "XXX" - obj_export_abs_dir = "XXX" - - layout.label(text="Obj Export Name:" + obj_export_name) - layout.label(text="Obj Export Dir" + obj_export_dir) - layout.label(text="Obj Export Abs Dir:" + obj_export_abs_dir) - - - if obj.type == "CAMERA": - layout.label(text="CameraPositionForUnreal (Loc):" + str(bfu_utils.EvaluateCameraPositionForUnreal(obj)[0])) - layout.label(text="CameraPositionForUnreal (Rot):" + str(bfu_utils.EvaluateCameraPositionForUnreal(obj)[1])) - layout.label(text="CameraPositionForUnreal (Scale):" + str(bfu_utils.EvaluateCameraPositionForUnreal(obj)[2])) - -# ------------------------------------------------------------------- -# Register & Unregister -# ------------------------------------------------------------------- - -classes = ( - # BFU_PT_BlenderForUnrealDebug, # Unhide for dev -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_export_correct_and_improv_panel.py b/blender-for-unrealengine/bfu_addon_parts/bfu_export_correct_and_improv_panel.py index d7f74a19..36cdf780 100644 --- a/blender-for-unrealengine/bfu_addon_parts/bfu_export_correct_and_improv_panel.py +++ b/blender-for-unrealengine/bfu_addon_parts/bfu_export_correct_and_improv_panel.py @@ -1,6 +1,7 @@ import bpy from .. import bfu_utils +from .. import languages class BFU_PT_CorrectAndImprov(bpy.types.Panel): # Is Clipboard panel @@ -12,23 +13,28 @@ class BFU_PT_CorrectAndImprov(bpy.types.Panel): bl_category = "Unreal Engine" class BFU_OT_CorrectExtremUV(bpy.types.Operator): - bl_label = "Correct Extrem UV For Unreal" + bl_label = (languages.ti('correct_use_extrem_uv_scale_name')) bl_idname = "object.correct_extrem_uv" - bl_description = ( - "Correct extrem UV island of the selected object" + - " for better use in real time engines" - ) + bl_description = (languages.tt('correct_extrem_uv_scale_operator_desc')) bl_options = {'REGISTER', 'UNDO'} - stepScale: bpy.props.IntProperty( - name="Step scale", + step_scale: bpy.props.IntProperty( + name=(languages.ti('correct_extrem_uv_scale_step_scale_name')), + description =(languages.tt('correct_use_extrem_uv_scale_desc')), default=2, min=1, - max=100) + max=100, + ) + + move_to_absolute: bpy.props.BoolProperty( + name=(languages.ti('correct_extrem_uv_scale_use_absolute_name')), + description =(languages.tt('correct_extrem_uv_scale_use_absolute_desc')), + default=False, + ) def execute(self, context): if bpy.context.active_object.mode == "EDIT": - bfu_utils.CorrectExtremeUV(stepScale=self.stepScale) + bfu_utils.CorrectExtremeUV(step_scale=self.step_scale, move_to_absolute=self.move_to_absolute) self.report( {'INFO'}, "UV corrected!") diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_export_ui_and_props.py b/blender-for-unrealengine/bfu_addon_parts/bfu_export_ui_and_props.py deleted file mode 100644 index e87ac9ba..00000000 --- a/blender-for-unrealengine/bfu_addon_parts/bfu_export_ui_and_props.py +++ /dev/null @@ -1,828 +0,0 @@ -import os -import bpy -from .. import bfu_export -from .. import bfu_write_text -from .. import bfu_basics -from .. import bfu_utils -from .. import bfu_check_potential_error -from .. import bfu_cached_asset_list -from .. import bfu_ui -from .. import bbpl -from .. import bps -from .. import bfu_collision -from .. import bfu_socket -from .. import bfu_assets_manager - - - -class BFU_PT_Export(bpy.types.Panel): - # Is Export panel - - bl_idname = "BFU_PT_Export" - bl_label = "BFU Export" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "Unreal Engine" - - # Prefix - bpy.types.Scene.bfu_static_mesh_prefix_export_name = bpy.props.StringProperty( - name="StaticMesh Prefix", - description="Prefix of staticMesh", - maxlen=32, - default="SM_") - - bpy.types.Scene.bfu_skeletal_mesh_prefix_export_name = bpy.props.StringProperty( - name="SkeletalMesh Prefix ", - description="Prefix of SkeletalMesh", - maxlen=32, - default="SKM_") - - bpy.types.Scene.bfu_skeleton_prefix_export_name = bpy.props.StringProperty( - name="skeleton Prefix ", - description="Prefix of skeleton", - maxlen=32, - default="SK_") - - bpy.types.Scene.bfu_alembic_animation_prefix_export_name = bpy.props.StringProperty( - name="Alembic Prefix ", - description="Prefix of Alembic (SkeletalMesh in unreal)", - maxlen=32, - default="SKM_") - - bpy.types.Scene.bfu_groom_simulation_prefix_export_name = bpy.props.StringProperty( - name="Groom Prefix ", - description="Prefix of Groom Simulation", - maxlen=32, - default="GS_") - - - - bpy.types.Scene.bfu_anim_prefix_export_name = bpy.props.StringProperty( - name="AnimationSequence Prefix", - description="Prefix of AnimationSequence", - maxlen=32, - default="Anim_") - - bpy.types.Scene.bfu_pose_prefix_export_name = bpy.props.StringProperty( - name="AnimationSequence(Pose) Prefix", - description="Prefix of AnimationSequence with only one frame", - maxlen=32, - default="Pose_") - - bpy.types.Scene.bfu_camera_prefix_export_name = bpy.props.StringProperty( - name="Camera anim Prefix", - description="Prefix of camera animations", - maxlen=32, - default="Cam_") - - bpy.types.Scene.bfu_spline_prefix_export_name = bpy.props.StringProperty( - name="Spline anim Prefix", - description="Prefix of spline animations", - maxlen=32, - default="Spline_") - - # Sub folder - bpy.types.Scene.bfu_anim_subfolder_name = bpy.props.StringProperty( - name="Animations sub folder name", - description=( - "The name of sub folder for animations New." + - " You can now use ../ for up one directory."), - maxlen=512, - default="Anim") - - # File path - bpy.types.Scene.bfu_export_static_file_path = bpy.props.StringProperty( - name="StaticMesh Export Path", - description="Choose a directory to export StaticMesh(s)", - maxlen=512, - default="//" + os.path.join("ExportedFbx", "StaticMesh"), - subtype='DIR_PATH') - - bpy.types.Scene.bfu_export_skeletal_file_path = bpy.props.StringProperty( - name="SkeletalMesh Export Path", - description="Choose a directory to export SkeletalMesh(s)", - maxlen=512, - default="//" + os.path.join("ExportedFbx", "SkeletalMesh"), - subtype='DIR_PATH') - - bpy.types.Scene.bfu_export_alembic_file_path = bpy.props.StringProperty( - name="Alembic Export Path", - description="Choose a directory to export Alembic animation(s)", - maxlen=512, - default="//" + os.path.join("ExportedFbx", "Alembic"), - subtype='DIR_PATH') - - bpy.types.Scene.bfu_export_groom_file_path = bpy.props.StringProperty( - name="Groom Export Path", - description="Choose a directory to export Groom simulation(s)", - maxlen=512, - default="//" + os.path.join("ExportedFbx", "Groom"), - subtype='DIR_PATH') - - bpy.types.Scene.bfu_export_camera_file_path = bpy.props.StringProperty( - name="Camera Export Path", - description="Choose a directory to export Camera(s)", - maxlen=512, - default="//" + os.path.join("ExportedFbx", "Sequencer"), - subtype='DIR_PATH') - - bpy.types.Scene.bfu_export_spline_file_path = bpy.props.StringProperty( - name="Spline Export Path", - description="Choose a directory to export Spline(s)", - maxlen=512, - default="//" + os.path.join("ExportedFbx", "Spline"), - subtype='DIR_PATH') - - bpy.types.Scene.bfu_export_other_file_path = bpy.props.StringProperty( - name="Other Export Path", - description="Choose a directory to export text file and other", - maxlen=512, - default="//" + os.path.join("ExportedFbx"), - subtype='DIR_PATH') - - # File name - bpy.types.Scene.bfu_file_export_log_name = bpy.props.StringProperty( - name="Export log name", - description="Export log name", - maxlen=64, - default="ExportLog.txt") - - bpy.types.Scene.bfu_file_import_asset_script_name = bpy.props.StringProperty( - name="Import asset script Name", - description="Import asset script name", - maxlen=64, - default="ImportAssetScript.py") - - bpy.types.Scene.bfu_file_import_sequencer_script_name = bpy.props.StringProperty( - name="Import sequencer script Name", - description="Import sequencer script name", - maxlen=64, - default="ImportSequencerScript.py") - - bpy.types.Scene.bfu_unreal_import_module = bpy.props.StringProperty( - name="Unreal import module", - description="Which module (plugin name) to import to. Default is 'Game', meaning it will be put into your project's /Content/ folder. If you wish to import to a plugin (for example a plugin called 'myPlugin'), just write its name here", - maxlen=512, - default='Game') - - bpy.types.Scene.bfu_unreal_import_location = bpy.props.StringProperty( - name="Unreal import location", - description="Unreal assets import location inside the module", - maxlen=512, - default='ImportedFbx') - - class BFU_MT_NomenclaturePresets(bpy.types.Menu): - bl_label = 'Nomenclature Presets' - preset_subdir = 'blender-for-unrealengine/nomenclature-presets' - preset_operator = 'script.execute_preset' - draw = bpy.types.Menu.draw_preset - - from bl_operators.presets import AddPresetBase - - class BFU_OT_AddNomenclaturePreset(AddPresetBase, bpy.types.Operator): - bl_idname = 'object.add_nomenclature_preset' - bl_label = 'Add or remove a preset for Nomenclature' - bl_description = 'Add or remove a preset for Nomenclature' - preset_menu = 'BFU_MT_NomenclaturePresets' - - # Common variable used for all preset values - preset_defines = [ - 'obj = bpy.context.object', - 'scene = bpy.context.scene' - ] - - # Properties to store in the preset - preset_values = [ - 'scene.bfu_static_mesh_prefix_export_name', - 'scene.bfu_skeletal_mesh_prefix_export_name', - 'scene.bfu_skeleton_prefix_export_name', - 'scene.bfu_alembic_animation_prefix_export_name', - 'scene.bfu_groom_simulation_prefix_export_name', - 'scene.bfu_anim_prefix_export_name', - 'scene.bfu_pose_prefix_export_name', - 'scene.bfu_camera_prefix_export_name', - 'scene.bfu_spline_prefix_export_name', - 'scene.bfu_anim_subfolder_name', - 'scene.bfu_export_static_file_path', - 'scene.bfu_export_skeletal_file_path', - 'scene.bfu_export_alembic_file_path', - 'scene.bfu_export_groom_file_path', - 'scene.bfu_export_camera_file_path', - 'scene.bfu_export_spline_file_path', - 'scene.bfu_export_other_file_path', - 'scene.bfu_file_export_log_name', - 'scene.bfu_file_import_asset_script_name', - 'scene.bfu_file_import_sequencer_script_name', - # Import location: - 'scene.bfu_unreal_import_module', - 'scene.bfu_unreal_import_location', - ] - - # Directory to store the presets - preset_subdir = 'blender-for-unrealengine/nomenclature-presets' - - class BFU_OT_ShowAssetToExport(bpy.types.Operator): - bl_label = "Show asset(s)" - bl_idname = "object.showasset" - bl_description = "Click to show assets that are to be exported." - - def execute(self, context): - - obj = context.object - if obj: - if obj.type == "ARMATURE": - animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) - animation_asset_cache.UpdateActionCache() - - - final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() - final_asset_list_to_export = final_asset_cache.GetFinalAssetList() - popup_title = "Assets list" - if len(final_asset_list_to_export) > 0: - popup_title = str(len(final_asset_list_to_export))+' asset(s) will be exported.' - else: - popup_title = 'No exportable assets were found.' - - def draw(self, context): - col = self.layout.column() - for asset in final_asset_list_to_export: - asset :bfu_cached_asset_list.AssetToExport - row = col.row() - if asset.obj is not None: - if asset.action is not None: - if (type(asset.action) is bpy.types.Action): - # Action name - action = asset.action.name - elif (type(asset.action) is bpy.types.AnimData): - # Nonlinear name - action = asset.obj.bfu_anim_nla_export_name - else: - action = "..." - row.label( - text="- ["+asset.name+"] --> " + - action+" ("+asset.asset_type+")") - else: - if asset.asset_type != "Collection StaticMesh": - row.label( - text="- "+asset.name + - " ("+asset.asset_type+")") - else: - row.label( - text="- "+asset.obj.name + - " ("+asset.asset_type+")") - - else: - row.label(text="- ("+asset.asset_type+")") - bpy.context.window_manager.popup_menu( - draw, - title=popup_title, - icon='PACKAGE') - return {'FINISHED'} - - class BFU_OT_CheckPotentialErrorPopup(bpy.types.Operator): - bl_label = "Check Potential Errors" - bl_idname = "object.checkpotentialerror" - bl_description = "Check potential errors." - text = "none" - - def execute(self, context): - fix_info = bfu_check_potential_error.process_general_fix() - invoke_info = "" - for x, fix_info_key in enumerate(fix_info): - fix_info_data = fix_info[fix_info_key] - invoke_info += fix_info_key + ": " + str(fix_info_data) - if x < len(fix_info)-1: - invoke_info += "\n" - - - bfu_check_potential_error.UpdateUnrealPotentialError() - bpy.ops.object.openpotentialerror( - "INVOKE_DEFAULT", - invoke_info=invoke_info, - ) - return {'FINISHED'} - - class BFU_OT_OpenPotentialErrorPopup(bpy.types.Operator): - bl_label = "Open potential errors" - bl_idname = "object.openpotentialerror" - bl_description = "Open potential errors" - invoke_info: bpy.props.StringProperty(default="...") - - class BFU_OT_FixitTarget(bpy.types.Operator): - bl_label = "Fix it !" - bl_idname = "object.fixit_objet" - bl_description = "Correct target error" - errorIndex: bpy.props.IntProperty(default=-1) - - def execute(self, context): - result = bfu_check_potential_error.TryToCorrectPotentialError(self.errorIndex) - self.report({'INFO'}, result) - return {'FINISHED'} - - class BFU_OT_SelectObjectButton(bpy.types.Operator): - bl_label = "Select(Object)" - bl_idname = "object.select_error_objet" - bl_description = "Select target Object." - errorIndex: bpy.props.IntProperty(default=-1) - - def execute(self, context): - bfu_check_potential_error.SelectPotentialErrorObject(self.errorIndex) - return {'FINISHED'} - - class BFU_OT_SelectVertexButton(bpy.types.Operator): - bl_label = "Select(Vertex)" - bl_idname = "object.select_error_vertex" - bl_description = "Select target Vertex." - errorIndex: bpy.props.IntProperty(default=-1) - - def execute(self, context): - bfu_check_potential_error.SelectPotentialErrorVertex(self.errorIndex) - return {'FINISHED'} - - class BFU_OT_SelectPoseBoneButton(bpy.types.Operator): - bl_label = "Select(PoseBone)" - bl_idname = "object.select_error_posebone" - bl_description = "Select target Pose Bone." - errorIndex: bpy.props.IntProperty(default=-1) - - def execute(self, context): - bfu_check_potential_error.SelectPotentialErrorPoseBone(self.errorIndex) - return {'FINISHED'} - - class BFU_OT_OpenPotentialErrorDocs(bpy.types.Operator): - bl_label = "Open docs" - bl_idname = "object.open_potential_error_docs" - bl_description = "Open potential error docs." - octicon: bpy.props.StringProperty(default="") - - def execute(self, context): - os.system( - "start \"\" " + - "https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/How-avoid-potential-errors" + - "#"+self.octicon) - return {'FINISHED'} - - def execute(self, context): - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_popup(self, width=1020) - - def check(self, context): - return True - - def draw(self, context): - - layout = self.layout - if len(bpy.context.scene.potentialErrorList) > 0: - popup_title = ( - str(len(bpy.context.scene.potentialErrorList)) + - " potential error(s) found!") - else: - popup_title = "No potential error to correct!" - - - layout.label(text=popup_title) - invoke_info_lines = self.invoke_info.split("\n") - for invoke_info_line in invoke_info_lines: - layout.label(text="- "+invoke_info_line) - - layout.separator() - row = layout.row() - col = row.column() - for x in range(len(bpy.context.scene.potentialErrorList)): - error = bpy.context.scene.potentialErrorList[x] - - myLine = col.box().split(factor=0.85) - # ---- - if error.type == 0: - msgType = 'INFO' - msgIcon = 'INFO' - elif error.type == 1: - msgType = 'WARNING' - msgIcon = 'ERROR' - elif error.type == 2: - msgType = 'ERROR' - msgIcon = 'CANCEL' - # ---- - - # Text - TextLine = myLine.column() - errorFullMsg = msgType+": "+error.text - splitedText = errorFullMsg.split("\n") - - for text, Line in enumerate(splitedText): - if (text < 1): - - FisrtTextLine = TextLine.row() - if (error.docsOcticon != "None"): # Doc button - props = FisrtTextLine.operator( - "object.open_potential_error_docs", - icon="HELP", - text="") - props.octicon = error.docsOcticon - - FisrtTextLine.label(text=Line, icon=msgIcon) - else: - TextLine.label(text=Line) - - # Select and fix button - ButtonLine = myLine.column() - if (error.correctRef != "None"): - props = ButtonLine.operator( - "object.fixit_objet", - text=error.correctlabel) - props.errorIndex = x - if (error.object is not None): - if (error.selectObjectButton): - props = ButtonLine.operator( - "object.select_error_objet") - props.errorIndex = x - if (error.selectVertexButton): - props = ButtonLine.operator( - "object.select_error_vertex") - props.errorIndex = x - if (error.selectPoseBoneButton): - props = ButtonLine.operator( - "object.select_error_posebone") - props.errorIndex = x - - class BFU_OT_ExportForUnrealEngineButton(bpy.types.Operator): - bl_label = "Export for Unreal Engine" - bl_idname = "object.exportforunreal" - bl_description = "Export all assets of this scene." - - def execute(self, context): - scene = bpy.context.scene - - def isReadyForExport(): - - def GetIfOneTypeCheck(): - all_assets = bfu_assets_manager.bfu_asset_manager_utils.get_all_asset_class() - for assets in all_assets: - assets: bfu_assets_manager.bfu_asset_manager_type.BFU_BaseAssetClass - if assets.can_export_asset(): - return True - - if (scene.static_collection_export - or scene.anin_export): - return True - else: - return False - - if not bfu_basics.CheckPluginIsActivated("io_scene_fbx"): - self.report( - {'WARNING'}, - 'Add-on FBX format is not activated!' + - ' Edit > Preferences > Add-ons > And check "FBX format"') - return False - - if not GetIfOneTypeCheck(): - self.report( - {'WARNING'}, - "No asset type is checked.") - return False - - final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() - final_asset_list_to_export = final_asset_cache.GetFinalAssetList() - if not len(final_asset_list_to_export) > 0: - self.report( - {'WARNING'}, - "Not found assets with" + - " \"Export recursive\" properties " + - "or collection to export.") - return False - - if not bpy.data.is_saved: - # Primary check if file is saved - # to avoid windows PermissionError - self.report( - {'WARNING'}, - "Please save this .blend file before export.") - return False - - if bbpl.scene_utils.is_tweak_mode(): - # Need exit Tweakmode because the Animation data is read only. - self.report( - {'WARNING'}, - "Exit Tweakmode in NLA Editor. [Tab]") - return False - - return True - - if not isReadyForExport(): - return {'FINISHED'} - - scene.UnrealExportedAssetsList.clear() - counter = bps.utils.CounterTimer() - bfu_check_potential_error.process_general_fix() - bfu_export.bfu_export_asset.process_export(self) - bfu_write_text.WriteAllTextFiles() - - self.report( - {'INFO'}, - "Export of " + str(len(scene.UnrealExportedAssetsList)) + " asset(s) has been finalized in " + counter.get_str_time() + " Look in console for more info.") - print( - "=========================" + - " Exported asset(s) " + - "=========================") - print("") - lines = bfu_write_text.WriteExportLog().splitlines() - for line in lines: - print(line) - print("") - print( - "=========================" + - " ... " + - "=========================") - - return {'FINISHED'} - - class BFU_OT_CopyImportAssetScriptCommand(bpy.types.Operator): - bl_label = "Copy import script (Assets)" - bl_idname = "object.copy_importassetscript_command" - bl_description = "Copy Import Asset Script command" - - def execute(self, context): - scene = context.scene - bfu_basics.setWindowsClipboard(bfu_utils.GetImportAssetScriptCommand()) - self.report( - {'INFO'}, - "command for "+scene.bfu_file_import_asset_script_name + - " copied") - return {'FINISHED'} - - class BFU_OT_CopyImportSequencerScriptCommand(bpy.types.Operator): - bl_label = "Copy import script (Sequencer)" - bl_idname = "object.copy_importsequencerscript_command" - bl_description = "Copy Import Sequencer Script command" - - def execute(self, context): - scene = context.scene - bfu_basics.setWindowsClipboard(bfu_utils.GetImportSequencerScriptCommand()) - self.report( - {'INFO'}, - "command for "+scene.bfu_file_import_sequencer_script_name + - " copied") - return {'FINISHED'} - - # Categories : - bpy.types.Scene.static_export = bpy.props.BoolProperty( - name="StaticMesh(s)", - description="Check mark to export StaticMesh(s)", - default=True - ) - - bpy.types.Scene.static_collection_export = bpy.props.BoolProperty( - name="Collection(s) ", - description="Check mark to export Collection(s)", - default=True - ) - - bpy.types.Scene.skeletal_export = bpy.props.BoolProperty( - name="SkeletalMesh(s)", - description="Check mark to export SkeletalMesh(s)", - default=True - ) - - bpy.types.Scene.anin_export = bpy.props.BoolProperty( - name="Animation(s)", - description="Check mark to export Animation(s)", - default=True - ) - - bpy.types.Scene.alembic_export = bpy.props.BoolProperty( - name="Alembic Animation(s)", - description="Check mark to export Alembic animation(s)", - default=True - ) - - bpy.types.Scene.groom_simulation_export = bpy.props.BoolProperty( - name="Groom Simulation(s)", - description="Check mark to export Alembic animation(s)", - default=True - ) - - bpy.types.Scene.camera_export = bpy.props.BoolProperty( - name="Camera(s)", - description="Check mark to export Camera(s)", - default=True - ) - - bpy.types.Scene.spline_export = bpy.props.BoolProperty( - name="Spline(s)", - description="Check mark to export Spline(s)", - default=True - ) - - # Additional file - bpy.types.Scene.text_ExportLog = bpy.props.BoolProperty( - name="Export Log", - description="Check mark to write export log file", - default=True - ) - - bpy.types.Scene.text_ImportAssetScript = bpy.props.BoolProperty( - name="Import assets script", - description="Check mark to write import asset script file", - default=True - ) - - bpy.types.Scene.text_ImportSequenceScript = bpy.props.BoolProperty( - name="Import sequence script", - description="Check mark to write import sequencer script file", - default=True - ) - - bpy.types.Scene.text_AdditionalData = bpy.props.BoolProperty( - name="Additional data", - description=( - "Check mark to write additional data" + - " like parameter or anim tracks"), - default=True - ) - - # exportProperty - bpy.types.Scene.bfu_export_selection_filter = bpy.props.EnumProperty( - name="Selection filter", - items=[ - ('default', "No Filter", "Export as normal all objects with the recursive export option.", 0), - ('only_object', "Only selected", "Export only the selected and visible object(s)", 1), - ('only_object_action', "Only selected and active action", - "Export only the selected and visible object(s) and active action on this object", 2), - ], - description=( - "Choose what need be export from asset list."), - default="default" - ) - - def draw(self, context): - scene = context.scene - scene = context.scene - addon_prefs = bfu_basics.GetAddonPrefs() - - # Categories : - layout = self.layout - - # Presets - row = self.layout.row(align=True) - row.menu('BFU_MT_NomenclaturePresets', text='Export Presets') - row.operator('object.add_nomenclature_preset', text='', icon='ADD') - row.operator( - 'object.add_nomenclature_preset', - text='', - icon='REMOVE').remove_active = True - - scene.bfu_nomenclature_properties_expanded.draw(layout) - if scene.bfu_nomenclature_properties_expanded.is_expend(): - - # Prefix - propsPrefix = self.layout.row() - propsPrefix = propsPrefix.column() - propsPrefix.prop(scene, 'bfu_static_mesh_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_skeletal_mesh_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_skeleton_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_alembic_animation_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_groom_simulation_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_anim_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_pose_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_camera_prefix_export_name', icon='OBJECT_DATA') - propsPrefix.prop(scene, 'bfu_spline_prefix_export_name', icon='OBJECT_DATA') - - # Sub folder - propsSub = self.layout.row() - propsSub = propsSub.column() - propsSub.prop(scene, 'bfu_anim_subfolder_name', icon='FILE_FOLDER') - - if addon_prefs.useGeneratedScripts: - bfu_unreal_import_module = propsSub.column() - bfu_unreal_import_module.prop( - scene, - 'bfu_unreal_import_module', - icon='FILE_FOLDER') - bfu_unreal_import_location = propsSub.column() - bfu_unreal_import_location.prop( - scene, - 'bfu_unreal_import_location', - icon='FILE_FOLDER') - - # File path - filePath = self.layout.row() - filePath = filePath.column() - filePath.prop(scene, 'bfu_export_static_file_path') - filePath.prop(scene, 'bfu_export_skeletal_file_path') - filePath.prop(scene, 'bfu_export_alembic_file_path') - filePath.prop(scene, 'bfu_export_groom_file_path') - filePath.prop(scene, 'bfu_export_camera_file_path') - filePath.prop(scene, 'bfu_export_spline_file_path') - filePath.prop(scene, 'bfu_export_other_file_path') - - # File name - fileName = self.layout.row() - fileName = fileName.column() - fileName.prop(scene, 'bfu_file_export_log_name', icon='FILE') - if addon_prefs.useGeneratedScripts: - fileName.prop( - scene, - 'bfu_file_import_asset_script_name', - icon='FILE') - fileName.prop( - scene, - 'bfu_file_import_sequencer_script_name', - icon='FILE') - - scene.bfu_export_filter_properties_expanded.draw(layout) - if scene.bfu_export_filter_properties_expanded.is_expend(): - - # Assets - row = layout.row() - AssetsCol = row.column() - AssetsCol.label(text="Asset types to export", icon='PACKAGE') - AssetsCol.prop(scene, 'static_export') - AssetsCol.prop(scene, 'static_collection_export') - AssetsCol.prop(scene, 'skeletal_export') - AssetsCol.prop(scene, 'anin_export') - AssetsCol.prop(scene, 'alembic_export') - AssetsCol.prop(scene, 'groom_simulation_export') - AssetsCol.prop(scene, 'camera_export') - AssetsCol.prop(scene, 'spline_export') - layout.separator() - - # Additional file - FileCol = row.column() - FileCol.label(text="Additional file", icon='PACKAGE') - FileCol.prop(scene, 'text_ExportLog') - FileCol.prop(scene, 'text_ImportAssetScript') - FileCol.prop(scene, 'text_ImportSequenceScript') - if addon_prefs.useGeneratedScripts: - FileCol.prop(scene, 'text_AdditionalData') - - # exportProperty - export_by_select = layout.row() - export_by_select.prop(scene, 'bfu_export_selection_filter') - - scene.bfu_export_process_properties_expanded.draw(layout) - if scene.bfu_export_process_properties_expanded.is_expend(): - - # Feedback info : - final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() - final_asset_list_to_export = final_asset_cache.GetFinalAssetList() - AssetNum = len(final_asset_list_to_export) - AssetInfo = layout.row().box().split(factor=0.75) - AssetFeedback = str(AssetNum) + " Asset(s) will be exported." - AssetInfo.label(text=AssetFeedback, icon='INFO') - AssetInfo.operator("object.showasset") - - # Export button : - checkButton = layout.row(align=True) - checkButton.operator("object.checkpotentialerror", icon='FILE_TICK') - checkButton.operator("object.openpotentialerror", icon='LOOP_BACK', text="") - - exportButton = layout.row() - exportButton.scale_y = 2.0 - exportButton.operator("object.exportforunreal", icon='EXPORT') - - scene.bfu_script_tool_expanded.draw(layout) - if scene.bfu_script_tool_expanded.is_expend(): - if addon_prefs.useGeneratedScripts: - copyButton = layout.row() - copyButton.operator("object.copy_importassetscript_command") - copyButton.operator("object.copy_importsequencerscript_command") - layout.label(text="Click on one of the buttons to copy the import command.", icon='INFO') - layout.label(text="Then paste it into the cmd console of unreal.") - layout.label(text="You need activate python plugins in Unreal Engine.") - - else: - layout.label(text='(Generated scripts are deactivated.)') - -# ------------------------------------------------------------------- -# Register & Unregister -# ------------------------------------------------------------------- - -classes = ( - BFU_PT_Export, - BFU_PT_Export.BFU_MT_NomenclaturePresets, - BFU_PT_Export.BFU_OT_AddNomenclaturePreset, - BFU_PT_Export.BFU_OT_ShowAssetToExport, - BFU_PT_Export.BFU_OT_CheckPotentialErrorPopup, - BFU_PT_Export.BFU_OT_OpenPotentialErrorPopup, - BFU_PT_Export.BFU_OT_OpenPotentialErrorPopup.BFU_OT_FixitTarget, - BFU_PT_Export.BFU_OT_OpenPotentialErrorPopup.BFU_OT_SelectObjectButton, - BFU_PT_Export.BFU_OT_OpenPotentialErrorPopup.BFU_OT_SelectVertexButton, - BFU_PT_Export.BFU_OT_OpenPotentialErrorPopup.BFU_OT_SelectPoseBoneButton, - BFU_PT_Export.BFU_OT_OpenPotentialErrorPopup.BFU_OT_OpenPotentialErrorDocs, - BFU_PT_Export.BFU_OT_ExportForUnrealEngineButton, - BFU_PT_Export.BFU_OT_CopyImportAssetScriptCommand, - BFU_PT_Export.BFU_OT_CopyImportSequencerScriptCommand, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_object_ui_and_props.py b/blender-for-unrealengine/bfu_addon_parts/bfu_object_ui_and_props.py deleted file mode 100644 index 814d9293..00000000 --- a/blender-for-unrealengine/bfu_addon_parts/bfu_object_ui_and_props.py +++ /dev/null @@ -1,1653 +0,0 @@ -# ====================== BEGIN GPL LICENSE BLOCK ============================ -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# All rights reserved. -# -# ======================= END GPL LICENSE BLOCK ============================= - - -import os -import bpy -import addon_utils -from . import bfu_modular_skeletal_specified_parts_meshs -from . import bfu_unreal_engine_refs_props -from .. import bbpl -from .. import bps -from .. import bfu_export_procedure -from .. import bfu_basics -from .. import bfu_utils -from .. import bfu_cached_asset_list -from .. import bfu_export -from .. import bfu_ui -from .. import languages -from .. import bfu_custom_property -from .. import bfu_material -from .. import bfu_camera -from .. import bfu_spline -from .. import bfu_vertex_color -from .. import bfu_static_mesh -from .. import bfu_skeletal_mesh -from .. import bfu_lod -from .. import bfu_alembic_animation -from .. import bfu_groom -from .. import bfu_assets_manager -from .. import bfu_light_map - - -class BFU_PT_BlenderForUnrealObject(bpy.types.Panel): - # Unreal engine export panel - - bl_idname = "BFU_PT_BlenderForUnrealObject" - bl_label = "Blender for Unreal Engine" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "Unreal Engine" - - # Object Properties - bpy.types.Object.bfu_export_type = bpy.props.EnumProperty( - name="Export type", - description="Export procedure", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ("auto", - "Auto", - "Export with the parent if the parents is \"Export recursive\"", - "BOIDS", - 1), - ("export_recursive", - "Export recursive", - "Export self object and all children", - "KEYINGSET", - 2), - ("dont_export", - "Not exported", - "Will never export", - "CANCEL", - 3) - ] - ) - - bpy.types.Object.bfu_export_folder_name = bpy.props.StringProperty( - name="Sub folder name", - description=( - 'The name of sub folder.' + - ' You can now use ../ for up one directory.' - ), - override={'LIBRARY_OVERRIDABLE'}, - maxlen=64, - default="", - subtype='FILE_NAME' - ) - - # Collection Properties - bpy.types.Collection.bfu_export_folder_name = bpy.props.StringProperty( - name="Sub folder name", - description=( - 'The name of sub folder.' + - ' You can now use ../ for up one directory.' - ), - override={'LIBRARY_OVERRIDABLE'}, - maxlen=64, - default="", - subtype='FILE_NAME' - ) - - bpy.types.Object.bfu_export_as_lod_mesh = bpy.props.BoolProperty( - name="Export as lod?", - description=( - "If true this mesh will be exported" + - " as a level of detail for another mesh" - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_export_deform_only = bpy.props.BoolProperty( - name="Export only deform bones", - description=( - "Only write deforming bones" + - " (and non-deforming ones when they have deforming children)" - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True - ) - - bpy.types.Object.bfu_use_custom_export_name = bpy.props.BoolProperty( - name="Export with custom name", - description=("Specify a custom name for the exported file"), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_custom_export_name = bpy.props.StringProperty( - name="", - description="The name of exported file", - override={'LIBRARY_OVERRIDABLE'}, - default="MyObjectExportName.fbx" - ) - - # Object Import Properties - - # Lod list - bpy.types.Object.bfu_lod_target1 = bpy.props.PointerProperty( - name="LOD1", - description="Target objet for level of detail 01", - override={'LIBRARY_OVERRIDABLE'}, - type=bpy.types.Object - ) - - bpy.types.Object.bfu_lod_target2 = bpy.props.PointerProperty( - name="LOD2", - description="Target objet for level of detail 02", - override={'LIBRARY_OVERRIDABLE'}, - type=bpy.types.Object - ) - - bpy.types.Object.bfu_lod_target3 = bpy.props.PointerProperty( - name="LOD3", - description="Target objet for level of detail 03", - override={'LIBRARY_OVERRIDABLE'}, - type=bpy.types.Object - ) - - bpy.types.Object.bfu_lod_target4 = bpy.props.PointerProperty( - name="LOD4", - description="Target objet for level of detail 04", - override={'LIBRARY_OVERRIDABLE'}, - type=bpy.types.Object - ) - - bpy.types.Object.bfu_lod_target5 = bpy.props.PointerProperty( - name="LOD5", - description="Target objet for level of detail 05", - override={'LIBRARY_OVERRIDABLE'}, - type=bpy.types.Object - ) - - # ImportUI - # https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxImportUI/index.html - - bpy.types.Object.bfu_create_physics_asset = bpy.props.BoolProperty( - name="Create PhysicsAsset", - description="If checked, create a PhysicsAsset when is imported", - override={'LIBRARY_OVERRIDABLE'}, - default=True - ) - - - # StaticMeshImportData - # https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxStaticMeshImportData/index.html - - bpy.types.Object.bfu_use_static_mesh_lod_group = bpy.props.BoolProperty( - name="", - description='', - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_static_mesh_lod_group = bpy.props.StringProperty( - name="LOD Group", - description=( - "The LODGroup to associate with this mesh when it is imported." + - " Default: LevelArchitecture, SmallProp, " + - "LargeProp, Deco, Vista, Foliage, HighDetail" - ), - override={'LIBRARY_OVERRIDABLE'}, - maxlen=32, - default="SmallProp" - ) - - bpy.types.Object.bfu_static_mesh_light_map_mode = bpy.props.EnumProperty( - name="Light Map", - description='Specify how the light map resolution will be generated', - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ("Default", - "Default", - "Has no effect on light maps", - 1), - ("CustomMap", - "Custom map", - "Set the custom light map resolution", - 2), - ("SurfaceArea", - "Surface Area", - "Set light map resolution depending on the surface Area", - 3) - ] - ) - - bpy.types.Object.bfu_static_mesh_custom_light_map_res = bpy.props.IntProperty( - name="Light Map Resolution", - description="This is the resolution of the light map", - override={'LIBRARY_OVERRIDABLE'}, - soft_max=2048, - soft_min=16, - max=4096, # Max for unreal - min=4, # Min for unreal - default=64 - ) - - bpy.types.Object.computedStaticMeshLightMapRes = bpy.props.FloatProperty( - name="Computed Light Map Resolution", - description="This is the computed resolution of the light map", - override={'LIBRARY_OVERRIDABLE'}, - default=64.0 - ) - - bpy.types.Object.bfu_static_mesh_light_map_surface_scale = bpy.props.FloatProperty( - name="Surface scale", - description="This is for resacle the surface Area value", - override={'LIBRARY_OVERRIDABLE'}, - min=0.00001, # Min for unreal - default=64 - ) - - bpy.types.Object.bfu_static_mesh_light_map_round_power_of_two = bpy.props.BoolProperty( - name="Round power of 2", - description=( - "round Light Map resolution to nearest power of 2" - ), - default=True - ) - - bpy.types.Object.bfu_use_static_mesh_light_map_world_scale = bpy.props.BoolProperty( - name="Use world scale", - description=( - "If not that will use the object scale." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_generate_light_map_uvs = bpy.props.BoolProperty( - name="Generate LightmapUVs", - description=( - "If checked, UVs for Lightmap will automatically be generated." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True, - ) - - bpy.types.Object.bfu_convert_geometry_node_attribute_to_uv = bpy.props.BoolProperty( - name="Convert Attribute To Uv", - description=( - "convert target geometry node attribute to UV when found." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False, - ) - - bpy.types.Object.bfu_convert_geometry_node_attribute_to_uv_name = bpy.props.StringProperty( - name="Attribute name", - description=( - "Name of the Attribute to convert" - ), - override={'LIBRARY_OVERRIDABLE'}, - default="UVMap", - ) - - bpy.types.Object.bfu_correct_extrem_uv_scale = bpy.props.BoolProperty( - name=(languages.ti('correct_extrem_uv_scale_name')), - description=(languages.tt('correct_extrem_uv_scale_desc')), - override={'LIBRARY_OVERRIDABLE'}, - default=False, - ) - - bpy.types.Object.bfu_auto_generate_collision = bpy.props.BoolProperty( - name="Auto Generate Collision", - description=( - "If checked, collision will automatically be generated" + - " (ignored if custom collision is imported or used)." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True, - ) - - - bpy.types.Object.bfu_collision_trace_flag = bpy.props.EnumProperty( - name="Collision Complexity", - description="Collision Trace Flag", - override={'LIBRARY_OVERRIDABLE'}, - # Vania python - # https://docs.unrealengine.com/en-US/PythonAPI/class/CollisionTraceFlag.html - # C++ API - # https://api.unrealengine.com/INT/API/Runtime/Engine/PhysicsEngine/ECollisionTraceFlag/index.html - items=[ - ("CTF_UseDefault", - "Project Default", - "Create only complex shapes (per poly)." + - " Use complex shapes for all scene queries" + - " and collision tests." + - " Can be used in simulation for" + - " static shapes only" + - " (i.e can be collided against but not moved" + - " through forces or velocity.", - 1), - ("CTF_UseSimpleAndComplex", - "Use Simple And Complex", - "Use project physics settings (DefaultShapeComplexity)", - 2), - ("CTF_UseSimpleAsComplex", - "Use Simple as Complex", - "Create both simple and complex shapes." + - " Simple shapes are used for regular scene queries" + - " and collision tests. Complex shape (per poly)" + - " is used for complex scene queries.", - 3), - ("CTF_UseComplexAsSimple", - "Use Complex as Simple", - "Create only simple shapes." + - " Use simple shapes for all scene" + - " queries and collision tests.", - 4) - ] - ) - - bpy.types.Object.bfu_enable_skeletal_mesh_per_poly_collision = bpy.props.BoolProperty( - name="Enable Per-Poly Collision", - description="Enable per-polygon collision for Skeletal Mesh", - default=False - ) - - - - bpy.types.Object.bfu_anim_action_export_enum = bpy.props.EnumProperty( - name="Action to export", - description="Export procedure for actions (Animations and poses)", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ("export_auto", - "Export auto", - "Export all actions connected to the bones names", - "FILE_SCRIPT", - 1), - ("export_specific_list", - "Export specific list", - "Export only actions that are checked in the list", - "LINENUMBERS_ON", - 3), - ("export_specific_prefix", - "Export specific prefix", - "Export only actions with a specific prefix" + - " or the beginning of the actions names", - "SYNTAX_ON", - 4), - ("dont_export", - "Not exported", - "No action will be exported", - "MATPLANE", - 5), - ("export_current", - "Export Current", - "Export only the current actions", - "FILE_SCRIPT", - 6), - ] - ) - - bpy.types.Object.bfu_prefix_name_to_export = bpy.props.StringProperty( - # properties used with ""export_specific_prefix" on bfu_anim_action_export_enum - name="Prefix name", - description="Indicate the prefix of the actions that must be exported", - override={'LIBRARY_OVERRIDABLE'}, - maxlen=32, - default="Example_", - ) - - bpy.types.Object.bfu_anim_action_start_end_time_enum = bpy.props.EnumProperty( - name="Action Start/End Time", - description="Set when animation starts and end", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ("with_keyframes", - "Auto", - "The time will be defined according" + - " to the first and the last frame", - "KEYTYPE_KEYFRAME_VEC", - 1), - ("with_sceneframes", - "Scene time", - "Time will be equal to the scene time", - "SCENE_DATA", - 2), - ("with_customframes", - "Custom time", - 'The time of all the animations of this object' + - ' is defined by you.' + - ' Use "bfu_anim_action_custom_start_frame" and "bfu_anim_action_custom_end_frame"', - "HAND", - 3), - ] - ) - - bpy.types.Object.bfu_anim_action_start_frame_offset = bpy.props.IntProperty( - name="Offset at start frame", - description="Offset for the start frame.", - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_anim_action_end_frame_offset = bpy.props.IntProperty( - name="Offset at end frame", - description=( - "Offset for the end frame. +1" + - " is recommended for the sequences | 0 is recommended" + - " for UnrealEngine cycles | -1 is recommended for Sketchfab cycles" - ), - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_anim_action_custom_start_frame = bpy.props.IntProperty( - name="Custom start time", - description="Set when animation start", - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_anim_action_custom_end_frame = bpy.props.IntProperty( - name="Custom end time", - description="Set when animation end", - override={'LIBRARY_OVERRIDABLE'}, - default=1 - ) - - bpy.types.Object.bfu_anim_nla_start_end_time_enum = bpy.props.EnumProperty( - name="NLA Start/End Time", - description="Set when animation starts and end", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ("with_sceneframes", - "Scene time", - "Time will be equal to the scene time", - "SCENE_DATA", - 1), - ("with_customframes", - "Custom time", - 'The time of all the animations of this object' + - ' is defined by you.' + - ' Use "bfu_anim_action_custom_start_frame" and "bfu_anim_action_custom_end_frame"', - "HAND", - 2), - ] - ) - - bpy.types.Object.bfu_anim_nla_start_frame_offset = bpy.props.IntProperty( - name="Offset at start frame", - description="Offset for the start frame.", - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_anim_nla_end_frame_offset = bpy.props.IntProperty( - name="Offset at end frame", - description=( - "Offset for the end frame. +1" + - " is recommended for the sequences | 0 is recommended" + - " for UnrealEngine cycles | -1 is recommended for Sketchfab cycles" - ), - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_anim_nla_custom_start_frame = bpy.props.IntProperty( - name="Custom start time", - description="Set when animation start", - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_anim_nla_custom_end_frame = bpy.props.IntProperty( - name="Custom end time", - description="Set when animation end", - override={'LIBRARY_OVERRIDABLE'}, - default=1 - ) - - - bpy.types.Object.bfu_sample_anim_for_export = bpy.props.FloatProperty( - name="Sampling Rate", - description="How often to evaluate animated values (in frames)", - override={'LIBRARY_OVERRIDABLE'}, - min=0.01, max=100.0, - soft_min=0.01, soft_max=100.0, - default=1.0, - ) - - bpy.types.Object.bfu_simplify_anim_for_export = bpy.props.FloatProperty( - name="Simplify animations", - description=( - "How much to simplify baked values" + - " (0.0 to disable, the higher the more simplified)" - ), - override={'LIBRARY_OVERRIDABLE'}, - # No simplification to up to 10% of current magnitude tolerance. - min=0.0, max=100.0, - soft_min=0.0, soft_max=10.0, - default=0.0, - ) - - bpy.types.Object.bfu_disable_free_scale_animation = bpy.props.BoolProperty( - name="Disable non-uniform scale animation.", - description=( - "If checked, scale animation track's elements always have same value. " + - "This applies basic bones only." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_anim_nla_use = bpy.props.BoolProperty( - name="Export NLA (Nonlinear Animation)", - description=( - "If checked, exports the all animation of the scene with the NLA " + - "(Don't work with Auto-Rig Pro for the moment.)" - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_anim_nla_export_name = bpy.props.StringProperty( - name="NLA export name", - description="Export NLA name (Don't work with Auto-Rig Pro for the moment.)", - override={'LIBRARY_OVERRIDABLE'}, - maxlen=64, - default="NLA_animation", - subtype='FILE_NAME' - ) - - bpy.types.Object.bfu_anim_naming_type = bpy.props.EnumProperty( - name="Naming type", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ('action_name', "Action name", 'Exemple: "Anim_MyAction"'), - ('include_armature_name', - "Include Armature Name", - 'Include armature name in animation export file name.' + - ' Exemple: "Anim_MyArmature_MyAction"'), - ('include_custom_name', - "Include custom name", - 'Include custom name in animation export file name.' + - ' Exemple: "Anim_MyCustomName_MyAction"'), - ], - default='action_name' - ) - - bpy.types.Object.bfu_anim_naming_custom = bpy.props.StringProperty( - name="Export name", - override={'LIBRARY_OVERRIDABLE'}, - default='MyCustomName' - ) - - bpy.types.Object.bfu_export_global_scale = bpy.props.FloatProperty( - name="Global scale", - description="Scale, change is not recommended with SkeletalMesh.", - override={'LIBRARY_OVERRIDABLE'}, - default=1.0 - ) - - bpy.types.Object.bfu_override_procedure_preset = bpy.props.BoolProperty( - name="Override Export Preset", - description="If true override the export precedure preset.", - override={'LIBRARY_OVERRIDABLE'}, - default=False, - ) - - bpy.types.Object.bfu_export_use_space_transform = bpy.props.BoolProperty( - name="Use Space Transform", - default=True, - ) - - bpy.types.Object.bfu_export_axis_forward = bpy.props.EnumProperty( - name="Axis Forward", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ('X', "X Forward", ""), - ('Y', "Y Forward", ""), - ('Z', "Z Forward", ""), - ('-X', "-X Forward", ""), - ('-Y', "-Y Forward", ""), - ('-Z', "-Z Forward", ""), - ], - default='-Z', - ) - - bpy.types.Object.bfu_export_axis_up = bpy.props.EnumProperty( - name="Axis Up", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ('X', "X Up", ""), - ('Y', "Y Up", ""), - ('Z', "Z Up", ""), - ('-X', "-X Up", ""), - ('-Y', "-Y Up", ""), - ('-Z', "-Z Up", ""), - ], - default='Y', - ) - - bpy.types.Object.bfu_export_primary_bone_axis = bpy.props.EnumProperty( - name="Primary Axis Bone", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ('X', "X", ""), - ('Y', "Y", ""), - ('Z', "Z", ""), - ('-X', "-X", ""), - ('-Y', "-Y", ""), - ('-Z', "-Z", ""), - ], - default='Y', - ) - - bpy.types.Object.bfu_export_secondary_bone_axis = bpy.props.EnumProperty( - name="Secondary Axis Bone", - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ('X', "X", ""), - ('Y', "Y", ""), - ('Z', "Z", ""), - ('-X', "-X", ""), - ('-Y', "-Y", ""), - ('-Z', "-Z", ""), - ], - default='X', - ) - - bpy.types.Object.bfu_export_animation_without_mesh = bpy.props.BoolProperty( - name="Export animation without mesh", - description=( - "If checked, When exporting animation, do not include mesh data in the FBX file." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True - ) - - bpy.types.Object.bfu_mirror_symmetry_right_side_bones = bpy.props.BoolProperty( - name="Revert direction of symmetry right side bones", - description=( - "If checked, The right-side bones will be mirrored for mirroring physic object in UE PhysicAsset Editor." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True - ) - - bpy.types.Object.bfu_use_ue_mannequin_bone_alignment = bpy.props.BoolProperty( - name="Apply bone alignments similar to UE Mannequin.", - description=( - "If checked, similar to the UE Mannequin, the leg bones will be oriented upwards, and the pelvis and feet bone will be aligned facing upwards during export." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_move_to_center_for_export = bpy.props.BoolProperty( - name="Move to center", - description=( - "If true use object origin else use scene origin." + - " | If true the mesh will be moved to the center" + - " of the scene for export." + - " (This is used so that the origin of the fbx file" + - " is the same as the mesh in blender)" - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True - ) - - bpy.types.Object.bfu_rotate_to_zero_for_export = bpy.props.BoolProperty( - name="Rotate to zero", - description=( - "If true use object rotation else use scene rotation." + - " | If true the mesh will use zero rotation for export." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_move_action_to_center_for_export = bpy.props.BoolProperty( - name="Move animation to center", - description=( - "(Action animation only) If true use object origin else use scene origin." + - " | If true the mesh will be moved to the center" + - " of the scene for export." + - " (This is used so that the origin of the fbx file" + - " is the same as the mesh in blender)" + - " Note: Unreal Engine ignore the position of the skeleton at the import." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True - ) - - bpy.types.Object.bfu_rotate_action_to_zero_for_export = bpy.props.BoolProperty( - name="Rotate Action to zero", - description=( - "(Action animation only) If true use object rotation else use scene rotation." + - " | If true the mesh will use zero rotation for export." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_move_nla_to_center_for_export = bpy.props.BoolProperty( - name="Move NLA to center", - description=( - "(Non linear animation only) If true use object origin else use scene origin." + - " | If true the mesh will be moved to the center" + - " of the scene for export." + - " (This is used so that the origin of the fbx file" + - " is the same as the mesh in blender)" + - " Note: Unreal Engine ignore the position of the skeleton at the import." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=True - ) - - bpy.types.Object.bfu_rotate_nla_to_zero_for_export = bpy.props.BoolProperty( - name="Rotate NLA to zero", - description=( - "(Non linear animation only) If true use object rotation else use scene rotation." + - " | If true the mesh will use zero rotation for export." - ), - override={'LIBRARY_OVERRIDABLE'}, - default=False - ) - - bpy.types.Object.bfu_additional_location_for_export = bpy.props.FloatVectorProperty( - name="Additional location", - description=( - "This will add a additional absolute location to the mesh" - ), - override={'LIBRARY_OVERRIDABLE'}, - subtype="TRANSLATION", - default=(0, 0, 0) - ) - - bpy.types.Object.bfu_additional_rotation_for_export = bpy.props.FloatVectorProperty( - name="Additional rotation", - description=( - "This will add a additional absolute rotation to the mesh" - ), - override={'LIBRARY_OVERRIDABLE'}, - subtype="EULER", - default=(0, 0, 0) - ) - - # Scene and global - - - - class BFU_OT_OpenDocumentationPage(bpy.types.Operator): - bl_label = "Documentation" - bl_idname = "object.bfu_open_documentation_page" - bl_description = "Clic for open documentation page on GitHub" - - def execute(self, context): - os.system( - "start \"\" " + - "https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki" - ) - return {'FINISHED'} - - - - # Animation : - - class BFU_UL_ActionExportTarget(bpy.types.UIList): - def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): - action_is_valid = False - if item.name in bpy.data.actions: - action_is_valid = True - - if self.layout_type in {'DEFAULT', 'COMPACT'}: - if action_is_valid: # If action is valid - layout.prop( - bpy.data.actions[item.name], - "name", - text="", - emboss=False, - icon="ACTION" - ) - layout.prop(item, "use", text="") - else: - dataText = ( - 'Action data named "' + item.name + - '" Not Found. Please click on update' - ) - layout.label(text=dataText, icon="ERROR") - # Not optimized for 'GRID' layout type. - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - - class BFU_OT_UpdateObjActionListButton(bpy.types.Operator): - bl_label = "Update action list" - bl_idname = "object.updateobjactionlist" - bl_description = "Update action list" - - def execute(self, context): - def UpdateExportActionList(obj): - # Update the provisional action list known by the object - - def SetUseFromLast(anim_list, ActionName): - for item in anim_list: - if item[0] == ActionName: - if item[1]: - return True - return False - - animSave = [["", False]] - for Anim in obj.bfu_animation_asset_list: # CollectionProperty - name = Anim.name - use = Anim.use - animSave.append([name, use]) - obj.bfu_animation_asset_list.clear() - for action in bpy.data.actions: - obj.bfu_animation_asset_list.add().name = action.name - useFromLast = SetUseFromLast(animSave, action.name) - obj.bfu_animation_asset_list[action.name].use = useFromLast - UpdateExportActionList(bpy.context.object) - return {'FINISHED'} - - class BFU_OT_ShowActionToExport(bpy.types.Operator): - bl_label = "Show action(s)" - bl_idname = "object.showobjaction" - bl_description = ( - "Click to show actions that are" + - " to be exported with this armature." - ) - - def execute(self, context): - obj = context.object - animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) - animation_asset_cache.UpdateActionCache() - animation_to_export = animation_asset_cache.GetAnimationAssetList() - - popup_title = "Action list" - if len(animation_to_export) > 0: - animationNumber = len(animation_to_export) - if obj.bfu_anim_nla_use: - animationNumber += 1 - popup_title = ( - str(animationNumber) + - ' action(s) found for obj named "'+obj.name+'".' - ) - else: - popup_title = ( - 'No action found for obj named "' + - obj.name+'".') - - def draw(self, context): - col = self.layout.column() - - def addAnimRow( - action_name, - action_type, - frame_start, - frame_end): - row = col.row() - row.label( - text="- ["+action_name + - "] Frame "+frame_start+" to "+frame_end + - " ("+action_type+")" - ) - - for action in animation_to_export: - Frames = bfu_utils.GetDesiredActionStartEndTime(obj, action) - frame_start = str(Frames[0]) - frame_end = str(Frames[1]) - addAnimRow(action.name, bfu_utils.GetActionType(action), frame_start, frame_end) - if obj.bfu_anim_nla_use: - scene = context.scene - addAnimRow(obj.bfu_anim_nla_export_name, "NlAnim", str(scene.frame_start), str(scene.frame_end)) - - bpy.context.window_manager.popup_menu( - draw, - title=popup_title, - icon='ACTION' - ) - return {'FINISHED'} - - class BFU_MT_ObjectGlobalPropertiesPresets(bpy.types.Menu): - bl_label = 'Global Properties Presets' - preset_subdir = 'blender-for-unrealengine/global-properties-presets' - preset_operator = 'script.execute_preset' - draw = bpy.types.Menu.draw_preset - - from bl_operators.presets import AddPresetBase - - - - class BFU_OT_AddObjectGlobalPropertiesPreset(AddPresetBase, bpy.types.Operator): - bl_idname = 'object.add_globalproperties_preset' - bl_label = 'Add or remove a preset for Global properties' - bl_description = 'Add or remove a preset for Global properties' - preset_menu = 'BFU_MT_ObjectGlobalPropertiesPresets' - - def get_object_global_preset_propertys(): - preset_values = [ - 'obj.bfu_export_type', - 'obj.bfu_export_folder_name', - 'col.bfu_export_folder_name', - 'obj.bfu_export_as_lod_mesh', - 'obj.bfu_export_deform_only', - 'obj.bfu_lod_target1', - 'obj.bfu_lod_target2', - 'obj.bfu_lod_target3', - 'obj.bfu_lod_target4', - 'obj.bfu_lod_target5', - 'obj.bfu_create_physics_asset', - 'obj.bfu_use_static_mesh_lod_group', - 'obj.bfu_static_mesh_lod_group', - 'obj.bfu_static_mesh_light_map_mode', - 'obj.bfu_static_mesh_custom_light_map_res', - 'obj.bfu_static_mesh_light_map_surface_scale', - 'obj.bfu_static_mesh_light_map_round_power_of_two', - 'obj.bfu_use_static_mesh_light_map_world_scale', - 'obj.bfu_generate_light_map_uvs', - 'obj.bfu_convert_geometry_node_attribute_to_uv', - 'obj.bfu_convert_geometry_node_attribute_to_uv_name', - 'obj.bfu_correct_extrem_uv_scale', - 'obj.bfu_auto_generate_collision', - 'obj.bfu_collision_trace_flag', - 'obj.bfu_enable_skeletal_mesh_per_poly_collision', - 'obj.bfu_anim_action_export_enum', - 'obj.bfu_prefix_name_to_export', - 'obj.bfu_anim_action_start_end_time_enum', - 'obj.bfu_anim_nla_start_end_time_enum', - 'obj.bfu_anim_action_start_frame_offset', - 'obj.bfu_anim_action_end_frame_offset', - 'obj.bfu_anim_action_custom_start_frame', - 'obj.bfu_anim_action_custom_end_frame', - 'obj.bfu_anim_nla_start_frame_offset', - 'obj.bfu_anim_nla_end_frame_offset', - 'obj.bfu_anim_nla_custom_start_frame', - 'obj.bfu_anim_nla_custom_end_frame', - 'obj.bfu_sample_anim_for_export', - 'obj.bfu_simplify_anim_for_export', - 'obj.bfu_anim_nla_use', - 'obj.bfu_anim_nla_export_name', - 'obj.bfu_anim_naming_type', - 'obj.bfu_anim_naming_custom', - 'obj.bfu_export_global_scale', - 'obj.bfu_override_procedure_preset', - 'obj.bfu_export_use_space_transform', - 'obj.bfu_export_axis_forward', - 'obj.bfu_export_axis_up', - 'obj.bfu_export_with_meta_data', - 'obj.bfu_export_axis_forward', - 'obj.bfu_export_axis_up', - 'obj.bfu_export_primary_bone_axis', - 'obj.bfu_export_secondary_bone_axis', - 'obj.bfu_export_animation_without_mesh', - 'obj.bfu_mirror_symmetry_right_side_bones', - 'obj.bfu_use_ue_mannequin_bone_alignment', - 'obj.bfu_disable_free_scale_animation', - 'obj.bfu_move_to_center_for_export', - 'obj.bfu_rotate_to_zero_for_export', - 'obj.bfu_move_action_to_center_for_export', - 'obj.bfu_rotate_action_to_zero_for_export', - 'obj.bfu_move_nla_to_center_for_export', - 'obj.bfu_rotate_nla_to_zero_for_export', - 'obj.bfu_additional_location_for_export', - 'obj.bfu_additional_rotation_for_export', - ] - preset_values += bfu_modular_skeletal_specified_parts_meshs.get_preset_values() - preset_values += bfu_unreal_engine_refs_props.get_preset_values() - preset_values += bfu_custom_property.bfu_custom_property_props.get_preset_values() - preset_values += bfu_material.bfu_material_props.get_preset_values() - preset_values += bfu_camera.bfu_camera_ui_and_props.get_preset_values() - preset_values += bfu_spline.bfu_spline_ui_and_props.get_preset_values() - preset_values += bfu_static_mesh.bfu_static_mesh_props.get_preset_values() - preset_values += bfu_skeletal_mesh.bfu_skeletal_mesh_props.get_preset_values() - preset_values += bfu_alembic_animation.bfu_alembic_animation_props.get_preset_values() - preset_values += bfu_vertex_color.bfu_vertex_color_props.get_preset_values() - return preset_values - - # Common variable used for all preset values - preset_defines = [ - 'obj = bpy.context.object', - 'col = bpy.context.collection', - 'scene = bpy.context.scene' - ] - - # Properties to store in the preset - preset_values = get_object_global_preset_propertys() - - # Directory to store the presets - preset_subdir = 'blender-for-unrealengine/global-properties-presets' - - - - class BFU_UL_CollectionExportTarget(bpy.types.UIList): - - def draw_item(self, context, layout, data, item, icon, active_data, active_property, index, flt_flag): - - collection_is_valid = False - if item.name in bpy.data.collections: - collection_is_valid = True - - if self.layout_type in {'DEFAULT', 'COMPACT'}: - if collection_is_valid: # If action is valid - layout.prop( - bpy.data.collections[item.name], - "name", - text="", - emboss=False, - icon="OUTLINER_COLLECTION") - layout.prop(item, "use", text="") - else: - dataText = ( - 'Collection named "' + - item.name + - '" Not Found. Please clic on update') - layout.label(text=dataText, icon="ERROR") - # Not optimised for 'GRID' layout type. - elif self.layout_type in {'GRID'}: - layout.alignment = 'CENTER' - layout.label(text="", icon_value=icon) - - class BFU_OT_UpdateCollectionButton(bpy.types.Operator): - bl_label = "Update collection list" - bl_idname = "object.updatecollectionlist" - bl_description = "Update collection list" - - def execute(self, context): - def UpdateExportCollectionList(scene): - # Update the provisional collection list known by the object - - def SetUseFromLast(col_list, CollectionName): - for item in col_list: - if item[0] == CollectionName: - if item[1]: - return True - return False - - colSave = [["", False]] - for col in scene.bfu_collection_asset_list: # CollectionProperty - name = col.name - use = col.use - colSave.append([name, use]) - scene.bfu_collection_asset_list.clear() - for col in bpy.data.collections: - scene.bfu_collection_asset_list.add().name = col.name - useFromLast = SetUseFromLast(colSave, col.name) - scene.bfu_collection_asset_list[col.name].use = useFromLast - UpdateExportCollectionList(context.scene) - return {'FINISHED'} - - class BFU_OT_ShowCollectionToExport(bpy.types.Operator): - bl_label = "Show collection(s)" - bl_idname = "object.showscenecollection" - bl_description = "Click to show collections to export" - - def execute(self, context): - scene = context.scene - collection_asset_cache = bfu_cached_asset_list.GetCollectionAssetCache() - collection_export_asset_list = collection_asset_cache.GetCollectionAssetList() - popup_title = "Collection list" - if len(collection_export_asset_list) > 0: - popup_title = ( - str(len(collection_export_asset_list))+' collection(s) to export found.') - else: - popup_title = 'No collection to export found.' - - def draw(self, context): - col = self.layout.column() - for collection in collection_export_asset_list: - row = col.row() - row.label(text="- "+collection.name) - bpy.context.window_manager.popup_menu( - draw, - title=popup_title, - icon='GROUP') - return {'FINISHED'} - - def draw(self, context): - - scene = bpy.context.scene - obj = bpy.context.object - addon_prefs = bfu_basics.GetAddonPrefs() - layout = self.layout - - if bpy.app.version >= (4, 2, 0): - version_str = 'Version '+ bbpl.blender_extension.extension_utils.get_package_version("blender_for_unrealengine") - else: - version_str = 'Version '+ bbpl.blender_addon.addon_utils.get_addon_version_str("Blender for UnrealEngine") - - credit_box = layout.box() - credit_box.label(text=languages.ti('intro')) - credit_box.label(text=version_str) - credit_box.operator("object.bfu_open_documentation_page", icon="HELP") - - row = layout.row(align=True) - row.menu( - 'BFU_MT_ObjectGlobalPropertiesPresets', - text='Global Properties Presets' - ) - row.operator( - 'object.add_globalproperties_preset', - text='', - icon='ADD' - ) - row.operator( - 'object.add_globalproperties_preset', - text='', - icon='REMOVE' - ).remove_active = True - - layout.row().prop(scene, "bfu_active_tab", expand=True) - if scene.bfu_active_tab == "OBJECT": - layout.row().prop(scene, "bfu_active_object_tab", expand=True) - if scene.bfu_active_tab == "SCENE": - layout.row().prop(scene, "bfu_active_scene_tab", expand=True) - - bfu_material.bfu_material_ui.draw_ui_object_collision(layout) - bfu_vertex_color.bfu_vertex_color_ui.draw_ui_object_collision(layout) - - if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): - - scene.bfu_object_properties_expanded.draw(layout) - if scene.bfu_object_properties_expanded.is_expend(): - - if obj is None: - layout.row().label(text='No selected object.') - else: - - AssetType = layout.row() - AssetType.prop(obj, 'name', text="", icon='OBJECT_DATA') - # Show asset type - asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) - if asset_class: - asset_type_name = asset_class.get_asset_type_name(obj) - else: - asset_type_name = "Asset type not found." - - AssetType.label(text='('+asset_type_name+')') - - ExportType = layout.column() - ExportType.prop(obj, 'bfu_export_type') - - - if obj.bfu_export_type == "export_recursive": - - folderNameProperty = layout.column() - folderNameProperty.prop(obj, 'bfu_export_folder_name', icon='FILE_FOLDER') - - ProxyProp = layout.column() - if bfu_utils.GetExportAsProxy(obj): - ProxyProp.label(text="The Armature was detected as a proxy.") - proxy_child = bfu_utils.GetExportProxyChild(obj) - if proxy_child: - ProxyProp.label(text="Proxy child: " + proxy_child.name) - else: - ProxyProp.label(text="Proxy child not found") - - if not bfu_utils.GetExportAsProxy(obj): - # exportCustomName - exportCustomName = layout.row() - exportCustomName.prop(obj, "bfu_use_custom_export_name") - useCustomName = obj.bfu_use_custom_export_name - exportCustomNameText = exportCustomName.column() - exportCustomNameText.prop(obj, "bfu_custom_export_name") - exportCustomNameText.enabled = useCustomName - bfu_alembic_animation.bfu_alembic_animation_ui.draw_general_ui_object(layout, obj) - bfu_groom.bfu_groom_ui.draw_general_ui_object(layout, obj) - bfu_skeletal_mesh.bfu_skeletal_mesh_ui.draw_general_ui_object(layout, obj) - - - - - - bfu_camera.bfu_camera_ui_and_props.draw_ui_object_camera(layout, obj) - bfu_spline.bfu_spline_ui_and_props.draw_ui_object_spline(layout, obj) - bfu_skeletal_mesh.bfu_skeletal_mesh_ui.draw_ui_object(layout, obj) - bfu_static_mesh.bfu_static_mesh_ui.draw_ui_object(layout, obj) - bfu_lod.bfu_lod_ui.draw_ui_object(layout, obj) - bfu_alembic_animation.bfu_alembic_animation_ui.draw_ui_object(layout, obj) - bfu_groom.bfu_groom_ui.draw_ui_object(layout, obj) - - scene.bfu_object_advanced_properties_expanded.draw(layout) - if scene.bfu_object_advanced_properties_expanded.is_expend(): - if obj is not None: - if obj.bfu_export_type == "export_recursive": - - transformProp = layout.column() - is_not_alembic_animation = not bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj) - is_not_camera = not bfu_camera.bfu_camera_utils.is_camera(obj) - if is_not_alembic_animation and is_not_camera: - transformProp.prop(obj, "bfu_move_to_center_for_export") - transformProp.prop(obj, "bfu_rotate_to_zero_for_export") - transformProp.prop(obj, "bfu_additional_location_for_export") - transformProp.prop(obj, "bfu_additional_rotation_for_export") - - transformProp.prop(obj, 'bfu_export_global_scale') - if bfu_camera.bfu_camera_utils.is_camera(obj): - transformProp.prop(obj, "bfu_additional_location_for_export") - - AxisProperty = layout.column() - - AxisProperty.prop(obj, 'bfu_override_procedure_preset') - if obj.bfu_override_procedure_preset: - AxisProperty.prop(obj, 'bfu_export_use_space_transform') - AxisProperty.prop(obj, 'bfu_export_axis_forward') - AxisProperty.prop(obj, 'bfu_export_axis_up') - bbpl.blender_layout.layout_doc_button.add_doc_page_operator(AxisProperty, text="About axis Transforms", url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Axis-Transforms") - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - BoneAxisProperty = layout.column() - BoneAxisProperty.prop(obj, 'bfu_export_primary_bone_axis') - BoneAxisProperty.prop(obj, 'bfu_export_secondary_bone_axis') - else: - box = layout.box() - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - preset = bfu_export_procedure.bfu_skeleton_export_procedure.get_obj_skeleton_procedure_preset(obj) - else: - preset = bfu_export_procedure.bfu_static_export_procedure.get_obj_static_procedure_preset(obj) - var_lines = box.column() - for key, value in preset.items(): - display_key = bps.utils.format_property_name(key) - var_lines.label(text=f"{display_key}: {value}\n") - export_data = layout.column() - bfu_custom_property.bfu_custom_property_utils.draw_ui_custom_property(export_data, obj) - export_data.prop(obj, "bfu_export_with_meta_data") - - - else: - layout.label(text='(No properties to show.)') - - - - scene.bfu_engine_ref_properties_expanded.draw(layout) - if scene.bfu_engine_ref_properties_expanded.is_expend(): - if addon_prefs.useGeneratedScripts and obj is not None: - if obj.bfu_export_type == "export_recursive": - - # SkeletalMesh prop - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - if not obj.bfu_export_as_lod_mesh: - unreal_engine_refs = layout.column() - bfu_unreal_engine_refs_props.draw_skeleton_prop(unreal_engine_refs, obj) - bfu_unreal_engine_refs_props.draw_skeletal_mesh_prop(unreal_engine_refs, obj) - else: - layout.label(text='(No properties to show.)') - - - - - if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): - if obj is not None: - if obj.bfu_export_type == "export_recursive" and not obj.bfu_export_as_lod_mesh: - - scene.bfu_animation_action_properties_expanded.draw(layout) - if scene.bfu_animation_action_properties_expanded.is_expend(): - if (bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj) or - bfu_camera.bfu_camera_utils.is_camera(obj) or - bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj)): - - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - # Action list - ActionListProperty = layout.column() - ActionListProperty.prop(obj, 'bfu_anim_action_export_enum') - if obj.bfu_anim_action_export_enum == "export_specific_list": - ActionListProperty.template_list( - # type and unique id - "BFU_UL_ActionExportTarget", "", - # pointer to the CollectionProperty - obj, "bfu_animation_asset_list", - # pointer to the active identifier - obj, "bfu_active_animation_asset_list", - maxrows=5, - rows=5 - ) - ActionListProperty.operator( - "object.updateobjactionlist", - icon='RECOVER_LAST') - if obj.bfu_anim_action_export_enum == "export_specific_prefix": - ActionListProperty.prop(obj, 'bfu_prefix_name_to_export') - - # Action Time - if obj.type != "CAMERA" and obj.bfu_skeleton_export_procedure != "auto-rig-pro": - ActionTimeProperty = layout.column() - ActionTimeProperty.enabled = obj.bfu_anim_action_export_enum != 'dont_export' - ActionTimeProperty.prop(obj, 'bfu_anim_action_start_end_time_enum') - if obj.bfu_anim_action_start_end_time_enum == "with_customframes": - OfsetTime = ActionTimeProperty.row() - OfsetTime.prop(obj, 'bfu_anim_action_custom_start_frame') - OfsetTime.prop(obj, 'bfu_anim_action_custom_end_frame') - if obj.bfu_anim_action_start_end_time_enum != "with_customframes": - OfsetTime = ActionTimeProperty.row() - OfsetTime.prop(obj, 'bfu_anim_action_start_frame_offset') - OfsetTime.prop(obj, 'bfu_anim_action_end_frame_offset') - - else: - layout.label( - text=( - "Note: animation start/end use scene frames" + - " with the camera for the sequencer.") - ) - - # Nomenclature - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - export_anim_naming = layout.column() - export_anim_naming.enabled = obj.bfu_anim_action_export_enum != 'dont_export' - export_anim_naming.prop(obj, 'bfu_anim_naming_type') - if obj.bfu_anim_naming_type == "include_custom_name": - export_anim_naming_text = export_anim_naming.column() - export_anim_naming_text.prop(obj, 'bfu_anim_naming_custom') - - - - else: - layout.label( - text='(This assets is not a SkeletalMesh or Camera)') - - scene.bfu_animation_action_advanced_properties_expanded.draw(layout) - if scene.bfu_animation_action_advanced_properties_expanded.is_expend(): - - if bfu_alembic_animation.bfu_alembic_animation_utils.is_not_alembic_animation(obj): - transformProp = layout.column() - transformProp.enabled = obj.bfu_anim_action_export_enum != 'dont_export' - transformProp.prop(obj, "bfu_move_action_to_center_for_export") - transformProp.prop(obj, "bfu_rotate_action_to_zero_for_export") - - scene.bfu_animation_nla_properties_expanded.draw(layout) - if scene.bfu_animation_nla_properties_expanded.is_expend(): - # NLA - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - NLAAnim = layout.row() - NLAAnim.prop(obj, 'bfu_anim_nla_use') - NLAAnimChild = NLAAnim.column() - NLAAnimChild.enabled = obj.bfu_anim_nla_use - NLAAnimChild.prop(obj, 'bfu_anim_nla_export_name') - if obj.bfu_skeleton_export_procedure == "auto-rig-pro": - NLAAnim.enabled = False - NLAAnimChild.enabled = False - - # NLA Time - if obj.type != "CAMERA" and obj.bfu_skeleton_export_procedure != "auto-rig-pro": - NLATimeProperty = layout.column() - NLATimeProperty.enabled = obj.bfu_anim_nla_use - NLATimeProperty.prop(obj, 'bfu_anim_nla_start_end_time_enum') - if obj.bfu_anim_nla_start_end_time_enum == "with_customframes": - OfsetTime = NLATimeProperty.row() - OfsetTime.prop(obj, 'bfu_anim_nla_custom_start_frame') - OfsetTime.prop(obj, 'bfu_anim_nla_custom_end_frame') - if obj.bfu_anim_nla_start_end_time_enum != "with_customframes": - OfsetTime = NLATimeProperty.row() - OfsetTime.prop(obj, 'bfu_anim_nla_start_frame_offset') - OfsetTime.prop(obj, 'bfu_anim_nla_end_frame_offset') - - - scene.bfu_animation_nla_advanced_properties_expanded.draw(layout) - if scene.bfu_animation_nla_advanced_properties_expanded.is_expend(): - if bfu_alembic_animation.bfu_alembic_animation_utils.is_not_alembic_animation(obj): - transformProp2 = layout.column() - transformProp2.enabled = obj.bfu_anim_nla_use - transformProp2.prop(obj, "bfu_move_nla_to_center_for_export") - transformProp2.prop(obj, "bfu_rotate_nla_to_zero_for_export") - - - scene.bfu_animation_advanced_properties_expanded.draw(layout) - if scene.bfu_animation_advanced_properties_expanded.is_expend(): - # Animation fbx properties - if bfu_alembic_animation.bfu_alembic_animation_utils.is_not_alembic_animation(obj): - propsFbx = layout.row() - if obj.bfu_skeleton_export_procedure != "auto-rig-pro": - propsFbx.prop(obj, 'bfu_sample_anim_for_export') - propsFbx.prop(obj, 'bfu_simplify_anim_for_export') - propsScaleAnimation = layout.row() - propsScaleAnimation.prop(obj, "bfu_disable_free_scale_animation") - - # Armature export action list feedback - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - layout.label( - text='Note: The Action with only one' + - ' frame are exported like Pose.') - ArmaturePropertyInfo = ( - layout.row().box().split(factor=0.75) - ) - animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) - animation_to_export = animation_asset_cache.GetAnimationAssetList() - ActionNum = len(animation_to_export) - if obj.bfu_anim_nla_use: - ActionNum += 1 - actionFeedback = ( - str(ActionNum) + - " Animation(s) will be exported with this object.") - ArmaturePropertyInfo.label( - text=actionFeedback, - icon='INFO') - ArmaturePropertyInfo.operator("object.showobjaction") - else: - layout.label(text='(No properties to show.)') - else: - layout.label(text='(No properties to show.)') - - if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "MISC"): - - scene.bfu_object_lod_properties_expanded.draw(layout) - if scene.bfu_object_lod_properties_expanded.is_expend(): - if addon_prefs.useGeneratedScripts and obj is not None: - if obj.bfu_export_type == "export_recursive": - - # Lod selection - if not obj.bfu_export_as_lod_mesh: - # Unreal python no longer support Skeletal mesh LODS import. - if (bfu_static_mesh.bfu_static_mesh_utils.is_static_mesh(obj)): - LodList = layout.column() - LodList.prop(obj, 'bfu_lod_target1') - LodList.prop(obj, 'bfu_lod_target2') - LodList.prop(obj, 'bfu_lod_target3') - LodList.prop(obj, 'bfu_lod_target4') - LodList.prop(obj, 'bfu_lod_target5') - - # StaticMesh prop - if bfu_static_mesh.bfu_static_mesh_utils.is_static_mesh(obj): - if not obj.bfu_export_as_lod_mesh: - bfu_static_mesh_lod_group = layout.row() - bfu_static_mesh_lod_group.prop( - obj, - 'bfu_use_static_mesh_lod_group', - text="") - SMLODGroupChild = bfu_static_mesh_lod_group.column() - SMLODGroupChild.enabled = obj.bfu_use_static_mesh_lod_group - SMLODGroupChild.prop( - obj, - 'bfu_static_mesh_lod_group' - ) - scene.bfu_object_collision_properties_expanded.draw(layout) - if scene.bfu_object_collision_properties_expanded.is_expend(): - if addon_prefs.useGeneratedScripts and obj is not None: - if obj.bfu_export_type == "export_recursive": - - # StaticMesh prop - if bfu_static_mesh.bfu_static_mesh_utils.is_static_mesh(obj): - if not obj.bfu_export_as_lod_mesh: - auto_generate_collision = layout.row() - auto_generate_collision.prop( - obj, - 'bfu_auto_generate_collision' - ) - collision_trace_flag = layout.row() - collision_trace_flag.prop( - obj, - 'bfu_collision_trace_flag' - ) - # SkeletalMesh prop - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - if not obj.bfu_export_as_lod_mesh: - create_physics_asset = layout.row() - create_physics_asset.prop(obj, "bfu_create_physics_asset") - enable_skeletal_mesh_per_poly_collision = layout.row() - enable_skeletal_mesh_per_poly_collision.prop(obj, 'bfu_enable_skeletal_mesh_per_poly_collision') - - - - scene.bfu_object_light_map_properties_expanded.draw(layout) - if scene.bfu_object_light_map_properties_expanded.is_expend(): - if addon_prefs.useGeneratedScripts and obj is not None: - if obj.bfu_export_type == "export_recursive": - - # Light map - if bfu_static_mesh.bfu_static_mesh_utils.is_static_mesh(obj): - StaticMeshLightMapRes = layout.box() - StaticMeshLightMapRes.prop(obj, 'bfu_static_mesh_light_map_mode') - if obj.bfu_static_mesh_light_map_mode == "CustomMap": - CustomLightMap = StaticMeshLightMapRes.column() - CustomLightMap.prop(obj, 'bfu_static_mesh_custom_light_map_res') - if obj.bfu_static_mesh_light_map_mode == "SurfaceArea": - SurfaceAreaLightMap = StaticMeshLightMapRes.column() - SurfaceAreaLightMapButton = SurfaceAreaLightMap.row() - SurfaceAreaLightMapButton.operator("object.computlightmap", icon='TEXTURE') - SurfaceAreaLightMapButton.operator("object.computalllightmap", icon='TEXTURE') - SurfaceAreaLightMap.prop(obj, 'bfu_use_static_mesh_light_map_world_scale') - SurfaceAreaLightMap.prop(obj, 'bfu_static_mesh_light_map_surface_scale') - SurfaceAreaLightMap.prop(obj, 'bfu_static_mesh_light_map_round_power_of_two') - if obj.bfu_static_mesh_light_map_mode != "Default": - CompuntedLightMap = str(bfu_light_map.bfu_light_map_utils.GetCompuntedLightMap(obj)) - StaticMeshLightMapRes.label(text='Compunted light map: ' + CompuntedLightMap) - bfu_generate_light_map_uvs = layout.row() - bfu_generate_light_map_uvs.prop(obj, 'bfu_generate_light_map_uvs') - - - scene.bfu_object_uv_map_properties_expanded.draw(layout) - if scene.bfu_object_uv_map_properties_expanded.is_expend(): - if obj.bfu_export_type == "export_recursive": - # Geometry Node Uv - bfu_convert_geometry_node_attribute_to_uv = layout.column() - convert_geometry_node_attribute_to_uv_use = bfu_convert_geometry_node_attribute_to_uv.row() - convert_geometry_node_attribute_to_uv_use.prop(obj, 'bfu_convert_geometry_node_attribute_to_uv') - bbpl.blender_layout.layout_doc_button.add_doc_page_operator(convert_geometry_node_attribute_to_uv_use, url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/UV-Maps#geometry-node-uv") - bfu_convert_geometry_node_attribute_to_uv_name = bfu_convert_geometry_node_attribute_to_uv.row() - bfu_convert_geometry_node_attribute_to_uv_name.prop(obj, 'bfu_convert_geometry_node_attribute_to_uv_name') - bfu_convert_geometry_node_attribute_to_uv_name.enabled = obj.bfu_convert_geometry_node_attribute_to_uv - - # Extreme UV Scale - bfu_correct_extrem_uv_scale = layout.row() - bfu_correct_extrem_uv_scale.prop(obj, 'bfu_correct_extrem_uv_scale') - bbpl.blender_layout.layout_doc_button.add_doc_page_operator(bfu_correct_extrem_uv_scale, url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/UV-Maps#extreme-uv-scale") - - if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("SCENE", "GENERAL"): - - scene.bfu_collection_properties_expanded.draw(layout) - if scene.bfu_collection_properties_expanded.is_expend(): - collectionListProperty = layout.column() - collectionListProperty.template_list( - # type and unique id - "BFU_UL_CollectionExportTarget", "", - # pointer to the CollectionProperty - scene, "bfu_collection_asset_list", - # pointer to the active identifier - scene, "bfu_active_collection_asset_list", - maxrows=5, - rows=5 - ) - collectionListProperty.operator( - "object.updatecollectionlist", - icon='RECOVER_LAST') - - if scene.bfu_active_collection_asset_list < len(scene.bfu_collection_asset_list): - col_name = scene.bfu_collection_asset_list[scene.bfu_active_collection_asset_list].name - if col_name in bpy.data.collections: - col = bpy.data.collections[col_name] - col_prop = layout - col_prop.prop(col, 'bfu_export_folder_name', icon='FILE_FOLDER') - bfu_export_procedure.bfu_export_procedure_ui.draw_collection_export_procedure(layout, col) - - collectionPropertyInfo = layout.row().box().split(factor=0.75) - collection_asset_cache = bfu_cached_asset_list.GetCollectionAssetCache() - collection_export_asset_list = collection_asset_cache.GetCollectionAssetList() - collectionNum = len(collection_export_asset_list) - collectionFeedback = ( - str(collectionNum) + - " Collection(s) will be exported.") - collectionPropertyInfo.label(text=collectionFeedback, icon='INFO') - collectionPropertyInfo.operator("object.showscenecollection") - layout.label(text='Note: The collection are exported like StaticMesh.') - - - -class BFU_OT_SceneCollectionExport(bpy.types.PropertyGroup): - name: bpy.props.StringProperty(name="collection data name", default="Unknown", override={'LIBRARY_OVERRIDABLE'}) - use: bpy.props.BoolProperty(name="export this collection", default=False, override={'LIBRARY_OVERRIDABLE'}) - -class BFU_OT_ObjExportAction(bpy.types.PropertyGroup): - name: bpy.props.StringProperty(name="Action data name", default="Unknown", override={'LIBRARY_OVERRIDABLE'}) - use: bpy.props.BoolProperty(name="use this action", default=False, override={'LIBRARY_OVERRIDABLE'}) - - - - -# ------------------------------------------------------------------- -# Register & Unregister -# ------------------------------------------------------------------- - -classes = ( - BFU_PT_BlenderForUnrealObject, - BFU_PT_BlenderForUnrealObject.BFU_MT_ObjectGlobalPropertiesPresets, - BFU_PT_BlenderForUnrealObject.BFU_OT_AddObjectGlobalPropertiesPreset, - BFU_PT_BlenderForUnrealObject.BFU_OT_OpenDocumentationPage, - BFU_PT_BlenderForUnrealObject.BFU_UL_ActionExportTarget, - BFU_PT_BlenderForUnrealObject.BFU_OT_UpdateObjActionListButton, - BFU_PT_BlenderForUnrealObject.BFU_OT_ShowActionToExport, - BFU_PT_BlenderForUnrealObject.BFU_UL_CollectionExportTarget, - BFU_PT_BlenderForUnrealObject.BFU_OT_UpdateCollectionButton, - BFU_PT_BlenderForUnrealObject.BFU_OT_ShowCollectionToExport, - BFU_OT_SceneCollectionExport, - BFU_OT_ObjExportAction, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - bpy.types.Scene.bfu_collection_asset_list = bpy.props.CollectionProperty( - type=BFU_OT_SceneCollectionExport, - options={'LIBRARY_EDITABLE'}, - override={'LIBRARY_OVERRIDABLE', 'USE_INSERTION'}, - ) - - bpy.types.Scene.bfu_active_collection_asset_list = bpy.props.IntProperty( - name="Active Collection", - description="Index of the currently active collection", - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_animation_asset_list = bpy.props.CollectionProperty( - type=BFU_OT_ObjExportAction, - options={'LIBRARY_EDITABLE'}, - override={'LIBRARY_OVERRIDABLE', 'USE_INSERTION'}, - ) - - bpy.types.Object.bfu_active_animation_asset_list = bpy.props.IntProperty( - name="Active Scene Action", - description="Index of the currently active object action", - override={'LIBRARY_OVERRIDABLE'}, - default=0 - ) - - bpy.types.Object.bfu_export_with_meta_data = bpy.props.BoolProperty( - name=(languages.ti('export_with_meta_data_name')), - description=(languages.tt('export_with_meta_data_desc')), - override={'LIBRARY_OVERRIDABLE'}, - default=False, - ) - - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_panel_export.py b/blender-for-unrealengine/bfu_addon_parts/bfu_panel_export.py new file mode 100644 index 00000000..505fb970 --- /dev/null +++ b/blender-for-unrealengine/bfu_addon_parts/bfu_panel_export.py @@ -0,0 +1,103 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import os +import bpy +from .. import bfu_export_nomenclature +from .. import bfu_export_filter +from .. import bfu_export_process + +class BFU_PT_Export(bpy.types.Panel): + # Is Export panel + + bl_idname = "BFU_PT_Export" + bl_label = "UE AE Export" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Unreal Engine" + + + class BFU_MT_NomenclaturePresets(bpy.types.Menu): + bl_label = 'Nomenclature Presets' + preset_subdir = 'blender-for-unrealengine/nomenclature-presets' + preset_operator = 'script.execute_preset' + draw = bpy.types.Menu.draw_preset + + from bl_operators.presets import AddPresetBase + + class BFU_OT_AddNomenclaturePreset(AddPresetBase, bpy.types.Operator): + bl_idname = 'object.add_nomenclature_preset' + bl_label = 'Add or remove a preset for Nomenclature' + bl_description = 'Add or remove a preset for Nomenclature' + preset_menu = 'BFU_MT_NomenclaturePresets' + + def get_export_global_preset_propertys(): + preset_values = [] + preset_values += bfu_export_nomenclature.bfu_export_nomenclature_props.get_preset_values() + preset_values += bfu_export_filter.bfu_export_filter_props.get_preset_values() + preset_values += bfu_export_process.bfu_export_process_props.get_preset_values() + return preset_values + + # Common variable used for all preset values + preset_defines = [ + 'obj = bpy.context.object', + 'scene = bpy.context.scene' + ] + + # Properties to store in the preset + preset_values = get_export_global_preset_propertys() + + # Directory to store the presets + preset_subdir = 'blender-for-unrealengine/nomenclature-presets' + + + def draw(self, context: bpy.types.Context): + + layout = self.layout + + # Presets + row = layout.row(align=True) + row.menu('BFU_MT_NomenclaturePresets', text='Export Presets') + row.operator('object.add_nomenclature_preset', text='', icon='ADD') + row.operator('object.add_nomenclature_preset', text='', icon='REMOVE').remove_active = True + + # Export sections + bfu_export_nomenclature.bfu_export_nomenclature_ui.draw_ui(layout, context) + bfu_export_filter.bfu_export_filter_ui.draw_ui(layout, context) + bfu_export_process.bfu_export_process_ui.draw_ui(layout, context) + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_PT_Export, + BFU_PT_Export.BFU_MT_NomenclaturePresets, + BFU_PT_Export.BFU_OT_AddNomenclaturePreset, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_panel_object.py b/blender-for-unrealengine/bfu_addon_parts/bfu_panel_object.py new file mode 100644 index 00000000..cca99b16 --- /dev/null +++ b/blender-for-unrealengine/bfu_addon_parts/bfu_panel_object.py @@ -0,0 +1,196 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import os +import bpy +from .. import bbpl +from .. import bfu_basics +from .. import languages +from .. import bfu_custom_property +from .. import bfu_base_object +from .. import bfu_adv_object +from .. import bfu_base_collection +from .. import bfu_material +from .. import bfu_camera +from .. import bfu_spline +from .. import bfu_vertex_color +from .. import bfu_static_mesh +from .. import bfu_skeletal_mesh +from .. import bfu_modular_skeletal_mesh +from .. import bfu_lod +from .. import bfu_alembic_animation +from .. import bfu_anim_base +from .. import bfu_anim_action +from .. import bfu_anim_action_adv +from .. import bfu_anim_nla +from .. import bfu_anim_nla_adv +from .. import bfu_groom +from .. import bfu_uv_map +from .. import bfu_light_map +from .. import bfu_assets_references +from .. import bfu_collision + +class BFU_PT_BlenderForUnrealObject(bpy.types.Panel): + # Unreal engine export panel + + bl_idname = "BFU_PT_BlenderForUnrealObject" + bl_label = "Unreal Engine Assets Exporter" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Unreal Engine" + + + class BFU_MT_ObjectGlobalPropertiesPresets(bpy.types.Menu): + bl_label = 'Global Properties Presets' + preset_subdir = 'blender-for-unrealengine/global-properties-presets' + preset_operator = 'script.execute_preset' + draw = bpy.types.Menu.draw_preset + + from bl_operators.presets import AddPresetBase + + class BFU_OT_AddObjectGlobalPropertiesPreset(AddPresetBase, bpy.types.Operator): + bl_idname = 'object.add_globalproperties_preset' + bl_label = 'Add or remove a preset for Global properties' + bl_description = 'Add or remove a preset for Global properties' + preset_menu = 'BFU_MT_ObjectGlobalPropertiesPresets' + + def get_object_global_preset_propertys(): + preset_values = [] + preset_values += bfu_base_object.bfu_base_obj_props.get_preset_values() + preset_values += bfu_adv_object.bfu_adv_obj_props.get_preset_values() + preset_values += bfu_base_collection.bfu_base_col_props.get_preset_values() + preset_values += bfu_modular_skeletal_mesh.bfu_modular_skeletal_mesh_props.get_preset_values() + preset_values += bfu_custom_property.bfu_custom_property_props.get_preset_values() + preset_values += bfu_material.bfu_material_props.get_preset_values() + preset_values += bfu_camera.bfu_camera_ui_and_props.get_preset_values() + preset_values += bfu_spline.bfu_spline_ui_and_props.get_preset_values() + preset_values += bfu_static_mesh.bfu_static_mesh_props.get_preset_values() + preset_values += bfu_skeletal_mesh.bfu_skeletal_mesh_props.get_preset_values() + preset_values += bfu_alembic_animation.bfu_alembic_animation_props.get_preset_values() + preset_values += bfu_anim_base.bfu_anim_base_props.get_preset_values() + preset_values += bfu_anim_action.bfu_anim_action_props.get_preset_values() + preset_values += bfu_anim_action_adv.bfu_anim_action_adv_props.get_preset_values() + preset_values += bfu_anim_nla.bfu_anim_nla_props.get_preset_values() + preset_values += bfu_anim_nla_adv.bfu_anim_nla_adv_props.get_preset_values() + preset_values += bfu_vertex_color.bfu_vertex_color_props.get_preset_values() + preset_values += bfu_lod.bfu_lod_props.get_preset_values() + preset_values += bfu_uv_map.bfu_uv_map_props.get_preset_values() + preset_values += bfu_light_map.bfu_light_map_props.get_preset_values() + preset_values += bfu_assets_references.bfu_asset_ref_props.get_preset_values() + preset_values += bfu_collision.bfu_collision_props.get_preset_values() + return preset_values + + # Common variable used for all preset values + preset_defines = [ + 'obj = bpy.context.object', + 'col = bpy.context.collection', + 'scene = bpy.context.scene' + ] + + # Properties to store in the preset + preset_values = get_object_global_preset_propertys() + + # Directory to store the presets + preset_subdir = 'blender-for-unrealengine/global-properties-presets' + + + def draw(self, context: bpy.types.Context): + + scene = bpy.context.scene + obj = bpy.context.object + addon_prefs = bfu_basics.GetAddonPrefs() + layout = self.layout + + # Extension details + if bpy.app.version >= (4, 2, 0): + version_str = 'Version '+ str(bbpl.blender_extension.extension_utils.get_package_version()) + else: + version_str = 'Version '+ bbpl.blender_addon.addon_utils.get_addon_version_str("Unreal Engine Assets Exporter") + + credit_box = layout.box() + credit_box.label(text=languages.ti('intro')) + credit_box.label(text=version_str) + bbpl.blender_layout.layout_doc_button.functions.add_doc_page_operator( + layout = layout, + url = "https://github.com/xavier150/Blender-For-UnrealEngine-Addons", + text = "Open Github page", + icon="HELP" + ) + + # Presets + row = layout.row(align=True) + row.menu('BFU_MT_ObjectGlobalPropertiesPresets', text='Global Properties Presets') + row.operator('object.add_globalproperties_preset', text='', icon='ADD') + row.operator('object.add_globalproperties_preset', text='', icon='REMOVE').remove_active = True + + # Tab Buttions + layout.row().prop(scene, "bfu_active_tab", expand=True) + if scene.bfu_active_tab == "OBJECT": + layout.row().prop(scene, "bfu_active_object_tab", expand=True) + if scene.bfu_active_tab == "SCENE": + layout.row().prop(scene, "bfu_active_scene_tab", expand=True) + + # Object + bfu_base_object.bfu_base_obj_ui.draw_ui(layout, obj) + bfu_adv_object.bfu_adv_obj_ui.draw_ui(layout, obj) + bfu_static_mesh.bfu_static_mesh_ui.draw_ui_object(layout, obj) + bfu_skeletal_mesh.bfu_skeletal_mesh_ui.draw_ui_object(layout, obj) + bfu_modular_skeletal_mesh.bfu_modular_skeletal_mesh_ui.draw_ui_object(layout, obj) + bfu_alembic_animation.bfu_alembic_animation_ui.draw_ui_object(layout, obj) + bfu_groom.bfu_groom_ui.draw_ui_object(layout, obj) + bfu_camera.bfu_camera_ui_and_props.draw_ui_object_camera(layout, obj) + bfu_spline.bfu_spline_ui_and_props.draw_ui_object_spline(layout, obj) + bfu_lod.bfu_lod_ui.draw_ui(layout, obj) + bfu_collision.bfu_collision_ui.draw_ui_object(layout, obj) + bfu_uv_map.bfu_uv_map_ui.draw_obj_ui(layout, obj) + bfu_light_map.bfu_light_map_ui.draw_obj_ui(layout, obj) + bfu_material.bfu_material_ui.draw_ui_object(layout) + bfu_vertex_color.bfu_vertex_color_ui.draw_ui_object(layout) + bfu_assets_references.bfu_asset_ref_ui.draw_ui(layout, obj) + + # Animations + bfu_anim_action.bfu_anim_action_ui.draw_ui(layout, obj) + bfu_anim_action_adv.bfu_anim_action_adv_ui.draw_ui(layout, obj) + bfu_anim_nla.bfu_anim_nla_ui.draw_ui(layout, obj) + bfu_anim_nla_adv.bfu_anim_nla_adv_ui.draw_ui(layout, obj) + bfu_anim_base.bfu_anim_base_ui.draw_ui(layout, obj) + bfu_anim_base.bfu_anim_base_ui.draw_animation_tab_foot_ui(layout, obj) + + # Scene + bfu_base_collection.bfu_base_col_ui.draw_ui(layout, context) + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_PT_BlenderForUnrealObject, + BFU_PT_BlenderForUnrealObject.BFU_MT_ObjectGlobalPropertiesPresets, + BFU_PT_BlenderForUnrealObject.BFU_OT_AddObjectGlobalPropertiesPreset, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_panel_tools.py b/blender-for-unrealengine/bfu_addon_parts/bfu_panel_tools.py new file mode 100644 index 00000000..74a5638d --- /dev/null +++ b/blender-for-unrealengine/bfu_addon_parts/bfu_panel_tools.py @@ -0,0 +1,70 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_camera +from .. import bfu_spline +from .. import bfu_collision +from .. import bfu_socket +from .. import bfu_uv_map +from .. import bfu_light_map + +class BFU_PT_BlenderForUnrealTool(bpy.types.Panel): + # Tool panel + + bl_idname = "BFU_PT_BlenderForUnrealTool" + bl_label = "UE AE Tools" + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Unreal Engine" + + def draw(self, context: bpy.types.Context): + + layout = self.layout + + # Tools sections + bfu_camera.bfu_camera_ui_and_props.draw_tools_ui(layout, context) + bfu_spline.bfu_spline_ui_and_props.draw_tools_ui(layout, context) + + bfu_collision.bfu_collision_ui.draw_tools_ui(layout, context) + bfu_socket.bfu_socket_ui_and_props.draw_tools_ui(layout, context) + + bfu_uv_map.bfu_uv_map_ui.draw_tools_ui(layout, context) + bfu_light_map.bfu_light_map_ui.draw_tools_ui(layout, context) + + +'' + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_PT_BlenderForUnrealTool, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_tool_ui_and_props.py b/blender-for-unrealengine/bfu_addon_parts/bfu_tool_ui_and_props.py deleted file mode 100644 index c8b4f314..00000000 --- a/blender-for-unrealengine/bfu_addon_parts/bfu_tool_ui_and_props.py +++ /dev/null @@ -1,85 +0,0 @@ -import bpy -from .. import bfu_basics -from .. import bfu_utils -from .. import bfu_ui -from .. import bfu_camera -from .. import bfu_spline -from .. import bfu_collision -from .. import bfu_socket -from .. import bbpl - - -class BFU_PT_BlenderForUnrealTool(bpy.types.Panel): - # Tool panel - - bl_idname = "BFU_PT_BlenderForUnrealTool" - bl_label = "BFU Tool" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "Unreal Engine" - - - - - - - - - def draw(self, context): - - - layout = self.layout - scene = bpy.context.scene - - - - - - bfu_camera.bfu_camera_ui_and_props.draw_ui_scene_camera(layout) - bfu_spline.bfu_spline_ui_and_props.draw_ui_scene_spline(layout) - - bfu_collision.bfu_collision_ui_and_props.draw_ui_scene_collision(layout) - bfu_socket.bfu_socket_ui_and_props.draw_ui_scene_socket(layout) - - scene.bfu_uvmap_expanded.draw(layout) - if scene.bfu_uvmap_expanded.is_expend(): - ready_for_correct_extrem_uv_scale = False - obj = bpy.context.object - if obj and obj.type == "MESH": - if bbpl.utils.active_mode_is("EDIT"): - ready_for_correct_extrem_uv_scale = True - else: - layout.label(text="Switch to Edit Mode.", icon='INFO') - else: - layout.label(text="Select an mesh object", icon='INFO') - - - # Draw buttons (correct_extrem_uv) - Buttons_correct_extrem_uv_scale = layout.row() - Button_correct_extrem_uv_scale = Buttons_correct_extrem_uv_scale.column() - Button_correct_extrem_uv_scale.enabled = ready_for_correct_extrem_uv_scale - Button_correct_extrem_uv_scale.operator("object.correct_extrem_uv", icon='UV') - bbpl.blender_layout.layout_doc_button.add_doc_page_operator(Buttons_correct_extrem_uv_scale, url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/UV-Maps#extreme-uv-scale") - - scene.bfu_lightmap_expanded.draw(layout) - if scene.bfu_lightmap_expanded.is_expend(): - checkButton = layout.column() - checkButton.operator("object.computalllightmap", icon='TEXTURE') - -# ------------------------------------------------------------------- -# Register & Unregister -# ------------------------------------------------------------------- - -classes = ( - BFU_PT_BlenderForUnrealTool, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_addon_pref.py b/blender-for-unrealengine/bfu_addon_pref.py index 3537bb5e..ed9c23c4 100644 --- a/blender-for-unrealengine/bfu_addon_pref.py +++ b/blender-for-unrealengine/bfu_addon_pref.py @@ -184,7 +184,7 @@ def execute(self, context): ) return {'FINISHED'} - def draw(self, context): + def draw(self, context: bpy.types.Context): layout: bpy.types.UILayout = self.layout boxColumn = layout.column().split( diff --git a/blender-for-unrealengine/bfu_adv_object/__init__.py b/blender-for-unrealengine/bfu_adv_object/__init__.py new file mode 100644 index 00000000..4781bf01 --- /dev/null +++ b/blender-for-unrealengine/bfu_adv_object/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_adv_obj_props +from . import bfu_adv_obj_ui +from . import bfu_adv_obj_utils + +if "bfu_adv_obj_props" in locals(): + importlib.reload(bfu_adv_obj_props) +if "bfu_adv_obj_ui" in locals(): + importlib.reload(bfu_adv_obj_ui) +if "bfu_adv_obj_utils" in locals(): + importlib.reload(bfu_adv_obj_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_adv_obj_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_adv_obj_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_props.py b/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_props.py new file mode 100644 index 00000000..ff9a3a28 --- /dev/null +++ b/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_props.py @@ -0,0 +1,210 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import languages + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_move_to_center_for_export', + 'obj.bfu_rotate_to_zero_for_export', + 'obj.bfu_additional_location_for_export', + 'obj.bfu_additional_rotation_for_export', + 'obj.bfu_export_global_scale', + 'obj.bfu_override_procedure_preset', + 'obj.bfu_export_use_space_transform', + 'obj.bfu_export_axis_forward', + 'obj.bfu_export_axis_up', + 'obj.bfu_export_primary_bone_axis', + 'obj.bfu_export_secondary_bone_axis', + 'obj.bfu_export_with_meta_data', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_object_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Object Advanced Properties") + + bpy.types.Object.bfu_move_to_center_for_export = bpy.props.BoolProperty( + name="Move to center", + description=( + "If true use object origin else use scene origin." + + " | If true the mesh will be moved to the center" + + " of the scene for export." + + " (This is used so that the origin of the fbx file" + + " is the same as the mesh in blender)" + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True + ) + + bpy.types.Object.bfu_rotate_to_zero_for_export = bpy.props.BoolProperty( + name="Rotate to zero", + description=( + "If true use object rotation else use scene rotation." + + " | If true the mesh will use zero rotation for export." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + + bpy.types.Object.bfu_additional_location_for_export = bpy.props.FloatVectorProperty( + name="Additional location", + description=( + "This will add a additional absolute location to the mesh" + ), + override={'LIBRARY_OVERRIDABLE'}, + subtype="TRANSLATION", + default=(0, 0, 0) + ) + + bpy.types.Object.bfu_additional_rotation_for_export = bpy.props.FloatVectorProperty( + name="Additional rotation", + description=( + "This will add a additional absolute rotation to the mesh" + ), + override={'LIBRARY_OVERRIDABLE'}, + subtype="EULER", + default=(0, 0, 0) + ) + + bpy.types.Object.bfu_export_global_scale = bpy.props.FloatProperty( + name="Global scale", + description="Scale, change is not recommended with SkeletalMesh.", + override={'LIBRARY_OVERRIDABLE'}, + default=1.0 + ) + + bpy.types.Object.bfu_override_procedure_preset = bpy.props.BoolProperty( + name="Override Export Preset", + description="If true override the export precedure preset.", + override={'LIBRARY_OVERRIDABLE'}, + default=False, + ) + + bpy.types.Object.bfu_export_use_space_transform = bpy.props.BoolProperty( + name="Use Space Transform", + default=True, + ) + + bpy.types.Object.bfu_export_axis_forward = bpy.props.EnumProperty( + name="Axis Forward", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ('X', "X Forward", ""), + ('Y', "Y Forward", ""), + ('Z', "Z Forward", ""), + ('-X', "-X Forward", ""), + ('-Y', "-Y Forward", ""), + ('-Z', "-Z Forward", ""), + ], + default='-Z', + ) + + bpy.types.Object.bfu_export_axis_up = bpy.props.EnumProperty( + name="Axis Up", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ('X', "X Up", ""), + ('Y', "Y Up", ""), + ('Z', "Z Up", ""), + ('-X', "-X Up", ""), + ('-Y', "-Y Up", ""), + ('-Z', "-Z Up", ""), + ], + default='Y', + ) + + bpy.types.Object.bfu_export_primary_bone_axis = bpy.props.EnumProperty( + name="Primary Axis Bone", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ('X', "X", ""), + ('Y', "Y", ""), + ('Z', "Z", ""), + ('-X', "-X", ""), + ('-Y', "-Y", ""), + ('-Z', "-Z", ""), + ], + default='Y', + ) + + bpy.types.Object.bfu_export_secondary_bone_axis = bpy.props.EnumProperty( + name="Secondary Axis Bone", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ('X', "X", ""), + ('Y', "Y", ""), + ('Z', "Z", ""), + ('-X', "-X", ""), + ('-Y', "-Y", ""), + ('-Z', "-Z", ""), + ], + default='X', + ) + + bpy.types.Object.bfu_export_with_meta_data = bpy.props.BoolProperty( + name=(languages.ti('export_with_meta_data_name')), + description=(languages.tt('export_with_meta_data_desc')), + override={'LIBRARY_OVERRIDABLE'}, + default=False, + ) + + + + + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_export_with_meta_data + + del bpy.types.Object.bfu_export_secondary_bone_axis + del bpy.types.Object.bfu_export_primary_bone_axis + del bpy.types.Object.bfu_export_axis_up + del bpy.types.Object.bfu_export_axis_forward + del bpy.types.Object.bfu_export_use_space_transform + del bpy.types.Object.bfu_override_procedure_preset + + del bpy.types.Object.bfu_export_global_scale + del bpy.types.Object.bfu_additional_rotation_for_export + del bpy.types.Object.bfu_additional_location_for_export + del bpy.types.Object.bfu_rotate_to_zero_for_export + del bpy.types.Object.bfu_move_to_center_for_export + + del bpy.types.Scene.bfu_object_advanced_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_ui.py b/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_ui.py new file mode 100644 index 00000000..a4e9ad04 --- /dev/null +++ b/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_ui.py @@ -0,0 +1,89 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bpl +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_alembic_animation +from .. import bfu_camera +from .. import bfu_skeletal_mesh +from .. import bfu_export_procedure +from .. import bfu_custom_property + + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + if obj is None: + return + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + scene.bfu_object_advanced_properties_expanded.draw(layout) + if scene.bfu_object_advanced_properties_expanded.is_expend(): + transformProp = layout.column() + is_not_alembic_animation = not bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj) + is_not_camera = not bfu_camera.bfu_camera_utils.is_camera(obj) + if is_not_alembic_animation and is_not_camera: + transformProp.prop(obj, "bfu_move_to_center_for_export") + transformProp.prop(obj, "bfu_rotate_to_zero_for_export") + transformProp.prop(obj, "bfu_additional_location_for_export") + transformProp.prop(obj, "bfu_additional_rotation_for_export") + + transformProp.prop(obj, 'bfu_export_global_scale') + if bfu_camera.bfu_camera_utils.is_camera(obj): + transformProp.prop(obj, "bfu_additional_location_for_export") + + AxisProperty = layout.column() + + AxisProperty.prop(obj, 'bfu_override_procedure_preset') + if obj.bfu_override_procedure_preset: + AxisProperty.prop(obj, 'bfu_export_use_space_transform') + AxisProperty.prop(obj, 'bfu_export_axis_forward') + AxisProperty.prop(obj, 'bfu_export_axis_up') + bbpl.blender_layout.layout_doc_button.add_doc_page_operator(AxisProperty, text="About axis Transforms", url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Axis-Transforms") + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + BoneAxisProperty = layout.column() + BoneAxisProperty.prop(obj, 'bfu_export_primary_bone_axis') + BoneAxisProperty.prop(obj, 'bfu_export_secondary_bone_axis') + else: + box = layout.box() + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + preset = bfu_export_procedure.bfu_skeleton_export_procedure.get_obj_skeleton_procedure_preset(obj) + else: + preset = bfu_export_procedure.bfu_static_export_procedure.get_obj_static_procedure_preset(obj) + var_lines = box.column() + for key, value in preset.items(): + display_key = bpl.utils.format_property_name(key) + var_lines.label(text=f"{display_key}: {value}\n") + export_data = layout.column() + bfu_custom_property.bfu_custom_property_utils.draw_ui_custom_property(export_data, obj) + export_data.prop(obj, "bfu_export_with_meta_data") \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_utils.py b/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_adv_object/bfu_adv_obj_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_alembic_animation/bfu_alembic_animation_type.py b/blender-for-unrealengine/bfu_alembic_animation/bfu_alembic_animation_type.py index 175a35bf..72aa9800 100644 --- a/blender-for-unrealengine/bfu_alembic_animation/bfu_alembic_animation_type.py +++ b/blender-for-unrealengine/bfu_alembic_animation/bfu_alembic_animation_type.py @@ -30,7 +30,7 @@ def __init__(self): super().__init__() self.use_materials = True - def support_asset_type(self, obj): + def support_asset_type(self, obj, details = None): if obj.bfu_export_as_alembic_animation: return True return False @@ -48,18 +48,24 @@ def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): return bfu_basics.ValidFilename(scene.bfu_alembic_animation_prefix_export_name+desired_name+fileType) return bfu_basics.ValidFilename(scene.bfu_alembic_animation_prefix_export_name+obj.name+fileType) - def get_obj_export_directory_path(self, obj): + def get_obj_export_directory_path(self, obj, absolute = True): folder_name = bfu_utils.get_export_folder_name(obj) scene = bpy.context.scene + if(absolute): + root_path = bpy.path.abspath(scene.bfu_export_alembic_file_path) + else: + root_path = scene.bfu_export_alembic_file_path + + if obj.bfu_create_sub_folder_with_alembic_name: - dirpath = os.path.join(scene.bfu_export_alembic_file_path, folder_name, self.get_asset_type_name(obj)) + dirpath = os.path.join(root_path, folder_name, self.get_asset_type_name(obj)) else: - dirpath = os.path.join(scene.bfu_export_alembic_file_path, folder_name) + dirpath = os.path.join(root_path, folder_name) return dirpath def can_export_asset(self): scene = bpy.context.scene - return scene.alembic_export + return scene.bfu_use_alembic_export def can_export_obj_asset(self, obj): return self.can_export_asset() diff --git a/blender-for-unrealengine/bfu_anim_action/__init__.py b/blender-for-unrealengine/bfu_anim_action/__init__.py new file mode 100644 index 00000000..02866fae --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_anim_action_props +from . import bfu_anim_action_ui +from . import bfu_anim_action_utils + +if "bfu_anim_action_props" in locals(): + importlib.reload(bfu_anim_action_props) +if "bfu_anim_action_ui" in locals(): + importlib.reload(bfu_anim_action_ui) +if "bfu_anim_action_utils" in locals(): + importlib.reload(bfu_anim_action_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_anim_action_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_anim_action_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_props.py b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_props.py new file mode 100644 index 00000000..82c04bf6 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_props.py @@ -0,0 +1,277 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_anim_action_export_enum', + 'obj.bfu_prefix_name_to_export', + 'obj.bfu_anim_action_start_end_time_enum', + 'obj.bfu_anim_action_start_frame_offset', + 'obj.bfu_anim_action_end_frame_offset', + 'obj.bfu_anim_action_custom_start_frame', + 'obj.bfu_anim_action_custom_end_frame', + 'obj.bfu_anim_naming_type', + 'obj.bfu_anim_naming_custom', + ] + return preset_values + +class BFU_UL_ActionExportTarget(bpy.types.UIList): + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index): + action_is_valid = False + if item.name in bpy.data.actions: + action_is_valid = True + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + if action_is_valid: # If action is valid + layout.prop( + bpy.data.actions[item.name], + "name", + text="", + emboss=False, + icon="ACTION" + ) + layout.prop(item, "use", text="") + else: + dataText = ( + 'Action data named "' + item.name + + '" Not Found. Please click on update' + ) + layout.label(text=dataText, icon="ERROR") + # Not optimized for 'GRID' layout type. + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) + +class BFU_OT_UpdateObjActionListButton(bpy.types.Operator): + bl_label = "Update action list" + bl_idname = "object.updateobjactionlist" + bl_description = "Update action list" + + def execute(self, context): + def UpdateExportActionList(obj): + # Update the provisional action list known by the object + + def SetUseFromLast(anim_list, ActionName): + for item in anim_list: + if item[0] == ActionName: + if item[1]: + return True + return False + + animSave = [["", False]] + for Anim in obj.bfu_action_asset_list: # CollectionProperty + name = Anim.name + use = Anim.use + animSave.append([name, use]) + obj.bfu_action_asset_list.clear() + for action in bpy.data.actions: + obj.bfu_action_asset_list.add().name = action.name + useFromLast = SetUseFromLast(animSave, action.name) + obj.bfu_action_asset_list[action.name].use = useFromLast + UpdateExportActionList(bpy.context.object) + return {'FINISHED'} + +class BFU_OT_ObjExportAction(bpy.types.PropertyGroup): + name: bpy.props.StringProperty(name="Action data name", default="Unknown", override={'LIBRARY_OVERRIDABLE'}) + use: bpy.props.BoolProperty(name="use this action", default=False, override={'LIBRARY_OVERRIDABLE'}) + + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_UL_ActionExportTarget, + BFU_OT_UpdateObjActionListButton, + BFU_OT_ObjExportAction, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_animation_action_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Actions Properties") + + bpy.types.Object.bfu_action_asset_list = bpy.props.CollectionProperty( + type=BFU_OT_ObjExportAction, + options={'LIBRARY_EDITABLE'}, + override={'LIBRARY_OVERRIDABLE', 'USE_INSERTION'}, + ) + + bpy.types.Object.bfu_active_action_asset_list = bpy.props.IntProperty( + name="Active Scene Action", + description="Index of the currently active object action", + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + bpy.types.Object.bfu_anim_action_export_enum = bpy.props.EnumProperty( + name="Action to export", + description="Export procedure for actions (Animations and poses)", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ("export_auto", + "Export auto", + "Export all actions connected to the bones names", + "FILE_SCRIPT", + 1), + ("export_specific_list", + "Export specific list", + "Export only actions that are checked in the list", + "LINENUMBERS_ON", + 3), + ("export_specific_prefix", + "Export specific prefix", + "Export only actions with a specific prefix" + + " or the beginning of the actions names", + "SYNTAX_ON", + 4), + ("dont_export", + "Not exported", + "No action will be exported", + "MATPLANE", + 5), + ("export_current", + "Export Current", + "Export only the current actions", + "FILE_SCRIPT", + 6), + ] + ) + + bpy.types.Object.bfu_prefix_name_to_export = bpy.props.StringProperty( + # properties used with ""export_specific_prefix" on bfu_anim_action_export_enum + name="Prefix name", + description="Indicate the prefix of the actions that must be exported", + override={'LIBRARY_OVERRIDABLE'}, + maxlen=32, + default="Example_", + ) + + bpy.types.Object.bfu_anim_action_start_end_time_enum = bpy.props.EnumProperty( + name="Action Start/End Time", + description="Set when animation starts and end", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ("with_keyframes", + "Auto", + "The time will be defined according" + + " to the first and the last frame", + "KEYTYPE_KEYFRAME_VEC", + 1), + ("with_sceneframes", + "Scene time", + "Time will be equal to the scene time", + "SCENE_DATA", + 2), + ("with_customframes", + "Custom time", + 'The time of all the animations of this object' + + ' is defined by you.' + + ' Use "bfu_anim_action_custom_start_frame" and "bfu_anim_action_custom_end_frame"', + "HAND", + 3), + ] + ) + + bpy.types.Object.bfu_anim_action_start_frame_offset = bpy.props.IntProperty( + name="Offset at start frame", + description="Offset for the start frame.", + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + bpy.types.Object.bfu_anim_action_end_frame_offset = bpy.props.IntProperty( + name="Offset at end frame", + description=( + "Offset for the end frame. +1" + + " is recommended for the sequences | 0 is recommended" + + " for UnrealEngine cycles | -1 is recommended for Sketchfab cycles" + ), + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + + bpy.types.Object.bfu_anim_action_custom_start_frame = bpy.props.IntProperty( + name="Custom start time", + description="Set when animation start", + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + bpy.types.Object.bfu_anim_action_custom_end_frame = bpy.props.IntProperty( + name="Custom end time", + description="Set when animation end", + override={'LIBRARY_OVERRIDABLE'}, + default=1 + ) + + + bpy.types.Object.bfu_anim_naming_type = bpy.props.EnumProperty( + name="Naming type", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ('action_name', "Action name", 'Exemple: "Anim_MyAction"'), + ('include_armature_name', + "Include Armature Name", + 'Include armature name in animation export file name.' + + ' Exemple: "Anim_MyArmature_MyAction"'), + ('include_custom_name', + "Include custom name", + 'Include custom name in animation export file name.' + + ' Exemple: "Anim_MyCustomName_MyAction"'), + ], + default='action_name' + ) + + bpy.types.Object.bfu_anim_naming_custom = bpy.props.StringProperty( + name="Export name", + override={'LIBRARY_OVERRIDABLE'}, + default='MyCustomName' + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_anim_naming_custom + del bpy.types.Object.bfu_anim_naming_type + + del bpy.types.Object.bfu_anim_action_custom_end_frame + del bpy.types.Object.bfu_anim_action_custom_start_frame + del bpy.types.Object.bfu_anim_action_end_frame_offset + del bpy.types.Object.bfu_anim_action_start_frame_offset + del bpy.types.Object.bfu_anim_action_start_end_time_enum + + del bpy.types.Object.bfu_prefix_name_to_export + del bpy.types.Object.bfu_anim_action_export_enum + del bpy.types.Object.bfu_active_action_asset_list + del bpy.types.Object.bfu_action_asset_list + del bpy.types.Scene.bfu_animation_action_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_ui.py b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_ui.py new file mode 100644 index 00000000..621f63a3 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_ui.py @@ -0,0 +1,101 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_skeletal_mesh +from .. import bfu_alembic_animation +from .. import bfu_camera + + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + is_skeletal_mesh = bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj) + is_camera = bfu_camera.bfu_camera_utils.is_camera(obj) + is_alembic_animation = bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj) + + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + if True not in [is_skeletal_mesh, is_camera, is_alembic_animation]: + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): + scene.bfu_animation_action_properties_expanded.draw(layout) + if scene.bfu_animation_action_properties_expanded.is_expend(): + if is_skeletal_mesh: + # Action list + ActionListProperty = layout.column() + ActionListProperty.prop(obj, 'bfu_anim_action_export_enum') + if obj.bfu_anim_action_export_enum == "export_specific_list": + ActionListProperty.template_list( + # type and unique id + "BFU_UL_ActionExportTarget", "", + # pointer to the CollectionProperty + obj, "bfu_action_asset_list", + # pointer to the active identifier + obj, "bfu_active_action_asset_list", + maxrows=5, + rows=5 + ) + ActionListProperty.operator( + "object.updateobjactionlist", + icon='RECOVER_LAST') + if obj.bfu_anim_action_export_enum == "export_specific_prefix": + ActionListProperty.prop(obj, 'bfu_prefix_name_to_export') + + # Action Time + if obj.type != "CAMERA" and obj.bfu_skeleton_export_procedure != "auto-rig-pro": + ActionTimeProperty = layout.column() + ActionTimeProperty.enabled = obj.bfu_anim_action_export_enum != 'dont_export' + ActionTimeProperty.prop(obj, 'bfu_anim_action_start_end_time_enum') + if obj.bfu_anim_action_start_end_time_enum == "with_customframes": + OfsetTime = ActionTimeProperty.row() + OfsetTime.prop(obj, 'bfu_anim_action_custom_start_frame') + OfsetTime.prop(obj, 'bfu_anim_action_custom_end_frame') + if obj.bfu_anim_action_start_end_time_enum != "with_customframes": + OfsetTime = ActionTimeProperty.row() + OfsetTime.prop(obj, 'bfu_anim_action_start_frame_offset') + OfsetTime.prop(obj, 'bfu_anim_action_end_frame_offset') + + else: + layout.label( + text=( + "Note: animation start/end use scene frames" + + " with the camera for the sequencer.") + ) + + # Nomenclature + if is_skeletal_mesh: + export_anim_naming = layout.column() + export_anim_naming.enabled = obj.bfu_anim_action_export_enum != 'dont_export' + export_anim_naming.prop(obj, 'bfu_anim_naming_type') + if obj.bfu_anim_naming_type == "include_custom_name": + export_anim_naming_text = export_anim_naming.column() + export_anim_naming_text.prop(obj, 'bfu_anim_naming_custom') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_utils.py b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action/bfu_anim_action_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_anim_action_adv/__init__.py b/blender-for-unrealengine/bfu_anim_action_adv/__init__.py new file mode 100644 index 00000000..5ad38cca --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action_adv/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_anim_action_adv_props +from . import bfu_anim_action_adv_ui +from . import bfu_anim_action_adv_utils + +if "bfu_anim_action_adv_props" in locals(): + importlib.reload(bfu_anim_action_adv_props) +if "bfu_anim_action_adv_ui" in locals(): + importlib.reload(bfu_anim_action_adv_ui) +if "bfu_anim_action_adv_utils" in locals(): + importlib.reload(bfu_anim_action_adv_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_anim_action_adv_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_anim_action_adv_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_props.py b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_props.py new file mode 100644 index 00000000..9c4fa152 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_props.py @@ -0,0 +1,81 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_move_action_to_center_for_export', + 'obj.bfu_rotate_action_to_zero_for_export', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_animation_action_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Actions Advanced Properties") + + bpy.types.Object.bfu_move_action_to_center_for_export = bpy.props.BoolProperty( + name="Move animation to center", + description=( + "(Action animation only) If true use object origin else use scene origin." + + " | If true the mesh will be moved to the center" + + " of the scene for export." + + " (This is used so that the origin of the fbx file" + + " is the same as the mesh in blender)" + + " Note: Unreal Engine ignore the position of the skeleton at the import." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True + ) + + bpy.types.Object.bfu_rotate_action_to_zero_for_export = bpy.props.BoolProperty( + name="Rotate Action to zero", + description=( + "(Action animation only) If true use object rotation else use scene rotation." + + " | If true the mesh will use zero rotation for export." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_rotate_action_to_zero_for_export + del bpy.types.Object.bfu_move_action_to_center_for_export + + del bpy.types.Scene.bfu_animation_action_advanced_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_ui.py b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_ui.py new file mode 100644 index 00000000..e3e716d6 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_ui.py @@ -0,0 +1,48 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_alembic_animation + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + if bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj): + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): + scene.bfu_animation_action_advanced_properties_expanded.draw(layout) + if scene.bfu_animation_action_advanced_properties_expanded.is_expend(): + transformProp = layout.column() + transformProp.enabled = obj.bfu_anim_action_export_enum != 'dont_export' + transformProp.prop(obj, "bfu_move_action_to_center_for_export") + transformProp.prop(obj, "bfu_rotate_action_to_zero_for_export") \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_utils.py b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_action_adv/bfu_anim_action_adv_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_anim_base/__init__.py b/blender-for-unrealengine/bfu_anim_base/__init__.py new file mode 100644 index 00000000..8631601c --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_base/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_anim_base_props +from . import bfu_anim_base_ui +from . import bfu_anim_base_utils + +if "bfu_anim_base_props" in locals(): + importlib.reload(bfu_anim_base_props) +if "bfu_anim_base_ui" in locals(): + importlib.reload(bfu_anim_base_ui) +if "bfu_anim_base_utils" in locals(): + importlib.reload(bfu_anim_base_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_anim_base_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_anim_base_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_props.py b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_props.py new file mode 100644 index 00000000..9762e21b --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_props.py @@ -0,0 +1,151 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_cached_asset_list + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_disable_free_scale_animation', + 'obj.bfu_sample_anim_for_export', + 'obj.bfu_simplify_anim_for_export', + ] + return preset_values + +class BFU_OT_ShowActionToExport(bpy.types.Operator): + bl_label = "Show action(s)" + bl_idname = "object.showobjaction" + bl_description = ( + "Click to show actions that are" + + " to be exported with this armature." + ) + + def execute(self, context): + obj = context.object + animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) + animation_asset_cache.UpdateActionCache() + animation_to_export = animation_asset_cache.GetAnimationAssetList() + + popup_title = "Action list" + if len(animation_to_export) > 0: + animationNumber = len(animation_to_export) + if obj.bfu_anim_nla_use: + animationNumber += 1 + popup_title = ( + str(animationNumber) + + ' action(s) found for obj named "'+obj.name+'".' + ) + else: + popup_title = ( + 'No action found for obj named "' + + obj.name+'".') + + def draw(self, context: bpy.types.Context): + col = self.layout.column() + + def addAnimRow( + action_name, + action_type, + frame_start, + frame_end): + row = col.row() + row.label( + text="- ["+action_name + + "] Frame "+frame_start+" to "+frame_end + + " ("+action_type+")" + ) + + for action in animation_to_export: + Frames = bfu_utils.GetDesiredActionStartEndTime(obj, action) + frame_start = str(Frames[0]) + frame_end = str(Frames[1]) + addAnimRow(action.name, bfu_utils.GetActionType(action), frame_start, frame_end) + if obj.bfu_anim_nla_use: + scene = context.scene + addAnimRow(obj.bfu_anim_nla_export_name, "NlAnim", str(scene.frame_start), str(scene.frame_end)) + + bpy.context.window_manager.popup_menu( + draw, + title=popup_title, + icon='ACTION' + ) + return {'FINISHED'} + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_OT_ShowActionToExport, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_animation_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Animation Advanced Properties") + + bpy.types.Object.bfu_sample_anim_for_export = bpy.props.FloatProperty( + name="Sampling Rate", + description="How often to evaluate animated values (in frames)", + override={'LIBRARY_OVERRIDABLE'}, + min=0.01, max=100.0, + soft_min=0.01, soft_max=100.0, + default=1.0, + ) + + bpy.types.Object.bfu_simplify_anim_for_export = bpy.props.FloatProperty( + name="Simplify animations", + description=( + "How much to simplify baked values" + + " (0.0 to disable, the higher the more simplified)" + ), + override={'LIBRARY_OVERRIDABLE'}, + # No simplification to up to 10% of current magnitude tolerance. + min=0.0, max=100.0, + soft_min=0.0, soft_max=10.0, + default=0.0, + ) + + bpy.types.Object.bfu_disable_free_scale_animation = bpy.props.BoolProperty( + name="Disable non-uniform scale animation.", + description=( + "If checked, scale animation track's elements always have same value. " + + "This applies basic bones only." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_disable_free_scale_animation + del bpy.types.Object.bfu_simplify_anim_for_export + del bpy.types.Object.bfu_sample_anim_for_export + del bpy.types.Scene.bfu_animation_advanced_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py new file mode 100644 index 00000000..2eaac84d --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_ui.py @@ -0,0 +1,89 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_skeletal_mesh +from .. import bfu_alembic_animation +from .. import bfu_cached_asset_list + + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): + scene.bfu_animation_advanced_properties_expanded.draw(layout) + if scene.bfu_animation_advanced_properties_expanded.is_expend(): + # Animation fbx properties + if bfu_alembic_animation.bfu_alembic_animation_utils.is_not_alembic_animation(obj): + propsFbx = layout.row() + if obj.bfu_skeleton_export_procedure != "auto-rig-pro": + propsFbx.prop(obj, 'bfu_sample_anim_for_export') + propsFbx.prop(obj, 'bfu_simplify_anim_for_export') + propsScaleAnimation = layout.row() + propsScaleAnimation.prop(obj, "bfu_disable_free_scale_animation") + +def draw_animation_tab_foot_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_not_skeletal_mesh(obj): + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): + # Armature export action list feedback + layout.label( + text='Note: The Action with only one' + + ' frame are exported like Pose.') + ArmaturePropertyInfo = ( + layout.row().box().split(factor=0.75) + ) + animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) + animation_to_export = animation_asset_cache.GetAnimationAssetList() + ActionNum = len(animation_to_export) + if obj.bfu_anim_nla_use: + ActionNum += 1 + actionFeedback = ( + str(ActionNum) + + " Animation(s) will be exported with this object.") + ArmaturePropertyInfo.label( + text=actionFeedback, + icon='INFO') + ArmaturePropertyInfo.operator("object.showobjaction") diff --git a/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_utils.py b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_base/bfu_anim_base_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_anim_nla/__init__.py b/blender-for-unrealengine/bfu_anim_nla/__init__.py new file mode 100644 index 00000000..54d04a70 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_anim_nla_props +from . import bfu_anim_nla_ui +from . import bfu_anim_nla_utils + +if "bfu_anim_nla_props" in locals(): + importlib.reload(bfu_anim_nla_props) +if "bfu_anim_nla_ui" in locals(): + importlib.reload(bfu_anim_nla_ui) +if "bfu_anim_nla_utils" in locals(): + importlib.reload(bfu_anim_nla_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_anim_nla_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_anim_nla_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_props.py b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_props.py new file mode 100644 index 00000000..45cd120d --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_props.py @@ -0,0 +1,137 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_anim_nla_use', + 'obj.bfu_anim_nla_export_name', + 'obj.bfu_anim_nla_start_end_time_enum', + 'obj.bfu_anim_nla_start_frame_offset', + 'obj.bfu_anim_nla_end_frame_offset', + 'obj.bfu_anim_nla_custom_start_frame', + 'obj.bfu_anim_nla_custom_end_frame', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_animation_nla_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="NLA Properties") + + bpy.types.Object.bfu_anim_nla_use = bpy.props.BoolProperty( + name="Export NLA (Nonlinear Animation)", + description=( + "If checked, exports the all animation of the scene with the NLA " + + "(Don't work with Auto-Rig Pro for the moment.)" + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + + bpy.types.Object.bfu_anim_nla_export_name = bpy.props.StringProperty( + name="NLA export name", + description="Export NLA name (Don't work with Auto-Rig Pro for the moment.)", + override={'LIBRARY_OVERRIDABLE'}, + maxlen=64, + default="NLA_animation", + subtype='FILE_NAME' + ) + + bpy.types.Object.bfu_anim_nla_start_end_time_enum = bpy.props.EnumProperty( + name="NLA Start/End Time", + description="Set when animation starts and end", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ("with_sceneframes", + "Scene time", + "Time will be equal to the scene time", + "SCENE_DATA", + 1), + ("with_customframes", + "Custom time", + 'The time of all the animations of this object' + + ' is defined by you.' + + ' Use "bfu_anim_action_custom_start_frame" and "bfu_anim_action_custom_end_frame"', + "HAND", + 2), + ] + ) + + bpy.types.Object.bfu_anim_nla_start_frame_offset = bpy.props.IntProperty( + name="Offset at start frame", + description="Offset for the start frame.", + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + bpy.types.Object.bfu_anim_nla_end_frame_offset = bpy.props.IntProperty( + name="Offset at end frame", + description=( + "Offset for the end frame. +1" + + " is recommended for the sequences | 0 is recommended" + + " for UnrealEngine cycles | -1 is recommended for Sketchfab cycles" + ), + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + bpy.types.Object.bfu_anim_nla_custom_start_frame = bpy.props.IntProperty( + name="Custom start time", + description="Set when animation start", + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + bpy.types.Object.bfu_anim_nla_custom_end_frame = bpy.props.IntProperty( + name="Custom end time", + description="Set when animation end", + override={'LIBRARY_OVERRIDABLE'}, + default=1 + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_anim_nla_custom_end_frame + del bpy.types.Object.bfu_anim_nla_custom_start_frame + del bpy.types.Object.bfu_anim_nla_end_frame_offset + del bpy.types.Object.bfu_anim_nla_start_frame_offset + del bpy.types.Object.bfu_anim_nla_export_name + del bpy.types.Object.bfu_anim_nla_use + + del bpy.types.Scene.bfu_animation_nla_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py new file mode 100644 index 00000000..cb861e34 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_ui.py @@ -0,0 +1,69 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_skeletal_mesh + + + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + is_skeletal_mesh = bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj) + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): + scene.bfu_animation_nla_properties_expanded.draw(layout) + if scene.bfu_animation_nla_properties_expanded.is_expend(): + # NLA + if is_skeletal_mesh: + NLAAnim = layout.row() + NLAAnim.prop(obj, 'bfu_anim_nla_use') + NLAAnimChild = NLAAnim.column() + NLAAnimChild.enabled = obj.bfu_anim_nla_use + NLAAnimChild.prop(obj, 'bfu_anim_nla_export_name') + if obj.bfu_skeleton_export_procedure == "auto-rig-pro": + NLAAnim.enabled = False + NLAAnimChild.enabled = False + + # NLA Time + if obj.type != "CAMERA" and obj.bfu_skeleton_export_procedure != "auto-rig-pro": + NLATimeProperty = layout.column() + NLATimeProperty.enabled = obj.bfu_anim_nla_use + NLATimeProperty.prop(obj, 'bfu_anim_nla_start_end_time_enum') + if obj.bfu_anim_nla_start_end_time_enum == "with_customframes": + OfsetTime = NLATimeProperty.row() + OfsetTime.prop(obj, 'bfu_anim_nla_custom_start_frame') + OfsetTime.prop(obj, 'bfu_anim_nla_custom_end_frame') + if obj.bfu_anim_nla_start_end_time_enum != "with_customframes": + OfsetTime = NLATimeProperty.row() + OfsetTime.prop(obj, 'bfu_anim_nla_start_frame_offset') + OfsetTime.prop(obj, 'bfu_anim_nla_end_frame_offset') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_utils.py b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla/bfu_anim_nla_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_anim_nla_adv/__init__.py b/blender-for-unrealengine/bfu_anim_nla_adv/__init__.py new file mode 100644 index 00000000..83baefc6 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla_adv/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_anim_nla_adv_props +from . import bfu_anim_nla_adv_ui +from . import bfu_anim_nla_adv_utils + +if "bfu_anim_nla_adv_props" in locals(): + importlib.reload(bfu_anim_nla_adv_props) +if "bfu_anim_nla_adv_ui" in locals(): + importlib.reload(bfu_anim_nla_adv_ui) +if "bfu_anim_nla_adv_utils" in locals(): + importlib.reload(bfu_anim_nla_adv_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_anim_nla_adv_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_anim_nla_adv_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_props.py b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_props.py new file mode 100644 index 00000000..f6ad145c --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_props.py @@ -0,0 +1,81 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_move_nla_to_center_for_export', + 'obj.bfu_rotate_nla_to_zero_for_export', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_animation_nla_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="NLA Advanced Properties") + + bpy.types.Object.bfu_move_nla_to_center_for_export = bpy.props.BoolProperty( + name="Move NLA to center", + description=( + "(Non linear animation only) If true use object origin else use scene origin." + + " | If true the mesh will be moved to the center" + + " of the scene for export." + + " (This is used so that the origin of the fbx file" + + " is the same as the mesh in blender)" + + " Note: Unreal Engine ignore the position of the skeleton at the import." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True + ) + + bpy.types.Object.bfu_rotate_nla_to_zero_for_export = bpy.props.BoolProperty( + name="Rotate NLA to zero", + description=( + "(Non linear animation only) If true use object rotation else use scene rotation." + + " | If true the mesh will use zero rotation for export." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_rotate_nla_to_zero_for_export + del bpy.types.Object.bfu_move_nla_to_center_for_export + + del bpy.types.Scene.bfu_animation_nla_advanced_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py new file mode 100644 index 00000000..3b7dba6b --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_ui.py @@ -0,0 +1,50 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_alembic_animation + + + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + if bfu_alembic_animation.bfu_alembic_animation_utils.is_alembic_animation(obj): + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "ANIM"): + scene.bfu_animation_nla_advanced_properties_expanded.draw(layout) + if scene.bfu_animation_nla_advanced_properties_expanded.is_expend(): + transformProp2 = layout.column() + transformProp2.enabled = obj.bfu_anim_nla_use + transformProp2.prop(obj, "bfu_move_nla_to_center_for_export") + transformProp2.prop(obj, "bfu_rotate_nla_to_zero_for_export") \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_utils.py b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_anim_nla_adv/bfu_anim_nla_adv_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_type.py b/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_type.py index 4b95952d..49df4d63 100644 --- a/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_type.py +++ b/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_type.py @@ -27,7 +27,7 @@ def __init__(self): self.use_materials = False self.use_sockets = False - def support_asset_type(self, obj): + def support_asset_type(self, obj, details = None): return False def get_asset_type_name(self, obj): @@ -39,12 +39,8 @@ def get_obj_export_name(self, obj): def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): return "" - def get_obj_export_directory_path(self, obj): + def get_obj_export_directory_path(self, obj, absolute = True): return "" - - def get_obj_export_abs_directory_path(self, obj): - dirpath = self.get_obj_export_directory_path(obj) - return bpy.path.abspath(dirpath) def can_export_asset(self): return False diff --git a/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_utils.py b/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_utils.py index 04911731..492794c2 100644 --- a/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_utils.py +++ b/blender-for-unrealengine/bfu_assets_manager/bfu_asset_manager_utils.py @@ -22,10 +22,10 @@ -def get_asset_class(obj) -> bfu_asset_manager_type.BFU_BaseAssetClass: +def get_asset_class(obj, details = None) -> bfu_asset_manager_type.BFU_BaseAssetClass: for asset in bfu_asset_manager_registred_assets.get_registred_asset_class(): asset: bfu_asset_manager_type.BFU_BaseAssetClass - if asset.support_asset_type(obj): + if asset.support_asset_type(obj, details): return asset def get_all_asset_class(): diff --git a/blender-for-unrealengine/bfu_assets_references/__init__.py b/blender-for-unrealengine/bfu_assets_references/__init__.py new file mode 100644 index 00000000..bbb33671 --- /dev/null +++ b/blender-for-unrealengine/bfu_assets_references/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_asset_ref_props +from . import bfu_asset_ref_ui +from . import bfu_asset_ref_utils + +if "bfu_asset_ref_props" in locals(): + importlib.reload(bfu_asset_ref_props) +if "bfu_asset_ref_ui" in locals(): + importlib.reload(bfu_asset_ref_ui) +if "bfu_asset_ref_utils" in locals(): + importlib.reload(bfu_asset_ref_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_asset_ref_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_asset_ref_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_unreal_engine_refs_props.py b/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_props.py similarity index 80% rename from blender-for-unrealengine/bfu_addon_parts/bfu_unreal_engine_refs_props.py rename to blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_props.py index e5d7078d..38143165 100644 --- a/blender-for-unrealengine/bfu_addon_parts/bfu_unreal_engine_refs_props.py +++ b/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_props.py @@ -16,35 +16,14 @@ # # ======================= END GPL LICENSE BLOCK ============================= + import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl -classes = ( -) -def draw_skeleton_prop(layout: bpy.types.UILayout, obj: bpy.types.Object): - layout.prop(obj, "bfu_engine_ref_skeleton_search_mode") - if obj.bfu_engine_ref_skeleton_search_mode == "auto": - pass - if obj.bfu_engine_ref_skeleton_search_mode == "custom_name": - layout.prop(obj, "bfu_engine_ref_skeleton_custom_name") - if obj.bfu_engine_ref_skeleton_search_mode == "custom_path_name": - layout.prop(obj, "bfu_engine_ref_skeleton_custom_path") - layout.prop(obj, "bfu_engine_ref_skeleton_custom_name") - if obj.bfu_engine_ref_skeleton_search_mode == "custom_reference": - layout.prop(obj, "bfu_engine_ref_skeleton_custom_ref") - - -def draw_skeletal_mesh_prop(layout: bpy.types.UILayout, obj: bpy.types.Object): - layout.prop(obj, "bfu_engine_ref_skeletal_mesh_search_mode") - if obj.bfu_engine_ref_skeletal_mesh_search_mode == "auto": - pass - if obj.bfu_engine_ref_skeletal_mesh_search_mode == "custom_name": - layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_name") - if obj.bfu_engine_ref_skeletal_mesh_search_mode == "custom_path_name": - layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_path") - layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_name") - if obj.bfu_engine_ref_skeletal_mesh_search_mode == "custom_reference": - layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_ref") def get_preset_values(): @@ -61,11 +40,21 @@ def get_preset_values(): ] return preset_values +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + def register(): for cls in classes: bpy.utils.register_class(cls) + bpy.types.Scene.bfu_engine_ref_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Engine Refs") + + bpy.types.Object.bfu_engine_ref_skeleton_search_mode = bpy.props.EnumProperty( name="Skeleton Ref", description='Specify the skeleton location in Unreal', @@ -163,8 +152,6 @@ def register(): default="SkeletalMesh'/Game/ImportedFbx/SKM_MySkeletalMesh.SKM_MySkeletalMesh'" ) - - def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) @@ -177,4 +164,6 @@ def unregister(): del bpy.types.Object.bfu_engine_ref_skeletal_mesh_custom_ref del bpy.types.Object.bfu_engine_ref_skeletal_mesh_custom_name del bpy.types.Object.bfu_engine_ref_skeletal_mesh_custom_path - del bpy.types.Object.bfu_engine_ref_skeletal_mesh_search_mode \ No newline at end of file + del bpy.types.Object.bfu_engine_ref_skeletal_mesh_search_mode + + del bpy.types.Scene.bfu_engine_ref_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_ui.py b/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_ui.py new file mode 100644 index 00000000..3de1e89c --- /dev/null +++ b/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_ui.py @@ -0,0 +1,83 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bbpl +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bfu_skeletal_mesh + + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + if obj is None: + return + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if addon_prefs.useGeneratedScripts is False: + return + if obj.bfu_export_type != "export_recursive": + return + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_not_skeletal_mesh(obj): + return + + # Draw UI + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + scene.bfu_engine_ref_properties_expanded.draw(layout) + if scene.bfu_engine_ref_properties_expanded.is_expend(): + + # SkeletalMesh prop + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + if not obj.bfu_export_as_lod_mesh: + unreal_engine_refs = layout.column() + draw_skeleton_prop(unreal_engine_refs, obj) + draw_skeletal_mesh_prop(unreal_engine_refs, obj) + + +def draw_skeleton_prop(layout: bpy.types.UILayout, obj: bpy.types.Object): + layout.prop(obj, "bfu_engine_ref_skeleton_search_mode") + if obj.bfu_engine_ref_skeleton_search_mode == "auto": + pass + if obj.bfu_engine_ref_skeleton_search_mode == "custom_name": + layout.prop(obj, "bfu_engine_ref_skeleton_custom_name") + if obj.bfu_engine_ref_skeleton_search_mode == "custom_path_name": + layout.prop(obj, "bfu_engine_ref_skeleton_custom_path") + layout.prop(obj, "bfu_engine_ref_skeleton_custom_name") + if obj.bfu_engine_ref_skeleton_search_mode == "custom_reference": + layout.prop(obj, "bfu_engine_ref_skeleton_custom_ref") + +def draw_skeletal_mesh_prop(layout: bpy.types.UILayout, obj: bpy.types.Object): + layout.prop(obj, "bfu_engine_ref_skeletal_mesh_search_mode") + if obj.bfu_engine_ref_skeletal_mesh_search_mode == "auto": + pass + if obj.bfu_engine_ref_skeletal_mesh_search_mode == "custom_name": + layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_name") + if obj.bfu_engine_ref_skeletal_mesh_search_mode == "custom_path_name": + layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_path") + layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_name") + if obj.bfu_engine_ref_skeletal_mesh_search_mode == "custom_reference": + layout.prop(obj, "bfu_engine_ref_skeletal_mesh_custom_ref") \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_utils.py b/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_utils.py new file mode 100644 index 00000000..a6e76eb3 --- /dev/null +++ b/blender-for-unrealengine/bfu_assets_references/bfu_asset_ref_utils.py @@ -0,0 +1,22 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import fnmatch +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_backward_compatibility.py b/blender-for-unrealengine/bfu_backward_compatibility.py index 3602a5be..f90a4ae4 100644 --- a/blender-for-unrealengine/bfu_backward_compatibility.py +++ b/blender-for-unrealengine/bfu_backward_compatibility.py @@ -39,8 +39,6 @@ def update_old_variables(): print("Updating old bfu variables...") for obj in bpy.data.objects: - - update_variable(obj, ["bfu_skeleton_search_mode"], "bfu_engine_ref_skeleton_search_mode", enum_callback) update_variable(obj, ["bfu_target_skeleton_custom_path"], "bfu_engine_ref_skeleton_custom_path") update_variable(obj, ["bfu_target_skeleton_custom_name"], "bfu_engine_ref_skeleton_custom_name") @@ -71,7 +69,6 @@ def update_old_variables(): update_variable(obj, ["GenerateLightmapUVs"], "bfu_generate_light_map_uvs") update_variable(obj, ["convert_geometry_node_attribute_to_uv"], "bfu_convert_geometry_node_attribute_to_uv") update_variable(obj, ["convert_geometry_node_attribute_to_uv_name"], "bfu_convert_geometry_node_attribute_to_uv_name") - update_variable(obj, ["correct_extrem_uv_scale"], "bfu_correct_extrem_uv_scale") update_variable(obj, ["AutoGenerateCollision"], "bfu_auto_generate_collision") update_variable(obj, ["MaterialSearchLocation"], "bfu_material_search_location", enum_callback) update_variable(obj, ["CollisionTraceFlag"], "bfu_collision_trace_flag", enum_callback) @@ -99,13 +96,13 @@ def update_old_variables(): update_variable(obj, ["AdditionalLocationForExport"], "bfu_additional_location_for_export") update_variable(obj, ["AdditionalRotationForExport"], "bfu_additional_rotation_for_export") - update_variable(obj, ["exportActionList"], "bfu_animation_asset_list") - update_variable(obj, ["active_ObjectAction"], "bfu_active_animation_asset_list") + update_variable(obj, ["exportActionList, bfu_animation_asset_list"], "bfu_action_asset_list") + update_variable(obj, ["active_ObjectAction, bfu_active_animation_asset_list"], "bfu_active_action_asset_list") update_variable(obj, ["ExportAsAlembic, bfu_export_as_alembic"], "bfu_export_as_alembic_animation") - - + update_variable(obj, ["correct_extrem_uv_scale", "bfu_correct_extrem_uv_scale"], "bfu_use_correct_extrem_uv_scale") + update_variable(obj, ["bfu_invert_normal_maps"], "bfu_flip_normal_map_green_channel") for col in bpy.data.collections: update_variable(col, ["exportFolderName"], "bfu_export_folder_name") @@ -133,6 +130,20 @@ def update_old_variables(): update_variable(scene, ["CollectionExportList"], "bfu_collection_asset_list") update_variable(scene, ["active_CollectionExportList"], "bfu_active_collection_asset_list") + update_variable(scene, ["static_export"], "bfu_use_static_export") + update_variable(scene, ["static_collection_export"], "bfu_use_static_collection_export") + update_variable(scene, ["skeletal_export"], "bfu_use_skeletal_export") + update_variable(scene, ["anin_export"], "bfu_use_anin_export") + update_variable(scene, ["alembic_export"], "bfu_use_alembic_export") + update_variable(scene, ["groom_simulation_export"], "bfu_use_groom_simulation_export") + update_variable(scene, ["camera_export"], "bfu_use_camera_export") + update_variable(scene, ["spline_export"], "bfu_use_spline_export") + + update_variable(scene, ["text_ExportLog"], "bfu_use_text_export_log") + update_variable(scene, ["text_ImportAssetScript"], "bfu_use_text_import_asset_script") + update_variable(scene, ["text_ImportSequenceScript"], "bfu_use_text_import_sequence_script") + update_variable(scene, ["text_AdditionalData"], "bfu_use_text_additional_data") + def enum_callback(data, old_var_name, new_var_name): value = data[old_var_name] # Get value ast int diff --git a/blender-for-unrealengine/bfu_base_collection/__init__.py b/blender-for-unrealengine/bfu_base_collection/__init__.py new file mode 100644 index 00000000..ed58fcc4 --- /dev/null +++ b/blender-for-unrealengine/bfu_base_collection/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_base_col_props +from . import bfu_base_col_ui +from . import bfu_base_col_utils + +if "bfu_base_col_props" in locals(): + importlib.reload(bfu_base_col_props) +if "bfu_base_col_ui" in locals(): + importlib.reload(bfu_base_col_ui) +if "bfu_base_col_utils" in locals(): + importlib.reload(bfu_base_col_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_base_col_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_base_col_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_base_collection/bfu_base_col_props.py b/blender-for-unrealengine/bfu_base_collection/bfu_base_col_props.py new file mode 100644 index 00000000..110bdb6c --- /dev/null +++ b/blender-for-unrealengine/bfu_base_collection/bfu_base_col_props.py @@ -0,0 +1,176 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + ] + return preset_values + +class BFU_UL_CollectionExportTarget(bpy.types.UIList): + + def draw_item(self, context, layout, data, item, icon, active_data, active_property, index, flt_flag): + + collection_is_valid = False + if item.name in bpy.data.collections: + collection_is_valid = True + + if self.layout_type in {'DEFAULT', 'COMPACT'}: + if collection_is_valid: # If action is valid + layout.prop( + bpy.data.collections[item.name], + "name", + text="", + emboss=False, + icon="OUTLINER_COLLECTION") + layout.prop(item, "use", text="") + else: + dataText = ( + 'Collection named "' + + item.name + + '" Not Found. Please clic on update') + layout.label(text=dataText, icon="ERROR") + # Not optimised for 'GRID' layout type. + elif self.layout_type in {'GRID'}: + layout.alignment = 'CENTER' + layout.label(text="", icon_value=icon) + +class BFU_OT_UpdateCollectionButton(bpy.types.Operator): + bl_label = "Update collection list" + bl_idname = "object.updatecollectionlist" + bl_description = "Update collection list" + + def execute(self, context): + def UpdateExportCollectionList(scene): + # Update the provisional collection list known by the object + + def SetUseFromLast(col_list, CollectionName): + for item in col_list: + if item[0] == CollectionName: + if item[1]: + return True + return False + + colSave = [["", False]] + for col in scene.bfu_collection_asset_list: # CollectionProperty + name = col.name + use = col.use + colSave.append([name, use]) + scene.bfu_collection_asset_list.clear() + for col in bpy.data.collections: + scene.bfu_collection_asset_list.add().name = col.name + useFromLast = SetUseFromLast(colSave, col.name) + scene.bfu_collection_asset_list[col.name].use = useFromLast + UpdateExportCollectionList(context.scene) + return {'FINISHED'} + +class BFU_OT_ShowCollectionToExport(bpy.types.Operator): + bl_label = "Show collection(s)" + bl_idname = "object.showscenecollection" + bl_description = "Click to show collections to export" + + def execute(self, context): + scene = context.scene + collection_asset_cache = bfu_cached_asset_list.GetCollectionAssetCache() + collection_export_asset_list = collection_asset_cache.GetCollectionAssetList() + popup_title = "Collection list" + if len(collection_export_asset_list) > 0: + popup_title = ( + str(len(collection_export_asset_list))+' collection(s) to export found.') + else: + popup_title = 'No collection to export found.' + + def draw(self, context: bpy.types.Context): + col = self.layout.column() + for collection in collection_export_asset_list: + row = col.row() + row.label(text="- "+collection.name) + bpy.context.window_manager.popup_menu( + draw, + title=popup_title, + icon='GROUP') + return {'FINISHED'} + + +class BFU_OT_SceneCollectionExport(bpy.types.PropertyGroup): + name: bpy.props.StringProperty(name="collection data name", default="Unknown", override={'LIBRARY_OVERRIDABLE'}) + use: bpy.props.BoolProperty(name="export this collection", default=False, override={'LIBRARY_OVERRIDABLE'}) + + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_UL_CollectionExportTarget, + BFU_OT_UpdateCollectionButton, + BFU_OT_ShowCollectionToExport, + BFU_OT_SceneCollectionExport, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_collection_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Collection Properties") + + bpy.types.Scene.bfu_collection_asset_list = bpy.props.CollectionProperty( + type=BFU_OT_SceneCollectionExport, + options={'LIBRARY_EDITABLE'}, + override={'LIBRARY_OVERRIDABLE', 'USE_INSERTION'}, + ) + + bpy.types.Scene.bfu_active_collection_asset_list = bpy.props.IntProperty( + name="Active Collection", + description="Index of the currently active collection", + override={'LIBRARY_OVERRIDABLE'}, + default=0 + ) + + bpy.types.Collection.bfu_export_folder_name = bpy.props.StringProperty( + name="Sub folder name", + description=( + 'The name of sub folder.' + + ' You can now use ../ for up one directory.' + ), + override={'LIBRARY_OVERRIDABLE'}, + maxlen=64, + default="", + subtype='FILE_NAME' + ) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Collection.bfu_export_folder_name + + del bpy.types.Scene.bfu_active_collection_asset_list + del bpy.types.Scene.bfu_collection_asset_list + del bpy.types.Scene.bfu_collection_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_base_collection/bfu_base_col_ui.py b/blender-for-unrealengine/bfu_base_collection/bfu_base_col_ui.py new file mode 100644 index 00000000..1b5bc0f0 --- /dev/null +++ b/blender-for-unrealengine/bfu_base_collection/bfu_base_col_ui.py @@ -0,0 +1,70 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_export_procedure +from .. import bfu_cached_asset_list + + +def draw_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + + + scene = bpy.context.scene + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("SCENE", "GENERAL"): + + scene.bfu_collection_properties_expanded.draw(layout) + if scene.bfu_collection_properties_expanded.is_expend(): + collectionListProperty = layout.column() + collectionListProperty.template_list( + # type and unique id + "BFU_UL_CollectionExportTarget", "", + # pointer to the CollectionProperty + scene, "bfu_collection_asset_list", + # pointer to the active identifier + scene, "bfu_active_collection_asset_list", + maxrows=5, + rows=5 + ) + collectionListProperty.operator( + "object.updatecollectionlist", + icon='RECOVER_LAST') + + if scene.bfu_active_collection_asset_list < len(scene.bfu_collection_asset_list): + col_name = scene.bfu_collection_asset_list[scene.bfu_active_collection_asset_list].name + if col_name in bpy.data.collections: + col = bpy.data.collections[col_name] + col_prop = layout + col_prop.prop(col, 'bfu_export_folder_name', icon='FILE_FOLDER') + bfu_export_procedure.bfu_export_procedure_ui.draw_collection_export_procedure(layout, col) + + collectionPropertyInfo = layout.row().box().split(factor=0.75) + collection_asset_cache = bfu_cached_asset_list.GetCollectionAssetCache() + collection_export_asset_list = collection_asset_cache.GetCollectionAssetList() + collectionNum = len(collection_export_asset_list) + collectionFeedback = ( + str(collectionNum) + + " Collection(s) will be exported.") + collectionPropertyInfo.label(text=collectionFeedback, icon='INFO') + collectionPropertyInfo.operator("object.showscenecollection") + layout.label(text='Note: The collection are exported like StaticMesh.') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_base_collection/bfu_base_col_utils.py b/blender-for-unrealengine/bfu_base_collection/bfu_base_col_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_base_collection/bfu_base_col_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_base_object/__init__.py b/blender-for-unrealengine/bfu_base_object/__init__.py new file mode 100644 index 00000000..8d5b79d1 --- /dev/null +++ b/blender-for-unrealengine/bfu_base_object/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_base_obj_props +from . import bfu_base_obj_ui +from . import bfu_base_obj_utils + +if "bfu_base_obj_props" in locals(): + importlib.reload(bfu_base_obj_props) +if "bfu_base_obj_ui" in locals(): + importlib.reload(bfu_base_obj_ui) +if "bfu_base_obj_utils" in locals(): + importlib.reload(bfu_base_obj_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_base_obj_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_base_obj_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_base_object/bfu_base_obj_props.py b/blender-for-unrealengine/bfu_base_object/bfu_base_obj_props.py new file mode 100644 index 00000000..ce012967 --- /dev/null +++ b/blender-for-unrealengine/bfu_base_object/bfu_base_obj_props.py @@ -0,0 +1,111 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_export_type', + 'obj.bfu_export_folder_name', + 'obj.bfu_use_custom_export_name', + 'obj.bfu_custom_export_name', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_object_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Object Properties") + + bpy.types.Object.bfu_export_type = bpy.props.EnumProperty( + name="Export type", + description="Export procedure", + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ("auto", + "Auto", + "Export with the parent if the parents is \"Export recursive\"", + "BOIDS", + 1), + ("export_recursive", + "Export recursive", + "Export self object and all children", + "KEYINGSET", + 2), + ("dont_export", + "Not exported", + "Will never export", + "CANCEL", + 3) + ] + ) + + bpy.types.Object.bfu_export_folder_name = bpy.props.StringProperty( + name="Sub folder name", + description=( + 'The name of sub folder.' + + ' You can now use ../ for up one directory.' + ), + override={'LIBRARY_OVERRIDABLE'}, + maxlen=64, + default="", + subtype='FILE_NAME' + ) + + bpy.types.Object.bfu_use_custom_export_name = bpy.props.BoolProperty( + name="Export with custom name", + description=("Specify a custom name for the exported file"), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + + bpy.types.Object.bfu_custom_export_name = bpy.props.StringProperty( + name="", + description="The name of exported file", + override={'LIBRARY_OVERRIDABLE'}, + default="MyObjectExportName.fbx" + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_custom_export_name + del bpy.types.Object.bfu_use_custom_export_name + + del bpy.types.Object.bfu_export_folder_name + del bpy.types.Object.bfu_export_type + + del bpy.types.Scene.bfu_object_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_base_object/bfu_base_obj_ui.py b/blender-for-unrealengine/bfu_base_object/bfu_base_obj_ui.py new file mode 100644 index 00000000..c74b9d25 --- /dev/null +++ b/blender-for-unrealengine/bfu_base_object/bfu_base_obj_ui.py @@ -0,0 +1,87 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_assets_manager +from .. import bfu_alembic_animation +from .. import bfu_groom +from .. import bfu_skeletal_mesh + + +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + # Hide filters + if obj is None: + layout.row().label(text='No active object.') + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + scene.bfu_object_properties_expanded.draw(layout) + if scene.bfu_object_properties_expanded.is_expend(): + AssetType = layout.row() + AssetType.prop(obj, 'name', text="", icon='OBJECT_DATA') + # Show asset type + asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) + if asset_class: + asset_type_name = asset_class.get_asset_type_name(obj) + else: + asset_type_name = "Asset type not found." + + AssetType.label(text='('+asset_type_name+')') + + ExportType = layout.column() + ExportType.prop(obj, 'bfu_export_type') + + + if obj.bfu_export_type == "export_recursive": + + folderNameProperty = layout.column() + folderNameProperty.prop(obj, 'bfu_export_folder_name', icon='FILE_FOLDER') + + ProxyProp = layout.column() + if bfu_utils.GetExportAsProxy(obj): + ProxyProp.label(text="The Armature was detected as a proxy.") + proxy_child = bfu_utils.GetExportProxyChild(obj) + if proxy_child: + ProxyProp.label(text="Proxy child: " + proxy_child.name) + else: + ProxyProp.label(text="Proxy child not found") + + if not bfu_utils.GetExportAsProxy(obj): + # exportCustomName + exportCustomName = layout.row() + exportCustomName.prop(obj, "bfu_use_custom_export_name") + useCustomName = obj.bfu_use_custom_export_name + exportCustomNameText = exportCustomName.column() + exportCustomNameText.prop(obj, "bfu_custom_export_name") + exportCustomNameText.enabled = useCustomName + bfu_alembic_animation.bfu_alembic_animation_ui.draw_general_ui_object(layout, obj) + bfu_groom.bfu_groom_ui.draw_general_ui_object(layout, obj) + bfu_skeletal_mesh.bfu_skeletal_mesh_ui.draw_general_ui_object(layout, obj) diff --git a/blender-for-unrealengine/bfu_base_object/bfu_base_obj_utils.py b/blender-for-unrealengine/bfu_base_object/bfu_base_obj_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_base_object/bfu_base_obj_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_basics.py b/blender-for-unrealengine/bfu_basics.py index e142385d..89e396f3 100644 --- a/blender-for-unrealengine/bfu_basics.py +++ b/blender-for-unrealengine/bfu_basics.py @@ -28,69 +28,21 @@ def GetAddonPrefs(): return bpy.context.preferences.addons[__package__].preferences -def is_deleted(o): - if o and o is not None: - return not (o.name in bpy.data.objects) - else: - return True - - def CheckPluginIsActivated(PluginName): is_enabled, is_loaded = addon_utils.check(PluginName) return is_enabled and is_loaded -def ChecksRelationship(arrayA, arrayB): - # Checks if it exits an identical variable in two lists - - for a in arrayA: - for b in arrayB: - if a == b: - return True - return False - - -def nextPowerOfTwo(n): - # compute power of two greater than or equal to n - - # decrement n (to handle cases when n itself - # is a power of 2) - n = n - 1 - - # do till only one bit is left - while n & n - 1: - n = n & n - 1 # unset rightmost bit - - # n is now a power of two (less than n) - return n << 1 - - -def previousPowerOfTwo(n): - # compute power of two less than or equal to n - - # do till only one bit is left - while (n & n - 1): - n = n & n - 1 # unset rightmost bit - - # n is now a power of two (less than or equal to n) - return n - - def RemoveFolderTree(folder): dirpath = Path(folder) if dirpath.exists() and dirpath.is_dir(): shutil.rmtree(dirpath, ignore_errors=True) - - - - def getRootBoneParent(bone): if bone.parent is not None: return getRootBoneParent(bone.parent) return bone - def getFirstDeformBoneParent(bone): if bone.parent is not None: if bone.use_deform is True: @@ -99,7 +51,6 @@ def getFirstDeformBoneParent(bone): return getFirstDeformBoneParent(bone.parent) return bone - def SetCollectionUse(collection): # Set if collection is hide and selectable collection.hide_viewport = False diff --git a/blender-for-unrealengine/bfu_cached_asset_list.py b/blender-for-unrealengine/bfu_cached_asset_list.py index 05d9416a..06266e02 100644 --- a/blender-for-unrealengine/bfu_cached_asset_list.py +++ b/blender-for-unrealengine/bfu_cached_asset_list.py @@ -159,7 +159,7 @@ def GetAnimationAssetList(self): elif obj.bfu_anim_action_export_enum == "export_specific_list": for action in bpy.data.actions: - for targetAction in obj.bfu_animation_asset_list: + for targetAction in obj.bfu_action_asset_list: if targetAction.use: if targetAction.name == action.name: TargetActionToExport.append(action) @@ -230,7 +230,7 @@ def getHaveParentToExport(obj): for collection in collectionList: # Collection - if scene.static_collection_export: + if scene.bfu_use_static_collection_export: TargetAssetToExport.append(AssetToExport(collection, None, "Collection StaticMesh")) for obj in objList: @@ -239,7 +239,7 @@ def getHaveParentToExport(obj): if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): # Skeletal Mesh - if scene.skeletal_export: + if scene.bfu_use_skeletal_export: if obj.bfu_modular_skeletal_mesh_mode == "all_in_one": asset = AssetToExport(obj, None, "SkeletalMesh") asset.name = obj.name @@ -270,7 +270,7 @@ def getHaveParentToExport(obj): TargetAssetToExport.append(asset) # NLA - if scene.anin_export: + if scene.bfu_use_anin_export: if obj.bfu_anim_nla_use: TargetAssetToExport.append(AssetToExport(obj, obj.animation_data, "NlAnim")) @@ -283,12 +283,12 @@ def getHaveParentToExport(obj): TargetAssetToExport.append(AssetToExport(obj, action, "Action")) else: # Action - if scene.anin_export: + if scene.bfu_use_anin_export: if bfu_utils.GetActionType(action) == "Action": TargetAssetToExport.append(AssetToExport(obj, action, "Action")) # Pose - if scene.anin_export: + if scene.bfu_use_anin_export: if bfu_utils.GetActionType(action) == "Pose": TargetAssetToExport.append(AssetToExport(obj, action, "Pose")) # Others diff --git a/blender-for-unrealengine/bfu_camera/bfu_camera_data.py b/blender-for-unrealengine/bfu_camera/bfu_camera_data.py index 6469cbf8..25e99c62 100644 --- a/blender-for-unrealengine/bfu_camera/bfu_camera_data.py +++ b/blender-for-unrealengine/bfu_camera/bfu_camera_data.py @@ -2,7 +2,7 @@ import math from typing import Dict, Any from . import bfu_camera_unreal_utils -from .. import bps +from .. import bpl from .. import bbpl from .. import languages from .. import bfu_basics @@ -283,7 +283,7 @@ def evaluate_all_tracks(self, camera, frame_start, frame_end): addon_prefs = bfu_basics.GetAddonPrefs() print(f"Start evaluate camera {camera.name} Frames:({str(frame_start)}-{str(frame_end)})") - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() slms = bfu_utils.TimelineMarkerSequence() @@ -341,7 +341,7 @@ def optimizated_evaluate_track_at_frame(evaluate: BFU_CameraTracks): scene = bpy.context.scene addon_prefs = bfu_basics.GetAddonPrefs() - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() slms = bfu_utils.TimelineMarkerSequence() diff --git a/blender-for-unrealengine/bfu_camera/bfu_camera_type.py b/blender-for-unrealengine/bfu_camera/bfu_camera_type.py index 01b08cdb..5bc2d788 100644 --- a/blender-for-unrealengine/bfu_camera/bfu_camera_type.py +++ b/blender-for-unrealengine/bfu_camera/bfu_camera_type.py @@ -30,7 +30,7 @@ def __init__(self): super().__init__() pass - def support_asset_type(self, obj): + def support_asset_type(self, obj, details = None): if obj.type == "CAMERA": return True return False @@ -48,15 +48,20 @@ def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): return bfu_basics.ValidFilename(scene.bfu_camera_prefix_export_name+desired_name+fileType) return bfu_basics.ValidFilename(scene.bfu_camera_prefix_export_name+obj.name+fileType) - def get_obj_export_directory_path(self, obj): + def get_obj_export_directory_path(self, obj, absolute = True): folder_name = bfu_utils.get_export_folder_name(obj) scene = bpy.context.scene - dirpath = os.path.join(scene.bfu_export_camera_file_path, folder_name) + if(absolute): + root_path = bpy.path.abspath(scene.bfu_export_camera_file_path) + else: + root_path = scene.bfu_export_camera_file_path + + dirpath = os.path.join(root_path, folder_name) return dirpath def can_export_asset(self): scene = bpy.context.scene - return scene.camera_export + return scene.bfu_use_camera_export def can_export_obj_asset(self, obj): return self.can_export_asset() diff --git a/blender-for-unrealengine/bfu_camera/bfu_camera_ui_and_props.py b/blender-for-unrealengine/bfu_camera/bfu_camera_ui_and_props.py index 409d49d9..a6d10a60 100644 --- a/blender-for-unrealengine/bfu_camera/bfu_camera_ui_and_props.py +++ b/blender-for-unrealengine/bfu_camera/bfu_camera_ui_and_props.py @@ -22,6 +22,7 @@ from . import bfu_camera_utils from . import bfu_camera_write_paste_commands from .. import bfu_basics +from .. import bfu_ui from .. import bbpl from .. import languages @@ -48,29 +49,29 @@ def draw_ui_object_camera(layout: bpy.types.UILayout, obj: bpy.types.Object): camera_ui = layout.column() scene = bpy.context.scene - scene.bfu_camera_properties_expanded.draw(camera_ui) - if scene.bfu_camera_properties_expanded.is_expend(): - if obj.type == "CAMERA": - camera_ui_pop = camera_ui.column() - - export_procedure_prop = camera_ui_pop.column() - export_procedure_prop.prop(obj, 'bfu_camera_export_procedure') - - camera_ui_pop.prop(obj, 'bfu_desired_camera_type') - if obj.bfu_desired_camera_type == "CUSTOM": - camera_ui_pop.prop(obj, 'bfu_custom_camera_actor') - camera_ui_pop.prop(obj, 'bfu_custom_camera_default_actor') - camera_ui_pop.prop(obj, 'bfu_custom_camera_component') - camera_ui_pop.prop(obj, 'bfu_export_fbx_camera') - camera_ui_pop.prop(obj, 'bfu_fix_axis_flippings') - camera_ui_pop.enabled = obj.bfu_export_type == "export_recursive" - camera_ui.operator("object.bfu_copy_active_camera_data", icon="COPYDOWN") - - -def draw_ui_scene_camera(layout: bpy.types.UILayout): - + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + scene.bfu_camera_properties_expanded.draw(camera_ui) + if scene.bfu_camera_properties_expanded.is_expend(): + if obj.type == "CAMERA": + camera_ui_pop = camera_ui.column() + + export_procedure_prop = camera_ui_pop.column() + export_procedure_prop.prop(obj, 'bfu_camera_export_procedure') + + camera_ui_pop.prop(obj, 'bfu_desired_camera_type') + if obj.bfu_desired_camera_type == "CUSTOM": + camera_ui_pop.prop(obj, 'bfu_custom_camera_actor') + camera_ui_pop.prop(obj, 'bfu_custom_camera_default_actor') + camera_ui_pop.prop(obj, 'bfu_custom_camera_component') + camera_ui_pop.prop(obj, 'bfu_export_fbx_camera') + camera_ui_pop.prop(obj, 'bfu_fix_axis_flippings') + camera_ui_pop.enabled = obj.bfu_export_type == "export_recursive" + camera_ui.operator("object.bfu_copy_active_camera_data", icon="COPYDOWN") + + +def draw_tools_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene camera_ui = layout.column() - scene = bpy.context.scene scene.bfu_camera_tools_expanded.draw(camera_ui) if scene.bfu_camera_tools_expanded.is_expend(): camera_ui.operator("object.copy_selected_cameras_data", icon="COPYDOWN") diff --git a/blender-for-unrealengine/bfu_check_potential_error.py b/blender-for-unrealengine/bfu_check_potential_error.py deleted file mode 100644 index 1baedd8a..00000000 --- a/blender-for-unrealengine/bfu_check_potential_error.py +++ /dev/null @@ -1,748 +0,0 @@ -# ====================== BEGIN GPL LICENSE BLOCK ============================ -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# All rights reserved. -# -# ======================= END GPL LICENSE BLOCK ============================= - - -import bpy -import fnmatch -import math - -from . import bbpl -from . import bfu_basics -from . import bfu_assets_manager -from . import bfu_utils -from . import bfu_cached_asset_list - -from . import bfu_collision -from . import bfu_socket -from . import bfu_camera -from . import bfu_alembic_animation -from . import bfu_groom -from . import bfu_spline -from . import bfu_skeletal_mesh -from . import bfu_static_mesh - - - - -def process_general_fix(): - fixed_collisions = bfu_collision.bfu_collision_utils.fix_export_type_on_collision() - fixed_collision_names = bfu_collision.bfu_collision_utils.fix_name_on_collision() - fixed_sockets = bfu_socket.bfu_socket_utils.fix_export_type_on_socket() - fixed_socket_names = bfu_socket.bfu_socket_utils.fix_name_on_socket() - - fix_info = { - "Fixed Collision(s)": fixed_collisions, - "Fixed Collision Names(s)": fixed_collision_names, - "Fixed Socket(s)": fixed_sockets, - "Fixed Socket Names(s)": fixed_socket_names, - } - - return fix_info - - - - -def GetVertexWithZeroWeight(Armature, Mesh): - vertices = [] - - # Créez un ensemble des noms des os de l'armature pour une recherche plus rapide - armature_bone_names = set(bone.name for bone in Armature.data.bones) - - - for vertex in Mesh.data.vertices: #MeshVertex(bpy_struct) - cumulateWeight = 0 - - if vertex.groups: - for group_elem in vertex.groups: #VertexGroupElement(bpy_struct) - if group_elem.weight > 0: - group_index = group_elem.group - group_len = len(Mesh.vertex_groups) - if group_index <= group_len: - group = Mesh.vertex_groups[group_elem.group] - - # Utilisez l'ensemble des noms d'os pour vérifier l'appartenance à l'armature - if group.name in armature_bone_names: - cumulateWeight += group_elem.weight - - if cumulateWeight == 0: - vertices.append(vertex) - - return vertices - - -def ContainsArmatureModifier(obj): - for mod in obj.modifiers: - if mod.type == "ARMATURE": - return True - return False - -def GetSkeletonMeshs(obj): - meshs = [] - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - childs = bfu_utils.GetExportDesiredChilds(obj) - for child in childs: - if child.type == "MESH": - meshs.append(child) - return meshs - - -def UpdateUnrealPotentialError(): - # Find and reset list of all potential error in scene - - addon_prefs = bfu_basics.GetAddonPrefs() - PotentialErrors = bpy.context.scene.potentialErrorList - PotentialErrors.clear() - - # prepares the data to avoid unnecessary loops - objToCheck = [] - final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() - final_asset_list_to_export = final_asset_cache.GetFinalAssetList() - for Asset in final_asset_list_to_export: - if Asset.obj in bfu_utils.GetAllobjectsByExportType("export_recursive"): - if Asset.obj not in objToCheck: - objToCheck.append(Asset.obj) - for child in bfu_utils.GetExportDesiredChilds(Asset.obj): - if child not in objToCheck: - objToCheck.append(child) - - MeshTypeToCheck = [] - for obj in objToCheck: - if obj.type == 'MESH': - MeshTypeToCheck.append(obj) - - MeshTypeWithoutCol = [] # is Mesh Type To Check Without Collision - for obj in MeshTypeToCheck: - if not bfu_utils.CheckIsCollision(obj): - MeshTypeWithoutCol.append(obj) - - def CheckUnitScale(): - # Check if the unit scale is equal to 0.01. - if addon_prefs.notifyUnitScalePotentialError: - unit_scale = bfu_utils.get_scene_unit_scale() - if not bfu_utils.get_scene_unit_scale_is_close(0.01): - MyError = PotentialErrors.add() - MyError.name = bpy.context.scene.name - MyError.type = 1 - MyError.text = ('Scene "'+bpy.context.scene.name + '" has a UnitScale egal to ' + str(unit_scale)) - MyError.text += ('\nFor Unreal unit scale equal to 0.01 is recommended.') - MyError.text += ('\n(You can disable this potential error in addon_prefs)') - MyError.object = None - MyError.correctRef = "SetUnrealUnit" - MyError.correctlabel = 'Set Unreal Unit' - - def CheckSceneFrameRate(): - # Check Scene Frame Rate. - scene = bpy.context.scene - denominator = scene.render.fps_base - numerator = scene.render.fps - - # Ensure denominator and numerator are at least 1 and int 32 - new_denominator = max(round(denominator), 1) - new_numerator = max(round(numerator), 1) - - if denominator != new_denominator or numerator != new_numerator: - message = ('Frame rate denominator and numerator must be an int32 over zero.\n' - 'Float denominator and numerator is not supported in Unreal Engine Sequencer.\n' - f'- Denominator: {denominator} -> {new_denominator}\n' - f'- Numerator: {numerator} -> {new_numerator}') - - MyError = PotentialErrors.add() - MyError.name = bpy.context.scene.name - MyError.type = 2 - MyError.text = (message) - MyError.docsOcticon = 'scene-frame-rate' - - - def CheckObjType(): - # Check if objects use a non-recommended type - for obj in objToCheck: - if (obj.type == "SURFACE" or - obj.type == "META" or - obj.type == "FONT"): - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 1 - MyError.text = ( - 'Object "'+obj.name + - '" is a '+obj.type + - '. The object of the type SURFACE,' + - ' META and FONT is not recommended.') - MyError.object = obj - MyError.correctRef = "ConvertToMesh" - MyError.correctlabel = 'Convert to mesh' - - def CheckShapeKeys(): - for obj in MeshTypeToCheck: - if obj.data.shape_keys is not None: - # Check that no modifiers is destructive for the key shapes - if len(obj.data.shape_keys.key_blocks) > 0: - for modif in obj.modifiers: - if modif.type != "ARMATURE": - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 2 - MyError.object = obj - MyError.itemName = modif.name - MyError.text = ( - 'In object "'+obj.name + - '" the modifier '+modif.type + - ' named "'+modif.name + - '" can destroy shape keys.' + - ' Please use only Armature modifier' + - ' with shape keys.') - MyError.correctRef = "RemoveModfier" - MyError.correctlabel = 'Remove modifier' - - # Check that the key shapes are not out of bounds for Unreal - for key in obj.data.shape_keys.key_blocks: - # Min - if key.slider_min < -5: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 1 - MyError.object = obj - MyError.itemName = key.name - MyError.text = ( - 'In object "'+obj.name + - '" the shape key "'+key.name + - '" is out of bounds for Unreal.' + - ' The min range of must not be inferior to -5.') - MyError.correctRef = "SetKeyRangeMin" - MyError.correctlabel = 'Set min range to -5' - - # Max - if key.slider_max > 5: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 1 - MyError.object = obj - MyError.itemName = key.name - MyError.text = ( - 'In object "'+obj.name + - '" the shape key "'+key.name + - '" is out of bounds for Unreal.' + - ' The max range of must not be superior to 5.') - MyError.correctRef = "SetKeyRangeMax" - MyError.correctlabel = 'Set max range to -5' - - def CheckUVMaps(): - # Check that the objects have at least one UV map valid - for obj in MeshTypeWithoutCol: - if len(obj.data.uv_layers) < 1: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 1 - MyError.text = ( - 'Object "'+obj.name + - '" does not have any UV Layer.') - MyError.object = obj - MyError.correctRef = "CreateUV" - MyError.correctlabel = 'Create Smart UV Project' - - def CheckBadStaicMeshExportedLikeSkeletalMesh(): - # Check if the correct object is defined as exportable - for obj in MeshTypeToCheck: - for modif in obj.modifiers: - if modif.type == "ARMATURE": - if obj.bfu_export_type == "export_recursive": - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 1 - MyError.text = ( - 'In object "'+obj.name + - '" the modifier '+modif.type + - ' named "'+modif.name + - '" will not be applied when exported' + - ' with StaticMesh assets.\nNote: with armature' + - ' if you want export objets as skeletal mesh you' + - ' need set only the armature as' + - ' export_recursive not the childs') - MyError.object = obj - - def CheckArmatureScale(): - # Check if the ARMATURE use the same value on all scale axes - for obj in objToCheck: - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - if obj.scale.z != obj.scale.y or obj.scale.z != obj.scale.x: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 2 - MyError.text = ( - 'In object "'+obj.name + - '" do not use the same value on all scale axes ') - MyError.text += ( - '\nScale x:' + - str(obj.scale.x)+' y:'+str(obj.scale.y) + - ' z:'+str(obj.scale.z)) - MyError.object = obj - - def CheckArmatureNumber(): - # check Modifier or Constraint ARMATURE number = 1 - for obj in objToCheck: - meshs = GetSkeletonMeshs(obj) - for mesh in meshs: - # Count - armature_modifiers = 0 - armature_constraint = 0 - for mod in mesh.modifiers: - if mod.type == "ARMATURE": - armature_modifiers += 1 - for const in mesh.constraints: - if const.type == "ARMATURE": - armature_constraint += 1 - - # Check result > 1 - if armature_modifiers + armature_constraint > 1: - MyError = PotentialErrors.add() - MyError.name = mesh.name - MyError.type = 2 - MyError.text = ( - 'In object "'+mesh.name + '" ' + - str(armature_modifiers) + ' Armature modifier(s) and ' + - str(armature_modifiers) + ' Armature constraint(s) was found. ' + - ' Please use only one Armature modifier or one Armature constraint.') - MyError.object = mesh - - # Check result == 0 - if armature_modifiers + armature_constraint == 0: - MyError = PotentialErrors.add() - MyError.name = mesh.name - MyError.type = 2 - MyError.text = ( - 'In object "'+mesh.name + '" ' + - ' no Armature modifiers or constraints was found. ' + - ' Please use only one Armature modifier or one Armature constraint.') - MyError.object = mesh - - def CheckArmatureModData(): - # check the parameter of Modifier ARMATURE - for obj in MeshTypeToCheck: - for mod in obj.modifiers: - if mod.type == "ARMATURE": - if mod.use_deform_preserve_volume: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 2 - MyError.text = ( - 'In object "'+obj.name + - '" the modifier '+mod.type + - ' named "'+mod.name + - '". The parameter Preserve Volume' + - ' must be set to False.') - MyError.object = obj - MyError.itemName = mod.name - MyError.correctRef = "PreserveVolume" - MyError.correctlabel = 'Set Preserve Volume to False' - - def CheckArmatureConstData(): - # check the parameter of constraint ARMATURE - for obj in MeshTypeToCheck: - for const in obj.constraints: - if const.type == "ARMATURE": - pass - # TO DO. - - def CheckArmatureBoneData(): - # check the parameter of the ARMATURE bones - for obj in objToCheck: - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - for bone in obj.data.bones: - if (not obj.bfu_export_deform_only or - (bone.use_deform and obj.bfu_export_deform_only)): - - if bone.bbone_segments > 1: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 1 - MyError.text = ( - 'In object3 "'+obj.name + - '" the bone named "'+bone.name + - '". The parameter Bendy Bones / Segments' + - ' must be set to 1.') - MyError.text += ( - '\nBendy bones are not supported by' + - ' Unreal Engine, so that better to disable' + - ' it if you want the same animation preview' + - ' in Unreal and blender.') - MyError.object = obj - MyError.itemName = bone.name - MyError.selectPoseBoneButton = True - MyError.correctRef = "BoneSegments" - MyError.correctlabel = 'Set Bone Segments to 1' - MyError.docsOcticon = 'bendy-bone' - - def CheckArmatureValidChild(): - # Check that skeleton also has a mesh to export - - for obj in objToCheck: - export_as_proxy = bfu_utils.GetExportAsProxy(obj) - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - childs = bfu_utils.GetExportDesiredChilds(obj) - validChild = 0 - for child in childs: - if child.type == "MESH": - validChild += 1 - if export_as_proxy: - if bfu_utils.GetExportProxyChild(obj) is not None: - validChild += 1 - if validChild < 1: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 2 - MyError.text = ( - 'Object "'+obj.name + - '" is an Armature and does not have' + - ' any valid children.') - MyError.object = obj - - def CheckArmatureChildWithBoneParent(): - # If you use Parent Bone to parent your mesh to your armature the import will fail. - for obj in objToCheck: - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - childs = bfu_utils.GetExportDesiredChilds(obj) - for child in childs: - if child.type == "MESH": - if child.parent_type == 'BONE': - MyError = PotentialErrors.add() - MyError.name = child.name - MyError.type = 2 - MyError.text = ( - 'Object "'+child.name + - '" use Parent Bone to parent. ' + - '\n If you use Parent Bone to parent your mesh to your armature the import will fail.') - MyError.object = child - MyError.docsOcticon = 'armature-child-with-bone-parent' - - def CheckArmatureMultipleRoots(): - # Check that skeleton have multiples roots - for obj in objToCheck: - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - rootBones = bfu_utils.GetArmatureRootBones(obj) - - if len(rootBones) > 1: - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 1 - MyError.text = ( - 'Object "'+obj.name + - '" have Multiple roots bones.' + - ' Unreal only support single root bone') - MyError.text += '\nA custom root bone will be added at the export.' - MyError.text += ' '+str(len(rootBones))+' root bones found: ' - MyError.text += '\n' - for rootBone in rootBones: - MyError.text += rootBone.name+', ' - MyError.object = obj - - def CheckArmatureNoDeformBone(): - # Check that skeleton have at less one deform bone - for obj in objToCheck: - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - if obj.bfu_export_deform_only: - for bone in obj.data.bones: - if bone.use_deform: - return - MyError = PotentialErrors.add() - MyError.name = obj.name - MyError.type = 2 - MyError.text = ( - 'Object "'+obj.name + - '" don\'t have any deform bones.' + - ' Unreal will import it like a StaticMesh.') - MyError.object = obj - - def CheckMarkerOverlay(): - # Check that there is no overlap with the Marker - usedFrame = [] - for marker in bpy.context.scene.timeline_markers: - if marker.frame in usedFrame: - MyError = PotentialErrors.add() - MyError.type = 2 - MyError.text = ( - 'In the scene timeline the frame "' + - str(marker.frame)+'" contains overlaped Markers' + - '\n To avoid camera conflict in the generation' + - ' of sequencer you must use max one marker per frame.') - else: - usedFrame.append(marker.frame) - - def CheckVertexGroupWeight(): - # Check that all vertex have a weight - for obj in objToCheck: - meshs = GetSkeletonMeshs(obj) - for mesh in meshs: - if mesh.type == "MESH": - if ContainsArmatureModifier(mesh): - # Result data - VertexWithZeroWeight = GetVertexWithZeroWeight( - obj, - mesh) - if len(VertexWithZeroWeight) > 0: - MyError = PotentialErrors.add() - MyError.name = mesh.name - MyError.type = 1 - MyError.text = ( - 'Object named "'+mesh.name + - '" contains '+str(len(VertexWithZeroWeight)) + - ' vertex with zero cumulative valid weight.') - MyError.text += ( - '\nNote: Vertex groups must have' + - ' a bone with the same name to be valid.') - MyError.object = mesh - MyError.selectVertexButton = True - MyError.selectOption = "VertexWithZeroWeight" - - def CheckZeroScaleKeyframe(): - # Check that animations do not use a invalid value - for obj in objToCheck: - if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) - animation_to_export = animation_asset_cache.GetAnimationAssetList() - for action in animation_to_export: - for fcurve in action.fcurves: - if fcurve.data_path.split(".")[-1] == "scale": - for key in fcurve.keyframe_points: - xCurve, yCurve = key.co - if key.co[1] == 0: - MyError = PotentialErrors.add() - MyError.type = 2 - MyError.text = ( - 'In action "'+action.name + - '" at frame '+str(key.co[0]) + - ', the bone named "' + - fcurve.data_path.split('"')[1] + - '" has a zero value in scale' + - ' transform. ' + - 'This is invalid in Unreal.') - - CheckUnitScale() - CheckSceneFrameRate() - CheckObjType() - CheckShapeKeys() - CheckUVMaps() - CheckBadStaicMeshExportedLikeSkeletalMesh() - CheckArmatureScale() - CheckArmatureNumber() - CheckArmatureModData() - CheckArmatureConstData() - CheckArmatureBoneData() - CheckArmatureValidChild() - CheckArmatureMultipleRoots() - CheckArmatureChildWithBoneParent() - CheckArmatureNoDeformBone() - CheckMarkerOverlay() - CheckVertexGroupWeight() - CheckZeroScaleKeyframe() - - return PotentialErrors - - -def SelectPotentialErrorObject(errorIndex): - # Select potential error - - bbpl.utils.safe_mode_set('OBJECT', bpy.context.active_object) - scene = bpy.context.scene - error = scene.potentialErrorList[errorIndex] - obj = error.object - - bpy.ops.object.select_all(action='DESELECT') - obj.hide_viewport = False - obj.hide_set(False) - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - - # show collection for select object - for collection in bpy.data.collections: - for ColObj in collection.objects: - if ColObj == obj: - bfu_basics.SetCollectionUse(collection) - bpy.ops.view3d.view_selected() - return obj - - -def SelectPotentialErrorVertex(errorIndex): - # Select potential error - SelectPotentialErrorObject(errorIndex) - bbpl.utils.safe_mode_set('EDIT') - - scene = bpy.context.scene - error = scene.potentialErrorList[errorIndex] - obj = error.object - bpy.ops.mesh.select_mode(type="VERT") - bpy.ops.mesh.select_all(action='DESELECT') - - bbpl.utils.safe_mode_set('OBJECT') - if error.selectOption == "VertexWithZeroWeight": - for vertex in GetVertexWithZeroWeight(obj.parent, obj): - vertex.select = True - bbpl.utils.safe_mode_set('EDIT') - bpy.ops.view3d.view_selected() - return obj - - -def SelectPotentialErrorPoseBone(errorIndex): - # Select potential error - SelectPotentialErrorObject(errorIndex) - bbpl.utils.safe_mode_set('POSE') - - scene = bpy.context.scene - error = scene.potentialErrorList[errorIndex] - obj = error.object - bone = obj.data.bones[error.itemName] - - # Make bone visible if hide in a layer - for x, layer in enumerate(bone.layers): - if not obj.data.layers[x] and layer: - obj.data.layers[x] = True - - bpy.ops.pose.select_all(action='DESELECT') - obj.data.bones.active = bone - bone.select = True - - bpy.ops.view3d.view_selected() - return obj - - -def TryToCorrectPotentialError(errorIndex): - # Try to correct potential error - - scene = bpy.context.scene - error = scene.potentialErrorList[errorIndex] - global successCorrect - successCorrect = False - - local_view_areas = bbpl.scene_utils.move_to_global_view() - - MyCurrentDataSave = bbpl.utils.UserSceneSave() - MyCurrentDataSave.save_current_scene() - - bbpl.utils.safe_mode_set('OBJECT', MyCurrentDataSave.user_select_class.user_active) - - print("Start correct") - - def SelectObj(obj): - bpy.ops.object.select_all(action='DESELECT') - obj.select_set(True) - bpy.context.view_layer.objects.active = obj - - # Correction list - - if error.correctRef == "SetUnrealUnit": - bpy.context.scene.unit_settings.scale_length = 0.01 - successCorrect = True - - if error.correctRef == "ConvertToMesh": - obj = error.object - SelectObj(obj) - bpy.ops.object.convert(target='MESH') - successCorrect = True - - if error.correctRef == "SetKeyRangeMin": - obj = error.object - key = obj.data.shape_keys.key_blocks[error.itemName] - key.slider_min = -5 - successCorrect = True - - if error.correctRef == "SetKeyRangeMax": - obj = error.object - key = obj.data.shape_keys.key_blocks[error.itemName] - key.slider_max = 5 - successCorrect = True - - if error.correctRef == "CreateUV": - obj = error.object - SelectObj(obj) - if bbpl.utils.safe_mode_set("EDIT", obj): - bpy.ops.uv.smart_project() - successCorrect = True - else: - successCorrect = False - - if error.correctRef == "RemoveModfier": - obj = error.object - mod = obj.modifiers[error.itemName] - obj.modifiers.remove(mod) - successCorrect = True - - if error.correctRef == "PreserveVolume": - obj = error.object - mod = obj.modifiers[error.itemName] - mod.use_deform_preserve_volume = False - successCorrect = True - - if error.correctRef == "BoneSegments": - obj = error.object - bone = obj.data.bones[error.itemName] - bone.bbone_segments = 1 - successCorrect = True - - if error.correctRef == "InheritScale": - obj = error.object - bone = obj.data.bones[error.itemName] - bone.use_inherit_scale = True - successCorrect = True - - # ----------------------------------------Reset data - MyCurrentDataSave.reset_select_by_name() - MyCurrentDataSave.reset_scene_at_save() - bbpl.scene_utils.move_to_local_view(local_view_areas) - - # ---------------------------------------- - - if successCorrect: - scene.potentialErrorList.remove(errorIndex) - print("end correct, Error: " + error.correctRef) - return "Corrected" - print("end correct, Error not found") - return "Correct fail" - - -class BFU_OT_UnrealPotentialError(bpy.types.PropertyGroup): - type: bpy.props.IntProperty(default=0) # 0:Info, 1:Warning, 2:Error - object: bpy.props.PointerProperty(type=bpy.types.Object) - ### - selectObjectButton: bpy.props.BoolProperty(default=True) - selectVertexButton: bpy.props.BoolProperty(default=False) - selectPoseBoneButton: bpy.props.BoolProperty(default=False) - ### - selectOption: bpy.props.StringProperty(default="None") # 0:VertexWithZeroWeight - itemName: bpy.props.StringProperty(default="None") - text: bpy.props.StringProperty(default="Unknown") - correctRef: bpy.props.StringProperty(default="None") - correctlabel: bpy.props.StringProperty(default="Fix it !") - correctDesc: bpy.props.StringProperty(default="Correct target error") - docsOcticon: bpy.props.StringProperty(default="None") - - -classes = ( - BFU_OT_UnrealPotentialError, -) - - -def register(): - for cls in classes: - bpy.utils.register_class(cls) - - bpy.types.Scene.potentialErrorList = bpy.props.CollectionProperty(type=BFU_OT_UnrealPotentialError) - - -def unregister(): - for cls in reversed(classes): - bpy.utils.unregister_class(cls) - - del bpy.types.Scene.potentialErrorList diff --git a/blender-for-unrealengine/bfu_check_potential_error/__init__.py b/blender-for-unrealengine/bfu_check_potential_error/__init__.py new file mode 100644 index 00000000..8291b872 --- /dev/null +++ b/blender-for-unrealengine/bfu_check_potential_error/__init__.py @@ -0,0 +1,52 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_check_props +from . import bfu_check_operators +from . import bfu_check_ui +from . import bfu_check_utils + +if "bfu_check_props" in locals(): + importlib.reload(bfu_check_props) +if "bfu_check_operators" in locals(): + importlib.reload(bfu_check_operators) +if "bfu_check_ui" in locals(): + importlib.reload(bfu_check_ui) +if "bfu_check_utils" in locals(): + importlib.reload(bfu_check_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_check_props.register() + bfu_check_operators.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_check_operators.unregister() + bfu_check_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_check_potential_error/bfu_check_operators.py b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_operators.py new file mode 100644 index 00000000..ac7b9de4 --- /dev/null +++ b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_operators.py @@ -0,0 +1,278 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_cached_asset_list +from .. import bfu_check_potential_error + + +class BFU_OT_ShowAssetToExport(bpy.types.Operator): + bl_label = "Show asset(s)" + bl_idname = "object.showasset" + bl_description = "Click to show assets that are to be exported." + + def execute(self, context): + + obj = context.object + if obj: + if obj.type == "ARMATURE": + animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) + animation_asset_cache.UpdateActionCache() + + + final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() + final_asset_list_to_export = final_asset_cache.GetFinalAssetList() + popup_title = "Assets list" + if len(final_asset_list_to_export) > 0: + popup_title = str(len(final_asset_list_to_export))+' asset(s) will be exported.' + else: + popup_title = 'No exportable assets were found.' + + def draw(self, context: bpy.types.Context): + col = self.layout.column() + for asset in final_asset_list_to_export: + asset :bfu_cached_asset_list.AssetToExport + row = col.row() + if asset.obj is not None: + if asset.action is not None: + if (type(asset.action) is bpy.types.Action): + # Action name + action = asset.action.name + elif (type(asset.action) is bpy.types.AnimData): + # Nonlinear name + action = asset.obj.bfu_anim_nla_export_name + else: + action = "..." + row.label( + text="- ["+asset.name+"] --> " + + action+" ("+asset.asset_type+")") + else: + if asset.asset_type != "Collection StaticMesh": + row.label( + text="- "+asset.name + + " ("+asset.asset_type+")") + else: + row.label( + text="- "+asset.obj.name + + " ("+asset.asset_type+")") + + else: + row.label(text="- ("+asset.asset_type+")") + bpy.context.window_manager.popup_menu( + draw, + title=popup_title, + icon='PACKAGE') + return {'FINISHED'} + +class BFU_OT_CheckPotentialErrorPopup(bpy.types.Operator): + bl_label = "Check Potential Errors" + bl_idname = "object.checkpotentialerror" + bl_description = "Check potential errors." + text = "none" + + def execute(self, context): + fix_info = bfu_check_potential_error.bfu_check_utils.process_general_fix() + invoke_info = "" + for x, fix_info_key in enumerate(fix_info): + fix_info_data = fix_info[fix_info_key] + invoke_info += fix_info_key + ": " + str(fix_info_data) + if x < len(fix_info)-1: + invoke_info += "\n" + + + bfu_check_potential_error.bfu_check_utils.update_unreal_potential_error() + bpy.ops.object.openpotentialerror( + "INVOKE_DEFAULT", + invoke_info=invoke_info, + ) + return {'FINISHED'} + +class BFU_OT_OpenPotentialErrorPopup(bpy.types.Operator): + bl_label = "Open potential errors" + bl_idname = "object.openpotentialerror" + bl_description = "Open potential errors" + invoke_info: bpy.props.StringProperty(default="...") + + class BFU_OT_FixitTarget(bpy.types.Operator): + bl_label = "Fix it !" + bl_idname = "object.fixit_objet" + bl_description = "Correct target error" + errorIndex: bpy.props.IntProperty(default=-1) + + def execute(self, context): + result = bfu_check_potential_error.bfu_check_utils.TryToCorrectPotentialError(self.errorIndex) + self.report({'INFO'}, result) + return {'FINISHED'} + + class BFU_OT_SelectObjectButton(bpy.types.Operator): + bl_label = "Select(Object)" + bl_idname = "object.select_error_objet" + bl_description = "Select target Object." + errorIndex: bpy.props.IntProperty(default=-1) + + def execute(self, context): + bfu_check_potential_error.bfu_check_utils.select_potential_error_object(self.errorIndex) + return {'FINISHED'} + + class BFU_OT_SelectVertexButton(bpy.types.Operator): + bl_label = "Select(Vertex)" + bl_idname = "object.select_error_vertex" + bl_description = "Select target Vertex." + errorIndex: bpy.props.IntProperty(default=-1) + + def execute(self, context): + bfu_check_potential_error.bfu_check_utils.SelectPotentialErrorVertex(self.errorIndex) + return {'FINISHED'} + + class BFU_OT_SelectPoseBoneButton(bpy.types.Operator): + bl_label = "Select(PoseBone)" + bl_idname = "object.select_error_posebone" + bl_description = "Select target Pose Bone." + errorIndex: bpy.props.IntProperty(default=-1) + + def execute(self, context): + bfu_check_potential_error.bfu_check_utils.SelectPotentialErrorPoseBone(self.errorIndex) + return {'FINISHED'} + + class BFU_OT_OpenPotentialErrorDocs(bpy.types.Operator): + bl_label = "Open docs" + bl_idname = "object.open_potential_error_docs" + bl_description = "Open potential error docs." + octicon: bpy.props.StringProperty(default="") + + def execute(self, context): + os.system( + "start \"\" " + + "https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/How-avoid-potential-errors" + + "#"+self.octicon) + return {'FINISHED'} + + def execute(self, context): + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_popup(self, width=1020) + + def check(self, context): + return True + + def draw(self, context: bpy.types.Context): + + layout = self.layout + if len(bpy.context.scene.potentialErrorList) > 0: + popup_title = ( + str(len(bpy.context.scene.potentialErrorList)) + + " potential error(s) found!") + else: + popup_title = "No potential error to correct!" + + + layout.label(text=popup_title) + invoke_info_lines = self.invoke_info.split("\n") + for invoke_info_line in invoke_info_lines: + layout.label(text="- "+invoke_info_line) + + layout.separator() + row = layout.row() + col = row.column() + for x in range(len(bpy.context.scene.potentialErrorList)): + error = bpy.context.scene.potentialErrorList[x] + + myLine = col.box().split(factor=0.85) + # ---- + if error.type == 0: + msgType = 'INFO' + msgIcon = 'INFO' + elif error.type == 1: + msgType = 'WARNING' + msgIcon = 'ERROR' + elif error.type == 2: + msgType = 'ERROR' + msgIcon = 'CANCEL' + # ---- + + # Text + TextLine = myLine.column() + errorFullMsg = msgType+": "+error.text + splitedText = errorFullMsg.split("\n") + + for text, Line in enumerate(splitedText): + if (text < 1): + + FisrtTextLine = TextLine.row() + if (error.docsOcticon != "None"): # Doc button + props = FisrtTextLine.operator( + "object.open_potential_error_docs", + icon="HELP", + text="") + props.octicon = error.docsOcticon + + FisrtTextLine.label(text=Line, icon=msgIcon) + else: + TextLine.label(text=Line) + + # Select and fix button + ButtonLine = myLine.column() + if (error.correctRef != "None"): + props = ButtonLine.operator( + "object.fixit_objet", + text=error.correctlabel) + props.errorIndex = x + if (error.object is not None): + if (error.selectObjectButton): + props = ButtonLine.operator( + "object.select_error_objet") + props.errorIndex = x + if (error.selectVertexButton): + props = ButtonLine.operator( + "object.select_error_vertex") + props.errorIndex = x + if (error.selectPoseBoneButton): + props = ButtonLine.operator( + "object.select_error_posebone") + props.errorIndex = x + + + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_OT_ShowAssetToExport, + BFU_OT_CheckPotentialErrorPopup, + BFU_OT_OpenPotentialErrorPopup, + BFU_OT_OpenPotentialErrorPopup.BFU_OT_FixitTarget, + BFU_OT_OpenPotentialErrorPopup.BFU_OT_SelectObjectButton, + BFU_OT_OpenPotentialErrorPopup.BFU_OT_SelectVertexButton, + BFU_OT_OpenPotentialErrorPopup.BFU_OT_SelectPoseBoneButton, + BFU_OT_OpenPotentialErrorPopup.BFU_OT_OpenPotentialErrorDocs, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + diff --git a/blender-for-unrealengine/bfu_check_potential_error/bfu_check_props.py b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_props.py new file mode 100644 index 00000000..7aafb916 --- /dev/null +++ b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_props.py @@ -0,0 +1,67 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from . import bfu_check_operators + + + + +def get_preset_values(): + preset_values = [ + ] + return preset_values + +class BFU_OT_UnrealPotentialError(bpy.types.PropertyGroup): + type: bpy.props.IntProperty(default=0) # 0:Info, 1:Warning, 2:Error + object: bpy.props.PointerProperty(type=bpy.types.Object) + ### + selectObjectButton: bpy.props.BoolProperty(default=True) + selectVertexButton: bpy.props.BoolProperty(default=False) + selectPoseBoneButton: bpy.props.BoolProperty(default=False) + ### + selectOption: bpy.props.StringProperty(default="None") # 0:VertexWithZeroWeight + itemName: bpy.props.StringProperty(default="None") + text: bpy.props.StringProperty(default="Unknown") + correctRef: bpy.props.StringProperty(default="None") + correctlabel: bpy.props.StringProperty(default="Fix it !") + correctDesc: bpy.props.StringProperty(default="Correct target error") + docsOcticon: bpy.props.StringProperty(default="None") + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_OT_UnrealPotentialError, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.potentialErrorList = bpy.props.CollectionProperty(type=BFU_OT_UnrealPotentialError) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.potentialErrorList diff --git a/blender-for-unrealengine/bfu_check_potential_error/bfu_check_ui.py b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_ui.py new file mode 100644 index 00000000..22370baa --- /dev/null +++ b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_ui.py @@ -0,0 +1,25 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy + + + +def draw_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + pass \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_check_potential_error/bfu_check_utils.py b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_utils.py new file mode 100644 index 00000000..dec12f8d --- /dev/null +++ b/blender-for-unrealengine/bfu_check_potential_error/bfu_check_utils.py @@ -0,0 +1,694 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +import fnmatch +import math + +from .. import bbpl +from .. import bfu_basics +from .. import bfu_assets_manager +from .. import bfu_utils +from .. import bfu_cached_asset_list + +from .. import bfu_collision +from .. import bfu_socket +from .. import bfu_camera +from .. import bfu_alembic_animation +from .. import bfu_groom +from .. import bfu_spline +from .. import bfu_skeletal_mesh +from .. import bfu_static_mesh + + + + +def process_general_fix(): + fixed_collisions = bfu_collision.bfu_collision_utils.fix_export_type_on_collision() + fixed_collision_names = bfu_collision.bfu_collision_utils.fix_name_on_collision() + fixed_sockets = bfu_socket.bfu_socket_utils.fix_export_type_on_socket() + fixed_socket_names = bfu_socket.bfu_socket_utils.fix_name_on_socket() + + fix_info = { + "Fixed Collision(s)": fixed_collisions, + "Fixed Collision Names(s)": fixed_collision_names, + "Fixed Socket(s)": fixed_sockets, + "Fixed Socket Names(s)": fixed_socket_names, + } + + return fix_info + + + + +def GetVertexWithZeroWeight(Armature, Mesh): + vertices = [] + + # Créez un ensemble des noms des os de l'armature pour une recherche plus rapide + armature_bone_names = set(bone.name for bone in Armature.data.bones) + + + for vertex in Mesh.data.vertices: #MeshVertex(bpy_struct) + cumulateWeight = 0 + + if vertex.groups: + for group_elem in vertex.groups: #VertexGroupElement(bpy_struct) + if group_elem.weight > 0: + group_index = group_elem.group + group_len = len(Mesh.vertex_groups) + if group_index <= group_len: + group = Mesh.vertex_groups[group_elem.group] + + # Utilisez l'ensemble des noms d'os pour vérifier l'appartenance à l'armature + if group.name in armature_bone_names: + cumulateWeight += group_elem.weight + + if cumulateWeight == 0: + vertices.append(vertex) + + return vertices + + +def ContainsArmatureModifier(obj): + for mod in obj.modifiers: + if mod.type == "ARMATURE": + return True + return False + +def GetSkeletonMeshs(obj): + meshs = [] + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + childs = bfu_utils.GetExportDesiredChilds(obj) + for child in childs: + if child.type == "MESH": + meshs.append(child) + return meshs + + +def update_unreal_potential_error(): + # Find and reset list of all potential error in scene + + addon_prefs = bfu_basics.GetAddonPrefs() + potential_errors = bpy.context.scene.potentialErrorList + potential_errors.clear() + + # prepares the data to avoid unnecessary loops + obj_to_check = [] + final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() + final_asset_list_to_export = final_asset_cache.GetFinalAssetList() + for Asset in final_asset_list_to_export: + if Asset.obj in bfu_utils.GetAllobjectsByExportType("export_recursive"): + if Asset.obj not in obj_to_check: + obj_to_check.append(Asset.obj) + for child in bfu_utils.GetExportDesiredChilds(Asset.obj): + if child not in obj_to_check: + obj_to_check.append(child) + + mesh_type_to_check = [] + for obj in obj_to_check: + if obj.type == 'MESH': + mesh_type_to_check.append(obj) + + mesh_type_without_col = [] # is Mesh Type To Check Without Collision + for obj in mesh_type_to_check: + if not bfu_utils.CheckIsCollision(obj): + mesh_type_without_col.append(obj) + + def check_unit_scale(): + # Check if the unit scale is equal to 0.01. + if addon_prefs.notifyUnitScalePotentialError: + if not bfu_utils.get_scene_unit_scale_is_close(0.01): + str_unit_scale = str(bfu_utils.get_scene_unit_scale()) + my_po_error = potential_errors.add() + my_po_error.name = bpy.context.scene.name + my_po_error.type = 1 + my_po_error.text = (f'Scene "{bpy.context.scene.name}" has a Unit Scale equal to {str_unit_scale}.') + my_po_error.text += ('\nFor Unreal, a unit scale equal to 0.01 is recommended.') + my_po_error.text += ('\n(You can disable this potential error in the addon preferences.)') + my_po_error.object = None + my_po_error.correctRef = "SetUnrealUnit" + my_po_error.correctlabel = 'Set Unreal Unit' + + def check_scene_frame_rate(): + # Check Scene Frame Rate. + scene = bpy.context.scene + denominator = scene.render.fps_base + numerator = scene.render.fps + + # Ensure denominator and numerator are at least 1 and int 32 + new_denominator = max(round(denominator), 1) + new_numerator = max(round(numerator), 1) + + if denominator != new_denominator or numerator != new_numerator: + message = ('Frame rate denominator and numerator must be an int32 over zero.\n' + 'Float denominator and numerator is not supported in Unreal Engine Sequencer.\n' + f'- Denominator: {denominator} -> {new_denominator}\n' + f'- Numerator: {numerator} -> {new_numerator}') + + my_po_error = potential_errors.add() + my_po_error.name = bpy.context.scene.name + my_po_error.type = 2 + my_po_error.text = (message) + my_po_error.docsOcticon = 'scene-frame-rate' + + + def check_obj_type(): + # Check if objects use a non-recommended type + + non_recommended_types = {"SURFACE", "META", "FONT"} + for obj in obj_to_check: + if obj.type in non_recommended_types: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 1 + my_po_error.text = ( + f'Object "{obj.name}" is a {obj.type}. The object of the type ' + 'SURFACE, META, and FONT is not recommended.' + ) + my_po_error.object = obj + my_po_error.correctRef = "ConvertToMesh" + my_po_error.correctlabel = 'Convert to mesh' + + def check_shape_keys(): + destructive_modifiers = {"ARMATURE"} + + for obj in mesh_type_to_check: + shape_keys = obj.data.shape_keys + if shape_keys is not None and len(shape_keys.key_blocks) > 0: + # Check that no modifiers is destructive for the key shapes + for modif in obj.modifiers: + if modif.type in destructive_modifiers: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 2 + my_po_error.object = obj + my_po_error.itemName = modif.name + my_po_error.text = ( + f'In object "{obj.name}", the modifier "{modif.type}" ' + f'named "{modif.name}" can destroy shape keys. ' + 'Please use only the Armature modifier with shape keys.' + ) + my_po_error.correctRef = "RemoveModifier" + my_po_error.correctlabel = 'Remove modifier' + + # Check shape key ranges for Unreal Engine compatibility + unreal_engine_shape_key_max = 5 + unreal_engine_shape_key_min = -5 + for key in shape_keys.key_blocks: + # Min check + if key.slider_min < unreal_engine_shape_key_min: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 1 + my_po_error.object = obj + my_po_error.itemName = key.name + my_po_error.text = ( + f'In object "{obj.name}", the shape key "{key.name}" ' + 'is out of bounds for Unreal. The minimum range must not be less than {unreal_engine_shape_key_min}.' + ) + my_po_error.correctRef = "SetKeyRangeMin" + my_po_error.correctlabel = f'Set min range to {unreal_engine_shape_key_min}' + + # Max check + if key.slider_max > unreal_engine_shape_key_max: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 1 + my_po_error.object = obj + my_po_error.itemName = key.name + my_po_error.text = ( + f'In object "{obj.name}", the shape key "{key.name}" ' + 'is out of bounds for Unreal. The maximum range must not exceed {unreal_engine_shape_key_max}.' + ) + my_po_error.correctRef = "SetKeyRangeMax" + my_po_error.correctlabel = f'Set max range to {unreal_engine_shape_key_max}' + + def check_uv_maps(): + # Check that the objects have at least one UV map valid + for obj in mesh_type_without_col: + if len(obj.data.uv_layers) < 1: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 1 + my_po_error.text = (f'Object "{obj.name}" does not have any UV Layer.') + my_po_error.object = obj + my_po_error.correctRef = "CreateUV" + my_po_error.correctlabel = 'Create Smart UV Project' + + def check_bad_static_mesh_exported_like_skeletal_mesh(): + # Check if the correct object is defined as exportable + for obj in mesh_type_to_check: + for modif in obj.modifiers: + if modif.type == "ARMATURE" and obj.bfu_export_type == "export_recursive": + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 1 + my_po_error.text = ( + f'In object "{obj.name}", the modifier "{modif.type}" ' + f'named "{modif.name}" will not be applied when exported ' + 'with StaticMesh assets.\nNote: with armature, if you want to export ' + 'objects as skeletal mesh, you need to set only the armature as ' + 'export_recursive, not the child objects.' + ) + my_po_error.object = obj + + def check_armature_scale(): + # Check if the ARMATURE use the same value on all scale axes + for obj in obj_to_check: + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + if obj.scale.z != obj.scale.y or obj.scale.z != obj.scale.x: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 2 + my_po_error.text = ( + f'In object "{obj.name}", the scale values are not consistent across all axes.' + ) + my_po_error.text += ( + f'\nScale x: {obj.scale.x}, y: {obj.scale.y}, z: {obj.scale.z}' + ) + my_po_error.object = obj + + def check_armature_number(): + # Check if the number of ARMATURE modifiers or constraints is exactly 1 + for obj in obj_to_check: + meshs = GetSkeletonMeshs(obj) + for mesh in meshs: + # Count the number of ARMATURE modifiers and constraints + armature_modifiers = sum(1 for mod in mesh.modifiers if mod.type == "ARMATURE") + armature_constraints = sum(1 for const in mesh.constraints if const.type == "ARMATURE") + + # Check if the total number of ARMATURE modifiers and constraints is greater than 1 + if armature_modifiers + armature_constraints > 1: + my_po_error = potential_errors.add() + my_po_error.name = mesh.name + my_po_error.type = 2 + my_po_error.text = ( + f'In object "{mesh.name}", {armature_modifiers} Armature modifier(s) and ' + f'{armature_constraints} Armature constraint(s) were found. ' + 'Please use only one Armature modifier or one Armature constraint.' + ) + my_po_error.object = mesh + + # Check if no ARMATURE modifiers or constraints are found + if armature_modifiers + armature_constraints == 0: + my_po_error = potential_errors.add() + my_po_error.name = mesh.name + my_po_error.type = 2 + my_po_error.text = ( + f'In object "{mesh.name}", no Armature modifiers or constraints were found. ' + 'Please use one Armature modifier or one Armature constraint.' + ) + my_po_error.object = mesh + + def check_armature_mod_data(): + # Check the parameters of ARMATURE modifiers + for obj in mesh_type_to_check: + for mod in obj.modifiers: + if mod.type == "ARMATURE": + if mod.use_deform_preserve_volume: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 2 + my_po_error.text = ( + f'In object "{obj.name}", the ARMATURE modifier ' + f'named "{mod.name}" has the Preserve Volume parameter set to True. ' + 'This parameter must be set to False.' + ) + my_po_error.object = obj + my_po_error.itemName = mod.name + my_po_error.correctRef = "PreserveVolume" + my_po_error.correctlabel = 'Set Preserve Volume to False' + + def check_armature_const_data(): + # Check the parameters of ARMATURE constraints + for obj in mesh_type_to_check: + for const in obj.constraints: + if const.type == "ARMATURE": + # TO DO. + pass + + def check_armature_bone_data(): + # Check the parameters of the ARMATURE bones + for obj in obj_to_check: + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + for bone in obj.data.bones: + if (not obj.bfu_export_deform_only or + (bone.use_deform and obj.bfu_export_deform_only)): + + if bone.bbone_segments > 1: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 1 + my_po_error.text = ( + f'In object "{obj.name}", the bone named "{bone.name}" ' + 'has the Bendy Bones / Segments parameter set to more than 1. ' + 'This parameter must be set to 1.' + ) + my_po_error.text += ( + '\nBendy bones are not supported by Unreal Engine, ' + 'so it is better to disable it if you want the same ' + 'animation preview in Unreal and Blender.' + ) + my_po_error.object = obj + my_po_error.itemName = bone.name + my_po_error.selectPoseBoneButton = True + my_po_error.correctRef = "BoneSegments" + my_po_error.correctlabel = 'Set Bone Segments to 1' + my_po_error.docsOcticon = 'bendy-bone' + + def check_armature_valid_child(): + # Check that the skeleton has at least one valid mesh child to export + for obj in obj_to_check: + export_as_proxy = bfu_utils.GetExportAsProxy(obj) + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + childs = bfu_utils.GetExportDesiredChilds(obj) + valid_child = sum(1 for child in childs if child.type == "MESH") + + if export_as_proxy and bfu_utils.GetExportProxyChild(obj) is not None: + valid_child += 1 + + if valid_child < 1: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 2 + my_po_error.text = ( + f'Object "{obj.name}" is an Armature and does not have ' + 'any valid children.' + ) + my_po_error.object = obj + + def check_armature_child_with_bone_parent(): + # Check if a mesh child is parented to a bone, which will cause import issues + for obj in obj_to_check: + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + childs = bfu_utils.GetExportDesiredChilds(obj) + for child in childs: + if child.type == "MESH" and child.parent_type == 'BONE': + my_po_error = potential_errors.add() + my_po_error.name = child.name + my_po_error.type = 2 + my_po_error.text = ( + f'Object "{child.name}" uses Parent Bone to parent. ' + '\nIf you use Parent Bone to parent your mesh to your armature, the import will fail.' + ) + my_po_error.object = child + my_po_error.docsOcticon = 'armature-child-with-bone-parent' + + def check_armature_multiple_roots(): + # Check if the skeleton has multiple root bones + for obj in obj_to_check: + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + root_bones = bfu_utils.get_armature_root_bones(obj) + + root_bones_str = "" + for bone in root_bones: + if bone.use_deform: + root_bones_str += bone.name + "(def), " + else: + root_bones_str += bone.name + "(def child(s)), " + + if len(root_bones) > 1: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 1 + my_po_error.text = f'Object "{obj.name}" has multiple root bones. Unreal only supports a single root bone.' + my_po_error.text += '\n' + f' {len(root_bones)} root bone(s) found: {root_bones_str}' + my_po_error.text += '\n' + 'A custom root bone will be added at export.' + my_po_error.object = obj + + def check_armature_no_deform_bone(): + # Check that the skeleton has at least one deform bone + for obj in obj_to_check: + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + if obj.bfu_export_deform_only: + has_deform_bone = any(bone.use_deform for bone in obj.data.bones) + if not has_deform_bone: + my_po_error = potential_errors.add() + my_po_error.name = obj.name + my_po_error.type = 2 + my_po_error.text = ( + f'Object "{obj.name}" does not have any deform bones. ' + 'Unreal will import it as a StaticMesh.' + ) + my_po_error.object = obj + + def check_marker_overlay(): + # Check that there is no overlap with the markers in the scene timeline + used_frames = [] + for marker in bpy.context.scene.timeline_markers: + if marker.frame in used_frames: + my_po_error = potential_errors.add() + my_po_error.type = 2 + my_po_error.text = ( + f'In the scene timeline, the frame "{marker.frame}" contains overlapping markers.' + '\nTo avoid camera conflicts in the generation of the sequencer, ' + 'you must use a maximum of one marker per frame.' + ) + else: + used_frames.append(marker.frame) + + def check_vertex_group_weight(): + # Check that all vertices have a weight + for obj in obj_to_check: + meshes = GetSkeletonMeshs(obj) + for mesh in meshes: + if mesh.type == "MESH" and ContainsArmatureModifier(mesh): + # Get vertices with zero weight + vertices_with_zero_weight = GetVertexWithZeroWeight(obj, mesh) + if vertices_with_zero_weight: + my_po_error = potential_errors.add() + my_po_error.name = mesh.name + my_po_error.type = 1 + my_po_error.text = ( + f'Object "{mesh.name}" contains {len(vertices_with_zero_weight)} ' + 'vertices with zero cumulative valid weight.' + ) + my_po_error.text += ( + '\nNote: Vertex groups must have a bone with the same name to be valid.' + ) + my_po_error.object = mesh + my_po_error.selectVertexButton = True + my_po_error.selectOption = "VertexWithZeroWeight" + + def check_zero_scale_keyframe(): + # Check that animations do not use an invalid scale value + for obj in obj_to_check: + if bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): + animation_asset_cache = bfu_cached_asset_list.GetAnimationAssetCache(obj) + animations_to_export = animation_asset_cache.GetAnimationAssetList() + for action in animations_to_export: + for fcurve in action.fcurves: + if fcurve.data_path.split(".")[-1] == "scale": + for key in fcurve.keyframe_points: + x_curve, y_curve = key.co + if y_curve == 0: + bone_name = fcurve.data_path.split('"')[1] + my_po_error = potential_errors.add() + my_po_error.type = 2 + my_po_error.text = ( + f'In action "{action.name}" at frame {x_curve}, ' + f'the bone named "{bone_name}" has a zero value in the scale ' + 'transform. This is invalid in Unreal.' + ) + + check_unit_scale() + check_scene_frame_rate() + check_obj_type() + check_shape_keys() + check_uv_maps() + check_bad_static_mesh_exported_like_skeletal_mesh() + check_armature_scale() + check_armature_number() + check_armature_mod_data() + check_armature_const_data() + check_armature_bone_data() + check_armature_valid_child() + check_armature_multiple_roots() + check_armature_child_with_bone_parent() + check_armature_no_deform_bone() + check_marker_overlay() + check_vertex_group_weight() + check_zero_scale_keyframe() + + return potential_errors + + +def select_potential_error_object(errorIndex): + # Select potential error + + bbpl.utils.safe_mode_set('OBJECT', bpy.context.active_object) + scene = bpy.context.scene + error = scene.potentialErrorList[errorIndex] + obj = error.object + + bpy.ops.object.select_all(action='DESELECT') + obj.hide_viewport = False + obj.hide_set(False) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # show collection for select object + for collection in bpy.data.collections: + for ColObj in collection.objects: + if ColObj == obj: + bfu_basics.SetCollectionUse(collection) + bpy.ops.view3d.view_selected() + return obj + + +def SelectPotentialErrorVertex(errorIndex): + # Select potential error + select_potential_error_object(errorIndex) + bbpl.utils.safe_mode_set('EDIT') + + scene = bpy.context.scene + error = scene.potentialErrorList[errorIndex] + obj = error.object + bpy.ops.mesh.select_mode(type="VERT") + bpy.ops.mesh.select_all(action='DESELECT') + + bbpl.utils.safe_mode_set('OBJECT') + if error.selectOption == "VertexWithZeroWeight": + for vertex in GetVertexWithZeroWeight(obj.parent, obj): + vertex.select = True + bbpl.utils.safe_mode_set('EDIT') + bpy.ops.view3d.view_selected() + return obj + + +def SelectPotentialErrorPoseBone(errorIndex): + # Select potential error + select_potential_error_object(errorIndex) + bbpl.utils.safe_mode_set('POSE') + + scene = bpy.context.scene + error = scene.potentialErrorList[errorIndex] + obj = error.object + bone = obj.data.bones[error.itemName] + + # Make bone visible if hide in a layer + for x, layer in enumerate(bone.layers): + if not obj.data.layers[x] and layer: + obj.data.layers[x] = True + + bpy.ops.pose.select_all(action='DESELECT') + obj.data.bones.active = bone + bone.select = True + + bpy.ops.view3d.view_selected() + return obj + + +def TryToCorrectPotentialError(errorIndex): + # Try to correct potential error + + scene = bpy.context.scene + error = scene.potentialErrorList[errorIndex] + global successCorrect + successCorrect = False + + local_view_areas = bbpl.scene_utils.move_to_global_view() + + MyCurrentDataSave = bbpl.save_data.scene_save.UserSceneSave() + MyCurrentDataSave.save_current_scene() + + bbpl.utils.safe_mode_set('OBJECT', MyCurrentDataSave.user_select_class.user_active) + + print("Start correct") + + def SelectObj(obj): + bpy.ops.object.select_all(action='DESELECT') + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + # Correction list + + if error.correctRef == "SetUnrealUnit": + bpy.context.scene.unit_settings.scale_length = 0.01 + successCorrect = True + + if error.correctRef == "ConvertToMesh": + obj = error.object + SelectObj(obj) + bpy.ops.object.convert(target='MESH') + successCorrect = True + + if error.correctRef == "SetKeyRangeMin": + obj = error.object + key = obj.data.shape_keys.key_blocks[error.itemName] + key.slider_min = -5 + successCorrect = True + + if error.correctRef == "SetKeyRangeMax": + obj = error.object + key = obj.data.shape_keys.key_blocks[error.itemName] + key.slider_max = 5 + successCorrect = True + + if error.correctRef == "CreateUV": + obj = error.object + SelectObj(obj) + if bbpl.utils.safe_mode_set("EDIT", obj): + bpy.ops.uv.smart_project() + successCorrect = True + else: + successCorrect = False + + if error.correctRef == "RemoveModfier": + obj = error.object + mod = obj.modifiers[error.itemName] + obj.modifiers.remove(mod) + successCorrect = True + + if error.correctRef == "PreserveVolume": + obj = error.object + mod = obj.modifiers[error.itemName] + mod.use_deform_preserve_volume = False + successCorrect = True + + if error.correctRef == "BoneSegments": + obj = error.object + bone = obj.data.bones[error.itemName] + bone.bbone_segments = 1 + successCorrect = True + + if error.correctRef == "InheritScale": + obj = error.object + bone = obj.data.bones[error.itemName] + bone.use_inherit_scale = True + successCorrect = True + + # ----------------------------------------Reset data + MyCurrentDataSave.reset_select(use_names = True) + MyCurrentDataSave.reset_scene_at_save() + bbpl.scene_utils.move_to_local_view(local_view_areas) + + # ---------------------------------------- + + if successCorrect: + scene.potentialErrorList.remove(errorIndex) + print("end correct, Error: " + error.correctRef) + return "Corrected" + print("end correct, Error not found") + return "Correct fail" + + + + + diff --git a/blender-for-unrealengine/bfu_collision/__init__.py b/blender-for-unrealengine/bfu_collision/__init__.py index 5cbfbcc8..5d2cc497 100644 --- a/blender-for-unrealengine/bfu_collision/__init__.py +++ b/blender-for-unrealengine/bfu_collision/__init__.py @@ -19,11 +19,17 @@ import bpy import importlib -from . import bfu_collision_ui_and_props +from . import bfu_collision_props +from . import bfu_collision_types +from . import bfu_collision_ui from . import bfu_collision_utils -if "bfu_collision_ui_and_props" in locals(): - importlib.reload(bfu_collision_ui_and_props) +if "bfu_collision_types" in locals(): + importlib.reload(bfu_collision_types) +if "bfu_collision_props" in locals(): + importlib.reload(bfu_collision_props) +if "bfu_collision_ui" in locals(): + importlib.reload(bfu_collision_ui) if "bfu_collision_utils" in locals(): importlib.reload(bfu_collision_utils) @@ -35,10 +41,12 @@ def register(): for cls in classes: bpy.utils.register_class(cls) - bfu_collision_ui_and_props.register() + bfu_collision_types.register() + bfu_collision_props.register() def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) - bfu_collision_ui_and_props.unregister() \ No newline at end of file + bfu_collision_props.unregister() + bfu_collision_types.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_collision/bfu_collision_props.py b/blender-for-unrealengine/bfu_collision/bfu_collision_props.py new file mode 100644 index 00000000..6a733b7f --- /dev/null +++ b/blender-for-unrealengine/bfu_collision/bfu_collision_props.py @@ -0,0 +1,129 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from . import bfu_collision_utils +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + +def get_preset_values(): + preset_values = [ + 'obj.bfu_create_physics_asset', + 'obj.bfu_auto_generate_collision', + 'obj.bfu_collision_trace_flag', + 'obj.bfu_enable_skeletal_mesh_per_poly_collision', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_object_collision_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Collision") + bpy.types.Scene.bfu_tools_collision_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Collision") + + # ImportUI + # https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxImportUI/index.html + + bpy.types.Object.bfu_create_physics_asset = bpy.props.BoolProperty( + name="Create PhysicsAsset", + description="If checked, create a PhysicsAsset when is imported", + override={'LIBRARY_OVERRIDABLE'}, + default=True + ) + + + bpy.types.Object.bfu_auto_generate_collision = bpy.props.BoolProperty( + name="Auto Generate Collision", + description=( + "If checked, collision will automatically be generated" + + " (ignored if custom collision is imported or used)." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True, + ) + + + bpy.types.Object.bfu_collision_trace_flag = bpy.props.EnumProperty( + name="Collision Complexity", + description="Collision Trace Flag", + override={'LIBRARY_OVERRIDABLE'}, + # Vania python + # https://docs.unrealengine.com/en-US/PythonAPI/class/CollisionTraceFlag.html + # C++ API + # https://api.unrealengine.com/INT/API/Runtime/Engine/PhysicsEngine/ECollisionTraceFlag/index.html + items=[ + ("CTF_UseDefault", + "Project Default", + "Create only complex shapes (per poly)." + + " Use complex shapes for all scene queries" + + " and collision tests." + + " Can be used in simulation for" + + " static shapes only" + + " (i.e can be collided against but not moved" + + " through forces or velocity.", + 1), + ("CTF_UseSimpleAndComplex", + "Use Simple And Complex", + "Use project physics settings (DefaultShapeComplexity)", + 2), + ("CTF_UseSimpleAsComplex", + "Use Simple as Complex", + "Create both simple and complex shapes." + + " Simple shapes are used for regular scene queries" + + " and collision tests. Complex shape (per poly)" + + " is used for complex scene queries.", + 3), + ("CTF_UseComplexAsSimple", + "Use Complex as Simple", + "Create only simple shapes." + + " Use simple shapes for all scene" + + " queries and collision tests.", + 4) + ] + ) + + bpy.types.Object.bfu_enable_skeletal_mesh_per_poly_collision = bpy.props.BoolProperty( + name="Enable Per-Poly Collision", + description="Enable per-polygon collision for Skeletal Mesh", + default=False + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_enable_skeletal_mesh_per_poly_collision + + del bpy.types.Object.bfu_collision_trace_flag + del bpy.types.Object.bfu_auto_generate_collision + del bpy.types.Object.bfu_create_physics_asset + + del bpy.types.Scene.bfu_tools_collision_properties_expanded + del bpy.types.Scene.bfu_object_collision_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_collision/bfu_collision_ui_and_props.py b/blender-for-unrealengine/bfu_collision/bfu_collision_types.py similarity index 72% rename from blender-for-unrealengine/bfu_collision/bfu_collision_ui_and_props.py rename to blender-for-unrealengine/bfu_collision/bfu_collision_types.py index 5455b8a4..6cf2c623 100644 --- a/blender-for-unrealengine/bfu_collision/bfu_collision_ui_and_props.py +++ b/blender-for-unrealengine/bfu_collision/bfu_collision_types.py @@ -24,7 +24,6 @@ from .. import bfu_ui from .. import bbpl -#@TODO Move in bfu_collision_types.py class BFU_OT_ConvertToCollisionButtonBox(bpy.types.Operator): bl_label = "Convert to box (UBX)" bl_idname = "object.converttoboxcollision" @@ -47,7 +46,6 @@ def execute(self, context): " (Active object is the owner of the collision)") return {'FINISHED'} -#@TODO Move in bfu_collision_types.py class BFU_OT_ConvertToCollisionButtonCapsule(bpy.types.Operator): bl_label = "Convert to capsule (UCP)" bl_idname = "object.converttocapsulecollision" @@ -70,7 +68,6 @@ def execute(self, context): " (Active object is the owner of the collision)") return {'FINISHED'} -#@TODO Move in bfu_collision_types.py class BFU_OT_ConvertToCollisionButtonSphere(bpy.types.Operator): bl_label = "Convert to sphere (USP)" bl_idname = "object.converttospherecollision" @@ -93,7 +90,6 @@ def execute(self, context): " (Active object is the owner of the collision)") return {'FINISHED'} -#@TODO Move in bfu_collision_types.py class BFU_OT_ConvertToCollisionButtonConvex(bpy.types.Operator): bl_label = "Convert to convex shape (UCX)" bl_idname = "object.converttoconvexcollision" @@ -116,7 +112,6 @@ def execute(self, context): " (Active object is the owner of the collision)") return {'FINISHED'} -#@TODO Move in bfu_collision_types.py class BFU_OT_ToggleCollisionVisibility(bpy.types.Operator): bl_label = "Toggle Collision Visibility" bl_idname = "object.toggle_collision_visibility" @@ -132,40 +127,6 @@ def execute(self, context): return {'FINISHED'} -def draw_ui_scene_collision(layout: bpy.types.UILayout): - #@TODO Move in bfu_collision_ui.py - scene = bpy.context.scene - scene.bfu_collision_expanded.draw(layout) - if scene.bfu_collision_expanded.is_expend(): - - # Draw user tips and check can use buttons - ready_for_convert_collider = False - if not bbpl.utils.active_mode_is("OBJECT"): - layout.label(text="Switch to Object Mode.", icon='INFO') - else: - if bbpl.utils.found_type_in_selection("MESH", False): - if bbpl.utils.active_type_is_not("ARMATURE") and len(bpy.context.selected_objects) > 1: - layout.label(text="Click on button for convert to collider.", icon='INFO') - ready_for_convert_collider = True - else: - layout.label(text="Select with [SHIFT] the collider owner.", icon='INFO') - else: - layout.label(text="Please select your collider Object(s). Active should be the owner.", icon='INFO') - - # Draw buttons - convertButtons = layout.row().split(factor=0.80) - convertStaticCollisionButtons = convertButtons.column() - convertStaticCollisionButtons.enabled = ready_for_convert_collider - convertStaticCollisionButtons.operator("object.converttoboxcollision", icon='MESH_CUBE') - convertStaticCollisionButtons.operator("object.converttoconvexcollision", icon='MESH_ICOSPHERE') - convertStaticCollisionButtons.operator("object.converttocapsulecollision", icon='MESH_CAPSULE') - convertStaticCollisionButtons.operator("object.converttospherecollision", icon='MESH_UVSPHERE') - layout.operator("object.toggle_collision_visibility", text="Toggle Collision Visibility", icon='HIDE_OFF') - -def draw_ui_object_collision(layout: bpy.types.UILayout): - #@TODO Move in bfu_collision_ui.py - pass - # Move collision content from bfu_object_ui_and_property.py # ------------------------------------------------------------------- # Register & Unregister diff --git a/blender-for-unrealengine/bfu_collision/bfu_collision_ui.py b/blender-for-unrealengine/bfu_collision/bfu_collision_ui.py new file mode 100644 index 00000000..6fab13c9 --- /dev/null +++ b/blender-for-unrealengine/bfu_collision/bfu_collision_ui.py @@ -0,0 +1,100 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_static_mesh +from .. import bfu_skeletal_mesh + + +def draw_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + is_static_mesh = bfu_static_mesh.bfu_static_mesh_utils.is_static_mesh(obj) + is_skeletal_mesh = bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj) + + # Hide filters + if obj is None: + return + if addon_prefs.useGeneratedScripts is False: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + if is_static_mesh == False and is_skeletal_mesh == False: + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "MISC"): + scene.bfu_object_collision_properties_expanded.draw(layout) + if scene.bfu_object_collision_properties_expanded.is_expend(): + # StaticMesh prop + if is_static_mesh: + if not obj.bfu_export_as_lod_mesh: + auto_generate_collision = layout.row() + auto_generate_collision.prop( + obj, + 'bfu_auto_generate_collision' + ) + collision_trace_flag = layout.row() + collision_trace_flag.prop( + obj, + 'bfu_collision_trace_flag' + ) + # SkeletalMesh prop + if is_skeletal_mesh: + if not obj.bfu_export_as_lod_mesh: + create_physics_asset = layout.row() + create_physics_asset.prop(obj, "bfu_create_physics_asset") + enable_skeletal_mesh_per_poly_collision = layout.row() + enable_skeletal_mesh_per_poly_collision.prop(obj, 'bfu_enable_skeletal_mesh_per_poly_collision') + + +def draw_tools_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene + + scene.bfu_tools_collision_properties_expanded.draw(layout) + if scene.bfu_tools_collision_properties_expanded.is_expend(): + + # Draw user tips and check can use buttons + ready_for_convert_collider = False + if not bbpl.utils.active_mode_is("OBJECT"): + layout.label(text="Switch to Object Mode.", icon='INFO') + else: + if bbpl.utils.found_type_in_selection("MESH", False): + if bbpl.utils.active_type_is_not("ARMATURE") and len(bpy.context.selected_objects) > 1: + layout.label(text="Click on button for convert to collider.", icon='INFO') + ready_for_convert_collider = True + else: + layout.label(text="Select with [SHIFT] the collider owner.", icon='INFO') + else: + layout.label(text="Please select your collider Object(s). Active should be the owner.", icon='INFO') + + # Draw buttons + convertButtons = layout.row().split(factor=0.80) + convertStaticCollisionButtons = convertButtons.column() + convertStaticCollisionButtons.enabled = ready_for_convert_collider + convertStaticCollisionButtons.operator("object.converttoboxcollision", icon='MESH_CUBE') + convertStaticCollisionButtons.operator("object.converttoconvexcollision", icon='MESH_ICOSPHERE') + convertStaticCollisionButtons.operator("object.converttocapsulecollision", icon='MESH_CAPSULE') + convertStaticCollisionButtons.operator("object.converttospherecollision", icon='MESH_UVSPHERE') + layout.operator("object.toggle_collision_visibility", text="Toggle Collision Visibility", icon='HIDE_OFF') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export/bfu_export_asset.py b/blender-for-unrealengine/bfu_export/bfu_export_asset.py index 0f4ebb90..1a28e3e4 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_asset.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_asset.py @@ -28,7 +28,6 @@ from . import bfu_export_single_static_mesh_collection from . import bfu_export_single_groom_simulation from .. import bfu_cached_asset_list -from .. import bps from .. import bbpl from .. import bfu_assets_manager from .. import bfu_basics @@ -45,7 +44,7 @@ def IsValidActionForExport(scene, obj, animType): if animType == "Action": - if scene.anin_export: + if scene.bfu_use_anin_export: if obj.bfu_skeleton_export_procedure == 'auto-rig-pro': if bfu_basics.CheckPluginIsActivated('auto_rig_pro-master'): return True @@ -54,7 +53,7 @@ def IsValidActionForExport(scene, obj, animType): else: return False elif animType == "Pose": - if scene.anin_export: + if scene.bfu_use_anin_export: if obj.bfu_skeleton_export_procedure == 'auto-rig-pro': if bfu_basics.CheckPluginIsActivated('auto_rig_pro-master'): return True @@ -63,7 +62,7 @@ def IsValidActionForExport(scene, obj, animType): else: return False elif animType == "NLA": - if scene.anin_export: + if scene.bfu_use_anin_export: if obj.bfu_skeleton_export_procedure == 'auto-rig-pro': return False else: @@ -80,7 +79,9 @@ def IsValidObjectForExport(scene, obj): return asset_class.can_export_obj_asset(obj) def PrepareSceneForExport(): - for obj in bpy.data.objects: + scene = bpy.context.scene + + for obj in scene.objects: if obj.hide_select: obj.hide_select = False if obj.hide_viewport: @@ -109,8 +110,8 @@ def process_export(op): local_view_areas = bbpl.scene_utils.move_to_global_view() - MyCurrentDataSave = bbpl.utils.UserSceneSave() - MyCurrentDataSave.save_current_scene() + user_scene_save = bbpl.save_data.scene_save.UserSceneSave() + user_scene_save.save_current_scene() if export_filter == "default": PrepareSceneForExport() @@ -123,7 +124,7 @@ def process_export(op): PrepareSceneForExport() - bbpl.utils.safe_mode_set('OBJECT', MyCurrentDataSave.user_select_class.user_active) + bbpl.utils.safe_mode_set('OBJECT', user_scene_save.user_select_class.user_active) if addon_prefs.revertExportPath: bfu_basics.RemoveFolderTree(bpy.path.abspath(scene.bfu_export_static_file_path)) @@ -155,12 +156,12 @@ def process_export(op): if Asset.obj not in obj_list: obj_list.append(Asset.obj) - MyCurrentDataSave.reset_select_by_name() - MyCurrentDataSave.reset_scene_at_save(print_removed_items = True) + user_scene_save.reset_select(use_names = True) + user_scene_save.reset_scene_at_save(print_removed_items = True) # Clean actions for action in bpy.data.actions: - if action.name not in MyCurrentDataSave.action_names: + if action.name not in user_scene_save.action_names: bpy.data.actions.remove(action) bbpl.scene_utils.move_to_local_view(local_view_areas) @@ -182,7 +183,7 @@ def export_collection_from_asset_list(op, asset_list: bfu_cached_asset_list.Asse addon_prefs = bfu_basics.GetAddonPrefs() print("Start Export collection(s)") - if scene.static_collection_export: + if scene.bfu_use_static_collection_export: collection_asset_cache = bfu_cached_asset_list.GetCollectionAssetCache() collection_export_asset_list = collection_asset_cache.GetCollectionAssetList() for col in collection_export_asset_list: @@ -204,7 +205,7 @@ def export_camera_from_asset_list(op, asset_list: bfu_cached_asset_list.AssetToE camera_list = [] - use_camera_evaluate = (scene.text_AdditionalData and addon_prefs.useGeneratedScripts) + use_camera_evaluate = (scene.bfu_use_text_additional_data and addon_prefs.useGeneratedScripts) if use_camera_evaluate: multi_camera_tracks = bfu_camera.bfu_camera_data.BFU_MultiCameraTracks() multi_camera_tracks.set_start_end_frames(scene.frame_start, scene.frame_end+1) @@ -246,7 +247,7 @@ def export_spline_from_asset_list(op, asset_list: bfu_cached_asset_list.AssetToE spline_list = [] - use_spline_evaluate = (scene.text_AdditionalData and addon_prefs.useGeneratedScripts) + use_spline_evaluate = (scene.bfu_use_text_additional_data and addon_prefs.useGeneratedScripts) if use_spline_evaluate: multi_spline_tracks = bfu_spline.bfu_spline_data.BFU_MultiSplineTracks() @@ -300,7 +301,7 @@ def export_skeletal_mesh_from_asset_list(op, asset_list: bfu_cached_asset_list.A print("Start Export SkeletalMesh(s)") for asset in asset_list: asset: bfu_cached_asset_list.AssetToExport - if asset.asset_type == bfu_skeletal_mesh.bfu_skeletal_mesh_config.asset_type_name: + if asset.asset_type == bfu_skeletal_mesh.bfu_skeletal_mesh_config.mesh_asset_type_name: armature = asset.obj mesh_parts = asset.obj_list desired_name = asset.name diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_alembic_animation.py b/blender-for-unrealengine/bfu_export/bfu_export_single_alembic_animation.py index 492a9e46..88761f26 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_alembic_animation.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_alembic_animation.py @@ -32,7 +32,7 @@ def ProcessAlembicAnimationExport(obj): scene = bpy.context.scene asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) - dirpath = asset_class.get_obj_export_directory_path(obj) + dirpath = asset_class.get_obj_export_directory_path(obj, True) file_name = asset_class.get_obj_file_name(obj, obj.name, "") MyAsset: bfu_export_logs.BFU_OT_UnrealExportedAsset = scene.UnrealExportedAssetsList.add() diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_camera.py b/blender-for-unrealengine/bfu_export/bfu_export_single_camera.py index 757a85b3..2ad7a134 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_camera.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_camera.py @@ -21,11 +21,9 @@ from . import bfu_fbx_export from . import bfu_export_utils from .. import bfu_camera -from .. import bps from .. import bbpl from .. import bfu_basics from .. import bfu_utils -from .. import bfu_naming from .. import bfu_export_logs from .. import bfu_assets_manager @@ -37,7 +35,7 @@ def ProcessCameraExport(op, obj, pre_bake_camera: bfu_camera.bfu_camera_data.BFU asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) asset_type = asset_class.get_asset_type_name(obj) - dirpath = asset_class.get_obj_export_directory_path(obj) + dirpath = asset_class.get_obj_export_directory_path(obj, True) file_name = asset_class.get_obj_file_name(obj, obj.name, "") file_name_at = asset_class.get_obj_file_name(obj, obj.name+"_AdditionalTrack", "") @@ -60,7 +58,7 @@ def ProcessCameraExport(op, obj, pre_bake_camera: bfu_camera.bfu_camera_data.BFU ExportSingleFbxCamera(op, dirpath, file.GetFileWithExtension(), obj) - if scene.text_AdditionalData and addon_prefs.useGeneratedScripts: + if scene.bfu_use_text_additional_data and addon_prefs.useGeneratedScripts: file: bfu_export_logs.BFU_OT_FileExport = MyAsset.files.add() file.file_name = file_name_at diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_action.py b/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_action.py index 64107d35..6cb6972f 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_action.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_action.py @@ -32,8 +32,8 @@ def ProcessActionExport(op, obj, action, action_curve_scale): scene = bpy.context.scene - asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) - dirpath = asset_class.get_obj_export_directory_path(obj) + asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj, "SkeletalAnimation") + dirpath = asset_class.get_obj_export_directory_path(obj, True) MyAsset: bfu_export_logs.BFU_OT_UnrealExportedAsset = scene.UnrealExportedAssetsList.add() MyAsset.object = obj diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_nla_anim.py b/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_nla_anim.py index f07c441d..68a062df 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_nla_anim.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_fbx_nla_anim.py @@ -34,8 +34,8 @@ def ProcessNLAAnimExport(op, obj): scene = bpy.context.scene - asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) - dirpath = asset_class.get_obj_export_directory_path(obj) + asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj, "SkeletalAnimation") + dirpath = asset_class.get_obj_export_directory_path(obj, True) scene.frame_end += 1 # Why ? diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_groom_simulation.py b/blender-for-unrealengine/bfu_export/bfu_export_single_groom_simulation.py index 67e0e620..f78b9a4e 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_groom_simulation.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_groom_simulation.py @@ -32,7 +32,7 @@ def ProcessGroomSimulationExport(obj): scene = bpy.context.scene asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) - dirpath = asset_class.get_obj_export_directory_path(obj) + dirpath = asset_class.get_obj_export_directory_path(obj, True) file_name = asset_class.get_obj_file_name(obj, obj.name, "") asset_type = asset_class.get_asset_type_name(obj) diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_skeletal_mesh.py b/blender-for-unrealengine/bfu_export/bfu_export_single_skeletal_mesh.py index 506e1380..4c19a96e 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_skeletal_mesh.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_skeletal_mesh.py @@ -37,8 +37,7 @@ def ProcessSkeletalMeshExport(op, armature, mesh_parts, desired_name=""): asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(armature) asset_type = asset_class.get_asset_type_name(armature) - dirpath = asset_class.get_obj_export_directory_path(armature) - absdirpath = asset_class.get_obj_export_abs_directory_path(armature) + dirpath = asset_class.get_obj_export_directory_path(armature, True) if desired_name: final_name = desired_name @@ -66,14 +65,14 @@ def ProcessSkeletalMeshExport(op, armature, mesh_parts, desired_name=""): ExportSingleSkeletalMesh(op, scene, dirpath, file.GetFileWithExtension(), armature, mesh_parts) if not armature.bfu_export_as_lod_mesh: - if (scene.text_AdditionalData and addon_prefs.useGeneratedScripts): + if (scene.bfu_use_text_additional_data and addon_prefs.useGeneratedScripts): file: bfu_export_logs.BFU_OT_FileExport = MyAsset.files.add() file.file_name = file_name_at file.file_extension = "json" file.file_path = dirpath file.file_type = "AdditionalTrack" - bfu_export_utils.ExportAdditionalParameter(absdirpath, file.GetFileWithExtension(), MyAsset) + bfu_export_utils.ExportAdditionalParameter(dirpath, file.GetFileWithExtension(), MyAsset) MyAsset.EndAssetExport(True) return MyAsset @@ -88,6 +87,8 @@ def ExportSingleSkeletalMesh( mesh_parts ): + print("--", dirpath, filename) + ''' ##################################################### #SKELETAL MESH diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_spline.py b/blender-for-unrealengine/bfu_export/bfu_export_single_spline.py index 2494561f..deb1373d 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_spline.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_spline.py @@ -21,11 +21,9 @@ from . import bfu_fbx_export from . import bfu_export_utils from .. import bfu_spline -from .. import bps from .. import bbpl from .. import bfu_basics from .. import bfu_utils -from .. import bfu_naming from .. import bfu_export_logs from .. import bfu_assets_manager @@ -38,7 +36,7 @@ def ProcessSplineExport(op, obj, pre_bake_spline: bfu_spline.bfu_spline_data.BFU asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) asset_type = asset_class.get_asset_type_name(obj) - dirpath = asset_class.get_obj_export_directory_path(obj) + dirpath = asset_class.get_obj_export_directory_path(obj, True) file_name = asset_class.get_obj_file_name(obj, obj.name, "") file_name_at = asset_class.get_obj_file_name(obj, obj.name+"_AdditionalTrack", "") @@ -61,7 +59,7 @@ def ProcessSplineExport(op, obj, pre_bake_spline: bfu_spline.bfu_spline_data.BFU ExportSingleFbxSpline(op, dirpath, file.GetFileWithExtension(), obj) - if scene.text_AdditionalData and addon_prefs.useGeneratedScripts: + if scene.bfu_use_text_additional_data and addon_prefs.useGeneratedScripts: file: bfu_export_logs.BFU_OT_FileExport = MyAsset.files.add() file.file_name = file_name_at diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh.py b/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh.py index e343050b..aa1fbfbe 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh.py @@ -37,8 +37,7 @@ def ProcessStaticMeshExport(op, obj, desired_name=""): asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) asset_type = asset_class.get_asset_type_name(obj) - dirpath = asset_class.get_obj_export_directory_path(obj) - absdirpath = asset_class.get_obj_export_abs_directory_path(obj) + dirpath = asset_class.get_obj_export_directory_path(obj, True) if desired_name: final_name = desired_name @@ -65,14 +64,14 @@ def ProcessStaticMeshExport(op, obj, desired_name=""): ExportSingleStaticMesh(op, dirpath, file.GetFileWithExtension(), obj) if not obj.bfu_export_as_lod_mesh: - if (scene.text_AdditionalData and addon_prefs.useGeneratedScripts): + if (scene.bfu_use_text_additional_data and addon_prefs.useGeneratedScripts): file: bfu_export_logs.BFU_OT_FileExport = MyAsset.files.add() file.file_name = file_name_at file.file_extension = "json" file.file_path = dirpath file.file_type = "AdditionalTrack" - bfu_export_utils.ExportAdditionalParameter(absdirpath, file.GetFileWithExtension(), MyAsset) + bfu_export_utils.ExportAdditionalParameter(dirpath, file.GetFileWithExtension(), MyAsset) MyAsset.EndAssetExport(True) return MyAsset @@ -84,6 +83,8 @@ def ExportSingleStaticMesh( filename, obj ): + + print("--", dirpath, filename) ''' ##################################################### diff --git a/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh_collection.py b/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh_collection.py index f51a36c1..5a32d58e 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh_collection.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_single_static_mesh_collection.py @@ -33,7 +33,6 @@ def ProcessCollectionExport(op, col): addon_prefs = bfu_basics.GetAddonPrefs() dirpath = bfu_utils.GetCollectionExportDir(bpy.data.collections[col.name]) - absdirpath = bpy.path.abspath(dirpath) scene = bpy.context.scene MyAsset: bfu_export_logs.BFU_OT_UnrealExportedAsset = scene.UnrealExportedAssetsList.add() @@ -52,14 +51,14 @@ def ProcessCollectionExport(op, col): MyAsset.StartAssetExport() ExportSingleStaticMeshCollection(op, dirpath, file.GetFileWithExtension(), col.name) - if (scene.text_AdditionalData and addon_prefs.useGeneratedScripts): + if (scene.bfu_use_text_additional_data and addon_prefs.useGeneratedScripts): file: bfu_export_logs.BFU_OT_FileExport = MyAsset.files.add() file.file_name = bfu_naming.get_collection_file_name(col, col.name+"_AdditionalTrack", "") file.file_extension = "json" file.file_path = dirpath file.file_type = "AdditionalTrack" - bfu_export_utils.ExportAdditionalParameter(absdirpath, file.GetFileWithExtension(), MyAsset) + bfu_export_utils.ExportAdditionalParameter(dirpath, file.GetFileWithExtension(), MyAsset) MyAsset.EndAssetExport(True) return MyAsset diff --git a/blender-for-unrealengine/bfu_export/bfu_export_utils.py b/blender-for-unrealengine/bfu_export/bfu_export_utils.py index a2d0fb78..e3c407cf 100644 --- a/blender-for-unrealengine/bfu_export/bfu_export_utils.py +++ b/blender-for-unrealengine/bfu_export/bfu_export_utils.py @@ -47,6 +47,7 @@ def GetExportFullpath(dirpath, filename): def ApplyProxyData(obj): + scene = bpy.context.scene # Apply proxy data if needed. if bfu_utils.GetExportProxyChild(obj) is not None: @@ -76,8 +77,8 @@ def ReasignProxySkeleton(newArmature, oldArmature): cons.target.name + "_UEProxyChild" ) - if ChildProxyName in bpy.data.objects: - cons.target = bpy.data.objects[ChildProxyName] + if ChildProxyName in scene.objects: + cons.target = scene.objects[ChildProxyName] # Get old armature in selected objects OldProxyChildArmature = None @@ -101,17 +102,17 @@ def ReasignProxySkeleton(newArmature, oldArmature): else: ToRemove.append(selectedObj) ReasignProxySkeleton(obj, OldProxyChildArmature) - SavedSelect = bbpl.utils.UserSelectSave() + SavedSelect = bbpl.save_data.select_save.UserSelectSave() SavedSelect.save_current_select() RemovedObjects = bfu_utils.CleanDeleteObjects(ToRemove) SavedSelect.remove_from_list_by_name(RemovedObjects) - SavedSelect.reset_select_by_ref() + SavedSelect.reset_select() def BakeArmatureAnimation(armature, frame_start, frame_end): # Change to pose mode - SavedSelect = bbpl.utils.UserSelectSave() + SavedSelect = bbpl.save_data.select_save.UserSelectSave() SavedSelect.save_current_select() bpy.ops.object.select_all(action='DESELECT') bbpl.utils.select_specific_object(armature) @@ -125,7 +126,7 @@ def BakeArmatureAnimation(armature, frame_start, frame_end): bake_types={'POSE'} ) bpy.ops.object.select_all(action='DESELECT') - SavedSelect.reset_select_by_ref() + SavedSelect.reset_select() def DuplicateSelectForExport(): @@ -138,12 +139,12 @@ def __init__(self): self.duplicate_select = None def SetOriginSelect(self): - select = bbpl.utils.UserSelectSave() + select = bbpl.save_data.select_save.UserSelectSave() select.save_current_select() self.origin_select = select def SetDuplicateSelect(self): - select = bbpl.utils.UserSelectSave() + select = bbpl.save_data.select_save.UserSelectSave() select.save_current_select() self.duplicate_select = select @@ -157,6 +158,7 @@ def __init__(self, data_name, data_type): def RemoveData(self): bfu_utils.RemoveUselessSpecificData(self.data_name, self.data_type) + scene = bpy.context.scene duplicate_data = DuplicateData() duplicate_data.SetOriginSelect() for user_selected in duplicate_data.origin_select.user_selecteds: @@ -181,7 +183,7 @@ def RemoveData(self): for objSelect in currentSelectNames: if objSelect not in bpy.context.selected_objects: - bpy.data.objects[objSelect].select_set(True) + scene.objects[objSelect].select_set(True) # Make sigle user and clean useless data. for objScene in bpy.context.selected_objects: @@ -215,7 +217,8 @@ def ResetDuplicateNameAfterExport(duplicate_data): def ConvertSelectedCurveToMesh(): # Have to convert curve to mesh before MakeSelectVisualReal for avoid double duplicate issue. - select = bbpl.utils.UserSelectSave() + scene = bpy.context.scene + select = bbpl.save_data.select_save.UserSelectSave() select.save_current_select() bpy.ops.object.select_all(action='DESELECT') @@ -223,11 +226,11 @@ def ConvertSelectedCurveToMesh(): for selected_obj in select.user_selecteds: if selected_obj.type == "CURVE": - selected_obj.select_set(True) + selected_obj.select_set(False) # Save object list previous_objects = [] - for obj in bpy.data.objects: + for obj in scene.objects: previous_objects.append(obj) # Convert to mesh @@ -235,20 +238,21 @@ def ConvertSelectedCurveToMesh(): bpy.context.view_layer.objects.active = bpy.context.selected_objects[0] #Convert fail if active is none. bpy.ops.object.convert(target='MESH') - select.reset_select_by_name() + select.reset_select(use_names = True) # Select the new objects - for obj in bpy.data.objects: + for obj in scene.objects: if obj not in previous_objects: obj.select_set(True) def MakeSelectVisualReal(): - select = bbpl.utils.UserSelectSave() + scene = bpy.context.scene + select = bbpl.save_data.select_save.UserSelectSave() select.save_current_select() # Save object list previous_objects = [] - for obj in bpy.data.objects: + for obj in scene.objects: previous_objects.append(obj) # Visual Transform Apply @@ -260,10 +264,10 @@ def MakeSelectVisualReal(): use_hierarchy=True ) - select.reset_select_by_name() + select.reset_select(use_names = True) # Select the new objects - for obj in bpy.data.objects: + for obj in scene.objects: if obj not in previous_objects: obj.select_set(True) @@ -402,7 +406,7 @@ def ConvertGeometryNodeAttributeToUV(obj, attrib_name): if attribute.name == attrib_name: obj.data.attributes.active_index = x - SavedSelect = bbpl.utils.UserSelectSave() + SavedSelect = bbpl.save_data.select_save.UserSelectSave() SavedSelect.save_current_select() bbpl.utils.select_specific_object(obj) if bpy.app.version >= (3, 5, 0): @@ -411,7 +415,7 @@ def ConvertGeometryNodeAttributeToUV(obj, attrib_name): else: if obj.data.attributes.active: bpy.ops.geometry.attribute_convert(mode='UV_MAP', domain='CORNER', data_type='FLOAT2') - SavedSelect.reset_select_by_ref() + SavedSelect.reset_select() # Because it not possible to move UV index I need recreate all UV for place new UV Map at start... if len(obj.data.uv_layers) < 8: # Blender Cannot add more than 8 UV maps. @@ -465,13 +469,14 @@ def ConvertGeometryNodeAttributeToUV(obj, attrib_name): def CorrectExtremUVAtExport(obj): - if obj.bfu_correct_extrem_uv_scale: - SavedSelect = bbpl.utils.UserSelectSave() + if obj.bfu_use_correct_extrem_uv_scale: + SavedSelect = bbpl.save_data.select_save.UserSelectSave() SavedSelect.save_current_select() + bbpl.utils.select_specific_object(obj) if bfu_utils.GoToMeshEditMode(): - bfu_utils.CorrectExtremeUV(2) + bfu_utils.CorrectExtremeUV(obj.bfu_correct_extrem_uv_scale_step_scale, obj.bfu_correct_extrem_uv_scale_use_absolute) bbpl.utils.safe_mode_set('OBJECT') - SavedSelect.reset_select_by_ref() + SavedSelect.reset_select() return True return False diff --git a/blender-for-unrealengine/bfu_export/bfu_fbx_export.py b/blender-for-unrealengine/bfu_export/bfu_fbx_export.py index e4101cf8..45688f91 100644 --- a/blender-for-unrealengine/bfu_export/bfu_fbx_export.py +++ b/blender-for-unrealengine/bfu_export/bfu_fbx_export.py @@ -21,9 +21,9 @@ # Better to look about an class that amange all export type in future? import traceback -import sys import bpy from mathutils import Matrix +from .. import bpl from .. import fbxio @@ -105,9 +105,8 @@ def export_scene_fbx_with_custom_fbx_io(operator, context, filepath='', check_ex fbxio.current_fbxio.export_fbx_bin.save(**params) except Exception as e: # Capture and print the detailed error information - exc_type, exc_value, exc_tb = sys.exc_info() - error_message = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)) - print(f"\033[91m{error_message}\033[0m") + error_message = traceback.format_exc() + print(bpl.color_set.red(error_message)) def export_scene_fbx(filepath='', check_existing=True, filter_glob='*.fbx', use_selection=False, use_visible=False, use_active_collection=False, global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE', use_space_transform=True, bake_space_transform=False, object_types={'ARMATURE', 'CAMERA', 'EMPTY', 'LIGHT', 'MESH', 'OTHER'}, use_mesh_modifiers=True, use_mesh_modifiers_render=True, mesh_smooth_type='OFF', colors_type='SRGB', prioritize_active_color=False, use_subsurf=False, use_mesh_edges=False, use_tspace=False, use_triangles=False, use_custom_props=False, add_leaf_bones=True, primary_bone_axis='Y', secondary_bone_axis='X', use_armature_deform_only=False, armature_nodetype='NULL', bake_anim=True, bake_anim_use_all_bones=True, bake_anim_use_nla_strips=True, bake_anim_use_all_actions=True, bake_anim_force_startend_keying=True, bake_anim_step=1.0, bake_anim_simplify_factor=1.0, path_mode='AUTO', embed_textures=False, batch_mode='OFF', use_batch_own_dir=True, use_metadata=True, axis_forward='-Z', axis_up='Y'): @@ -175,6 +174,5 @@ def export_scene_fbx(filepath='', check_existing=True, filter_glob='*.fbx', use_ bpy.ops.export_scene.fbx(**params) except Exception as e: # Capture and print the detailed error information - exc_type, exc_value, exc_tb = sys.exc_info() - error_message = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb)) - print(f"\033[91m{error_message}\033[0m") + error_message = traceback.format_exc() + print(bpl.color_set.red(error_message)) diff --git a/blender-for-unrealengine/bfu_export_filter/__init__.py b/blender-for-unrealengine/bfu_export_filter/__init__.py new file mode 100644 index 00000000..b04174cf --- /dev/null +++ b/blender-for-unrealengine/bfu_export_filter/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_export_filter_props +from . import bfu_export_filter_ui +from . import bfu_export_filter_utils + +if "bfu_export_filter_props" in locals(): + importlib.reload(bfu_export_filter_props) +if "bfu_export_filter_ui" in locals(): + importlib.reload(bfu_export_filter_ui) +if "bfu_export_filter_utils" in locals(): + importlib.reload(bfu_export_filter_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_export_filter_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_export_filter_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_props.py b/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_props.py new file mode 100644 index 00000000..6bdffd11 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_props.py @@ -0,0 +1,176 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + # Filter Categories + 'scene.bfu_use_static_export', + 'scene.bfu_use_static_collection_export', + 'scene.bfu_use_skeletal_export', + 'scene.bfu_use_anin_export', + 'scene.bfu_use_alembic_export', + 'scene.bfu_use_groom_simulation_export', + 'scene.bfu_use_camera_export', + 'scene.bfu_use_spline_export', + + # Additional Files + 'scene.bfu_use_text_export_log', + 'scene.bfu_use_text_import_asset_script', + 'scene.bfu_use_text_import_sequence_script', + 'scene.bfu_use_text_additional_data', + + # Export Filter + 'scene.bfu_export_selection_filter', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_export_filter_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Export filters") + + # Filter Categories + bpy.types.Scene.bfu_use_static_export = bpy.props.BoolProperty( + name="StaticMesh(s)", + description="Check mark to export StaticMesh(s)", + default=True + ) + + bpy.types.Scene.bfu_use_static_collection_export = bpy.props.BoolProperty( + name="Collection(s) ", + description="Check mark to export Collection(s)", + default=True + ) + + bpy.types.Scene.bfu_use_skeletal_export = bpy.props.BoolProperty( + name="SkeletalMesh(s)", + description="Check mark to export SkeletalMesh(s)", + default=True + ) + + bpy.types.Scene.bfu_use_anin_export = bpy.props.BoolProperty( + name="Animation(s)", + description="Check mark to export Animation(s)", + default=True + ) + + bpy.types.Scene.bfu_use_alembic_export = bpy.props.BoolProperty( + name="Alembic Animation(s)", + description="Check mark to export Alembic animation(s)", + default=True + ) + + bpy.types.Scene.bfu_use_groom_simulation_export = bpy.props.BoolProperty( + name="Groom Simulation(s)", + description="Check mark to export Alembic animation(s)", + default=True + ) + + bpy.types.Scene.bfu_use_camera_export = bpy.props.BoolProperty( + name="Camera(s)", + description="Check mark to export Camera(s)", + default=True + ) + + bpy.types.Scene.bfu_use_spline_export = bpy.props.BoolProperty( + name="Spline(s)", + description="Check mark to export Spline(s)", + default=True + ) + + # Additional Files + bpy.types.Scene.bfu_use_text_export_log = bpy.props.BoolProperty( + name="Export Log", + description="Check mark to write export log file", + default=True + ) + + bpy.types.Scene.bfu_use_text_import_asset_script = bpy.props.BoolProperty( + name="Import assets script", + description="Check mark to write import asset script file", + default=True + ) + + bpy.types.Scene.bfu_use_text_import_sequence_script = bpy.props.BoolProperty( + name="Import sequence script", + description="Check mark to write import sequencer script file", + default=True + ) + + bpy.types.Scene.bfu_use_text_additional_data = bpy.props.BoolProperty( + name="Additional data", + description=( + "Check mark to write additional data" + + " like parameter or anim tracks"), + default=True + ) + + # Export Filter + bpy.types.Scene.bfu_export_selection_filter = bpy.props.EnumProperty( + name="Selection filter", + items=[ + ('default', "No Filter", "Export as normal all objects with the recursive export option.", 0), + ('only_object', "Only selected", "Export only the selected and visible object(s)", 1), + ('only_object_action', "Only selected and active action", + "Export only the selected and visible object(s) and active action on this object", 2), + ], + description=( + "Choose what need be export from asset list."), + default="default" + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.bfu_export_selection_filter + + del bpy.types.Scene.bfu_use_text_additional_data + del bpy.types.Scene.bfu_use_text_import_sequence_script + del bpy.types.Scene.bfu_use_text_import_asset_script + del bpy.types.Scene.bfu_use_text_export_log + + del bpy.types.Scene.bfu_use_spline_export + del bpy.types.Scene.bfu_use_camera_export + del bpy.types.Scene.bfu_use_groom_simulation_export + del bpy.types.Scene.bfu_use_alembic_export + del bpy.types.Scene.bfu_use_anin_export + del bpy.types.Scene.bfu_use_skeletal_export + del bpy.types.Scene.bfu_use_static_collection_export + del bpy.types.Scene.bfu_use_static_export + + del bpy.types.Scene.bfu_export_filter_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_ui.py b/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_ui.py new file mode 100644 index 00000000..71cdee75 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_ui.py @@ -0,0 +1,59 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + +def draw_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + scene.bfu_export_filter_properties_expanded.draw(layout) + if scene.bfu_export_filter_properties_expanded.is_expend(): + + # Assets + row = layout.row() + AssetsCol = row.column() + AssetsCol.label(text="Asset types to export", icon='PACKAGE') + AssetsCol.prop(scene, 'bfu_use_static_export') + AssetsCol.prop(scene, 'bfu_use_static_collection_export') + AssetsCol.prop(scene, 'bfu_use_skeletal_export') + AssetsCol.prop(scene, 'bfu_use_anin_export') + AssetsCol.prop(scene, 'bfu_use_alembic_export') + AssetsCol.prop(scene, 'bfu_use_groom_simulation_export') + AssetsCol.prop(scene, 'bfu_use_camera_export') + AssetsCol.prop(scene, 'bfu_use_spline_export') + layout.separator() + + # Additional file + FileCol = row.column() + FileCol.label(text="Additional file", icon='PACKAGE') + FileCol.prop(scene, 'bfu_use_text_export_log') + FileCol.prop(scene, 'bfu_use_text_import_asset_script') + FileCol.prop(scene, 'bfu_use_text_import_sequence_script') + if addon_prefs.useGeneratedScripts: + FileCol.prop(scene, 'bfu_use_text_additional_data') + + # exportProperty + export_by_select = layout.row() + export_by_select.prop(scene, 'bfu_export_selection_filter') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_utils.py b/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_filter/bfu_export_filter_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_export_nomenclature/__init__.py b/blender-for-unrealengine/bfu_export_nomenclature/__init__.py new file mode 100644 index 00000000..c877d807 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_nomenclature/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_export_nomenclature_props +from . import bfu_export_nomenclature_ui +from . import bfu_export_nomenclature_utils + +if "bfu_export_nomenclature_props" in locals(): + importlib.reload(bfu_export_nomenclature_props) +if "bfu_export_nomenclature_ui" in locals(): + importlib.reload(bfu_export_nomenclature_ui) +if "bfu_export_nomenclature_utils" in locals(): + importlib.reload(bfu_export_nomenclature_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_export_nomenclature_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_export_nomenclature_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_props.py b/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_props.py new file mode 100644 index 00000000..eb8522eb --- /dev/null +++ b/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_props.py @@ -0,0 +1,256 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import os +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + + + +def get_preset_values(): + preset_values = [ + # Prefix + 'scene.bfu_static_mesh_prefix_export_name', + 'scene.bfu_skeletal_mesh_prefix_export_name', + 'scene.bfu_skeleton_prefix_export_name', + 'scene.bfu_alembic_animation_prefix_export_name', + 'scene.bfu_groom_simulation_prefix_export_name', + 'scene.bfu_anim_prefix_export_name', + 'scene.bfu_pose_prefix_export_name', + 'scene.bfu_camera_prefix_export_name', + 'scene.bfu_spline_prefix_export_name', + + # Sub folder + 'scene.bfu_anim_subfolder_name', + + # Import location + 'scene.bfu_unreal_import_module', + 'scene.bfu_unreal_import_location', + + # File path + 'scene.bfu_export_static_file_path', + 'scene.bfu_export_skeletal_file_path', + 'scene.bfu_export_alembic_file_path', + 'scene.bfu_export_groom_file_path', + 'scene.bfu_export_camera_file_path', + 'scene.bfu_export_spline_file_path', + 'scene.bfu_export_other_file_path', + + # File name + 'scene.bfu_file_export_log_name', + 'scene.bfu_file_import_asset_script_name', + 'scene.bfu_file_import_sequencer_script_name', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_nomenclature_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Nomenclature") + + # Prefix + bpy.types.Scene.bfu_static_mesh_prefix_export_name = bpy.props.StringProperty( + name="StaticMesh Prefix", + description="Prefix of staticMesh", + maxlen=32, + default="SM_") + + bpy.types.Scene.bfu_skeletal_mesh_prefix_export_name = bpy.props.StringProperty( + name="SkeletalMesh Prefix ", + description="Prefix of SkeletalMesh", + maxlen=32, + default="SKM_") + + bpy.types.Scene.bfu_skeleton_prefix_export_name = bpy.props.StringProperty( + name="skeleton Prefix ", + description="Prefix of skeleton", + maxlen=32, + default="SK_") + + bpy.types.Scene.bfu_alembic_animation_prefix_export_name = bpy.props.StringProperty( + name="Alembic Prefix ", + description="Prefix of Alembic (SkeletalMesh in unreal)", + maxlen=32, + default="SKM_") + + bpy.types.Scene.bfu_groom_simulation_prefix_export_name = bpy.props.StringProperty( + name="Groom Prefix ", + description="Prefix of Groom Simulation", + maxlen=32, + default="GS_") + + bpy.types.Scene.bfu_anim_prefix_export_name = bpy.props.StringProperty( + name="AnimationSequence Prefix", + description="Prefix of AnimationSequence", + maxlen=32, + default="Anim_") + + bpy.types.Scene.bfu_pose_prefix_export_name = bpy.props.StringProperty( + name="AnimationSequence(Pose) Prefix", + description="Prefix of AnimationSequence with only one frame", + maxlen=32, + default="Pose_") + + bpy.types.Scene.bfu_camera_prefix_export_name = bpy.props.StringProperty( + name="Camera anim Prefix", + description="Prefix of camera animations", + maxlen=32, + default="Cam_") + + bpy.types.Scene.bfu_spline_prefix_export_name = bpy.props.StringProperty( + name="Spline anim Prefix", + description="Prefix of spline animations", + maxlen=32, + default="Spline_") + + # Sub folder + bpy.types.Scene.bfu_anim_subfolder_name = bpy.props.StringProperty( + name="Animations sub folder name", + description=( + "The name of sub folder for animations New." + + " You can now use ../ for up one directory."), + maxlen=512, + default="Anim") + + # Import location + bpy.types.Scene.bfu_unreal_import_module = bpy.props.StringProperty( + name="Unreal import module", + description="Which module (plugin name) to import to. Default is 'Game', meaning it will be put into your project's /Content/ folder. If you wish to import to a plugin (for example a plugin called 'myPlugin'), just write its name here", + maxlen=512, + default='Game') + + bpy.types.Scene.bfu_unreal_import_location = bpy.props.StringProperty( + name="Unreal import location", + description="Unreal assets import location inside the module", + maxlen=512, + default='ImportedFbx') + + # File path + bpy.types.Scene.bfu_export_static_file_path = bpy.props.StringProperty( + name="StaticMesh Export Path", + description="Choose a directory to export StaticMesh(s)", + maxlen=512, + default="//" + os.path.join("ExportedFbx", "StaticMesh"), + subtype='DIR_PATH') + + bpy.types.Scene.bfu_export_skeletal_file_path = bpy.props.StringProperty( + name="SkeletalMesh Export Path", + description="Choose a directory to export SkeletalMesh(s)", + maxlen=512, + default="//" + os.path.join("ExportedFbx", "SkeletalMesh"), + subtype='DIR_PATH') + + bpy.types.Scene.bfu_export_alembic_file_path = bpy.props.StringProperty( + name="Alembic Export Path", + description="Choose a directory to export Alembic animation(s)", + maxlen=512, + default="//" + os.path.join("ExportedFbx", "Alembic"), + subtype='DIR_PATH') + + bpy.types.Scene.bfu_export_groom_file_path = bpy.props.StringProperty( + name="Groom Export Path", + description="Choose a directory to export Groom simulation(s)", + maxlen=512, + default="//" + os.path.join("ExportedFbx", "Groom"), + subtype='DIR_PATH') + + bpy.types.Scene.bfu_export_camera_file_path = bpy.props.StringProperty( + name="Camera Export Path", + description="Choose a directory to export Camera(s)", + maxlen=512, + default="//" + os.path.join("ExportedFbx", "Sequencer"), + subtype='DIR_PATH') + + bpy.types.Scene.bfu_export_spline_file_path = bpy.props.StringProperty( + name="Spline Export Path", + description="Choose a directory to export Spline(s)", + maxlen=512, + default="//" + os.path.join("ExportedFbx", "Spline"), + subtype='DIR_PATH') + + bpy.types.Scene.bfu_export_other_file_path = bpy.props.StringProperty( + name="Other Export Path", + description="Choose a directory to export text file and other", + maxlen=512, + default="//" + os.path.join("ExportedFbx"), + subtype='DIR_PATH') + + # File name + bpy.types.Scene.bfu_file_export_log_name = bpy.props.StringProperty( + name="Export log name", + description="Export log name", + maxlen=64, + default="ExportLog.txt") + + bpy.types.Scene.bfu_file_import_asset_script_name = bpy.props.StringProperty( + name="Import asset script Name", + description="Import asset script name", + maxlen=64, + default="ImportAssetScript.py") + + bpy.types.Scene.bfu_file_import_sequencer_script_name = bpy.props.StringProperty( + name="Import sequencer script Name", + description="Import sequencer script name", + maxlen=64, + default="ImportSequencerScript.py") + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.bfu_file_import_sequencer_script_name + del bpy.types.Scene.bfu_file_import_asset_script_name + del bpy.types.Scene.bfu_file_export_log_name + + del bpy.types.Scene.bfu_export_other_file_path + del bpy.types.Scene.bfu_export_spline_file_path + del bpy.types.Scene.bfu_export_camera_file_path + del bpy.types.Scene.bfu_export_groom_file_path + del bpy.types.Scene.bfu_export_alembic_file_path + del bpy.types.Scene.bfu_export_skeletal_file_path + del bpy.types.Scene.bfu_export_static_file_path + + del bpy.types.Scene.bfu_unreal_import_location + del bpy.types.Scene.bfu_unreal_import_module + del bpy.types.Scene.bfu_anim_subfolder_name + + del bpy.types.Scene.bfu_spline_prefix_export_name + del bpy.types.Scene.bfu_camera_prefix_export_name + del bpy.types.Scene.bfu_pose_prefix_export_name + del bpy.types.Scene.bfu_anim_prefix_export_name + del bpy.types.Scene.bfu_groom_simulation_prefix_export_name + del bpy.types.Scene.bfu_alembic_animation_prefix_export_name + del bpy.types.Scene.bfu_skeleton_prefix_export_name + del bpy.types.Scene.bfu_skeletal_mesh_prefix_export_name + del bpy.types.Scene.bfu_static_mesh_prefix_export_name + + del bpy.types.Scene.bfu_nomenclature_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_ui.py b/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_ui.py new file mode 100644 index 00000000..dc4b6f22 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_ui.py @@ -0,0 +1,87 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + +def draw_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + scene.bfu_nomenclature_properties_expanded.draw(layout) + if scene.bfu_nomenclature_properties_expanded.is_expend(): + + # Prefix + propsPrefix = layout.row() + propsPrefix = propsPrefix.column() + propsPrefix.prop(scene, 'bfu_static_mesh_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_skeletal_mesh_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_skeleton_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_alembic_animation_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_groom_simulation_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_anim_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_pose_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_camera_prefix_export_name', icon='OBJECT_DATA') + propsPrefix.prop(scene, 'bfu_spline_prefix_export_name', icon='OBJECT_DATA') + + # Sub folder + propsSub = layout.row() + propsSub = propsSub.column() + propsSub.prop(scene, 'bfu_anim_subfolder_name', icon='FILE_FOLDER') + + if addon_prefs.useGeneratedScripts: + bfu_unreal_import_module = propsSub.column() + bfu_unreal_import_module.prop( + scene, + 'bfu_unreal_import_module', + icon='FILE_FOLDER') + bfu_unreal_import_location = propsSub.column() + bfu_unreal_import_location.prop( + scene, + 'bfu_unreal_import_location', + icon='FILE_FOLDER') + + # File path + filePath = layout.row() + filePath = filePath.column() + filePath.prop(scene, 'bfu_export_static_file_path') + filePath.prop(scene, 'bfu_export_skeletal_file_path') + filePath.prop(scene, 'bfu_export_alembic_file_path') + filePath.prop(scene, 'bfu_export_groom_file_path') + filePath.prop(scene, 'bfu_export_camera_file_path') + filePath.prop(scene, 'bfu_export_spline_file_path') + filePath.prop(scene, 'bfu_export_other_file_path') + + # File name + fileName = layout.row() + fileName = fileName.column() + fileName.prop(scene, 'bfu_file_export_log_name', icon='FILE') + if addon_prefs.useGeneratedScripts: + fileName.prop( + scene, + 'bfu_file_import_asset_script_name', + icon='FILE') + fileName.prop( + scene, + 'bfu_file_import_sequencer_script_name', + icon='FILE') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_utils.py b/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_nomenclature/bfu_export_nomenclature_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_export_process/__init__.py b/blender-for-unrealengine/bfu_export_process/__init__.py new file mode 100644 index 00000000..d9a93ef2 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_process/__init__.py @@ -0,0 +1,52 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_export_process_props +from . import bfu_export_process_operators +from . import bfu_export_process_ui +from . import bfu_export_process_utils + +if "bfu_export_process_props" in locals(): + importlib.reload(bfu_export_process_props) +if "bfu_export_process_operators" in locals(): + importlib.reload(bfu_export_process_operators) +if "bfu_export_process_ui" in locals(): + importlib.reload(bfu_export_process_ui) +if "bfu_export_process_utils" in locals(): + importlib.reload(bfu_export_process_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_export_process_props.register() + bfu_export_process_operators.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_export_process_operators.unregister() + bfu_export_process_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_process/bfu_export_process_operators.py b/blender-for-unrealengine/bfu_export_process/bfu_export_process_operators.py new file mode 100644 index 00000000..876ba06f --- /dev/null +++ b/blender-for-unrealengine/bfu_export_process/bfu_export_process_operators.py @@ -0,0 +1,176 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bpl +from .. import bbpl +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bfu_assets_manager +from .. import bfu_cached_asset_list +from .. import bfu_check_potential_error +from .. import bfu_export +from .. import bfu_write_text + + +class BFU_OT_ExportForUnrealEngineButton(bpy.types.Operator): + bl_label = "Export for Unreal Engine" + bl_idname = "object.exportforunreal" + bl_description = "Export all assets of this scene." + + def execute(self, context): + scene = bpy.context.scene + + def isReadyForExport(): + + def GetIfOneTypeCheck(): + all_assets = bfu_assets_manager.bfu_asset_manager_utils.get_all_asset_class() + for assets in all_assets: + assets: bfu_assets_manager.bfu_asset_manager_type.BFU_BaseAssetClass + if assets.can_export_asset(): + return True + + if (scene.bfu_use_static_collection_export + or scene.bfu_use_anin_export): + return True + else: + return False + + if not bfu_basics.CheckPluginIsActivated("io_scene_fbx"): + self.report( + {'WARNING'}, + 'Add-on FBX format is not activated!' + + ' Edit > Preferences > Add-ons > And check "FBX format"') + return False + + if not GetIfOneTypeCheck(): + self.report( + {'WARNING'}, + "No asset type is checked.") + return False + + final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() + final_asset_list_to_export = final_asset_cache.GetFinalAssetList() + if not len(final_asset_list_to_export) > 0: + self.report( + {'WARNING'}, + "Not found assets with" + + " \"Export recursive\" properties " + + "or collection to export.") + return False + + if not bpy.data.is_saved: + # Primary check if file is saved + # to avoid windows PermissionError + self.report( + {'WARNING'}, + "Please save this .blend file before export.") + return False + + if bbpl.scene_utils.is_tweak_mode(): + # Need exit Tweakmode because the Animation data is read only. + self.report( + {'WARNING'}, + "Exit Tweakmode in NLA Editor. [Tab]") + return False + + return True + + if not isReadyForExport(): + return {'FINISHED'} + + scene.UnrealExportedAssetsList.clear() + counter = bpl.utils.CounterTimer() + bfu_check_potential_error.bfu_check_utils.process_general_fix() + bfu_export.bfu_export_asset.process_export(self) + bfu_write_text.WriteAllTextFiles() + + self.report( + {'INFO'}, + "Export of " + str(len(scene.UnrealExportedAssetsList)) + " asset(s) has been finalized in " + counter.get_str_time() + " Look in console for more info.") + print( + "=========================" + + " Exported asset(s) " + + "=========================") + print("") + lines = bfu_write_text.WriteExportLog().splitlines() + for line in lines: + print(line) + print("") + print( + "=========================" + + " ... " + + "=========================") + + return {'FINISHED'} + +class BFU_OT_CopyImportAssetScriptCommand(bpy.types.Operator): + bl_label = "Copy import script (Assets)" + bl_idname = "object.copy_importassetscript_command" + bl_description = "Copy Import Asset Script command" + + def execute(self, context): + scene = context.scene + bfu_basics.setWindowsClipboard(bfu_utils.GetImportAssetScriptCommand()) + self.report( + {'INFO'}, + "command for "+scene.bfu_file_import_asset_script_name + + " copied") + return {'FINISHED'} + +class BFU_OT_CopyImportSequencerScriptCommand(bpy.types.Operator): + bl_label = "Copy import script (Sequencer)" + bl_idname = "object.copy_importsequencerscript_command" + bl_description = "Copy Import Sequencer Script command" + + def execute(self, context): + scene = context.scene + bfu_basics.setWindowsClipboard(bfu_utils.GetImportSequencerScriptCommand()) + self.report( + {'INFO'}, + "command for "+scene.bfu_file_import_sequencer_script_name + + " copied") + return {'FINISHED'} + + +def get_preset_values(): + preset_values = [ + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( + BFU_OT_ExportForUnrealEngineButton, + BFU_OT_CopyImportAssetScriptCommand, + BFU_OT_CopyImportSequencerScriptCommand, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/blender-for-unrealengine/bfu_export_process/bfu_export_process_props.py b/blender-for-unrealengine/bfu_export_process/bfu_export_process_props.py new file mode 100644 index 00000000..5ca05fed --- /dev/null +++ b/blender-for-unrealengine/bfu_export_process/bfu_export_process_props.py @@ -0,0 +1,52 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + +def get_preset_values(): + preset_values = [ + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_export_process_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Export process") + bpy.types.Scene.bfu_script_tool_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Copy Import Script") + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.bfu_script_tool_expanded + del bpy.types.Scene.bfu_export_process_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_process/bfu_export_process_ui.py b/blender-for-unrealengine/bfu_export_process/bfu_export_process_ui.py new file mode 100644 index 00000000..73b20bb8 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_process/bfu_export_process_ui.py @@ -0,0 +1,64 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import bfu_cached_asset_list + + +def draw_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + scene.bfu_export_process_properties_expanded.draw(layout) + if scene.bfu_export_process_properties_expanded.is_expend(): + + # Feedback info : + final_asset_cache = bfu_cached_asset_list.GetfinalAssetCache() + final_asset_list_to_export = final_asset_cache.GetFinalAssetList() + AssetNum = len(final_asset_list_to_export) + AssetInfo = layout.row().box().split(factor=0.75) + AssetFeedback = str(AssetNum) + " Asset(s) will be exported." + AssetInfo.label(text=AssetFeedback, icon='INFO') + AssetInfo.operator("object.showasset") + + # Export button : + checkButton = layout.row(align=True) + checkButton.operator("object.checkpotentialerror", icon='FILE_TICK') + checkButton.operator("object.openpotentialerror", icon='LOOP_BACK', text="") + + exportButton = layout.row() + exportButton.scale_y = 2.0 + exportButton.operator("object.exportforunreal", icon='EXPORT') + + scene.bfu_script_tool_expanded.draw(layout) + if scene.bfu_script_tool_expanded.is_expend(): + if addon_prefs.useGeneratedScripts: + copyButton = layout.row() + copyButton.operator("object.copy_importassetscript_command") + copyButton.operator("object.copy_importsequencerscript_command") + layout.label(text="Click on one of the buttons to copy the import command.", icon='INFO') + layout.label(text="Then paste it into the cmd console of unreal.") + layout.label(text="You need activate python plugins in Unreal Engine.") + + else: + layout.label(text='(Generated scripts are deactivated.)') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_export_process/bfu_export_process_utils.py b/blender-for-unrealengine/bfu_export_process/bfu_export_process_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_export_process/bfu_export_process_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_groom/bfu_groom_type.py b/blender-for-unrealengine/bfu_groom/bfu_groom_type.py index 34a52122..67bb0f8d 100644 --- a/blender-for-unrealengine/bfu_groom/bfu_groom_type.py +++ b/blender-for-unrealengine/bfu_groom/bfu_groom_type.py @@ -30,7 +30,7 @@ def __init__(self): super().__init__() pass - def support_asset_type(self, obj): + def support_asset_type(self, obj, details = None): if obj.bfu_export_as_groom_simulation: return True return False @@ -48,15 +48,20 @@ def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): return bfu_basics.ValidFilename(scene.bfu_groom_simulation_prefix_export_name+desired_name+fileType) return bfu_basics.ValidFilename(scene.bfu_groom_simulation_prefix_export_name+obj.name+fileType) - def get_obj_export_directory_path(self, obj): + def get_obj_export_directory_path(self, obj, absolute = True): folder_name = bfu_utils.get_export_folder_name(obj) scene = bpy.context.scene - dirpath = os.path.join(scene.bfu_export_groom_file_path, folder_name) + if(absolute): + root_path = bpy.path.abspath(scene.bfu_export_groom_file_path) + else: + root_path = scene.bfu_export_groom_file_path + + dirpath = os.path.join(root_path, folder_name) return dirpath def can_export_asset(self): scene = bpy.context.scene - return scene.groom_simulation_export + return scene.bfu_use_groom_simulation_export def can_export_obj_asset(self, obj): return self.can_export_asset() diff --git a/blender-for-unrealengine/bfu_import_module/__init__.py b/blender-for-unrealengine/bfu_import_module/__init__.py index 8950af17..1fd4d242 100644 --- a/blender-for-unrealengine/bfu_import_module/__init__.py +++ b/blender-for-unrealengine/bfu_import_module/__init__.py @@ -18,7 +18,8 @@ import importlib -from . import bps +from . import bpl +from . import config from . import import_module_utils from . import import_module_unreal_utils from . import import_module_post_treatment @@ -26,11 +27,17 @@ from . import asset_import from . import sequencer_import from . import sequencer_utils +from . import bfu_import_animations from . import bfu_import_materials +from . import bfu_import_vertex_color from . import bfu_import_sequencer +from . import import_module_tasks_class +from . import import_module_tasks_helper -if "bps" in locals(): - importlib.reload(bps) +if "bpl" in locals(): + importlib.reload(bpl) +if "config" in locals(): + importlib.reload(config) if "import_module_utils" in locals(): importlib.reload(import_module_utils) if "import_module_unreal_utils" in locals(): @@ -43,15 +50,24 @@ importlib.reload(sequencer_import) if "sequencer_utils" in locals(): importlib.reload(sequencer_utils) +if "bfu_import_animations" in locals(): + importlib.reload(bfu_import_animations) if "bfu_import_materials" in locals(): importlib.reload(bfu_import_materials) +if "bfu_import_vertex_color" in locals(): + importlib.reload(bfu_import_vertex_color) if "bfu_import_sequencer" in locals(): importlib.reload(bfu_import_sequencer) +if "import_module_tasks_class" in locals(): + importlib.reload(import_module_tasks_class) +if "import_module_tasks_helper" in locals(): + importlib.reload(import_module_tasks_helper) -def run_asset_import(assets_data, show_finished_popup=True): +def run_asset_import(assets_data, show_finished_popup=False): if asset_import.ready_for_asset_import(): return asset_import.ImportAllAssets(assets_data, show_finished_popup) -def run_sequencer_import(sequence_data, show_finished_popup=True): +def run_sequencer_import(sequence_data, show_finished_popup=False): if sequencer_import.ready_for_sequence_import(): - return sequencer_import.CreateSequencer(sequence_data, show_finished_popup) \ No newline at end of file + return sequencer_import.CreateSequencer(sequence_data, show_finished_popup) + diff --git a/blender-for-unrealengine/bfu_import_module/asset_import.py b/blender-for-unrealengine/bfu_import_module/asset_import.py index 0c63e60d..e7669321 100644 --- a/blender-for-unrealengine/bfu_import_module/asset_import.py +++ b/blender-for-unrealengine/bfu_import_module/asset_import.py @@ -18,11 +18,16 @@ import os.path -from . import bps +from . import bpl from . import import_module_utils from . import import_module_unreal_utils from . import import_module_post_treatment +from . import import_module_tasks_class +from . import import_module_tasks_helper +from . import bfu_import_animations from . import bfu_import_materials +from . import bfu_import_vertex_color +from . import config try: import unreal @@ -33,26 +38,26 @@ def ready_for_asset_import(): - if import_module_unreal_utils.is_unreal_version_greater_or_equal(4,20): # TO DO: EditorAssetLibrary was added in witch version exactly? - if not hasattr(unreal, 'EditorAssetLibrary'): - message = 'WARNING: Editor Scripting Utilities should be activated.' + "\n" - message += 'Edit > Plugin > Scripting > Editor Scripting Utilities.' - import_module_unreal_utils.show_warning_message("Editor Scripting Utilities not activated.", message) - return False + if not import_module_unreal_utils.editor_scripting_utilities_active(): + message = 'WARNING: Editor Scripting Utilities Plugin should be activated.' + "\n" + message += 'Edit > Plugin > Scripting > Editor Scripting Utilities.' + import_module_unreal_utils.show_warning_message("Editor Scripting Utilities not activated.", message) + return False return True -def ImportAsset(asset_data): +def ImportTask(asset_data): + asset_type = asset_data["asset_type"] - if asset_data["asset_type"] == "StaticMesh" or asset_data["asset_type"] == "SkeletalMesh": + if asset_type == "StaticMesh" or asset_type == "SkeletalMesh": if "lod" in asset_data: if asset_data["lod"] > 0: # Lod should not be imported here so return if lod is not 0. return "FAIL", None - if asset_data["asset_type"] == "Alembic": + if asset_type == "Alembic": FileType = "ABC" else: FileType = "FBX" @@ -65,73 +70,54 @@ def GetAdditionalData(): asset_additional_data = GetAdditionalData() - if asset_data["asset_type"] in ["Animation", "SkeletalMesh"]: - origin_skeletal_mesh = None + if asset_type in ["Animation", "SkeletalMesh"]: origin_skeleton = None + origin_skeletal_mesh = None - find_asset = unreal.find_asset(asset_data["target_skeleton_ref"]) - if isinstance(find_asset, unreal.Skeleton): - origin_skeleton = find_asset - elif isinstance(find_asset, unreal.SkeletalMesh): - origin_skeletal_mesh = find_asset - origin_skeleton = find_asset.skeleton - else: - origin_skeleton = None + + if "target_skeleton_ref" in asset_data: + find_sk_asset = import_module_unreal_utils.load_asset(asset_data["target_skeleton_ref"]) + if isinstance(find_sk_asset, unreal.Skeleton): + origin_skeleton = find_sk_asset + elif isinstance(find_sk_asset, unreal.SkeletalMesh): + origin_skeleton = find_sk_asset.skeleton + origin_skeletal_mesh = find_sk_asset + + if "target_skeletal_mesh_ref" in asset_data: + find_skm_asset = import_module_unreal_utils.load_asset(asset_data["target_skeletal_mesh_ref"]) + if isinstance(find_skm_asset, unreal.SkeletalMesh): + origin_skeleton = find_skm_asset.skeleton + origin_skeletal_mesh = find_skm_asset + elif isinstance(find_skm_asset, unreal.Skeleton): + origin_skeletal_mesh = find_skm_asset - if origin_skeleton is None: - if asset_data["asset_type"] == "Animation": + + if asset_type == "Animation": + if origin_skeleton: + print('"target_skeleton_ref": ' + asset_data["target_skeleton_ref"] + "was found:", origin_skeleton) + else: message = "WARNING: Could not find skeleton." + "\n" message += '"target_skeleton_ref": ' + asset_data["target_skeleton_ref"] import_module_unreal_utils.show_warning_message("Skeleton not found.", message) - # docs.unrealengine.com/5.3/en-US/PythonAPI/class/AssetImportTask.html - task = unreal.AssetImportTask() + itask = import_module_tasks_class.ImportTaks() - def GetStaticMeshImportData() -> unreal.FbxStaticMeshImportData: - if asset_data["asset_type"] == "StaticMesh": - return task.get_editor_property('options').static_mesh_import_data - return None - - def GetSkeletalMeshImportData() -> unreal.FbxSkeletalMeshImportData: - if asset_data["asset_type"] == "SkeletalMesh": - return task.get_editor_property('options').skeletal_mesh_import_data - return None - - def GetAnimationImportData() -> unreal.FbxAnimSequenceImportData: - if asset_data["asset_type"] == "Animation": - return task.get_editor_property('options').anim_sequence_import_data - return None - - def GetAlembicImportData(): - if asset_data["asset_type"] == "Alembic": - return task.get_editor_property('options') - return None - - def GetMeshImportData(): - if asset_data["asset_type"] == "StaticMesh": - return GetStaticMeshImportData() - if asset_data["asset_type"] == "SkeletalMesh": - return GetSkeletalMeshImportData() - return None - - if asset_data["asset_type"] == "Alembic": - task.filename = asset_data["abc_path"] - else: - task.filename = asset_data["fbx_path"] - task.destination_path = os.path.normpath(asset_data["full_import_path"]).replace('\\', '/') - task.automated = True - # task.automated = False #Debug for show dialog - task.save = True - task.replace_existing = True - - if asset_data["asset_type"] == "Alembic": - task.set_editor_property('options', unreal.AbcImportSettings()) + if asset_type == "Alembic": + itask.get_task().filename = asset_data["abc_path"] else: - task.set_editor_property('options', unreal.FbxImportUI()) - + itask.get_task().filename = asset_data["fbx_path"] + itask.get_task().destination_path = os.path.normpath(asset_data["full_import_path"]).replace('\\', '/') + itask.get_task().automated = config.automated_import_tasks + itask.get_task().save = True + itask.get_task().replace_existing = True + + TaskOption = import_module_tasks_helper.init_options_data(asset_type, itask.use_interchange) + itask.set_task_option(TaskOption) + print("S1") # Alembic - alembic_import_data = GetAlembicImportData() - if alembic_import_data: + + if asset_type == "Alembic": + alembic_import_data = itask.get_abc_import_settings() alembic_import_data.static_mesh_settings.set_editor_property("merge_meshes", True) alembic_import_data.set_editor_property("import_type", unreal.AlembicImportType.SKELETAL) alembic_import_data.conversion_settings.set_editor_property("flip_u", False) @@ -142,168 +128,225 @@ def GetMeshImportData(): alembic_import_data.conversion_settings.set_editor_property("scale", ue_scale) alembic_import_data.conversion_settings.set_editor_property("rotation", rotation) - # Vertex color - vertex_override_color = import_module_unreal_utils.get_vertex_override_color(asset_additional_data) - vertex_color_import_option = import_module_unreal_utils.get_vertex_color_import_option(asset_additional_data) - # #################################[Change] # unreal.FbxImportUI # https://docs.unrealengine.com/4.26/en-US/PythonAPI/class/FbxImportUI.html + print("S1.5") # Import transform - anim_sequence_import_data = GetAnimationImportData() - if anim_sequence_import_data: + if itask.use_interchange: + animation_pipeline = itask.get_igap_animation() + if "do_not_import_curve_with_zero" in asset_data: + animation_pipeline.set_editor_property('do_not_import_curve_with_zero', asset_data["do_not_import_curve_with_zero"]) + + else: + anim_sequence_import_data = itask.get_animation_import_data() anim_sequence_import_data.import_translation = unreal.Vector(0, 0, 0) if "do_not_import_curve_with_zero" in asset_data: anim_sequence_import_data.set_editor_property('do_not_import_curve_with_zero', asset_data["do_not_import_curve_with_zero"]) - # Vertex color - if vertex_color_import_option and GetMeshImportData(): - GetMeshImportData().set_editor_property('vertex_color_import_option', vertex_color_import_option) + print("S2") - if vertex_override_color and GetMeshImportData(): - GetMeshImportData().set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) - - if asset_data["asset_type"] == "Alembic": - task.get_editor_property('options').set_editor_property('import_type', unreal.AlembicImportType.SKELETAL) + if asset_type in ["SkeletalMesh", "StaticMesh"]: + # Vertex color + bfu_import_vertex_color.bfu_import_vertex_color_utils.apply_import_settings(itask, asset_type, asset_additional_data) + if asset_type == "Alembic": + print("S2.1") + itask.get_abc_import_settings().set_editor_property('import_type', unreal.AlembicImportType.SKELETAL) + else: - if asset_data["asset_type"] == "Animation" or asset_data["asset_type"] == "SkeletalMesh": - if origin_skeleton: - task.get_editor_property('options').set_editor_property('Skeleton', origin_skeleton) + + print("S2.2") + if asset_type == "Animation" : + if itask.use_interchange: + if origin_skeleton: + itask.get_igap_skeletal_mesh().set_editor_property('Skeleton', origin_skeleton) + itask.get_igap_skeletal_mesh().set_editor_property('import_only_animations', True) + print("S2.25") + else: + fail_reason = 'Skeleton ' + asset_data["target_skeleton_ref"] + ' Not found for ' + asset_data["asset_name"] + ' asset.' + return fail_reason, None + else: - if asset_data["asset_type"] == "Animation": + if origin_skeleton: + itask.get_fbx_import_ui().set_editor_property('Skeleton', origin_skeleton) + else: fail_reason = 'Skeleton ' + asset_data["target_skeleton_ref"] + ' Not found for ' + asset_data["asset_name"] + ' asset.' return fail_reason, None + + print("S2.3") + if asset_type == "SkeletalMesh": + if itask.use_interchange: + if origin_skeleton: + itask.get_igap_skeletal_mesh().set_editor_property('Skeleton', origin_skeleton) else: print("Skeleton is not set, a new skeleton asset will be created...") + + else: + if origin_skeleton: + itask.get_fbx_import_ui().set_editor_property('Skeleton', origin_skeleton) + else: + print("Skeleton is not set, a new skeleton asset will be created...") + + + print("S3") + # Set Asset Type + if itask.use_interchange: + if asset_type == "StaticMesh": + itask.get_igap_common_mesh().set_editor_property('force_all_mesh_as_type', unreal.InterchangeForceMeshType.IFMT_STATIC_MESH) + if asset_type == "SkeletalMesh": + itask.get_igap_common_mesh().set_editor_property('force_all_mesh_as_type', unreal.InterchangeForceMeshType.IFMT_SKELETAL_MESH) + if asset_type == "Animation": + itask.get_igap_common_mesh().set_editor_property('force_all_mesh_as_type', unreal.InterchangeForceMeshType.IFMT_NONE) + else: + itask.get_igap_common_mesh().set_editor_property('force_all_mesh_as_type', unreal.InterchangeForceMeshType.IFMT_NONE) - - if asset_data["asset_type"] == "StaticMesh": - task.get_editor_property('options').set_editor_property('original_import_type', unreal.FBXImportType.FBXIT_STATIC_MESH) - elif asset_data["asset_type"] == "Animation": - task.get_editor_property('options').set_editor_property('original_import_type', unreal.FBXImportType.FBXIT_ANIMATION) else: - task.get_editor_property('options').set_editor_property('original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) - - if asset_data["asset_type"] == "Animation": - task.get_editor_property('options').set_editor_property('import_materials', False) + if asset_type == "StaticMesh": + itask.get_fbx_import_ui().set_editor_property('original_import_type', unreal.FBXImportType.FBXIT_STATIC_MESH) + elif asset_type == "Animation": + itask.get_fbx_import_ui().set_editor_property('original_import_type', unreal.FBXImportType.FBXIT_ANIMATION) + else: + itask.get_fbx_import_ui().set_editor_property('original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) + print("S4") + # Set Material Use + if itask.use_interchange: + if asset_type == "Animation": + itask.get_igap_material().set_editor_property('import_materials', False) + else: + itask.get_igap_material().set_editor_property('import_materials', True) else: - task.get_editor_property('options').set_editor_property('import_materials', True) - - task.get_editor_property('options').set_editor_property('import_textures', False) - - if asset_data["asset_type"] == "Animation": - - task.get_editor_property('options').set_editor_property('import_animations', True) - task.get_editor_property('options').set_editor_property('import_mesh', False) - task.get_editor_property('options').set_editor_property('create_physics_asset',False) + if asset_type == "Animation": + itask.get_fbx_import_ui().set_editor_property('import_materials', False) + else: + itask.get_fbx_import_ui().set_editor_property('import_materials', True) + print("S5") + # Set Texture Use + if itask.use_interchange: + itask.get_igap_texture().set_editor_property('import_textures', False) + else: + itask.get_fbx_import_ui().set_editor_property('import_textures', False) + + print("S6") + if itask.use_interchange: + if asset_type == "Animation": + itask.get_igap_animation().set_editor_property('import_animations', True) + itask.get_igap_mesh().set_editor_property('import_skeletal_meshes', False) + itask.get_igap_mesh().set_editor_property('import_static_meshes', False) + itask.get_igap_mesh().set_editor_property('create_physics_asset',False) + else: + itask.get_igap_animation().set_editor_property('import_animations', False) + itask.get_igap_mesh().set_editor_property('import_skeletal_meshes', True) + itask.get_igap_mesh().set_editor_property('import_static_meshes', True) + if "create_physics_asset" in asset_data: + itask.get_igap_mesh().set_editor_property('create_physics_asset', asset_data["create_physics_asset"]) else: - task.get_editor_property('options').set_editor_property('import_animations', False) - task.get_editor_property('options').set_editor_property('import_mesh', True) - if "create_physics_asset" in asset_data: - task.get_editor_property('options').set_editor_property('create_physics_asset', asset_data["create_physics_asset"]) + if asset_type == "Animation": + itask.get_fbx_import_ui().set_editor_property('import_as_skeletal',True) + itask.get_fbx_import_ui().set_editor_property('import_animations', True) + itask.get_fbx_import_ui().set_editor_property('import_mesh', False) + itask.get_fbx_import_ui().set_editor_property('create_physics_asset',False) + else: + itask.get_fbx_import_ui().set_editor_property('import_animations', False) + itask.get_fbx_import_ui().set_editor_property('import_mesh', True) + if "create_physics_asset" in asset_data: + itask.get_fbx_import_ui().set_editor_property('create_physics_asset', asset_data["create_physics_asset"]) # unreal.FbxMeshImportData - - bfu_import_materials.bfu_import_materials_utils.update_task_with_material_data(task, asset_data) - - - if asset_data["asset_type"] == "StaticMesh": - # unreal.FbxStaticMeshImportData - task.get_editor_property('options').static_mesh_import_data.set_editor_property('combine_meshes', True) - if "auto_generate_collision" in asset_data: - task.get_editor_property('options').static_mesh_import_data.set_editor_property('auto_generate_collision', asset_data["auto_generate_collision"]) + print("S7") + bfu_import_materials.bfu_import_materials_utils.apply_import_settings(itask, asset_data) + + print("S8") + if itask.use_interchange: + itask.get_igap_mesh().set_editor_property('combine_static_meshes', True) + itask.get_igap_mesh().set_editor_property('combine_skeletal_meshes', True) + # @TODO auto_generate_collision Removed with InterchangeGenericAssetsPipeline? + # I yes need also remove auto_generate_collision from the addon propertys. if "static_mesh_lod_group" in asset_data: - if asset_data["static_mesh_lod_group"]: - task.get_editor_property('options').static_mesh_import_data.set_editor_property('static_mesh_lod_group', asset_data["static_mesh_lod_group"]) + lod_group = asset_data["static_mesh_lod_group"] + if lod_group: + itask.get_igap_mesh().set_editor_property('lod_group', lod_group) if "generate_lightmap_u_vs" in asset_data: - task.get_editor_property('options').static_mesh_import_data.set_editor_property('generate_lightmap_u_vs', asset_data["generate_lightmap_u_vs"]) + itask.get_igap_mesh().set_editor_property('generate_lightmap_u_vs', asset_data["generate_lightmap_u_vs"]) + itask.get_igap_mesh().set_editor_property('import_morph_targets', True) - if asset_data["asset_type"] == "SkeletalMesh" or asset_data["asset_type"] == "Animation": - # unreal.FbxSkeletalMeshImportData - task.get_editor_property('options').skeletal_mesh_import_data.set_editor_property('import_morph_targets', True) - task.get_editor_property('options').skeletal_mesh_import_data.set_editor_property('convert_scene', True) - task.get_editor_property('options').skeletal_mesh_import_data.set_editor_property('normal_import_method', unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS_AND_TANGENTS) + else: + if asset_type == "StaticMesh": + # unreal.FbxStaticMeshImportData + itask.get_static_mesh_import_data().set_editor_property('combine_meshes', True) + if "auto_generate_collision" in asset_data: + itask.get_static_mesh_import_data().set_editor_property('auto_generate_collision', asset_data["auto_generate_collision"]) + if "static_mesh_lod_group" in asset_data: + lod_group = asset_data["static_mesh_lod_group"] + if lod_group: + itask.get_static_mesh_import_data().set_editor_property('static_mesh_lod_group', lod_group) + if "generate_lightmap_u_vs" in asset_data: + itask.get_static_mesh_import_data().set_editor_property('generate_lightmap_u_vs', asset_data["generate_lightmap_u_vs"]) + + if asset_type == "SkeletalMesh" or asset_type == "Animation": + # unreal.FbxSkeletalMeshImportData + itask.get_skeletal_mesh_import_data().set_editor_property('import_morph_targets', True) + itask.get_skeletal_mesh_import_data().set_editor_property('convert_scene', True) + itask.get_skeletal_mesh_import_data().set_editor_property('normal_import_method', unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS_AND_TANGENTS) # ###############[ pre import ]################ - + print("S9") # Check is the file alredy exit if asset_additional_data: if "preview_import_path" in asset_additional_data: - task_asset_full_path = task.destination_path+"/"+asset_additional_data["preview_import_path"]+"."+asset_additional_data["preview_import_path"] - find_asset = unreal.find_asset(task_asset_full_path) - if find_asset: + task_asset_full_path = itask.get_task().destination_path+"/"+asset_additional_data["preview_import_path"]+"."+asset_additional_data["preview_import_path"] + find_target_asset = import_module_unreal_utils.load_asset(task_asset_full_path) + if find_target_asset: # Vertex color - - asset_import_data = find_asset.get_editor_property('asset_import_data') - if vertex_color_import_option: - asset_import_data.set_editor_property('vertex_color_import_option', vertex_color_import_option) - - if vertex_override_color: - asset_import_data.set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) - + bfu_import_vertex_color.bfu_import_vertex_color_utils.apply_asset_settings(itask, find_target_asset, asset_additional_data) + # ###############[ import asset ]################ - - if asset_data["asset_type"] == "Animation": + print("S10") + if asset_type == "Animation": # For animation the script will import a skeletal mesh and remove after. - # If the skeletal mesh alredy exist try to remove. - - - AssetName = asset_data["asset_name"] - AssetName = import_module_unreal_utils.ValidUnrealAssetsName(AssetName) - AssetPath = "SkeletalMesh'"+asset_data["full_import_path"]+"/"+AssetName+"."+AssetName+"'" - - if unreal.EditorAssetLibrary.does_asset_exist(AssetPath): - oldAsset = unreal.EditorAssetLibrary.find_asset_data(AssetPath) - if oldAsset.asset_class == "SkeletalMesh": - unreal.EditorAssetLibrary.delete_asset(AssetPath) + # If the skeletal mesh already exists, try to remove it. - unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + asset_name = import_module_unreal_utils.valid_unreal_asset_name(asset_data["asset_name"]) + asset_path = f"SkeletalMesh'{asset_data['full_import_path']}/{asset_name}.{asset_name}'" - if len(task.imported_object_paths) > 0: - asset_path = task.imported_object_paths[0] - asset = unreal.find_asset(asset_path) - else: - asset = None + if unreal.EditorAssetLibrary.does_asset_exist(asset_path): + old_asset = unreal.EditorAssetLibrary.find_asset_data(asset_path) + if old_asset.asset_class == "SkeletalMesh": + unreal.EditorAssetLibrary.delete_asset(asset_path) - if asset is None: + print("S10.5") + itask.import_asset_task() + print("S11") + + if len(itask.get_imported_assets()) == 0: fail_reason = 'Error zero imported object for: ' + asset_data["asset_name"] return fail_reason, None - + + print("S11.5") if asset_data["asset_type"] == "Animation": - # For animation remove the extra mesh - if type(asset) is not unreal.AnimSequence: - p = task.imported_object_paths[0] - animAssetName = p.split('.')[0]+'_anim.'+p.split('.')[1]+'_anim' - animAssetNameDesiredPath = p.split('.')[0]+'.'+p.split('.')[1] - animAsset = unreal.find_asset(animAssetName) - if animAsset is not None: - unreal.EditorAssetLibrary.delete_asset(p) - unreal.EditorAssetLibrary.rename_asset(animAssetName, animAssetNameDesiredPath) - asset = animAsset - else: - fail_reason = 'animAsset ' + asset_data["asset_name"] + ' not found for after inport: ' + animAssetName - return fail_reason, None + bfu_import_animations.bfu_import_animations_utils.apply_post_import_assets_changes(itask, asset_data) + print("S12") # ###############[ Post treatment ]################ - asset_import_data = asset.get_editor_property('asset_import_data') - if asset_data["asset_type"] == "StaticMesh": + + + if asset_type == "StaticMesh": if "static_mesh_lod_group" in asset_data: if asset_data["static_mesh_lod_group"]: - asset.set_editor_property('lod_group', asset_data["static_mesh_lod_group"]) + itask.get_imported_static_mesh().set_editor_property('lod_group', asset_data["static_mesh_lod_group"]) if "use_custom_light_map_resolution" in asset_data: if asset_data["use_custom_light_map_resolution"]: if "light_map_resolution" in asset_data: - asset.set_editor_property('light_map_resolution', asset_data["light_map_resolution"]) - build_settings = unreal.EditorStaticMeshLibrary.get_lod_build_settings(asset, 0) + itask.get_imported_static_mesh().set_editor_property('light_map_resolution', asset_data["light_map_resolution"]) + build_settings = unreal.EditorStaticMeshLibrary.get_lod_build_settings(itask.get_imported_static_mesh(), 0) build_settings.min_lightmap_resolution = asset_data["light_map_resolution"] - unreal.EditorStaticMeshLibrary.set_lod_build_settings(asset, 0, build_settings) + unreal.EditorStaticMeshLibrary.set_lod_build_settings(itask.get_imported_static_mesh(), 0, build_settings) if "collision_trace_flag" in asset_data: - collision_data = asset.get_editor_property('body_setup') + collision_data = itask.get_imported_static_mesh().get_editor_property('body_setup') if collision_data: if asset_data["collision_trace_flag"] == "CTF_UseDefault": collision_data.set_editor_property('collision_trace_flag', unreal.CollisionTraceFlag.CTF_USE_DEFAULT) @@ -314,31 +357,47 @@ def GetMeshImportData(): elif asset_data["collision_trace_flag"] == "CTF_UseComplexAsSimple": collision_data.set_editor_property('collision_trace_flag', unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE) + print("S13") + if asset_type == "SkeletalMesh": + if origin_skeleton is None: + # Unreal create a new skeleton when no skeleton was selected, so addon rename it. + skeleton = itask.get_imported_skeleton() + if skeleton: + unreal.EditorAssetLibrary.rename_asset(skeleton.get_path_name(), asset_data["target_skeleton_ref"]) + + print("S13.5") + if itask.use_interchange: + if asset_type == "StaticMesh": + if "generate_lightmap_u_vs" in asset_data: + mesh_pipeline = itask.get_imported_static_mesh().get_editor_property('asset_import_data').get_pipelines()[0].get_editor_property('mesh_pipeline') + mesh_pipeline.set_editor_property('generate_lightmap_u_vs', asset_data["generate_lightmap_u_vs"]) # Import data + unreal.EditorStaticMeshLibrary.set_generate_lightmap_uv(itask.get_imported_static_mesh(), asset_data["generate_lightmap_u_vs"]) # Build settings at lod + + if asset_type == "SkeletalMesh": + if "enable_skeletal_mesh_per_poly_collision" in asset_data: + itask.get_imported_skeletal_mesh().set_editor_property('enable_per_poly_collision', asset_data["enable_skeletal_mesh_per_poly_collision"]) + + else: + if asset_type == "StaticMesh": + asset_import_data = itask.get_imported_static_mesh().get_editor_property('asset_import_data') + if "generate_lightmap_u_vs" in asset_data: + asset_import_data.set_editor_property('generate_lightmap_u_vs', asset_data["generate_lightmap_u_vs"]) # Import data + unreal.EditorStaticMeshLibrary.set_generate_lightmap_uv(itask.get_imported_static_mesh(), asset_data["generate_lightmap_u_vs"]) # Build settings at lod - if asset_data["asset_type"] == "StaticMesh": - if "generate_lightmap_u_vs" in asset_data: - asset_import_data.set_editor_property('generate_lightmap_u_vs', asset_data["generate_lightmap_u_vs"]) # Import data - unreal.EditorStaticMeshLibrary.set_generate_lightmap_uv(asset, asset_data["generate_lightmap_u_vs"]) # Build settings at lod + elif asset_type == "SkeletalMesh": + asset_import_data = itask.get_imported_skeletal_mesh().get_editor_property('asset_import_data') + asset_import_data.set_editor_property('normal_import_method', unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS_AND_TANGENTS) - if asset_data["asset_type"] == "SkeletalMesh": - asset_import_data.set_editor_property('normal_import_method', unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS_AND_TANGENTS) - if origin_skeleton is None: - #Unreal create a new skeleton when no skeleton was selected, so addon rename it. - p = task.imported_object_paths[0] - old_skeleton_name = p.split('.')[0]+'_Skeleton.'+p.split('.')[1]+'_Skeleton' - new_skeleton_name = asset_data["target_skeleton_ref"] - unreal.EditorAssetLibrary.rename_asset(old_skeleton_name, new_skeleton_name) - - if "enable_skeletal_mesh_per_poly_collision" in asset_data: - asset.set_editor_property('enable_per_poly_collision', asset_data["enable_skeletal_mesh_per_poly_collision"]) + if "enable_skeletal_mesh_per_poly_collision" in asset_data: + itask.get_imported_skeletal_mesh().set_editor_property('enable_per_poly_collision', asset_data["enable_skeletal_mesh_per_poly_collision"]) - + print("S14") # Socket - if asset_data["asset_type"] == "SkeletalMesh": + if asset_type == "SkeletalMesh": # Import the SkeletalMesh socket(s) sockets_to_add = asset_additional_data["Sockets"] for socket in sockets_to_add: - old_socket = asset.find_socket(socket["SocketName"]) + old_socket = itask.get_imported_skeletal_mesh().find_socket(socket["SocketName"]) if old_socket: # Edit socket pass @@ -358,30 +417,47 @@ def GetMeshImportData(): # NEED UNREAL ENGINE IMPLEMENTATION IN PYTHON API. # skeleton.add_socket(new_socket) + print("S15") # Lod - if asset_data["asset_type"] == "StaticMesh": - import_module_post_treatment.set_static_mesh_lods(asset, asset_data, asset_additional_data) + if asset_type == "StaticMesh": + import_module_post_treatment.set_static_mesh_lods(itask.get_imported_static_mesh(), itask.get_task_options(), asset_data, asset_additional_data) - if asset_data["asset_type"] == "SkeletalMesh": - import_module_post_treatment.set_skeletal_mesh_lods(asset, asset_data, asset_additional_data) + print("S15.1") + if asset_type == "SkeletalMesh": + import_module_post_treatment.set_skeletal_mesh_lods(itask.get_imported_skeletal_mesh(), itask.get_task_options(), asset_data, asset_additional_data) + print("S15.2") # Preview mesh - if asset_data["asset_type"] == "Animation": - import_module_post_treatment.set_sequence_preview_skeletal_mesh(asset, origin_skeletal_mesh) - - # Vertex color - if vertex_override_color: - asset_import_data.set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) - - if vertex_color_import_option: - asset_import_data.set_editor_property('vertex_color_import_option', vertex_color_import_option) - + if asset_type == "Animation": + import_module_post_treatment.set_sequence_preview_skeletal_mesh(itask.get_imported_anim_sequence(), origin_skeletal_mesh) + + print("S15.3") + if asset_type in ["SkeletalMesh", "StaticMesh"]: + # Vertex color + bfu_import_vertex_color.bfu_import_vertex_color_utils.apply_asset_settings(itask, itask.get_imported_static_mesh(), asset_additional_data) + bfu_import_vertex_color.bfu_import_vertex_color_utils.apply_asset_settings(itask, itask.get_imported_skeletal_mesh(), asset_additional_data) + + print("S15.4") + if asset_type == "Alembic": + pass + # @TODO Need to found how create an physical asset, generate bodies, and assign it. + """ + skeletal_mesh_path = itask.GetImportedSkeletalMeshAsset().get_path_name() + path = skeletal_mesh_path.rsplit('/', 1)[0] + name = skeletal_mesh_path.rsplit('/', 1)[1] + "_Physics" + + physical_asset_factory = unreal.PhysicsAssetFactory() + physical_asset = unreal.AssetToolsHelpers.get_asset_tools().create_asset( + asset_name=name, + package_path=path, + asset_class=unreal.PhysicsAsset, + factory=physical_asset_factory + ) + """ + + print("S16") # #################################[EndChange] - if asset_data["asset_type"] == "StaticMesh" or asset_data["asset_type"] == "SkeletalMesh": - unreal.EditorAssetLibrary.save_loaded_asset(asset) - return "SUCCESS", asset - - + return "SUCCESS", itask.get_imported_assets() def ImportAllAssets(assets_data, show_finished_popup=True): ImportedList = [] @@ -394,13 +470,14 @@ def GetAssetByType(type): target_assets.append(asset) return target_assets - def PrepareImportAsset(asset_data): + def PrepareImportTask(asset_data): counter = str(len(ImportedList)+1) + "/" + str(len(assets_data["assets"])) print("Import asset " + counter + ": ", asset_data["asset_name"]) - - result, asset = ImportAsset(asset_data) + print("S0") + result, assets = ImportTask(asset_data) + print("S17") if result == "SUCCESS": - ImportedList.append([asset, asset_data["asset_type"]]) + ImportedList.append([assets, asset_data["asset_type"]]) else: ImportFailList.append(result) @@ -408,18 +485,18 @@ def PrepareImportAsset(asset_data): # Process import print('========================= Import started ! =========================') - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() # Import assets with a specific order for asset_data in GetAssetByType("Alembic"): - PrepareImportAsset(asset_data) + PrepareImportTask(asset_data) for asset_data in GetAssetByType("StaticMesh"): - PrepareImportAsset(asset_data) + PrepareImportTask(asset_data) for asset_data in GetAssetByType("SkeletalMesh"): - PrepareImportAsset(asset_data) + PrepareImportTask(asset_data) for asset_data in GetAssetByType("Animation"): - PrepareImportAsset(asset_data) + PrepareImportTask(asset_data) print('========================= Full import completed ! =========================') @@ -428,15 +505,17 @@ def PrepareImportAsset(asset_data): SkeletalMesh_ImportedList = [] Alembic_ImportedList = [] Animation_ImportedList = [] - for asset in ImportedList: - if asset[1] == 'StaticMesh': - StaticMesh_ImportedList.append(asset[0]) - elif asset[1] == 'SkeletalMesh': - SkeletalMesh_ImportedList.append(asset[0]) - elif asset[1] == 'Alembic': - Alembic_ImportedList.append(asset[0]) + for inport_data in ImportedList: + assets = inport_data[0] + source_asset_type = inport_data[1] + if source_asset_type == 'StaticMesh': + StaticMesh_ImportedList.append(assets) + elif source_asset_type == 'SkeletalMesh': + SkeletalMesh_ImportedList.append(assets) + elif source_asset_type == 'Alembic': + Alembic_ImportedList.append(assets) else: - Animation_ImportedList.append(asset[0]) + Animation_ImportedList.append(assets) import_log = [] import_log.append('Imported StaticMesh: '+str(len(StaticMesh_ImportedList))) @@ -453,8 +532,9 @@ def PrepareImportAsset(asset_data): # Select asset(s) in content browser PathList = [] - for asset in (StaticMesh_ImportedList + SkeletalMesh_ImportedList + Alembic_ImportedList + Animation_ImportedList): - PathList.append(asset.get_path_name()) + for assets in (StaticMesh_ImportedList + SkeletalMesh_ImportedList + Alembic_ImportedList + Animation_ImportedList): + for asset in assets: + PathList.append(asset.get_path_name()) unreal.EditorAssetLibrary.sync_browser_to_objects(PathList) print('=========================') diff --git a/blender-for-unrealengine/bfu_import_module/asset_import_script.py b/blender-for-unrealengine/bfu_import_module/asset_import_script.py index b529a59b..dcfd6d3b 100644 --- a/blender-for-unrealengine/bfu_import_module/asset_import_script.py +++ b/blender-for-unrealengine/bfu_import_module/asset_import_script.py @@ -1,65 +1,30 @@ -# This script was generated with the addons Blender for UnrealEngine : https://github.com/xavier150/Blender-For-UnrealEngine-Addons -# It will import into Unreal Engine all the assets of type StaticMesh, SkeletalMesh, Animation and Pose -# The script must be used in Unreal Engine Editor with Python plugins : https://docs.unrealengine.com/en-US/Engine/Editor/ScriptingAndAutomation/Python -# Use this command in Unreal cmd consol: py "[ScriptLocation]\asset_import_script.py" +# This script was generated with the addons Unreal Engine Assets Exporter. +# This script should be run in Unreal Engine to import into Unreal Engine 4 and 5 assets. +# The assets are exported from from Unreal Engine Assets Exporter. More detail here. https://github.com/xavier150/Blender-For-UnrealEngine-Addons +# Use the following command in Unreal Engine cmd consol to import assets: +# py "[ScriptLocation]\asset_import_script.py" import importlib -import sys +import importlib.util import os -import json +from . import import_module_utils -def JsonLoad(json_file): - # Changed in Python 3.9: The keyword argument encoding has been removed. - if sys.version_info >= (3, 9): - return json.load(json_file) - else: - return json.load(json_file, encoding="utf8") - -def JsonLoadFile(json_file_path): - if sys.version_info[0] < 3: - with open(json_file_path, "r") as json_file: - return JsonLoad(json_file) - else: - with open(json_file_path, "r", encoding="utf8") as json_file: - return JsonLoad(json_file) - -def load_module(import_module_path): - # Import and run the module - module_name = os.path.basename(import_module_path).replace('.py', '') - module_dir = os.path.dirname(import_module_path) - - if module_dir not in sys.path: - sys.path.append(module_dir) - - imported_module = importlib.import_module(module_name) - importlib.reload(imported_module) - - # Assuming the module has a main function to run - if hasattr(imported_module, 'main'): - imported_module.main() - - return imported_module, module_name - -def unload_module(module_name): - # Vérifier si le module est dans sys.modules - if module_name in sys.modules: - # Récupérer la référence du module - module = sys.modules[module_name] - # Supprimer la référence globale - del sys.modules[module_name] - del module def RunImportScriptWithJsonData(): # Prepare process import json_data_file = 'ImportAssetData.json' dir_path = os.path.dirname(os.path.realpath(__file__)) - - import_assets_data = JsonLoadFile(os.path.join(dir_path, json_data_file)) + import_file_path = os.path.join(dir_path, json_data_file) + assets_data = import_module_utils.JsonLoadFile(import_file_path) - import_module_path = import_assets_data["info"]["import_modiule_path"] # Module to run - imported_module, module_name = load_module(import_module_path) - imported_module.run_asset_import(import_assets_data, False) - unload_module(module_name) + file_path = os.path.join(assets_data["info"]["addon_path"],'run_unreal_import_script.py') + spec = importlib.util.spec_from_file_location("__import_assets__", file_path) + module = importlib.util.module_from_spec(spec) + + # Run script module function + spec.loader.exec_module(module) + module.run_from_asset_import_script(import_file_path) if __name__ == "__main__": RunImportScriptWithJsonData() + diff --git a/blender-for-unrealengine/bfu_import_module/bfu_import_animations/__init__.py b/blender-for-unrealengine/bfu_import_module/bfu_import_animations/__init__.py new file mode 100644 index 00000000..e2759498 --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/bfu_import_animations/__init__.py @@ -0,0 +1,24 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import importlib + +from . import bfu_import_animations_utils + +if "bfu_import_animations_utils" in locals(): + importlib.reload(bfu_import_animations_utils) diff --git a/blender-for-unrealengine/bfu_import_module/bfu_import_animations/bfu_import_animations_utils.py b/blender-for-unrealengine/bfu_import_module/bfu_import_animations/bfu_import_animations_utils.py new file mode 100644 index 00000000..e79bdf54 --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/bfu_import_animations/bfu_import_animations_utils.py @@ -0,0 +1,85 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +from .. import import_module_unreal_utils +from .. import import_module_tasks_class + +try: + import unreal +except ImportError: + import unreal_engine as unreal + + + +def apply_post_import_assets_changes(itask: import_module_tasks_class.ImportTaks, asset_data): + """Applies post-import changes based on whether Interchange or FBX is used.""" + if itask.use_interchange: + apply_interchange_post_import(itask, asset_data) + else: + apply_fbxui_post_import(itask, asset_data) + +def apply_interchange_post_import(itask: import_module_tasks_class.ImportTaks, asset_data): + # When Import FBX animation using the Interchange it create Anim_0_Root and Root_MorphAnim_0. + # I'm not sure if that a bug... So remove I Root_MorphAnim_0 or other animations and I rename Anim_0_Root. + asset_paths_to_remove = [] + main_anim_path = None + for imported_asset in itask.get_imported_assets(): + if type(imported_asset) is unreal.AnimSequence: + anim_asset_path = imported_asset.get_path_name() + path, name = anim_asset_path.rsplit('/', 1) + if name == "Anim_0_Root.Anim_0_Root": + main_anim_path = imported_asset.get_path_name() + else: + asset_paths_to_remove.append(imported_asset.get_path_name()) + + # Remove wrong animation assets + for asset_path in asset_paths_to_remove: + unreal.EditorAssetLibrary.delete_asset(asset_path) + + # Rename correct animation asset + if main_anim_path: + anim_asset_path = imported_asset.get_path_name() + path, name = anim_asset_path.rsplit('/', 1) + new_anim_path = path + "/" + asset_data["asset_name"] + "." + asset_data["asset_name"] + unreal.EditorAssetLibrary.rename_asset(main_anim_path, new_anim_path) + else: + fail_reason = f"animAsset {asset_data['asset_name']} not found after import: {main_anim_path}" + return fail_reason, None + +def apply_fbxui_post_import(itask: import_module_tasks_class.ImportTaks, asset_data): + """Applies post-import changes for FBX pipeline.""" + # When Import FBX animation using FbxImportUI it create a skeletal mesh and the animation at this side. + # I'm not sure if that a bug too... So remove the extra mesh + imported_anim_sequence = itask.get_imported_anim_sequence() + if imported_anim_sequence is None: + # If Imported Anim Sequence is None it maybe imported the asset as Skeletal Mesh. + skeleta_mesh_assset = itask.get_imported_skeletal_mesh() + if skeleta_mesh_assset: + # If Imported as Skeletal Mesh Search the real Anim Sequence + path = skeleta_mesh_assset.get_path_name() + base_name = path.split('.')[0] + anim_asset_name = f"{base_name}_anim.{base_name.split('/')[-1]}_anim" + desired_anim_path = f"{base_name}.{base_name.split('/')[-1]}" + animAsset = import_module_unreal_utils.load_asset(anim_asset_name) + if animAsset is not None: + # Remove the imported skeletal mesh and rename te anim sequence with his correct name. + unreal.EditorAssetLibrary.delete_asset(path) + unreal.EditorAssetLibrary.rename_asset(anim_asset_name, desired_anim_path) + else: + fail_reason = f"animAsset {asset_data['asset_name']} not found after import: {anim_asset_name}" + return fail_reason, None \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/bfu_import_materials/bfu_import_materials_utils.py b/blender-for-unrealengine/bfu_import_module/bfu_import_materials/bfu_import_materials_utils.py index 007f1cb0..0c1db363 100644 --- a/blender-for-unrealengine/bfu_import_module/bfu_import_materials/bfu_import_materials_utils.py +++ b/blender-for-unrealengine/bfu_import_module/bfu_import_materials/bfu_import_materials_utils.py @@ -16,50 +16,88 @@ # # ======================= END GPL LICENSE BLOCK ============================= +from .. import import_module_unreal_utils +from .. import import_module_tasks_class + try: import unreal except ImportError: import unreal_engine as unreal -def update_task_with_material_data(task: unreal.AssetImportTask, asset_data): - - if asset_data["asset_type"] in ["StaticMesh", "SkeletalMesh"]: +def apply_import_settings(itask: import_module_tasks_class.ImportTaks, asset_data: dict) -> None: + """Applies material and texture import settings to StaticMesh and SkeletalMesh assets.""" + + print("Mat S0") + + asset_type = asset_data.get("asset_type") + if asset_type not in ["StaticMesh", "SkeletalMesh"]: + # Only apply settings for StaticMesh and SkeletalMesh + return - fbx_import_ui = task.get_editor_property('options') - fbx_import_ui: unreal.FbxTextureImportData + print("Mat S1") + + # Material and texture import settings + if itask.use_interchange: + if "import_materials" in asset_data: + itask.get_igap_material().set_editor_property('import_materials', asset_data["import_materials"]) + if "import_textures" in asset_data: + itask.get_igap_texture().set_editor_property('import_textures', asset_data["import_textures"]) + else: if "import_materials" in asset_data: - fbx_import_ui.set_editor_property('import_materials', asset_data["import_materials"]) + itask.get_fbx_import_ui().set_editor_property('import_materials', asset_data["import_materials"]) if "import_textures" in asset_data: - fbx_import_ui.set_editor_property('import_textures', asset_data["import_textures"]) + itask.get_fbx_import_ui().set_editor_property('import_textures', asset_data["import_textures"]) + print("Mat S2") + + # Material search location and normal map green channel flip + if itask.use_interchange: + if "material_search_location" in asset_data: + search_location = asset_data["material_search_location"] + location_enum = { + "Local": unreal.InterchangeMaterialSearchLocation.LOCAL, + "UnderParent": unreal.InterchangeMaterialSearchLocation.UNDER_PARENT, + "UnderRoot": unreal.InterchangeMaterialSearchLocation.UNDER_ROOT, + "AllAssets": unreal.InterchangeMaterialSearchLocation.ALL_ASSETS + } + if search_location in location_enum: + itask.get_igap_material().set_editor_property('search_location', location_enum[search_location]) - fbx_texture_import_data = fbx_import_ui.texture_import_data - fbx_texture_import_data: unreal.FbxTextureImportData + if "flip_normal_map_green_channel" in asset_data: + itask.get_igap_texture().set_editor_property('flip_normal_map_green_channel', asset_data["flip_normal_map_green_channel"]) + else: + texture_import_data = itask.get_texture_import_data() + if "material_search_location" in asset_data: - if asset_data["material_search_location"] == "Local": - fbx_texture_import_data.set_editor_property('material_search_location', unreal.MaterialSearchLocation.LOCAL) - if asset_data["material_search_location"] == "UnderParent": - fbx_texture_import_data.set_editor_property('material_search_location', unreal.MaterialSearchLocation.UNDER_PARENT) - if asset_data["material_search_location"] == "UnderRoot": - fbx_texture_import_data.set_editor_property('material_search_location', unreal.MaterialSearchLocation.UNDER_ROOT) - if asset_data["material_search_location"] == "AllAssets": - fbx_texture_import_data.set_editor_property('material_search_location', unreal.MaterialSearchLocation.ALL_ASSETS) + search_location = asset_data["material_search_location"] + location_enum = { + "Local": unreal.MaterialSearchLocation.LOCAL, + "UnderParent": unreal.MaterialSearchLocation.UNDER_PARENT, + "UnderRoot": unreal.MaterialSearchLocation.UNDER_ROOT, + "AllAssets": unreal.MaterialSearchLocation.ALL_ASSETS + } + if search_location in location_enum: + texture_import_data.set_editor_property('material_search_location', location_enum[search_location]) - if "invert_normal_maps" in asset_data: - fbx_texture_import_data.set_editor_property('invert_normal_maps', asset_data["invert_normal_maps"]) + if "flip_normal_map_green_channel" in asset_data: + texture_import_data.set_editor_property('invert_normal_maps', asset_data["flip_normal_map_green_channel"]) + print("Mat S3") + + # Mat order + if itask.use_interchange: + # @TODO reorder_material_to_fbx_order Removed with InterchangeGenericAssetsPipeline? + # I yes need also remove reorder_material_to_fbx_order from the addon propertys. + pass - if asset_data["asset_type"] =="StaticMesh": - static_mesh_import_data = fbx_import_ui.static_mesh_import_data - static_mesh_import_data: unreal.FbxSkeletalMeshImportData + else: + if asset_type =="StaticMesh": if "reorder_material_to_fbx_order" in asset_data: - static_mesh_import_data.set_editor_property('reorder_material_to_fbx_order', asset_data["reorder_material_to_fbx_order"]) + itask.get_static_mesh_import_data().set_editor_property('reorder_material_to_fbx_order', asset_data["reorder_material_to_fbx_order"]) - if asset_data["asset_type"] == "SkeletalMesh": - skeletal_mesh_import_data = fbx_import_ui.skeletal_mesh_import_data - skeletal_mesh_import_data: unreal.FbxStaticMeshImportData + elif asset_type == "SkeletalMesh": if "reorder_material_to_fbx_order" in asset_data: - skeletal_mesh_import_data.set_editor_property('reorder_material_to_fbx_order', asset_data["reorder_material_to_fbx_order"]) \ No newline at end of file + itask.get_skeletal_mesh_import_data().set_editor_property('reorder_material_to_fbx_order', asset_data["reorder_material_to_fbx_order"]) \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/bfu_import_vertex_color/__init__.py b/blender-for-unrealengine/bfu_import_module/bfu_import_vertex_color/__init__.py new file mode 100644 index 00000000..a8c4cb76 --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/bfu_import_vertex_color/__init__.py @@ -0,0 +1,24 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import importlib + +from . import bfu_import_vertex_color_utils + +if "bfu_import_vertex_color_utils" in locals(): + importlib.reload(bfu_import_vertex_color_utils) diff --git a/blender-for-unrealengine/bfu_import_module/bfu_import_vertex_color/bfu_import_vertex_color_utils.py b/blender-for-unrealengine/bfu_import_module/bfu_import_vertex_color/bfu_import_vertex_color_utils.py new file mode 100644 index 00000000..ecebc4e9 --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/bfu_import_vertex_color/bfu_import_vertex_color_utils.py @@ -0,0 +1,128 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +from typing import Optional +from .. import import_module_unreal_utils +from .. import import_module_tasks_class + +try: + import unreal +except ImportError: + import unreal_engine as unreal + +support_interchange = import_module_unreal_utils.get_support_interchange() + +def get_vertex_override_color(asset_additional_data: dict) -> Optional[unreal.LinearColor]: + """Retrieves the vertex override color from the asset data, if available.""" + if asset_additional_data is None: + return None + + if "vertex_override_color" in asset_additional_data: + return unreal.LinearColor( + asset_additional_data["vertex_override_color"][0], + asset_additional_data["vertex_override_color"][1], + asset_additional_data["vertex_override_color"][2] + ) + + return None + +if support_interchange: + def get_interchange_vertex_color_import_option(asset_additional_data: dict) -> Optional[unreal.InterchangeVertexColorImportOption]: + """Retrieves the vertex color import option based on the asset data and pipeline.""" + if asset_additional_data is None: + return None + + key = "vertex_color_import_option" + option_value = asset_additional_data.get(key) + + # For unreal.InterchangeGenericCommonMeshesProperties + if option_value == "IGNORE": + return unreal.InterchangeVertexColorImportOption.IVCIO_IGNORE + elif option_value == "OVERRIDE": + return unreal.InterchangeVertexColorImportOption.IVCIO_OVERRIDE + elif option_value == "REPLACE": + return unreal.InterchangeVertexColorImportOption.IVCIO_REPLACE + return unreal.InterchangeVertexColorImportOption.IVCIO_REPLACE # Default + + +def get_vertex_color_import_option(asset_additional_data: dict) -> Optional[unreal.VertexColorImportOption]: + """Retrieves the vertex color import option based on the asset data and pipeline.""" + if asset_additional_data is None: + return None + + key = "vertex_color_import_option" + option_value = asset_additional_data.get(key) + + # For unreal.FbxStaticMeshImportData + if option_value == "IGNORE": + return unreal.VertexColorImportOption.IGNORE + elif option_value == "OVERRIDE": + return unreal.VertexColorImportOption.OVERRIDE + elif option_value == "REPLACE": + return unreal.VertexColorImportOption.REPLACE + return unreal.VertexColorImportOption.REPLACE # Default + + + +def apply_import_settings(itask: import_module_tasks_class.ImportTaks, asset_type: str, asset_additional_data: dict) -> None: + """Applies vertex color settings during the import process.""" + vertex_override_color = get_vertex_override_color(asset_additional_data) + if itask.use_interchange: + vertex_color_import_option = get_interchange_vertex_color_import_option(asset_additional_data) + else: + vertex_color_import_option = get_vertex_color_import_option(asset_additional_data) + + if itask.use_interchange: + itask.get_igap_common_mesh().set_editor_property('vertex_color_import_option', vertex_color_import_option) + if vertex_override_color: + itask.get_igap_common_mesh().set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) + else: + if asset_type == "StaticMesh": + itask.get_static_mesh_import_data().set_editor_property('vertex_color_import_option', vertex_color_import_option) + if vertex_override_color: + itask.get_static_mesh_import_data().set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) + + elif asset_type == "SkeletalMesh": + itask.get_skeletal_mesh_import_data().set_editor_property('vertex_color_import_option', vertex_color_import_option) + if vertex_override_color: + itask.get_skeletal_mesh_import_data().set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) + + +def apply_asset_settings(itask: import_module_tasks_class.ImportTaks, asset: unreal.Object, asset_additional_data: dict) -> None: + """Applies vertex color settings to an already imported asset.""" + if asset is None: + return + + vertex_override_color = get_vertex_override_color(asset_additional_data) + if itask.use_interchange: + vertex_color_import_option = get_interchange_vertex_color_import_option(asset_additional_data) + else: + vertex_color_import_option = get_vertex_color_import_option(asset_additional_data) + + if itask.use_interchange: + common_meshes_properties = asset.get_editor_property('asset_import_data').get_pipelines()[0].get_editor_property('common_meshes_properties') + if vertex_override_color: + common_meshes_properties.set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) + if vertex_color_import_option: + common_meshes_properties.set_editor_property('vertex_color_import_option', vertex_color_import_option) + else: + asset_import_data = asset.get_editor_property('asset_import_data') + if vertex_override_color: + asset_import_data.set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) + if vertex_color_import_option: + asset_import_data.set_editor_property('vertex_color_import_option', vertex_color_import_option) \ No newline at end of file diff --git a/blender-for-unrealengine/bps/__init__.py b/blender-for-unrealengine/bfu_import_module/bpl/__init__.py similarity index 93% rename from blender-for-unrealengine/bps/__init__.py rename to blender-for-unrealengine/bfu_import_module/bpl/__init__.py index 3da665cf..ff3e288d 100644 --- a/blender-for-unrealengine/bps/__init__.py +++ b/blender-for-unrealengine/bfu_import_module/bpl/__init__.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- @@ -45,4 +46,5 @@ if "blender_sub_process" in locals(): importlib.reload(blender_sub_process) if "naming" in locals(): - importlib.reload(naming) \ No newline at end of file + importlib.reload(naming) + diff --git a/blender-for-unrealengine/bfu_import_module/bps/advprint.py b/blender-for-unrealengine/bfu_import_module/bpl/advprint.py similarity index 85% rename from blender-for-unrealengine/bfu_import_module/bps/advprint.py rename to blender-for-unrealengine/bfu_import_module/bpl/advprint.py index 75ac6f27..414d5efb 100644 --- a/blender-for-unrealengine/bfu_import_module/bps/advprint.py +++ b/blender-for-unrealengine/bfu_import_module/bpl/advprint.py @@ -17,13 +17,12 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- - -import sys import time @@ -34,7 +33,7 @@ def _get_name(self): def _set_name(self, value): if not isinstance(value, str): - raise TypeError("name must be set to an String") + raise TypeError("name must be set to a String") self.__name = value name = property(_get_name, _set_name) @@ -57,7 +56,6 @@ def _get_total_step(self): def _set_total_step(self, value): if not (isinstance(value, int) or isinstance(value, float)): raise TypeError("total_step must be set to an Integer or Float") - self.__total_step = value total_step = property(_get_total_step, _set_total_step) @@ -80,29 +78,25 @@ def update_progress(self, progress): total_step = self.__total_step self.__previous_step = progress # Update the previous step. - is_done = False - if progress >= total_step: - is_done = True + is_done = progress >= total_step - # Write message. + # Write message msg = "\r{0}:".format(job_title) if self.show_block: - block = int(round(length*progress/total_step)) - msg += " [{0}]".format("#"*block + "-"*(length-block)) + block = int(round(length * progress / total_step)) + msg += " [{0}]".format("#" * block + "-" * (length - block)) if self.show_steps: msg += " {0}/{1}".format(progress, total_step) if is_done: - msg += " DONE IN {0}s\r\n".format(round(time.perf_counter()-self.__counter_start, 3)) - - else: - if self.show_percentage: - msg += " {0}%".format(round((progress*100)/total_step, 2)) + msg += " DONE IN {0}s\r\n".format(round(time.perf_counter() - self.__counter_start, 3)) + elif self.show_percentage: + msg += " {0}%".format(round((progress * 100) / total_step, 2)) - sys.stdout.write(msg) - sys.stdout.flush() + # Print the progress message on the same line + print(msg, end='', flush=True) def print_separation(number=60, char="-"): diff --git a/blender-for-unrealengine/bfu_import_module/bps/blender_sub_process.py b/blender-for-unrealengine/bfu_import_module/bpl/blender_sub_process.py similarity index 93% rename from blender-for-unrealengine/bfu_import_module/bps/blender_sub_process.py rename to blender-for-unrealengine/bfu_import_module/bpl/blender_sub_process.py index f845d516..035cac54 100644 --- a/blender-for-unrealengine/bfu_import_module/bps/blender_sub_process.py +++ b/blender-for-unrealengine/bfu_import_module/bpl/blender_sub_process.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bfu_import_module/bps/color_set.py b/blender-for-unrealengine/bfu_import_module/bpl/color_set.py similarity index 98% rename from blender-for-unrealengine/bfu_import_module/bps/color_set.py rename to blender-for-unrealengine/bfu_import_module/bpl/color_set.py index 8d39ebe4..3b066ea5 100644 --- a/blender-for-unrealengine/bfu_import_module/bps/color_set.py +++ b/blender-for-unrealengine/bfu_import_module/bpl/color_set.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bfu_import_module/bpl/console_utils.py b/blender-for-unrealengine/bfu_import_module/bpl/console_utils.py new file mode 100644 index 00000000..c6c1ba47 --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/bpl/console_utils.py @@ -0,0 +1,29 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import os + +def clear_console(): + os.system('cls' if os.name == 'nt' else 'clear') \ No newline at end of file diff --git a/blender-for-unrealengine/bps/math.py b/blender-for-unrealengine/bfu_import_module/bpl/math.py similarity index 96% rename from blender-for-unrealengine/bps/math.py rename to blender-for-unrealengine/bfu_import_module/bpl/math.py index d036605a..376bc45b 100644 --- a/blender-for-unrealengine/bps/math.py +++ b/blender-for-unrealengine/bfu_import_module/bpl/math.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bfu_import_module/bps/naming.py b/blender-for-unrealengine/bfu_import_module/bpl/naming.py similarity index 97% rename from blender-for-unrealengine/bfu_import_module/bps/naming.py rename to blender-for-unrealengine/bfu_import_module/bpl/naming.py index 9c43c4f6..497e43e1 100644 --- a/blender-for-unrealengine/bfu_import_module/bps/naming.py +++ b/blender-for-unrealengine/bfu_import_module/bpl/naming.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bps/utils.py b/blender-for-unrealengine/bfu_import_module/bpl/utils.py similarity index 97% rename from blender-for-unrealengine/bps/utils.py rename to blender-for-unrealengine/bfu_import_module/bpl/utils.py index 320f3319..bc6592d7 100644 --- a/blender-for-unrealengine/bps/utils.py +++ b/blender-for-unrealengine/bfu_import_module/bpl/utils.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bfu_import_module/bps/console_utils.py b/blender-for-unrealengine/bfu_import_module/bps/console_utils.py deleted file mode 100644 index 596331d9..00000000 --- a/blender-for-unrealengine/bfu_import_module/bps/console_utils.py +++ /dev/null @@ -1,4 +0,0 @@ -import os - -def clear_console(): - os.system('cls' if os.name == 'nt' else 'clear') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/config.py b/blender-for-unrealengine/bfu_import_module/config.py new file mode 100644 index 00000000..e1383f5a --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/config.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +automated_import_tasks = True #False when debug only, used to show import dialog. +force_use_interchange = "Auto" #Auto by default, you can use Interchange or FBX \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py b/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py index cb584134..8798a244 100644 --- a/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py +++ b/blender-for-unrealengine/bfu_import_module/import_module_post_treatment.py @@ -25,9 +25,9 @@ import unreal_engine as unreal -def import_static_lod(asset, asset_data, asset_additional_data, lod_name, lod_number): - vertex_override_color = import_module_unreal_utils.get_vertex_override_color(asset_additional_data) - vertex_color_import_option = import_module_unreal_utils.get_vertex_color_import_option(asset_additional_data) +def import_static_lod(asset, asset_options, asset_data, asset_additional_data, lod_name, lod_number): + + print(f"Start Import Lod_{str(lod_number)} ({lod_name})") if "LevelOfDetail" in asset_additional_data: if lod_name in asset_additional_data["LevelOfDetail"]: @@ -38,18 +38,16 @@ def import_static_lod(asset, asset_data, asset_additional_data, lod_name, lod_nu lodTask.automated = True lodTask.replace_existing = True - # Set vertex color import settings to replicate base StaticMesh's behaviour - if asset_data["asset_type"] == "Alembic": - lodTask.set_editor_property('options', unreal.AbcImportSettings()) + if asset_options: + lodTask.set_editor_property('options', asset_options) else: - lodTask.set_editor_property('options', unreal.FbxImportUI()) + # Replicate asset import settings when asset_options is None + lodTask.set_editor_property('options', asset.get_editor_property('asset_import_data')) - lodTask.get_editor_property('options').static_mesh_import_data.set_editor_property('vertex_color_import_option', vertex_color_import_option) - lodTask.get_editor_property('options').static_mesh_import_data.set_editor_property('vertex_override_color', vertex_override_color.to_rgbe()) unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([lodTask]) if len(lodTask.imported_object_paths) > 0: - lodAsset = unreal.find_asset(lodTask.imported_object_paths[0]) + lodAsset = import_module_unreal_utils.load_asset(lodTask.imported_object_paths[0]) slot_replaced = unreal.EditorStaticMeshLibrary.set_lod_from_static_mesh(asset, lod_number, lodAsset, 0, True) unreal.EditorAssetLibrary.delete_asset(lodTask.imported_object_paths[0]) @@ -59,27 +57,27 @@ def import_skeletal_lod(asset, asset_data, asset_additional_data, lod_name, lod_ # Unreal python no longer support Skeletal mesh LODS import. pass -def set_static_mesh_lods(asset, asset_data, asset_additional_data): +def set_static_mesh_lods(asset, asset_options, asset_data, asset_additional_data): # Import the StaticMesh lod(s) unreal.EditorStaticMeshLibrary.remove_lods(asset) - import_static_lod(asset, asset_data, asset_additional_data, "lod_1", 1) - import_static_lod(asset, asset_data, asset_additional_data, "lod_2", 2) - import_static_lod(asset, asset_data, asset_additional_data, "lod_3", 3) - import_static_lod(asset, asset_data, asset_additional_data, "lod_4", 4) - import_static_lod(asset, asset_data, asset_additional_data, "lod_5", 5) + import_static_lod(asset, asset_options, asset_data, asset_additional_data, "lod_1", 1) + import_static_lod(asset, asset_options, asset_data, asset_additional_data, "lod_2", 2) + import_static_lod(asset, asset_options, asset_data, asset_additional_data, "lod_3", 3) + import_static_lod(asset, asset_options, asset_data, asset_additional_data, "lod_4", 4) + import_static_lod(asset, asset_options, asset_data, asset_additional_data, "lod_5", 5) -def set_skeletal_mesh_lods(asset, asset_data, asset_additional_data): +def set_skeletal_mesh_lods(asset, asset_options, asset_data, asset_additional_data): # Import the SkeletalMesh lod(s) - import_skeletal_lod(asset, asset_data, asset_additional_data, "lod_1", 1) - import_skeletal_lod(asset, asset_data, asset_additional_data, "lod_2", 2) - import_skeletal_lod(asset, asset_data, asset_additional_data, "lod_3", 3) - import_skeletal_lod(asset, asset_data, asset_additional_data, "lod_4", 4) - import_skeletal_lod(asset, asset_data, asset_additional_data, "lod_5", 5) + import_skeletal_lod(asset, asset_options, asset_data, asset_additional_data, "lod_1", 1) + import_skeletal_lod(asset, asset_options, asset_data, asset_additional_data, "lod_2", 2) + import_skeletal_lod(asset, asset_options, asset_data, asset_additional_data, "lod_3", 3) + import_skeletal_lod(asset, asset_options, asset_data, asset_additional_data, "lod_4", 4) + import_skeletal_lod(asset, asset_options, asset_data, asset_additional_data, "lod_5", 5) def set_sequence_preview_skeletal_mesh(asset: unreal.AnimSequence, origin_skeletal_mesh): if origin_skeletal_mesh: - #todo:: preview_pose_asset doesn’t retarget right now. Need wait update in Unreal Engine Python API. + # @TODO preview_pose_asset doesn’t retarget right now. Need wait update in Unreal Engine Python API. asset.get_editor_property('preview_pose_asset') pass \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/import_module_tasks_class.py b/blender-for-unrealengine/bfu_import_module/import_module_tasks_class.py new file mode 100644 index 00000000..07e2d8c0 --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/import_module_tasks_class.py @@ -0,0 +1,142 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +from typing import List, Optional +from . import import_module_unreal_utils +from . import config + +try: + import unreal +except ImportError: + import unreal_engine as unreal + +support_interchange = import_module_unreal_utils.get_support_interchange() + +class ImportTaks(): + + def __init__(self) -> None: + self.task = unreal.AssetImportTask() + self.task_option = None + + if config.force_use_interchange == "Interchange": + self.use_interchange = True + + elif config.force_use_interchange == "FBX": + self.use_interchange = False + + else: + # Interchange is avaliable since 5.0, + # but I preffer start to use at 5.5 to avoid issue with previous versions. + if support_interchange and import_module_unreal_utils.is_unreal_version_greater_or_equal(5,5): + # Set values inside unreal.InterchangeGenericAssetsPipeline (unreal.InterchangeGenericCommonMeshesProperties or ...) + self.use_interchange = True + else: + # Set values inside unreal.FbxStaticMeshImportData or ... + self.use_interchange = False + + def set_task_option(self, new_task_option): + self.task_option = new_task_option + + def get_task(self): + return self.task + + def get_fbx_import_ui(self) -> unreal.FbxImportUI: + return self.task_option + + def get_abc_import_settings(self) -> unreal.AbcImportSettings: + return self.task_option + + def get_static_mesh_import_data(self) -> unreal.FbxStaticMeshImportData: + return self.task_option.static_mesh_import_data + + def get_skeletal_mesh_import_data(self) -> unreal.FbxSkeletalMeshImportData: + return self.task_option.skeletal_mesh_import_data + + def get_animation_import_data(self) -> unreal.FbxAnimSequenceImportData: + return self.task_option.anim_sequence_import_data + + def get_texture_import_data(self) -> unreal.FbxTextureImportData: + return self.task_option.texture_import_data + + + + if support_interchange: + def get_igap(self) -> unreal.InterchangeGenericAssetsPipeline: + # unreal.InterchangeGenericAssetsPipeline + return self.task_option + + def get_igap_mesh(self) -> unreal.InterchangeGenericMeshPipeline: + # unreal.InterchangeGenericMeshPipeline + return self.task_option.get_editor_property('mesh_pipeline') + + def get_igap_skeletal_mesh(self) -> unreal.InterchangeGenericCommonSkeletalMeshesAndAnimationsProperties: + # unreal.InterchangeGenericCommonSkeletalMeshesAndAnimationsProperties + return self.task_option.get_editor_property('common_skeletal_meshes_and_animations_properties') + + def get_igap_common_mesh(self) -> unreal.InterchangeGenericCommonMeshesProperties: + # unreal.InterchangeGenericCommonMeshesProperties + return self.task_option.get_editor_property('common_meshes_properties') + + def get_igap_material(self) -> unreal.InterchangeGenericMaterialPipeline: + # unreal.InterchangeGenericMaterialPipeline + return self.task_option.get_editor_property('material_pipeline') + + def get_igap_texture(self) -> unreal.InterchangeGenericTexturePipeline: + # unreal.InterchangeGenericTexturePipeline + return self.task_option.get_editor_property('material_pipeline').get_editor_property('texture_pipeline') + + def get_igap_animation(self) -> unreal.InterchangeGenericAnimationPipeline: + # unreal.InterchangeGenericAnimationPipeline + return self.task_option.get_editor_property('animation_pipeline') + + def get_imported_assets(self) -> List[unreal.Object]: + assets = [] + for path in self.task.imported_object_paths: + search_asset = import_module_unreal_utils.load_asset(path) + if search_asset: + assets.append(search_asset) + return assets + + def get_imported_static_mesh(self) -> Optional[unreal.StaticMesh]: + return next((asset for asset in self.get_imported_assets() if isinstance(asset, unreal.StaticMesh)), None) + + def get_imported_skeleton(self) -> Optional[unreal.Skeleton]: + return next((asset for asset in self.get_imported_assets() if isinstance(asset, unreal.Skeleton)), None) + + def get_imported_skeletal_mesh(self) -> Optional[unreal.SkeletalMesh]: + return next((asset for asset in self.get_imported_assets() if isinstance(asset, unreal.SkeletalMesh)), None) + + def get_imported_anim_sequence(self) -> Optional[unreal.AnimSequence]: + return next((asset for asset in self.get_imported_assets() if isinstance(asset, unreal.AnimSequence)), None) + + def import_asset_task(self): + if self.use_interchange: + self.task.set_editor_property('options', unreal.InterchangePipelineStackOverride()) + self.task.get_editor_property('options').add_pipeline(self.task_option) + else: + self.task.set_editor_property('options', self.task_option) + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([self.task]) + + def get_task_options(self): + if self.use_interchange: + new_option = unreal.InterchangePipelineStackOverride() + new_option.add_pipeline(self.task_option) + return new_option + else: + return self.task_option \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/import_module_tasks_helper.py b/blender-for-unrealengine/bfu_import_module/import_module_tasks_helper.py new file mode 100644 index 00000000..9bf63d8e --- /dev/null +++ b/blender-for-unrealengine/bfu_import_module/import_module_tasks_helper.py @@ -0,0 +1,124 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +from typing import Union +from . import import_module_unreal_utils +from . import import_module_tasks_class + +try: + import unreal +except ImportError: + import unreal_engine as unreal + +support_interchange = import_module_unreal_utils.get_support_interchange() + +if support_interchange: + def task_options_default_preset(use_interchange: bool = True) -> Union[unreal.FbxImportUI, unreal.InterchangeGenericAssetsPipeline]: + """Returns default task options preset based on interchange usage and Unreal version.""" + if use_interchange: + options = unreal.InterchangeGenericAssetsPipeline() + else: + options = unreal.FbxImportUI() + return options + + def task_options_static_mesh_preset(use_interchange: bool = True) -> Union[unreal.InterchangeGenericAssetsPipeline, unreal.FbxImportUI]: + """Returns static mesh task options preset based on interchange usage.""" + if use_interchange: + options = unreal.InterchangeGenericAssetsPipeline() + else: + options = unreal.FbxImportUI() + return options + + def task_options_skeletal_mesh_preset(use_interchange: bool = True) -> Union[unreal.InterchangeGenericAssetsPipeline, unreal.FbxImportUI]: + """Returns skeletal mesh task options preset based on interchange usage.""" + if use_interchange: + options = unreal.InterchangeGenericAssetsPipeline() + else: + options = unreal.FbxImportUI() + return options + + def task_options_animation_preset(use_interchange: bool = True) -> Union[unreal.InterchangeGenericAssetsPipeline, unreal.FbxImportUI]: + """Returns animation task options preset based on interchange usage.""" + if use_interchange: + options = unreal.InterchangeGenericAssetsPipeline() + else: + options = unreal.FbxImportUI() + return options +else: + def task_options_default_preset(use_interchange: bool = True) -> unreal.FbxImportUI: + """Returns default task options preset for Unreal Engine versions below 5, without interchange support.""" + return unreal.FbxImportUI() + + def task_options_static_mesh_preset(use_interchange: bool = True) -> unreal.FbxImportUI: + """Returns static mesh task options preset without interchange support.""" + return unreal.FbxImportUI() + + def task_options_skeletal_mesh_preset(use_interchange: bool = True) -> unreal.FbxImportUI: + """Returns skeletal mesh task options preset without interchange support.""" + return unreal.FbxImportUI() + + def task_options_animation_preset(use_interchange: bool = True) -> unreal.FbxImportUI: + """Returns animation task options preset without interchange support.""" + return unreal.FbxImportUI() + +def task_options_alembic_preset(use_interchange: bool = True) -> unreal.AbcImportSettings: + """Returns Alembic task options preset.""" + options = unreal.AbcImportSettings() + return options + +if support_interchange: + def init_options_data(asset_type: str, use_interchange: bool = True) -> Union[unreal.InterchangeGenericAssetsPipeline, unreal.FbxImportUI, unreal.AbcImportSettings]: + """Initializes task options based on asset type and interchange usage.""" + + if asset_type == "Alembic": + options = task_options_alembic_preset(use_interchange) + + elif asset_type == "StaticMesh": + options = task_options_static_mesh_preset(use_interchange) + + elif asset_type == "SkeletalMesh": + options = task_options_skeletal_mesh_preset(use_interchange) + + elif asset_type == "Animation": + options = task_options_animation_preset(use_interchange) + + else: + options = task_options_default_preset(use_interchange) + + return options +else: + def init_options_data(asset_type: str, use_interchange: bool = True) -> Union[unreal.FbxImportUI, unreal.AbcImportSettings]: + """Initializes task options based on asset type and interchange usage.""" + + if asset_type == "Alembic": + options = task_options_alembic_preset(use_interchange) + + elif asset_type == "StaticMesh": + options = task_options_static_mesh_preset(use_interchange) + + elif asset_type == "SkeletalMesh": + options = task_options_skeletal_mesh_preset(use_interchange) + + elif asset_type == "Animation": + options = task_options_animation_preset(use_interchange) + + else: + options = task_options_default_preset(use_interchange) + + return options \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/import_module_unreal_utils.py b/blender-for-unrealengine/bfu_import_module/import_module_unreal_utils.py index f47925e3..a76b9ba8 100644 --- a/blender-for-unrealengine/bfu_import_module/import_module_unreal_utils.py +++ b/blender-for-unrealengine/bfu_import_module/import_module_unreal_utils.py @@ -7,45 +7,54 @@ # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # All rights reserved. # # ======================= END GPL LICENSE BLOCK ============================= import string +from typing import List, Tuple +from . import import_module_unreal_utils try: import unreal except ImportError: import unreal_engine as unreal -def get_selected_level_actors(): - selected_actors = unreal.EditorLevelLibrary.get_selected_level_actors() - return selected_actors +def load_asset(name): + find_asset = unreal.find_asset(name, follow_redirectors=True) + if find_asset is None: + # Load asset if not find. + find_asset = unreal.load_asset(name, follow_redirectors=True) + return find_asset + -def get_unreal_version(): +def get_selected_level_actors() -> List[unreal.Actor]: + """Returns a list of selected actors in the level.""" + return unreal.EditorLevelLibrary.get_selected_level_actors() + +def get_unreal_version() -> Tuple[int, int, int]: + """Returns the Unreal Engine version as a tuple of (major, minor, patch).""" version_info = unreal.SystemLibrary.get_engine_version().split('-')[0] - version_numbers = version_info.split('.') - major = int(version_numbers[0]) - minor = int(version_numbers[1]) - patch = int(version_numbers[2]) + major, minor, patch = map(int, version_info.split('.')) return major, minor, patch -def is_unreal_version_greater_or_equal(target_major, target_minor=0, target_patch=0): +def is_unreal_version_greater_or_equal(target_major: int, target_minor: int = 0, target_patch: int = 0) -> bool: + """Checks if the Unreal Engine version is greater than or equal to the target version.""" major, minor, patch = get_unreal_version() - - if major > target_major or (major == target_major and minor >= target_minor) or (major == target_major and minor == target_minor and patch >= target_patch): - return True - else: - return False + return ( + major > target_major or + (major == target_major and minor > target_minor) or + (major == target_major and minor == target_minor and patch >= target_patch) + ) -def ValidUnrealAssetsName(filename): - # Normalizes string, removes non-alpha characters - # Asset name in Unreal use + +def valid_unreal_asset_name(filename): + """Returns a valid Unreal asset name by replacing invalid characters.""" filename = filename.replace('.', '_') filename = filename.replace('(', '_') @@ -55,37 +64,30 @@ def ValidUnrealAssetsName(filename): filename = ''.join(c for c in filename if c in valid_chars) return filename -def show_simple_message(title, message): - return unreal.EditorDialog.show_message(title, message, unreal.AppMsgType.OK) - -def show_warning_message(title, message): - print('--------------------------------------------------') - print(message) - return unreal.EditorDialog.show_message(title, message, unreal.AppMsgType.OK) - -def get_vertex_override_color(asset_additional_data): - if asset_additional_data is None: - return None +def show_simple_message(title: str, message: str) -> unreal.AppReturnType: + """Displays a simple message dialog in Unreal Editor.""" + if hasattr(unreal, 'EditorDialog'): + return unreal.EditorDialog.show_message(title, message, unreal.AppMsgType.OK) + else: + print('--------------------------------------------------') + print(message) - if "vertex_override_color" in asset_additional_data: - vertex_override_color = unreal.LinearColor( - asset_additional_data["vertex_override_color"][0], - asset_additional_data["vertex_override_color"][1], - asset_additional_data["vertex_override_color"][2] - ) - return vertex_override_color +def show_warning_message(title: str, message: str) -> unreal.AppReturnType: + """Displays a warning message in Unreal Editor and prints it to the console.""" + if hasattr(unreal, 'EditorDialog'): + unreal.EditorDialog.show_message(title, message, unreal.AppMsgType.OK) + else: + print('--------------------------------------------------') + print(message) -def get_vertex_color_import_option(asset_additional_data): - if asset_additional_data is None: - return None +def get_support_interchange() -> bool: + return import_module_unreal_utils.is_unreal_version_greater_or_equal(5, 0) - vertex_color_import_option = unreal.VertexColorImportOption.REPLACE # Default - if "vertex_color_import_option" in asset_additional_data: - if asset_additional_data["vertex_color_import_option"] == "IGNORE": - vertex_color_import_option = unreal.VertexColorImportOption.IGNORE - elif asset_additional_data["vertex_color_import_option"] == "OVERRIDE": - vertex_color_import_option = unreal.VertexColorImportOption.OVERRIDE - elif asset_additional_data["vertex_color_import_option"] == "REPLACE": - vertex_color_import_option = unreal.VertexColorImportOption.REPLACE - return vertex_color_import_option +def editor_scripting_utilities_active() -> bool: + if is_unreal_version_greater_or_equal(4,20): + if hasattr(unreal, 'EditorAssetLibrary'): + return True + return False +def sequencer_scripting_active() -> bool: + return hasattr(unreal.MovieSceneSequence, 'set_display_rate') diff --git a/blender-for-unrealengine/bfu_import_module/import_module_utils.py b/blender-for-unrealengine/bfu_import_module/import_module_utils.py index 6c5d3df3..51f0b5e7 100644 --- a/blender-for-unrealengine/bfu_import_module/import_module_utils.py +++ b/blender-for-unrealengine/bfu_import_module/import_module_utils.py @@ -19,6 +19,7 @@ import sys import json + def JsonLoad(json_file): # Changed in Python 3.9: The keyword argument encoding has been removed. if sys.version_info >= (3, 9): diff --git a/blender-for-unrealengine/bfu_import_module/sequencer_import.py b/blender-for-unrealengine/bfu_import_module/sequencer_import.py index 118af9dc..9970dc66 100644 --- a/blender-for-unrealengine/bfu_import_module/sequencer_import.py +++ b/blender-for-unrealengine/bfu_import_module/sequencer_import.py @@ -17,33 +17,30 @@ # ======================= END GPL LICENSE BLOCK ============================= import os.path + try: import unreal except ImportError: import unreal_engine as unreal -from . import bps + from . import import_module_utils from . import import_module_unreal_utils from . import sequencer_utils - - - - def ready_for_sequence_import(): - if import_module_unreal_utils.is_unreal_version_greater_or_equal(4,20): # TO DO: EditorAssetLibrary was added in witch version exactly? - if not hasattr(unreal, 'EditorAssetLibrary'): - message = 'WARNING: Editor Scripting Utilities should be activated.' + "\n" - message += 'Edit > Plugin > Scripting > Editor Scripting Utilities.' - import_module_unreal_utils.show_warning_message("Editor Scripting Utilities not activated.", message) - return False - if not hasattr(unreal.MovieSceneSequence, 'set_display_rate'): - message = 'WARNING: Editor Scripting Utilities should be activated.' + "\n" - message += 'Edit > Plugin > Scripting > Sequencer Scripting.' + if not import_module_unreal_utils.editor_scripting_utilities_active(): + message = 'WARNING: Editor Scripting Utilities Plugin should be activated.' + "\n" + message += 'Edit > Plugin > Scripting > Editor Scripting Utilities.' import_module_unreal_utils.show_warning_message("Editor Scripting Utilities not activated.", message) return False + + if not import_module_unreal_utils.sequencer_scripting_active(): + message = 'WARNING: Sequencer Scripting Plugin should be activated.' + "\n" + message += 'Edit > Plugin > Scripting > Sequencer Scripting.' + import_module_unreal_utils.show_warning_message("Sequencer Scripting not activated.", message) + return False return True def CreateSequencer(sequence_data, show_finished_popup=True): diff --git a/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py b/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py index 55ebf220..03a66105 100644 --- a/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py +++ b/blender-for-unrealengine/bfu_import_module/sequencer_import_script.py @@ -1,65 +1,29 @@ -# This script was generated with the addons Blender for UnrealEngine : https://github.com/xavier150/Blender-For-UnrealEngine-Addons -# It will import into Unreal Engine all the assets of type StaticMesh, SkeletalMesh, Animation and Pose -# The script must be used in Unreal Engine Editor with Python plugins : https://docs.unrealengine.com/en-US/Engine/Editor/ScriptingAndAutomation/Python -# Use this command in Unreal cmd consol: py "[ScriptLocation]\sequencer_import_script.py" +# This script was generated with the addons Unreal Engine Assets Exporter. +# This script should be run in Unreal Engine to import into Unreal Engine 4 and 5 assets. +# The assets are exported from from Unreal Engine Assets Exporter. More detail here. https://github.com/xavier150/Blender-For-UnrealEngine-Addons +# Use the following command in Unreal Engine cmd consol to import sequencer: +# py "[ScriptLocation]\sequencer_import_script.py" import importlib -import sys +import importlib.util import os -import json +from . import import_module_utils -def JsonLoad(json_file): - # Changed in Python 3.9: The keyword argument encoding has been removed. - if sys.version_info >= (3, 9): - return json.load(json_file) - else: - return json.load(json_file, encoding="utf8") - -def JsonLoadFile(json_file_path): - if sys.version_info[0] < 3: - with open(json_file_path, "r") as json_file: - return JsonLoad(json_file) - else: - with open(json_file_path, "r", encoding="utf8") as json_file: - return JsonLoad(json_file) - -def load_module(import_module_path): - # Import and run the module - module_name = os.path.basename(import_module_path).replace('.py', '') - module_dir = os.path.dirname(import_module_path) - - if module_dir not in sys.path: - sys.path.append(module_dir) - - imported_module = importlib.import_module(module_name) - importlib.reload(imported_module) - - # Assuming the module has a main function to run - if hasattr(imported_module, 'main'): - imported_module.main() - - return imported_module, module_name - -def unload_module(module_name): - # Vérifier si le module est dans sys.modules - if module_name in sys.modules: - # Récupérer la référence du module - module = sys.modules[module_name] - # Supprimer la référence globale - del sys.modules[module_name] - del module def RunImportScriptWithJsonData(): # Prepare process import json_data_file = 'ImportSequencerData.json' dir_path = os.path.dirname(os.path.realpath(__file__)) - - sequence_data = JsonLoadFile(os.path.join(dir_path, json_data_file)) + import_file_path = os.path.join(dir_path, json_data_file) + sequence_data = import_module_utils.JsonLoadFile(import_file_path) - import_module_path = sequence_data["info"]["import_modiule_path"] # Module to run - imported_module, module_name = load_module(import_module_path) - imported_module.run_sequencer_import(sequence_data, False) - unload_module(module_name) + file_path = os.path.join(sequence_data["info"]["addon_path"],'run_unreal_import_script.py') + spec = importlib.util.spec_from_file_location("__import_sequencer__", file_path) + module = importlib.util.module_from_spec(spec) + + # Run script module function + spec.loader.exec_module(module) + module.run_from_sequencer_import_script(import_file_path) if __name__ == "__main__": RunImportScriptWithJsonData() diff --git a/blender-for-unrealengine/bfu_import_module/sequencer_utils.py b/blender-for-unrealengine/bfu_import_module/sequencer_utils.py index 3d2575a2..fc748913 100644 --- a/blender-for-unrealengine/bfu_import_module/sequencer_utils.py +++ b/blender-for-unrealengine/bfu_import_module/sequencer_utils.py @@ -16,241 +16,245 @@ # # ======================= END GPL LICENSE BLOCK ============================= +from typing import TYPE_CHECKING, Dict, Any + try: import unreal except ImportError: import unreal_engine as unreal + from . import import_module_utils from . import import_module_unreal_utils -if import_module_unreal_utils.is_unreal_version_greater_or_equal(5,1): - MovieSceneBindingProxy = unreal.MovieSceneBindingProxy -else: - MovieSceneBindingProxy = unreal.SequencerBindingProxy +# Since 5.1 MovieSceneBindingProxy replace SequencerBindingProxy. +use_movie_scene = import_module_unreal_utils.is_unreal_version_greater_or_equal(5,1) +sequencer_scripting_active = import_module_unreal_utils.sequencer_scripting_active() + + + +if sequencer_scripting_active: -from typing import Dict, Any + def get_sequencer_framerate(denominator = 1, numerator = 24) -> unreal.FrameRate: + """ + Adjusts the given frame rate to be compatible with Unreal Engine Sequencer. -def get_sequencer_framerate(denominator = 1, numerator = 24) -> unreal.FrameRate: - """ - Adjusts the given frame rate to be compatible with Unreal Engine Sequencer. + Ensures the denominator and numerator are integers over zero and warns if the input values are adjusted. - Ensures the denominator and numerator are integers over zero and warns if the input values are adjusted. + Parameters: + - denominator (float): The original denominator value. + - numerator (float): The original numerator value. - Parameters: - - denominator (float): The original denominator value. - - numerator (float): The original numerator value. + Returns: + - unreal.FrameRate: The adjusted frame rate object. + """ + # Ensure denominator and numerator are at least 1 and int 32 + new_denominator = max(round(denominator), 1) + new_numerator = max(round(numerator), 1) + myFFrameRate = unreal.FrameRate(numerator=new_numerator, denominator=new_denominator) - Returns: - - unreal.FrameRate: The adjusted frame rate object. - """ - # Ensure denominator and numerator are at least 1 and int 32 - new_denominator = max(round(denominator), 1) - new_numerator = max(round(numerator), 1) - myFFrameRate = unreal.FrameRate(numerator=new_numerator, denominator=new_denominator) + if denominator != new_denominator or numerator != new_numerator: + message = ('WARNING: Frame rate denominator and numerator must be an int32 over zero.\n' + 'Float denominator and numerator is not supported in Unreal Engine Sequencer.\n\n' + f'- Before: Denominator: {denominator}, Numerator: {numerator}\n' + f'- After: Denominator: {new_denominator}, Numerator: {new_numerator}') + import_module_unreal_utils.show_warning_message("Frame Rate Adjustment Warning", message) - if denominator != new_denominator or numerator != new_numerator: - message = ('WARNING: Frame rate denominator and numerator must be an int32 over zero.\n' - 'Float denominator and numerator is not supported in Unreal Engine Sequencer.\n\n' - f'- Before: Denominator: {denominator}, Numerator: {numerator}\n' - f'- After: Denominator: {new_denominator}, Numerator: {new_numerator}') - import_module_unreal_utils.show_warning_message("Frame Rate Adjustment Warning", message) + return myFFrameRate - return myFFrameRate + def get_section_all_channel(section: unreal.MovieSceneSection): + if import_module_unreal_utils.is_unreal_version_greater_or_equal(5,0): + return section.get_all_channels() + else: + return section.get_channels() -def get_section_all_channel(section: unreal.MovieSceneSection): - if import_module_unreal_utils.is_unreal_version_greater_or_equal(5,0): - return section.get_all_channels() - else: - return section.get_channels() + def AddSequencerSectionTransformKeysByIniFile(section: unreal.MovieSceneSection, track_dict: Dict[str, Any]): + for key in track_dict.keys(): + value = track_dict[key] # (x,y,z x,y,z x,y,z) + frame = unreal.FrameNumber(int(key)) -def AddSequencerSectionTransformKeysByIniFile(section: unreal.MovieSceneSection, track_dict: Dict[str, Any]): - for key in track_dict.keys(): - value = track_dict[key] # (x,y,z x,y,z x,y,z) - frame = unreal.FrameNumber(int(key)) + get_section_all_channel(section)[0].add_key(frame, value["location_x"]) + get_section_all_channel(section)[1].add_key(frame, value["location_y"]) + get_section_all_channel(section)[2].add_key(frame, value["location_z"]) + get_section_all_channel(section)[3].add_key(frame, value["rotation_x"]) + get_section_all_channel(section)[4].add_key(frame, value["rotation_y"]) + get_section_all_channel(section)[5].add_key(frame, value["rotation_z"]) + get_section_all_channel(section)[6].add_key(frame, value["scale_x"]) + get_section_all_channel(section)[7].add_key(frame, value["scale_y"]) + get_section_all_channel(section)[8].add_key(frame, value["scale_z"]) - get_section_all_channel(section)[0].add_key(frame, value["location_x"]) - get_section_all_channel(section)[1].add_key(frame, value["location_y"]) - get_section_all_channel(section)[2].add_key(frame, value["location_z"]) - get_section_all_channel(section)[3].add_key(frame, value["rotation_x"]) - get_section_all_channel(section)[4].add_key(frame, value["rotation_y"]) - get_section_all_channel(section)[5].add_key(frame, value["rotation_z"]) - get_section_all_channel(section)[6].add_key(frame, value["scale_x"]) - get_section_all_channel(section)[7].add_key(frame, value["scale_y"]) - get_section_all_channel(section)[8].add_key(frame, value["scale_z"]) + def AddSequencerSectionDoubleVectorKeysByIniFile(section, track_dict: Dict[str, Any]): + for key in track_dict.keys(): + value = track_dict[key] # (x,y,z x,y,z x,y,z) + frame = unreal.FrameNumber(int(key)) + get_section_all_channel(section)[0].add_key(frame, value["x"]) + get_section_all_channel(section)[1].add_key(frame, value["y"]) -def AddSequencerSectionDoubleVectorKeysByIniFile(section, track_dict: Dict[str, Any]): - for key in track_dict.keys(): - value = track_dict[key] # (x,y,z x,y,z x,y,z) - frame = unreal.FrameNumber(int(key)) + def AddSequencerSectionFloatKeysByIniFile(section, track_dict: Dict[str, Any]): + for key in track_dict.keys(): + frame = unreal.FrameNumber(int(key)) + value = track_dict[key] - get_section_all_channel(section)[0].add_key(frame, value["x"]) - get_section_all_channel(section)[1].add_key(frame, value["y"]) + get_section_all_channel(section)[0].add_key(frame, value) -def AddSequencerSectionFloatKeysByIniFile(section, track_dict: Dict[str, Any]): - for key in track_dict.keys(): - frame = unreal.FrameNumber(int(key)) - value = track_dict[key] + def AddSequencerSectionBoolKeysByIniFile(section, track_dict: Dict[str, Any]): + for key in track_dict.keys(): + frame = unreal.FrameNumber(int(key)) + value = track_dict[key] - get_section_all_channel(section)[0].add_key(frame, value) + get_section_all_channel(section)[0].add_key(frame, value) + def create_new_sequence(): + factory = unreal.LevelSequenceFactoryNew() + asset_tools = unreal.AssetToolsHelpers.get_asset_tools() + seq = asset_tools.create_asset_with_dialog('MySequence', '/Game', None, factory) + if seq is None: + return 'ERROR: level sequencer factory_create fail' + return seq -def AddSequencerSectionBoolKeysByIniFile(section, track_dict: Dict[str, Any]): - for key in track_dict.keys(): - frame = unreal.FrameNumber(int(key)) - value = track_dict[key] + def Sequencer_add_new_camera(seq: unreal.LevelSequence, camera_target_class = unreal.CineCameraActor, camera_name = "MyCamera", is_spawnable_camera = False): - get_section_all_channel(section)[0].add_key(frame, value) + #Create bindings + if is_spawnable_camera: + ''' + I preffer create an level camera an convert to spawnable than use seq.add_spawnable_from_class() + Because with seq.add_spawnable_from_class() it not possible to change actor name an add create camera_component_binding. + Need more control in the API. + ''' + # Create camera + temp_camera_actor = unreal.EditorLevelLibrary().spawn_actor_from_class(camera_target_class, unreal.Vector(0, 0, 0), unreal.Rotator(0, 0, 0)) + temp_camera_actor.set_actor_label(camera_name) -def create_new_sequence(): - factory = unreal.LevelSequenceFactoryNew() - asset_tools = unreal.AssetToolsHelpers.get_asset_tools() - seq = asset_tools.create_asset_with_dialog('MySequence', '/Game', None, factory) - if seq is None: - return 'ERROR: level sequencer factory_create fail' - return seq + # Add camera to sequencer + temp_camera_binding = seq.add_possessable(temp_camera_actor) -def Sequencer_add_new_camera(seq: unreal.LevelSequence, camera_target_class = unreal.CineCameraActor, camera_name = "MyCamera", is_spawnable_camera = False) -> MovieSceneBindingProxy: + if isinstance(temp_camera_actor, unreal.CineCameraActor): + camera_component_binding = seq.add_possessable(temp_camera_actor.get_cine_camera_component()) + elif isinstance(temp_camera_actor, unreal.CameraActor): + camera_component_binding = seq.add_possessable(temp_camera_actor.camera_component) + else: + camera_component_binding = seq.add_possessable(temp_camera_actor.get_component_by_class(unreal.CameraComponent)) + # Convert to spawnable + camera_binding = seq.add_spawnable_from_instance(temp_camera_actor) + camera_component_binding.set_parent(camera_binding) + temp_camera_binding.remove() - #Create bindings - if is_spawnable_camera: - ''' - I preffer create an level camera an convert to spawnable than use seq.add_spawnable_from_class() - Because with seq.add_spawnable_from_class() it not possible to change actor name an add create camera_component_binding. - Need more control in the API. - ''' + #Clean old camera + temp_camera_actor.destroy_actor() - # Create camera - temp_camera_actor = unreal.EditorLevelLibrary().spawn_actor_from_class(camera_target_class, unreal.Vector(0, 0, 0), unreal.Rotator(0, 0, 0)) - temp_camera_actor.set_actor_label(camera_name) - # Add camera to sequencer - temp_camera_binding = seq.add_possessable(temp_camera_actor) - if isinstance(temp_camera_actor, unreal.CineCameraActor): - camera_component_binding = seq.add_possessable(temp_camera_actor.get_cine_camera_component()) - elif isinstance(temp_camera_actor, unreal.CameraActor): - camera_component_binding = seq.add_possessable(temp_camera_actor.camera_component) else: - camera_component_binding = seq.add_possessable(temp_camera_actor.get_component_by_class(unreal.CameraComponent)) - - - # Convert to spawnable - camera_binding = seq.add_spawnable_from_instance(temp_camera_actor) - camera_component_binding.set_parent(camera_binding) - temp_camera_binding.remove() - - #Clean old camera - temp_camera_actor.destroy_actor() - - - - else: - # Create possessable camera - camera_actor = unreal.EditorLevelLibrary().spawn_actor_from_class(camera_target_class, unreal.Vector(0, 0, 0), unreal.Rotator(0, 0, 0)) - camera_actor.set_actor_label(camera_name) - camera_binding = seq.add_possessable(camera_actor) - camera_component_binding = seq.add_possessable(camera_actor.get_cine_camera_component()) - - if import_module_unreal_utils.is_unreal_version_greater_or_equal(4,26): - camera_binding.set_display_name(camera_name) - else: - pass - - return camera_binding, camera_component_binding - - - -def update_sequencer_camera_tracks(seq: unreal.LevelSequence, camera_binding: MovieSceneBindingProxy, camera_component_binding: MovieSceneBindingProxy, camera_tracks: Dict[str, Any]): - - - # Transform - transform_track = camera_binding.add_track(unreal.MovieScene3DTransformTrack) - transform_section = transform_track.add_section() - transform_section.set_end_frame_bounded(False) - transform_section.set_start_frame_bounded(False) - AddSequencerSectionTransformKeysByIniFile(transform_section, camera_tracks['ue_camera_transform']) - - # Focal Length - TrackFocalLength = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) - TrackFocalLength.set_property_name_and_path('Current Focal Length', 'CurrentFocalLength') - sectionFocalLength = TrackFocalLength.add_section() - sectionFocalLength.set_end_frame_bounded(False) - sectionFocalLength.set_start_frame_bounded(False) - AddSequencerSectionFloatKeysByIniFile(sectionFocalLength, camera_tracks['camera_focal_length']) - - # Sensor Width - TrackSensorWidth = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) - TrackSensorWidth.set_property_name_and_path('Sensor Width (Filmback)', 'Filmback.SensorWidth') - sectionSensorWidth = TrackSensorWidth.add_section() - sectionSensorWidth.set_end_frame_bounded(False) - sectionSensorWidth.set_start_frame_bounded(False) - AddSequencerSectionFloatKeysByIniFile(sectionSensorWidth, camera_tracks['ue_camera_sensor_width']) - - # Sensor Height - TrackSensorHeight = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) - TrackSensorHeight.set_property_name_and_path('Sensor Height (Filmback)', 'Filmback.SensorHeight') - sectionSensorHeight = TrackSensorHeight.add_section() - sectionSensorHeight.set_end_frame_bounded(False) - sectionSensorHeight.set_start_frame_bounded(False) - AddSequencerSectionFloatKeysByIniFile(sectionSensorHeight, camera_tracks['ue_camera_sensor_height']) - - # Focus Distance - TrackFocusDistance = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) - if import_module_unreal_utils.is_unreal_version_greater_or_equal(4,24): - TrackFocusDistance.set_property_name_and_path('Manual Focus Distance (Focus Settings)', 'FocusSettings.ManualFocusDistance') - else: - TrackFocusDistance.set_property_name_and_path('Current Focus Distance', 'ManualFocusDistance') - sectionFocusDistance = TrackFocusDistance.add_section() - sectionFocusDistance.set_end_frame_bounded(False) - sectionFocusDistance.set_start_frame_bounded(False) - AddSequencerSectionFloatKeysByIniFile(sectionFocusDistance, camera_tracks['camera_focus_distance']) - - # Current Aperture - TracknAperture = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) - TracknAperture.set_property_name_and_path('Current Aperture', 'CurrentAperture') - sectionAperture = TracknAperture.add_section() - sectionAperture.set_end_frame_bounded(False) - sectionAperture.set_start_frame_bounded(False) - AddSequencerSectionFloatKeysByIniFile(sectionAperture, camera_tracks['camera_aperture']) - - if camera_tracks['camera_type'] == "ARCHVIS": - - # MovieSceneDoubleVectorTrack not supported in Unreal Engine 5.0 and older - if import_module_unreal_utils.is_unreal_version_greater_or_equal(5,0): - # Camera Shift X/Y - TrackArchVisShift = camera_component_binding.add_track(unreal.MovieSceneDoubleVectorTrack) - TrackArchVisShift.set_property_name_and_path('Manual Correction (Shift)', 'ProjectionOffset') - TrackArchVisShift.set_num_channels_used(2) - SectionArchVisShift = TrackArchVisShift.add_section() - SectionArchVisShift.set_end_frame_bounded(False) - SectionArchVisShift.set_start_frame_bounded(False) - AddSequencerSectionDoubleVectorKeysByIniFile(SectionArchVisShift, camera_tracks['archvis_camera_shift']) - - # Disable auto correct perspective - TrackArchVisCorrectPersp = camera_component_binding.add_track(unreal.MovieSceneBoolTrack) - TrackArchVisCorrectPersp.set_property_name_and_path('Correct Perspective (Auto)', 'bCorrectPerspective') - SectionArchVisCorrectPersp = TrackArchVisCorrectPersp.add_section() - start_frame = unreal.FrameNumber(int(camera_tracks['frame_start'])) - get_section_all_channel(SectionArchVisCorrectPersp)[0].add_key(start_frame, False) - - - - # Spawned - tracksSpawned = camera_binding.find_tracks_by_exact_type(unreal.MovieSceneSpawnTrack) - if len(tracksSpawned) > 0: - sectionSpawned = tracksSpawned[0].get_sections()[0] - AddSequencerSectionBoolKeysByIniFile(sectionSpawned, camera_tracks['camera_spawned']) - - # @TODO Need found a way to set this values... - #camera_component.set_editor_property('aspect_ratio', camera_tracks['desired_screen_ratio']) - - #Projection mode supported since UE 4.26. - #camera_component.set_editor_property('projection_mode', camera_tracks['projection_mode']) - #camera_component.set_editor_property('ortho_width', camera_tracks['ortho_scale']) + # Create possessable camera + camera_actor = unreal.EditorLevelLibrary().spawn_actor_from_class(camera_target_class, unreal.Vector(0, 0, 0), unreal.Rotator(0, 0, 0)) + camera_actor.set_actor_label(camera_name) + camera_binding = seq.add_possessable(camera_actor) + camera_component_binding = seq.add_possessable(camera_actor.get_cine_camera_component()) + + if import_module_unreal_utils.is_unreal_version_greater_or_equal(4,26): + camera_binding.set_display_name(camera_name) + else: + pass + + return camera_binding, camera_component_binding + + def update_sequencer_camera_tracks(seq: unreal.LevelSequence, camera_binding, camera_component_binding, camera_tracks: Dict[str, Any]): + + if TYPE_CHECKING: + if use_movie_scene: + camera_binding: unreal.MovieSceneBindingProxy + camera_component_binding: unreal.MovieSceneBindingProxy + else: + camera_binding: unreal.SequencerBindingProxy + camera_component_binding: unreal.SequencerBindingProxy + + # Transform + transform_track = camera_binding.add_track(unreal.MovieScene3DTransformTrack) + transform_section = transform_track.add_section() + transform_section.set_end_frame_bounded(False) + transform_section.set_start_frame_bounded(False) + AddSequencerSectionTransformKeysByIniFile(transform_section, camera_tracks['ue_camera_transform']) + + # Focal Length + TrackFocalLength = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) + TrackFocalLength.set_property_name_and_path('Current Focal Length', 'CurrentFocalLength') + sectionFocalLength = TrackFocalLength.add_section() + sectionFocalLength.set_end_frame_bounded(False) + sectionFocalLength.set_start_frame_bounded(False) + AddSequencerSectionFloatKeysByIniFile(sectionFocalLength, camera_tracks['camera_focal_length']) + + # Sensor Width + TrackSensorWidth = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) + TrackSensorWidth.set_property_name_and_path('Sensor Width (Filmback)', 'Filmback.SensorWidth') + sectionSensorWidth = TrackSensorWidth.add_section() + sectionSensorWidth.set_end_frame_bounded(False) + sectionSensorWidth.set_start_frame_bounded(False) + AddSequencerSectionFloatKeysByIniFile(sectionSensorWidth, camera_tracks['ue_camera_sensor_width']) + + # Sensor Height + TrackSensorHeight = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) + TrackSensorHeight.set_property_name_and_path('Sensor Height (Filmback)', 'Filmback.SensorHeight') + sectionSensorHeight = TrackSensorHeight.add_section() + sectionSensorHeight.set_end_frame_bounded(False) + sectionSensorHeight.set_start_frame_bounded(False) + AddSequencerSectionFloatKeysByIniFile(sectionSensorHeight, camera_tracks['ue_camera_sensor_height']) + + # Focus Distance + TrackFocusDistance = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) + if import_module_unreal_utils.is_unreal_version_greater_or_equal(4,24): + TrackFocusDistance.set_property_name_and_path('Manual Focus Distance (Focus Settings)', 'FocusSettings.ManualFocusDistance') + else: + TrackFocusDistance.set_property_name_and_path('Current Focus Distance', 'ManualFocusDistance') + sectionFocusDistance = TrackFocusDistance.add_section() + sectionFocusDistance.set_end_frame_bounded(False) + sectionFocusDistance.set_start_frame_bounded(False) + AddSequencerSectionFloatKeysByIniFile(sectionFocusDistance, camera_tracks['camera_focus_distance']) + + # Current Aperture + TracknAperture = camera_component_binding.add_track(unreal.MovieSceneFloatTrack) + TracknAperture.set_property_name_and_path('Current Aperture', 'CurrentAperture') + sectionAperture = TracknAperture.add_section() + sectionAperture.set_end_frame_bounded(False) + sectionAperture.set_start_frame_bounded(False) + AddSequencerSectionFloatKeysByIniFile(sectionAperture, camera_tracks['camera_aperture']) + + if camera_tracks['camera_type'] == "ARCHVIS": + + # MovieSceneDoubleVectorTrack not supported in Unreal Engine 5.0 and older + if import_module_unreal_utils.is_unreal_version_greater_or_equal(5,0): + # Camera Shift X/Y + TrackArchVisShift = camera_component_binding.add_track(unreal.MovieSceneDoubleVectorTrack) + TrackArchVisShift.set_property_name_and_path('Manual Correction (Shift)', 'ProjectionOffset') + TrackArchVisShift.set_num_channels_used(2) + SectionArchVisShift = TrackArchVisShift.add_section() + SectionArchVisShift.set_end_frame_bounded(False) + SectionArchVisShift.set_start_frame_bounded(False) + AddSequencerSectionDoubleVectorKeysByIniFile(SectionArchVisShift, camera_tracks['archvis_camera_shift']) + + # Disable auto correct perspective + TrackArchVisCorrectPersp = camera_component_binding.add_track(unreal.MovieSceneBoolTrack) + TrackArchVisCorrectPersp.set_property_name_and_path('Correct Perspective (Auto)', 'bCorrectPerspective') + SectionArchVisCorrectPersp = TrackArchVisCorrectPersp.add_section() + start_frame = unreal.FrameNumber(int(camera_tracks['frame_start'])) + get_section_all_channel(SectionArchVisCorrectPersp)[0].add_key(start_frame, False) + + + + # Spawned + tracksSpawned = camera_binding.find_tracks_by_exact_type(unreal.MovieSceneSpawnTrack) + if len(tracksSpawned) > 0: + sectionSpawned = tracksSpawned[0].get_sections()[0] + AddSequencerSectionBoolKeysByIniFile(sectionSpawned, camera_tracks['camera_spawned']) + + # @TODO Need found a way to set this values... + #camera_component.set_editor_property('aspect_ratio', camera_tracks['desired_screen_ratio']) - #camera_component.lens_settings.set_editor_property('min_f_stop', camera_tracks['ue_lens_minfstop']) - #camera_component.lens_settings.set_editor_property('max_f_stop', camera_tracks['ue_lens_maxfstop']) + #Projection mode supported since UE 4.26. + #camera_component.set_editor_property('projection_mode', camera_tracks['projection_mode']) + #camera_component.set_editor_property('ortho_width', camera_tracks['ortho_scale']) + + #camera_component.lens_settings.set_editor_property('min_f_stop', camera_tracks['ue_lens_minfstop']) + #camera_component.lens_settings.set_editor_property('max_f_stop', camera_tracks['ue_lens_maxfstop']) diff --git a/blender-for-unrealengine/bfu_light_map/bfu_light_map_props.py b/blender-for-unrealengine/bfu_light_map/bfu_light_map_props.py index 670bfdbf..965187d0 100644 --- a/blender-for-unrealengine/bfu_light_map/bfu_light_map_props.py +++ b/blender-for-unrealengine/bfu_light_map/bfu_light_map_props.py @@ -25,11 +25,20 @@ from .. import bbpl - +def get_preset_values(): + preset_values = [ + 'obj.bfu_static_mesh_light_map_mode', + 'obj.bfu_static_mesh_custom_light_map_res', + 'obj.bfu_static_mesh_light_map_surface_scale', + 'obj.bfu_static_mesh_light_map_round_power_of_two', + 'obj.bfu_use_static_mesh_light_map_world_scale', + 'obj.bfu_generate_light_map_uvs', + ] + return preset_values class BFU_OT_ComputLightMap(bpy.types.Operator): bl_label = "Calculate surface area" - bl_idname = "object.computlightmap" + bl_idname = "object.comput_lightmap" bl_description = "Click to calculate the surface of the object" def execute(self, context): @@ -43,7 +52,7 @@ def execute(self, context): class BFU_OT_ComputAllLightMap(bpy.types.Operator): bl_label = "Calculate all surface area" - bl_idname = "object.computalllightmap" + bl_idname = "object.comput_all_lightmap" bl_description = ( "Click to calculate the surface of the all object in the scene" ) @@ -68,9 +77,97 @@ def register(): for cls in classes: bpy.utils.register_class(cls) + bpy.types.Scene.bfu_object_light_map_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Light map") + bpy.types.Scene.bfu_tools_light_map_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Light Map") + + + # StaticMeshImportData + # https://api.unrealengine.com/INT/API/Editor/UnrealEd/Factories/UFbxStaticMeshImportData/index.html + + + bpy.types.Object.bfu_static_mesh_light_map_mode = bpy.props.EnumProperty( + name="Light Map", + description='Specify how the light map resolution will be generated', + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ("Default", + "Default", + "Has no effect on light maps", + 1), + ("CustomMap", + "Custom map", + "Set the custom light map resolution", + 2), + ("SurfaceArea", + "Surface Area", + "Set light map resolution depending on the surface Area", + 3) + ] + ) + + bpy.types.Object.bfu_static_mesh_custom_light_map_res = bpy.props.IntProperty( + name="Light Map Resolution", + description="This is the resolution of the light map", + override={'LIBRARY_OVERRIDABLE'}, + soft_max=2048, + soft_min=16, + max=4096, # Max for unreal + min=4, # Min for unreal + default=64 + ) + + bpy.types.Object.computedStaticMeshLightMapRes = bpy.props.FloatProperty( + name="Computed Light Map Resolution", + description="This is the computed resolution of the light map", + override={'LIBRARY_OVERRIDABLE'}, + default=64.0 + ) + + bpy.types.Object.bfu_static_mesh_light_map_surface_scale = bpy.props.FloatProperty( + name="Surface scale", + description="This is for resacle the surface Area value", + override={'LIBRARY_OVERRIDABLE'}, + min=0.00001, # Min for unreal + default=64 + ) + bpy.types.Object.bfu_static_mesh_light_map_round_power_of_two = bpy.props.BoolProperty( + name="Round power of 2", + description=( + "round Light Map resolution to nearest power of 2" + ), + default=True + ) + bpy.types.Object.bfu_use_static_mesh_light_map_world_scale = bpy.props.BoolProperty( + name="Use world scale", + description=( + "If not that will use the object scale." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + + bpy.types.Object.bfu_generate_light_map_uvs = bpy.props.BoolProperty( + name="Generate LightmapUVs", + description=( + "If checked, UVs for Lightmap will automatically be generated." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True, + ) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_generate_light_map_uvs + del bpy.types.Object.bfu_use_static_mesh_light_map_world_scale + del bpy.types.Object.bfu_static_mesh_light_map_round_power_of_two + del bpy.types.Object.bfu_static_mesh_light_map_surface_scale + del bpy.types.Object.computedStaticMeshLightMapRes + del bpy.types.Object.bfu_static_mesh_custom_light_map_res + del bpy.types.Object.bfu_static_mesh_light_map_mode + + del bpy.types.Scene.bfu_tools_light_map_properties_expanded + del bpy.types.Scene.bfu_object_light_map_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_light_map/bfu_light_map_ui.py b/blender-for-unrealengine/bfu_light_map/bfu_light_map_ui.py index 51398bc4..18b1c673 100644 --- a/blender-for-unrealengine/bfu_light_map/bfu_light_map_ui.py +++ b/blender-for-unrealengine/bfu_light_map/bfu_light_map_ui.py @@ -18,14 +18,56 @@ import bpy +from . import bfu_light_map_utils from .. import bfu_basics from .. import bfu_utils from .. import bfu_ui from .. import bbpl +from .. import bfu_static_mesh -def draw_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): - pass +def draw_obj_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): -def draw_ui_scene(layout: bpy.types.UILayout): - pass \ No newline at end of file + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + is_static_mesh = bfu_static_mesh.bfu_static_mesh_utils.is_static_mesh(obj) + + # Hide filters + if obj is None: + return + if addon_prefs.useGeneratedScripts is False: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "MISC"): + scene.bfu_object_light_map_properties_expanded.draw(layout) + if scene.bfu_object_light_map_properties_expanded.is_expend(): + if is_static_mesh: + StaticMeshLightMapRes = layout.box() + StaticMeshLightMapRes.prop(obj, 'bfu_static_mesh_light_map_mode') + if obj.bfu_static_mesh_light_map_mode == "CustomMap": + CustomLightMap = StaticMeshLightMapRes.column() + CustomLightMap.prop(obj, 'bfu_static_mesh_custom_light_map_res') + if obj.bfu_static_mesh_light_map_mode == "SurfaceArea": + SurfaceAreaLightMap = StaticMeshLightMapRes.column() + SurfaceAreaLightMapButton = SurfaceAreaLightMap.row() + SurfaceAreaLightMapButton.operator("object.comput_lightmap", icon='TEXTURE') + SurfaceAreaLightMapButton.operator("object.comput_all_lightmap", icon='TEXTURE') + SurfaceAreaLightMap.prop(obj, 'bfu_use_static_mesh_light_map_world_scale') + SurfaceAreaLightMap.prop(obj, 'bfu_static_mesh_light_map_surface_scale') + SurfaceAreaLightMap.prop(obj, 'bfu_static_mesh_light_map_round_power_of_two') + if obj.bfu_static_mesh_light_map_mode != "Default": + CompuntedLightMap = str(bfu_light_map_utils.GetCompuntedLightMap(obj)) + StaticMeshLightMapRes.label(text='Compunted light map: ' + CompuntedLightMap) + bfu_generate_light_map_uvs = layout.row() + bfu_generate_light_map_uvs.prop(obj, 'bfu_generate_light_map_uvs') + +def draw_tools_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene + scene.bfu_tools_light_map_properties_expanded.draw(layout) + if scene.bfu_tools_light_map_properties_expanded.is_expend(): + checkButton = layout.column() + checkButton.operator("object.comput_all_lightmap", icon='TEXTURE') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_light_map/bfu_light_map_utils.py b/blender-for-unrealengine/bfu_light_map/bfu_light_map_utils.py index 66c1619a..bbd8a1df 100644 --- a/blender-for-unrealengine/bfu_light_map/bfu_light_map_utils.py +++ b/blender-for-unrealengine/bfu_light_map/bfu_light_map_utils.py @@ -18,13 +18,10 @@ import bpy import fnmatch -from .. import bps +from .. import bpl from .. import bbpl from .. import bfu_basics from .. import bfu_utils -from .. import bfu_unreal_utils -from .. import bfu_export_logs -from .. import bfu_assets_manager from .. import bfu_static_mesh @@ -33,7 +30,7 @@ def GetExportRealSurfaceArea(obj): local_view_areas = bbpl.scene_utils.move_to_global_view() bbpl.utils.safe_mode_set('OBJECT') - SavedSelect = bbpl.utils.UserSelectSave() + SavedSelect = bbpl.save_data.select_save.UserSelectSave() SavedSelect.save_current_select() bfu_utils.SelectParentAndDesiredChilds(obj) @@ -66,7 +63,7 @@ def GetExportRealSurfaceArea(obj): active = bpy.context.view_layer.objects.active area = bfu_basics.GetSurfaceArea(active) bfu_utils.CleanDeleteObjects(bpy.context.selected_objects) - SavedSelect.reset_select_by_ref() + SavedSelect.reset_select() bbpl.scene_utils.move_to_local_view(local_view_areas) return area @@ -95,7 +92,7 @@ def GetCompuntedLightMap(obj): area *= obj.bfu_static_mesh_light_map_surface_scale/2 if obj.bfu_static_mesh_light_map_round_power_of_two: - return bps.math.nearest_power_of_two(int(round(area))) + return bpl.math.nearest_power_of_two(int(round(area))) return int(round(area)) def UpdateAreaLightMapList(objects_to_update=None): @@ -113,7 +110,7 @@ def UpdateAreaLightMapList(objects_to_update=None): UpdatedRes = 0 - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() for obj in objs: obj.computedStaticMeshLightMapRes = GetExportRealSurfaceArea(obj) UpdatedRes += 1 diff --git a/blender-for-unrealengine/bfu_lod/bfu_lod_props.py b/blender-for-unrealengine/bfu_lod/bfu_lod_props.py index 6a1c8c85..06dfa9e0 100644 --- a/blender-for-unrealengine/bfu_lod/bfu_lod_props.py +++ b/blender-for-unrealengine/bfu_lod/bfu_lod_props.py @@ -27,6 +27,19 @@ +def get_preset_values(): + preset_values = [ + 'obj.bfu_export_as_lod_mesh', + 'obj.bfu_use_static_mesh_lod_group', + 'obj.bfu_static_mesh_lod_group', + 'obj.bfu_lod_target1', + 'obj.bfu_lod_target2', + 'obj.bfu_lod_target3', + 'obj.bfu_lod_target4', + 'obj.bfu_lod_target5', + ] + return preset_values + # ------------------------------------------------------------------- # Register & Unregister # ------------------------------------------------------------------- @@ -41,9 +54,85 @@ def register(): bpy.types.Scene.bfu_lod_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Lod") + bpy.types.Object.bfu_export_as_lod_mesh = bpy.props.BoolProperty( + name="Export as lod?", + description=( + "If true this mesh will be exported" + + " as a level of detail for another mesh" + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + + # Lod Group + bpy.types.Object.bfu_use_static_mesh_lod_group = bpy.props.BoolProperty( + name="", + description='', + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) + + bpy.types.Object.bfu_static_mesh_lod_group = bpy.props.StringProperty( + name="LOD Group", + description=( + "The LODGroup to associate with this mesh when it is imported." + + " Default: LevelArchitecture, SmallProp, " + + "LargeProp, Deco, Vista, Foliage, HighDetail" + ), + override={'LIBRARY_OVERRIDABLE'}, + maxlen=32, + default="SmallProp" + ) + + # Lod list + bpy.types.Object.bfu_lod_target1 = bpy.props.PointerProperty( + name="LOD1", + description="Target objet for level of detail 01", + override={'LIBRARY_OVERRIDABLE'}, + type=bpy.types.Object + ) + + bpy.types.Object.bfu_lod_target2 = bpy.props.PointerProperty( + name="LOD2", + description="Target objet for level of detail 02", + override={'LIBRARY_OVERRIDABLE'}, + type=bpy.types.Object + ) + + bpy.types.Object.bfu_lod_target3 = bpy.props.PointerProperty( + name="LOD3", + description="Target objet for level of detail 03", + override={'LIBRARY_OVERRIDABLE'}, + type=bpy.types.Object + ) + + bpy.types.Object.bfu_lod_target4 = bpy.props.PointerProperty( + name="LOD4", + description="Target objet for level of detail 04", + override={'LIBRARY_OVERRIDABLE'}, + type=bpy.types.Object + ) + + bpy.types.Object.bfu_lod_target5 = bpy.props.PointerProperty( + name="LOD5", + description="Target objet for level of detail 05", + override={'LIBRARY_OVERRIDABLE'}, + type=bpy.types.Object + ) + def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) + del bpy.types.Object.bfu_lod_target5 + del bpy.types.Object.bfu_lod_target4 + del bpy.types.Object.bfu_lod_target3 + del bpy.types.Object.bfu_lod_target2 + del bpy.types.Object.bfu_lod_target1 + + del bpy.types.Object.bfu_static_mesh_lod_group + del bpy.types.Object.bfu_use_static_mesh_lod_group + + del bpy.types.Object.bfu_export_as_lod_mesh del bpy.types.Scene.bfu_lod_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_lod/bfu_lod_ui.py b/blender-for-unrealengine/bfu_lod/bfu_lod_ui.py index 9087aa88..3de0ed91 100644 --- a/blender-for-unrealengine/bfu_lod/bfu_lod_ui.py +++ b/blender-for-unrealengine/bfu_lod/bfu_lod_ui.py @@ -24,9 +24,10 @@ from .. import bfu_ui from .. import bbpl from .. import bfu_assets_manager +from .. import bfu_static_mesh -def draw_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): +def draw_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): if obj is None: return @@ -34,17 +35,44 @@ def draw_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): scene = bpy.context.scene addon_prefs = bfu_basics.GetAddonPrefs() - if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if addon_prefs.useGeneratedScripts is False: + return + if obj.bfu_export_type != "export_recursive": + return + + # Draw UI + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "MISC"): scene.bfu_lod_properties_expanded.draw(layout) if scene.bfu_lod_properties_expanded.is_expend(): - if obj.bfu_export_type == "export_recursive": - if not bfu_utils.GetExportAsProxy(obj): - if addon_prefs.useGeneratedScripts: - # Unreal python no longer support Skeletal mesh LODS import. - asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) - if asset_class and asset_class.use_lods == True: - LodProp = layout.column() - LodProp.prop(obj, 'bfu_export_as_lod_mesh') - -def draw_ui_scene(layout: bpy.types.UILayout): - pass \ No newline at end of file + + # Unreal python no longer support Skeletal mesh LODS import. + asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) + if asset_class and asset_class.use_lods == True: + LodProp = layout.column() + LodProp.prop(obj, 'bfu_export_as_lod_mesh') + + #Static only because Unreal python not support Skeletal mesh LODS import. + if (bfu_static_mesh.bfu_static_mesh_utils.is_static_mesh(obj)): + + # Lod Groups + bfu_static_mesh_lod_group = layout.row() + bfu_static_mesh_lod_group.prop(obj, 'bfu_use_static_mesh_lod_group', text="") + SMLODGroupChild = bfu_static_mesh_lod_group.column() + SMLODGroupChild.enabled = obj.bfu_use_static_mesh_lod_group + SMLODGroupChild.prop(obj, 'bfu_static_mesh_lod_group') + bfu_static_mesh_lod_group.enabled = obj.bfu_export_as_lod_mesh is False + + # Lod Slots + LodList = layout.column() + LodList.prop(obj, 'bfu_lod_target1') + LodList.prop(obj, 'bfu_lod_target2') + LodList.prop(obj, 'bfu_lod_target3') + LodList.prop(obj, 'bfu_lod_target4') + LodList.prop(obj, 'bfu_lod_target5') + LodList.enabled = obj.bfu_export_as_lod_mesh is False and obj.bfu_use_static_mesh_lod_group is False + diff --git a/blender-for-unrealengine/bfu_lod/bfu_lod_utils.py b/blender-for-unrealengine/bfu_lod/bfu_lod_utils.py index db041b58..a6e76eb3 100644 --- a/blender-for-unrealengine/bfu_lod/bfu_lod_utils.py +++ b/blender-for-unrealengine/bfu_lod/bfu_lod_utils.py @@ -19,8 +19,4 @@ import bpy import fnmatch from .. import bbpl -from .. import bfu_basics -from .. import bfu_utils -from .. import bfu_unreal_utils -from .. import bfu_export_logs diff --git a/blender-for-unrealengine/bfu_material/bfu_material_props.py b/blender-for-unrealengine/bfu_material/bfu_material_props.py index 27d5dcc9..45c3def2 100644 --- a/blender-for-unrealengine/bfu_material/bfu_material_props.py +++ b/blender-for-unrealengine/bfu_material/bfu_material_props.py @@ -60,9 +60,9 @@ def register(): default=False ) - # Used for set invert_normal_maps in FbxTextureImportData + # Used for set flip_normal_map_green_channel in FbxTextureImportData # https://docs.unrealengine.com/5.3/en-US/PythonAPI/class/FbxTextureImportData.html - bpy.types.Object.bfu_invert_normal_maps = bpy.props.BoolProperty( + bpy.types.Object.bfu_flip_normal_map_green_channel = bpy.props.BoolProperty( name="Invert Normal Maps", description="This option will cause normal map Y (Green) values to be inverted.", default=False @@ -117,7 +117,7 @@ def unregister(): del bpy.types.Object.bfu_material_search_location del bpy.types.Object.bfu_reorder_material_to_fbx_order - del bpy.types.Object.bfu_invert_normal_maps + del bpy.types.Object.bfu_flip_normal_map_green_channel del bpy.types.Object.bfu_import_textures del bpy.types.Object.bfu_import_materials diff --git a/blender-for-unrealengine/bfu_material/bfu_material_ui.py b/blender-for-unrealengine/bfu_material/bfu_material_ui.py index 1926b87e..01dedd34 100644 --- a/blender-for-unrealengine/bfu_material/bfu_material_ui.py +++ b/blender-for-unrealengine/bfu_material/bfu_material_ui.py @@ -27,7 +27,7 @@ -def draw_ui_object_collision(layout: bpy.types.UILayout): +def draw_ui_object(layout: bpy.types.UILayout): if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "MISC"): scene = bpy.context.scene @@ -46,7 +46,7 @@ def draw_ui_object_collision(layout: bpy.types.UILayout): bfu_material_search_location.prop(obj, 'bfu_material_search_location') bfu_material_search_location.prop(obj, 'bfu_import_materials') bfu_material_search_location.prop(obj, 'bfu_import_textures') - bfu_material_search_location.prop(obj, 'bfu_invert_normal_maps') + bfu_material_search_location.prop(obj, 'bfu_flip_normal_map_green_channel') bfu_material_search_location.prop(obj, 'bfu_reorder_material_to_fbx_order') diff --git a/blender-for-unrealengine/bfu_material/bfu_material_utils.py b/blender-for-unrealengine/bfu_material/bfu_material_utils.py index a0f86577..568d8b3b 100644 --- a/blender-for-unrealengine/bfu_material/bfu_material_utils.py +++ b/blender-for-unrealengine/bfu_material/bfu_material_utils.py @@ -30,7 +30,7 @@ def get_material_asset_data(asset: bfu_export_logs.BFU_OT_UnrealExportedAsset): if asset.asset_type in ["StaticMesh", "SkeletalMesh"]: asset_data["import_materials"] = asset.object.bfu_import_materials asset_data["import_textures"] = asset.object.bfu_import_textures - asset_data["invert_normal_maps"] = asset.object.bfu_invert_normal_maps + asset_data["flip_normal_map_green_channel"] = asset.object.bfu_flip_normal_map_green_channel asset_data["reorder_material_to_fbx_order"] = asset.object.bfu_reorder_material_to_fbx_order asset_data["material_search_location"] = asset.object.bfu_material_search_location return asset_data \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_modular_skeletal_mesh/__init__.py b/blender-for-unrealengine/bfu_modular_skeletal_mesh/__init__.py new file mode 100644 index 00000000..588d2061 --- /dev/null +++ b/blender-for-unrealengine/bfu_modular_skeletal_mesh/__init__.py @@ -0,0 +1,54 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_modular_skeletal_mesh_props +from . import bfu_modular_skeletal_mesh_type +from . import bfu_modular_skeletal_mesh_ui +from . import bfu_modular_skeletal_mesh_utils + + +if "bfu_modular_skeletal_mesh_props" in locals(): + importlib.reload(bfu_modular_skeletal_mesh_props) +if "bfu_modular_skeletal_mesh_ui" in locals(): + importlib.reload(bfu_modular_skeletal_mesh_ui) +if "bfu_modular_skeletal_mesh_utils" in locals(): + importlib.reload(bfu_modular_skeletal_mesh_utils) +if "bfu_modular_skeletal_mesh_type" in locals(): + importlib.reload(bfu_modular_skeletal_mesh_type) + + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_modular_skeletal_mesh_props.register() + bfu_modular_skeletal_mesh_type.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_modular_skeletal_mesh_type.unregister() + bfu_modular_skeletal_mesh_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_props.py b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_props.py new file mode 100644 index 00000000..57286b84 --- /dev/null +++ b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_props.py @@ -0,0 +1,80 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bbpl + +def get_preset_values(): + preset_values = [ + 'obj.bfu_modular_skeletal_mesh_mode', + 'obj.bfu_modular_skeletal_mesh_every_meshs_separate', + 'obj.bfu_modular_skeletal_specified_parts_meshs_template' + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_modular_skeletal_mesh_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Modular Skeletal Mesh") + + bpy.types.Object.bfu_modular_skeletal_mesh_mode = bpy.props.EnumProperty( + name="Modular Skeletal Mesh Mode", + description='Modular skeletal mesh mode', + override={'LIBRARY_OVERRIDABLE'}, + items=[ + ("all_in_one", + "All In One", + "Export all child meshs of the armature as one skeletal mesh.", + 1), + ("every_meshs", + "Every Meshs", + "Export one skeletal mesh for every child meshs of the armature.", + 2), + ("specified_parts", + "Specified Parts", + "Export specified mesh parts.", + 3) + ] + ) + + bpy.types.Object.bfu_modular_skeletal_mesh_every_meshs_separate = bpy.props.StringProperty( + name="Separate string", + description="String between armature name and mesh name", + override={'LIBRARY_OVERRIDABLE'}, + default="_" + ) + + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.bfu_modular_skeletal_mesh_properties_expanded + del bpy.types.Object.bfu_modular_skeletal_mesh_every_meshs_separate + del bpy.types.Object.bfu_modular_skeletal_mesh_mode \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_addon_parts/bfu_modular_skeletal_specified_parts_meshs.py b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_type.py similarity index 67% rename from blender-for-unrealengine/bfu_addon_parts/bfu_modular_skeletal_specified_parts_meshs.py rename to blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_type.py index bb2e874a..c081e196 100644 --- a/blender-for-unrealengine/bfu_addon_parts/bfu_modular_skeletal_specified_parts_meshs.py +++ b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_type.py @@ -16,23 +16,17 @@ # # ======================= END GPL LICENSE BLOCK ============================= +import os import bpy +import fnmatch from .. import bbpl -def get_preset_values(): - preset_values = [ - 'obj.bfu_modular_skeletal_mesh_mode', - 'obj.bfu_modular_skeletal_mesh_every_meshs_separate', - 'obj.bfu_modular_skeletal_specified_parts_meshs_template' - ] - return preset_values - BBPL_UI_TemplateItem = bbpl.blender_layout.layout_template_list.types.create_template_item_class() -BBPL_UI_TemplateItemDraw = bbpl.blender_layout.layout_template_list.types.create_template_item_draw_class() -BBPL_UI_TemplateList = bbpl.blender_layout.layout_template_list.types.create_template_list_class(BBPL_UI_TemplateItem, BBPL_UI_TemplateItemDraw) +BBPL_UL_TemplateItemDraw = bbpl.blender_layout.layout_template_list.types.create_template_item_draw_class() +BBPL_UI_TemplateList = bbpl.blender_layout.layout_template_list.types.create_template_list_class(BBPL_UI_TemplateItem, BBPL_UL_TemplateItemDraw) -class BFU_UL_ModularSkeletalSpecifiedPartsTargetItem(BBPL_UI_TemplateItem): +class BFU_UI_ModularSkeletalSpecifiedPartsTargetItem(BBPL_UI_TemplateItem): # Item class (bpy.types.PropertyGroup) enabled: bpy.props.BoolProperty( name="Use", default=True @@ -60,8 +54,7 @@ class BFU_UL_ModularSkeletalSpecifiedPartsTargetItem(BBPL_UI_TemplateItem): type=bpy.types.Collection, ) - -class BFU_UL_ModularSkeletalSpecifiedPartsTargetItemDraw(BBPL_UI_TemplateItemDraw): +class BFU_UL_ModularSkeletalSpecifiedPartsTargetItemDraw(BBPL_UL_TemplateItemDraw): # Draw Item class (bpy.types.UIList) def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): prop_line = layout @@ -86,18 +79,14 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn elif item.target_type == "COLLECTION": prop_data.prop(item, "collection", text="") prop_data.enabled = item.enabled - -class BFU_ModularSkeletalSpecifiedPartsTargetList(BBPL_UI_TemplateList): - template_collection: bpy.props.CollectionProperty(type=BFU_UL_ModularSkeletalSpecifiedPartsTargetItem) - template_collection_uilist_class: bpy.props.StringProperty(default = "BFU_UL_ModularSkeletalSpecifiedPartsTargetItemDraw") +class BFU_UI_ModularSkeletalSpecifiedPartsTargetList(BBPL_UI_TemplateList): # Draw Item class (bpy.types.UIList) + template_collection: bpy.props.CollectionProperty(type=BFU_UI_ModularSkeletalSpecifiedPartsTargetItem) + template_collection_uilist_class_name = "BFU_UL_ModularSkeletalSpecifiedPartsTargetItemDraw" rows: bpy.props.IntProperty(default = 3) maxrows: bpy.props.IntProperty(default = 3) - - - -class BFU_UL_ModularSkeletalSpecifiedPartsMeshItem(BBPL_UI_TemplateItem): +class BFU_UI_ModularSkeletalSpecifiedPartsMeshItem(BBPL_UI_TemplateItem): # Item class (bpy.types.PropertyGroup) enabled: bpy.props.BoolProperty( name="Use", default=True @@ -110,10 +99,10 @@ class BFU_UL_ModularSkeletalSpecifiedPartsMeshItem(BBPL_UI_TemplateItem): ) skeletal_parts: bpy.props.PointerProperty( - type=BFU_ModularSkeletalSpecifiedPartsTargetList + type=BFU_UI_ModularSkeletalSpecifiedPartsTargetList ) -class BFU_UL_ModularSkeletalSpecifiedPartsMeshItemDraw(BBPL_UI_TemplateItemDraw): +class BFU_UL_ModularSkeletalSpecifiedPartsMeshItemDraw(BBPL_UL_TemplateItemDraw): # Draw Item class (bpy.types.UIList) def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): prop_line = layout @@ -144,11 +133,10 @@ def draw_item(self, context, layout, data, item, icon, active_data, active_propn if obj_len+col_len == 0: prop_data.label(text="", icon="ERROR") prop_data.enabled = item.enabled - -class BFU_ModularSkeletalSpecifiedPartsMeshs(BBPL_UI_TemplateList): - template_collection: bpy.props.CollectionProperty(type=BFU_UL_ModularSkeletalSpecifiedPartsMeshItem) - template_collection_uilist_class: bpy.props.StringProperty(default = "BFU_UL_ModularSkeletalSpecifiedPartsMeshItemDraw") +class BFU_UI_ModularSkeletalSpecifiedPartsMeshs(BBPL_UI_TemplateList): # Draw Item class (bpy.types.UIList) + template_collection: bpy.props.CollectionProperty(type=BFU_UI_ModularSkeletalSpecifiedPartsMeshItem) + template_collection_uilist_class_name = "BFU_UL_ModularSkeletalSpecifiedPartsMeshItemDraw" def draw(self, layout: bpy.types.UILayout): super().draw(layout) @@ -167,16 +155,20 @@ def draw(self, layout: bpy.types.UILayout): prop_data.enabled = item.enabled item.skeletal_parts.draw(box).enabled = item.enabled - +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + classes = ( - BFU_UL_ModularSkeletalSpecifiedPartsTargetItem, + BFU_UI_ModularSkeletalSpecifiedPartsTargetItem, BFU_UL_ModularSkeletalSpecifiedPartsTargetItemDraw, - BFU_ModularSkeletalSpecifiedPartsTargetList, - BFU_UL_ModularSkeletalSpecifiedPartsMeshItem, + BFU_UI_ModularSkeletalSpecifiedPartsTargetList, + + BFU_UI_ModularSkeletalSpecifiedPartsMeshItem, BFU_UL_ModularSkeletalSpecifiedPartsMeshItemDraw, - BFU_ModularSkeletalSpecifiedPartsMeshs, + BFU_UI_ModularSkeletalSpecifiedPartsMeshs, ) @@ -184,41 +176,11 @@ def register(): for cls in classes: bpy.utils.register_class(cls) - bpy.types.Object.bfu_modular_skeletal_mesh_mode = bpy.props.EnumProperty( - name="Modular Skeletal Mesh Mode", - description='Modular skeletal mesh mode', - override={'LIBRARY_OVERRIDABLE'}, - items=[ - ("all_in_one", - "All In One", - "Export all child meshs of the armature as one skeletal mesh.", - 1), - ("every_meshs", - "Every Meshs", - "Export one skeletal mesh for every child meshs of the armature.", - 2), - ("specified_parts", - "Specified Parts", - "Export specified mesh parts.", - 3) - ] - ) - - bpy.types.Object.bfu_modular_skeletal_mesh_every_meshs_separate = bpy.props.StringProperty( - name="Separate string", - description="String between armature name and mesh name", - override={'LIBRARY_OVERRIDABLE'}, - default="_" - ) - - - bpy.types.Object.bfu_modular_skeletal_specified_parts_meshs_template = bpy.props.PointerProperty(type=BFU_ModularSkeletalSpecifiedPartsMeshs) + bpy.types.Object.bfu_modular_skeletal_specified_parts_meshs_template = bpy.props.PointerProperty(type=BFU_UI_ModularSkeletalSpecifiedPartsMeshs) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) - del bpy.types.Object.bfu_modular_skeletal_specified_parts_meshs_template - del bpy.types.Object.bfu_modular_skeletal_mesh_every_meshs_separate - del bpy.types.Object.bfu_modular_skeletal_mesh_mode \ No newline at end of file + del bpy.types.Object.bfu_modular_skeletal_specified_parts_meshs_template \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_ui.py b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_ui.py new file mode 100644 index 00000000..d791b5cb --- /dev/null +++ b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_ui.py @@ -0,0 +1,74 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_ui +from .. import bfu_skeletal_mesh + + +def draw_general_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): + if obj is None: + return + + if obj.type != "ARMATURE": + return + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + if scene.bfu_object_properties_expanded.is_expend(): + if obj.bfu_export_type == "export_recursive": + if not obj.bfu_export_as_alembic_animation: + AssetType2 = layout.column() + # Show asset type + AssetType2.prop(obj, "bfu_export_skeletal_mesh_as_static_mesh") + +def draw_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + is_skeletal_mesh = bfu_skeletal_mesh.bfu_skeletal_mesh_utils.is_skeletal_mesh(obj) + + + if obj is None: + return + if obj.type != "ARMATURE": + return + if is_skeletal_mesh is False: + return + if obj.bfu_export_type != "export_recursive": + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + scene.bfu_modular_skeletal_mesh_properties_expanded.draw(layout) + if scene.bfu_modular_skeletal_mesh_properties_expanded.is_expend(): + + # SkeletalMesh prop + if not obj.bfu_export_as_lod_mesh: + modular_skeletal_mesh = layout.column() + modular_skeletal_mesh.prop(obj, "bfu_modular_skeletal_mesh_mode") + if obj.bfu_modular_skeletal_mesh_mode == "every_meshs": + modular_skeletal_mesh.prop(obj, "bfu_modular_skeletal_mesh_every_meshs_separate") + if obj.bfu_modular_skeletal_mesh_mode == "specified_parts": + obj.bfu_modular_skeletal_specified_parts_meshs_template.draw(modular_skeletal_mesh) + +def draw_ui_scene(layout: bpy.types.UILayout): + pass \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_utils.py b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_utils.py new file mode 100644 index 00000000..e57be8f1 --- /dev/null +++ b/blender-for-unrealengine/bfu_modular_skeletal_mesh/bfu_modular_skeletal_mesh_utils.py @@ -0,0 +1,19 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy diff --git a/blender-for-unrealengine/bfu_naming.py b/blender-for-unrealengine/bfu_naming.py index 93c7ae6b..8f72a824 100644 --- a/blender-for-unrealengine/bfu_naming.py +++ b/blender-for-unrealengine/bfu_naming.py @@ -18,16 +18,6 @@ import bpy -import bmesh -import string -import fnmatch -import mathutils -import math -import os -import math -from . import bbpl -from . import bps -from . import bfu_write_text from . import bfu_basics from . import bfu_utils diff --git a/blender-for-unrealengine/bfu_propertys/bfu_scene_propertys.py b/blender-for-unrealengine/bfu_propertys/bfu_scene_propertys.py index 13d98b7f..f09f68f9 100644 --- a/blender-for-unrealengine/bfu_propertys/bfu_scene_propertys.py +++ b/blender-for-unrealengine/bfu_propertys/bfu_scene_propertys.py @@ -28,31 +28,6 @@ def register(): for cls in classes: bpy.utils.register_class(cls) - bpy.types.Scene.bfu_object_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Object Properties") - bpy.types.Scene.bfu_object_lod_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Lod") - bpy.types.Scene.bfu_object_collision_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Collision") - bpy.types.Scene.bfu_object_light_map_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Light map") - bpy.types.Scene.bfu_object_uv_map_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="UV map") - - bpy.types.Scene.bfu_animation_action_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Actions Properties") - bpy.types.Scene.bfu_animation_action_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Actions Advanced Properties") - bpy.types.Scene.bfu_animation_nla_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="NLA Properties") - bpy.types.Scene.bfu_animation_nla_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="NLA Advanced Properties") - bpy.types.Scene.bfu_animation_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Animation Advanced Properties") - - bpy.types.Scene.bfu_engine_ref_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Engine Refs") - - bpy.types.Scene.bfu_collection_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Collection Properties") - bpy.types.Scene.bfu_object_advanced_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Object advanced Properties") - bpy.types.Scene.bfu_collision_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Collision") - bpy.types.Scene.bfu_socket_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Socket") - bpy.types.Scene.bfu_uvmap_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="UV Map") - bpy.types.Scene.bfu_lightmap_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Light Map") - bpy.types.Scene.bfu_nomenclature_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Nomenclature") - bpy.types.Scene.bfu_export_filter_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Export filters") - bpy.types.Scene.bfu_export_process_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Export process") - bpy.types.Scene.bfu_script_tool_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Copy Import Script") - bpy.types.Scene.bfu_active_tab = bpy.props.EnumProperty( items=( ('OBJECT', 'Object', 'Object tab.'), @@ -81,30 +56,7 @@ def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) - del bpy.types.Scene.bfu_object_properties_expanded - del bpy.types.Scene.bfu_object_lod_properties_expanded - del bpy.types.Scene.bfu_object_collision_properties_expanded - del bpy.types.Scene.bfu_object_light_map_properties_expanded - del bpy.types.Scene.bfu_object_uv_map_properties_expanded - - del bpy.types.Scene.bfu_animation_action_properties_expanded - del bpy.types.Scene.bfu_animation_action_advanced_properties_expanded - del bpy.types.Scene.bfu_animation_nla_properties_expanded - del bpy.types.Scene.bfu_animation_nla_advanced_properties_expanded - del bpy.types.Scene.bfu_animation_advanced_properties_expanded - - del bpy.types.Scene.bfu_engine_ref_properties_expanded - - del bpy.types.Scene.bfu_collection_properties_expanded - del bpy.types.Scene.bfu_object_advanced_properties_expanded - del bpy.types.Scene.bfu_collision_expanded - del bpy.types.Scene.bfu_uvmap_expanded - del bpy.types.Scene.bfu_socket_expanded - del bpy.types.Scene.bfu_lightmap_expanded - del bpy.types.Scene.bfu_nomenclature_properties_expanded - del bpy.types.Scene.bfu_export_filter_properties_expanded - del bpy.types.Scene.bfu_export_process_properties_expanded - del bpy.types.Scene.bfu_script_tool_expanded - + del bpy.types.Scene.bfu_active_scene_tab del bpy.types.Scene.bfu_active_object_tab + del bpy.types.Scene.bfu_active_tab diff --git a/blender-for-unrealengine/bfu_skeletal_mesh/__init__.py b/blender-for-unrealengine/bfu_skeletal_mesh/__init__.py index aa244360..f993167a 100644 --- a/blender-for-unrealengine/bfu_skeletal_mesh/__init__.py +++ b/blender-for-unrealengine/bfu_skeletal_mesh/__init__.py @@ -23,6 +23,7 @@ from . import bfu_skeletal_mesh_ui from . import bfu_skeletal_mesh_utils from . import bfu_skeletal_mesh_type +from . import bfu_skeletal_animation_type from . import bfu_skeletal_mesh_config if "bfu_skeletal_mesh_props" in locals(): @@ -33,6 +34,8 @@ importlib.reload(bfu_skeletal_mesh_utils) if "bfu_skeletal_mesh_type" in locals(): importlib.reload(bfu_skeletal_mesh_type) +if "bfu_skeletal_animation_type" in locals(): + importlib.reload(bfu_skeletal_animation_type) if "bfu_skeletal_mesh_config" in locals(): importlib.reload(bfu_skeletal_mesh_config) @@ -47,11 +50,13 @@ def register(): bfu_skeletal_mesh_props.register() bfu_skeletal_mesh_type.register() + bfu_skeletal_animation_type.register() def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) + bfu_skeletal_animation_type.unregister() bfu_skeletal_mesh_type.unregister() bfu_skeletal_mesh_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_animation_type.py b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_animation_type.py new file mode 100644 index 00000000..bd77af96 --- /dev/null +++ b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_animation_type.py @@ -0,0 +1,100 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import os +import bpy +import fnmatch +from . import bfu_skeletal_mesh_config +from .. import bfu_assets_manager +from .. import bfu_utils +from .. import bfu_basics + + +class BFU_SkeletalAnimation(bfu_assets_manager.bfu_asset_manager_type.BFU_BaseAssetClass): + def __init__(self): + super().__init__() + self.use_materials = True + + def support_asset_type(self, obj, details = None): + if obj.type == "ARMATURE" and not obj.bfu_export_skeletal_mesh_as_static_mesh: + if details == "SkeletalAnimation": + return True + return False + + def get_asset_type_name(self, obj): + return bfu_skeletal_mesh_config.animation_asset_type_name + + def get_obj_export_name(self, obj): + if bfu_utils.GetExportAsProxy(obj): + proxy_child = bfu_utils.GetExportProxyChild(obj) + if proxy_child is not None: + return bfu_basics.ValidFilename(proxy_child.name) + return super().get_obj_export_name(obj) + + def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): + # Generate assset file name for skeletal mesh + scene = bpy.context.scene + if obj.bfu_use_custom_export_name: + if obj.bfu_custom_export_name: + return obj.bfu_custom_export_name + if desired_name: + return bfu_basics.ValidFilename(scene.bfu_skeletal_mesh_prefix_export_name+desired_name+fileType) + return bfu_basics.ValidFilename(scene.bfu_skeletal_mesh_prefix_export_name+obj.name+fileType) + + def get_obj_export_directory_path(self, obj, absolute = True): + folder_name = bfu_utils.get_export_folder_name(obj) + scene = bpy.context.scene + if(absolute): + root_path = bpy.path.abspath(scene.bfu_export_skeletal_file_path) + else: + root_path = scene.bfu_export_skeletal_file_path + + if obj.bfu_create_sub_folder_with_skeletal_mesh_name: + dirpath = os.path.join(root_path, folder_name, self.get_obj_export_name(obj), scene.bfu_anim_subfolder_name) + else: + dirpath = os.path.join(root_path, folder_name, scene.bfu_anim_subfolder_name) + return dirpath + + def get_meshs_object_for_skeletal_mesh(self, obj): + meshs = [] + if self.support_asset_type(obj): # Skeleton / Armature + childs = bfu_utils.GetExportDesiredChilds(obj) + for child in childs: + if child.type == "MESH": + meshs.append(child) + return meshs + + def can_export_asset(self): + scene = bpy.context.scene + return scene.bfu_use_skeletal_export + + def can_export_obj_asset(self, obj): + if self.can_export_asset(): + if obj.bfu_skeleton_export_procedure == 'auto-rig-pro': + if bfu_basics.CheckPluginIsActivated('auto_rig_pro-master'): + return True + else: + return True + else: + False + +def register(): + bfu_assets_manager.bfu_asset_manager_registred_assets.register_asset_class(BFU_SkeletalAnimation()) + +def unregister(): + pass \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_config.py b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_config.py index f262672b..4e7f9e20 100644 --- a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_config.py +++ b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_config.py @@ -16,4 +16,5 @@ # # ======================= END GPL LICENSE BLOCK ============================= -asset_type_name = "SkeletalMesh" +mesh_asset_type_name = "SkeletalMesh" +animation_asset_type_name = "SkeletalAnimation" \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_props.py b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_props.py index 97e69aef..12032a1a 100644 --- a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_props.py +++ b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_props.py @@ -27,9 +27,13 @@ def get_preset_values(): preset_values = [ + 'obj.bfu_export_deform_only', 'obj.bfu_export_skeletal_mesh_as_static_mesh', - 'obj.bfu_create_sub_folder_with_skeletal_mesh_name' - ] + 'obj.bfu_create_sub_folder_with_skeletal_mesh_name', + 'obj.bfu_export_animation_without_mesh', + 'obj.bfu_mirror_symmetry_right_side_bones', + 'obj.bfu_use_ue_mannequin_bone_alignment', + ] return preset_values # ------------------------------------------------------------------- @@ -44,6 +48,18 @@ def register(): for cls in classes: bpy.utils.register_class(cls) + bpy.types.Scene.bfu_skeleton_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Skeleton") + + bpy.types.Object.bfu_export_deform_only = bpy.props.BoolProperty( + name="Export only deform bones", + description=( + "Only write deforming bones" + + " (and non-deforming ones when they have deforming children)" + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True + ) + bpy.types.Object.bfu_export_skeletal_mesh_as_static_mesh = bpy.props.BoolProperty( name="Export as Static Mesh", description="If true this mesh will be exported as a Static Mesh", @@ -58,17 +74,45 @@ def register(): default=True ) - bpy.types.Scene.bfu_skeleton_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Skeleton") - bpy.types.Scene.bfu_modular_skeletal_mesh_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Modular Skeletal Mesh") + bpy.types.Object.bfu_export_animation_without_mesh = bpy.props.BoolProperty( + name="Export animation without mesh", + description=( + "If checked, When exporting animation, do not include mesh data in the FBX file." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True + ) + bpy.types.Object.bfu_mirror_symmetry_right_side_bones = bpy.props.BoolProperty( + name="Revert direction of symmetry right side bones", + description=( + "If checked, The right-side bones will be mirrored for mirroring physic object in UE PhysicAsset Editor." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=True + ) + + bpy.types.Object.bfu_use_ue_mannequin_bone_alignment = bpy.props.BoolProperty( + name="Apply bone alignments similar to UE Mannequin.", + description=( + "If checked, similar to the UE Mannequin, the leg bones will be oriented upwards, and the pelvis and feet bone will be aligned facing upwards during export." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False + ) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls) - del bpy.types.Scene.bfu_modular_skeletal_mesh_properties_expanded - del bpy.types.Scene.bfu_skeleton_properties_expanded + del bpy.types.Object.bfu_use_ue_mannequin_bone_alignment + del bpy.types.Object.bfu_mirror_symmetry_right_side_bones + del bpy.types.Object.bfu_export_animation_without_mesh del bpy.types.Object.bfu_create_sub_folder_with_skeletal_mesh_name - del bpy.types.Object.bfu_export_skeletal_mesh_as_static_mesh \ No newline at end of file + del bpy.types.Object.bfu_export_skeletal_mesh_as_static_mesh + + del bpy.types.Object.bfu_export_deform_only + + del bpy.types.Scene.bfu_skeleton_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_type.py b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_type.py index b692c731..9c5e6a45 100644 --- a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_type.py +++ b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_type.py @@ -30,13 +30,14 @@ def __init__(self): super().__init__() self.use_materials = True - def support_asset_type(self, obj): + def support_asset_type(self, obj, details = None): if obj.type == "ARMATURE" and not obj.bfu_export_skeletal_mesh_as_static_mesh: - return True + if details == None: + return True return False def get_asset_type_name(self, obj): - return bfu_skeletal_mesh_config.asset_type_name + return bfu_skeletal_mesh_config.mesh_asset_type_name def get_obj_export_name(self, obj): if bfu_utils.GetExportAsProxy(obj): @@ -55,13 +56,18 @@ def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): return bfu_basics.ValidFilename(scene.bfu_skeletal_mesh_prefix_export_name+desired_name+fileType) return bfu_basics.ValidFilename(scene.bfu_skeletal_mesh_prefix_export_name+obj.name+fileType) - def get_obj_export_directory_path(self, obj): + def get_obj_export_directory_path(self, obj, absolute = True): folder_name = bfu_utils.get_export_folder_name(obj) scene = bpy.context.scene + if(absolute): + root_path = bpy.path.abspath(scene.bfu_export_skeletal_file_path) + else: + root_path = scene.bfu_export_skeletal_file_path + if obj.bfu_create_sub_folder_with_skeletal_mesh_name: - dirpath = os.path.join(scene.bfu_export_skeletal_file_path, folder_name, self.get_obj_export_name(obj)) + dirpath = os.path.join(root_path, folder_name, self.get_obj_export_name(obj)) else: - dirpath = os.path.join(scene.bfu_export_skeletal_file_path, folder_name) + dirpath = os.path.join(root_path, folder_name) return dirpath def get_meshs_object_for_skeletal_mesh(self, obj): @@ -75,7 +81,7 @@ def get_meshs_object_for_skeletal_mesh(self, obj): def can_export_asset(self): scene = bpy.context.scene - return scene.skeletal_export + return scene.bfu_use_skeletal_export def can_export_obj_asset(self, obj): if self.can_export_asset(): diff --git a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_ui.py b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_ui.py index 6baa08b9..8102f39a 100644 --- a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_ui.py +++ b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_ui.py @@ -44,55 +44,43 @@ def draw_general_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): AssetType2.prop(obj, "bfu_export_skeletal_mesh_as_static_mesh") def draw_ui_object(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + addon_prefs = bfu_basics.GetAddonPrefs() + is_skeletal_mesh = bfu_skeletal_mesh_utils.is_skeletal_mesh(obj) + if obj is None: return - if obj.type != "ARMATURE": return + if is_skeletal_mesh is False: + return + if obj.bfu_export_type != "export_recursive": + return + if obj.bfu_export_as_lod_mesh: + return - scene = bpy.context.scene - addon_prefs = bfu_basics.GetAddonPrefs() - if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): - if scene.bfu_object_properties_expanded.is_expend(): - - if bfu_skeletal_mesh_utils.is_skeletal_mesh(obj): - scene.bfu_skeleton_properties_expanded.draw(layout) - if scene.bfu_skeleton_properties_expanded.is_expend(): - if addon_prefs.useGeneratedScripts and obj is not None: - if obj.bfu_export_type == "export_recursive": - - # SkeletalMesh prop - if not obj.bfu_export_as_lod_mesh: - AssetType2 = layout.column() - - export_procedure_prop = AssetType2.column() - export_procedure_prop.prop(obj, 'bfu_skeleton_export_procedure') + scene.bfu_skeleton_properties_expanded.draw(layout) + if scene.bfu_skeleton_properties_expanded.is_expend(): - AssetType2.prop(obj, 'bfu_create_sub_folder_with_skeletal_mesh_name') - AssetType2.prop(obj, 'bfu_export_deform_only') - ue_standard_skeleton = layout.column() - ue_standard_skeleton.label(text="(ue-standard)") - ue_standard_skeleton_props = ue_standard_skeleton.column() - ue_standard_skeleton_props.enabled = obj.bfu_skeleton_export_procedure == "ue-standard" - ue_standard_skeleton_props.prop(obj, "bfu_export_animation_without_mesh") - ue_standard_skeleton_props.prop(obj, "bfu_mirror_symmetry_right_side_bones") - mirror_symmetry_right_side_bones = ue_standard_skeleton_props.row() - mirror_symmetry_right_side_bones.enabled = obj.bfu_mirror_symmetry_right_side_bones - mirror_symmetry_right_side_bones.prop(obj, "bfu_use_ue_mannequin_bone_alignment") + # SkeletalMesh prop + AssetType2 = layout.column() - scene.bfu_modular_skeletal_mesh_properties_expanded.draw(layout) - if scene.bfu_modular_skeletal_mesh_properties_expanded.is_expend(): - if obj.bfu_export_type == "export_recursive": + export_procedure_prop = AssetType2.column() + export_procedure_prop.prop(obj, 'bfu_skeleton_export_procedure') - # SkeletalMesh prop - if not obj.bfu_export_as_lod_mesh: - modular_skeletal_mesh = layout.column() - modular_skeletal_mesh.prop(obj, "bfu_modular_skeletal_mesh_mode") - if obj.bfu_modular_skeletal_mesh_mode == "every_meshs": - modular_skeletal_mesh.prop(obj, "bfu_modular_skeletal_mesh_every_meshs_separate") - if obj.bfu_modular_skeletal_mesh_mode == "specified_parts": - obj.bfu_modular_skeletal_specified_parts_meshs_template.draw(modular_skeletal_mesh) + AssetType2.prop(obj, 'bfu_create_sub_folder_with_skeletal_mesh_name') + AssetType2.prop(obj, 'bfu_export_deform_only') + ue_standard_skeleton = layout.column() + ue_standard_skeleton.label(text="(ue-standard)") + ue_standard_skeleton_props = ue_standard_skeleton.column() + ue_standard_skeleton_props.enabled = obj.bfu_skeleton_export_procedure == "ue-standard" + ue_standard_skeleton_props.prop(obj, "bfu_export_animation_without_mesh") + ue_standard_skeleton_props.prop(obj, "bfu_mirror_symmetry_right_side_bones") + mirror_symmetry_right_side_bones = ue_standard_skeleton_props.row() + mirror_symmetry_right_side_bones.enabled = obj.bfu_mirror_symmetry_right_side_bones + mirror_symmetry_right_side_bones.prop(obj, "bfu_use_ue_mannequin_bone_alignment") def draw_ui_scene(layout: bpy.types.UILayout): pass \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_utils.py b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_utils.py index fb66e2ff..fad88326 100644 --- a/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_utils.py +++ b/blender-for-unrealengine/bfu_skeletal_mesh/bfu_skeletal_mesh_utils.py @@ -43,7 +43,7 @@ def deselect_socket(obj): def is_skeletal_mesh(obj): asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(obj) if asset_class: - if asset_class.get_asset_type_name(obj) == bfu_skeletal_mesh_config.asset_type_name: + if asset_class.get_asset_type_name(obj) == bfu_skeletal_mesh_config.mesh_asset_type_name: return True return False diff --git a/blender-for-unrealengine/bfu_socket/bfu_socket_ui_and_props.py b/blender-for-unrealengine/bfu_socket/bfu_socket_ui_and_props.py index b69e8654..2ba1d94c 100644 --- a/blender-for-unrealengine/bfu_socket/bfu_socket_ui_and_props.py +++ b/blender-for-unrealengine/bfu_socket/bfu_socket_ui_and_props.py @@ -69,10 +69,11 @@ def execute(self, context): "Skeletal sockets copied. Paste in Unreal Engine Skeletal Mesh assets for import sockets. (Ctrl+V)") return {'FINISHED'} -def draw_ui_scene_socket(layout: bpy.types.UILayout): - scene = bpy.context.scene - scene.bfu_socket_expanded.draw(layout) - if scene.bfu_socket_expanded.is_expend(): +def draw_tools_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene + + scene.bfu_tools_socket_properties_expanded.draw(layout) + if scene.bfu_tools_socket_properties_expanded.is_expend(): addon_prefs = bfu_basics.GetAddonPrefs() # Draw user tips and check can use buttons @@ -162,6 +163,8 @@ def register(): for cls in classes: bpy.utils.register_class(cls) + bpy.types.Scene.bfu_tools_socket_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="Socket") + bpy.types.Object.bfu_use_socket_custom_Name = bpy.props.BoolProperty( name="Socket custom name", description='Use a custom name in Unreal Engine for this socket?', @@ -181,3 +184,5 @@ def unregister(): del bpy.types.Object.bfu_socket_custom_Name del bpy.types.Object.bfu_use_socket_custom_Name + + del bpy.types.Scene.bfu_tools_socket_properties_expanded \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_spline/bfu_spline_data.py b/blender-for-unrealengine/bfu_spline/bfu_spline_data.py index 45066a68..2fe93ce6 100644 --- a/blender-for-unrealengine/bfu_spline/bfu_spline_data.py +++ b/blender-for-unrealengine/bfu_spline/bfu_spline_data.py @@ -4,7 +4,7 @@ from typing import Dict, Any from . import bfu_spline_utils from . import bfu_spline_unreal_utils -from .. import bps +from .. import bpl from .. import bbpl from .. import languages from .. import bfu_basics @@ -204,7 +204,7 @@ def evaluate_spline_data(self, spline_obj: bpy.types.Object, spline_data: bpy.ty addon_prefs = bfu_basics.GetAddonPrefs() #print(f"Start evaluate spline_data index {str(index)}") - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() if spline_data.type in ["POLY"]: for point in spline_data.points: @@ -274,7 +274,7 @@ def evaluate_spline_obj_data(self, spline_obj: bpy.types.Object): addon_prefs = bfu_basics.GetAddonPrefs() #print(f"Start evaluate spline {spline_obj.name}") - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() for x, spline_data in enumerate(spline_obj.data.splines): simple_spline = self.simple_splines[x] = BFU_SimpleSpline(spline_data) @@ -301,7 +301,7 @@ def evaluate_all_splines(self): scene = bpy.context.scene addon_prefs = bfu_basics.GetAddonPrefs() - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() slms = bfu_utils.TimelineMarkerSequence() diff --git a/blender-for-unrealengine/bfu_spline/bfu_spline_type.py b/blender-for-unrealengine/bfu_spline/bfu_spline_type.py index ceb5fa87..0df76a78 100644 --- a/blender-for-unrealengine/bfu_spline/bfu_spline_type.py +++ b/blender-for-unrealengine/bfu_spline/bfu_spline_type.py @@ -30,7 +30,7 @@ def __init__(self): super().__init__() pass - def support_asset_type(self, obj): + def support_asset_type(self, obj, details = None): if obj.type == "CURVE" and not obj.bfu_export_spline_as_static_mesh: return True return False @@ -48,15 +48,20 @@ def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): return bfu_basics.ValidFilename(scene.bfu_spline_prefix_export_name+desired_name+fileType) return bfu_basics.ValidFilename(scene.bfu_spline_prefix_export_name+obj.name+fileType) - def get_obj_export_directory_path(self, obj): + def get_obj_export_directory_path(self, obj, absolute = True): folder_name = bfu_utils.get_export_folder_name(obj) scene = bpy.context.scene - dirpath = os.path.join(scene.bfu_export_spline_file_path, folder_name) + if(absolute): + root_path = bpy.path.abspath(scene.bfu_export_spline_file_path) + else: + root_path = scene.bfu_export_spline_file_path + + dirpath = os.path.join(root_path, folder_name) return dirpath def can_export_asset(self): scene = bpy.context.scene - return scene.spline_export + return scene.bfu_use_spline_export def can_export_obj_asset(self, obj): return self.can_export_asset() diff --git a/blender-for-unrealengine/bfu_spline/bfu_spline_ui_and_props.py b/blender-for-unrealengine/bfu_spline/bfu_spline_ui_and_props.py index 79bf68c0..f3bb45b2 100644 --- a/blender-for-unrealengine/bfu_spline/bfu_spline_ui_and_props.py +++ b/blender-for-unrealengine/bfu_spline/bfu_spline_ui_and_props.py @@ -22,6 +22,7 @@ from . import bfu_spline_utils from . import bfu_spline_write_paste_commands from .. import bfu_basics +from .. import bfu_ui from .. import bbpl from .. import languages from ..bbpl.blender_layout import layout_doc_button @@ -46,31 +47,32 @@ def draw_ui_object_spline(layout: bpy.types.UILayout, obj: bpy.types.Object): spline_ui = layout.column() scene = bpy.context.scene - scene.bfu_spline_properties_expanded.draw(spline_ui) - if scene.bfu_spline_properties_expanded.is_expend(): - if obj.type == "CURVE": - spline_ui_pop = spline_ui.column() - spline_ui_as_static_mesh = spline_ui_pop.column() - spline_ui_as_static_mesh.prop(obj, 'bfu_export_spline_as_static_mesh') - spline_ui_as_static_mesh.prop(obj, 'bfu_export_fbx_spline') - spline_ui_as_static_mesh.enabled = obj.bfu_export_type == "export_recursive" - - spline_ui_spline_type = spline_ui_pop.column() - spline_ui_spline_type.prop(obj, 'bfu_desired_spline_type') - if obj.bfu_desired_spline_type == "CUSTOM": - spline_ui_spline_type.prop(obj, 'bfu_custom_spline_component') - if bfu_spline_utils.contain_nurbs_spline(obj): - resample_resolution = spline_ui_spline_type.row() - resample_resolution.prop(obj, 'bfu_spline_resample_resolution') - layout_doc_button.add_doc_page_operator(resample_resolution, text="", url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Curve-and-Spline#notes") - spline_ui_spline_type.enabled = obj.bfu_export_spline_as_static_mesh is False - spline_ui.operator("object.bfu_copy_active_spline_data", icon="COPYDOWN") - - -def draw_ui_scene_spline(layout: bpy.types.UILayout): + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "GENERAL"): + scene.bfu_spline_properties_expanded.draw(spline_ui) + if scene.bfu_spline_properties_expanded.is_expend(): + if obj.type == "CURVE": + spline_ui_pop = spline_ui.column() + spline_ui_as_static_mesh = spline_ui_pop.column() + spline_ui_as_static_mesh.prop(obj, 'bfu_export_spline_as_static_mesh') + spline_ui_as_static_mesh.prop(obj, 'bfu_export_fbx_spline') + spline_ui_as_static_mesh.enabled = obj.bfu_export_type == "export_recursive" + + spline_ui_spline_type = spline_ui_pop.column() + spline_ui_spline_type.prop(obj, 'bfu_desired_spline_type') + if obj.bfu_desired_spline_type == "CUSTOM": + spline_ui_spline_type.prop(obj, 'bfu_custom_spline_component') + if bfu_spline_utils.contain_nurbs_spline(obj): + resample_resolution = spline_ui_spline_type.row() + resample_resolution.prop(obj, 'bfu_spline_resample_resolution') + layout_doc_button.add_doc_page_operator(resample_resolution, text="", url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/Curve-and-Spline#notes") + spline_ui_spline_type.enabled = obj.bfu_export_spline_as_static_mesh is False + spline_ui.operator("object.bfu_copy_active_spline_data", icon="COPYDOWN") + + +def draw_tools_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene spline_ui = layout.column() - scene = bpy.context.scene scene.bfu_spline_tools_expanded.draw(spline_ui) if scene.bfu_spline_tools_expanded.is_expend(): spline_ui.operator("object.copy_selected_splines_data", icon="COPYDOWN") diff --git a/blender-for-unrealengine/bfu_spline/bfu_spline_write_paste_commands.py b/blender-for-unrealengine/bfu_spline/bfu_spline_write_paste_commands.py index 8cc71bef..82048f08 100644 --- a/blender-for-unrealengine/bfu_spline/bfu_spline_write_paste_commands.py +++ b/blender-for-unrealengine/bfu_spline/bfu_spline_write_paste_commands.py @@ -46,7 +46,7 @@ def AddSplineToCommand(spline: bpy.types.Object, pre_bake_spline: bfu_spline_dat def GetImportSplineScriptCommand(objs): # Return (success, command) scene = bpy.context.scene - save_select = bbpl.utils.UserSelectSave() + save_select = bbpl.save_data.select_save.UserSelectSave() save_select.save_current_select() success = False @@ -79,5 +79,5 @@ def GetImportSplineScriptCommand(objs): success = True command = t report = str(add_spline_num) + " Spline(s) copied. Paste in Unreal Engine scene for import the spline. (Use CTRL+V in Unreal viewport)" - save_select.reset_select_by_name() + save_select.reset_select(use_names = True) return (success, command, report) \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_static_mesh/bfu_static_mesh_type.py b/blender-for-unrealengine/bfu_static_mesh/bfu_static_mesh_type.py index 6424a8a6..b58b06e4 100644 --- a/blender-for-unrealengine/bfu_static_mesh/bfu_static_mesh_type.py +++ b/blender-for-unrealengine/bfu_static_mesh/bfu_static_mesh_type.py @@ -32,7 +32,7 @@ def __init__(self): self.use_lods = True self.use_materials = True - def support_asset_type(self, obj): + def support_asset_type(self, obj, details = None): if obj.bfu_export_skeletal_mesh_as_static_mesh: return True elif obj.bfu_export_as_groom_simulation: @@ -58,15 +58,20 @@ def get_obj_file_name(self, obj, desired_name="", fileType=".fbx"): return bfu_basics.ValidFilename(scene.bfu_static_mesh_prefix_export_name+desired_name+fileType) return bfu_basics.ValidFilename(scene.bfu_static_mesh_prefix_export_name+obj.name+fileType) - def get_obj_export_directory_path(self, obj): + def get_obj_export_directory_path(self, obj, absolute = True): + folder_name = bfu_utils.get_export_folder_name(obj) scene = bpy.context.scene - dirpath = os.path.join(scene.bfu_export_static_file_path, folder_name) + if(absolute): + root_path = bpy.path.abspath(scene.bfu_export_static_file_path) + else: + root_path = scene.bfu_export_static_file_path + dirpath = os.path.join(root_path, folder_name) return dirpath def can_export_asset(self): scene = bpy.context.scene - return scene.static_export + return scene.bfu_use_static_export def can_export_obj_asset(self, obj): return self.can_export_asset() diff --git a/blender-for-unrealengine/bfu_unreal_utils.py b/blender-for-unrealengine/bfu_unreal_utils.py index 5f23eca6..5351d8b4 100644 --- a/blender-for-unrealengine/bfu_unreal_utils.py +++ b/blender-for-unrealengine/bfu_unreal_utils.py @@ -18,46 +18,43 @@ import os import bpy -from . import bbpl -from . import bps -from . import bfu_basics from . import bfu_utils -def GetPredictedSkeletonName(obj): +def get_predicted_skeleton_name(obj): # Get the predicted skeleton name in Unreal Engine scene = bpy.context.scene return scene.bfu_skeleton_prefix_export_name + bfu_utils.ValidUnrealAssetsName(obj.name) + "_Skeleton" -def GetPredictedSkeletonPath(obj): +def get_predicted_skeleton_path(obj): scene = bpy.context.scene skeleton_path = os.path.join("/" + scene.bfu_unreal_import_module + "/", scene.bfu_unreal_import_location, obj.bfu_export_folder_name) skeleton_path = skeleton_path.replace('\\', '/') return skeleton_path -def GetPredictedSkeletonRef(obj): - name = GetPredictedSkeletonName(obj) - path = GetPredictedSkeletonPath(obj) - skeleton_ref = os.path.join(path, name + "." + name) +def get_predicted_skeleton_ref(obj): + name = get_predicted_skeleton_name(obj) + path = get_predicted_skeleton_path(obj) + skeleton_ref = os.path.join(path, f"{name}.{name}") skeleton_ref = skeleton_ref.replace('\\', '/') - return "/Script/Engine.Skeleton'" + skeleton_ref + "'" + return f"/Script/Engine.Skeleton'{skeleton_ref}'" -def GetPredictedSkeletalMeshName(obj): +def get_predicted_skeleton_name(obj): # Get the predicted SkeletalMesh name in Unreal Engine scene = bpy.context.scene return scene.bfu_skeletal_mesh_prefix_export_name + bfu_utils.ValidUnrealAssetsName(obj.name) -def GetPredictedSkeletalMeshPath(obj): +def get_predicted_skeleton_path(obj): scene = bpy.context.scene skeleton_path = os.path.join("/" + scene.bfu_unreal_import_module + "/", scene.bfu_unreal_import_location, obj.bfu_export_folder_name) skeleton_path = skeleton_path.replace('\\', '/') return skeleton_path -def GetPredictedSkeletalMeshRef(obj): - name = GetPredictedSkeletalMeshName(obj) - path = GetPredictedSkeletalMeshPath(obj) - skeletal_mesh_ref = os.path.join(path, name + "." + name) - skeletal_mesh_ref = skeletal_mesh_ref.replace('\\', '/') - return "/Script/Engine.SkeletalMesh'" + skeletal_mesh_ref + "'" +def get_predicted_skeleton_ref(obj): + name = get_predicted_skeleton_name(obj) + path = get_predicted_skeleton_path(obj) + skeleton_ref = os.path.join(path, f"{name}.{name}") + skeleton_ref = skeleton_ref.replace('\\', '/') + return f"/Script/Engine.Skeleton'{skeleton_ref}'" def generate_name_for_unreal_engine(desired_name, current_name = ""): # Generate a new name with suffix number diff --git a/blender-for-unrealengine/bfu_utils.py b/blender-for-unrealengine/bfu_utils.py index 1ab55c1c..06996bdb 100644 --- a/blender-for-unrealengine/bfu_utils.py +++ b/blender-for-unrealengine/bfu_utils.py @@ -24,13 +24,10 @@ import mathutils import math import os -import math -import addon_utils from typing import List from . import bbpl -from . import bps from . import bfu_basics -from . import bfu_assets_manager + class SavedBones(): @@ -660,7 +657,7 @@ def SelectParentAndSpecificChilds(active, objects): def RemoveSocketFromSelectForProxyArmature(): - select = bbpl.utils.UserSelectSave() + select = bbpl.save_data.select_save.UserSelectSave() select.save_current_select() # With skeletal mesh the socket must be not exported, # ue4 read it like a bone @@ -669,7 +666,7 @@ def RemoveSocketFromSelectForProxyArmature(): if fnmatch.fnmatchcase(obj.name, "SOCKET*"): sockets.append(obj) CleanDeleteObjects(sockets) - select.reset_select_by_name() + select.reset_select(use_names = True) def GoToMeshEditMode(): @@ -683,14 +680,14 @@ def GoToMeshEditMode(): def ApplyNeededModifierToSelect(): - SavedSelect = bbpl.utils.UserSelectSave() + SavedSelect = bbpl.save_data.select_save.UserSelectSave() SavedSelect.save_current_select() # Get selected objects with modifiers. for obj in bpy.context.selected_objects: ApplyObjectModifiers(obj, ['ARMATURE']) - SavedSelect.reset_select_by_ref() + SavedSelect.reset_select() def ApplyObjectModifiers(obj: bpy.types.Object, blacklist_type = []): @@ -725,8 +722,8 @@ def ApplyObjectModifiers(obj: bpy.types.Object, blacklist_type = []): -def CorrectExtremeUV(stepScale=2): - +def CorrectExtremeUV(step_scale=2, move_to_absolute=False): + def GetHaveConnectedLoop(faceTarget): # In bmesh faces for loop in faceTarget.loops: @@ -779,16 +776,24 @@ def GetAllIsland(bm, uv_lay): return Islands - def MoveItlandToCenter(faces, uv_lay, minDistance): + def MoveItlandToCenter(faces, uv_lay, min_distance, absolute): loop = faces[-1].loops[-1] - x = round(loop[uv_lay].uv[0]/minDistance, 0)*minDistance - y = round(loop[uv_lay].uv[1]/minDistance, 0)*minDistance + delta_x = round(loop[uv_lay].uv[0]/min_distance, 0)*min_distance + delta_y = round(loop[uv_lay].uv[1]/min_distance, 0)*min_distance for face in faces: for loop in face.loops: - loop[uv_lay].uv[0] -= x - loop[uv_lay].uv[1] -= y + loop[uv_lay].uv[0] -= delta_x + loop[uv_lay].uv[1] -= delta_y + + if(absolute == True): + # Move Faces to make it alway positive + for face in faces: + for loop in face.loops: + loop[uv_lay].uv[0] = abs(loop[uv_lay].uv[0]) + loop[uv_lay].uv[1] = abs(loop[uv_lay].uv[1]) + def IsValidForUvEdit(obj): if obj.type == "MESH": @@ -806,11 +811,10 @@ def IsValidForUvEdit(obj): for faces in GetAllIsland(bm, uv_lay): uv_lay = bm.loops.layers.uv.active - MoveItlandToCenter(faces, uv_lay, stepScale) + MoveItlandToCenter(faces, uv_lay, step_scale, move_to_absolute) obj.data.update() - def ApplyExportTransform(obj, use_type="Object"): newMatrix = obj.matrix_world @ mathutils.Matrix.Translation((0, 0, 0)) @@ -912,7 +916,7 @@ def ApplySkeletalExportScale(self, rescale, target_animation_data=None, is_a_pro armature_animation_data.clear_animation_data(armature) if is_a_proxy: - SavedSelect = bbpl.utils.UserSelectSave() + SavedSelect = bbpl.save_data.select_save.UserSelectSave() SavedSelect.save_current_select() bpy.ops.object.select_all(action='DESELECT') armature.select_set(True) @@ -931,7 +935,7 @@ def ApplySkeletalExportScale(self, rescale, target_animation_data=None, is_a_pro properties=True ) if is_a_proxy: - SavedSelect.reset_select_by_ref() + SavedSelect.reset_select() # Apply armature location armature.location = old_location*rescale @@ -1096,27 +1100,27 @@ def GetAnimSample(obj): return obj.bfu_sample_anim_for_export -def GetArmatureRootBones(obj): - rootBones = [] - if obj.type == "ARMATURE": - - if not obj.bfu_export_deform_only: - for bone in obj.data.bones: - if bone.parent is None: - rootBones.append(bone) +def get_armature_root_bones(armature: bpy.types.Object) -> List[bpy.types.EditBone]: + root_bones = [] + if armature.type == "ARMATURE": - if obj.bfu_export_deform_only: - for bone in obj.data.bones: + if armature.bfu_export_deform_only: + for bone in armature.data.bones: if bone.use_deform: rootBone = bfu_basics.getRootBoneParent(bone) - if rootBone not in rootBones: - rootBones.append(rootBone) - return rootBones + if rootBone not in root_bones: + root_bones.append(rootBone) + + else: + for bone in armature.data.bones: + if bone.parent is None: + root_bones.append(bone) + return root_bones def GetDesiredExportArmatureName(obj): addon_prefs = bfu_basics.GetAddonPrefs() - single_root = len(GetArmatureRootBones(obj)) == 1 + single_root = len(get_armature_root_bones(obj)) == 1 if addon_prefs.add_skeleton_root_bone or single_root != 1: return addon_prefs.skeleton_root_bone_name return "Armature" diff --git a/blender-for-unrealengine/bfu_uv_map/__init__.py b/blender-for-unrealengine/bfu_uv_map/__init__.py new file mode 100644 index 00000000..18cbb4bb --- /dev/null +++ b/blender-for-unrealengine/bfu_uv_map/__init__.py @@ -0,0 +1,47 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +import importlib + +from . import bfu_uv_map_props +from . import bfu_uv_map_ui +from . import bfu_uv_map_utils + +if "bfu_uv_map_props" in locals(): + importlib.reload(bfu_uv_map_props) +if "bfu_uv_map_ui" in locals(): + importlib.reload(bfu_uv_map_ui) +if "bfu_uv_map_utils" in locals(): + importlib.reload(bfu_uv_map_utils) + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bfu_uv_map_props.register() + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + bfu_uv_map_props.unregister() \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_props.py b/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_props.py new file mode 100644 index 00000000..96ababd6 --- /dev/null +++ b/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_props.py @@ -0,0 +1,107 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl +from .. import languages + + + + +def get_preset_values(): + preset_values = [ + 'obj.bfu_convert_geometry_node_attribute_to_uv', + 'obj.bfu_convert_geometry_node_attribute_to_uv_name', + 'obj.bfu_use_correct_extrem_uv_scale', + 'obj.bfu_correct_extrem_uv_scale_step_scale', + 'obj.bfu_correct_extrem_uv_scale_use_absolute', + ] + return preset_values + +# ------------------------------------------------------------------- +# Register & Unregister +# ------------------------------------------------------------------- + +classes = ( +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + bpy.types.Scene.bfu_object_uv_map_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="UV map") + bpy.types.Scene.bfu_tools_uv_map_properties_expanded = bbpl.blender_layout.layout_accordion.add_ui_accordion(name="UV Map") + + bpy.types.Object.bfu_convert_geometry_node_attribute_to_uv = bpy.props.BoolProperty( + name="Convert Attribute To Uv", + description=( + "convert target geometry node attribute to UV when found." + ), + override={'LIBRARY_OVERRIDABLE'}, + default=False, + ) + + bpy.types.Object.bfu_convert_geometry_node_attribute_to_uv_name = bpy.props.StringProperty( + name="Attribute name", + description=( + "Name of the Attribute to convert" + ), + override={'LIBRARY_OVERRIDABLE'}, + default="UVMap", + ) + + bpy.types.Object.bfu_use_correct_extrem_uv_scale = bpy.props.BoolProperty( + name=(languages.ti('correct_use_extrem_uv_scale_name')), + description=(languages.tt('correct_use_extrem_uv_scale_desc')), + override={'LIBRARY_OVERRIDABLE'}, + default=False, + ) + + bpy.types.Object.bfu_correct_extrem_uv_scale_step_scale = bpy.props.IntProperty( + name=(languages.ti('correct_extrem_uv_scale_step_scale_name')), + description=(languages.tt('correct_extrem_uv_scale_step_scale_desc')), + override={'LIBRARY_OVERRIDABLE'}, + default=2, + min=1, + max=100, + ) + + bpy.types.Object.bfu_correct_extrem_uv_scale_use_absolute = bpy.props.BoolProperty( + name=(languages.ti('correct_extrem_uv_scale_use_absolute_name')), + description=(languages.tt('correct_extrem_uv_scale_use_absolute_desc')), + override={'LIBRARY_OVERRIDABLE'}, + default=False, + ) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Object.bfu_correct_extrem_uv_scale_use_absolute + del bpy.types.Object.bfu_correct_extrem_uv_scale_step_scale + del bpy.types.Object.bfu_use_correct_extrem_uv_scale + del bpy.types.Object.bfu_convert_geometry_node_attribute_to_uv_name + del bpy.types.Object.bfu_convert_geometry_node_attribute_to_uv + + del bpy.types.Scene.bfu_tools_uv_map_properties_expanded + del bpy.types.Scene.bfu_object_uv_map_properties_expanded diff --git a/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_ui.py b/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_ui.py new file mode 100644 index 00000000..59ecc754 --- /dev/null +++ b/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_ui.py @@ -0,0 +1,82 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + + +import bpy +from .. import bfu_basics +from .. import bfu_utils +from .. import bfu_ui +from .. import bbpl + + +def draw_obj_ui(layout: bpy.types.UILayout, obj: bpy.types.Object): + + scene = bpy.context.scene + + # Hide filters + if obj is None: + return + if bfu_utils.GetExportAsProxy(obj): + return + if obj.bfu_export_type != "export_recursive": + return + + if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "MISC"): + scene.bfu_object_uv_map_properties_expanded.draw(layout) + if scene.bfu_object_uv_map_properties_expanded.is_expend(): + # Geometry Node Uv + bfu_convert_geometry_node_attribute_to_uv = layout.column() + convert_geometry_node_attribute_to_uv_use = bfu_convert_geometry_node_attribute_to_uv.row() + convert_geometry_node_attribute_to_uv_use.prop(obj, 'bfu_convert_geometry_node_attribute_to_uv') + bbpl.blender_layout.layout_doc_button.add_doc_page_operator(convert_geometry_node_attribute_to_uv_use, url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/UV-Maps#geometry-node-uv") + bfu_convert_geometry_node_attribute_to_uv_name = bfu_convert_geometry_node_attribute_to_uv.column() + bfu_convert_geometry_node_attribute_to_uv_name.prop(obj, 'bfu_convert_geometry_node_attribute_to_uv_name') + bfu_convert_geometry_node_attribute_to_uv_name.enabled = obj.bfu_convert_geometry_node_attribute_to_uv + + # Extreme UV Scale + ui_correct_extrem_uv_scale = layout.column() + ui_correct_extrem_uv_scale_use = ui_correct_extrem_uv_scale.row() + ui_correct_extrem_uv_scale_use.prop(obj, 'bfu_use_correct_extrem_uv_scale') + bbpl.blender_layout.layout_doc_button.add_doc_page_operator(ui_correct_extrem_uv_scale_use, url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/UV-Maps#extreme-uv-scale") + ui_correct_extrem_uv_scale_options = ui_correct_extrem_uv_scale.column() + ui_correct_extrem_uv_scale_options.prop(obj, 'bfu_correct_extrem_uv_scale_step_scale') + ui_correct_extrem_uv_scale_options.prop(obj, 'bfu_correct_extrem_uv_scale_use_absolute') + ui_correct_extrem_uv_scale_options.enabled = obj.bfu_use_correct_extrem_uv_scale + + +def draw_tools_ui(layout: bpy.types.UILayout, context: bpy.types.Context): + scene = context.scene + scene.bfu_tools_uv_map_properties_expanded.draw(layout) + if scene.bfu_tools_uv_map_properties_expanded.is_expend(): + ready_for_correct_extrem_uv_scale = False + obj = bpy.context.object + if obj and obj.type == "MESH": + if bbpl.utils.active_mode_is("EDIT"): + ready_for_correct_extrem_uv_scale = True + else: + layout.label(text="Switch to Edit Mode.", icon='INFO') + else: + layout.label(text="Select an mesh object", icon='INFO') + + + # Draw buttons (correct_extrem_uv) + Buttons_correct_extrem_uv_scale = layout.row() + Button_correct_extrem_uv_scale = Buttons_correct_extrem_uv_scale.column() + Button_correct_extrem_uv_scale.enabled = ready_for_correct_extrem_uv_scale + Button_correct_extrem_uv_scale.operator("object.correct_extrem_uv", icon='UV') + bbpl.blender_layout.layout_doc_button.add_doc_page_operator(Buttons_correct_extrem_uv_scale, url="https://github.com/xavier150/Blender-For-UnrealEngine-Addons/wiki/UV-Maps#extreme-uv-scale") diff --git a/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_utils.py b/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_utils.py new file mode 100644 index 00000000..1f5fdd31 --- /dev/null +++ b/blender-for-unrealengine/bfu_uv_map/bfu_uv_map_utils.py @@ -0,0 +1,21 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +import bpy +from .. import bbpl + diff --git a/blender-for-unrealengine/bfu_vertex_color/bfu_vertex_color_ui.py b/blender-for-unrealengine/bfu_vertex_color/bfu_vertex_color_ui.py index 5f1de0ab..e0e04a4a 100644 --- a/blender-for-unrealengine/bfu_vertex_color/bfu_vertex_color_ui.py +++ b/blender-for-unrealengine/bfu_vertex_color/bfu_vertex_color_ui.py @@ -26,7 +26,7 @@ -def draw_ui_object_collision(layout: bpy.types.UILayout): +def draw_ui_object(layout: bpy.types.UILayout): if bfu_ui.bfu_ui_utils.DisplayPropertyFilter("OBJECT", "MISC"): scene = bpy.context.scene diff --git a/blender-for-unrealengine/bfu_write_import_asset_script.py b/blender-for-unrealengine/bfu_write_import_asset_script.py index 4b0f1b73..41e9119f 100644 --- a/blender-for-unrealengine/bfu_write_import_asset_script.py +++ b/blender-for-unrealengine/bfu_write_import_asset_script.py @@ -91,7 +91,7 @@ def WriteImportAssetScript(): # Skeleton if(asset.object.bfu_engine_ref_skeleton_search_mode) == "auto": - asset_data["target_skeleton_ref"] = bfu_unreal_utils.GetPredictedSkeletonRef(asset.object) + asset_data["target_skeleton_ref"] = bfu_unreal_utils.get_predicted_skeleton_ref(asset.object) elif(asset.object.bfu_engine_ref_skeleton_search_mode) == "custom_name": name = bfu_utils.ValidUnrealAssetsName(asset.object.bfu_engine_ref_skeleton_custom_name) @@ -111,7 +111,7 @@ def WriteImportAssetScript(): # Skeletal Mesh if(asset.object.bfu_engine_ref_skeletal_mesh_search_mode) == "auto": - asset_data["target_skeletal_mesh_ref"] = bfu_unreal_utils.GetPredictedSkeletalMeshRef(asset.object) + asset_data["target_skeletal_mesh_ref"] = bfu_unreal_utils.get_predicted_skeleton_ref(asset.object) elif(asset.object.bfu_engine_ref_skeletal_mesh_search_mode) == "custom_name": name = bfu_utils.ValidUnrealAssetsName(asset.object.bfu_engine_ref_skeletal_mesh_custom_name) diff --git a/blender-for-unrealengine/bfu_write_text.py b/blender-for-unrealengine/bfu_write_text.py index 3ff4f5c7..717e6e28 100644 --- a/blender-for-unrealengine/bfu_write_text.py +++ b/blender-for-unrealengine/bfu_write_text.py @@ -22,7 +22,7 @@ import bpy import math -from . import bps +from . import bpl from . import bbpl from . import languages from . import bfu_basics @@ -42,7 +42,7 @@ def ExportSingleText(text, dirpath, filename): # Export single text - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() absdirpath = bpy.path.abspath(dirpath) bfu_basics.VerifiDirs(absdirpath) @@ -59,7 +59,7 @@ def ExportSingleText(text, dirpath, filename): def ExportSingleJson(json_data, dirpath, filename): # Export single Json - counter = bps.utils.CounterTimer() + counter = bpl.utils.CounterTimer() absdirpath = bpy.path.abspath(dirpath) bfu_basics.VerifiDirs(absdirpath) @@ -172,7 +172,7 @@ def WriteSingleMeshAdditionalParameter(unreal_exported_asset): def GetLodPath(lod_obj): asset_class = bfu_assets_manager.bfu_asset_manager_utils.get_asset_class(lod_obj) if asset_class: - directory_path = asset_class.get_obj_export_abs_directory_path(lod_obj) + directory_path = asset_class.get_obj_export_directory_path(lod_obj, True) file_name = asset_class.get_obj_file_name(lod_obj) return os.path.join(directory_path, file_name) @@ -211,33 +211,34 @@ def WriteAllTextFiles(): scene = bpy.context.scene addon_prefs = bfu_basics.GetAddonPrefs() - - if scene.text_ExportLog: + + root_dirpath = bpy.path.abspath(scene.bfu_export_other_file_path) + if scene.bfu_use_text_export_log: Text = languages.ti("write_text_additional_track_start") + "\n" Text += "" + "\n" Text += WriteExportLog() if Text is not None: Filename = bfu_basics.ValidFilename(scene.bfu_file_export_log_name) - ExportSingleText(Text, scene.bfu_export_other_file_path, Filename) + ExportSingleText(Text, root_dirpath, Filename) # Import script if bpy.app.version >= (4, 2, 0): - bfu_path = os.path.join(bbpl.blender_extension.extension_utils.get_package_path("blender_for_unrealengine"), "bfu_import_module") + bfu_path = os.path.join(bbpl.blender_extension.extension_utils.get_package_path(), "bfu_import_module") else: - bfu_path = os.path.join(bbpl.blender_addon.addon_utils.get_addon_path("Blender for UnrealEngine"), "bfu_import_module") + bfu_path = os.path.join(bbpl.blender_addon.addon_utils.get_addon_path("Unreal Engine Assets Exporter"), "bfu_import_module") - if scene.text_ImportAssetScript: + if scene.bfu_use_text_import_asset_script: json_data = bfu_write_import_asset_script.WriteImportAssetScript() - ExportSingleJson(json_data, scene.bfu_export_other_file_path, "ImportAssetData.json") + ExportSingleJson(json_data, root_dirpath, "ImportAssetData.json") source = os.path.join(bfu_path, "asset_import_script.py") filename = bfu_basics.ValidFilename(scene.bfu_file_import_asset_script_name) - destination = bpy.path.abspath(os.path.join(scene.bfu_export_other_file_path, filename)) + destination = os.path.join(root_dirpath, filename) copyfile(source, destination) - if scene.text_ImportSequenceScript: + if scene.bfu_use_text_import_sequence_script: json_data = bfu_write_import_sequencer_script.WriteImportSequencerTracks() - ExportSingleJson(json_data, scene.bfu_export_other_file_path, "ImportSequencerData.json") + ExportSingleJson(json_data, root_dirpath, "ImportSequencerData.json") source = os.path.join(bfu_path, "sequencer_import_script.py") filename = bfu_basics.ValidFilename(scene.bfu_file_import_sequencer_script_name) - destination = bpy.path.abspath(os.path.join(scene.bfu_export_other_file_path, filename)) + destination = os.path.join(root_dirpath, filename) copyfile(source, destination) diff --git a/blender-for-unrealengine/bfu_write_utils.py b/blender-for-unrealengine/bfu_write_utils.py index 62890db0..d4eaaf24 100644 --- a/blender-for-unrealengine/bfu_write_utils.py +++ b/blender-for-unrealengine/bfu_write_utils.py @@ -19,6 +19,7 @@ import os import bpy import datetime + from . import bbpl from . import bfu_basics from . import bfu_utils @@ -70,11 +71,11 @@ def add_generated_json_meta_data(json_data): blender_file_path = bpy.data.filepath if bpy.app.version >= (4, 2, 0): - version_str = 'Version '+ bbpl.blender_extension.extension_utils.get_package_version("blender_for_unrealengine") - addon_path = bbpl.blender_extension.extension_utils.get_package_path("blender_for_unrealengine") + version_str = 'Version '+ str(bbpl.blender_extension.extension_utils.get_package_version()) + addon_path = bbpl.blender_extension.extension_utils.get_package_path() else: - version_str = 'Version '+ bbpl.blender_addon.addon_utils.get_addon_version_str("Blender for UnrealEngine") - addon_path = bbpl.blender_addon.addon_utils.get_addon_path("Blender for UnrealEngine") + version_str = 'Version '+ bbpl.blender_addon.addon_utils.get_addon_version_str("Unreal Engine Assets Exporter") + addon_path = bbpl.blender_addon.addon_utils.get_addon_path("Unreal Engine Assets Exporter") import_modiule_path = os.path.join(addon_path, "bfu_import_module") diff --git a/blender-for-unrealengine/blender_manifest.toml b/blender-for-unrealengine/blender_manifest.toml deleted file mode 100644 index e494c6d6..00000000 --- a/blender-for-unrealengine/blender_manifest.toml +++ /dev/null @@ -1,27 +0,0 @@ -schema_version = "1.0.0" - -id = "blender_for_unrealengine" -version = "4.3.1" -name = "Blender for UnrealEngine" -tagline = "Allows to batch export and import in Unreal Engine" -maintainer = "Loux Xavier (BleuRaven) xavierloux.loux@gmail.com" -type = "add-on" - -website = "https://github.com/xavier150/Blender-For-UnrealEngine-Addons/" - -tags = ["Import-Export"] - -blender_version_min = "4.2.0" - -license = [ - "SPDX:GPL-3.0-or-later", -] - -copyright = [ - "Copyright 2024, Xavier Loux. All rights reserved.", -] - -platforms = ['windows-x64', 'linux-x64'] - -[permissions] -files = "Import/export FBX from/to disk" \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/bps/__init__.py b/blender-for-unrealengine/bpl/__init__.py similarity index 93% rename from blender-for-unrealengine/bfu_import_module/bps/__init__.py rename to blender-for-unrealengine/bpl/__init__.py index 3da665cf..ff3e288d 100644 --- a/blender-for-unrealengine/bfu_import_module/bps/__init__.py +++ b/blender-for-unrealengine/bpl/__init__.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- @@ -45,4 +46,5 @@ if "blender_sub_process" in locals(): importlib.reload(blender_sub_process) if "naming" in locals(): - importlib.reload(naming) \ No newline at end of file + importlib.reload(naming) + diff --git a/blender-for-unrealengine/bps/advprint.py b/blender-for-unrealengine/bpl/advprint.py similarity index 85% rename from blender-for-unrealengine/bps/advprint.py rename to blender-for-unrealengine/bpl/advprint.py index 75ac6f27..414d5efb 100644 --- a/blender-for-unrealengine/bps/advprint.py +++ b/blender-for-unrealengine/bpl/advprint.py @@ -17,13 +17,12 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- - -import sys import time @@ -34,7 +33,7 @@ def _get_name(self): def _set_name(self, value): if not isinstance(value, str): - raise TypeError("name must be set to an String") + raise TypeError("name must be set to a String") self.__name = value name = property(_get_name, _set_name) @@ -57,7 +56,6 @@ def _get_total_step(self): def _set_total_step(self, value): if not (isinstance(value, int) or isinstance(value, float)): raise TypeError("total_step must be set to an Integer or Float") - self.__total_step = value total_step = property(_get_total_step, _set_total_step) @@ -80,29 +78,25 @@ def update_progress(self, progress): total_step = self.__total_step self.__previous_step = progress # Update the previous step. - is_done = False - if progress >= total_step: - is_done = True + is_done = progress >= total_step - # Write message. + # Write message msg = "\r{0}:".format(job_title) if self.show_block: - block = int(round(length*progress/total_step)) - msg += " [{0}]".format("#"*block + "-"*(length-block)) + block = int(round(length * progress / total_step)) + msg += " [{0}]".format("#" * block + "-" * (length - block)) if self.show_steps: msg += " {0}/{1}".format(progress, total_step) if is_done: - msg += " DONE IN {0}s\r\n".format(round(time.perf_counter()-self.__counter_start, 3)) - - else: - if self.show_percentage: - msg += " {0}%".format(round((progress*100)/total_step, 2)) + msg += " DONE IN {0}s\r\n".format(round(time.perf_counter() - self.__counter_start, 3)) + elif self.show_percentage: + msg += " {0}%".format(round((progress * 100) / total_step, 2)) - sys.stdout.write(msg) - sys.stdout.flush() + # Print the progress message on the same line + print(msg, end='', flush=True) def print_separation(number=60, char="-"): diff --git a/blender-for-unrealengine/bps/blender_sub_process.py b/blender-for-unrealengine/bpl/blender_sub_process.py similarity index 93% rename from blender-for-unrealengine/bps/blender_sub_process.py rename to blender-for-unrealengine/bpl/blender_sub_process.py index f845d516..035cac54 100644 --- a/blender-for-unrealengine/bps/blender_sub_process.py +++ b/blender-for-unrealengine/bpl/blender_sub_process.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bps/color_set.py b/blender-for-unrealengine/bpl/color_set.py similarity index 98% rename from blender-for-unrealengine/bps/color_set.py rename to blender-for-unrealengine/bpl/color_set.py index 8d39ebe4..3b066ea5 100644 --- a/blender-for-unrealengine/bps/color_set.py +++ b/blender-for-unrealengine/bpl/color_set.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bpl/console_utils.py b/blender-for-unrealengine/bpl/console_utils.py new file mode 100644 index 00000000..c6c1ba47 --- /dev/null +++ b/blender-for-unrealengine/bpl/console_utils.py @@ -0,0 +1,29 @@ +# ====================== BEGIN GPL LICENSE BLOCK ============================ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# All rights reserved. +# +# ======================= END GPL LICENSE BLOCK ============================= + +# ---------------------------------------------- +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL +# BleuRaven.fr +# XavierLoux.com +# ---------------------------------------------- + +import os + +def clear_console(): + os.system('cls' if os.name == 'nt' else 'clear') \ No newline at end of file diff --git a/blender-for-unrealengine/bfu_import_module/bps/math.py b/blender-for-unrealengine/bpl/math.py similarity index 96% rename from blender-for-unrealengine/bfu_import_module/bps/math.py rename to blender-for-unrealengine/bpl/math.py index d036605a..376bc45b 100644 --- a/blender-for-unrealengine/bfu_import_module/bps/math.py +++ b/blender-for-unrealengine/bpl/math.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bps/naming.py b/blender-for-unrealengine/bpl/naming.py similarity index 97% rename from blender-for-unrealengine/bps/naming.py rename to blender-for-unrealengine/bpl/naming.py index 9c43c4f6..497e43e1 100644 --- a/blender-for-unrealengine/bps/naming.py +++ b/blender-for-unrealengine/bpl/naming.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bfu_import_module/bps/utils.py b/blender-for-unrealengine/bpl/utils.py similarity index 97% rename from blender-for-unrealengine/bfu_import_module/bps/utils.py rename to blender-for-unrealengine/bpl/utils.py index 320f3319..bc6592d7 100644 --- a/blender-for-unrealengine/bfu_import_module/bps/utils.py +++ b/blender-for-unrealengine/bpl/utils.py @@ -17,7 +17,8 @@ # ======================= END GPL LICENSE BLOCK ============================= # ---------------------------------------------- -# BPS -> BleuRaven Python Script +# BPL -> BleuRaven Python Library +# https://github.com/xavier150/BPL # BleuRaven.fr # XavierLoux.com # ---------------------------------------------- diff --git a/blender-for-unrealengine/bps/console_utils.py b/blender-for-unrealengine/bps/console_utils.py deleted file mode 100644 index 596331d9..00000000 --- a/blender-for-unrealengine/bps/console_utils.py +++ /dev/null @@ -1,4 +0,0 @@ -import os - -def clear_console(): - os.system('cls' if os.name == 'nt' else 'clear') \ No newline at end of file diff --git a/blender-for-unrealengine/languages/local_list/en_US.json b/blender-for-unrealengine/languages/local_list/en_US.json index 476e8888..3efe1e2a 100644 --- a/blender-for-unrealengine/languages/local_list/en_US.json +++ b/blender-for-unrealengine/languages/local_list/en_US.json @@ -1,8 +1,10 @@ { "interface": { - "intro": "Blender for Unreal Engine by Xavier Loux. (BleuRaven)", + "intro": "Unreal Engine Assets Exporter by Xavier Loux. (BleuRaven)", "bake_armature_action_name": "Bake Armature animation", - "correct_extrem_uv_scale_name": "Correct Extrem UV Scale", + "correct_use_extrem_uv_scale_name": "Correct Extrem UV Scale For Unreal", + "correct_extrem_uv_scale_step_scale_name": "Step Scale", + "correct_extrem_uv_scale_use_absolute_name": "Use Positive Pos", "add_skeleton_root_bone_name": "Add root bone", "skeleton_root_bone_name_name": "Skeleton root bone name", "rescale_full_rig_at_export_name": "Rescale exported rig", @@ -37,7 +39,10 @@ }, "tooltips": { "bake_armature_action_desc": "Bake Armature animation for export (Export will take more time).", - "correct_extrem_uv_scale_desc": "Correct Extrem UV Scale for better UV quality in UE4 (Export will take more time).", + "correct_use_extrem_uv_scale_desc": "Correct Extrem UV Scale for better UV quality in Unreal Engine (Export will take more time).", + "correct_extrem_uv_scale_step_scale_desc": "Scale of the snap grid.", + "correct_extrem_uv_scale_use_absolute_desc": "Keep uv islands to positive positions.", + "correct_extrem_uv_scale_operator_desc": "Correct Extrem UV Scale for better UV quality in Unreal Engine (Export will take more time).", "add_skeleton_root_bone_desc": "Remove the armature root bone.", "skeleton_root_bone_name_desc": "Name of the armature when exported. This is used to change the root bone name. If egal \"Armature\" Ue4 will remove the Armature root bone.", "rescale_full_rig_at_export_desc": "This will rescale the full rig at the export with the all constraints.", diff --git a/blender-for-unrealengine/languages/local_list/fr_FR.json b/blender-for-unrealengine/languages/local_list/fr_FR.json index 528c16c2..ebd9cc07 100644 --- a/blender-for-unrealengine/languages/local_list/fr_FR.json +++ b/blender-for-unrealengine/languages/local_list/fr_FR.json @@ -2,7 +2,7 @@ "tooltips": { }, "interface": { - "intro": "Blender for Unreal Engine par Xavier Loux." + "intro": "Unreal Engine Assets Exporter par Xavier Loux." }, "new_data": { } diff --git a/blender-for-unrealengine/languages/local_list/ru_RU.json b/blender-for-unrealengine/languages/local_list/ru_RU.json index 3889e8ae..38200197 100644 --- a/blender-for-unrealengine/languages/local_list/ru_RU.json +++ b/blender-for-unrealengine/languages/local_list/ru_RU.json @@ -1,8 +1,10 @@ { "interface": { - "intro": "Blender for Unreal Engine от Xavier Loux. (BleuRaven)", + "intro": "Unreal Engine Assets Exporter от Xavier Loux. (BleuRaven)", "bake_armature_action_name": "Запечь анимацию скелета", - "correct_extrem_uv_scale_name": "Исправить масштаб UV", + "correct_use_extrem_uv_scale_name": "Исправить масштаб UV", + "correct_extrem_uv_scale_step_scale_name": "Step Scale", + "correct_extrem_uv_scale_use_absolute_name": "Use Positive Pos", "add_skeleton_root_bone_name": "Добавить корневую кость", "skeleton_root_bone_name_name": "Название корневой кости скелета", "rescale_full_rig_at_export_name": "Изменить масштаб экспортированного рига", @@ -37,7 +39,10 @@ }, "tooltips": { "bake_armature_action_desc": "Запечь анимацию скелета для экспорта (экспорт займет больше времени).", - "correct_extrem_uv_scale_desc": "Исправить масштаб UV для лучшего качества UV в UE4 (экспорт займет больше времени).", + "correct_use_extrem_uv_scale_desc": "Исправить масштаб UV для лучшего качества UV в UE4 (экспорт займет больше времени).", + "correct_extrem_uv_scale_step_scale_desc": "Scale of the snap grid.", + "correct_extrem_uv_scale_use_absolute_desc": "Keep uv islands to positive positions.", + "correct_extrem_uv_scale_operator_desc": "Correct Extrem UV Scale for better UV quality in Unreal Engine (Export will take more time).", "add_skeleton_root_bone_desc": "Удалить корневую кость скелета.", "skeleton_root_bone_name_desc": "Название скелета при экспорте. Это используется для изменения имени корневой кости. Если равно \"Armature\", Ue4 удалит корневую кость Armature.", "rescale_full_rig_at_export_desc": "Это изменит масштаб всего рига при экспорте со всеми ограничениями.", diff --git a/blender-for-unrealengine/languages/local_list/zh_HANS.json b/blender-for-unrealengine/languages/local_list/zh_HANS.json new file mode 100644 index 00000000..3c5709f5 --- /dev/null +++ b/blender-for-unrealengine/languages/local_list/zh_HANS.json @@ -0,0 +1,69 @@ +{ + "interface": { + "intro": "Blender for Unreal Engine by Xavier Loux. (BleuRaven)", + "bake_armature_action_name": "烘焙骨架动画", + "correct_extrem_uv_scale_name": "纠正极端UV缩放", + "add_skeleton_root_bone_name": "添加根骨骼", + "skeleton_root_bone_name_name": "骨架根骨骼名称", + "rescale_full_rig_at_export_name": "导出时调整rig比例", + "rescale_full_rig_at_export_auto_name": "自动", + "rescale_full_rig_at_export_custom_rescale_name": "自定义比例", + "rescale_full_rig_at_export_dont_rescale_name": "不调整比例", + "new_rig_scale_name": "新比例", + "static_sockets_add_90_x_name": "导出静态网格插槽时X轴+90度", + "rescale_sockets_at_export_name": "导出时调整插槽比例", + "rescale_sockets_at_export_auto_name": "自动", + "rescale_sockets_at_export_custom_rescale_name": "自定义比例", + "rescale_sockets_at_export_dont_rescale_name": "不调整比例", + "static_sockets_imported_size_name": "静态网格插槽导入尺寸", + "skeletal_sockets_imported_size_name": "骨架网格插槽导入尺寸", + "export_camera_as_fbx_name": "导出相机为FBX", + "export_spline_as_fbx_name": "导出样条为FBX", + "bake_only_key_visible_in_cut_name": "仅烘焙可见帧", + "ignore_nla_for_action_name": "忽略Actions的非线性动画", + "export_with_custom_props_name": "导出自定义属性", + "export_with_custom_curves_name": "导出自定义曲线值", + "export_with_meta_data_name": "导出元数据", + "revert_export_path_name": "每次导出时重置所有导出路径。", + "use_generated_scripts_name": "使用生成的脚本导入资源和序列。", + "collision_color_name": "碰撞体颜色", + "notify_unit_scale_potential_error_name": "如果单位比例不等于0.01, 则通知潜在错误", + "write_text_additional_track_start": "此文件由插件Blender for UnrealEngine生成: https://github.com/xavier150/Blender-For-UnrealEngine-Addons", + "write_text_additional_track_end": "此脚本应在UE编辑器中与其Python插件一起使用: https://docs.unrealengine.com/en-US/Engine/Editor/ScriptingAndAutomation/Python", + "write_text_additional_track_spline": "此文件包含fbx文件不支持的样条线数据附加信息。", + "write_text_additional_track_camera": "此文件包含fbx文件不支持的相机动画附加信息。", + "write_text_additional_track_all": "用于在UE中导入所有类型的资源, 如StaticMesh、SkeletalMesh、Animation、Pose、Camera等", + "end": "结束" + }, + "tooltips": { + "bake_armature_action_desc": "烘焙骨架动画以便导出(更长导出时间)。", + "correct_extrem_uv_scale_desc": "修正UV比例以提高UE4中的UV质量(更长导出时间)。", + "add_skeleton_root_bone_desc": "移除骨架的根骨骼。", + "skeleton_root_bone_name_desc": "导出时的骨架名称。用于更改根骨骼的名称。如果为“Armature”, Ue4将删除Armature根骨骼。", + "rescale_full_rig_at_export_desc": "导出时调整整个骨架的比例,包括所有约束。", + "rescale_full_rig_at_export_auto_desc": "仅在单位比例不等于0.01时调整比例。", + "rescale_full_rig_at_export_custom_rescale_desc": "您可以选择导出时如何调整骨架比例", + "rescale_full_rig_at_export_dont_rescale_desc": "不调整rig比例", + "new_rig_scale_desc": "新rig比例。自动: [新比例] = 100 * [单位比例]", + "static_sockets_add_90_x_desc": "静态网格插槽在UE中以X轴-90度自动导入", + "rescale_sockets_at_export_desc": "导出时调整所有插槽的比例。", + "rescale_sockets_at_export_auto_desc": "仅在单位比例不等于0.01时调整比例。", + "rescale_sockets_at_export_custom_rescale_desc": "您可以选择导出时如何调整插槽比例。", + "rescale_sockets_at_export_dont_rescale_desc": "不调整插槽比例。自动: 1([新比例] = 100 / [单位比例])", + "static_sockets_imported_size_desc": "在UE中导入时的插槽尺寸。", + "skeletal_sockets_imported_size_desc": "在UE中导入时的插槽尺寸。自动: 1([新比例] = 100 / [单位比例])", + "export_camera_as_fbx_desc": "当导出附加数据(导出 -> 导出过滤器)并使用序列导入脚本时,取消选中此项可以缩短导出时间。", + "export_spline_as_fbx_desc": "导出样条线为FBX文件", + "bake_only_key_visible_in_cut_desc": "仅当在相机帧中可见时才烘焙相机。 仅在相机帧中可见时烘焙相机。", + "ignore_nla_for_action_desc": "导出动作并忽略非线性动画中的所有层。", + "export_with_custom_props_desc": "处理带有自定义属性的导出(可用于元数据)。", + "export_with_custom_curves_desc": "处理带有自定义属性的导出(用于UE自定义曲线值)", + "export_with_meta_data_desc": "处理带有元数据的导出。", + "revert_export_path_desc": "每次导出时删除所有导出路径的文件夹。", + "use_generated_scripts_desc": "如果为 false, 则所有仅适用于导入脚本的属性都将被禁用。", + "collision_color_desc": "Blender中的碰撞体颜色。", + "notify_unit_scale_potential_error_desc": "如果单位比例不等于0.01, 则通知潜在错误。", + "end": "结束" + }, + "new_data": {} +} \ No newline at end of file diff --git a/blender-for-unrealengine/run_unreal_import_script.py b/blender-for-unrealengine/run_unreal_import_script.py new file mode 100644 index 00000000..5e08ca81 --- /dev/null +++ b/blender-for-unrealengine/run_unreal_import_script.py @@ -0,0 +1,102 @@ +# This script should be run in Unreal Engine to import assets into Unreal Engine 4 and 5. +# The assets are exported from the Unreal Engine Assets Exporter. More details can be found here: https://github.com/xavier150/Blender-For-UnrealEngine-Addons +# Use the following command in Unreal cmd console and follow the instructions: +# py "[ScriptLocation]\run_unreal_import_script.py" -h +# py "M:\MMVS_ProjectFiles\content\Level\TalasCozyHouse\ExportedFbx\ImportAssetScript.py" -h + +import os +import sys +import importlib.util +import argparse +import json + +def json_load(json_file): + # In Python 3.9: The keyword argument encoding has been removed. + if sys.version_info >= (3, 9): + return json.load(json_file) + else: + return json.load(json_file, encoding="utf8") + +def json_load_file(json_file_path): + # In Python 3.9: The keyword argument encoding has been removed. + if sys.version_info[0] < 3: + with open(json_file_path, "r") as json_file: + return json_load(json_file) + else: + with open(json_file_path, "r", encoding="utf8") as json_file: + return json_load(json_file) + +def import_unreal_module(): + # Get the script directory + script_dir = os.path.dirname(os.path.abspath(__file__)) + module_name = "bfu_import_module" + module_file = os.path.join(script_dir, module_name, "__init__.py") + + # Load the module dynamically + spec = importlib.util.spec_from_file_location(module_name, module_file) + module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = module + spec.loader.exec_module(module) + return module + +def clear_unreal_module(): + module_name = "bfu_import_module" + del sys.modules[module_name] + +def run_from_asset_import_script(import_data_filepath): + module = import_unreal_module() + try: + module.run_asset_import(json_load_file(import_data_filepath)) + except Exception as e: + print(f"An error has occurred: {e}") + clear_unreal_module() + +def run_from_sequencer_import_script(import_data_filepath): + module = import_unreal_module() + try: + module.run_sequencer_import(json_load_file(import_data_filepath)) + except Exception as e: + print(f"An error has occurred: {e}") + clear_unreal_module() + +def run_from_arguments(): + args_valid = False + parser = argparse.ArgumentParser(description='Process Unreal Engine asset or sequencer import.') + parser.add_argument('--type', type=str, required=True, help='Content type to import in Unreal Engine. (required)') + parser.add_argument('--data_filepath', type=str, required=True, help='JSON filename with data to import. (required)') + parser.add_argument('--show_finished_popup', action='store_true', help='Show a popup when finished. (optional)') + + try: + args = parser.parse_args() + args_valid = True + except argparse.ArgumentError as e: + print(f"Argument error: {e}") + except SystemExit as e: + print("Error: Required arguments are missing. Use -h in arguments for help.") + + if(args_valid): + import_type = args.type + import_data_filepath = args.data_filepath + show_finished_popup = args.show_finished_popup + + if import_type == "assets": + asset_data = json_load_file(import_data_filepath) + module = import_unreal_module() + try: + module.run_asset_import(asset_data, show_finished_popup) + except Exception as e: + print(f"An error has occurred: {e}") + clear_unreal_module() + elif import_type == "sequencer": + asset_data = json_load_file(import_data_filepath) + module = import_unreal_module() + try: + module.run_sequencer_import(asset_data, show_finished_popup) + except Exception as e: + print(f"An error has occurred: {e}") + clear_unreal_module() + else: + print("Error: --type must be 'assets' or 'sequencer'") + +if __name__ == "__main__": + run_from_arguments() diff --git a/docs/Examples/AssetsExample_Alembic.blend b/docs/Examples/AssetsExample_Alembic.blend index 7903567e..6b130603 100644 Binary files a/docs/Examples/AssetsExample_Alembic.blend and b/docs/Examples/AssetsExample_Alembic.blend differ diff --git a/docs/Examples/AssetsExample_Animation_SkeletonSearch.blend b/docs/Examples/AssetsExample_Animation_SkeletonSearch.blend index 20943d62..7bc3b5b0 100644 Binary files a/docs/Examples/AssetsExample_Animation_SkeletonSearch.blend and b/docs/Examples/AssetsExample_Animation_SkeletonSearch.blend differ diff --git a/docs/Examples/AssetsExample_Lod.blend b/docs/Examples/AssetsExample_Lod.blend index 4d1dc5ae..ce5e564b 100644 Binary files a/docs/Examples/AssetsExample_Lod.blend and b/docs/Examples/AssetsExample_Lod.blend differ diff --git a/docs/Examples/AssetsExample_ModularExport.blend b/docs/Examples/AssetsExample_ModularExport.blend index 5b2fb4bf..d93cbb8f 100644 Binary files a/docs/Examples/AssetsExample_ModularExport.blend and b/docs/Examples/AssetsExample_ModularExport.blend differ diff --git a/docs/Examples/AssetsExample_MultiTypeExport.blend b/docs/Examples/AssetsExample_MultiTypeExport.blend index f5c44cec..4091a86d 100644 Binary files a/docs/Examples/AssetsExample_MultiTypeExport.blend and b/docs/Examples/AssetsExample_MultiTypeExport.blend differ diff --git a/docs/promo_pictures/Source Featured Images/Featured Image (NoBlenderLogo) 16.9.pdn b/docs/promo_pictures/Source Featured Images/Featured Image (NoBlenderLogo) 16.9.pdn new file mode 100644 index 00000000..e5d7a443 Binary files /dev/null and b/docs/promo_pictures/Source Featured Images/Featured Image (NoBlenderLogo) 16.9.pdn differ diff --git a/docs/promo_pictures/Source Featured Images/Icon Image (NoBlenderLogo) 1.1.pdn b/docs/promo_pictures/Source Featured Images/Icon Image (NoBlenderLogo) 1.1.pdn new file mode 100644 index 00000000..819ede66 --- /dev/null +++ b/docs/promo_pictures/Source Featured Images/Icon Image (NoBlenderLogo) 1.1.pdn @@ -0,0 +1,1892 @@ +PDN3V OPaintDotNet.Data, Version=5.13.8830.42291, Culture=neutral, PublicKeyToken=nullPaintDotNet.Document +isDisposedlayerswidthheight savedWithuserMetadataItemsPaintDotNet.LayerListSystem.VersionSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][] XX  PaintDotNet.LayerListparentArrayList+_itemsArrayList+_sizeArrayList+_versionPaintDotNet.Document   System.Version_Major_Minor_Build _Revision ~"3System.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]System.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]keyvalue $exif.tag0[0] +D $exif.tag4[0] / $exif.tag5[0]7 $exif.tag6[0]7              !OPaintDotNet.Core, Version=5.13.8830.42291, Culture=neutral, PublicKeyToken=nullPaintDotNet.BitmapLayer +propertiessurfaceLayer+isDisposed Layer+width Layer+heightLayer+properties-PaintDotNet.BitmapLayer+BitmapLayerPropertiesPaintDotNet.Surface!!PaintDotNet.Layer+LayerProperties " #XX $ % &XX ' ( )XX * + ,XX - . /XX 0 1 2XX 3 4 5XX 6 7 8XX 9 : ;XX < = >XX ? @ AXX B C DXX E  F GXX H"-PaintDotNet.BitmapLayer+BitmapLayerPropertiesblendOp&PaintDotNet.UserBlendOps+NormalBlendOp I#PaintDotNet.Surfacewidthheightstridescan0PaintDotNet.MemoryBlock!!XX` J$!PaintDotNet.Layer+LayerPropertiesnameuserMetadataItemsvisible isBackgroundopacity blendModeSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]][]PaintDotNet.LayerBlendModeKBlender LPaintDotNet.LayerBlendModevalue__%" N&#XX` O'$PUE L(" S)#XX` T*$UUE Logo L+" S,#XX` Y-$ U L." S/#XX` ^0$ U L1" S2#XX` c3$ U L4" S5#XX` h6$ U L7" S8#XX` m9$ U L:" q;#XX` r<$sLine L=" v>#XX` w?$xArrow L@" {A#XX` |B$}Text LC" {D#XX` E$ } L|F" {G#XX` H$ } LwI&PaintDotNet.UserBlendOps+NormalBlendOpJPaintDotNet.MemoryBlocklength64 hasParentdeferred !LSystem.Collections.Generic.KeyValuePair`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]NIOJSITJYJ^JcJhJmJqIrJvIwJ{I|JJJ  +w#Iv-4{G[b6]UfF%vayDfL և{@ Dsoۓ}erxx6~ϟoFiX_ڵkgkoMg=ue9|V\7^߭O:۠zO_uֿ.zF=t9k/3ǣ[N{aTuvww9q:3VCyO?As4uU͜kS?u>89[_N :v gNG.fٿF9g,aY>4 ge^9/K :o̺0o{ ?}֜F='xŰۡW0nV|7n/[;r̭ζdm?igs(:m2>؍Lt^{kY^֭{Azk {v֧FS^: +Vyy\.tnep ,;(b\Vνְ궗kc}ҭ=1Zfg~Vl]am?֯1ҌեY^ ͙~+w'N~9֧s5}}fcn9yjҾ&'ᶖZf=8u{M.v]sl8긗>ۤ_;խ[w|'m8d~{_=F'Żd:H4juz\yo?\d*5>ީs?u9쇧&~][u[RèQ~z(,fLjWo+\z|1i'qG9g8es4q~XIO/g=]+OkT'_(βd>~ݍ~nciuVf:èg?\&L#֊Ao?v[_&NDh3Ybuܩ/շ~f+玓-~S=;rt|1iWUǽeӼ[ICӹ9vrN_6M/S>s0eU/W8؋e&Q',(C{n~bZzu2Y2a`6$|sj]}Y&ӌؠL+_tb^u#:_T{fgnu6 񙺱e/'G9ׇNuo}uXf}ɍ0nulCZ<nu2 YWK髝&Vm/2íuhżFu﹛n~{tM]N/|1:\9vT}#sqk=ϛqj κr԰SyIqmNo={Ź /Y/hgYحW4:6eh)^f=^ߜ l,]pMbN1ms߹ĭx R7NMASfQ/NβSߝVs3^~ajNumM~y9eqofx)\u~}<+[Ye#o>ŭu;8nLͰyjza~N>m7­޽Ӭnw591= %2Jm5izu3:w[nI3Nx{ansK}ݬw=M ν[=wkaWzyV?}zƵ70aA/zS{[-s5GOw~ڢ1K&W k|uVnOFcZu]E_wAg&ݏd\߻)طLsOf{GO/zyNMLsAƱ& ><ާq٧{Qԑc[f<^u39dIgmfA3jX:s_ff8?f24[[VI]C{Qi^uԭnARxVq+&jζg_\N}\_3226B>Ϙ}ǡi5ֱ^UW&n&/h[?Y~n\Ӡi6 c2ymn~lA'fXWn~mU;/^m*]>~^vV3}k_^{̹o2d/ՉwӟX짮/n=LU z:8(/pbca~bб1lyk_`WNI1nIJm-=&ݿ%SY;yE*kq>mZ#oG]t fί 3;psO\M6y՗{ +fF +ι[;jؚg2ﵙ-]t[v#i{ia69ir0džsF[ R/,OœR.]+4Դ0.eA~dgNIrYglGޫ '&<9/OߛUaG7n&Ӓٖnq:Y_9kO|1s~=!n O/UKu1J$~\o=Bpoe'[ʑl>Zs{ i>^n~wCtkQ+/֛έly^]Sz7-ufqw]n23,:-mizcdVhALwF3^ 3rQp_/on{aˠ`Z"_~Mr™ߥ e̽d"=g?4Lnqk/V3L7|/ڤ3sW|lu~~ӶrhjN{^G9s1̶oNr^ѭo.9nmӏn5.Eaim7~eXcG}KziNmk~w|uMI犲zUjuY5=Yzo9vO"*5*MF-SW3u3SO3yij^L-h\LYƐ9'I143Y'>g}\si78Y6}?u/l0 +-ren,?h۸m^liM欣I_voS?8IJ`+`0_K2eV>? 5weВPxGָd]N-OG[s2l;5[nGm,51-Y^^ۧӚmL> F]iՆti:['14g>e\{a{ia!۰S)ߠz-S~lK59<ؗ|!KcY󚬼loސjz r$exPwퟍ{l8a̷&ӸqM1ˤmksX7?[|{'m~dߴ́Ov_ οmDg}uaiYiwm^h7mK6>ƑKGIyκԞ ZCIl6%<$E?ȃ7U|?ܖ(5gaԗQMo͋YxpjknlM_3zp-2鱡aZ}fh~tu5NFQ>M61^X`\̨j< lj xR{3?mdAhͭ_ kWkGIUy ۇg'k<ŗ<4H&uT5Y;'+FKM9pzctPi(8pji&glռ4]7jν<=oN~6|Ü}^]iNc~?iif+]_nc?mdv3vtlMst=zWο:;vrO%<ٚ,n,bT`{9, jJ+7^ߕ\Mlul^r?6캜&VAt gߝ5n\ՏͧcYTF~^@xeu8MMVh~۩y^g[^4\eu76 t9']2"ڪ|Wsg?ޔ-,<*:{uڔd TVW$Iu"syO _:1Yṷ47[2Q}V?d6V>,4rjݒջIƋ˅Dz*0OPwDZVN̚W?s[uNR~,ӨiW;=I/b5,>0Qsհƕn =Z/뾟8ا-N'bm6>>ƌ_c/:i\l^ >1ؿ#'tdoD>>$IK/d9]śee:V%Fk;i 2'~cgU&GZO(˨JS9P6L}@ij&boZ5К &Msfrv/=O\IjlNcEkYtn=ō,GӲw[o$J_I?J?$hK?Kes7d[qԏTl[]kԴ3@L5Eq~yZnͽ4tOj6~ ]A NN~XͭEg| {tiC/g^6:tyV~[<ĿSѾ,$W2'/ >{*[O>WI~MZrnū瑩l=ٟfxij&i9Bﻦ9MWsO/j΂v5S4}wC7?gܙm: S]Ivbmg#&]o~nfi^4d$Tg컿*-1.?hv=>m1U-vFzݏՆ^+tO[vk&O_LFj^wL7YlO8sf.~j(IP?m*^tQ~Is?dۊ櫷?(lD9I^WM-MOsA~(W&M *AֈNmU^5ge6s3;͍״_΍w;SϘ@zRnlը>6Nz.z5ԎmWyF9Od7Oq?Kwkd;[oenT˥[gJ8˞bXkۼbӜ[SSgT{ܘ>xT\5x1`?K9Ǟyt~jV7oV4 _}q8xԟ=:UV?sivcp\۳.W%ߠ櫘JK~ ϒGl{юwO3UaQ:|^*DN2~'}6 Ыn]qcQc?s[;u;-ůw:KssŢBbٮկ~;/ͱnIklM9Yp wiGݖc~z^}M|)?˂U"BIm ++(ba}Q34tj8}n|?WMVub{Y@ԼaM7n^sv:!sI1榵ik޽+8m0u^^ǧ{]:玲~]~ڳ[YImE?P݌~DqMraOiW_<~ڞ3sSF%Nl%J Nƚcks^biُ;bA+YA<a@~xt85n@Nm1՟9z]ͻS^>4i6JO|%}le[eՅD _zU +}WnK?٭_ ۯƂ9~ֳs&s^9_m 9:~ր~rmAyל{nkSu;W5ښ}u=OKVz-_^3~W?yҰ.l,4) a'!k/"3|5{5W^|2Щ#7=_]Oժ +!h?~i_?M3>&]m^: m;9t=;nښ;Yޓi~jnQOWWq|o|K\,OY|Y.,|1U?9:=W1Ϗ鲍FCiXνZ^DrPxz7.b<y fjaIv5cvj?~L^붮wtYσ۴qZl~go&y; |i|RNKnB\0! /_-!lc=Ï63q=\ٔJ޼fRwG̻t=Ӿ:e>gYU`^s]9} 맍\Ǧzǔs+p}z1v߁[tCIYގJ%ʞɁeؗ߫ 4~ٻ%TQv}$;OIɧJ9t&zyL ?h_գOr;Q߃qCfr86uzz亮Ll._ v :їIRαMs/sǔSW5ۼW&|c?vJՀD>}? ǵ[߿+s;Y{C9O>ߕtr o۾l\ulkfn?|rZ-)J,^zڶSZ +jê®dN૯y>ЅWƻ.؝'ʔ%-y~3rPJf9u*'OɻƱ7lsjZ)cS EE"뻓3z]{N|f}Nxkmk4oQo(~UkRT[*,Z(*xoP*d*)dB%cG%V_-ǿVL{!߷}(r*_.j—۲͍T?=oiG1ߍoqӹ'8ϸv{={I__b^|6wY~mM?$m4cuֺ]ܿ/Ci ?RLS}i0՗KoricGn&O?oz(5>g2մ%`:<zMCFy}@&"W,RS:e_}-?`4^SZޱ-K"% IIS.%S\R앤_tCpvDIZ]< Je"u?V+aCZZR?KogfY2xReby|^kjT8TSvc~R3^qIu|.SZ K0$#奶WSlR?KylsOʊg\JGNK z?mci[5^*iqUvOsOZl~_+S Ns~ rkh|V,#>X?}3rRٌoVlKlǶeucUCOc^g@+s {,X󪝰Nŵ~n1J0n/fkujw^|vyvA'|D;}M)rЬK{eX|-|'IlG}Z]b/fj'SGnvjmO=0u6* ,'OaŬ+y4%UJJ9n w{WQЯ)7hA,0C+o, +Rڇn Mϧ5S5ҒRVMJVUN@G_)E$ݑH&,;e?Ie%,<,4R41~{'JPr2ei(*kFM9 +|=<z:(-ch+T9Åm)J.6$Ũъ͢b~]mͰ0]- ] -)FN@3LeRJCck>cu1j_7ȿiWƁy>p߱<:iON6IMw^|NZDFizaM֗W$p5P`&h~& 33vl>P1/J]K|Se2y!UyavN5X"1(*tP1)9~%zvo4IVÒXS0|^J?Q<-Sz ̂LEX,fLL1`=m9?ئZ)4uJ# +$}ed&@9itVѪ%+d4M)+r %ِ@zKR )A-ZҍQ:.KG4o|vMh7࿥]Glaг`kꪭUe4l;l'}X%ki-pX z5l$WSuV*L6Dܵ#̙dc<_wxlGW)Zޑ|@B{mmɇ$RU3Ee5S<0#:aUT2/OůKDXS!KFNCݔl]sYvK2Iˮ6d+)<%A'Bm WCo$Cc/.,X,7`'x)X||6 `|U(\I-JmڌJ[ԟmLx.KC%Sw[>ViZ-Wkشf`7ZJ"9ǐ6B*^z/) NiunHy/L1#q1 l.k)FF_t^Pq*ګZP|^mF+?-V(,Fi6Ciuw.֚_ϻfr^^ZZ bz]Gl?OӺۭϿ#䂬*_%Wy Y/JD.e2@?bSX H(]|iL 'F/[WV1ΪZcKFJmA;BW*QbUi'$5T!.g~t + a%k#)-<Qi3.YI]ӂa@2s\3]31DXQV>3]sRks62ϤNz%d7x22ZM2ͰM0~\{ \_7-c,U(kUȬK ]l/l>bv`Hlo0jS孪wp6B18i橈?3U-2(t6A_RFΣQ4t;2vy5~QC+v5iK [G;AC{jiZg2%| ZZ26+9w +y 1{ţD?1 ^z0b;&uyfuݧ]L=ԏ괍> 2m .6TzTB2o+ޕKٖޡI #J +> qa*pD;KZ˃_ +xe#0"f1{AUűeѾʴR?KƊ(7X`5ih: +]π4|Y1sC|X(dܓKv8 iwc>xOr9qd-cˢ|l" 9r\FMqbWJ1s|:_ڌƳ&9*~ND#*ZJgsc48ar2wvuSqy?CMk#2;gWW%gZMu g~_H.$*cUe.kݗ5!_Ҭ>1W\9?,([]G} NʌD5Z6¯X?u%U;!>:߆}L0c>E)I:6vHjGsa BY쒬We):%*~)lĺXsu.*K5o4mņd#헢D~Jun9s1خ&t 9FǁU ߇P}4ǰu,> Z~nŹ:8Aź7X31+ِ,VH>G>vI]bZ}֭S͌zq9Vr,kdcn@k`$hqĎEcۊ`UĥTL}Q\>F١+> {eA 0}盁niǼ1fjk@X5Y;. *?g[g g_% ȘdM-N#iFS#ԹT$6OyEO߹cX벟B^>.3n%9'.Ҧ錧^']0#]pV_ܿgzr- o{K癩-G6E/:n1S: ƜWL*~Fű`=ѱ,|%+L^gI)@*#Τ{"F1-eMBUAV1*)|╷RZU`l5֟8`% CFlzO⹻ZG)~bQV9Yz,jLԒΘxQGccȆ\te{gx6ؓ)o lERbkLyQsQ 4}ƀ%tȚp.8`ZNN^ʘqv d3]jYD"^i5ŃA?W f,b+41S^O +l!C݇6m_mƪYj*fF}vW6&mu3ط,w5Y)d=\ Djֽ]oI@I٘ܩٌ>Ht벴@nw[DI6rue\voʕtMҙvwi|+cg873'!0c))DccՎiXb hHsпJ)uX*"[M&wBS5-zf   ^}kJV,Aj;E?{DnR."u,Wt4Ydhmߎ塿. +* +BMT9x-l9 VΑGZ&t'[>8wTq|^{cդ~qڠ0~]ǰGٌP [8vWCvleYOIAQqJTԡYa~ҥTUkwV^l(Ӻé=5ϑ7{i E7œ`ߌ>tmUP+}ÎS m#n7Q>g +h61&g%FSZjjNQ`4j;'M8ogy`2ۡ jIVVVLXIV1tl߸Tkq/6gџ9:ֳLC5&ͩk}'Nfnצ٥_Yu{>PZ|/bK"ϲE ކ!rԜiV!1^CJRI<#s!SbJ>|1"3+JOgr; ȥK6* yY./J1SU!0<تpzoOgДr3O j=gsqbqhN{1Fq}(u;F;lUw˒K$#y bĺZݱbUURu$w.Dwlx5*rRR2,s(&X-htߵcߕҎ<l>sf캊_Ƴ%EɨW⽼[aɽԞFJRr#}%>kJ[J_ TVV{nXeW%,EX~j2}j.k3E!s==@Aš+'}*,ЌCc>35c\Zc"o(F1z8vguֶ3g|v6Mqmjgl -=u%l>xwټ*.R`:_\]Nb%7{6'A߿a߫lxI9uEL?g~yňհeZls_/Ӌ-v|RHc8E!|r\LՔopn֯ૄN1λ{eݴ*(Vޥbc.;3wl*b)e٬}4c˘H,6|ZquB9*VV %++28/ȥ reGoXڊlԡ]U$Zec Q,8* 1,f\削BSSҢ*ʰnӘ +EhRY 31m &Aڋv zc|cy YVl'c6Pn怇69N[2w! 5QZq[Ieqc^m*bƧ]r4elu ru.0ޖTG#lגoPM2/_U_YR_ 7I{|[ɀhُEo6L''oHI|HZS _k7*)l%f |]6z]qRܕ?tߪƝͽs6y J>/n%cO^";1ԾMǩx jP6LUK#yP,f|MEK!A1Z*'F#1f̙C0[KcѦ1^ڽ&?F?0N߻i 9<يcsJZg&7(,j֎\y#9ʯK,9ԪL+s笯4 /ۗ-v~LԩNg\慣ACʤOU>T1q0.rqrq9 bnCAYd 1*|t y̕ԢlTseϑtYEj˟暄VQhX=y.YIUؖ:W]DYGաE5t O7?>b~Έ`bKG1 +u(%:<-cdNJ>z+/ _^zxN-oJa}g>V[0sĬ :K2煝3pV6c %3s328'^@f]vJACB-?;$TgLl"ђ:7ЪtHWf+=C k3b:~NV(}0~:j3Ae%OF#ǩx5h:o>f4=-<%̧ksugv͠VO'}S%:'Fm]h֖q9|5iU'Fߩ>Z0S9<\T+jXWW +и?蛣 zFI0wZ+g'T:~kb\I0BlE7$؄>c##YdP,* C s%Tjҙ$bmò& + 2O^ ?d&r,X's,~K$X퐭IvW̨$^L`>Ȟ"c7["VRĈⵘ6{}L ݫuF+! =ģšn[`41aooX1R2}>@+h3UiNɾ>I-e@"hd3iʑHLi;gCmٙigr{_M v?)1~|ƻ=FIj5-`^hI6yad覓E< ?mcѠth5#Ilޗ \)ZD|Tr&x,}^\59޻S{|W .}F;m[iK5saŦW T:jLj10{A.,Oyn'ri \_\XftQVS@Sq=.l)zs0'V>1m$_y^?; lfȕ+WE$sШdT̓Sph浴7ǻ'"ġz8=;d'^ˀVEЉg4[ ݄Vr3.\Y'gxN2Ƶ}h[ڔld땹6fçEU|ʷpRSZ׷ZxΗYo_ϞgWRh|UW|hK0>#yeߙ:VqM#b>xHkRW_R|-ͫ[hةcpX?D37h$VZg*v)SwYx_~Mubz8_h4bTU7u>{21ek ͢2u6Mu9,ave29d}'Qh:qb~,Һ&=?kz^>k +^yBݶ^sF&{Oxkru'/D=//J!{x9izo*/s?4tsE\vVwz0w!WޑU$˘]Ԫ e[ŸMY18 "è$c"∲(wMuJ6?OwƟnhv:ϻMǯ*J.O1vo;u_$?Q(ae%v}3[]? +_E╖ sr{ݸ=с_{B*W:!0r;<\{xҺ{r>ĜoO*7Jh-7Qgy R;ebV }ސRȪiMKwcd۬/lߊ_oSůDB`^ۘ,BNf.l.ySm~MU4uBʣJ>C[ JU߫_yd3[}v8OԲlvX8}w?lZ7mYjcw ASzS}u֡~~ $y3*\Fͽ^)*3:c"Ժ@SaPj[:ou{gax#Ƴu7Xe$]chWǻ^46y99w?m"ޅط3o:54 _o#&ݱ53 Ϗ|h2sI'/~"W7dݺKį$ ͌1L&S:}͔_dwt  +y%ӈ&#*oc}Xu0 [)6dcw]jLFNG@=uNiߗ9hSqf#ie>`>0ZKd/nXx$u4rcS㌪#-݌w|F&1d3 ۃ!7Ʀ9s٪|:-bPT~|<`şMuvguu'>o}kXc0ƃs@cA|:t?槿^5+ *bruユ[x#7sθʎ8QWQy \Cd{f,xqΰEĥlB(.K,'+J跱`<ZG-īyؐ=7XY4*+h㼜[*|CYZr\Y +_UpleYf]] +;.UrqՊ!|A02ԝ,]2vYB<],iǚRhve;gE8\%dӲR1W֮=vB8ٖ(tPCnRi-;X>Cu?qZO b-ΜE疷9Xq.`dAXpm{M6vVvObַruqH7^.KЄWWy"*GU?ӬJۢYZU+r波c$Wޖ(}7;a.}rRg]dSݨ?B z-P|OJCe; +$ +7뺌1CY~z?{}?:w܏cѯ [3lfkf4郻Rqrst>+WW-fk\5c̝`JCΠquX=ֈ=S| >3hswF&w p@?aib4U< ?|2٢q/3r2LSn^rvAs E;OG1㺕+ꏒ}_'kȅ\ݽ z WS;qZQoR||: DKT0*rk-@[&^32+\NTrVQ\8/-u[ 3Z4q%S^Fw9q4UP ϑ3_(sd53[/g"N*-lY - sut(=9̍})@MʘyVdwSJ+==cyH*Ops{C7m3R5p?RxVֳ˲Q& +.޽|@xy=v{Oϵ.T 5m#>t3އkc_O 6JqxM7!W0/XHf噄X*/ #&)A;z7Y}w$粀5Tߥx j z*EUSZsK`{ ٻ~")7IRw$b-?fN'tYPsC j .Cqv4q,>K;Lkg<ںF?hgm&cٚY;ѥqΡJg'17MrȜ?\{qű&n{(h=stܙʝ>ͩ1|& кX̻\a~cя?Ǣۼ[,x/p$Q԰K?)KؙhIf|xS-__ _&)ޔ9΁\H<(Kbfމͪb4r{{nzɃ͉-!nH#!|sӫznw)}v+L=Cg PdfV+.αaQ[C^IݓsKjo;x_lNffMyS\! 0=7kW|fYx1 +ݫv^{tafqY-\!>ӜgrsySu74>ݍYNyD~L|en^Y>VSһeWvbaߍ؟dA`%W,wI0*7\TfQ}ĠD/lZ2k_~/ $?`.@[lv.,jk_D߀?9۹/ F;O?L l5 +X#R}$̼dv+,3\A~>r)2{r!{ ˱]Y|Y"vc2a*0塑ĘQf̕~8@(̝[wI \Zр&VLs1&E+'j^4Tq/+0hV ?4{y@1]h~Tf3{v|[W=q@Y;LⳲ4e4dv~",PْHem=i*g*9ysa#1DZksմsm's{͛G?$Msh&9wFSO)v~Ri_.`}:f q7e| l}߾:՟UWC[qW[})G54rC}$NdW%!} V7>G42jf?%%ݕN| +c.IC%,L}BWW+Ʌz0%0 +gey}IU:_W¾_mu̻`ǖT[5&nb>hHsΛU\R f*Id C؁ƴnncN ^³H ˡeuak6_UT(ð1W>1d[C|'\~ kDoJ\Poz&sVW?G૚,s胋hē$ǒe趤>t({IcuhWęWIb|-V7ؑ;+ic=ߟ5|-,6k3cXkr[Д=n8L#^ь<گ .:GR=C[k3xޱf\M2hŞqOsΊNbmq{V3Z;OtpĻΙjRlӹS*']{RZbN.m!7˥ˊ5K7w)_Ӿ~ǯ~>;\>s`b^2o}#bˍ2R\csҧI:񨵲3|k}U\~..-/6Z'\Sf#%CrzMKT2 ,edRVR~+UpmUZ3W++2ױdm4_fv,?ﯻ +lN0#|Qv + Y5R)Wwde`u8TWoSXc*|C킯鎸Ê1ZsOF}nC|1s/B:bqoXms,ڱt) +p& xo[O?%R||s(,vw$oPE~+' yVx|=%WiQ#cG/ ɷݯ E_Vҟ'wO\߻E|x/_ݓҭkR^̯U@^M6+B3Xe݉:gu_@%pCɳID./]njVJ(%W*eAQHi;/տ{q㹂yNUG{*MrKۙTM6_/K \m ּK]] e{?;t8]75r(kU3_]_ҲDoH#Lrc^ye/H}F,a Q;`F %!/jXVOd|5z 3$ +shQ}9g.O`|3r l:㱹Qs 7c< +nskUMYd}gGЖ!;ďoI5kR}=}[O[7ݲ#?_H]pKZ("xIݟ:8|xS ޮ"W#t^XWb[ ̂3W_ih|C;-WshDQ}}i}U"_F?`lk{,2 s?oX"֏ W푯fкZ xsTl 3ۖß~#y-ПŦ.Y+*g@r4?Cz1hJě5l,a٣%o%՟`qX_-64>T% K-H 7daGoE,cSi$1Ɩ; ,6Y(aCUTZiWsh-+ +~ %noV -ua2jmfc_o%Xb;js37*1E_f$sJZYa_J~sjiJ楧~gKH]܄Ƒo_ClҾc-߃nEHigG`4 ~3䙮lVv얓˚: v,{3̕SO6S>M&{7i33!Lj`2Ɲ)6g Lffqڼ^>foօ,,IqڵRߕLURCLj?O{X7ø +9"R#?v +|cōZߒ9}O~fegffw9ĝm#%Hc>x^2 I>@}ĖH)o||Lr`P[ID$%ܡ_ʘ3}R_\id3w,Jh*=ɀFjN.5dFWW_SVZ~dΫ৿qoqgy2\k/VbTR~$Y1_㷤m2)"4uɰ^ͪ10ƜL]ϫO/:ǜ|>fڧ[ym2Ӵ7GܙǬ{03W)Aw&Q3S9ΠUSFsx&gvzT.L34톑\Fޙ]e>4ϝ=~G;ou= R":k6􀵀i?Z0)#cᨘ'e5~e0ϓsݬS?߀q ;RVa?αkDˈYj"|5b>4U>/ m'o})3kJ{lmlP;Sl2XL|z>^wzkLiF[Su|cK2z=|Xo|eiYԭjqkx>Axo||0C6] +Ee@`粃Q ? |5A\c>Uܡկ̜5Ylfs*6c^RͳlC3?C8I>ߵks \ f29|51Jα=Q AکIsGr9E̿v/8aMu\մ/0GLy,_8d-Zd u0N^fØ&QZmŸFc;[eeg.wډLL[_54+<*ƋZ_]"wM$z99eJ,mp6`r0|wݵTwI-d Ɩ[ꧽwJQ+>WIX"Uxno#;||j|4K_dߊ9Z{o"WT_PVe|Ń=^·-Ph:b>̹|zyk6-b^=4 |MPꎮW+YtX?pJ23xwbCy*_ +ٕܤ>x~ڻ+؟+" ]7YRPn V8W棴?șۧ߷ů{'3klt?sp_9o{^;~",xBy:Y +ώQt~`*1^Igyf>f͛eT.fΤnVL;+YOǑKj4YvM }g%3hY0S$/I?|VǪ΁iXExx4_'*tn^VUy}g+K%g+or sU~dApO'K?gg?ZY@9GsMeI2C'n3t\W I!j SӇ24Ҟ'???4_yR9_|U+ +0_~F=}S5Ȁ c_Z4_m+|v\ R}{<%4# +9EinQfYtvcH +Q،e>S(Lr},,ϴyߴYygTX[GX|JrF*s8m<Wpw_SQ?{JU+?,ϨZbb*i悗Wzҷ/i|,;!"Zm]VQhe,c(EYc56ns L1ÂUu,gK٬>&QaU L?,k +4_L.Qj|$_F +r-kkɺrTuAh@J4'n \_uW?q-L`/{=H૤T +8($r_u!NɡJ];cg\ee~Jjf42qM~d˂d{< eɯm^g`>=bojUSfr,c*[ETRt> +q1c6cL͠5aݘ3O\kN34Y3dy\S)e9seN @f^rdL?d::(09O\CX.Ϥ^37:::B~lȳv;CU/:$ӵB|Ĝ_Y|egfn}ߠ+|tvD +Z;z MsW2_W1gs2sq6sef+uPGt![HFW_|7|b1L*|5t1k +~>\3ΓW +TkU +OżޱA׺,˽wxmx}>PM#e@PĨ׳O͝b_u~vφ5i=UU앱g֎W`5X9+_Ֆo|եsTrz%9LxVh{X;FաB0Sdb,1I,R d^&9Ӷ䬙)mg:Leߚ5sʺ90}Py'/]$|UN W`+#gf)r>#ʌzs|Pzcj8Y6{[xX[Aц{q wɯR rrj0?SSA* +;x?i|􋂯|j?^ɦ|t6N{ꎆ vOxL3d^!&&64M+` \w`[d ^srXr:B3cRx:v3{ g}%n9X3|uosɹ\c >LИ5Z=i"/z@G] _6|e +i\QlU(Z.c&\&2tLa2f\>3daZT.UՙF?[6,͖ +K7'TO'1GoL޷)PYxc·6)Yg%q%c-]n>l&|-^`Om {xy:콢sU:k:6*i=]Y+)[_ڗWb Qob(4+z>Zz|%UB+Πi*ib=#o+x1T;2OЯ:W|uA'-0Inxcn+Y3 gs2x킲~LK E/;4njǶEmt`z[j\%;j[~_ur5̅ׄ|{S, f +ǛQ@wg} -?Q=й_++hq kɺB4:-[һ:>f|.}e9l,̈́^&L]^'g+K'֐gvL;2efp+pr_ ->s{~c9GjJrfJuW?p"{Չ9֩a+i\bcB&hxyI+ȽpevRɽ JvftSS߹F_]+?٩>Wg{$9#C`#K_9sj@x UZүKGtă'79HvhX ".6Uyz@|d\tܜ]7;_s(.j}9yGi 27e?u)y K̳ ~x}!j4Yވu+#R_9| ݫ|{f Pk|Rd|{t|HYUTFd_1=1ILpYvLjeyY3eypE?"ϓ٘n?ad D֣&Y~b5F'7s\Qkؽa{֜5 !㿇aG=4 Խp4ݞrOBmzoR\A2LRO`@ZF^*7_Y|յ},Eʲ*)Z0UH +K? ?9=Z櫺~d_-k@M4Ǧ>J-FUS_=oXV_oT׭U+ jx¦аohMM#sd}ƨa(&`V{5W#]f3y_- @|YCy{Bo}=0ŋ,Vo= +ȍsz6arCO[pd-jLqO>s}W +[vf*[WFǹ~ 22.~1%Ic2fft,[/+.Sܳ}9UENXs}딳Vfvza4Y}ds.d_2=:O%_"nWvUO*˅5jA? z=xBV+5{2%,|__F/5:WtHxYލX.#_uܣ2^W^LP) }'*5OlQ,jZW+^ׁL i#QVdj^{jw.}r_  W|#cBiۦ>@-Iz>!kT<&YyUb2c_ن"gO.{q>ꃇ|@A̍#=xF_Y|[zO,u_|FyaȲ3 Lisોdwsk'u?i5\8W=sװ2mI?3i|L%[ rY 6_Y:_#K]]ЯW9ɺEnYXTi:4Jh\sS=9@5r#]&%~Cvɗw +4G>+ɧt|;%^?脧hI k\̺BAm!\)t2œdLUٗPv))x#䍏3.U#S|z? `1WTg~]սק^1; r&Ʃ#1'6~xgyCկ_^9_YBRjYw.% 0+i #~%1MWjD? e*&r{K3~ Y{zL\[TR:|dCV̅yM +c<R6\/( +Ẃ}3C' m䉐e|<܁t9 sHO[/̣^y,>Ndo^yLEs88\kǦoQOoS/~!eڮtdl$Kjsy3_+V<2rJ'>鱙̘Y˲:3{<_H"$u.r\9-w4f-2ObQ^b Dvv\u=6~W%Y sE:]7+gv FqJ᫒UyG?CJ57|c߄WOߙLÓMLO+So):q]L[#{ xՃx/ZG&JU;bl˟o{O/sQ-(T1S1_lA4s1% hzs>։5WMSqw5E]g?P\2t楥 <UDW2|uXd'|:q;q=?{rg)2freqd{3{0KNg:e_r?vY:j++@X*Lc SeNV~r،=拾@;~O<)fbOq`sS̻}Ȍx(W8#~=w#}r]40]v6=8g6|UϐWטJU\YĺE?hwSMR_,)`}Y QG>l}_DLlQcdOfîHw2cG(4OV\j,9 I^Py0c YQ^kUnfbBW}X_e3+_Ŋ _naѮl%۵vȷ/"s^ аV u@#Ma`̃xsfihlF,Y?~3Y;76"6Bu dQXZU*ѯzzWt`__&9L2eyVzylKsYf=z}Kesa2ɝf|O}y9A3p2ȃm}ϳ;==qӮ(6|erQr|t|E5=j +Dx\o_سPW!L:Xs?E? y6:Q,Q*SŝVsZ[Hot|eC fy᫃7d-%tW.YDѱ>+,.S.#+L37UXW2[^ eZݤPOWeNɮaPL+6y__YC W?V +WltoqRb"J+xBeWX~0* }\k(7 U%j(_qfy5+=z;`~ >J Sȷ'2+J|+U|v|,Ж _|eku_"a2#4 0ȋ֑y/;<LπǠ:χ6P:qu|JhLN07ǚrv#; ypd>_kN "ha lWݗdyT_}lu>b92dyϲp,rb t-P0.dFU㫳bYźˬz@Bd8 so2|U,ޣ6ٵ) liJCFA6|B\W_ͥJ2eWUD_5"޹z(Jo9+/g0&.>Z6ƺME͜mwE}򉌃3dKcO&9;Oa w7ɬ.<]"/.G5TjFf^ _)lK>gv_i^Wϩ)ռ:>-|}_C]& zYek0K ~a|}nX.֓G=,gJʹjddZku-S$1]df\,%-LjW:w@}r}{yuAu_q50ǵt5Kz#|eɷ|eX[&}p װxKWWU}CW8˞W|{>_ f\_#+ +K Q^_4LݷR~8@-E~ UBqG΃~Otf%wٷvW-^j +3sۢނh_W._e3bjnj |ezD'Ee+f Zp^7F^7U}WbbWޥm ΎSF^+=qVݮ\6+qEY +Scj^h_Yy2Sz֑ފ-zx|D;i-mx`J_u/r" 5"{<{Ԍ9>S4EvxJ ;GC_~߹U],Wyj_YV&+G|Ug2 _Q5]j_uz_+|m^E3OۏZE;.;q)n +YzE;bZTP^oC&Gr"耏F>}I,PZ7k_#gda |᫬_nPC5¸gWKgw^;e+zq_jeϋ +syѷ0sܛ\1{z;7{$45HfQtV/xop$JQ|ΟjOz7S=r +q\#gj'cOץϣg<&y~~_I}\Ppܴ\G:0@F)8 7yĆȋƟa',Яt}kڷ&W xf%\0_|*_ՁWr `_\Eٕ_LJ?=U\+_n|uVWw6r(|Ͱ,gu`⫥=?:NƃԽwzFC[%a(?k`gV1pMѯZس\'דW6Uڬޏ\ߣwOJH_>&c*A=ȷwg`^z/|#%[Us[wJnϖȩ2U2d7 F`4 Naheou a^f tTOX޽C~|C}L?@ +_-yfu0h;5䯮;''k {"{}H r_kIX?30d!;mk;SH㌗vg::6:IVo})rXo l 92B(u{ԙJ G}R{;g_-_]|5!,|ク,g{Tg0WW:|٦͓t{I9g[F)AW;O|:_:>|A=2q_쨏[;dc૰W Ѓc+ٴ3|X?SOIAtzpؗ=~DF]ߦ<ky?Eq;ocg_VxqZtQIo9`CV;TM\6HYh!Dl +QgrZ;|;_]z>WpY|g,t&]NUذy\k=V_u2_BSc4ߙWugKݳv@,4,m$E^| 9t32WTzľΎ]c |^vBBj~*Fqn*U|_~,j~RɶL\i2<#Ա]p a_S$phM]7`.oj + + +^"x%uS? +:FcڒO +ap1,;Z?3@*/2;r;\6E5] jw2h_S'OtYr#N%ȏwB{o)A5Soy}OS.k^N^Nu2\KUJ;~~U0h2rgcz0#Ğw\02WT &͠;`|eJY#G3gee`߽3>nwT|yI\/) w +;Wl[%\{_STUh/R^qsUB]-8KwU^<Ņy{ q1?%-|_ot=4Do$=^J;?Q(VgDʕp`*w<ip +{v {Ysιs9$6ik:yq\j "I>0V쮪1ƚ˰v ޟg1GiYCф2w +.TGP1T:Q=ک-N_A5@-]8գS?_[ս?q5jr`ڗF?=X.6Qfڛ?َ-;hYEugm>D5 j|T0/nyGU=y0.tkGjI,E5~?ja4y ߏg~Xau(7Jb^;W\ssI)<D~~x{^r 8|} \,FGJa2v*ކHS'lSב:::Tuvv*VwwwzzzUҿ{o"_F2QqRBGދ^Œ\~y[Z;{_yOXr9d2K/6.]ēӏo,Oo_;lF]] Ν?3gΠI}6u#***PU=jF]C,F4z gPX3)GZw2kwQ?(wpC+rwRT +k{R}h58|S,va Wk_m@p_'VPnrp OGh eQȡ,܉X/*̌*?jrP]F<;oۯ`YF /ߔv;fk$Td2<tJ~CQc"d:6JQx<7oDxdN<4 _aE&fwi|ՉH*Co~V\c:ӕz}ζY'w'QG{돱a'T +|UF5ztV^uJJ,x5x{ L=HGgfw_ea/M{^yU+Oa,k99 +_ Vg?kM('0R2" .ԈVװ_Yݰ~ Q-ns._ͥ4Tz[&_]]~_ુAВ+B9u^O+t&ⱟd2)=fmilC'=rn^-:  :aܙnFԇ|6s~. g(1d~.;Jgwd~jޡpXL٧E'^J;9:s W>YK߃n{*F_>W\_\UF{+,@Ue547b#cc&7QR*4Bfj*GCk%A"q?~y\1q;a+<)7 +{a +ѭdo)tw)xYLJ㵃e X찆)\9+0׉0q1byX&Os8}H?g 0D&BʼF79/WGqXLp8 ӡ(쿛G~+úayLJUZ]|6ܭq̍(/Dž +}05q@4CIG/r>[XBpi%$obtmwgO#U $zc:DO}-5_2Jj#dPꩻU:pySA#r?@;*]. տr\oQ1^F\"%T1cT^}ďkB"KF5c W6T|UCF7QCgGEP+Ѳ<|Tp)<<ܑ4<x^\} 6K7й0shg098u􎙁#ׯ[q2*N7ZWQsvJ6Y5)mE;C6Ml fu寞64ɣp|&+dwLϒ s'Gd:d{dd:c}Mf`exOK+"3ff"_MInd=[Z5̵gaMxVvطPK[ziZzJJQ?|;I~t#øtq~-~|(Ԣ1 l m (k) gQT =H$3[8wپ,2}SHGs)7{Wf[xsn?E`!½[WϏd_Wub5!*_q8Wm76!Nvd(e2'ask3R|gTLyzާ</-Y Z[Vz.}+ͬ.W}j lVU[xIޒ޼~E]L1ٙD,E `Oȃ@4$/6>Շj 00׏mm yRw̨YV)IvVR_?Ņ yc]E:*ÈgxJ 3%2weԯ2έi|U*Ydɱ,D?Š P#c1gʺs>yS.~.Uzh2ՠ{4449GQM߯*ց9L_:kwQ3+c,hdI 䫊cjN:#_XRFWUw^{Y'ͷk?էqW~vWޫ)3Q 9> +[Yո$|.Zdl#ȣ$_2R2:yŻTCs)VuSߏ:y>_Pilg+k+v1+O!pE2cuz{{P먅3F*hA ͎fL|ȴeC>ҿz6.^^*nl`%\ڹK{{<'{{z? I]+r5BmQw(8 7,=1ۜZD[jliFfT64lM 93ʍNTRv-Tm'P58Hh,7YZGW㨴8|5,d ! +˹aut3kD&pcvd{M=sig̲W\uex\W_/ I75F3"IM+,whoǁ>tvu u_yN=f9-']yt S籹EGjC\ڸwar{[k^],&15 Od'Q1̈́*TzЫ|*^;lԺY=50;k`Rg26g#.s.4{q'vyƌƈ|,}=vB+[ cmHh5Bn$HirT$ y"]ae17J';a+~FעU\&-b̍Qo0fKZ>h=@b1GW"_)}Lռ⫚u|dE|/jl'"nm?l7~@+5O5􏒯0S~ _EFChI֓A~'!b,oJ+;a.f.z;1fޢ,:(= ,=z|.oṕ@f)bnylS]x>'7VBJKespm5wK~J{,;*/}]v"7;lgC//!+~y/!~e@&;qR m-!.y8σH"(r H 54֓R28~}hgOJWUrC/<6pHe Ǖ9 _ǨцNo5Cl}]nyj*|mckxoS~^j}owr 3ӘYY︄ōK1#ĜqYzpV-VU5B6s2_f ў=!gIo$\MN 05dW\GjfiVffYvdGȎHfecŘ_K &I_8,C>dvxѣy[`q⨁zUV{5*_eNe&71U_)_(~嫮IX8W_{Dg;Mkj0f!ۇ:,hhi@M8lBt=Bk g8)-mD˃*zUg 9IV'y~P}1 cu\]?;if`^!u 7$OM>T re: K8LPL&_c&V1Mfg@Ƨ18_>ȸJ1}ݬ9jiRRԱ<^7ˬsOwtt׮zՏ%+ݦFļ:uD =mҬDJgtwbxt} 38_?Kb}'}?cQ+$vc,5#( FJMÙJ2л R 3.B s?A?^f5`!rb% 4,&%}0%Gsfiւfx|uAmE+@mtMIs<~r{/`=Gc!8;` T|H`j04YK4+E,OٱdV^_ΐp;9sr 3|;\+_k 5%|u&W?|5ܬ4s$;0(gLUͱ,s0v1{ ;&-Qon@mS-Ha5oWcih\Nyһm/>cP?7rz 1רEGhF`w Wyr!SY4/Ss=#;SڷJ%<%a-J*̝vl#[[o_ +a?sV2+K~ʚȼGpw6nMܼ~]I_[] +fgedxxX1𞼳S,8LRxwdN6pe7Θmis'kHe^Ɏ2;Nq!WdzSiymn)*,ͬܺ1rkWO檇Eq:]r dG;؋CId?3p]5 `P<~}y~^` ZZkqIzk*FƆz\r)m )a9'W?^/}9ݺyUjWygk?#gro؟3"Ze2^~XP{G%= 3'3o-1'Z1445;+9i_.WKJ<%.3Q tUJU|P W{/e7gRcCSYw; %S$VxOϢ)#2-l|B]Lڣ[/_Y/ +{̧сeG1`"˹Ӹ8"&BXd<ɩ950w%z<6y]0c.Ga`̳8 +pYǻFkd1j#ܮd ,_Ud 56Woa_*=|qAMDŽtylA&+_~g~GpT}V +;_$w%?Wpy¼\Xױ{C_ 3%%cpU8s<&^Ͻ| d7`}}SݿGܾ<-E'[gCwSJ!L},z:xLR},ͬΟϰSQ_8i~S<G0P%?%҂Vc :k~%'֗igDԫtzi\χ|nDU\U>\~1fԞ:x0Djx:?LhJߋuK54^^mc{WHOh731%!w}!kA2G1d <(.ozoPʠs[1sŌNUC5I'3JjEмASm=pEaA+*+~K _p{ <^7]f8.֠1c;٭αލ`Yv>RWxYavkbui6Wyq616rcgBԪ̼o/=0'a2o'a~ }c~e+,[O8e⒖VOo _e |>_UwN(:_9v ZPEt<ԯ\1w0ʹAU|uy@f|/C-1[< 9Wk~󕚓*0-7_%[`eyy9풩&Tv I}9+ƲE{FV}+U-2_/7VNd߭6+n;6XDDiїחpÏ>*-\U+&wVՏfgO֦ d4 U44ۢ~E3-\zw&Gc0PnBi.C8qqWߣk ;T2o.|UQs,ii ~~fDKb*RlE#üUo0b|`ҍrdY+kXWU4L sK,1tUFM.ٳ^)pVgoۉsr<(d!G`Ou!C^?Lx c_"cqHއDy&2qWoNzލ-|* Z/)z1.}K7?ѫdn̷1l}qK~/6-+ZBpu+,B W fb\28:_U=W5^QYЯΗa>M2MP~GC퐣frZѡs=,Vcyn_~*.oobgo[- +޺+t".\ǬkXɦxFvy{o>/̛dF/B~ Pu<6 #RԬbutb`CYN`~}K[kXݽOM6EF&_K{x1F`|eXy~Mںj;Pnv̛Ue|-|5Cjj|5s RꞂ5W٨[铵%ׂA)5\`/Q MJC6ՕU=Ξ;ՠ̭$={ߘYM _U*2%n?9] |jx״ѩ "W?hl0^_D#2Zh[Sژk/pW7N>3drTC}JLϏh8«-0ؓd{f%}KބT/nƱCLqC +syY󝹗&ymbnVjOJz2Y +z#YJ=ߒ:^kFu+CZ-?qOv%AOsc=XX/-Wyߵsmcڅqc43Lql\& ]X},'}1j=chN9ЅtG +f |o:k~kv֠!Q!pߒ;/б=;0t)&<s_'/އ?߃g0ׇ8*P? TV%|Ū0{MSm&<:uV s;|c*%7]+U|WvxycsX@͵*.|eO竺M,{Olcy t5}&J%e~ΜCcQ?s=Ft- ڼD]QFC̩4o p] +/澻eGAKvI1׻d)aJ<kdWp<z?jK1/8Wy<&) Z<(w`lզƞ}J0eX`ߓW| /ť5ְ.ͭ/}gq̙R3sfC~Ker1 +gL99:a[pvu.,Pۻ&;3ՁCzh-&XܮT58ϓ,?=`jz3sXX</bٮمLMbhlmGfגn㗁lT(O=9H1(~92l;(*cdּ1>4|?]|_mPӬ1u䝮AOBýjIW\\ofS NZ*Ar݊j4__:1oa7+ k$ (f9-sar^\O<(%Ku+f5 +պ;FwQgM]Fuan2?0I]v . EW¿uu{<ҫKx̡J>a߆d5͐*]wF>cy3Ɐ6WKJ;H98: _̭TI3\?ُ?J;[(Τז7BzH׈o) .<M=~eɨ]Qs-?[j-Z&s16N-K m?U2+~Ą/rx5wy4_UjB D+z`I/yʟqzG +ӧUrcv}*~u>U^, ٤utUX+ +|?W*^\*EQt;F5T3t r s@[r"6nӻcmS~Hle~Wq&֍xzyce,-bv)LN`|zLqM`75ZX a'>3 ;~/dWn¾M"?Q폌#U,} S~rQ8.|w{|_~UPXzF!IջU bd|䮅Mboav`~ f<3t\ +cj_N4sjR =@X;1^² ^/gڇ0?SQte0}s |0<(,s6"+D+PFͱB2']+9J<%|59Ns_|S۞2`W2/|%I.&HжH.-hwidJ+a]QjX-`U9guzZ f=r\y~u _ dS' #2=MChy!Xpk\7=oj_#S}?QORѩFaQ+ Lu5g }˞ W26L NiU> bȽ1xYx# +gyit̕d⥄idhO:Jg{j%f}%ӳGͷԹL2aVUrvT$''lo^cN&9,ĸr>u\ -^f<|h&tf <º>&Y2aaůL-(}Lgx@! F*pNJ,ҘLrjd[d,̷r۶BG5C۵ͣzJ{`|+Wq}Pwa,铫{ş1VC]̫>gfcGKZdY_I2[9`a1'"ST~]SÑܕPW(|-cRF=Vn + }3VP[Sydvޜ`]P/CQCʑyef7PgSsqݤE| *]ɓ򠩥cNS~3"OU3H Dc(2miw!>Rpf-r JJXR2YүK$56L#I_tr̤XUMؖr]hC$ZXGpyp!,VjvaSk9_Qo_g6h|v~-}+VT>GT#=$,)DРW/Teɇ9ག_鎴.gY?J3),zYoSjɃ5vW\׸>R/uFQ{AtL#5vy~}E6 RxQI1U+2PSsXXiVBmLc0Ɣ_`xayd1H^W?޻_@hdE8=+՚dZ1}͙5>gJsdW~niO_έz4*#kOn¿!SXc/Bܑd -|˂Gw)Aķ;]pG%xB'Z$/٣RȄd~'GVI縼j.d|_d{s<šJ벦%TpMi?Uz\^SJzWj5U?U7xvsKQ~܁u'eO=B /=(Eʮzi88ҏJz8fydiKռ0GIAm]cM2wg `Z .W2fol?elLz@WsZvFIy6 n[틼4V2UOQ@YImL)*+_syeܴ'̖o0s- ZyD~rnM[:ײգT nj JMgInԹs"vi`GEM%ʫ/y?MEUzK^u'5PpLrl?+%߳:YXV=,~cΏq= 󥻨&#Kjl#[|0Ijᦟ)` Ŭ){^֎u9dͩ!+ 1-PzX4F )+kA<) +rɤyZi1Ϳ|R+mKc1SFg[Rژ_y)<^ѯcscc=)Y'YaV)+<|常k_\3uJt˷?Z|^Frq{p7)doyLըF5B8\&kd#'=Ve΢_~.^Tn'L-u&SU72Dh1*9k.7UP +9S_WUƒj5K?S'NA5yL4B)1uW9+ ~Ddž#-ӯ\Ahq]U^rc/4{閵 Lv< ڔڵ?dzs!?Vc=K}~,U % ^V+aa {gpj?Qb%WHR]kd~Q8B5lR 4z~_uȱ!b~nk++μ-%KV-Rjat>UCOpilFcvt +k; Cy[vT(80>O!^Yro!sg0Rm'w +~Me j}r+d,cƄ|7 +īT,&=.J$^pxN7ˎwmg?I6P=X=ౣKȣo a5;9y=-0ꃳo|p^{4r1zAN@GO7S~ND˨s`t!̆W2FűI$%^ڇq֤ΖU'P4yrfdJ? z^K43ɢHH }ʊrbVE%WblQchAJwϽ!pYaюɜ1-9c/(kHKkF&zWPVYW[j5Q_]zy)5WwIx6l&TVV^}u%/&Z/DK+k~dT]s*dE;ڮU#|S૨FvMVZjlm0|e~lc@#R:k9{᠃cֻՅ ju999:4ݣXdx6Ϊ" i,K#2D|>''T:ᛘ>1O{ǭpJt5}~l>Y.q8qgW)*k t"M~j2WUh?XgK=Uք/-(e-WUr~f%sHE&TMp +[zDxX_eƢcI3%s8߅`/fy" :^jzn@6_ӘZ/$M~I^Lz_8yv՛͍_1Yig'׋D?™'wN30ܷ勝4_InndF3B __/:%sqe0[ ڙB?"Qk)Jۏ6Hׯb'f05eW6T۸^,,&#\kw S>8SL2s0nf+Xlee0]] 5G(,N +/~%c~l߸}1G A:{˨,Wk-m&_5`U_^&ud *?R<T# +iPD{HׯBf_͟]G +HYЬig3+~eZahpJu- Ԡw]Zu:'l7b(\*s +ΐ./ȾbNQ%kp=&xԍVAR/Jgn)^~N8V>yrleރ%sySzAc!xk=㭋Zwl܅Kֵ~^Ak}Sz|uc#HnSXO^>gC2PYpϕ)=3&?.&Y&Kw$_}R̳/(٫ScBO׺}ݿzu慣:GU'?r2)6,^C^UZYN8ϟ %袖?<sq'%^w:Rw"k{QKgg)\fdŭqzy=-WJ52.ɦDGd"ۦz2tnaV0wSWպ'Wev+.ڄ/5h\aEW-DUW]S=ۆgPaiL]t?20%; F9hG0<9Ξ~@VZF9j\R<UOȊC0U|Zs8A&C#ql*+ & f7Wsd1}8&Y2PJm14+ CoїL5ZD(G2<20mK7`YU"?LȄaF6Q5'%2#3 OQNmdKGг*b˜Ri,%IȡE4k"typuJgCEm{aay/\{e?e}㭯])'_[Jg;"dƃl%],(q7F֊VQ=-yYc^&ȷgϪUznERt^}.3E &xqg, ͅ$ [/D>}accԭreNJ3WZxPJU&M8ZuT{ +ߋ{@cQ,ȸ腟v0&i&B9$wn'w#H^]`jnGҿySicdB,R3Zn+S[uݖ~7_sV{n-Ɓ%$b\{7AM]5ɀb,Q&I?. 3zhg4}i\Tu 4tMԦq&WXZCAxWZz5bjt 6;9C^;ˢE1!5[ݨ/䫖k~^ɱR BI^;I8q޿>JiCcXdVײ /le^[ztAF+g $W(=\G"*!N?u2ZLBK_Hd:L|LZJ+юW~(괙sK`Qy54R|qDYKxL?f~x/ۛA#F ۣ/Rf'Xz;IyfJ^7y,gǵ |CC2c8 +̏yD#%*_iXq^e!?&9=zx8UJ1Pݞ:1E1W_`w\YvSRL6!"={Od&LSȕJ*iIYHkL|sf0Dx޻Ͼba}1͛uH~l9_:^Qu"0oW|50Y_KM^ezeٿu/ǥcꄴLJ/%DJ/1ËwK.r>vZϛ8釟's笄k/$}C{ xG222/eǿ"~;g3+;78,`x +c+/qbN^D9N 6)58"sv9 ЯwyX{bܯUc^6|t@߳= +*j<g7s]Vzi7rnC[sރc+/5|&G.Ibf+WSv-]d1U51 ~M0 ͌3I''>hɕsjN$]N!#F0X>8CQyK^^ +oMZj_k;Vs_9>fz >-9Z23D_Bg ØGsFDcR422X6o$Տ%Jض߹[".H C1kޙY*v6?V9:|۳,}<~SW?|@\Ͽ>k;_.}89x &~p~n;[:ґƉq\p,m*W$W]R=3픴OWMHc0%M4G_5ه+Rȱ+Ǒ^3 {ޏ$rGG|ǯ3y>6s^7ȿSԽg"3h}=t՗8q]xrGm΁qLC?pFHtkzKoMײ+~oe#Sx<_ȋ?$?_s>px/0ϧ[ *uK,qp&8 n&c3e덇7]15i}.~)h6g&'vG2bmKQ |;pAQ:뽪|5`]f1< aV8+=k40Uc+u]\11=Ld;F泪.-=c\%C1jQaRid/6 ,-[3TU:O剒juMq^|_-2 aZo]c2hU'׿F1c>8A& +6V{A]^=gH~ؖ}W=b<5+C|CU%ۋ/ ܻ|ŠGZaAf /^^j>DpǬʻ4E2WAj|Պt캸I$rk#\gn|139 +?r+/9#ׄl }揤vX?>tKˤ2U`h}~0+oU6i'8<΃޵VQH$%4s>/9ĸ qw,n &Ow+_sBn1G?k5Oe_̫/b?u'>a ?ZO̺~x`[QDv~ OI Lğ^D``}>dWb2ua3 9(>Sɩ5YG=mI^MaΗ +Hc0w(O5ћjLr/Us4tBUZUB2{XsM"k:hzPn+c9?bCWL18^>q* ^#^¾ͩz7gxUQb\+L +&f8rw]]]k֧qDVL3TAzõ~W&5A}z{oa^ |0f7ђ$WD3N·8)LMҔxS%K_E[ЬR9BjV+b<\~n++ +2]apV|fߏ+Oq̽Z`p%Bx "z_>xZRE{5EvHnsڤ20#I՛wQok8O$tYlQ[4!|<){G^c,lJ}A`{e{1J`1j$Եt lp|[!_F>9v@ޏl@/=+;>7VЬRxk*2èRۖEJZ2V3/~*u9GwmJm'0阽+v֯XtMm\FK&kOI"6[}ɂqOA~pR0q6]W?-iJm^P DnٰfJ7{?vhrmmm]ǑjMߓZ -}(Gߒ~;.-ls,/Qfsxب>)g}x_`2N\~*+%qħ`Gоi8*ep4e}ʏ71sX7:-]ղpC#cQb@ ΁P|7`.GYM5Ϛiub#5{J q]W*R)a=u?q9 7ϽSQcKo snj zJcT%?T>t/yZUmr1lezL:jV(V"yF2}[)]Zhz/D6"kQ2/epeo0։spl(_1?SB_w( +>,0eEO:% Ka(j6!FT9z+WouZՔaD>.V"GMEmo;<5MMȁ4*s7'Xlr"V_\F~X *psREhU*zcjǐ2_iCVv해z̗v(dҹ8Sڇo+ou@WyI}Fw驸΁a*6,{H1OZjNSJsnPՏ^6)NGڬ6)NHc,Zm&Nc8 m}V/K[Sʋ=Mu^Z:->Cccy.dGu BHZIj ihmi=j}i640"`}Uyʮ^@òaa0*UKng|~da:?5୆V~9HzUUJiuz=c*u L$T^R|y WjVx۪\U Fb !!vZ}~3^ݓ{L~X>AxO|ckrK,U|OU⚽McO?8,!rxSqɕ2<=tlQXd/#_g0VUXQ^Ѥ+^C +U0u'ADF#)^{+`*k|UkRծyj+ K'jY:GЃtN~Y6ws1x\XuE_ +eO8l/X2@? ={[GzuϬXv+rtI~%u}c*=^٠ `{w V5woq=dWgEUy"'.ҬZugLk/=P<׸i>=YS6au7gHՉB>%zI0 fswW_H zm>\fP۫jflC؎5CIޫ؅2p#W17<&.vQ>7û!GKXL^Χuڃl,vsiW7$c1YcLzbyw0&ҿA Ja>Vr0Vr{uzFɨ[tTUø[SevE8o{wZsdYjFi ]XmڒK2 G +KOP"õѰԵH\ȝdq6 <6=}Nz'j ^FD0W! %3(܀0ۡt'zlՆhpy(ŧ*<ȯ_y_"\v>$"0+Ě%(#,Wڕ+NߒYFH3w0ryei+NIGNէx+ێ: 蘽7<@R D!|'\Y7]~xт'c)bg wæK7r斉}6,HKZٸ4qr3h/olü bÐSc 1 `uxj__9vD 1vX6w`E{Sc?y’t_}7#+(r3}hBJ9&$}~.bFE̟D:8KGZ+$XyX +>Krj/ e2w2Wߋ6\p[3>Koob^Ox/Bo)_ZOٓd&ѿSbs쇤k<J?$Uw Zk||͈JT_*7?W? +ٗ`#WgcW_}!vx{g;_!1 ^ɯomQt;0މYXXC>p%CqK~4Ƭ;efGsl!jL5DAo:>"h*3%|$H`ɳ+q.+O%co~+ZyB֮ Tafu 雇 4Oi)rLW:UsxO0BSq/09rױkoȶt m?ՃSb!j)ҽ79qQyχIS~ΗʏV[%YL96 n +4@Q?{x>T=Ʈ=.gkqLڐl&-~ [zT#=(}Tn89*$rt[һXS#ɗQy+|_zzڇv@۾7Kжl=`Қ!-|){'gĽO]A-=?я{? a- mc{?%_,ͺG Ţ%2u3>-ju/ћ} xc]FzY"i~b?$xX;AZ$ 2_ldz+ye syK{Q$_۲[=0omz,Qg41. E#=kv>u@˅^W2TlEbl&c[% }y˟xzK 3ȅ1oT +7>pU>h ~gWRp$=Sl}[ nk<#vU`x2Gޟ8FX6XnWVs-4nv/]ptB) ~D&zڱً\gI +C67_S#?Gc +*6UiW3tƭ~t9$ +}*W90' B&|E0WM4%1i+FoYq#O9zYа\Ys0w5dofrPYxn>gsqO5Xg՗T>$Oe9HI  +Y|rW}΋OFz\*"G1yLU >)ާѽnrW:⽱/ăus5K'͊TYڝ*DQ]Wix5,1W j]T|m[?T]+EsvYνפً NhE5Wk'Q1fZ&[9߃Ť̜۳ﺄ5y526IcuyGA+]0;t%t=R5nG}/WOn)Q;5&''zJrzb][뵿VIU~e/χ);C]4-TO|3tuX['vK}eoz`찂q9gX͞^}}{ۣzt_ʪ_2 +_ +USb4cQ+hŗ qb;yGЭ+ŧ&=%Tq>b, `t* + ?H +t q?ev"έ9,!; %\iX|! @C?r:rx˘~M'#4SAϊ9Xgh-ۨc<90ԉU;cgRxG-V&ƊaPկj4z|ef+#n8*]7:KZ=C_\NG.XKxJ*E|.=}V(̰X\:u UGX+[*![q8-W$t$+<?2W 7q35\_Zs#ҙ@ߌ$LA2E^0Ěd/ZE>p<zXfLd=[MM z= ,f҉5ki2)]Ҭ#/w1rkc!Ss zr>p _L yznvJ]2gTR)އkpmQ|U`, 1x{'R5g:פR|gWl?r#+5Zћb9|Un-=WyZˋ< +!BS\Ѯs'_W:\Sjm\+<ܪ4kZREb_~vvHB;%:8-Ѿq B$u99qcЪb\Ⱥx>|X!.Ѝk#c^>Nn0^th +U5XL|_%_*Z_8bs[m7yϞ\YYYYik:G<1 -f5NU>֒7z;*RbD$1yY lG]nz¹|@ S}eSGS5 )_9^$_ERaC8Z|VZ|٪U#R1ix۷Tt.EتW]g#UџQK5|a0J=Uv?XĂ h\OTOCaDx+H(\~ub7{DƐFiŗ>梨6_~U_jR+8Q>osLg;vy]nB^BR!m76o~zCRwc1V+V5޺&Ľux{]O`/U m1\gJ݃ssyܨء{9S9͈'Y@yp6z ~El'7y~gj{-_&Qyr8h5-*yQj_]a1ss3K}JXWh;3)KO%z׾b[Hn(+p3"|y^C3p * AWRRյ^i|-)$O^\bOzTv܆Ґ;ҵX\[k]=-Swg*cxNaky "W\ekZl{΃ZwPz=?ehME > /: +1d)pWZVe`@ӗxLUXS^~<,m 2{juUT35 [1|mAV{-woxʾ4r_Haqجƹk3;ε(M]eM+~J&]aȗe_ZA(@1<NKp~q;.ޓ3{^ +@Ǎ⸉IcAa P$yv^Bo_GGbd4 N[sgNzy YO5̸Z--5,_̳.YLbjuM^7] +<̃a.n9+=Dk9bO [Gq 쿀;Q׆[p{?xn#v z_5kO +Z2(=#y'8\Pb(|- ZWE+\V\eXlŲ Yγc9=V~eD=ysBܒ< < ϠeGKXkl孺wQ>jhĢ|B;0`L{J*iJzJWDUGRVejXU1F&e%E Io RY1ܒϲma$ޛv ٫dЭ̵Y֭*Q6Ev8m,f9-L۞R k1)r}_㫽g_n=$N]]$zծWmhp gqdD*4x+Hҧ)!Qdž$3{2Ode$}ǒ>yKȇAxđʉ#^&.Kx0k/W)-UVWs[;Y[MjX,7{NXo{o Ł71\lg}J(t:kLƥkfwT>G[泊;RwzG:R" ~|r}d+hW]^ $;"8Nj-t< y[Jbt38 81Vւs`/<8"__7mi☉4BӢ|]-ӫz5s= =55W?+ +Dm+|`qdC97`4%GZ˼"<x/52e4^$cwFn_4Wj 4YA3;2#hф-.^wm. =k_"TKG(֦׉׀~ל_u?/V⓵+oy^5c}~Dୌ9^#`/ t=qsbZl;e)cWTx?`=>k?!L!&xȃ~ǒ9u[GvRc;][$)]Xp ؓYqa?y}N.7O%#SX+?u+fuھVj5w߆뮧!  Kqsб^kwDe 'Bc 8VP.{k0lFncwP#DY6AW|-ЯS؏hYЗe?q]~b.R l\9h]Z_,+W6mnyԴ2WZP-_|*m`dJҰXuu'kQǪMdUW1'ƚG=oTzG1V=Ԥ2<;?T2y=,Eؘp +1!I^7 Z/%{cW봾j;WC9MW-_+J0V~2䏤|ȇB3c~<8']^r;vC\~Np:[ +go)DBnSNC/"WDm-zym>d=~?Y|c,"Ѱ_N -,I"gUꗶҹuXv<Wu.>040֮==Szko96}_W?ɛBn2|bg5G %/6\/|y̏&JzbJfrU.%+bb[}}oM3Z +QWSښZYY^ZWOY\b^1nqg86}!BߐZ}Gre a⚫Ojټr)3k[躱׹Vh%53&[%3^`85#BʏL(ީsEւ%G$Xv6 Ruۣ|Ú X"4k[ҵ \Eb)Z]UyC释SP.0JqXs9D1|([y~(We3 +tV:GHN2U_ +CTw~DՎPY,7a^BqBb>=dyBSZXS"R+}fM]S!d=UUodmRb`rF3aAOkS{X a ־fW\iپX\74Ŷ9kB>?ccV2oc(l 8Ϥmb$gJ%Xˏ%:_nSy$E.-}O kqYԞu+=i yKyzͨ)QV02<(loΎ-:)ɎkD7ئv3-sW˾3<~M*A +]Bqw6A!7I7ǿ?>c=IuI4&%}ٷ뼄p]pO|[Śn Wv6q΀%]VtbqtG"'dAxc ƓoןH!0~9nC E [Qҕ>kG _zuX7ՋX^cRG]T8N\[  +}$! \B?"O|&):hZm /HII=/InyZ{{W%2r_E!)Obn7#4VM3nVUujeK('{/.A,VhW;@"S].505hYwXs*rz{pЭB'b,p_ o|\1zF!Q2꠪z]Z; >N\{{"Qqp̒r6tWx뒗@_e|~bV6ss^^F +`?L_x,)Q砙'1c$r䊄axBECg%q\z'gl74]> .\;<> 7-Vx9-q.Nmh{9+=*򀤐帐ͤwhw&zD'/B785uDEN<.?/~yϥ2.yIg>#=KV[cZo|BL{=7k!cPWl7Ytb)ccJV j,2֜XԲ^+GCEaW~^2֭_}S{V +Q323iiZ2n¯n +s.˸0| W!Q1'/%n ڿd|& ZY;/o{/tug|z!ɍK{,mr8 e9sXBk y=S:Eb9SuO 4hg[ *>ΙDN5u.#[{-波1GC7!7!N0#W+xܙxhH`xPc1O Nψo6sHNoI<-ޣ{O][<Mà'q^<<؋l^4'DJE4ACX[a?0F?DSc=~IS}3Qv|%\zq&܍L_f-u s0>ctzc>c cfn6T_lji}xøW}k)b)ikֽG|,ƥo4H2y;{סּ5t?W՚fSUS#}UW0n\Y o+kE ocq|KT(٧*GbhVd*_]1wkۚ>cuK+%7ՍRd+9EU?‡.SXa&/uC#^~'`o>`;Vs/ ]ՓE=Cxcjݤ9j櫋MLBsTv@'/ֲ'_ <>?#FVNWXSڭ'~/*ݣ3#ZLϊ}|.qӒ RZo_*[c|۶m9{'Pܣgbcxػ[֑N %{̌ƹ)LܘwÇzT/K{HOfK#4{ERQKC(,MQy œA{ 3\F6 +܏VXЇ2! B f#bIF>K,S0Jg7Ȼ,u-bK\^G8KɂbUc/*~SCklKXC _"Jc<ޏ~7T_3_@?ZuTƎQod!>p]1mtDQfk79ffB^{2ﲴVϮϮJ>-U:kVSâ^p}U++Xȵd1n7Uʚ3?dzֹJ>5u7ݓcXQL37ȉ>dxqlm=& a~QkV]WHײ<0 +%~s[^yޜD1@nO"-kmNKc FL-<պU^BWOn>#|=C]OhH +O\ c>R|a7,/k|!sփ|fld- : }aECEr'MT}R_5,zJ=4D>VJ?O +֌i0 J=:>C{?|̬ޓ-W_ʭ__q`_=OUcR|WkW<\vWA^Wҭ2Iq8~ö]\4u_9ԚzڹB<4$ݶWH {YT\#t0j^cy8 6*MJGiJZEqrQۤX*e +Z7co;gZz>Y'5P{M>InZ=ykޖ&~gU}0+oso>lIɏc~8'^sd8S~F zMJz|u]ak')K׭{E\k׾J"\{[~Ȩg폪Ͽ+9%/ꕞт nWҿW{/qK,j|M*&CKMZ=a!\6|Yߵ`9rbsa+Y<7J{~1cN$[}buGW>$'$7X[m~r%u7z&I +kϑ +2l 3(y"{&Cڸu' (!G +4/ws9Wq@>)Cs5ajUdA:RvJվ;E jJU|7 V%W'Uj̥Q"xsCoy.@#:Yv;+2P-cVܤ5qG6/3NY}b<[5jtƼ +s/kz#P;zx헳g{9o ?I:Xɵ/y6p"G5.ָJ2ŕ7+s>|y8]ߍЉn7*|>dPsK{—j́lϙŘ6F4EmNJ)51%%dUUPWIz@86c'nI5 $ΙVSb-=5 ȴ Ex2Hk߁}hXRRS+ߋvT.A͉sOy_ (ISӵ7NN4W{tNܤaۼ mJdJ;=䗛#Z o6?\ jb'ιQ/UOׂ LC_ s#vwIw;Eg=b? WJ*?ʂBʟ.J49Gzm ?(ܠX1MDVM-ٜ>r@:ƏJs6sF:U`->6rWEbt, m.jX|ĉ΁+'4n㽲{0W:U W><#|Rg]9G$ގx Y8yW59QծUrZ:UƠł?r~m=lk3]п@-׷ث9D/{B鰄 +>pȀ$Iz~<)c$;{]rJ}C߈<.cW%s$Oޖ(|}8 b+QPJ8eSݺIlma*{w6zn=UUM+s1m`r>ӷfvh~9F>֎:rYc^/WϫOjA\SFדn<.-MiXYa+Y;NrNĵ>kVp`B~,EJcJ_[mQzQ< ]8W΍wY__>$㷡QEԬ"О +?,*W{d2Ӣץ8ꊸ_MV+3_8+=Fj,m.p^R/o[Rb_NjC-k<a0'Yh:{y{<ФGŏs%49%a{Ki[{<3U}fTKڧRz%2`ȡ9czp~bcw9Ŋˊ/w~q=9U4k5Y$9`_ւVz,}k6Ok[Itʮ@x794lvg5k'ks'}*!Hu(IpQUcU}ˬ6cj.sn>ZvL$(Aw^/lL/7ZvP^Z'A +{yjs>+&3W-X`mGOCӌ^`LQmoO,3KMHGQ}v푫ؿ8scy+}-dգv.Sk:$/uy֒P[ 7o2v!GxH5-`W'9邏=>S|쥷п=AhZ&?k&X:zE`܆+;vW'mT52_TfMwP/q|}zSKq-]T湨3]Jk?I6م9~t`@x`N9O&."!Mmzw7k,7%q.=,@޺JPn+ZXS&Jm.>XZ88Qq96揥$h*|dFqQTcrNZʨ6kuf5u<`Xg]mSE=M-Vל\.[]߼1ua`>sv^C;H^BcGZϛWb+ֿUd>!7z1#M~[[3gZ>+Fmj}U +mzr3Mڙ_j|Gs4r\ت/u-V/_7jztT~XwxK$ ??s=`O~7šiOU3ت+}a0ۛ2-d+^oz#Uci\vA;Ź{eV'}[2f<91\L5.y|~f$ft=kk&_2/yx Vg6lڄ|j |\Z7 npk⹠YASb-?(N2֝bq@q:/W%z6A2w+k쭧z*_k={8~C"iqMmKnHEʏIWa{'nʒȴ JXJ}zI"=[zaF{QLbŰQmʦb~L'ޕM]jZMpnM,afzӵF C XLUuܸE:GJ3ecٕ &J%xyn୺K<¸́2`/FKI"?(f[l PwZ$0TMԺGNSQs![C^oQۂG/V?_w 75^1_A!_'xy=ԯ^I˶c_)Z!ιuKLM5ԋ٫u)n:ߟ#y?RM|/MoYOӞzu\пb1f=Ulk+ + kOB^\!x 8g&)5KOlCcbMF+^5I-{$+OT~s$)ۻ,ߦ{z!*rYRBY0"0 1јv5ݳ=JU}8 +/wþύ?,v ? W,t' +a}ί ldL&a(> >`>F#*aK sAKOJQjLdߤsMa-58a|!CjDM#l6Fj,10.=B1v\}b<~ =yާ+J,/ƊV4_ym/b7;#}~Cs]4Qb{cS +<{/c9.Ycq.T+I8{gQK$ʻS^\/& +lk*z_Wc>X}o;XGO ?k>p>`Sx>vrFcS)O|U忒@-kE+'a=߹WFb*[]vm9M>(1Ud_%>SaVy[mR_}M$pLpq8Ra )x'MOOĖ!t IDO^@u*.Fv> +paEQN0OϘx'L>Q7%?3JY(g⓽$h%>Y.B8Җ >灓u6a6+Bn4U5a F LR8''FRsqMƣB/-zK.ɫCUd8q=53(9vďԣϭCLpiA=9rv*W#{i-X;RyD-y~g-5Qca Mw+y 1MҤO#WWiM` "TQ\/ް0}7H'NW6Ykbtcu_'p1ͿO`X_+$J|5::[s\Z -؜Ro@6$^5{R K4GysM"e9}'}m|lKOoߑʿcH= +ksJ?q?X磶r|wߗ "`dFkNcFmBqRXَF7ؕD(ؘ/)9/2YѥJA.a4??w_KGVh1b=&>xm૽F61~g%k3w#ׅ2,_Ti@J??q2>ԽRc\!me|P _1!c$L+LY*dRK1s(zq-|O>/8}a~BΚBc0$kѳ(LP/bO76#s0 Oq 9g# .@◱#|OngMxUqsgv EHhl5W˝دO"Q(òtҟa/ܘ'KUOQ~*z<׊}xrb mB}jǞa ?_Jja0yȼd+/Lq c-VpO1+(1(R*d).Fp3szQ]fL6)Xgxa GvװcձL. ?U (oދVj\+_qx7#xȥ_9/2RbA~_YEs4;z?WrحjՅjL\^ЫZO:RBsQӢd! j)Ÿ$q)t3O- V",N|I\ָ';a98rGR<.k ux xbb^#N~3Pȟ`(0X:4M$Trp'p'1?1X]$ v#'aW>7M/^eϢe]$cK,x_MU^9V_vjMy,[~mbq%hN-UCFW&mڍmipRb<JpϷyFH\2o{ʵOϜ&|̽%HM%Q lU [Z:c8ggg)!(ec ⸝=7{iY\G<|6 /]/-sc>+!ͰF|Eg["N_ 6f~iV[mNձLW㿕&'Lj_*k%GjTYu>IS֘9 +=M]@ ?E<'F|a=dлiԪKc 2?D57(ٶWZXc!`]Q/l_1dt|0 1 q<-}_ jf*-)>Ok*Nu}IXr \ ˨<^=\\k0z~~Y/bbs { GCmd77jxlho8g/9Tq:'۩udknv|;7_Cтȷ;aJq;]w\}}g)AAuj8ZO!C_q.,,lI sz\3;e_iCXx?Llս}?Mk9Q{px+6c38nFǓ5 1M[IŢ)fuǮ/qWa/|,ȑWwLjr,na?׸G2Ӂ|8E1o*S1]g&+; ,c + hw!R%F\JλblJrT[혉׶Gz}  zl.cק;Pk,<H`=mk&Xz_GCE$i_@b*z_W'UU+3"U "5ǦPzᾨ=w:6x j7+_ lcN ,y.͟B2zW &U֋T9_ _ŵm{juӴî~f9ʘ`9^No^bPb>gqcw^A2b;# ڪ}a.7SNƯ~ +mRӈ_Rޫe/M lm V=Vj5:VHbܞTCSw5#8Ƌ1&i21W/5nm4f}7Jp}`@Qo}H4,UDv&B_4 #K~nY}Rsz?^3x /ռ6?UW+}MƷ֎ H dXFp&=45^kQWX#'`psx#Y+:!vsa,@B~s'f>C(D.3|$^kNoeZM.Z^T~4ЏMmV~<0p/'v:iNRGF*|'B[鄿^o>W*W\/8Slm}ۺU̾q+_fsuM^1h3٬p$YiQ\^e { Ƃ$܈(jjxkNx짵ubTl&j;I &5?X?]#vddl/ cpV+;k3(Y+ڭNz]vsK%~W.o//hxN:ՠhMz3?߃bzLlM/Qxrn/Z(F7oulW-^ Lރga!V>ZFiOU&'QOP }G= ͬw[J_-f8j=;2|LG]Y*ƮA숗{,F"9r뭻V2/| S {My.!ߖ??Ӌ[oı~PYo3ֶ_͎V0G?.֩cϰ{ l&t^GZWɵXmg},rß! ag(. VrsSO>6KܥCk@S}knlyFio9O}-ź͙iƸ^!<_DoӛI*98j( p\df(c8YW-Of-j3w+_mcrVpkk mSk7ԈObD.ɰmnn;?wˉ߹YZ$b_W7LHzX9ybABN؜pxqXx֊v\v2Jɓ0/&zI-\f%?L +oTIKs#O#Ʉ4ieKLl hBRLtW:33"_X{:Y=gS#_[5c@{VajL}P +#ֶ^QLV18:(ۭ. s1"a"!M4hvPܨ8jZ,&ںv70Xu^U-cZj`y!כuDٜ".iFeC)ExX؊T+H[?B$iX܂fM3Fmf5g_[29n͍v8j޺މu>gy99Jפ'$\a'ہT:~| +*V-ƴj3wSj/G[׌uӍ2/IjX$b#O={c<&&Wo0wl'/B.gΦ6s=oWd/!2~m/z~z<^69~k`Zpx!\@6ƾ<.c)m7@%810 s = .eڣzߗif9HU#J[=mW dh7:cF2p0N:gW_R՗`מ0rp_i~L^[C$V8ݚO1sh2ԫuҌFZjuۤ%gO3fXk2"q7vP|q_=A0۶ǹ}Vd?߯9MlO_Y>yJjyy6~*?ѓw"K\~jk- +‘0i/ \ku9$(n؍bNv㢾lκVFŔ1z`jR<ՌPSj:3DofO0rYm: GC'\31dw-ӕ>wy."moYMe5b<Ьz:7Wc ķ[[Gg 2kի1O@ Q`x6 Z?'X ;dDm=>I`?_55޾y/'xc7o\ pYY^aUb{躄)W<Ьܝm4/m_֫9bFV% kyFf|v?LJx"lY_؋xqXƀ?B5X"|ioo^ׯxx#͘K60cxp6ѯ߬YU%@ngoL-5 FV0Xc^jaO[R)C\ѴgS!Dr!*-cGvEM=U}c!zG_(s|GGwpM]gNPq iAyW?+_:$j2u^]Yxm+x, +_Ҽ}O8gwm@9KE䜳 9awy~zfȀ-lCF3梪>::;;̙3200p/zzzWy;744ÿO!Juu466 f T.u['EuMIMM=v}}H?O\K\[yz\?| wܒW.ŭ-Y\I}}!"mmm;ǓIl?W~}yY*m}['VAz}ގTGDgCe`>74KiiӽVY!}& WThت[G(GLJܖ?~{/޽ׯ^ʋ_wrall9ݥEN{ Ɵ-:>Ą~n ++1~ e}[>c#>Jv%?F>`{}SxKy-RT+h8%]jp{hGc%%q`dkὼ{J^<&>{u8'++233-#ek?<2g8ޣ !v;:yUw}_n|ߣ(ng綧##"'e=g=׏x/R+-nGݿ$/WήM.>#evMyһb8_M/rɐ^=Ptq u]驩Hޢ`d?0=mc.~;;hΎ6[E[Iښ~ossQMM"-mЌcщ菉3ߛZj2QU߰]z}4Ʀf<~>O .[-Fc{[~%[p,Hwn߿_>_W{D?<`>^d2\8Ts-6K%er@u4>k{.n=:u6[46Hc}TI:lT~(j~oI[prOx,A`y{p3>odC hw _K7.+GV~bMg3'pڮgV\ 8 &r6i9%=$ΖQ&p9f~vuo8y-oY+Wue.% B` zKlInה';Iuu ǠA]MKMKM_^߻{sMImOoul,*Wv¹8gJVˈ8qnw>K?ךm׾9ߎ({-Cڔ7?Gk#N Zx؋ d$٦QG2$_-/_([7e~ sZ:W+=H<,{%(ur軵4: +HT7K `)l샞4byV3*5q5vV♾ ❿"k[![_#%P矈o,6}ɾZ"W_Kgt]y%J\~)yf/ⱷ[^aëb{׌˛Tm\*?/U |˛ުkI~~*ʩ9SUmrEN1*%S~}2 lbehxsf< * ρ6QC?ϕ?e~ce;ƥVXoQ#kj&vxǦ_9x li V'= ??||Ko$&fAw˃M?K̓[y}d:U!>]c-|ڗ!?" pw<W"ۇw1I}/;I݊AG{Sw\IŜ_uEc86?O6#W 5 RY,)>=v|gFVD8]Y,hVv=ӒȀ, z{r׬;XfB=ep'/.Kpxz+W^H*uD.>0*Lx$kkd5UqKVt. bu)Onʨ>elL|~OMz/S.h@˽Ro~~»&_RxϨ{K.^Sdw7ex-hv86a^M`lC9%:@CTyV/doe~QIO ގkM~r:cǯϰ 9~046?>NۦMܧ/{YޣzGj?KоΝS]Ν[ǏO"Uo:VFxSo^{l;&kk22.+R +R64iWdyG䆿GoՇ5uqOmz/5~$h'ȿ>6DηruuI;4!WIAg3+U+C|2VJ=CT``.Ǹ'~N+'<9Nob3+իZUR +_[#+&=D KAenf4QEfCk^Tnbx>Am c;Y|ܽZ9Ե}=GVx`򮾒K%gDɡ+oG˸|_YRɘs2~;uғ! }-dZJ&?-WQ^sΕȴUإ;4soZ%6dPd]΅y ^s'|"fG{ .f k̿cE Zڌ kX\S2GiWO%x{2U_*R=%MWVFM +zŹF9~hW`?W봕5(O(rSJ =b[ǘW+ +CW]CAF\Vd8JAd+V77؟ޓlhRiSI,2[nVR'e7z1|sa[ʽeOtoI^HE򳱗>*//_ۈfW{|/HE_[^ +Ծ%'Q 833b,zF={X}KŴ{6t>ff9Ӆ5r xpFi\zgŎ Yqy7KdfredIL)__9 U]ͫ~Ej #SD٫gxA/IU,%U6KAl[kaݎ4̼ +)/+OK`:+nw[lQwη|rܻ }[>ǭ^k[[nXk~{SQx=, +>_Q"'=OоނiȮ߻{W6/\ճ22*ugџAf8gn!tE<$sr:yiNYO)qID< fpԖKeWdS1+[|ENJpz+|̵+WI,6` ʙHJ 5 ^[E/Z 3\8Dd~#Μ Vnq腯:DLXU"O:d,fٌg>w~U(.۪x7]l{pD1N^g,_~ !߹oun򶩬I_uY}#ӽOj{|k'YE5I'{v1}7d+km{8v3r󱼦Nr {7oڔ .=H>)Dˉ /m,=ж_i\QJ$[-Xz}_A j]{="ECm c䮖[٩t"c^/RU4{֕aԲxkdEb+jXe\"j]y ~"_W_ sw]կ+mQ" {t^yғ!o^3(G^l>;loӞ}4 u8][RpGJoK6^dz?)N#띅s{-2y*r峑kWiLd`/u_eBRPГ\<[f}_"_=6Ar85in/4!fZ^ F:2[̛=RȴocGB +3J"W^kv/&le~DJ{2`2 Fnp 3a /v/ 5}?q9e.d-^f~n5(jE_g?{\o0ON`t !՟G×Яr._XgjQeOz2=XtoI~ #|AjddĜ7ܡ=9ӷJ$O{'Hx䃥JȆLKΫR1].ׁc/7780lKB6խYyx?c"uN5,U ༲zqk #J*\Ke˒E}AoJT%{ ?>+[Mg~o تW{X5JImdW5Id謄&@OEmJs8-;ы}<(/~rq.-R20'#Rr]jJ˪kRtM/h/ey9J1/6PςOUVG&}CK" ` z>THǩ<m?{do:0[YE-'7:E|D<̰Ac2z|Bd*r  g^ ϋ㑯U3y:GSGIW_=??X6+oG|6ܟ!e3:˔בYk Aޮ'&Ǚu* YF谱Y{$yns${ p. rN݃`czFr8*Yr>3`9x`hJT6U +ݒsR38"CRP|\x]졿E΂? 2?Oid|2 2,7C?+W~^62UЎػmWLCړ, +[hvxZlD^=AK=K}A_1箚ɰ.n;8ЍwfK)1Yo2V(ZV`xF_asUЯt9y(uLU]#R*Ꝓ!=A L[r LIМԌDzbU*g6lM|@r!2W>_KKx O튺1[Y + 瑽J֏%8 +~T`g>d鯅y@ο1~ ̟w+\.0ǁkñϋ~O3XdHaڽШ<\;ه Vsg% ̛Ote*[a Q8 A~+@>s *_!rVI3jJ%`f~n_kI 'suj'>8Yי:Su;YQ),,hD/!t_rEK/ؒcT2{f )S&Sŗ1#(:=_#Oߺž\FR2v^h#|JP;+N*N͇RH +?\d"V+=)ד ݈/[ :'x& ! F&=f B2 +/x d#=Iucq}x~\8l DHY6*>w6i'p}n(nk¨y. ټxn?&Bck O J`O^kkO5{2t d2}9<`򉶸˾1/T Z?hTY䅧0>qߒz~]C16'UӨ*t>:J_]_՛=EyvpJ{~E?4,P ӹ̟Cbg毒:{6Pj[d%zj_k:`t9"[L9.f x>;䰯&pzu{/7l{9}p%{!0^]'rA h!7b-z[1žY̼͏} (R`*jMd0B槸1j]ׁs[!<s|:&>BdBx{~~Cqwя6윬9ޟ3۟[rU{[74JIM=jF"੼s/$l o@xqoZ )ȶO%+``#+j:v49`γ1|A2Ԭ[\{^zWԮ,?UggוiwC5d,d}9nAsZ`nsI>pv͍s ]ř9`jf5#t[9CǜC1x)w7>FR׬ڞc&G춋x=RmgcυT6^33ɽ%FE͎Y8O@---.<@k@OQd4hf@n1C O}sqW2a9~6毱N*k%uD"ȣ}L=O7{mvKVKL䐲ÚZh!8|6[Y,ff _-+ٗ36^hf+y])_dzb)벃9wr /s `3mp>~+5H% +λH"^& eUQ[f5BW}t>ն-$!_癟bHw~kx>Όq9Dx}Cf͠\?13hZQSD%yؖ=_%{l|Q)vܮ*p~زr@leW<#(Ym?xݘl,;g$s 3XS78oе4ƲC5 i0g2c= n#a)K1 GgU|[昴W M\Ґra_7;pfx<{Hcs5;KXq&q>8ZMciظP[uEU'R }?+_rW\^|9eiYd\ol |ԮerORcS`03Uadan+< u IjW?o{svq/'~}1z_£`U00Uմ#3hI~"WyP 5,EMxp[gzQ:t!hfڷ4[uXpyKf};nCL!U|LXY`΅'2Yv똀w 6΃OC:o=|Z +rMطȰAx̞Uxr̰cpV2gbz&\\M`T 8*h2\B0U)؊E"{/̿D>< s^AxU\IW^W\9&g+%5~=?Q>զ8;p/b,Wxd?vh? [E;/E]|FrE:) 2==ay>Y${Qds=ȃ6#2*_'||sbT2k5N?ֻWk`Ojkw^65l!xWsU.yp,XRu`*0oG;;~E ّ\~ŞQ_}2<,Z̠M\.x$Z1c<>`VCd@.4743s- fNR]gO꼡]A~ 9]Q"k)[t"[/'*XyH!n|}Nha? s><|@23Ίg*X9&զUC|l㪁<^_Bu* ~lI+As: +|=j|{b/W *SXu`0WU0y={[nobAQ\ag^nu"Ț|؊b_z;򊜭+[JQ/Uqn])v?xEg}\%Ξ9hne->d)uٓ{{dk T[YD/vCtR_}󷭼z;=hF[%?h-YZΈFY%pLpT6.jU}{AGy|eaf4#^,\| Xl{`rOg[5r`"GS:T>x!Nܙ{dαs)plek 9+ 9M:kxUnB  K{gA 8rW=vQxcY5+];H*fڙ!#9w44^ ٪ lUu'#g;+d8KgrC"2W7^iU *;ek/ i؈˺;R=Aw~;K?S"k~VNi2"[Uyop+_Ѿ+?~ ޿R^|z(wn݄xYYZXɉ տ=eeuV'CQzCtG<.uQ/.OC?X҇xVId#iUIj_5|eVsӪylQ̛V_qȥ83YԸ*j dyl\ pSs֟>bow%Osڧm8}2},zfzAlͼ{Lw-'Cq}u ~s =TDZGn ʏKxm1˚t~µV6dMuf[IW~` |AWF5*%}voL7brx~`"ۋ>->_ي\}Z+]+O![Ŭs|7jƝE97<;;+Wq 5+-1մvluWJgpaߌW?PL__|*;λ3{^%1 A8D ZV%4-rOث^5dw>ĴxB*b{;ʁdܾySܿ\B}9)$얧:㶓Z+/_˗dksC=NiQyE5=Oz29uى}(,ڏRi=G_A8,V +jY\U?$g>UK@~ɰ˩:ɨlvGjү4Y>h?McK[ U_M]HT' uɆ"~ۜ-JqFGQ/G VJ,m2ћ}Bf~ {a;#xL evξ\n6<-ٸ|E*J!rdq|]vk_֑CdWzdu`JbWXzV5+ +2/X%^w1iU|٭WxCjjk?#?; }Em~n\k"QqV+a;u5E|Syxܽ}K=ju{U'Cés:_KVKYB$"rQ9*<_WHNC +M2_v K&_$mCSҊU=VUfWWnU\9wQ@/,ސܳ mmʙ>\v`Yս 2#ԯt r٫m#Ԍ /tAb=vAJ5 p5kE\m#WUm|iCr +Re7 07%I^ZkΞfd_bR0 BkKgYՙr8W^JSG;2T*b|-- VG]z.YUw[<3\;n23^о{DVKEytⳞ,=ؙ^rw##oy'z!}*ddzic?bɒ|Mo_/_{/wܒ+{02:ɐ~V_5'~7dw<{} 8=o׌d䖣*_G֪JN|ૈF_5JW{WY ↗pf\Gg &dy8W_t>r*F-0Law$nX>UZ8>GWI eҼ9,%/*=1ª[,S̼gXO<:W rƌAծ2`|kRrC)ػl*Rxv2s2h+6n 60{,;`eqK.cVkpQ(~.qȪ 2.Rokk?jotլ5%.}Cʋ|04 +ֺ:gfvC9 Rhyb#,ޒ]J?d߃߇{;k;wv].^#/M7xtI13E޲x&icVƺGh_yCO=5ب }DCogVo^_<? #,W!6,dW*Xvg~Eb]9*uPڇ`?K7z١8Y)OO,݆v]|*S_~|;3+.dn MƢER ++ȷ+c!5'^n\75mdϭL# )Vn d_=+hH1v D웳oFБ^BsFxMS;wߘY|LWKnc,s%ubߋLhV6Oy yVCа lաtv+kU9U}߸jXY\?Xޤr829ݒU, s$nCSm*xlGJ"Я ]13sr+ uAʘQC3rٳf\2L=4,0i႔-\' 2sl(woPнTcx9p`9ʃYw,\yQ ړ3pl-#y_ԑ7><0,?=sfzQ +up3=#"f/JǶs^/]mC6%F`/J2k #ghjګ?dA`#?_O\z݆>U52{rst߅{`Lq-ҾYվz}89{]? ;Ԅ͑woMZ}/+si?AeիW.a?SZ?o={Ly{ٛGL5ot~&TVlYXU]*? +XEr:+`$^7u yY[u(k;fW2tTApv4Q!=p^E* _S>~*_ կ8ԮboQ]'2zO79,q7heӉ?4oIyq,/,RE?SDap)m<@ٌ9J?4Aru5Wm%ߗYQɹ,%|?I|歒9K~;#YD{T{&V:Αfsj_{;޼y]2N_=~|j<:2ԛ/WsSmŠ .rV6%Vxּ!{y| |t]8}c :XrRg"^s dM|-[e4ȩz "o}A? +,[-sWF/uQ!o' xmK ++|;{s?ĸq +p,B6lEV"Cr}@Y,k4*]{U|T|eg`+Rԛ y2ZGܕe0t+F >e{m'TҞWfoUe|4.%[`Mb +dСQ0P` #CfMZ`g8 d^9vН`9nkeE5)YXh{-A8,l c +Q*)l엔 +??gHmiC9]g"Eyz~{e`S_wk^iǚGe7|9묁98oϴ')QԾ,J䯃 &yr%YX^yi_6|VuJ9<NFSy}|Nݓ-_}qUsL2C"9qrߍNx7Cd}爐2wQ?R+5m(\N)nHIx$+\$??Rh^uLmlU1,#3?a*[nWc.ܗ^\Y\9vR+_g|R?^Xޠ_ASoPsge)뼪e1'1l8+y) GV*Nh~@.矢(WE~tPs +}SnaxG1fP3s 2/@^ ᱕meZ]wW+3q:_Tǩ-Ře毸]kM[9ҲqGʭ>W{W^xҢ[);ek"O{͚ٙonׯV}:5dU̱޼z.w1[[*}SR:,!;you>3EZ\c4t\|vf_G#<~헱\o`=>>mЏ=~ Nds5+;/*sk~uWIEmTKu^ꚤN:ކ񂛜1e +Ԭ>a+~+GǸDp ld<^u\5'Ye &Wj!UH0yq?,WE?W;rUEyL ԳUAkc]ghm2Y,{ge# Yk[cK{axVJV{]>6r6jv,5z.d\<' /z|wKqkV[\8K''$_Y3U[VLz^ߡ + uF`J5:TVf;$sckk\n?gB'`7|gV>Gsr,o~}T/֋E{o;ZٯOTv ^L;*C_?r*kgv{v$RT.-R1(U`1LscR1$:Yge1ߞY.YmkM~?EkH%9e\j8*^b|5y4 ˀgO{r=e17V̞0,F&\5 'Iuނ9Ȧ]x= {Qf|RukWd*8X/?a,Wnl]׮μ:vezl+A< _1inY}i~+=Ry}swNdۓ_eBznLQHt|~<*륮F*_*F'ܿ%CzZxk6;?ㆷz%rM? d0>;k7$;9?q,2; Y='>&='f;;}uߢhf{*rR_="˗J{36Jg FJ8ߎ_r +pkΑiix*\U9,சQJ-.GsDÜn Dq5⊿sNΞrB૬:B\̰~PgɸjlQ}XbGߩ =3k8P_q !@L ܕ2**ZTv f% UDZoJ̩dy98uS~b<WP̨}3WF'<:ЯW̿3%leiY^JokNuAV>6{YEpW" WlɽjǤX}4.^7lnMKiڇ%7*n{/E5RR׌h߲.trniL]Mݙ,E?LtfbԲ_yQc$>7of{!5>{GbYc:ru/IsY1*5}u\ys?diZZUX\"KLY\sRrЂԏ-J^3 Fc`dʻ 9AʅV[CtiKg%O~{EJTlJչRu핔㘔p +<w;r;? 7/iX.3jWVߓ!yEӫ3y뷺ߺG)`_ 7{pz\ q~bYּ _xқ!oovoxڋp]Sp8ʤ9<̔a^c6N๐ O#S20'9e.W .+p] +z{僮CtiTh\irUfI]BjWW૱#5>c𕓙,Ԧmz_?^9U݁}`x<r'uM=ύ}dF; "Olκ _VWåWe{ dq߱,Mߏm"[Wۨg͘w6W̐~{d|-o}Lg9XW+MuRJhIf.\V#8_{.Y͚ji~:oORiA͕RsT{iVQ5N\TE)ﵐNSX_g]_Otuh؜!Z}y_Ub{kqvYi^?Yۜ/fbW};f wb+jW8Z<ڗ] }/AR\3\Ox  [y:S"}UƪꛔJ97a]{j]5v|"Ce@߯ČU\uqնvM*q[1 +:G1v.H,OUܚ6sP_sXs Ys?~W2cCebYQ>έ[ϱ;V1sQAW̶;"|kwx_U7Oе2 +̙`,Y6r3}ʙ52I6λZE;d2$#R`(t(T^SU8eWuǠ#UrFrz%:9G};gߝb>_?݀ucSr{sXkBmWq< r~['бinȷߟ}>`\ʹ),״kveE]*]Yޅ+аxKgVzul??C +6f +B+ 9wL +~eHvJy9F Q7>.͍ R.MR|^oK ِ1xCst]gO/9 [' YſcМm`''gP֙U +UcR;9sɜ!!qu yO #6oZݻrhgea~^{ђϹO o?͐A+|L,yU ?O nUDqjBJF?]%W2+uHG=Y̴۠9}JզXvQ;q}앍-Uگ٫W*H^eȀяw'e/gE _+ Nm&[]Ld߄B۩WYm,{˰n*WqbOzy힐 )~:[%MPGVeVGt<毘ۏ,oPW>Vg?QfvhWd2S +Km>QbΝ++0w`^r: _e ìW)'=Ӎs=y7'[ + HvήsP ƨR=g;iUTe/ +OgrOEY'8,b.kn9^v;>([H\>Z?m7g{= ٦5bZX޳%׮^F<}H/k_i3e;yS\=0JgF i0K٦3R&"7BE̼ +~WԬ: <(PCe9*PZq[XYe0N#cO_.`f+bJ3c>~̘gVl+8?Ox~I_gH}%}sSaiaAG杨}уcЇo>0{%7Q_=*_+}O?hPd{ \̦yC3W:2su +l_%NVQ# VoH+WN:_w_yΦ1d%$D49slOٝS-ZB%0x +}ԩ:SЬ8^>R"W?ܾnB.hTY\UjL3pNH6汏R` 0W񔌁F[#G/ryW.Y4rRybrx\fjP犯ЩlŊZ}YHVle_a[de&n)_5Z/zm%_b)~OMzLe +9ս,7Wm~W% ?O=U=Wge5p$tX,sm[ fR&_ nWӃ8Ba nRfTuڥ{O/6k+RzvepFWQ/5H_d]0+zl8Wjso6ʹo;8g`/竫8|_.^ЌѪ9Cо%ԇirK==l_꼡o4sr-g#IkZ!/[,''e06&oOD{Y/d(Q%Nn8珚8+;/{se:_K%:cv,}8UV5%p5OUd5F*+ݻYlܓ{\**7ze _ad*jdvUwa 6}F=6s_jskZ_9CwCҼ Y}1<5Y~QsXW%c5@f3tYۮrpډ35e),;O`|5y_A3 +:+at YAg~P @Dl\<ЗqUF7WF;=V]'U6d$jE*`屲~ܕŠ,=G\Dsv {<^㽦o-kor#X m8uE-pVzОB9*θ!Gi|YVjYXSVH +ZVu|d3+zWz3le+d('O|ѷ{K<*99^Oens&=?I'UZ|-Ts^ZUM=О6 ?b՜k W'X= k,ރw{o4ۣ^ݱvZ;ʜ!y4X2xkdݼUh=~+oz׍הK;fch_=%]2a y?B`ȸY3 +%q9_5F[ЇJm?oME/7u7X1S{WаJga򛺔,k5rI?.齇pL])ݯ${g$A\F^EjV60st1=*SJZ+l_4ԝcIz׍6 +i>uRYV=glz=_Ԣm^1o[~7וKCZU~}Կv0<^?XߠR:'G+p<_yROI?qތPFj~ tWc_mԞ~Bmf+=M3Eܜ7W$:Q r +J?![~խ|E,u֢U?ZW/je/qv33rΠgsUB݈~Acᷚ-JS̲fzFjB'_r|}t  [>;tFK떜ڥˊ7w3TOԟ|qKBWv\tQ|gV}%>{N1ۿUMJ ).aOnefbsnu7p^N9W19toz}>~ԝjq ]-ي3Vϙ^,ka(>.[UyPN}0ǽY)p{[U/ՋY汇y([TՂN+{ΊW=l +?Yڕ惘8%z=q`yee26R*w<[sos_){vs}=rD=2g e8d_N/Kkc?/&=?˾G=Vʼ!}Owذkj96Hn'W|T"wQr_ԯ'ղC#2]!+_9uʪ||5kdvVZSl@bROϝNPw,mp~ÿA|nezp 릨,X!ɭoWNHb k;򎘋 m ~/:c&ٳ||uluFKc~()zvF-u Y^>R'G_T00[_.~ /ttMX 0U8pEq~r^gKբX,'|{7(=8gS/~k7~Ojk.Aþ"!{D8OgXat|~]ԥ lk\29jC2ne2+/r| ^f(`5Q^Nkȹ>RB\ο<׉ςsՌsÃt+U%v5׆Uod3;{lWY\Ws9穗 +W~.)j[{.:jY >Ѩ 5՚|Tsg/c!J~j_Of~ 5ZVډcm;Wd( >Swy~ŚԌJ9r-^|<2D_lkU~51gW+mੲCz<| fI2*4^k'O.Uԁ`fh 4g~zw^3aEuૌ\{eq8V?%ΥAn0z޺lu_YQ&_^9u7 ]8zA6lzfDR[zWU7G@f3!x[sm-EU t~p\oy1! `{,cMC#5Bf h䎚=~V`\ǯHU32 + r( |W1mu`rU7837r"wWVָlV3LL?\=Jkخ5[|'JmHg srm)xVA B;jʏ`OߪvSbkAa*}`˒]:i3O_x$#X.}z~p0l#_R9)z !e7jijS '{\\TL*p~5^62Z 5j`Kv c;tEf Ԍ*|626 VUUݐE⾁z>9`+,l`}tiz6'lCWi1G1qp{q_ޣn9Ɋ3O5E˔t6nYշm&_EKV^h;ꐯXe AxUjJyMN|LyJF2oڹ?<]V:t}1[q_%_qr[lExBZ/{#Y+ľ^W=`VO_u>\9?Ԧfy^dI O-J%gWyuo /"!`kzSZ3&OcszV~ZϽu>njhe1Yw< m6}_/ {Ŭb}Gf{yϾG¤X깜 T!Yz򫷝߆vү8SylC+[ُN +.㟘6] +j`'3r,Rلw烇=pk@gke1}|>JNIi7gV-Cezpl'9wP;ƅ:B]WjX&_R؊Wm3G':a+$t< ^Wf&T'$^Ĺ~)-_ռ0P7|v5| +Pg֛1U _iS儭|)/CTsWie2qnowRhT'Ņ&Kk1hT!·:5'ⶇO[9*&zlsFըȵf[W{{z"+3DeUyx= }Ǟ3Ԉpo*_R2 }3ݹG3_1GTsFh}:w%& +A2J냨s&_1_3WY_튎 ~8g{=/|"z`}Jԭ?? =_]Uϗ|e˱J;gqo׭q⃯ L2 ~Jk#?JET3^̉2v$Ӂ`> +]8b}ў߰j +^vJ +"YtWT૧:+g?8?O$%cÕO|IFJ2*AԃD4{hpPeW]ԬЬ9ު8l#p-TǾRVnu,{Q6ꃝ[F}\cׯLc kޭ+OYdveTxS%f%2˚?0D :z?YצXs~7A=؎{ZcCvYΪ9C{*s8Kڕ+Cuȼ jA̛#j_uyCo_}؃pʼ!ꄆjj֐h?!K/|WFh\>҅^lկ9 +<όFS⚶WMUN3x&j^R+/d.ѿ^6UAFVYge *2UZs甶u+A:3J8%gI9ŅϞ-8u!BгFOߑ o`qܗŠ1j  k[}uJgᜮ7W~ywo-*Ź3O\IʤY보 +Jl$tRFc!I%-wڕ>C}߇u@ەs8l|5Ͼ9CZ꼫*2t;lgja^nI&m9*_ռG7]8u '?KfZ dTAoό8߂j"Ƴ|UB߬^D+ϽyxywCM }>gʜ?vݱ{n˜ύً{ =ǾǽxY=1wb-ݫgtr7;|Ov^2WE}ċd|5֜d7O +|BSrD{ ^lWWy jlWKҬx rjʊK{ʨNtW-Z1hu]8_c[m]~>hv͐M}XAF#`4ً=м&/?µRR^I >;s`w⬯&|35V'|ŞA?}+_V?YwB-?Z rٙ{_˖S2X9Vs)^}.yܒƮaPY~L6?݉ Y}QxvMVvbCqUܱRo!f|%;_)feD:֐d>h^:૨eoqCmW'?HF!yXQݟUYG +@p&M]jǺr GUxv0wn6 VYUu1qV5Jg2O6kziN|fM1pliȣ%~ke~^β^>J\~fNzeleU?s`|W_l׾Zy뭞NJ?ܖ3DO9sg?+﫞nk_dku}ּ!' xuf/xT?+sk}/=!l;F3v<ڗ9C$KscQ ]2 QaW_|^3DlR/+華K ɣGrmQ<|zt1!WuAu`aK2ǴOǥw0(f%3ޑ-LϪ_A_zo_Q ݍLVZAEJWԄ-Â^DJkЦʃklͥ9sۂ)P,#m#km/ AèhYkhW9;*; +Le7끆NeVw&^9\ *?6'?Ç_*ƹ5&k+<_܏h=n6AUc_pnz1o||\YL P74;V˙}{/cN_S2le^ Z2Y˚|7볝jW&OTxW2*oЫd!PxA%xL]ԉYz:Ohm{}dxniyNeQ/ܓjˉ3o^=l3}Yϴu63]^Dl3~gbݑ X+\E= dS K45s22W•I^GD=G |Ug`]m/Uz_yWic3j|aUuZѯܦ'Yԫooէ}edaiu|&8?5q-zeB͊lq?|.<1+'`yoO_՛i +=6f+pc;|u-ˇed9kG&U'?i=oʃ2y/G~O8 9^h2Xl!͋tr,ɺzNgוYخhJ{W|t6_d/Kgkmzg;;%_q 0a +UDgeKYu@f釈\~ 3&|eڼZ`ɘ >,kN?w1ciݱьFޜ!~x\Ύ 1[ޜI_uJ7D_} UL_wtȬ*w]yt3낮OK<1"d8c0 p{ZԼq>#_q|̊jPdR-_\BJkO|EO}\eFX+Y=[=(}تW_.zgY,ř<|>8+Wp؍k%_V,cҧ_ _{sg>f<:f6+;T/߯[ѼL^}ԱҨ +`,WOXrS\QU#fC:Y7ݨz4pLg5 g+DnBr8 x^xuWlº|eXUl\IpSTN+XgsѬ *͜-jm̭gev;kEU*GK%|}2/o|?B69cvv}FN<=9CFS'}]뎙9Co4eq_ֲWe1oq\ۥ; ٢dO5}6 r o4 e鏍j3~߉Cu_h5# +ȺYG-+xBL*|O_y_uEFeb~EubVeL=ګ~+9Ju_uC&8_[gEl3ٲUZ*=Nԯ81_"b&CG: bb-/& +2rW-TAV Z+|řJX47:r6w|/O}39\1+$3aQc=l_Y~++\)[ܥ +g{=бvef n2A+o#f0o8p/_]̢i/<w VU~EU\5M؋U]+vFʱhs\0+Yي9Nj5 눞\{ +sU`y->͙5Ź\|.#M^2Ig=]'ᇊ\ao+v_蝭xɘ씺KЯ{W=:~4}v|(3[k_U q,wks9Cy_ޯ4䯦W;>_}/vy8 +c,q=g5tr,:Z){LjԮvs5wlڨǝ{PcݹmhƆ|Zgїz5־`׮eكN38 CMQ [_k?%vS&iױ։u>:K3&اk^<+0޷;~NVzc*/扲|Q2oهsک;^r_V԰ߎץv/=8'Zx6+ӥVSrZ5"̼궍^j.=5|7媃fMZi+>'G|V|Ճs &汵W2 +~$_t Sbm-aţ20comgVǡvC̒PZ7g9jsׂk4/˶냜|Leu5p37tnwe+0Z;3'\ż2| y/%{=nHًi} -ѯ^#_g3||/OϽϥ՜!}\s۩}Ys^<{sujׯ;6Z_YޫЯ+YSꀭ+/HΛ +_k;_Ԙq ykq`*' ;^W_Qzj>D}luW5i3F>W']yd\u퀯p#7yM6ӯ%`+>\wv1C1bm;>jnvW`Tu(>98u+U;5ZU8䔹28*닿Zો_iEI gX5;a5141]z3ΑÇ+s޾E[ 37z |QjXy5XЈ$[vWn`Uc+\7N1_Qbx+R:!]×N%-~),skpnUZlE>60cZ9WjЯiy;+{}'|o7W>!\3 Ÿ섯8w_uC[8֯zKڬ e3``7'V9x'it8vB# 8TX{ ~ +2rF9N=Wj>5l1B*tfWV_a;wQᦺle1,,7>3F=]\.Q/ȑ+0 >j`<4=[7svB\̲阯{wd_ ͯ4j ;}N}/;pDcuΜis֐<5:uRsD9Ky_^>W>g\Eo߆: ]a4KY5m3'_e E WWa[s6"_ѻՄiNhZJlqtY"Ο=8Y|Eղcjd)cƠ^zV YG{6WR/wsZU>3{])_ѿ -¹Ye@ڛdw0"+/ !nF-EI$/oMO]ꍥ%zDOFLovW$] sz_qNmju*U%j]g;,{;lˋ}y!ek\FZڔ-WO|̇şE~ͲW%)WtӻCȾ_ZN=*V֡Sq.ȌK79b+\k+M?zP2Q}{1cRd*g5gnv:[qsFٷ-5CԳ: r9g+ w:¢LF|ź55zf͌zۙ;]xϝս%y-NJ+$u={Պ"Mz2ݖF6x\ќ9ɯ/e=)$ڽ*% 1)zO(>;k|4 */{lgdeZkjxuGM_2kѨ߱YCWޡa3A/ | +_!X3&_~v"8+3q_ /]3f CU| .강W?^I2RO98'<*|+ܒ4_թu"{j_MvVⒸ8ަAJrWWimWFoFOߐ<翪݉~(5:R^;J%~ji܎k{$5? +{%;Wˋ,.H4?/#Y\>sNy sV._YZcluDəf=ebdZ3:5?ŪUZqڗg[n_sK>1[c;Js bH ]QO8>w,ol=&mǶGםNy;(|}2kލ}ׅU(MI#IS9iJSۋ|q RLe%528yg%{ZZ9 ^,)[V3l&_|OUJ3rG=J[բ'݊~bf*:ښs!>wWxά:g _]}%_aP;fMcXS lUjjS|xT© ï ǫٚ_|R+VJjFx3o+i-j1 ϕ0W<[૵F=Ds^ӺejX9Wm_]} F_1Otm[{TNߕpЍ/%xe(1g&:3 OCIIwL!~0d/>0xJxZA7{C= U5GvC`2JέH(S0۫WXW4,5Gg!ӺNIz夌,(jA]v8+I`Q7HޗVS^^Sϱx +8|X,?=b>f_mhgxH9.\QMD +_+whDڕp¹b :"x ¸z{7pSΟ%=疟(?ܭUWj1_ v:-.z ,+X: juHpߍY05A3hjW:W_AC`Q>g_X>WE|μ +p {lfPOKrdDJҎsr9 Y\Oi+C:~\u|)MJD;_x̭SjW8خ=7hװPW| uz#yN7::|7z#SUiԯx3JuWЇvhY.ިRԨ``7R3mg{y^j#VE|QV'|E\։pv1{vz*5g{$boYwP|OrY2 ˼^Ύډo||BpnVx/7>+&OWؿM7ׯр}GwW7ӥ>%2|}ۍm|1`%p I2,2W+d}k?5S_wc9SM&V97;ot5}hDj̰T-G@Â/~2Oy%Fmqr3 - 2s5_e=aKgֵ)Sjy2{!C4>-rv +|v~>khtG|sU.<7:&d +Ȳj:t:-ԨWl,mf)=_ؐ+H|߮|uzNpWԭF8[I3_*VM@;e/瞓{ch`[[=_a+.sCQm_1-~ơvg6WS5Cw%VXYVfdWyK2'oVjV}8oyax8&+Cѯ\3ʕ53VQ5z!7yfV*f3J*h^1pU~b+G:~C2̏} VrU{Q䠱mC65>G~\=zN?tIB+J,ݸsu|u wGOz5X,&ȰmY]YCyOzYge[U9_l?}b-Yu7a:AhTSQqM?tFϻP+c|ovS +u4`^\ve]8v1+/+|Vr_M )D"8G{j{!ObK6\MK,m#lǬ snXuWW332ɜ^CXz8^xS hRc`̩[1^ܾRBJ_HIpqA9L~Ħɮ 3_o{f1W{Fv;gع/5.g!`Ka(!BM紆;Gjڏc03~jki`~w|f؞&Y m;^!ף|U_Gr㺘y Cф_YSSM%>¹) l~EYԨ,Z_}Kgu3+6{τkfϑjr/UTL}2\2Q;goU\9y my+jW*Gvۃs@8SN]mwm;6:ydMp?s7n|vJ=hWQN۩56_dضf24[MYwꃸ>[]Kk$3(ЭfJޯ%I5ROSS4k*?ΞOT7˾.+g:_oM6nCٖqrUXhifQ%Sq"Of1WYwxN~Vfg{MG"3u?Ll^irͅm':3-aVu +l]r6O?Tf`;2Jk_ƹ*zΑǚ3CZ soW] hjIɔ% *cx,ꥍ^U]>_霢hp1UYkwW⚵-]_7oWd_.|+lsuPnoҮWs]?湪+FY}31AE+k ۍ23}`ۭ fa^D"`<rܒJ˒}ԴH -|O?s-j.uWixB}Wm\UYU 0|n(tlQn;EW\8ڝoZ϶VԺ_uȼw}\1#_mqi}v(Gl6b>Q(_ Ͱ6PzwV~Es42-Js:拺3fePg0 +ˋs"o C2uc :aUo/jmB͐}xΎ +R(WeلmU?XCg55+bv:[׮lUѯ&<ؖo))h&{%B݊+񃡘PF[4wG+ٲlGԂ%g|5+]8czmnV j|C'l|AkRjG2=YMwitAG ~[BΏB<ɼtv{T|\(vȬESS2s%T +V5vZ~;tQGHkԬ|&o)wOcA ڸfWo/82Xg7[o}%ˣV\e{v}<846(~\|r3yj.k"8tuxsUs9Y`iVvڰᵌ\Х:/%_W^9}瞰y;]X*Szԙ?>P89Gs|}fՁlQ]iݐ6j||+k+CW_+f1d5j2|Iիɶ;h&fi]5+_՘U"c9+qWv\hQayx^3N=|LAJOn|5B??:+񕵺xM1sX\nj5J_FUǮH^GH +?cWU4g.23E[}Ŝfo+%yCM_F xX'Z5yCk}u +lg_}yWcsW[󮝅x+#g{q=ׇk[?=U3 <ΛK3pg6(g;V&_% +|mWCW^9_6Z|٨|~=Kz|\QI>4ЯBߵϮ_i(sGA|ejI՗Y*ܣ2m6o2'Q}["_qW[?[|lWo١Ssc8+8);W]OPG$7_7_})oZWKgc~@M;|#CGob7MMH|UPa>7>GGL6E"*M튳#X#`43ZW]a*#GJhfI'n':7uY)_y9 +^@"_BZԲx WL zJ\|T|ˍAKێ+vz=1j_?̯ nP~E.ap$ %⪭;Wє ;]WZ4nŞG+^a_S._r+S|79~^ԎDM;Dr6Qg +|WYWÇ㼽&|a?5)|5yΙDY[sڍy!yKܹ:rS3_J2ӏ|`Lh2^ڋ3(cm]/eJ^K/{80V%_g]>^N?VcWԼޑ…X$G2a쵁y4˪wZ cyך耯4:f&:V +e ^"pM=x>H"WY- +hϬxW]-J_˼NK6Zb qW3W1X0b2:֜e+YR,`0$B^8 2+VѠ5B|}\N}<=98b+Pg WԪ&Ѐ_y{|5 |ej~nhOOz_q%{)+M:#Ԟrn_p O"*տӧE[0>e ^'5c[g%Wnf6dyɳMJsjRҌ8g7y 9~x`$Td5yky ]{Eja)Me5^} ϡUoY(e~+vc_^mỲ-:~1jU۽>LrA"4 eufru@g"c>nvY]6+#2j gqм,V5,mWg6ArtLϝ߯lXBQڕWG]lWA)g|> }>W)SEsO+ғI0K*[~ozc2^v]w#5?Ԃ +sz ITږ ׏Eِ`XvKP05iJX4aM+gj^ɹ%NJxD.NZ`[KXkd[e++ZڭlQձ[UQ)w|8ֽnUw+yꖌKR 9;_J̩)I 蝪l_]}}lQ#onUfq,J +\þ+QoDQŵ|B||;4)[뗺Rܿw]^؉뚋Ax]<=,obZJ%4]jWV}ك} c.,BynoWٿEvlԿ6̀f׿tA| kcv #g-O.Xe @@Gٽ&ҼdKо.IEb(Qk(RƂ_lIمV Ixrf_n@>YzU8ǻ }}]#֪eh=dI86i;;SӌXgم`qx[}١#~/ěuioe./y0ST/O+=tQ&X#wq`Z=*qCgůkK}8$w=='zރdnBNP sr7sxѪz,k-;Gb| v~v.+ 9N#DWva^l090Vs8;0 d32/lz< aT2,잗TQR键XRJHb+Nd<\1'g?H?(cc 3 m+*}apHs|\9h||x ZZM.{ba~5XLV߈y1Wd-e,_|턯_A37\xXoPɻ_IO8$gjW%uAH(-h.3%~g)5+^?@,\G|5L]?a:,_~kSjXCw}HBzƊv5-Wܦ-gFկUo\4@̆Tf;tW2|ɍciRm|:҅.xYlip%SE +_\W<3ݖ"9H3 "(@B([-r=3~:nQE`[?ϭp{^g\n4#Lb+nDDv$p_EҞX'%KWWttt\Z ?:;;yif$I6痷AǯOusQl6+@@iiiõt*-dJĢ1D" +/-.Sv{^."aw'O%&&K{$ Jgd$l"" |iiG@A w%׉ΐKI`tK<@g%ӛdwLba W7V릯;W!Ψ]m#\Ѧm+Ï6:,q\{>L&J$NK6vu~cTrl3)M/b}w=EZqO4CRK*_*]bȍc:"q1dG ++i\R7}r [ƍWҸi1 +Q\jI=n5!n/sm' [0ܔ5i_[iYWGӫb\~"ms1+IJXªoũ?0{}v؉ў{CZ/[_b۾㟤ԸRT;bڋMORL.Hu=/UBY鞒IxptJW=AW/YnB֧8~8I#$u8}) 𧻰MIir85nbCeۢc-=wSb YFbtEE*ky*'uҰ/ 8"-=q oźJlw?L6pR &:bWrRvh ,O^zQ{(] ŒE +-)~TA^VL_{}C~5L1m![U&c5UaĐT-=twR6 CP F҈{XS4f5)p21#ʈHJgL=ҩ3mX떴J蒴Mߕ +FiRLUhf<-h Niĺ-mY}&m!?"5|e|*_RevKu | +6DIvK8__W +&5oK:#8_kW-dGbZK2̝VP^npjl' +G%Eqpit:0OXyيZQ8yYU`@K9c+~WW?lz¾ԁ:#g>bgkԨcc-Cdz ɉ W V@󪃖>*ֽQ<=/b%^lu5]^楳aKo[8o!D^㩩MS_z;wgbZx(-һ'*ޟ>:c_ƽ;D)L*vb~<ϧvy ^BaWډ )-Jt2򑦵wXĠb%֤" zsmҊAU1oX*0ރ]W\i8;d΄S1(|V|ެGdc?Vb3z7$3! 1+"UP S 3<"^Y3K$Mc;$նhNQ%YN߇d/ŷquK,S8mV{Z1I.R ȷzPy\ Qd_XVI_sANuN3tHjpMȣ +z֩|)>_!?X ~1.٫<ޅrU!~;#"_aݱ@SXR5OڜVd*lMg xH:*TO+W%uRnR) 9̎YNLRvM#\)ԭlЦ[F]yjWDTG_}^IFF^|UgJ蛼W |?sm^*^ܥXsx.CwL`'JTаW5#L^;r ?U.Ӄw`ގDvr2{";w}GzKםJ݁PSC}3+- M#6&iv4C2JAZ\׶F7I>kkV͎&&DwLL>t!؉dq^2_۱~N>Џo}9=QQQQ*媳Fd38 ]tDy:#`!hw'%S B8|O-H]΅1/ +.kVd0M4ۣW~e8,{(*d3 Ug1wy|.5b.E&/׻JNn9cs8N.Y^A\g1TCz/WmBn}sL[>t'|gJ+}ت +qˊJG)|ݻ$4 bOWRO~vSUFmO=g'#Wq~ [9jV[1 |䀴3%ro뮼xD6]K1ܔ}u|w4uWq|RW`KؾcO+pV-hg^1Ȝl?.B/QY\\@o"[4ʿ@B$ +"{1#q+au1c!Av~ڦτ ?1oG'iANr̗Q9`'?Qb.JyR~'t'ɥUS0[U g٭[Ьo߈wj]LOWkti5|Q +DOүRA*0D|zjXR+:Xo˟s)"x|;ۏq}(ވ翨}aZrbN8mO?Ǖ™ +A³ZgO+4'|[_|_'`\VGx+0W3qrj e$6q*GW GXM8ߎ`%橠SO{!* -PAUJC7{[`7E=+OIi}lNWT>QuyBj:nx>.[Y&*93'hf d,+=|uAbaESEp_-I|aQ K{zG2>jߜT'=3.~ ޡWб;hk"yWnՏ||PUM/%?5 >4&敧ҊkoFy\׌5A*}cK l:iyCCo +[_l +y@͇^yCoZSQwU Mj6J[;\yI>ȃ GY yHm{"_#WS:cilP1mAZN$,*gx8C<%ENU^=X?2J0B'r.S}V.MIb,+M1U.ScJG||ub;sh/{O|֑SΔw l ]#]JRy=9:ws^ϙѫֶd+x?NDC4sQ>/HR*Y4b}wGz-ȓbORm?3wk5|d`-Jn1AkWW-怮]` _WM] */ߡ$dyyY|Z~Q?N+oIr +E?hV|kIEYEKgEU[nj_-KKnP"5#>(=\"W r5R[ cc"w?/4:Wu =ꭎV,C[&u@wNI%O%ܔ˗1isvzn0+yv +|$3؅|Bvg[Ʀdhz-<L ̈L.LtA˘Y[cFo˝KT{+Xc<Wݑխ;&7+k~w]ͷyWeicAL/M!&UL.Nt/s:r=nlb_A_)Q"n_x^Lq|e&_m?!.vY/ɠv&%Vru(1 ozÆ])]ֺ]fk4Ԋ~YZ =7K3|_J*Ox(kIL+5~MFZ2?7[+y0[} fpd9*RbmWW71|ulmW!UO4 +\)Vƻ$'v7ުVXobUL۩WHk2ڢsuk暴|Ef}Գ־7VV7sW6x9Ur /?tMǃwbC^ߌ "coX`)_Lo;0.<|=c2G﷋GvS>tdoR=~{y֟=oHȣw5oW#uoﮊ.lw.m.̝i=&jͺFuIc)/xuK|V_i|eh˟]V1’$[s,}sWϨ\}c}jpSn֦Kl1|5{ +ي{p@Zݥ~Š=9ep\AO N-*7oP9#lu!wm1+uexl™ܠiwA66NAgcowQ`> slMC~&vUgjU'Wo>Ɓ D ϩ{k]1ڢG Uz(u6ZF~oV*1վ*&:$]]{./Ďܹ}٤j8~IVQ>+m2F^X1taG:؟~l[]?@RlVNEv!I3+S'|VdϽwe{oKq=}"u`+W هLۊ8ΣmC֊,C;3J"r]N,9dtvTGfo1sG-`^ taȌ35/գ74=ց2fŘ-l܆5/xFWmroYT}#A+둭xX w\`Clks2M2<3$}*ߊ7~'Mr#CD}H#LkC_]jw-`P|D5~/3UR[D$oaOZ7>Ye| mjk|EoIllZeNC[T߶f_WkR]x rM6?4"_$k/@g![F*c}Գ־bM9sZڹb .}Wiߔz-b i `L_YVo(ϑ8"y&`--BOUui܈"88.`ZBnuT^?WOxFU~51@dn/R::_1yVbƩI=Q N}g}>Υ^ Ĥm*3rVLj9?N+U/3S291Яa2b7-`ub|}9flg2mW|ER؃ Z%9,-wY>ع/VEM=\_1GڼlE26떕־bwC([+>*룞(}e?מ7R Ȍ}2jT9{%Z5>=Εj3DŽEA5&U__ƫަE|YH N)΢O\Ĝ'&=wPE=`Ծz{<?\[_Jғx dr M+c?QW_g qPS4C +yCk!bJU < W^T??z* ߜ}G~GIbi_1v/=XwUmc;֤Y{J*0?8$xթ(D޺+]O_>jT]>Y[C!GwרF*6Wd-W𬟹(=}H+.U^a9qHdzCZ+oxNwTJYf:K{hmsR:?z/z5xl-5'|Q{ZNv[L+3C?^[t~:.Ci}WObA(۝iU*2 +JrPdrknLo)$aN~mj~Bgn)V}ۇ~yz |;ew=CM2熠׉[j>71,SOm)g J?#:[-+SpͤM*ߖS7a/4#zkczП0SGI߲D3"i,tfVe~}NwVe۶}tBREEF#{1mu9fFb =h<۩jzwWxvgYb Qp}ZRC*/kuXW5mGn2cxw} +F5?`wTq^y=~}y\dbv9_y#|b8͗yYz!eej[ه!7tr~9*P91?_.w_IvfU} +׸t\$$%EY.+^Y<hT ?vWn6W 9ząU{6-܀?GxvV=@☜7ȑoSjLFix_m{ BwqM{|UxK^u,i)g;֬? |[26=k}{_\V5nYIhD8U7/W.؊X5T'85,FXD 8x{\bIJ1f;YgAdH'TzKx7*Ljw; ;"v؈j\D,u_uAz֙/"L}sղUAc)]iNk{6#cT<A&߰N1$4' kQu՗P|zc&*"c+j>,j^A݋^-n8u:4܉ +ArW{<~W4</5᧩&u~EYm|gV33 믾S*k >@JcaRqTm(*մ[1м{bq,.XkWUI>P3kK%ԏmyG,[sJ[!3(s|chs;^}*S%ۙ*Hgw|t; Kx}a|>6#T-?Ƣ.Fax,#HH2<)\مT2SDzLuq}]Xw/]3R7,CwX/ 9VlljV,O8DgF^i/2 F~ZMp ky'E<݉}>`Kwzd*x8s!*xW?ۆmxW)uK']wy}c]=Lׯn5. DZdnU8 gl1@6LOMɏ?|w-l/9).ީD7õ8`@*aT[Tث|1XsῡY5jeep$Vv{íUU[h|U-ZWSoWW{bf/Dafl?oqrUPtb, T3 }E.|2]d +Xo4M44>i+ V\ৃ>;{iPgt%5%y'Q32Z.M1Go{vH0S@`\.-c-ϫB):,ﲣG^yIueUgc6r;3fn; }B^ 5$;qx|q~ eB}t=ØC}[D} +m58MUyϟ8V'dP+__y-3+k~b-㼝';k"[j!Gf\ӱc;Re _^BK;-~ZiIOP4*->d} +y܇{?]B9QunhX[4X:K$K̠T"g1U[~s־d %.Vk7( s;zkxuOJ=|Wff_Urlj3XS~DkH1'_8c217.˳О݂w\qk-jy*܆^Ŝ&zzNU٘{c~Awãd'j4lߔ2Y# [[Lc8'%gӘρ _ҝ}b݀b_H2/9κx.qrRO>Fc n`6Mlnb6W2J8VcMڽvd_Y?} \E?Wޭ-)g;ʻCyEޑ^;[ڸ>GoeEn_q}[]xQW]oh|emJ]4q>R[&;7bpO+$;enGO`+hVn>e0y^x&`^P[67p\9r@Q|UVg- +F1`}UeB7~_MJs1#Bx!#`pm*?=6q\ {]t+֬5-ġ;9|Ҵ'-gT1}oÇsrVS{Ns4V7Ƥ='cO!Kx~F~0J$՝T,US˓-K^9g=/_j9_5mBSsW{t1ur9xb}?9.|x0_˫yϲ/ɥ]B iM۹cf] дdgl՗o/.*>'e|E]5}(} Jj_&k"wW}US4r |E/bG^ y*=iDjW eAg/ى+r̶8‰ d=dS< {Qr\\hY[d4U>ROqt99dzu|5_|eB:K>r<5/ΘWٰdy;DpekQ퉍B4/(õ~q}/G\:))iyWlk9VN]lUWhg+/z mkJٕb'zۖb +y|uhA^LA}-WNg+-h3E^̠ۛ? +F$=g-Z`zfRVvM^N+MLr7?Kn<کRk mRMf84DRKĦ_scczvs[zo=oTlJw&vvH{ŸJU jzWr׋-~;זN瑮y1ȾA5˩Nfץ9ʐv~9)VoPE}=gRҒ&/$\kByқ'f0t68- +!q;u򅳍(q"_٦H7#. ."?O{~!{Od2wG&v|ëRRi귌Ʃ8]7~G|Ц8ȇA`A9" ³}y_դ(/rkX$&!X]%E̕JjŢ%k# +E+)7j.h5&7 +kU7g Wzĥdz jL دgRcٗ&T+ZVP|U=p[bȵiK?Ưk~]7_PVZTG/|0? 47_V6lvTB}|֑@{GP,Ôb>~pFʱVZ[ʵz󇶣=%_?#ff<-Զs+ža;yQ=s8{'oU|IxVN ?;WpF{qa?6;tlGT=/>c+3 Ұ_Al]Wcj= +CwW +USU 1HgTƦfdxh@lx/aݦ|rOA)i6nKdnK 4QV|WZsj+X!6Õ _]J=/2bY|Nq2NB=(#a B:n'CmjjZIMk<o ~l=ea#{{],>X{jy3|oc{p܎'=iY{+_KgTF乣=+>؟?wMWq໵&ѱP\I8.+w1Vñ8~q.ZoP:{FFgnK +l㟴dzMAت_oVJWg{ z~=$-mq^#W(jDQH묵kMfI,yx7rKEy~/C=#v9y7+%%;Jϫ:*xdGWuf_BoXy䫫qCKQhPT:g^9iLu6-*A8Nė?`_lA8"v e%q4"GbD;Io +udr>eeiԵa lV4O{,fYz (/R"cƦVTR߄>2f8ȗʟ{|E_[rϖa{[wW*Q \fó'Y9e;pO Ry-H;X 96øٝ+f@E6շl+/z8Qgb,/3!%ƃAJI{GxnyK!`Sq z$8`|)}펻(vLt8:J#LFNx U_Я\QǙ-Yge< +<p?9 掹~"񿸭hifo$&QLXm?4ǠX=WWeQ|zDFWKga*;%}a!GTc/irAjYz\_ ]ei-j7?i+iSc^u3:G[Zj^f:Og1sv;}MT_=bc?(jpQ=7*tQc\y?korI+"XPA`]Fi*2۱./a>9{Gf]_${i_09H쪶q~ֹ_-~Y_[E[w>}]?sdL>yr~CWnⅱu~ƴ>235(o)^yɵ`s30s-_žA.Pv7}dՠT&TQb% ˷J/?x?ysE}\1?^Wu Ie4@êa-8f@j_eԸ'c_(}/||{ -!8ݣy#?1ż)?ޑIĊ<ũ; B!/H{31_F_٥`b0%>^0"w+99hCziDŽ Gi:YCVoFW9Z$ kL뤩Q̭fB0FfXl`&;"8,"B DLp{e3pՉub}Lmbl4/ ltsJً|y9GU|~𳴃Tv|__ZU _Ll UyA\PK.rs׷.]Qkc#[}+ +o9_U2(EoߩPn޲ՋR45~0fvIE  moce dLNMCsF]-m9?˗oLI{6-mҼJ\k훐 +Jkzx'f->E:>t ry[sQP"#DQS!uDg*AԙM=9%4%]CRڑ&I5Q#_4$r)6OM!Cb,,;."\Zj^/7щF *F[/mdq`jJ+2قϬ\"X[ +İ,\Nx'}~֒-_,±B짧 Z߇)b5ܦ}~^OR{ׯW[+Dmq9 Ad)hvalEN#[#GѷEMBg=X.S3~]~+1} jDKl/azpWjVn7aԔn~ujmBmѤV[ +=-mrOȧ/~uZ=Or4HKϋk`9o;rj3(t`ոUr|⸋Cw$X(k$/~G\]D| ֙=&iM"ɰd:2jd&# %PS/eyKY,aK'#zSNU@1AJ8 ڟ7Dqp5]-:b>oBrySvéX-4AZ"Y І,k2rCWA +_B`}`2-^^ESaYF#Ҧߋusz~~|IR;߉ ;>5tM(/o |Whkk_]CZqR_+=o?rI'-뻂L>}=66ΡkZvx8%V76qu>0W%k{NXTPZ?IxxT?}\-isjt!-W`dWy_:?N +wZ֡ʦNa^8DHI4"_uTPc+{ +, AvhvOM6W:C!01 +X%H!uvp}\xz:o/z|1y"е`ɘWqT^#7UwzY'_5tK C vK+#]Fr/pWI_Fz 5$tr?*튯޿zT\:n~vF.Xqٙߥ}pj,elװZqđ>\Ϸw]IM0'i)hYgưO$M0.H 3U? uU(UDŽTB۫C> c9>a?y)[>(/98Vg)GCeGQ* RXPo#91' -iCXU D.2`5a7tJ7%ޤ +>"ӗ)dFľZ2.>/n̋Dw3FBx`X@*=s,$HPЮuҊ_UܡjE_ \VV XD+^`GMcKo)>6Up^N\d+'|W7<_q~I~h~}[7k6rL!֖~7o+/#يmV/2=5Ls~.f/*ғw0"oL^SdxnY|[Wk/@ + +^4+:5p,_ًHTvo> z>?=f1.3*MH}iޒȼԴ“Vp>VwcUN k3`jd~L1.X +cI"C)Bn>(rynED9OU.pD&O6*S?UR5MJ#ʉcd\c3b($4dwRҽ`^0N/4ѮEA}iK 2ذwJUFc#I<'Dn Wlo=`q= 1y,ƹ-mG>7_lcEN=dhV\x σ'?uL?Mw`Rj'RevKh|Wp}G jO%4-`%XH7yxWŸfz]N^5+\F 3 ~(}ղ+ mlDQ[lણ}ןIAh?a\Z7:$2<']*OL1Wcx9:K.gYᣮG֯YB ^fiu*=Jע'zR80S)$\o2SA.S䡗uD$JH +Lј)fj4x{S7o}!-HbrZu怣Nrjmjt@;_Y鞪=d4a|:_ HvވqG܄$SlS~;iRd?>Icp {}ȁUXkTܨ˟WӥW*??R1>Xop8Tn/*}jkcݸ%ȅ,~*+T^>zO~Eu=jw]w X3?$-eE.c+7&N%4?WH9w_}yتl{^) /;r 2vGBCsr3?& 3b%mj ;.^ +yFo(z<1.~:iFc|{sӕwȭL!lu`C_mY~*@R$\W%p/Iop>յm/d+>QWsvf~ 71كH*A )wn7>)-}QGϑߺWΈX۪,=Ux6s|ZTb[мz0uƥ +R +Zk3:OwpaY>C~/чDJ Qx]Q4Sq +J,dgBОz2;]mb\+OaJChnړWhg֤qMӷ%.BGp+~A,^NPY #10)ӋSI_3yi_Lx=?'lF35eqk/Ki~Ոh_E3Vd|>NOK[{%]:{E{ e]|TOH-Mp:Wsb\q|H#_@ǚb^gZ-GrC~]?mlȂ璦y߈۩JYeT5S]ԗ֐Znꥸf+&7,u%|E0E2EB465mP~C {%]K1;ǤwߔT /KΉWF:fvJu{k_$;3CHvRZU;RA~k$Oy{t^ $!dpB ޕ{gݿ7Hj@ )R~'NDlg6GߍsxYKMmghAޛyJy(ݧ5"E䁱6^J̒$$JrOq&&dzqRVfdysQvVdicAb}Q_M>PCx';wqٓ,i_ا0:&yë∏UnWLo|^,w{$o|Kg6|8?GBb7,aD$L 'elvDFebmEeHemK6d`Cvd|9 Kmު,IwoDVvd}UcmoXY<䜙bjuⵘ\ʨLs;)OLI2,[3,Gu C>ؖpL?=ώ>n8'F"vyೝxk˲$˻IBg$b3-ma zC%6&j]zMuՒ~H)oRNi'-S˘w󰵇a^|u|'ҟ~ww[)[X|mh+GuRcz9OWOWXnG%~XWtH*cRp-Z$Wf\gUò\o+gsԇɹ:މڻl'wAJUߌBۻr$c`)_4/+K"`m9`4+Z8j`KvdeoC Y3&CsɸtByd0$^XU⌃rz4:Np&CDmF9zR|&!Kv|UlI#)tB됮Ħz$9' 28(È YܞCμllGM zɁҴ |N;ɶdpUgvcZta? ?O^%`3{#R??3Vo+d+^^\~.xTtfwhTdvfFg?>ٻ:]Uh]>rD,([wJ{Qf$Keag.c.v7嫥'{Ï9'ۍޢ&쿖/Qﲾ=_Ao"_݃-+fhMJ!g!f`511'7c>;1TK1'h0bO@TRc\Ҥ,Kt ?|پHyiZ:Y[7cc24?(>MsaAf?5%^q 7c0̟HT4&2o|ǰ#fkNpx,8xLd6X|SI/or&Hߘht$x "Zq#n!#Kн<m2kཁ}vq6*\W߃p̵~;*lE59<Ƣ.jd| ?B +zg&d +>bܐǏȷlԇ."o0瓒,#r}{Z@MEttowG\zuܟ?<]u~WF-Ɍ)To҅d[𢵄T^쟍`(oG/R0jy6֩ Cr7'O}Soxפ ׷L-*b?@qޙBo[2=ifuAk]UZZ[-ڜF*)1-E$ylǖ-zRmFSc7_=Љv%rCݤK'rrIfŎ'nBhgT{BW,nqBFt']HD&eyF?(banҼJuqcJdJ`!sӾZ7eVHoRAoczvՂ}A AKAorÔŠA9^=ehkMucyDBE6urkzlN0qt͂<[5-ٟI6; + X71#?Kvd΍c^|L=>2j]d8W 5'Zi{䛗/nFs.we{i~-36'{?w{zbAS/?i7õi7t$jMZr|ʝ{3 +zgkphh.pQ- ڴ,m.9j ^u]r̰O$-i=]/]5Ree)Y}.y _]_x-3H ]XDMC HܘLO,oi}AV~h Vfo.3;Zgk[%@KV~>ɞzXH`ﭪ'ykQfWe +҄ Hѷy>(nwit! jW ߨZYײ{nMTfNT 57Dw-D&$V +)ג{=GyT _չ5I^ +3ñNuzjRg..MERru:7ۅ0d0r5+zKիϕ'uoQvnO3|E'yCI#7ivw.g{jO +MT4#-G7}):FGRΎc䁫Ξ]ç{3xn{_^W?rq|5d2uYqh/J|&V|uod]n'fٛ!kD"ኧb?5,B`[^xzuAc[^h_̯zp|&ӷsl&f3+g|fsЮ15s޾c_$O`'>ewģN/TvP'zZB0'Ǭ/Dϱ}ﰺ,C8 ^*i V2,}:3PMשkf/8q[BcMbqi<~u+OKW|[po9 ,tuTW|Uy;ܼ5ㄻZZQdcL #F[@.hfP}ҝ!SF>5^+GIgjG_"m)Gq9:3c=KmR3u #}:;}~~_{1j64:2"3NP.j7̟}x>%30Ƣ潺`߾Ȓju&_ 0ָx8#!=ϴQ{wA/=S?fzTSro|S-!wVya)_}]a=ݛ+ٷA `TZC+?vrq5\>i ۤV6xz'ٍnv%$\:#IIY'kt}KJWjͯ_}=NBgZ\ x yoba\_{\$b]/K;8.V8oz;r446ծ& 5٤3\.psp#n7Apng@{d焇ۏYRR|/%3O^fSmsn7`bɃzE[;6Ȇ܃] EмԠ3-mGWk:[؜מ_;кT{~~W2gJNǜ bԽ"]י)*u^`TԟW.(mmʤ F+⬖ZxzOwIT/OA+3w{ѯr]n׻+ҭ}?!˨OCs#{_偓;qztt] +e %b֮a6X _-xf]gCkrT+6ԤxI &IYXͥ Gm~p~&W= 8-cD**i)k5=jXn䍠3)@gY@])ZL@y.HRBY~\u—6Kتboȱsf!xptob*e o<-6 t8d/+ۈ\%rrզzzoKΰa{A6720o4u/6r bC2K*Er5){FoK5?2Y)1)OV(n|syo@?4YX_~ϟ>Q Sux7װ7ŭz(]2u ץ7²)1-%Я ݙm|+ +\+fہ)ra@ڑY8Ň56W;寉3rrxJ&F+F}QEÊ638sK&Ў\Y+|N->WCĕ\? mJerEx| Z)\F63uW )c>R~::q}.v,J!+mFztJ_KWgr P1(F50 mIQaCQIM BZA}Yଧ_, w +vpa ԑm"xɌzv ;}茊Z٧܌?u߾zQooFOs67<;W/%0槪<;S)]:誸WoEࢸCr9hZ^7_f~ _WrխY+vTVKy=iĵ*y^\\9䨭> ФeamzrBӣ W5zmubkFx J=3YtxnP5|sOvoOKH +#%őIiw\ӧ+jX1Ш>}Jɥ0ꊵ KAͧ>ݫwvB8R ̦6^p_fW6"oT5|sysufэ#]F'"G+w6w]faqIn~=e ؽL3{Hda_2w5C/b\gj૩==qOkW^67IǤR8٬uRScu-qB}$"B\-{pϽ}h·fl]p@kZc;I*?>5#:@*97<ܞܕbٯJ;ջ˜6j?D-rRX!?%yNƺ +W~@ၲ4c9zެZkr=b~}5C4tjlZUR_yz:<2WDc_ſgk-ׁ.v6ئPsyD"Cb"?/vWcX~}t5߳N 7sX6q}6F#H_9FrԤ,-.ζ̏ ߞst hW%5`r:7pkG}Zb#v1xZ1jzpRZԇ]dr;p>Pk{ {HCrEcWR۞zA +}􉩏;q6J1yԪ,H9oQڂ7nu>|U zҾWiXx&|(ŵW1{ W 7-s]X#RY,-ϞkYASЮ:<_9|?K q>C)g+44#ռĩZFU ['6_X=wԻj H]%H]B=RKNˮa>Cs!ylg^7窿gԯ~ޯ=]͗|Vf%XÃڧo.>>=nRc-WMONh-#9cdr7Q])NWM GRBSItvE}ߡaCB]'ܕ]&uxNr;3v=%U{X,cY|c&݆%^2{2sJ=Ims8[.64W;X)͝MFj78' mq9;ۘoGڢ!/?|Nqwإj6p{]Fqu1W^yZ!i|xdFdOK﫫d/W~g{_rJF-͎@k8l3~t.>WOkw-qCrhD=^Yb <|p_F$/vz~L=?!yKųPKS)ޗ*ƻ%f8t-cwibYso~ɧ/'aъ\[I r3Z`~O~ߘ9yYidQmPV:G*d$ Ldix"[x IFis1s;8, (}Sq*| Y+5M,uz/λzjBlj&}XC8˨!._z=z[nz\4kHoڪ2_/c)z{_=}H,+R~v^Is:뛁ϣYg }1K{Hb wS={H| 0w߼ٗrcckz99ߐ )!w?YQ9:hNo[\Ytw_c&ՍfX hk66rЎ5׊Y,Sm8 +Y )-WJJJS)V8/va `-?v٬3]u:Z6qZn̺qMn[:7Yׄ|IUHu-"ֆzսs?L=]?%wU,9$n9WW}<탛ړfɶ#1#Nf`mU'nt@VU}W l>:pV3<22^zS=$]w1ہ6zt w=rN ysΩREqvN5gY +)TYn:N1FFԶ*eBU@M#ܖpm@,zM38 Fyyj`juVrB?k5} >^q\^iH(T/' wHZb) +[/r3d75fU#F4xq ZxJ>TI]}~.̵2D,*]Q'gs.J6w듑a`8[.h2̓uҺW[?oU+"_.(.][YQO4]~QM{zrId H_Pa-5@Rs;5xvznh  _PӢT֒ՕJQ,eLdIrԘ{; Cd?ѼYeXr5Pϫ[r\q@gg@OC5d%pRs;#xc{[Csy:j\O_pxM^su5ǦF}Ȝ=y-A7qGI rGo)CENw&?Qy" ҆\]JQZ_rRߡNE"S{@|9 ߕ|şR|c kޯ[^ Q)Sօc^{d"[Wx%~58t$ֳv ƃ⅟rȂ^j]w܇| ;лNj_̓b[^_&L&uN___}*_>8;ÒMXG|vk{HlU yWݹ8}Q^:~ ԺvwW*r9Fz(Maݳ5BOߜ5m<2ڔR]xg> +WYZ +~2T;owDUm4it4{}s0Ԛ\7Ge*R/q ?9(2ow*.6|"7Y*j20nVP=CUeљzCkUW 6!8NVF|fsN Ӎ[wx1GsAx<⃮ߛ;x߷8H6l&>bS Yt+LeRO10WM:Z1c<6g΁Z_Ӓޓ{ŅW>]tPg1[Wu5xb07s~fa+6a^^'Ɉ$V ֭XRyc{._7 ~=JŅy:ً:YzFbph0,(W_es=}=897g7ql/!yBX*)Š|՚yv-ڕjSX#ԦAUS*ն*8N~U%RZU"U`(KﺪJo-5ZW_mp*X Gxmܛ]KIUjJ]R5"M#9mP4hf-әhD@ޮ%"FiqܟَsMKcZy$Kvd^^!Z/bdRſzsEa0W(\f} !yU;9M_/:t!_w,sgOnu]58)~}ƅړaqk0e')^^i&խ½Cz9)(Ӱs:Fs6gpԤzk]ocԼ,7ПzW'Z_{O/w5Mܕ_n SdzL2٪9koN{ǣBmJvDLmbdf4 +D "ZN"nY,Ջ҂ǵ𼈶M8ӓymRX@$Oy*\'ՒѹI2;0W++ ֖wܺ{G*++ߙ9<3z|<_}Y>^s%c1qս +ٜfݰQF0IӗMeGWonյq459xdzizZ*wũc<9i. IΏIC|Id@Zk a Zஸlcpu*+\ЯZhd.[mm:ޮx>/N;1̧p d!#d/;Nպ`ͧ.ؠ=Xj#݄SFYbϼk Ux[}Ƶ8:UȾ(^xʵqNz0!c$LMN]/SU~_bHm<>2aW8vq%ag9I.CV~#IE>0NନmP\a]EFYQg=Ug]7om|~1s^wseĽ4>6L1=T̼?}Л72UسkF4oa#9CJ՟[St'?gyAz+@s߳G=ry#cW N84!3&a5y3Qc)Y{) )\|(;2 remҁeS"xDzbZH,҈L,mּ,m/ڴt;3~Yf7elyDexaP&!=3^D;Qi6yD We>:oY zcK7&;HRDfפ::U?tLZ1bS&sw*4_80W} ~g}y>AكQOMN+$4痞&z +2*[ yhvuώ,Զ-9M3c^qez r Ժ..jEgCnm`ƺ81qQ-kk}`٧k^UT~rSis02޲?W$wtN ./oA54,Oh䧦phY4o΄OP =V95Mp C;LRFĽ@ +W ;_";Od`]7dx[voĮmvx=9xvs]z=>㧇rv$;< l5dxS6kضUY]oHOD㽒Z阴@ +ߥԈ#nz'070hJqtuRg=RLC|%f~1Q\D`a_UC>R*3yXշ{FXG{lt^#]ը!fyXKodS=֨k<2LKskڔ ݋ j@π[lM6꿛7xٷ&d|lL]+x}UaxK訡e}몏g=2 2Me|9ϕJ[ +RZ+ͪN1-GOhO366=}e5rouYfdbu :YJ'$4 슜!ԼLr]ԯR-RTw<+lW`ցM"X94sui5 =~?Ho=4=9_M3hߕ~ cVޡ=U56ӟnO>*WIQUaz<92\aP|6.A؆$й qI,EidEnwZvm̤`?-ffdc}MiR wQyvrjr"]cyZևM]\BjVf-4d-{ 9T[X +(=`*sCRY^ՕN~fִ,K6'32:;AGX +b?|t}]|d_}#3R8{(u5ޑa#3Low j:TK2D)e+>?~6~r&ihoXX9>\DqNGv z ^>0 >{!y.kK2f Y|U*C~^ˌEˋϾlNw_pK}w\v~K,zz=<[1׹l5J{!f,sj2^up +k +8}?MgKQuVSLYhJgCYR}H +g5'Wh\|̻xO֥}  ƾK=i]D,HoԟWu\1ǯVoΛ-b4O#ՃTM{!S/ڬm/Ϊl:tљQGoԼ̂Ƒ$p/&{ÜPV6Wiº\ j;FF^:Q/-콕էT+umJ1:b.k +W\:J2{v!?:XDRRU'$wgv|#tG`rU~e;hXfN69XIΖ C7"_C+lh\*@bF|sǮ/\Y1K\ evW{T_#kTzcsUﮓK烖grLe9Tnw:aTWKIC 9՗b'qtu)+ݷ Yc+к>n.oda~{WS->sL%'P}Q[+.X{SԦR0Xބ[:RmG~ޡEԊl)Sl,L/ҚtJXz%41c$ߐ_d =PDqXg|7'RWg_D ODA݋AcR$cު!Q0#21? $YBc~oxvHYw4K!G~$M ORJ3kFORtg>nU{L^c~dpL}Le|eVǿH7+,v7iچZlދ=9ycΓOjPXੳK+)PsQE֞hȡײ?㟿#O iOGzKf*rK/{~='..b>ӳy|,]&Cu㻛d+2V[bYz$W/N'5 j 1$H3ֺ@Ÿ9];|mj]1~mAR5/Eڔ-hQ>E,o.*Km֊,  MoWz8}1?;+OԆ܉kllB~2)L +:3/+l4& +-H!yɛsh:k_܈մkMvD.:Ag]k +G0j +p_o\f +җ~Zs'x>Pܗt).a>dL-MBR-<7 ؐS*"2ㄻO`U F=ivO9w{i9an`o}04/j\ #{GHJé:YοEEECl:XRvj?jbjloտ +({[*u tEw@ +yy: +\ __: Q9NSQ:Mg~ΞNaMIt nߗ+aWi |GoRpm|h]Y.ׇkKk{sCoyix]p?abϯN>7;|yB#(hXꥢ\55hTk\_Zچ6 ڼL/N8r~c7*auFotNJcV$|ezU)?e 7hRk3Hot);X%|5$wǶĵBb&Ӏ؁w}6p%h,m.lfҵpH74`70z/ ɑ=(^P#gNOh]!k(3M.D$u&9#ZԮL:Zf_OKŜC7?/`..rIk2oTR \gGX}mMT~HkW4뉞>~9wY5bhqXg,{rgF1"Ⱥe+p5*{ԃ[ZSs5ljN@.05: SsZ.[8F7vhf$LI#:UFɽk:4wwj.~.>KJ\oxYٜ't}{o-yϣsaqγZ]x{/6 L'$93|R3ex:&z]zgAv(Hߗrt knfiJWfF,m0= YKYb_rJЪoKs-Gd~a aM@7u]|9odӯz8rkjACЏitp 45*r;/ŷ{%od`u yG~6Ϻ2xl}h`VPZ;xɇXd_IQϺDrWk&wP9|e۾/&wi;%AnU{_vOJq Cgǿc䪏ȵ]:9kNǦ>ʦ3> kѫ ^8urU^IQRwGCg2s)/]S M "xU+k9IlJC)Y~U1n6?\ܬ vI'f1jGggu΂k.fc➞/ʗuo!aS囮+Z:u5<=(=+17m*m E*ڙYCjᠸTҹHXG_Glo3AvZG m(Rg!=U/9RR0'y]Cr'< E4KcsQ\ !lN:êa5O?m#'MMSbMdΌ8yluRk?OU08~'~8NioYYo׮5]HL=]c2|:-WԽ\&{nԼ]έͨc6\W氞̜zlj K?rEg׾X9x\չ?^1rqa>s 62~++KyZ`zC୳ծ=JkJ|U)XwJ~=hXrܫU zo̿LنﯿY*Q4+ SStԿחdvqF\qM>:<&k/t{)Yz +:P >P"UŹLo'R9geִpud65-WN!cdB9kyy3X G{..K8ME^g)s} q}Tb/sm`;XcTkJ@.F?2rq2 ͊}=ӓ^xљ/ m+שm`$ekT^bA +p@V/@@~dy~?}yN]+k}gfy~ؐUw^&zXysY`ӹbubFWzۢANukphvPTdrƢ/d/)=|z_Ib;^~O>98=R1Q>H qt\$ٙcm9iߤ]U|AT*nW]}53Qf? W]*T:U~Ez e`d|eY9?2W+є\Wx FzJ6W3|U1+x[cg2~i镚c42bdqm%^4@j;qt/iS׫ZuCO}auNSOgfWede]bsKItM`R/;}6Ukb?i5":K"҆|/ek?K:xb! qn͎97!kR-kY۫foiVUC4@EUb {60O80EYEN? _~g3߅9t@)*]V:{|d1z]981 fgqLsUN Kz\{*γL9@ݕjTS} ֖U/Vtgp-`?]UZ$SCg#O15ָt#GþuI 9tğo)_I<'^\q~#M<\?ۺ j.,Kq=d~0>WPf՞ Ywq5MJJ_\! x|ȧiNy6q-%[11eIPWU(I}@^hRW3E=/ǾbfGӉ G]!:<?*?JHoRjꕻ9.څR[)hq3z#:_2y]d|ͫk;̚\9] o_9@jXS/P:o/DC*W_/r  F 0}m?|ҨoJ|0* zifqZ\Q]dOH2HZX^pmw +G&_#oL,}<mr&Nj||Rм݆Ȭe􇑻]Mv)ILJIɏJޝ[<~lU,%5,G/Hse+}~v[S,Wq̔4!^] uWEUER\]u+RߑIYe$S SQ":P2 +@Az N ޓ|՚'lQ6Gېͧ7PO9úx;_^1q]rf/N%:ң}4S'Kia:74-sZG*kkE7 M~"ZZ|^)oI׽ȗuMJ Ԯ9O_cRi\̑^ςVۗot놀X:B +MQF~H֯RrW?>u/"VԀaCT^^uWuUŚӾT[gpUW??`kLLo~JJ#֪Z5fIm0! +Чt93Л=;w֓|Sлl <4]Y؟|~U7,>hjWC㫫$M! KwJN㾌I 肾z)M kDX-_O|Ml4839+Fm\}Fii8ݟVho!)CtpNVI+ ߶E>I֐Xl[,T n@nSD67WgrEjGZ U/_< q"Og/ԫ` +ЬJT*(B~+*Vm| _{Şw{7c@2L30$HQ@CFIr*z?TÈGEtO=UO{+5tujONO`D^z &Jh)_f.M<{h ~wƠ+綴7"6!(֑tIMQiu*ķն#S oWזG3o|RVjS\鍦Wf_S{!g3*TVW(Jgbyـ/9ćlCeMݙsgi/޻$1DKٛo\\Ktc}+0(ચ<_u7hX?d ^Sp#&ת@7Tb|ߍYkWk'EOVꙬpsUC5JJQye{U2}\#7jΏ5(QSS?&ܙDr(n$ !+[&m{'KyXYtj'uCзNo)ρpaA~Ԧ6VYw_4=XU_9|G Pҽ4 _k]}hTO̘{\P)yv9u(->RdO4{Xl*Mr5!Vז :d*mKn+ +SKtz\y>zǹʬ!~~xԠFbS>W)ָttU&=eT.SE0UƳ9dn gi!^q{w׵7}mγYCyy_x=0{]8KWњF Dʔ:R&]Pr_|:j?WA7lyCe37|QM*2rXř%Gڳ-걢vH{\>/f>W:dwT[;X^])ժ豢n͜ YZ3(lUyRFـ'}-'9@jTQlgZR"&>rڋ4gLܧGc`2ي~d7uܜ|4=T췔8Rԩ|EjCoPm?%+U(Cj} `g%h cc%q|hQ!FygC3.K{F"(.-F,ba]e+(}lՅ!W+ ?n9z/Ty[U6-"_yBʩL)WWFX/x.1x&s5J:}ꆯ*])T$RU|堵 f&1˛XX^ ́V=w}emL;qкzu^&Gԛ>,DˣF/ghTdJם?fU5]Gv>竚^bŷępVX20ѩN +YKj`[:C ~W0lrWWV\b9^sT@jW&OŹ̯**G2SHK؄u[X+7yp<²ᯢf~뻫XZ>k爉Ejht@Uժ>j!WnE;{6{~;~~uok6|Yc>}̺ fc +w+k]|%D_IPXE⩴:RKWW>W]Df_-rߜo~gSΧ1YS9*PLJ}Z}ULdPMNž [Z&mV5/g^(Wˠlg^bNJQXR.Wܟyv|y-듯Rҟ΢Z̪ +9_pOivtzW=[^0D + +'?9whO]>. 6ORSE3uo!)²:du͈p-a?#X^EUW&1uW~pHO-/+LϾĨhU1w^-oīE-^Ma68\xzFkyW__M땕oίgΣ W~L$҅MWf/"c(AO-sEEyE|rq9Ӈ?䨭-CFWgܜ#>to4R;tS Up֍reH'dm _ Umo^z{)F^ a~5&fcRӻ|Uf !URjWd.a FGف闓"q]9ijh ';܍Frwwt!7(6=^ɕ!k'|egwR_>)ౚfg=)DJIRRN͔[HJ#rxư#{y)'m" n>1tઅ7'T +?V%UgkR;Ъ; Fu^χw-A$ojfxetc|'/~ʊvƊ˺LcD<Ԑ쌈W8*3R̢ +g䊗8W}ùGy.䋮~CfE}Dȫ O9[."(>_G~S|4668 'ܾ\}X'Kg}xq o7Z<&#Hܿch>0233bDUmn&rǖxQ"7&F6|}(敫L;7;7y@\ndM!5d+ c4"i o:F:nb\SvFv!>,}%jFW阸p[qiU)n+cj@QԆya )cpT#@~EU5aym9%~sNEr^yQn齡&$lz18΂͙ xjr̺P0uWuV-}-Zՙ5}_'g{\?S}L_MUYt H*ѱ8W1>|Igje3+(E:{%'la[ g(XC861y^QZXĄUaue DLdtM*s|k3# I._o%WC+N=iƒֵb_]9q{&@smn|n#''ړ W&T^N˘k~=ơE+L6 QbsᲠ\%~{W-| 8yz?41^ԃ R4_E_]rN_?1$ߗdp0gؐ~;AqP};%r{&ZZY4uNyA7/W yݮՖr*618GGoUy73LhU3ӒUfNfk%'f+'͹B -|*|zWnx^uJ8+,ܰhvo6<^џvY_=`ů.Sṅqb,/?ϨWm,e*S e&ڕ Qw"1I>ۚÇѐ9p| u)_W<拦߫q_Fr^ v/ůW2|_n'GUiVVm?`Iكcj\5FO:qg-d PiGҮ04'1 1UQ2:Vu 1IPq=NsXUm(@}G𕷱o~DbN@9r ,U^]\jYcz`2V$JKJ/$jT3p_5%^u=^T?m[lʖ;Bv؃Ƽ6wQ.}B9򹂢a;R~'W#_I_lZϬu_&`ֈ~+=^ `@ow㛼CSl)]{-R[ f:>ƞ+[*|Վԍ/>O_oMhš0sAgOz8Z;O}Ek:W5{q6:67pU}(Cuwn#Ž$P as %Ͼ~&Sz4ye +PrEo!_4Moȭ5!{M cNJ՛b={52C8\}LE8!|BHaUyCeM({@l|#6H5Bn_A¨{7i?8Q!+tFZꊡ3L&}w[[:|p`7 M,ި_Z;,k1wV޸eR +5렧a@C(1w~6V06> LΌc`3Ӹ.8s4Fk@?InBk#CGxcvv==]ċg?_g߾Ç%}uu +U^l9=xݨojjBKK Z[[sZcc|?Ng*z2O=(ZaKsVEp(H8$Pe@o o 'l~6:`(ףX:>=c=WW1x\_ypy\:O&tkT>Yt-J ͇\'P*Ay\FW,r2҂b4fo5͉9Udu-C[zF|uOJ3xTUh.0Oe.+oTUU! +td1Q᰽Tj=f*/<^7<40h(n=`)UO.5_f_zS3D"AGțpgJ+Tk4>ݭ?on@7oBojPPR@ړQX%6I"}'tr0+f-/WXUθCU40kG\_8X;3K}(ch\YpŤ[XKH"-5P8]kH{{ʵ~ ŌBMn` SU:mU[ 04< z7qOa,blq53\ Ұ<L!|m?Lbz<_ +W},+`{vt|KL<_`,Gva\~ҹc(tZ}y^橊 +Fe-7 Fe_)0G8~>7>~'r.iמt2wh꼏 +~^vWt9gs3yx`=+VOήOY"Sv4?^6nC1; I)FAa +QXH:V^J n޸(= +6Q\EIMJa'[)hDAW?6薯Uށ@VfFpS\^)_uS|-ABwX̝Ke-I܃M +w:X,x,.(z=t]/ǑpYV`=BcVS%}UOkSV4vv$6f5ԅQ⫁>lY:8|{119a{_Of(14ԏ͍uyrq҇ Ҭ^֦~X<6ϑTS ]f2|u_*\|u޼W΍Ǣ1Ҝ~qIT !YS$U4%EK4-f<c 2_\T+ef]5siq}w >csҜҴm٤KNy07UAzWZٓV# OrU;;<4ϰ Ug^.U km˼븾'u\%+e?Ob2?\L%P:+S⇼ODaQ! 3TRt0ќVf$VsR(qW GNxH_"mL<~Ҡ(脧1ZM9<-7jvD^sM?Gnj$ܹsN1iW +_އwjyR}|>i/Vb1wr~3IRCf/!p#TB6D:AB!>am֩ i>Gi{85_vgxm>Op5>t,vH*+ȫ#h}߭[[EU:|?3?Q& z=&ZnC/m BI0OxMC^OՓQ瀭[cv7i$ [JPğsT"5R^ aHcQ 4mPΠt&?bqx1bhK4'(Ff&s3O0JWu2UO{d +{&|Y1bhhG.x:|>3Pe~#*UhMW=6kU9to/c.[qXVcAĪb$}fVV?ԇ^lY͊ 2_ufq +x6D\tpcnf4(\"qk[WV?w|W?y}0cus4c1_ES0DvqW[ +8%n<塜 kVI<䳢6H>+^6XIg-\^Fs +;et*}1/JDkH"ފUӼqBMc+ʺ&@S%i|q}BGUv>gSNZ;R(՝svGaJ^a09?ỿ~_ߡe4/+(흇vxC3kadz]G:7uճJzȟ =T5"ݚ|Citus|0cA_Տ02=Tc-Z;֍4v+yISjB1!$9lkS-veOq$HMz(jv,^KA :PS_@g.?]g1ׅ59,v/ha"#dC~Q>bg8^s{A- +-DQix5}HT'C4iU;8 ?&'ۉYRJGG18ևQ?܇Y<~ynhfQfzt;(j4LakWs˾ 4{JQE^K*WY&H9 ؽpy }9wl!ļI6Re&x\ru]/!H~:H hHzz@=p}ї_G~q>n]+nhnLLI7|^Z<]-4N1_~EZV^Ʊu$iBFgØ^,yR?L5by,K,4~smL|L?W%ܘ[Qbm= +欻wߑ9vla~fug+K3\AJw/+um ޟ4h_ⱈ7D~7kֆ { p Zp&BB}A5͞ϪwQ@֫Kti7 >7XJhVNW54cUM)UptW=Ń{^ǻjڠ}]  +<QB|0_ui +;w])_qN8SἜx޾% ۻ++]_K +WOU%׉_A?XŔ#,nf|b_Ey@$dأgiQb +cSA~+KNyBdoQy*8zo( j1T ϣdJy8BHZ9Kn]Tr]\W :{pmG #̮;~sFSބ9J Σv_/ 4z^|EA9VZOuJIhl@}_-fM''1FңB EJ3C-aze+Z^A6W(FϦ&uԝvD|LǬ=qXֆ:ۄvPp|~ SXZ +13ޓ {QܧG=J+_fW^\J}ݲU?\5G.)^ʕW;H+tfee7='cF{!]b Uq ֨k+UYqtuay9Xb-JgRiYs3(nJ'OcjrONM4VtW[W?kyWVGPɠz<}"~NW 8+.S% ! $R^0A;tj fY jl(2wnE8|]'HŚbk'W1UZajwl zbRүDg'[qDڢ5r/բWZzumoCg~媓[ۇv's6}=2bYwŏchV\'fB^9\RRb}M3UT ^TJ%d Mnr$n^V*@(Qw/u\߃_3}tZ1P!y.JS>^ky@f'>x< Mz5rfG>>t '[;#^%Kͬ,b + +n'(?Qʭ曎ʱlC>xc9m-/@°t$.b6e +o>ůκWBI>8>GlW9|U#˾\R +^{0oh`qwη:6g&̓U\cwG䳢oЈ׊Jխ<5Hq!NՇE΋~5BLὬfzXYҬ>93vsyF(CbX#J+a4闘*{Bw67oSXX&*(_4'|U4 +#}_C:s垉r@!,!݉yJ΢1c<&uA{{ zqRHט6Z"U [H&h)R-90>5BzS]_" %눶4Ӝxڕ+I]'JGQUvc}*g6$P~0:ջ/cZc3U6UJ;X<3k9EqXz:gH/fI7W< t[8 P8yWWVdSw!i~Jes{a븾_BO71ԚPW[j1S56gKj-2SqfQ ot6"B{,F)?Xz,j*A`\I5jkf ['ѵMLol`.rpgp4onTj.p8s=/r0>3]9?j59+IKN9sO/_Q7 ݰW;WFJ{Va} ϼZH`e^+ܻf#k֋eX B2vŗ01Zc5iZ9?}Dzt8=ؽ-;7A~_&Rr*_-_+| |urTcVqJ\+L=T7p8 ]zeOɝ-R;pջ}M2AW*q^*pHƉ|N4\T㹈*Tk>B9Dd[ L/]8gTF+M1W˭i2Zq}z!/dCzr-+[)U:̻Ebɧuˆp<,gjR5ٳUqvִנ{ #hk:sK3]8ax~c&oV* W',ZG^Mבn|\a^2,* %h~\<?%>NԷ։njXbn43:QqԷIR槹,ay}RA5bƱuoKߺƍg +_Y<'r^^\K$x [A>wV.tXtnyжVҽ1^USE>5f{g ӳS Ѭ>K_"z*y_;W[oѯUg)J*eOKj7kJG0tVR-w{aFUh,P3xsgkU]WX&)z棒R_*l#䠿_=a*SG?hf%e,Ѿ(T=4ǴF]f+o<@8 xܔ\gEek_]kpon~ywI}V'|uұT+zUy.E{>/b|*] Hf?js:]=I:kl'f7ȇ5m"JvʺU朠ted"stbm %ǹD2}m}7'~/EӺ{w'Yq}1_ n@ _=/3>W|%N-3f?%]h|3Cl]b|=WJo>y\nRKѥ]*s}gUu\߃oTOUCDa+ + xgJ|-]+95F{zDHOg?W= ]i"*`U2U{\糛P<'S9;PC8ͭ&mI= |^w6<^LLtxqH>N^JuNI|9Ԏ1l_Ti HPfOKrY.iֿ a>~ɏtٿ"NeUѪ8ȧc)CE6t}3s3;gR$t4E}c=7V%7CTgO>#NOOpr//ѻA +_Io?7ѾaN_{~Pժ6g ZetSˌRG nM-ɽ7aa~ՋgU z=Wd*/|m=ZbbD* 2"X՝Ո݆ڛdj^JթZ/a*ֽJ.٩ih_9=W\=5@?'`R}{.szs]wO?븾P5b~O8+KoBW)HU.lUC"|f}|䭝%JKZϝWZtctrKy_|^&vQῑHJ>k]|0:84Nz~g_ eVzcZZЍ?oˮlxr7V{ ?4'jTjq׊t2GYkֵⳮ:S~k.$iJy,-->c5)l>;}Y4YDG +T5+>W.z6嫆ya>ͭ,vz;"zkWܧ"w?|Gg@4E%ZzP_R4AҐ'sk; 3}G3gu)=03L+iX]gR(Ɩf `nc$\zG.g*՛[Z086Yu{}$uT +uUO>2*= KfyOzR/49)mN#3@^o'$G29=6mR;T-^ +p0Emd;X816 F;5F[1 g]3*'~QwZ0^?RO?R*9]vXB h'[nt]ǪQPe>ޚj5{(@WKPѭD6]rUn`qi^jbYJ9CD8[2gq/糚GiVY`}u JdjMÿW*0=~@Ç㔛 F0EҊT(.8繻% +9S42ۻCD朾J1IbG\[UOWhUINsz kWF*QU5+/mmY$260:!~6Ѽ^LEQBsc'181d`b$gZ؁Vz_J;N~-y5~`WW?3|sSY"`.?Ϯ5A Ghln^|s$*WUm(nJ"aaiU15]jx;6}*_g{V68sg(] +n8°hyӟ!ߐ=-ݴۨH|H0~"g8p ^`Rpb|3uߎf5+&(ou̹AF nI va)4FQMW`avK Eߺ,ȿݕNG3?E\Byuo`ՕʙySٗL%2Oqygf! tvr Υ}\.UY;w(;M[.,sy' 1UgT\xMhcȯA~jE A=z".8꽃*6⃲&{Q~z=󄉪$lu}STW~/> XT:{[ߊ{d!Fs11lnahx$εX,GD[o߾jkpShVg%+G(Qj,o:իw?fO,+f._R #}cyȏ"L|MOebYt5tJ>{d6ҾI_{p=4O$dil$։&GOδ Y?uX{|ay};zWsBsQK5E#h֐X FQ4h"x~u>umݱ Q٪ZCy*M_ Rkר W%9G5 F02ѹatVR SLڐ>M^6u?/ι=TI~4cbH~eX?H9Űs'sI9 _St&xt: Rײx^?Ǜ/i'ǰA^uљ:ꅫbAϚX&;hsVKYR*f 74OִnNC;tr`YEfuX33$?H|5):֖ȯD9{ +{%~uUtcR_tqƜ \wܧ˺!=2ufW$"{> !>o(3b'ObUCDS{jWd" +7;hΪUzqI(inQy"\x=]yHŗ0.~qvntx1Ϋ>=s=._R͈븾@f+-k 7i7H(}{륎Le2{~>׽!!ױ9?Ք*;fS)ZBuWkZƿ2olF1=8S?k( +2g^n d+s0mm,# (7ɸe|C{&Q[JRSޞnl|ۏ8>>Ft9J6 ]᫛$ZݥUe:77Pf)EGG67֤BnQ fլA 2Snm:{n58'U#k#SJk3pk:{׷VW:9.-*>UiXøFKE2s}i}z.ryou|'pSnϻI-oOZюpMT;p׽jjk9-ԹsoRYSOF ):ԬӃCW^_. –Yc42Pj纚wRy#҆_|!Y[Z'S]YbLg9Dz +Q@.] P5[p_=\5J<ŹUzUM$D/[ S+Yt6bz%}K"wY"V 3>31^\CMRkC=(LEk1Syz؇SI54ONm8$~9B3WWZƿ⚟"xI4;}k3[1Wq)G.,=ꉟf20Jk᫼pڲkZ*h2}\G\{eiF+;0Sʴ᫾ _5MI~t/<]nߤa&Cr{).ZVE^_b"z]|zK +Kaw3ЅG[K1_>'?X7E(<>U;w618#\:{ݓM>YN%L:i'Ҧ*Q6w]|5ttU~=t?_j(׹ Iڻ'j!UK5{123,疻h]M'@P$$*x?1_bÛ'ivӌ܇c>;7~Aϭ c+Vzj|0[0[)uY9q?L5[KN0\AXIqKǶaZ}M~#>eɃyDuBff 3k<s'&HwV*J=/6OLq+ˋR_]}C+cH? ?+:v.zuhm |6L03+׸>V&g/ҭXaO4UXrFIt>,mcxf~~v4 ^)?UC#Cֈ]e+W_p֮~+8H:\Ec_iŵtF&zAua/$-'xxxc#z`w_5v-lfk4gwI rjkgC,3B%|uk@fxt_*TzWf-\3կ_S0WfΜl=E|\V"T TaK7tp XbUɷn#=;mR~J{{&7krfŏ{=jXb Ԓf+'(˧xpX$niD K{|G Zŋ#'_sKz.^ux']e+^s {{{&? + ev]>=U9'}"W (%bbUqUNS<&y=X'xٹ,ω_h]*dG09 (2⚞܏Sqa:թ$^/eCsmnUtB^黿 LxOI92?^uQ>F9(vh]WSzDܴ}ù5=͞|pCҿ?0cUs}Op&pceABQz@^H^0l6bXΙj,l>sǼzɻyܗ +gmD7O:/_>&&&DŽ71(W<᫥WЍ?: }߂o _bs5s]؊^Π6"*,pH">Ul.;ڤW "#pN?Axk!Pž4~~R3`]|~WtյI~WU%aŷYt-f+3WJ + E0uVUszs&Q5@ O|S~z<3 5?%(Hέdu)Wqzfz޺<ṭu7['11"B'ZZ&CU| /Do4(Lzhno#]̙<#mqE<`[_qp_Aj}X+>8R=R +u.*/`8BUtr޵óC]BJ~ 7AM6T尕:bўRފ= +15'yPYtEz^ܾ-s~u??26!.U %䭲+f+-iVŔ1LO)G߼"yȏn<_e G*Α eC0oԿZKm oCY,?Dz.+˽V=%WY o^ 66 s3xx!6֥O6|^d͊JͰyo iNcfG(Y햾layy^j{k3[c3םxqQ{?6GJ4~eެ6Wނ{|ߕ|`@dž+ գCb=FJP?:ʅGWWtC7^|^g7cIOt+fszdzUnNd' wY ֑ͤ+3s}WE_Bj_ez<{^tZךqs+eSDLώBK/4l3|E_u0_ݼ}K4+wL0"9OC=j-WgjAh⛮nqsM%ܦfmܠ2yهq#݁+}a~kk+򚆓$6P#jD1CR\'|(5,>֐׳&GU2nFW_'bR_ڤ8 aCvvHP󋲮*DGN?g֕iQZʻOs.s&)A*pLÇF01Ġl9 2y6@^ 7gѧ_t/"lTeτJqHSئnW&{rI9qN5S5HrUj^S|EAek_ ;mfQ_YngH`(}!j-5/.q~vqWȷW<7s-E/g= + [7Pf6T/PC+;Sykmm¯ד -p^b _e-/.~pl"?X_S$GӧYm*qr. FNkl!t[*o5|0u+^b?;XڒhVcR;kX{{X\Gݹ67ϱ`DH_t-CeXF{I_-jvKsWllv}֥{+a*b|~5;ǽσΠGy'Ⳮ3⺢\W'UHevhokiW\ǒ ⽼r.Ё RW9}s_oeيkE [ŝꆦ[S*VabE_c;BWMGGUku'7؇5<~7o^py%]dQD9眳y{[UGG0n}0H'{Z{URhZCY#Y™m uxeVXgW DQ|uuwG}qw:-bKѫ +o iM`t`Ͷƅ͐c\N+KǶ0ҫy"> /yo?Ǻ?eX{sy\Çeɜ;>c+y^8SI hNQ?_FzQB-56UJW[n5yNlyL8GjEYnp]# Nz;W] הgd/>7:QzҪ*S,y*[q?AC)=#l94i#Ah`ʨ!$qgk xfz_'(&sks6N]q_eUzW~ gzp}]Agu[L?z Տ-h[›+@X3SLQz{h'm8#٢z|>unL9~ S\,?j x,潽YD<ׯ[rQs+\w4_6 D: kn;.;r&g!s5:ǩL9ߑXy'x_Xت!*p8g P$'3:/zYCfZ?5҉<Ţu +Ӟ!Yy6ϙ 6,+lƤ\[[k$A&EuH+%̏:MG.ѯtY&Mr?LS%C'ilwT?pUps"e+.L~5ӄԌ4[) Z.Ѭ{\5:1F9sfܫc*dt9x=gu[+Y ~cż)S4c鞴==Q˿Pv:| +S"בF*J5"]s |+q <,XJdD1kT҂nBWL3fQ5>#?Cڷr3y/.DYeU\O,CUѸET*EpUM UљsA֣q'^KH ^S|\* rё.Yꚠ;8 _WyM/Y_$[PD9u+ur" k=&qV#&-Wq=s>ne7lȟ5=5)M592[1_U~]5V颵hd}41>FǍs0_{{km5\'>T +ѸX +65:\TAYw.!#K\Ήgug)yvuA! &_=hjWyrn[ ߊ"}r̂w_~(l@Ck^_3M앚Mޥ4+yl_iwnۧ}[ւB'}Y$͵9(}Jl)~UJlmm,OnK~uud/O b`{J_b+;CZ#gr/-`\ߡc=oMs]oﯶCŭGk6=wHz֦O7=XЦS|U=<"|TAcLk?7Ilߤus9wWLYZ7=NE0nA3W֐W2g[2gZ g:jL4ghhHC3h}a0#֠ޠ j#Lz647lE- V>]EHg%,+w e +QY[:^ԗߥf}ڗ#:1fcK{ssK~Ϝˮ;9'.|m0?qڲpb=*}[pNXiK}A&?RC7=G3S77~uz-y|'l%DY֭H$u̮N^z 07E}83:<Él_n{uSPiSE+ aiEF2{֑: Ct6~R7ηwDpM2C V"\{,=383Ãב-i?s3-p)Y0"k,sR +}zh}LR3͵pt4wtbc8xܓּ{8Y)z Er4WE=Y:xFQPi난X0:۱I +fgCg, )@ҳGƚ8_g2 GhאA|}4V7wo%_ /9{D657‘g5QXGJo듣ހ ]MkB+N;Z{TVTJ EYJ,ҤȲu-ޣ >uL:1Fe'~v`#0zb!ϱ O-.*9gl5be{5BHDRG啵Y s/sA;籿W|٦z]H"ogA *֭;Sbv 9/_ihM>lP8G}jf"?PrfI[#d y_kT6OckwV{;sO>=T+3T'#αȯpsI ]i6ZYL^H7ϲ$cAkTR?9  0_EXK8L;0B3 UC4گ\}Cfgab|?*wEzL]a8`ޫT~TaƜLi\3Kev<-:5V CFv_0-EF$&Zպtlx{9kV֚J% +ՅHFUS5J_-&_El%uI=}WuTUjskߜOМۣd=/evLѥ{XWExN\wb*up^_@E%2|Ho@y3drZ^sz +&~b%? >wֵs9]>H]PڏmAαQ g !=Ƈ\5W.(oi9 *[qnƉH(|yW<s=*C#{SXj͵o?]EѼx|Nz)lb+ U0Z8!Ca+S,iubzi^#<~qQttCRTy q_EUv<*I, +_}gJϾ%F|uT=ˢmXW9>#3'Չ?s>țLe΀΢> s^PR + qa{QECgLGk>[ZZ] 5GQT5qV}:սuz]V:_jU& ݝW5e[UUahzͭ~8*[q6{=Gj>_+Q _FjK~| ŜNٟrש i=4}/5=نƘ ~uəRxȱW0IcL!{zZ*3tHji=ߐQ;4oֈ}lo]KIy$ ajo)D3BQE!JKQ ΪshW:3V*_L_*|Id_39Y\g---nɻ;UQ>0ZҶMŒeO 3^ietk"Yg^t5U=OqqxqWPgm&xk欚m{u\z[C^OͤQq_ +ٯ{Kʊ"}o|YV|}9_A:`~ կW<9y{syEu]VJ"qo?MBޟwWll5!!+6yo>=c2Xteg!_s9̗_ʒ~zNiAT^9N\S%lL7'Q"72ct?]⾨6֬Ja|1US&."Mؚͬ*+Yf*h +d4V{kܡsӏt,o,*-G<?Ǯ N1*b@er QzޗNx{Y3FVO4dmGBg Zϫ)"{99uApgZslC ˛iېR39I\3_uL+JS܈,͊n!r,? dw}P <܅X|Ž-i961ˌaHbUү?#3kPN@nXJ1Gytu}Ňo$]rW.V<_y_D:Oѽ{fQLVo|\Wƒtk0չQaڟ,WSuEx +K +gֆ=H7Joz޷֎n=36Q:EWww #>>]':Ez.r_x8{4}{Ϫ :5JNOƠ ߯RQdQBAh?:/Eb=+@)\YAEUziֽ}^'g]cϫ 2_s֕D`][D*PRy٪s48rR<[y{+.U4fs_1B1Vɩ,xЬu#9Mg^!~/T_t^r +jCX#&y;:"KVv$ڻ0:?113& [WL櫃p-:+Ҩ3/Y:WϽ%뷢=1b:[Աs's +TXIC+Sm`/<38nze[{&DzLNajb\zĺoEEeyh#d_c֊kz1Y\$/aFIb?ֵԡ ˳X\ƒ|>9;qO͡`{|E.᫼2^~Vw,X&\Zj=Ѕ ,/ +W--ȿG&3ЁqxzډlM7onr&Jetbye\ϝ}JSZN+=>WjӨ.s꛹<5B_ +H/ՈuTCOǏ͍ RM5 D䊊J(,*ҬbgYY.H2dѰ|KQT +{黕n`Gǩξ:/?fiMmhhG0T 8O|5#󕅘AD6ݟĸV!y`\eN8Zac߈lO{<Ө9Hw[{+J+ˤH?ͿkC_qM7h =e.|77|9(y߃jݓu8>z=61DlUa+P gWqi-TG8^>sS?E5~&xD|=;X@{E6cRT=WZ"g2߿S|m<ɯt=yvXTyQ]Db1, +[)ž+P\[R(!7$CcWK:QOLUEUA~uʑ?8O@f0?fl;-j\Fj%[4nycpsinIR-f**c|Uz֠8_f) HART]_(ckj_j9]{ +=L:gNkJ}(dүpt^Xꄯԏ6L>ueJ~~pjL:_5̌u삂| yZ{ߟ|*hX3z+Mc1[ `pxW$1r̯BC ԽnRcZ9f1_&$9+XU>-ς$mIՐF::Vթ.#\L+"{ Wz"}VskVU FuAhuŭy 30UӞwGێLn¿tщl{߬P쫼}`F8_pJ,s7KZ 0NLEjf̉ܳPISڂ`>ޣr#Y Ĉup\<*^!Z`^r'yn5q'G_?JQOy#,u~;Ogyav} $L/'o12<$::}<[yrYi:Wq kW,-x >"]#X!:\"3 iY͔mݪ:;;DbMH(sU;? + 9d`{zׅ2Ba=qqc?KKfWRd +dʣ!!T Շ*{vÞuټKTvHV ZD݆֟ IΜ/| |&Yg?01>J*%"p >P1XriU\ԙt b+viV"WK.c&7ԋV44HWU_x!`q(g^MO]g}=25ΩUEIʤ>RZ;ⱌ+gૻ'厁- y<^8HJMFzz:^K{}%V{r Pk|YptQSE| J:o\3;YGY]YBiY!^65hQ:kE~ [>u!%?S P8pM(uZUXRk5-Yé!om} +ϟ=;X^Em[?QZom~_u,_gi*{Y%3_=6%=?cg>L%?S)l7qO\Y0g%oWC&ށ-b}cEjt DYs MT|\dOy[}a~\2Y OnôARU'L"r'rlu:_Kb$etG1zesfN$ٽ$IpW2Us+|m|UDzPܫO`ޥ9s/aށLjJ")1S1S sNg͟>~-";GaMr4 ~A^+]kin5O]1_E؊Y*j7;*TEQ|X]]zYхR$퍿0>2{Ž_- _e4]A5eާzq7Y Z[u=69˟ LIOLīj9L4Ioo}y=kR*[puAn,˞‰1 >¦NzUGǝs>q$5!((IFN<3$F\Ef ,|>f+v8Y b|UQ@|Uc3m& Hmّ ?iKܯxeoN{˅>eַpߵpH5$Rk ;; +Ky"l^5U#|TT,`@\y?kWYtwI!wR\6Vm{d~g)"o"ؼT뜥`h_Bfr+JEz'?wҌ)鳸)>hM{X3lŵ\f)o|n& |hV#jyuVT}խSPJ\ ww:"L8jfaRn4u4FK%m= ni5Ho@&iKI^8_%sL| w|Von͟iq+(,j'WYWH 5"˕rҩ8accbJsnmq:kkYžbU$Kr +֔뷓nFk+_W| :6A <~b]q"(݈%gLJ"eۚ:k8=v]/g&û?M*l!+_Z`m9G;bW`o;iWF'xgƦ=M%\ǍRxbUE4&:7 י_4ҽERYֱfR`cP᪦Ra@kMj²RTsDFzea}k +{د`. yJz+= ԱJ֋YfsW?eȥi{)ߤnUI3Ak[+W^(GE.{gN煏}to8rԕ!RM53(+%*RJH$I( -ynkg>4S-!'%ʟ3oyÞ2[MҼ:8gF`&Gys[=0} +.zbS_^vzVaj";ouH&J(kU}yl%^:':jӕ-y_Xs[hV͊}VN!TVUhjɣ:PC51(&%Jx gXrH~NI %?<גç}̳&ݸW;UUb5`_{~_(|Lb?fB լ+^qzhgKJ|-T3H/&-{J@js_`w)AW\9NZEedqO)WUW[p72ҙFys^>$/ѫO-t0 +ЮdX:]K΀bo#^5]Ա, ~~\%NXzdt=}rW\'?=?KL0[~NN=jj> ͪUU]UYLic`~}Vkh 8sS8{f&+UeJ%zNૻ?qc[2s6`f&Jgo͟S +)Z5v޾^يٖ{\6[Gխ,9l9UάFzM; +aSيa/{Q=y~sys̴58ss3UbJ[thln=<};hnila?}R[Bӳ&lehɃ]kkWb1r~c ".dƐ*gY3R&r<|@-/kiա*K?ۧmQ؊j ZHGO}ѬF'5Jx?ԇe_}xthj=]>\N;śXE{ r7ޮW9qӧKl?qc:^Tbo=nU.x3|֯ Ӓ.uAzs.7V'hۄ"\9Nr{5ԪeZ4 +5We[r(YLQSf|7V/(_Iu3~4JMw+[lE6 NS6ByVTm"ݪ{p2[UǪjS^Fɏ:Uyc9y[  tنv&|5옯YW|j6CY}"1v TSlǂ dgg-DUqe1_үCpU=82W%,r9v!y9_?#=4(lT#wU}қL֯x>h'aOͼuM4n@ѯs{}Yc35F\:46h{PYU)VkT+ky폰vrҫsfoXV)tU5l8Toӑd?"#hÏ _νk:'՚yb?=?t+XGzH-mjck /_==t%\˝7ZsmHfal+M8M]7MoD5T}f,xmRxf*e?O8D/t+᫪i(W#hHRj5mW⮟/#p 1~ lAMd +[}57,[cz.Vvrv|C,q[q>AJF[?}F?tp%!f߆F'lḴ[HM0ZS N=JL8IrRm!nxhU&y[Zp {k^D}O!^7;[ ;n=F+Ұ,Ģ)]p/e#MG [bѮǿ~BFEy~Kځ_Ij]Գڍ9櫅7H)kGW3^ڤz_Nl*VO7#˪'ԉ̹WZCtEoМXT秗 3uz +o"lź"A9X) 4_:P=<{ZJ nmfE~/Kz;sFC0 upY3^Ӌc5s՚1UcC~_m{$> as/F+gTO;XEEE-'BO$m +=9m9ߤ| l>4yYԚqϯRS&ߘ$M%pUwܹ*+ε!}j{kC#k{Ώ?tlkjZ}IJ*i&TyS.{ꑌ+E/lBt}hO _="bzqD"}uxpŢZF_ 2BWQ;zp->GǪD6ݟĸ1Hq}uSI,/WNVjrE_VGq sY:|o5]x㸦H}ޜ.y>5*]r6*t;Klřu#CҢo^ͯ>\+I_CK6hjCMxNziӭYqBL;{Z"KDi;!f=?d]6vSxs-sJC80܏I_="z)3 y׋.||YXQw WWl Yq}^Dݟĸ1HqWw_[Y 7W%S|F|^?j?θWfVmݞjo"?)g$P O\ uΣ܉lM,֮ zɶ?$m$gS &*V^uWf_@mn'W#Ҿk]hWE1AGKʯꖿͽ/^%le Hln'}"2?qc檀yIC%k }T^zywa+^ t5d\`QrB c0m+rW,5[-Aq͒j=5`m%JQXKZV!/聫)sJzcccMj+c>b➃^E/XyWOMchhjn: +v踀ELhZNOeů4~\qO{ ~@xuO, [1[qCێann˰}a5зVcvn +䯚}񷫚Ҽozzs[˹c)W0_qE$볎f`]H^a]vuDck9/O3Ұ _eT CE؊YlK3\}~Yv]BX"a.ITCwŹ !h=ODƓ_yS4_ j:#}&'%S!}.[* _3\- [{ҴOk |>zaYy/|ܦ`4(_|T4+f+X4{@f_z'kx7-jM/k3גcpkܳJүsLPbHܛ|a]o -Ƞ:CF + +a/ig)gPs/؊s~J'}|zH>%q̚UQm!~+P[ 1W5~]NJ&-v0' qpū4|`}8+'\ŽZ`|BaPZ%e$=W-,\0jiq_U{u/qi]\B BlC` lvO f\yHJOBy2oTzOj WC#Ox+ŕE`tlTꂫ+KGTg"︢]M 0__D3 `p*JSyj~] OևKuXl^jRo:Ɔ$vy o0[ _JvU{W!Yk,?X W`,n9:[tq$JWqWU=wz >?=O!q_~ay+ײ`]V8~:'?ϳ3+Fi_ݨFwcfu%=]6U DCV!uI,Oh<ܻ/!fӵSU@ހԼ ծ[^|0fHmUQ>{Sj>ծrKHLO)eLLQ]Z{pG{h: s]=VV66݅J- U02sƢ8 ҷV*_={o`X{ +=Ã;4W/ɞ#O3kW\7\J=zw-#-|o ?cO(.)|&W3]fhpJp}ZS5Bۊ_S/ayyAj]T܅_u⧎:-CfR077qL9>K :2lх+=Vw?lŃVߟ;93 %_Jd1qWzg4_W͸ara69 +ooQzߡ:~S [q0Z݇(]z6ڎE$VsC牽V ^0Xzr>%YJ+1jՈ}SoWXp@lA4!dvAC]n.Yw,cIh$[٢#lQq+umx<ȓ90ogIb1pU|vB&>h{ E]e2x>jXznVjkcx5*_mqz4%X_]=)]m9}!H5EZQ0Ӭd/p"~e0*v +ϨrmIFW30/cw%lbfj ^4 #{ICu{vUJ阮XW7}JDwT8[gO٢jMr kvY6dm(+|vLb$U7z/_˟nc+'|1'^,|w"K*%{Ta&2MTbZ]]hWA֫b GЬxM7GP\VRT5T"TRk\Hѧše¼b^eL r] H|E㴯]G]CvXzH>_ZCMk'a^8ҪΉ.1ػ" ce{WENdr4U^+:||>8NXϒS^߱^R|38{_vId1*|: + xأs⻿dw+Vp/?F _{Pք ӣctwh>9f6L<;aA?u( [E33\t|.yu|.9KT`v=‡²B[=w4^'`l`|{N amc[Q&OXI[[뙌ŵpg>?}Uw|u+3&š$ˢб&_,{sb$ACZEe:N=|1}G;1,D;櫚⫗*pEz1kq]O7)3|?[),]{ݓ|e|UCK#y~ UϿm^{Q>N({X5܉=~].C `ZUrE3[VكAkqp٫c;W`?8#[tOzge.'N":UX젟cᴋAb$]/`b|v>\q ~&kQDS)-0$?y)2own]ЮY"9"ۧaو⫵?Qށ Oeyef{HEV_E)_uak5&&`}G* ףZO69= }ڷ޾Nbl[_ii~`UT[+7>EE0ϤWCe=U52^t nnY/.{2ժ>ϳːVb^ 1uq|W5*{'_js[g7:N2Ll>_<-,]/:XZH9'YzJnm#[|M5ҳr|Sx~pmp#|kOjhT_}  b+E>aoi+7y ҋ`/v]HLC\=a%5 +oIRM]3{v=xϩքLfLWM>; "vꃿ]*Uы`H[*>Q+)רT*~&;@&$|ܹXs=5\Kʫȕ=ʼH\oCNQ6]f~'}b$v řs,+sU"^g]p)P ~ȈzWo%2ߣbi%o{ACW]TA+䯪meh XK\fHn;ąW^=\jqhl|Rz\]}Hw qCЍ|}1 ].@"7YcxzEś)2kq:PWzfL܇U_A`e9`VYS743>Z֮,m4OG⫾Yy#Z04؏B,yfg1~]܊"8+P=FF9.HWfP*W*R#m1;Lq{W<U|̲ +lW,=8$F|k ̼*zfZ֘1u3!JԷWFGSlFF`2H 6J?&]gP}=ү^Z.Vq1Ott].٧:OuLMcsvzؗC:/[Ay 󳢁W]PͲ?VVhYMøWӋT~w%b:ޯHZUMm<6s? FNҹLzhmZLVǃJ]Cf{T*iUYuՂYluWY1x~~b$7x =bp"ꂜuկ~C<0<:7aUq~gLJ؊ؼj=B Ʌbxb8c瘅uں\~e/=~ z SXؘEh%ݻג52xt\/:q1{3 }$_jD<Tm:tW=օ% !޼yqWHz'j2CCjmcB+~}<>X,c=~[kI&ۊ4oR=Ho_DZkQGW[1_E؊^sm0ɿJG k *\5 suԔ4X*Z]H/yoaKb\Ux'vY~FW@s*gGo̟β(SS5N4 +ʎ`E +*/qYfgz=sChMQnR/EQr'vd[B?yCC7kWTϽ#Md#[g_h 4d1S'/mO+"'Ɒ]>_yոTa!S#][R%fm |cWAl?n{ |3;iGۇ[+~/ V uJ f[c1RQ+,yteٝY%ُec)zas6 K.sɏYL{W d9U|VWي*"juj_.B\>k6+'63|A&s2 +r 3w+'ThP׋Y)?|ܻz{HpMB60^ @k_gT]cbn+_2YbU-AN%Cb,Lϋ Qꭨo W;|iqdi@/'ݼz88!v"yWDՏZ>~vR my}-ۻI c!tL^uare+΁ʌ?6,!wEslQzeH֑GU0wZ*;u˹wЭ8sPR<y꼄^ŜWI5ë ,C38OJsfH:d*9c8U +}Ǝn+goYߋViO_+^{T/:O_1rUY]GoD^Dd\ݦ]i/kAKI +W0x RɊ͑_ޑ^e&FarxH~OYm/q|t1_E`YW6x~gZ-(>{0֪XySRb ,.-Ñ{V Slwb+K 2W)tJH52H +$T RPL@9s,J>/؊+J;4<⫲Ofcg*kVfeq'^* X3Z̾EB)\/7=dX\{_ݐqy3H؃3BALNLKz*Y6b(/ɥ]N=DdDr'X_~+f3~'~>$qlQnRW'g$'XcA{Z^*2SW@ +Gm5!b__AJ:o\1z{SX\v W3A=Al%+T_ QIZ&8\p3/ w#(c;K|キ(їMFE2kp_0dߤ|2 eX]Y4࣯Wgj,|hv +=*qu!jMlŞ+:83\63m ϯ͐jJ'/'GPC^BE= FǞ-Ce a/{Yo|Gk_x)1/ n_>FE&O9L_]r;<F=ZYxFV|~c]SYG1Nx ؚ_a=*Yh$hI&&(V"_#'JM=R E*X:8ݵp65Z7 ccV; `l+S7_YWٲlh,u;_SuՋ9w$2Rt oKאOKY Kg'6foW<^]Sk3wXbC֪)SP=`mu1GM]E*k*QT.t-"B Kk|e􋳃v]p^繁ʬd1dGpQb:]y}w]<_>9N3,~:__3d{D9zzb׼[ƿor>\ +sy~zԫRPYG~ļǩ ._ӊ}9jcmpJm%U,tw%<_eFpnb1>s&~^!@lV^_6tj)Q׈P}\['_ OWU*lX,F0aB(>z?(u +oaMʬ}][47=S߁\Գg {ҀsRWd!q3Oa`0 -CWP9Lu5'Fە+;*f +֜8 +VUQD +FSW.ŲZ;UTaa! +_jxлʠ8FFȘ>>d嚑[lʠ=7; N#N18;ەxqFYއ>1fȲq5rm͂ZDA=ID"yv;67û{gߟ`o}=?§PF >|hjLhG"g1<8x<{PtP -PҼL((ˇ…jJjEUgy%0sJ߿;*˰vEnxrz||d ǡoG涽&2Tu + + + + +)i8-hHp㽨}SatXGW߂l|M"{adtLUQoF|⿮J!ceeexʵ!{<}ߴJdpBo{ƿ~IfKLu3ciy.G|_ڃAl+3"#fb2\VbZ]^<6/_w:_MX}#&{=˳k|!]|Brqa׉Y~VyQ\]O\+rFtuv`Ńb1Bi}X~8 Pf(`rĸ0{:bļ YF*drVIԍ!LM`|f-=M(*+B[O+jjP\c ocmC>.؊#*}o=nwU)1x9P\\,تT4xH|u3. Gb,|N| C\׋l{fx";7 +s0*ޜOViдzX;< Wk6_mm'G7S,m@;u|h'#o~Ph W*PHwwҽs 85fgP|eX$fUqMy3#EFdbRUwu>bo͆g<0iZdaa/ /%%T7aD<-}"+7O)ʵ^Q-cmd41% | (P|+|0FcvDnShX7G+&[UZ[X] +[h+4#Ӕ:+MVFoZkZ:is59g~j;uNs4"[<8?{"Mʵ~ cLF]g`eV.ck)7ފQT2VUN +3Ô N r`=( +@=J|;+U@֮4: %z>UUU"|GzES8/uCʉ#OizZ3M84|5u +Y~8yvCe|l+k8>u򋳃PPXFfy;)t -*_qZ +~F9_Վ" +K`J§d&8-ќ"q\Sk]GB;Z 뫨u~on033Y _q})b*%$ 5pUt(/Bs:PG?&mrw-=( +B믢 Vt!hAlT +i ʋ+gϧ"ү' &^<CIúX#:62WjrˠX;XI 044eo"L+f-,f&n:ҍ*3L<Ou(_% iC" ?zZZ"4'r+S<]9V8{P^Ӑ+#o +%o+mn6\*]EU5}#ӃOk5$jBu}9]O m| + I ?8[L9ʂ:&y^}/@bx^놽X"&P VMYQw+ hW4&\wbVg+'j)g[=Xmb~J)U"03p#CQ<1?7 UKaBy[= |RiJg Ժ4skT*GǿcẔS"f5/cјPnШFү ++|p_RwnVE.+:0naM\x,T7RNw0 c[H%*?T!-3T'1}a̠s<T R% (g.{PV^|4WGY|0:vS4j>1;ℾn`feZ,SONvΫ]=h˷ +_ufX:x󈽬dx=,-.\x}x"=w{N(gԝd)/JW@U\ࢼkW +}6Z; + ;ڃft k5›]OIm~$" ɍ~!kIЗ_}H)1x sy=Zx1k(,G2qQ+T=HK~}16! |WW^Q|v8zO>zwùXJ bd3s)O]<.p; B% juu"5)S-͹F8OIr=]5UZ֯Y=&X ͟}JUi֌bjZPPBq~0lUuh!]֠д Wv.!m:Z$?Qi?|L{K^ ̭l:.WT1>&166bswW|kg{2__+k_qAwJ9ޮ,ǵ=}]88մ͞9Eboh(.*'y +h2ž13Yw-=y5HNֳX"B\!W9k۳ ѓE5 !Ťu]h\{e+%VӤ[EتB\.8+2fH Rg8^\RHsi|UW4ҳ(pޓBJs-[F7({Bcr'WԴ,.̿=L+K kO;JCr0?u(,A8_#eɸVh*[J?앰W /0oMN ֆp's\XK-" Swꠡڵ S:9@Oo:QTV|\TֈzK|-P |5]1_~kH!R|V.o*iOhښ89ڏWD=~!WĜMTm=d٩{!{SHoE}CB'z(o.ڟ +k {XVtM2rT1U׹x]Me/r-غ8kөq]ԉ +\`I]1j&Ω) +ծ һ(,BM}jQRQrT v%}T-FJA8R{d +MM?<_}۽E >ϹTt˫D;΁1Lgژ%&OrǚML~r]qOm^~cSPFqd[!n?}x'lZ?I}H_Sv^t/ȏ#y,= /֑RUIm1E |>`YQZ'2W]c+׼|7t PG؊sr򕶢S:;)~ 7/cQ*BJSITCCԮj~5t/oΑEYp Y4LW*+ծ4TI)V7}v$ܻJz||/ vc>+h??kyen𣢩FYz?'mJyAKbH+^ gEд^4[EIW)K^fUSsJ\&G)4_ yAs<)AkQ^~f3oo΄&cs?ksު& Y2_)ZBWH;Ү,#~Lsgy6)׷3u,z9Ş#e ssBo|:/nIHxI/9D{i/ODC{6#xߏ`Q i6(K OوZ\5Y7\ JKQYU)>ȵ󬉱~No25.[ðHggHM^ Rρ5+SO 'Ax`qTa#,CFA=>r9ފzckY^S$rW3r+$x3Dje竐R|3Tsqk[SZZBV-FG:-z(ފr'C4z: U=cٕ-d.niWY_>+HyڴT$%cq~Nxﯯmb}A)u/}=BRfϠ=A~h2wB~|F,&WfYb-KqVim+0v/.F? +>o1x|6W14:=4 !|=(Π9zkˁ5 +8dsP+Zنml):W= k,t] z3Br\{uT +i C_g "Vbjin(֜`>K/X*ZGUB=.iXi|kWL)|ꮾZ4:G΁rVl_`~BWr{['_ڇ5t?"?ꑙ)ka퇜4+RaC.1Bb+}z׼<9s4"-,-M[#B&ַˏALs_ܛ(gp&}}ZԌ(goiWSf5z{z3x+}x ]E +~w3?R&]WsT }z_ve9|>2;L&kӡ>pf]Ӹ8kAE+:PLENj W׼[__z\^.oEBߢ#'zMŋ" ۷93{ Bf$݋ӾGR[6/3!+ENF .VW?TT_5Oŕ0a?= ;SnB N,ptٛc›X(b*øfBpVk>x4/-x>J^ R0og1o1{q>KoEu"x>˓!~HUE'cIF@J@K5yyvsoh͠G| P"=,^#剰YK_ c}k?FVX -ޔ"''ΚpxpWmT廽s,ok)Fy+w!+k4dr{h^)}[O;{p>?9*ʯ+LZ/C1Y4dZ?MK_ jE`ؿE [Is>_+ *$*P(D͟t nlGoo7v70sYTo"Ug%MSFџ frWAݿ." =zDQ_ås_)J-Z[ş|G{Qo݋3~[wx2/w}jqI[h i>hR &Wchɷ {~ ?40vttGH&RY?p? +uWd} oiN_4Ȭ52[RNQڏb?%Vӓѷy!qH[_[|6O5ؗҞϺ|dI"oywvv`:/ }l;ǙJmT|?C#TT\מuXXS;8< JIr}y놥 ѽ+>R|z3<r뮜tN1cOUC;/){4ϓ--]u'"DyshJM_54Ûk?_ml4utgu׼=۠溤]}#_,ctbgZ9F5/kfP[o0't}q!n ;:k*{sP:(S_&O +i 9E^7Ex& P\[ud)N2,|bcH+lm $zoѹ-'!m^+>3ښj)c8?%|K'Bb_QiXӠ;[z;܄F84-Kn2z7M~8g)WmOm^GzǞS)J5\_C#R^0C[Zɼ56:B{AxkS<{xs*및nAqiߧko(ڪ|#_n!8~F5+A)yb<_q}v1C)]G(}okˤ<3[b;MP71ca.~#nB:26ɋ!@1|byy޼NnԷbVI{Bf#28B"}r{]˨A{]W᪪ɩVܿ9=K-js6x9demGEꭸ=;|,j9gY?JKH޻{(X}l7v>;ֻzLjW(:ǞS}gy)1xAs@M$<ʼԈA,-̋}YG|5^Edx2;f $V{z0"wz3!- +=BmG=T*d!/ib"? -+֛!fsm9E^COg XgT?bv ~}m!8:;`` ++K8bϗW#L*n+2#Ӑ!4Eiqi3fai?AD`e+ klƚHqȶf1N\C iSsoE#LX@t.Ӌk/O1^ R+R<^ߢq N?3p'kMcwwj>?/k9ԀtXXg2ՕathCJ[iU=(넵Ӏk+[s`Ws~ŋ!ޯE +i 罇.avyJ׷eoE{ +S-)z!Z@9|8y-jp +ru_ +Ҡ ,sWmP6:O\,j몡1gBo⪫e`35-bex_s_򅐘Q/x[RHcT΀,V%zfo9=.l [xxG&?A+&ݡ O| +CTEϻ/[E-w϶eAIE*1UVT6[8++Q. 4O)pO3Wމalono[>Iy .ovɻ== ]zTg^x.dO*_55bW>ը+1'*dHsݞ~,iYiHaYoj}V^|)#:oDk^{By21D9M} 67prމ=8ߴ0 mk۠ +~doQr/m5:VKE<0ڟ(ׯwWئ` AIZK z97Pi08׸VdANzHPҁ)/%s5\_:y+3Vss3i+f ޞ %"^D +{hpdމ޳̇]*/+Mg}Z묢 + +sG-< #b5,֯u47jT=$O)լSPwIGb^1R&Bƹwdmwu_ -ş3_򚏞S|>bk1wϘAo)1'c ~?|H窠XZ\Ζ)y_#*s\6h˪Cz<=K0-M9.Wk[ș:Jx3P wx鹖 ׻,FN\%Ǐ̔U^Zg>o[-_͡cՕbNޒf_)>uyUWW'X-O=K!cdx;?5c [G|$HJr%f9gmwO>rD|\2ThaXh`{q62cq/C< Kke~)T9$Vק>z23.)ZSooTxO|8}w8Lwػgm}#Bߒ?o)NgNJWX;}*(_Cnaέ/EuIQ'9MQ{m<ƌ5'^5[*SDc5.9S}̕8նx_')^'C%Ý>>Vq~s[\,%W Czs[N{qtt_oҟѯ6(n>1c=R*Թ~ǚ!49 y=WŚ5k֬eҢ( + + +GZy}/ϧk97U;V8H{Nm}׊9cUWhzcf+_ԖSE1f?T;<1cj +1_yyd"5IXdZkcVYfZfJ2+?++UN׉JmW5O]UvT[3u> + U}J9!_QW@A_&`[mů\*-$__8x||A3LU+**֬Yf-3l[,_ECPUW 㫼j+]h97=~ʿ嫲_lUD|ꯈ1[f͚5k4=*]u8_uNy߫oQ;3J'Znjkwe?u&2M~Pi:mL +P +S=~Ljx\Nź?/w~0,f%^{`aap;=֬Yf-q‰#bqDŽsRkIw!qRtݤSsI<;srְruthJ2?x m9.cZ']'oLelWT|"~*QnmsEM֨ *.ϝZYEYfZߨ垔Τ$Yx1If\z!9s1_dƯe#;kMAKuZ~'yTՎWpf-^:]f6t5fW|RT,Wy>ʋQ+ |e͚5k2W`RFՐk6+>V|+7b +2VveթiUJUM[ _9@<x|UT5k֬Y>rkL_pWuMg|fWJ *n+}~+L]Ugį1o奊_#3ԅswA8߲FScΣO5k֬Y;uuDhUKBI_m'E;?X=Nt]=VՆߞh#Sa/}iqm 4<RdžǯU|Em:W Ȍ(w_*'J0_%8Gȼ\{͟%X5k֬Y;-Iw_QtYm㯼q=qxStztCuMN-Rާqa/9M ^7ى?~:NIEZ25?YRT_sz fdŏS;₵J~ҽ +k!IߣIΏgŚ5k֬=mc^mtT-7rr)kCwnve*&Ph{*s0O^}?(yzޚ>m: Vof/2's^͗?7:0 /2W%|}ٗ|Vԫ +)2R3Wtw5=sY.cVs\g6j֧}wT}yhw8*DǣYh7~F~W5ֽ{W֬Yf*^=WA/n_3 +󞫜24&_M`NjS1ϋy&3>:9ߖ+-wO Uh~0U\K%k׽uGn%CYhg ZAo_ƕ? W'lxKlzs_1|_`/|>oW֬YfWJ;,Mkc|Wgp s[&s^2oe Ͽv=g>׵=<Ac=n~ǐ[+2 ǯj4󋜁`UZ_QcV|ο?>1<)FG9 ?ߘ{8fG0|86~| >81 9 5ղCǰ+k֬Yv|UDS,7^NݛMw/(Pu_AǽJ=dfZt|ŮU^Eu dZQR(V5|׸C1fa{Ĝ+k֬YV%cXOKsG1|>՛ލ~_bK_~sLx#8\!I$_Y8|Ư|l֭X*$%(49FMW>/뽂d1j{˽"CV:- \&;x*S|Mʣ5ɜ$*1A|ƽF|K_a/pO %Dt~܏pẃ-_Yf͚?ð-bhpԹ!^ W>&k@D+A󝯣ßϋ_bc??pͮgѫcgDvkcXA3L$k eKL_| +W+<$%g@ |^{| |׮N~PwŬ +UZxOaNғ///pmܷUU-^2?gbϞ00j3ժb_OOOc#GGpGFb b͚5k֪be-EVFnӞiF]װ#" : y4l>P22ځ>?<ꕃ5{)ꔞ'򅼇.y WoԿ|?#+?9=] Wjk){^x [.fJGJؚo><~|v^#K /z*<̼e7ּ\߰/?qOÿ=ŰX(!Q8f|i?SGb͚5kNdcrr{b>b6rw1U R- k=Aե4'1*&̓|h<&L2Kҍudh~X!i򕊭Q){ k3yLsJP 9?홭ou3rqQ_r4_b5r2utܠ<;_~%.9.y_t8SP\jӈR~pGl1y*֬YfDͩ<و A^n6¢4G&Hzd=d,λĘ B1obYۃC ƊK ,O+3W9u#֧/N_4T^#;׭50b`,C"򃂯z|k6m}rj_X`y,Rk7LƯ/~竒Z=J׽^ -qծS3X;8qo?qǢ],'GUf͚5kiJhVQ>E<׈.u-YϿ+eRS +O~:O2f< &_s/FY6- 9卐_1 +Gx3'b~#r4A9ȮSutX'=/Xl{꽪~i{W}PPnrsWzk>$ܷtM={q}5/Sz+|K v 3]ZSpTE8֬Yf-ɚ">bgg_g 88WTL #8 YGg|mzS>-[_nNٻ&S;׬Y*b,_q_sch0J4Yq*@ zJtAסՕӎ1DZd85>4F2f܅A3q9/vsp%b"}"vf}BVx r)ƌ mÏ)]QskeJhLվ 2*U݋JkN+fhH9w~(gRj݇v5k֬Y 7e+sR>Rk8~%zlPi .Ƽ- *'_Hi]1ǫz򕳿WakTx _ kCD%=ǡިh0aO_aW`u1o~nك1۟עV݄.݉c¦[1q5DѤu7kEuv=SkתX}jEN=$4@1}fy=S̮Vm*CIimEoB[Е +j wvz0SZNWU,`/?X+z0/0o0ʩ$ɇ-gyHDʼnd),f\xeގ؋ğy>ldNo}aǍX#jF"z/@y5?CqX{P淰1婏1O0Ƀ!lyC +|ߏ O}+xcw<˯Gi wX|sX^\؛ؼf- }K~/cFn&rLLƯJ +T 55WgE|b+;-=~!Q=(JԵ;uYV5k֬aɭv}|4 + )FD9¼,U`aF-ڬڎh撯T=@[Qt3)L݄}Bk5{)ϥUT0V Dsc)ea,l\y^~B?Q=!bUY#u, +EPd5탬zUob*f_qQw*W0sY_*7}ݾ ^5|5VRj^~_ֳ{uY^ /5k֬YY@3@ͨgeaM_I19ո=,ú/ۂ_szRt(\$ڮ-]?n7IY7=KBWbZC݀koS0 ڏqHk돃)R =kԸR>q\.Id"ɨ/$q];̦VkyY9[dA>yEQrnF^E {Q?fn?veUݛ>7ԗtoNy̠x5\sꬦkVVCYf͚i}C~CѾhӵ/ڠNæ(Eĺd:( /]ZQTZBےEE(sj6[xo sLL[Էf5r_P}=ZhybJG-B5p[o +f]f"1ڧzkX,{m̾I [sF[n('o4v!ZOZؙh9`*:A֝P^SndY#$5FzQr^}K˨ΩXjF91qX*8q\baTO12~Gs(oK!؊˭QCr;;'<"4"Ye=^&ųEŊ;bŭۭ=_XʙG&YV]fEŰC^ŸA}0⨑N};=spcW?qbb[>/BY1`"OΏ}C T2j<}.hռZW4CEvBj/0oaV4Q j8py^~=-rׅ9ԘWОbX獒 1^#HI bd]d7hzm{YѤ=G!39;Z6;^Eaaϰ70irqe:n!PSEh<"TRU)K1pR 2ODe!hսv*sKW59#VEKνϵeYٔs<0M7BYӌ跗;Mf.B^;J)]T%:2F9O&h2\HquJ VBW}\+k֬Y_ 3=fKR ʽ~k>üh8VP-OPwb++> +Slz5,,Y̹)ڱ)F0r0-.+?VNh'jghbUŞzRW]o~>zc n)!G18y $d;=QN*QZ-QԬ+J D^Q>`*ch;j6*G-DI0f} uǛrlzlyd/fY+]~3&ۉicjT,ڎfKkt_s=:m[sɥ|h?mMc&cq8tzΝڣE8ʜ'ZT|u Z~t-x\P\&ַDBw8)TX:O&[k6B^Ĩ +gK|udbD'M]DõŎO5k֬Ys ?GNϣ߳ÆW_3'S``‚GDZc}zηqŻ +zNY?_ +___yZL)ʡX*/W6?ڣ~;Ͱ&F嶢eKPL: ۈ:1bEMr0jN㗢jUhx]t5:S.ӉϻfCyiMG꧉sAG_` + +޿zw>!;va=`ݘnx'&\~FWߌwPz4]t ێK!f7܅ޛE+B^G֫ƹIOK2LV4-| .6=>o܋!{_'~TcgSf_1_QL+.U^_&ymkMQ(~KW?QNmլ2~k +zCGFW]AﯢJeVݚ5k֬9VR?ߓʳ#7^VC$AQy v"tm'@s)Z J|Uhbu+8UaW&_=q+jw]} _\W"嶣3_/%Sci-RGo\g#'VIP~+˜\kO*J5d͐U+ GV9 Yg"g^S.EiQw; +G+.܈6KFעūڭ ]o ]vEŌU2n,,|c{`A|⫿a4=D<ܴbSW_l#g4bdŖYfRF8TW$ud=ZQAV iάC:MKˡxOcnn 5ډ:m~ +c} 6 z^SѦKYR|%?1'C>hۥ6l,A\8l8͍_9J9M}ϾXO$Qvr< u<}1_n׽/ ^񫉔pg_}/8\\k p;Tz5k֬Y3Fɫb%Q(dK1`4L^ ՞ܖcvpgp{__`Pg_+W:T^H5 gB$Mo_}z;>>>>kE\E ky 32k1_+k֬Y;͌=Y+ofR: +r\+Gz`Vs4ޅ69ų[v7_MĜ:?U[rŹ}%u;.[<+s/$)Ra9L䲚C[QYkSkmD^OĻ+~cVsl(sL eYKWb/Rc)?KC?qc:@5k֬YSf#طت*v<)4F_hHZsv8OuZ6?h͚5k綩G"Z蕻9%GK]Ű_5?\U S(~M6V`0רҺ^}ѱjN8п}Q\_vc;t= +/(4fvm:q2nSe +.maz`1%8}Sr'61(-6=oqr1-*̌>+1rOĪT]_Q*IkC_& |EY8&8(ώ`.ײj#A|mk֬YvBSܣ{<߲c27?gNcM(P(f^^u@$YKp}R㹛45kfn^GsKCxs\Qŵcţzׂ֮8ϘٮӖjmɽ{DXiOnuwHct3U8}ҋ{,NiE/(}dE|{7Ű6sDy"-"sQ$ky~l$(on$|w>y#/N1$t̨f ?(;ky/侘55эMeHmcd:xeSN~QƓx x`LV d{yDU?S8cY^#='Iq#[OqaOa[:ΞjX,4r֎ƘڋP?ǑO>f"M+E8Õo`:FjxM+m]f͚s̹"Iy&_lU%shW\WB>=}=ߌKmsdu=_<,Jd*Y[jU\L$@rjK>J0Ե4& +e*}VBMqu?Sˆs$_^4;sJlkc +r +<,z>AƘ8K'f8*bW'a·DZ1L%֚xX5k֬f钩?\h<5ZBqY==9 |:ڬ\# *?c|2_)fDZd.PŊL4O!%_Qz"PbgcUf;NJjN|MWW!15B Y@k|dxL)>tHW[B>|u`;G0ҩC|e͚5kke}9?|rź6z-t 9go ]1j1莗QԺrk8y%^ksI_|wo-_BH,.tz+_^OyhLkA7?9GW[3gWѸJڇD_g9|j_/}ao}K|%UT_QZ)5갘Qj{b{+k֬Y;M͡q=c;ui:_)θWC8FF9e#w oٍdv2v{S`a1Jd9q'?W嫰 +g4Mz~PqUX~P?'m aY&WvWz֐UW$z7x;,(S}ǭx)U/uY)/ \xƖYf3^K|{奊x|k@[ia_P|VCLވ>n=dQs5BgMucK_`Ōw,)g.q֥b~0|[_ =`rkW9Qk֬YvY5+9?9P=ox?~e1 %Is[_բU?CgNW?*#]kbnU>rD%_|R_4Ie;V^U+^犱~2MX$*J0!t٦G?bŜW>䷿qc~|jWW=NDl;f͚5kg:WZPi8ANΉWf+~Х Q{fxO|qӃB{RįhW=3qWW^D5"qҏV^h-W ?ާ`2s4<=T9JiLkKrџfJ2WhWv_Ib6>z7Fc_)?8qg8#:G#S=y ֬Yf1)4$iy^O:m#9\h](Kt'>%'3nSi\jmp̄k *..s>J*WUzHA8NsqqS]_j\{MOT{}GUڗ{INf} +֗|UDcw\p\z^GpN|0F +~AWchyCL1Tt-H$K'bR閯Yf\2r3%rFNZz-mKe0Gլ:37Ako|p>LVNnK?qrrrR"rAϟWoq&U/b#C#cRF|Ox?=][H[)kk +^ĸh~r͗.?X&tNsreݞǓ&$r-XOHa}W7ה/~uCir$֕C֟{8~[f͚3cLsWv&yTS`!K9YSE@Iy}L\;:7,~Ԟ̩cC63sx}W,||E9XC3VAwZ/ZWF\OZ(=Ⱥ~yLdԭJ\ujZ!uF*9 +%H}ofǰmb/`Ϡ-(k:ũX]nc@!X5k֬jЅwȬւ)rv1zlns>ˆΖ72Wb)0"[ǽnܨ+ixƬwcN~kby4k;z9RN 7璯s710mN<_X?}|P𕪙JDj t\/h_LW:Wz~0$&*? +CzR_.KUE- c1ضu#V\Aϗ:W%`TM+k֬Y;,ߍ1b'q+-+G#TU}1JS~0WfU:DՖ' ++WR=_biu:03T8??+U +/iփIRZW# +a|O$!fS6wҚR=+.Zȝ[f͚s8#u0e^.4+inSs.RIJ>N\s[R Ն\V}r]!tM%2SeJ_T?L]7\:z.Czۓc_st̶XzckE+]wsQ#f +U9Jo{RVa)QO_:sV9\_^q`ݺDm~6S=ϧ1,ż?N\ʵ1|s>^5dRx(/L%:5k֬wZ30/.*П2 ZU@"_ڒsqw1SkzK[21HW:+Zhf+8g'*<1LזoDPk[i?h#[k=VYNP-xzf͚5k\KFϝsJ]d ]?Yr&Ńw6*UɍaN+- ,j_b5{5 bN*U^L\buU=Sk0$_{5E}m)m6g]Աg߿5k֬Y~ ?AzU0#q+-jϟgd6\GW*X|Xz 1=_1'N~ mβ$*GSf䫦_Q VX kKji4RyL*k|`CbX"iZ:o)T|Xaʚ5k+-^3ijlU8) ҐxkX eJ_g<~|j}[~h3yT} +֮`X[JdJwf~癪-_` ?nf-=*-X2jZD[n͚5kM͡zr|Ou]Shh%8׎cznF닓ʴU~~=MM:z_J3^і4nj$׃1JX{B/eQdTz«"RZd^pŵW\&u5tRT.c5k֬7іڞa-/8!\1FۖԃWǹ5ouvjK0 =7w}4u,xsOk?] }of_鳰Nzz$?Q;?y3{ύR=؎#J_77>Qq™+Qo"~n]oԚ^[:<]帏ɓ2~GU|*Sc J !:mېu5k֬[|d+/Uc9׶{!3Jr wtQ'+=7jVn9VˮlUW!n7+W$;J5k֬Y*3G|_3v9WKZ9]qg.~~N.?.+#l=yR)\ULX-<#=a@3W4|񕷧TPVw#~Mf͚5kg\_ku|9%8sMusK''BnnśSYrꯢWp|d *cW2_imJMC) +czF*,|4Bju\rk֬YS7-gGG{~\:^z??qqi;A ~E^%%<_5k&⬬j}QO?Tv ګaccw~u'i(gX*i9y]ԨQFuz|ըi}BgLSݿ\OeLҖ5k֬YZСPxh.J=E c^>i/iK)o/c\*G͚(q +jU;!/>^/5 +vB~J51_;܆j R1f_/uNر.4)Cqq)1֩UEss#йs5۷kj*-jKn`͚5kN޽skh~6qX\o+j',oLJ/qzyx۶D44_q*'={VKo_!_T~ K _v>VXU8Ox߉1-1-Ɔ 5iXf͚S}z_֔GMyS+hF>nڵhLO:,\Sk֬YS޽{:OiLz߾ѦM+mOuOqݻ~ ͛>_hB\nl]f͚U|֥Kعs>K|׾&m۶?EfUؘ k֬YVu?ڷoqa0aBci<>|s=ve~Q3;?gO>ԩ؏gmW´`lٲC qϣ*פFzvkfge"ޙikܤIq<1>:+ϘYW{5k֬tlժUb9Q<0sqy>|n|,+8~Μ9< .U7ce989y9{c~zqfEaŊ8qykN1A}+YfZl׮]yI uRyWs?|It?c-{8~9sOxb~y]xCnŋ1`jCfT-3Ԏ;95[9u9t޽:1r cذa\%b05c\ܱsNџ{֭}={6z_ ~>k,o?vRl2u2e +ƌ#W^yW2 3;;V\λ['x?.w͚5k~ ފ /Ѝ#5 +ܹs|8y璡C}~=~xu9R<]~r;OfРAP9`~ω;}`RqRߘo}}zs_y<7s!yLy|o xǰrJq|ޥ^*x5Dž5k֬YvZ~y^9yCx幚sEi˗㩧1'qc)|`kTWc9 9~n&`{W_ż_g\[9_wZ:R@RR +yU`( 3"'.11j45&ݸ|kT@p}}_};ݧt߾#\}oVթ^o=5A~ӟW_-z\z, #8B.9szs=Wk/(7|\x"'t<rE㏗~X;09# +7&?|w+^{\?\~m&?o}W*cǎuc=/䬳Β'n&jew~9dM 8PfҭZv[{ݘÆ =gom7ʏc'Ov}y繹O#Gʞ{<1wx~+7C #F8b?ygs12i$ꪫ䦛nr{C=뮻:{2e9+K.D;8?s\|&\s̙3GNgg꜍+،ǍΆ|s 0U?~p\v O2O<0n~wwwsɚ-:G}y99|#~s>!VA|QG +Og owSÁ#<_ ~ż<~{d/[|~=+n mp~XC§;=1np \-3wCy{z H؟iӦ9byƖaF_ +1{%~W~_NlDΝ[n +䫄>N!D ORㄟ$vć?/ Ư㷸 :cjG|o1 cb v-{|?킷e||6pC߿wx '#C~#W/ }`.a _ a.8ǩ;>V`׍78š01g`$j|gu`* +u+F,x^QŶ0$a_2w=:q1?#>c0'2cA]'L-홳[Ifs16b-~l?9E^'_Z[['~;c$LjLLLL\y?E}^wE `*X _I>>C;?:w z0|8 ೌOm~/~X߇[xv;u[W/3%VdV~mr0.B ~_cnbO#wf`#_ #=}~8' n{br>"-vxp- ag^g^IcO}j[cOeؒ[cؔ vb?8.GRloB>͙kX۔1;Dž?^8*}01111)߆_&@?I ?(na >1ߎW"knl>Q#lZ]` wcfy:yclN`a +~d^1ێ?'v>`3X}9=7 iMM9y`x8v3ؓ`l+ `${b c1l3w2nb'c87 02v p;+dfe 칏me"UOoSȱ;bKl\qP;BLgr 믿3O>5ދO'W<=(n|N׮]-cUWW;V5_8|7umܒ;#s1> +?w"|> +>;+ym 6pgn'a?f>W،>ÿ0[UUUƞ{vyq |7͜]x/9+^Þ &1c0Ǽ+q|xGDɡ1p>ؓ}։ca79c|lO +coc׆9S8bcel~z:=Qn:\abbbbo*UYJWR՞-7_z}mҶDa0}'{ʸWkSlʾbSl˶aruv3111YBk6yb^Wuۛ.(aE#gyξʾOy{+ v_^#?[}>ϣ׊޻&i^ʯi^!qXKOJ]Q벼|kޗ_eJۭ|sEϥzYrSaC[yd͒O#pc( NB3*Ww\z:jY F XGm>J-3VmRDmg† Ėl#UzSۛ;1ڿ7~`qBAm\󈉉IVXkY+c\? f/ǯ֋5Pǝ)|kĘ_|bXy)׃`,ˀ +~ J;ÿ=w+bcl +aCg@zgg{_@z# waW{7hyoA:C.k&&&&k+uvzםg>7z_A9N-̚}}gx9Obǝ>SMבW,Et߻"6@w={t+`>Gh^#cWĖ蹊=7=d 9zqqd>M5}(}_W\bbbbb +94s9|oݟa0X[|8kgzP 57U!ׂuyO=#s %8 #_'уҋ}}gu~| ;Lg;G b J8ؔ|0|c\~˲󚉉ɚu=;|7* -=9ȹ&=ԙ!mAۋ7K)c^jl\.~"lG2l;3ßs gYC[䤈`k*=k_bM^~[n#Gl +)p-W[xcV\bbbbb įfD !^ECj@!Hy*;B=g;;'$lτ+x_aį%φ|>gIJAa+G〸 띵P܊3y睎ySW>oΰ⸃Q<+Ėy?g55C̵Yi1{x/Ey$^_}=~TM %C F|4*^#5+lqįcagGlc5f#`'%J;^^0gp7o |E};OWtbbbbb5Q8WSC̊a-9 rDl%vkҌV=?H[L`1#enRC~%a2|*~x~fVF|X|/u;Ħ.SWJ7f Jl-n+;$sWsbL+06c)vb3111pɇ2;ub֢qWF_?5|4߯L|928xށf} x M:5>ǚ˱x10lH<͘|-+}|;ؑX!5vԵ\;)[|CzĐC2^c="qNzJSLLLLuU9N LluH}<}xBMv +_ҙ|Oolxq&r."r}s1x QK ]XM ױ/ o#eL ^؛u.ۂ3樓#K~F$ QlL}+&&&&p>}A~M9>k~J>GZ{ٴZVo>J(N<x1|O-&kٟ֖k|%\s{x>";u>Y'HL83aKDWbbbb.'׸&{N;cܪ*wm5_k5mCc_Cu_ma[?m{c;rgh_oTsɺ,kkv_vVbbbb.K}ڨWA[񕉉Uy񕉉Uy񕉉*#MLLLLL*#MLLLLL<<wFid 7Wc_;W+W&&&&To |*j+bOUe68e|yA6xc+]u/6|e+ VV%d5?~u+Xɺ!WJUі힯6|s+ +ů:oW+t`UŖ\3O>[l{_Y+-U]G!;S+_l|Uy._ҷoUgf|ebbbҾas vǯv}5M|?U+jsdJWC&&&&I8nWꋽ?Ʒ{_ly'˚AUk'qݣJ۲"$tf%Dl\ lU+u]c 24hһ%QJ_E&&&&E8>7:+v]vqٱ}e?ya+⑫ROg,lJl﨔M+}>2111,m7⇹[/1yFx c{ͮkS؊9gW'{2& +ё6Ĥ3H+榫7̣0V +1=[fge8-m +w3s9Ĥ3s/6]9ȜzbZݸUc1&c=;MᵼM6o$ 7rMsȜ2q[|4{3)fJLLLLv)w>gweٲmJ۳lZ󒉉.6v @K&&&&kTnjs`y Tdbbb6K6v @M&&&&kkL;f*{vFVdbbb_cn1s^-ḏD\A߇RJLLLLF {Jjs_qק+b+ {{mqoGo?%߇-c}ۡ(QݫWoݻ64뽤i]Ns}`Xz6+=¸}A﫫ώع~tUnt[6&۳g߷_GWoUW[%[mk4w:o<!AX뭏B[ϡ_]x+i4|O0ǗENدØHB>qcc$cՇeߏ:(vU' +{\-\9_54o]>|A^=}g{Wn3T0{*خˍnSVDf<?K4z\-^a/Rަ̞`6em +ex_kh9)M6dS'mΧ82hE1_>4:wR8{J_qꃜ]}8?1n29h$vex|#ȃ_9&Ho᫪5{Uj-V| \2nW}߯˱L#b2yDY3`em*`]w\y-f5sl$n_ð6-Wyb-ĿX+oYW󔉉$u҂U&*$\U&գO>^MPW5`ղhǍ*f>fE>+_h'-_y +8)C,0>~U‘y_WrcZf!_mW Ֆ竐scW ,.7N.~U\ [o=P?WU]_%K,_M+}2111Y**_ +RZ᫺87|>XB&XC+$?XWXVWXNu`XNo +‡rUgOQlcYk`pߓb1#_.B!_ů},$ǜe"U+_~aUCMv֖jW=UW&&&&&>?*WISa<{TL%S"W3?ce*~ů˭|8JU.~q[|U]W<_I/VWĤ/u|XEA(r񦰾=LzcQL'[+hea]M d%v|qUuU _; +bD!Oc'%UˏKU9 9>o-L,͎Uůj n%MU652111iURU|&6)uﻜ^RX)_eV^jO +ƭ-jK/KJ9_᫪|uڒzWyVY\ݙ_ϟ_EmQJ[\GTC+G/ݦ=KN_M6R4WU_DWê!Z?y&~b +q4AQܮpd7||ydo~Istw=/c4EUkOPgI5Gr&z?Ukbs丹{ɘ5>=|NsBզ-ǏL}v=?F]fk2NَmT`¾QQ(GCHWb_*!_Q h{6y{F1 77F~Ք-=dYhĞr qhv>h=3ۣKmn]}=2_?X߫ErlEgYlc\yO~gUU᫾w3?pPN(kRĂudQ̠~k>jXR9u=0] Sr~>N5>i߆L`\xtgcxI$`|s֍!^m !~!}=}Тpd_s>%X+XUPwueE9pIἅ3wl#;c >S$ɯ1d]0SÃA6.|~4[.Q0F2wqy󪉉ʱU_s={ĵ%|QU/GhRS%^>jՏ][Gi.F<*wO|q_Ǫ7U﨟~ +%= u+WA/ +sRi |ZgUXh{cne*zj|4stejU9W|:=e:F_[QXXSΫ&&&&WݺVҚ"gUZȮ뫏{}F񪈫ϻ^H*fxejl}P>0-uk&ZૄUAsT  lm}`tlqַ5s17JR|U[5<_z|:9B*RL*Nb`ӄ5d}󪉉ʱUq* |UR ٨NGPg+;7*Ԫϔc47U=5V)EKWE |]v\|ayɱ?2kM\1+zuԗ]a<_W[SU}m +lx+kӸ3df;_qL\!*퓐>&WzL(ůpma^51(âcՕԅeǕc+ _q-]ru6>9#{= l 0ۉ竚V*HUu =ޖr|g(StʵumWU.fV.?]=|BljUcrs _O +{.dCjRʳKI}{aiǮx_E+૞YKUvrILڔ/W_R _r\_SSWK:B*O#W%=zgyo*drל=_bY6|mfaU9_^֭5=_W&&&&W.K<_y__Ek {ZX^^g2^m2We ߻^a|Qߎhj6~UՊꯊI|‘\Ԣ}]=;`WUg(%W9k"r6.&QZMLLLV|dԟ1ꎳ}@4gc!=XHG+4mĬ±32g)ݎ̘>~՚ >&1Y_[ڇח`;EDϓZ3(=8|5Y\^}_P>7mJycJmGtdqqp.'/|EO3ŮGGIP~?C9;{yyI_\힙^^{f{Rggͭ2>#a{_;`h9ehO++l兣c\ԋ*I˲ϤG_KSfb{c]/:>N[Uyne3kqԞ4WMLLLVs?'UQ_3NIstA}l/iUœι18<ʍBt-!Z(<篢a'?`[U>}Ke}Z._u~xo罨~=E94'pϧ|^1a}N}5IOmvmecΌٖ|uW Q/s-suark<]0WMLLLV'x@QMw|*䘄_.P2^WGO* _iUg_iϏIg6mWQ/"Έ|Uۏ;z|}o*[ԚgS>VT~]CtDԳ|U XnodmD+.. o|?Q65_%wVCJ?gd6Ľ9v^5111Y9įR3G|,'JųQo(Fƭ<[|ƳYW\%[㕏|Y8\`},f ё|Unͧ_3{*P#J\\FYlZqj7Zxd]l4m*2_AoRڨin;!nh_k[=6gW&&&&Wy7b $9J9H| 2F_.}gyWlWW*a ~B-Viάt\wW S|qu3~.r3b +&+[9 ֈ&]npp(rX*+=˴Xv2s|PMۏ\~0^paWcu(Fi];Śm*{_uS_Qu91Qb~W|Րj}_2t+!;VfU!e*協 +zgvMYPWy+WiwTyf-f+GsQ a8&cDE~ǝG刎t# X>׍qB6MY-lZݮ|'QdӠ ۽K9_ĮJusr.U\'_)NU (~*~hH}ߖ2+ J\(5+W=ʲUW|y(+_䘨_zb=ZWA-_e*',WWa}{oք|U[Z+WE_ +_mԷ=Kk]V*djW jXWw+ uM;30xüeI ,_?]=T=]Q-3K*G/MWSG.+X־%k ʯnBuuե9B +ׄIl`wsΞZPW}N{GDŽ]MLLLVdSٰ_~lW(`0FZc_"68W5S^=Wӱx_uy{c[8Vn6zjZ|?/_kR֚e(K)9V=|U"?VsqJ| S=<::I|֖ΩFkf_)m_dB5z.>oUWuE?]?*#)yC-J< <Խ^1 +exg}t[;U?sGgƚ|NRj _}q5.fxˍ46|QC)S+z^F={{u{&Y>~raX:^PokIIl#5/ӏ>O>U[b&q' Ƣhlt}h} U_=E|ۨGi66[;]3[_oǯb6l)| z}_}0WXv^511Y뜿&q*٠Q|Ř8l uQcVN +jP6rZkOe~Rӝ(F zqP@KO]-^X~ѯoK qOujM85伯nr=wد +b# L| F.+1d))%u VrᶣNQ`wX%$Lm3w.ⱔLq-^Èy>NƘMk2\3a|Q\/u ;옳Ig+},_I|Z R߻:6tTlխ&h]2IU9f_9ժcldۡed1-Äi2in!#̒A3H}ea#&Ka-ghl[k6ΐ]"ݥq4Yv5A>Fk yl-V|9J?wږs.?Eײ>!aT}_r>XۙhP#^Pq昽w>]Ce{zur.yO6d5>êl-lWU +x4^ymJswxfr^y(g8$hx+r23111YC뜻Un=Wž˅WYO]ੈ;SyR#[l ml6xl8t$b ٦i١ٲ٬ql8O6o<-&!f ;]:2+dyJKn *G69gV\[qmn.T[doA~e忖iS~˥ !s9'2Kec+[8N6nʯKpNS5u ~mgm-gkr5xSٞ:J1| ek=u YVWA_q6jG5iF SuٵQL+赢d8i껩r9@u+% ]I~I;KCdcq oH|cL9s\]y4]u~Lc'*4>izߜNy/2M5?wiQ?TߓiU}}12OSxrūgISKeSKeazedޟd~>2ɘ*cϲ]w/ya)uvOs7_~=K~\,Z.vus.T>3dX?1(sr ֠P5k<ec'%:V?[?)6].1\OӐ]rr_JU_Ep *zık,k&&&벬__žFhO-(i=̕"[M=BT甋eoo~Lf̜eΓO]N^Lx=Kɽo'O2%2e2rԧ+,)>:멏e?}Ϭ?+sǗw_&LZgg2Mu ʔ?&|"M ;m\&PF=~CG>Uv=;hiF< +_eW;J{%|82cǝ4fD r5uںRe-SUU#]6ZQwn󯷕nR{=qA 0Z񫴇Wir_I.4w n+jϵĤsU|)P?Hl;h3eΗɗ"Gܶ@"2^V{r+ɱ~*_2gd~HT+W < dwoa#rSQ_!+*; f`8^ʼnAO(R w,9EŕRx^uC~]ד:TTb޲ِ2p,٦n!ÌeGN'=Avz@;ŷ=QF|L;Dgʎsg!84ϕ-LXifOil{YWkד֓U#\N5AOr}ڝoza|/1$}1W}o?+JLLLLvJkW7@Ƀx+2rȂ˩>{ɬGf?2\/_׾ɯ}!3}!^n'-RVȝo,{/\!UZ!4~[mƦ&+KMQ"5er}c9R(nY&GhHHV=pR-^"X'raS_u3e?CtT Ȝ>YSf:~ϷR}ݲYR*+hJs.sxq4ں>Kk,jg8f RfeYɖsN,[|CuKՁ*;t :< eI'@v9ۙW^\'õNn?O_Li|tÓ2 6݀.p [ew󴌿imqݣ2d>Vv?iW'X9bx2 dˮ+8F6i{wrsZ-{^i| +UMsƽ7L{.E_+uR  mW.Džs5;}ƨ.3^?2mA:TYi+Ɨ>z;ɨW3_Χrܫ_Y_=|u$*T+8tJUeזe?zc앏W2a|Wɕڝo-Xr?_r~r++ėW1,=T.za\%z\zًK{}(g?|krVg/x_}=9s?0mo2d !_ʮl_rJlDtݺǽ` +ԞZ-=*u:Y/QnCe}'Iakx4h~#V4r qcO~p̽ii9q2e/["s+2doސIw)~GZyW&{iTmO2E?5p34w^D~r??iz\Mde&1EF2o>-}n}C&L u"e#7vؖ㫂ЦZU~B|*X/]5ZO:˒z*z?11-Vgbbb6Uz-@=r.'.ѣlydߕq7|s9dW>#4Ry _3|5S٪E?rr~ +OW/,W}v̆4xs/},lts$宓]LNTե;KWo~,gR:rԋ+x-5Nu꩚O<]?w8qSK?>Xo>}9HO#}_e܅Ȁ-1_E}u;0?H/ʮݢ|ogyzYo=b5U㾲Yf2reˑ"~Hf-u[2wer{ L}C_k)4+N~V4?;5Vw_h24O@P0LYsO,uk'>Lк4g;Akrozc㑪gt >V]tnЍ~$c 8L7'Iﷲ=7z)W_r\gMJhznz~v&&&SW;N\υ]חg_YYŧVSW>˱Yo||o/\~gr㻪|.CB~?/Xxn +9HORv\5ScY3e׸1C *c{Fst[ vR["wD~CcbSM;XZǮoQM|a*&}nǨ{M>z4q}7oH:Wc7ݞaX!#TQ z?p'09kxV&4? r̃oEߖiFr JNܮLt~DfNS۩(zKeyq0kaT}#9d>jj=_}|ܴh S6 >L%2R?څQ4׫Kv i =7Py=8}1Fs^H~5D=}uܵ4X=ӯͯ\݋:ݥfa{ݥNH6Cdy2Ȝ+c|ebbI[LpPZ]AsC__uKҸ/kFj>>ugrro~.^_i?]z2úFjzct +Wwm_OvJ 2وfc:~x&MtLRNcg=DTBjeefd&טxsձ(o>w#Svjc펽Dlg:p.+Y^C9}O~BC{Ɇ7]CumrmOȼ_?+#o}Q[r c/~LcmRELq$CQKs9/LʳiOծS[:U W''.8FZKe8+=F]*RmǑSF=8㬇?!zU=]uלe]c1X{#onwv$>^\ߨ}F+XI<׶ƣI?v | :?qò/a?-oxV%im_LLL9Y= nPW=I֞lsZ4_T<\^JS_MaƵ)lRM~𑘯UNM D +t_99w?/g> 9ȌW&/L\Ҵܺtv;MEQ-HuĠm W)'SS+qhnUbR%|O bquJWm26f+r'+_]_ҩƱT8cXsUQp r.7V[ݺp^'U+W/k5YN.o ׻ɶ-ZM&"8Z}̹Z9dĕN?}H^3_Fyi-ܚݩ/~;4eKk [s&&&&)onj~U]QaSӚw\ aSW\rƧknp8娝_BNWR5U=KkX?آ> +oO˵]WO+׃J Lc9r}4~+j+331_RW&TUy3:ۣ؊X\w|ߓyWǵ; }5M:Un]_q\pBjMz\2iWkt mQ=VEQ/+zߝ_Ō5vV^p{/6=w=Wڢ%UTը|WEW:P>_= 0WmW/i|s&11;L&&&뒴_%+39:Wۜ3qb卥Q}xK9@}o~Pxei͕֫gjmE|rY#H#&<`Q;_S44=櫩yZ|rxWG|EQG~&)_\W~˼ǔj]-&|6H]$TN{įԞ!=\d#;xR;bB(g]3Ds^P?N@W!#*Ք[WzqH<wǫ& RR:민6>\+Ge&%W˒+jBWZNrվ1[%#|ٌ_E.r%Zcv_qlqG9`y24+2<4uC~ojߚReE>J)CW\gbbbQ?nSͮv||5@j_~(:NkrQz;Q}3~E#64ʧ6:G;WƯF}L 8T䧣x'k岟2|9Lc:5~5G+OT^>ů _xc5T$AWWJ|~-N< +vJ}q>?R|1WS-?A.եrƤQ?{βUW#.Ȅ㔯&aC'tyU_}հ ~믰S\չcgY>"(fn8y'\k(j` _k<)@h>\_Ht|Y?@|?Uo募>5/*^Trޑ~Ly]{2dȴߖO,y]Yx]9w_?@y=foGuLymkߒ^{S.|]Y2dn+3'ߓe#>yk|m{o{&S̾r؃ֿ!G<7weAPdWj^jkuk3ܫw_?L9h :teѣGX?``cKnF:R.Ko᪇ϩΞ2YnW?+_@֨Nl檮)Eo O)/TGW/S^;Je壟|GmY[)/yyM~/<7u/%Stlp&dHy?K?.8o<#WƽtZOctZq/mF thctvm]Ʀ>ڏ~?5گ󏐉?WVOKU˕x#Y:>⥏~*q+ƹ~<.:d=(P@]_Mhй;X_t}cG2O%U+f}c9enW)Z~2__}Wo+[M4>RLÜs=a_|W.x]n{]9w``*xej?늯nW|uuWjW>,V|_=_~2s;;vEUNz/ro>|Upeܼʩ|We|t['kj;_Ui᫥E?2Wm +Xé"c+}9fZ=6gޓoX) _PܵK+/8w+ϼ-?X,JpVfb|W_]_M{$8,m wSS]̟'ސ?b{~/pm^U"F*ˆFWop(6lvW# P}64*.h᫅ߩ-9 +^,H }_$=9"LWW{O}eUfٱx0b@0+t>Q |_swJ2A|vp<|Ws5aYfޣ~-_}gdŽPש} +j}WG Ço+h}Z=W}괹~ wdS< rL>>䈹x0LݷVMXi/P@:ҹ;߻A~SZWS~FGRltL@6/1jb] Oy#5NߗF}SO'g=s'uSmwPR<|RA`/Elu&꫱_ c'hR皣6ny_aÉ^ԟp|E?W6k_ɯQ} -Ҥ>fYY]f@ŵV_U[ܻA}hշCG_,\w70LTb?Ŋ<^0? Q8VۻDw;+?])bV]eMa,"C z;S y~7c )'?wſ -SyWU^@ ,GU?R +|O6+"#YiD͊Ojkhh|iWɄ# 4>W]|Wヺ3_5^X%|ߖ(uKۀ)/U=,S-c?WqU+_]JsGoAsϦ+2AL3[W'F~37%,SݷM%6ٻ._zoү>]w>'|+V*W go\qz}Vi~HV ->[ug@,1OW_ t8Vu<#|CPvY74d5vXy }>+z|:G:e]W>C^ܿ'ro|}v?>w7r_ę>|J_b9\nRc+K6#o'kHW?Ţ\W8jMO[C_ T|52+r|:|dyr?_9jW%wG<yѫ _%:巃x_mH{m^f'UE ~S̻e/MG}oeߛ^U&kn}Uz2geCMzS2e=x|cŏʡrG=>C?_6dRԇ( Oe|ޕjg(Zl8W#)?ց1ZxqvZc%>8^qOE Wfk u[*kSlDW1$lBma(R|?v#OGy-wgBJ}cyb_T|.WK>2w\]|X0Uku׮X* ~七G _ Nsv4 \>WO(rGͿeƍ/Ɍvee_MӮ}Z&/eo$yKecΖn +9_F\ڌ9'I|[N[_ $r:%~쮫fY7GߔU_}ȿZɇ<ᕏK/V402[QLU,5eKκ^z 7%@uY/ZkVy|5OO-3NHy9V ߹:(bй|?W֠ t{* /aVBJ#YcjГ)G)rƳm'y/ڜ/,|OPګ?O}Ÿ"@ۚ{2!vڡ?'Y>I:Wdྫ}[b/>a_EW+KY|c⫓՗x'9tqMh`[m6~G5 UyW!?yAL9zkZ/~R],>}Ҳ$۾Ҳtisӄ𙽤~RR8F.Z,˗/]ƍW`oiqجX=Yb1s{1Q֬^%NzDثE󕚈^u۬ߛU7M:"CGKv]deaNMUyUEッ`'uuyerCTK_%__n>?L2UOG%}'ΖF 4n찫f;W߀ +ewc٧](_r:đW^85m^ +2 +l +lՋE*{&Of>XKbKO)S.T&aGd o{FnyW򝻟Y<&x)1OUGdeW<$/{X>{٣}T~Hq<'nٿԻ$uJ૦*͹`U⿊uIъZ_k[~YQ^jVu2ɘ#ϒ++m%)c&K>VߝM|9cwT2XeEͧ&^?. v>IW3Î8Ajz95Hz}76 sբO#_V$+>U"^'Ș+,cbh?dk-_?XI/g!w}dEeJè]2945r@u9W_ HU])ȜKտ\K;[Ծ{#<_}!sR4ͽSs̽vWso_XFU~eԚdce;!ˏ!j37TN~/X} _u UʰGcWmX3龜Q|QHgNg\)ue}z-2pQ|W|]oVU`q~P2Mv]vJ}+_/Yjŏ!7r/ˍ=-{:l$rH8c1n1Fc?9it^'M}i̴b=pq֭rwS E},jQ/!esj&F}_vLZ_1XWϢyQw*3&Dc|J|W#Opyؙ2I.?Zdɬ[_}.[veCO?^M[-xvqF_诖9@}z衾4dT/P@*GMx九>-C?d3Q<)[2z+G9{k-4o늗ߔ[2#3n~UK^?[ƟtXrl3ml3} w Y 'ΐ1P`+\<ط|ȃ[8C}_7)smzkZG4Rx8>P_% ~Cf)碻é'˰9ke฽˘30»uzhTlNd⃁D.Oヵq$g9.~Nt{eҭC>aOWĕ\?\gߗU!,٫K/\;?۴5N fgTYn?YfNڏF<5(mgvj7~SKW7m;PixtLf;+.*Fc5ɀ ++ҟ9 źT=yj<'~zf}`47whud6u#!f~%'bMv&>פ2vOZ=W?L|讞 g][%z)8f{+lk'-7F,L1'eό qʞ {prsRu՛ůIn^F' iu[0P@ڟxfnRʽ<~lC+;}2%l*be+jX܇A^);} 2k T6CVp|ܳ[g)SqAmҠvY,Ǟ+;;@l#CRzr0\w+&fR[L_gΜ&k׬OpvBRAdSk!DѻTV8Ԯ'=y^{7>_'ޔLQ}i2h* i3vG]5 2T"[kC9?~?E +R N0+9ޙHUMjr>*nl8}yN0T;Gi^ {E[Ǿ)SIz1{ޏW +~o\<2nr]J^}vXu(u8~X~(PF`weհΑJb'q5Y!;~Fre1u]}h͍~E1xJT ̽_d|L~h2 S]m rrc{i"_1fSsx˥seb;2:J+Fo']&o'c,W_JͭZ2[^={|y2樯G8;K;-Hˀ.6nβdbYp5*g&F9$gKL1k 8T=)ʱ;"Ѣy8[n~ȟ'=pm9aUbݻ _V_ s_ŶCۄ;Pb,h Pфl +02fǸ3r1_9+|ERסmFxg4pKUgJ0N +U!fuZACKzոN,;s^ɸ4WqW*Ta(PWWC UcyV)NXuudcE=Gg[43^=_MتʭERfۭ[O^5 Y31>Uֶ"Le8z+g391{9nMZ$v=g]|_V_EW2[Y0H䈃֩cT᩼Z8/+82X&D]eb>+C)sl|+`U`,{VWrQׅ|_IùU +]lmsPmsߑ@0LC=:LGeʵأϒm笑~3cyM8;wB`c$bu.}ʬ}88ߤ]M}}JI޴wWX;%U _W|Uৰu 4mhIs{j_,yD9;uvEUsK\,8;ZOͿr*cJu3c8Ɨ|y|o +{@ +Lz qB.߱Ԩ^s-8䅔m'rKRz[Ϸx^dkɊ&flw$WIU6&Jsw*)ncIy?Nlq9+W^Sl՘Ȇe}j޻~F:H]>}OOOA_Ecr{2ҭ_5'c`xJ9;:ǣw˟Ƴ}_ ++o->اj`_=w^co|Tylu: l{\(b1r*7zͶ;d `*VV>gclW_ͮ35.w[_w* ⢒\˷Z+ʹX ^h.W~RN*_շ;*_7u*K8kWY=T{N (P vy%1}_99g%Ǐ)$u b+/;_a[aۂ&|NIǤ|>C[U|[T. +nK1u꟫Oǐ7W6Ƙ,bIG򛢚uO-V\ZRș&H|9IEOJl}Y<xWL:*_yCw+뿊jagm]kyNs,^ڻ,W\{`mўc- sŵ̇-/*]kQ,?U>ǘQ}&5L?8Əeo?ڵyӥyQvܠs}sʼGcl,f/0#Ә8#fC3m[sޏmcusc9y;?\"qﳮ(P@C Is?]Wâ;gqKbn\㯉?ۚf?3LiMG[G:{y~yxvcP|ű??9[72V\עE8Wio|3gt89*dLjAӓW,S_SʷJs4x#{2U59|)өbE%N7}ȏ__mH) +@un?[;k"|UZӱ_%+U暷\k]Q٣lVg_v.;QJ\a|8ZX~͜"3R[XPg5aS]Cy|մ|VC4W +F#!Uc|RjU5|mMӀ +5}W=mtUUVԇUJEy`+[O*2qL(_'W1r*ro'Wo^0_ag;_c+LmuU:k <UU)Sߪ_%x9j_J_EYz^Hw5y0}͉n#Uȿ +(P'[T]|0bXW>n֧xYA>ݼ)W~l?ǩ>G+?\*)<JۨX|:|_L_ecYBpܟr_v>3KEW)JupUN%++{8& ڤΗYRU(kW1P@6: vbՋjx}Roٽl,ځ V1loW~m7}'kvWWf-֕o=q8'T*k-W8*[KUz_K,WeWt,JUg%W g}C+bUn|"]AY%<\瓌3{@:#E(|Um63g)JN9o+._06%WfË֞%WudW%:m8|YWy%ЅXx:}X\(Y~/=5׵@|U\U> +{+d_ +@uV5`G+W5o૤&[W9+>GȦ+sJ(1JѿV/>י,W+ʼnNIl *,}555N?>,WWpVa,*^>l|k*[ӿ LUQ=,Z.5^%*+gۋѵFLylUR⃁|duZ&6Ew!5#+iR0W|rCR3};buIxט:q~ ^DIO?^>|5}i[zwqEZk3V2>.S%{e\|vT;Y7WZK*^Ntmq?pet3`k8!_eA3GumtmW|̑och+b{VWL/y|-@uVcnm j5GlkU߽fK]bXɻGgcyLٵw:Yl?^_q~TV8+;>__d=X8i -Z־]k׭3;ϴ;_?שo\o7cz#4e|ɳ^_ +ԙ֎Gkb?.n~n᫁)>Y-"|U"|}OJ~&L) em߹R_6Wf(_y'ss{w.>⯈dSX]ti\Atl[nAsw=k*_I-uO>k:]MNk+_)6L퉯FAWy|ø ܵ$*YQɮse@|{?goh| fZWY{/cO,5/5Z!V +/Wyۜ-b+ɱ֮߈1% _JcJ&sZnKm0wZJ5ՕMd V_׊N<Qի|4.@u糨O+JضJg"|ڷ+^sT+ +ZWoX2kr(׷i]\92mdڦŒ*eUX*_}}yNUA|Dk0 pGˁk sDiql}(cƦ?%r*ᫌNjϗ +*W9Ͽ+uПڜ_Y Od3r:~(Qxv[|vdn}U]!*ɑN=xmE+hj@\y> p>xcy_M&\ͅ/U\W?ұ|>^Wםi*'?d U@:#R{EGx`*b $5%y_M?X٨7;-q 0Q{V㟞}*c˿*M ⃞O#UCcA,)j}Ukep^K|'fҔg?fõ>Οɿʯ{MYdeJ^*Ϳc{edk2ՕA_#՞3 +-<᫒C*5,W3l|q̯TAJ@0Mmd}P>*,fVXǻmRcecIYG緃`-[Y[\S*“k-W3▅9^|: +|17 +}dLy*Z?Y|ժ*|(PFEVURr +`q`__<._Ey"jC{Nߟ*ljI (WNʬ]k_CvpSI(c25KIϐW浆}*4ǒJr㶓5eځ3r}_e1W'|B|UᫎPӨ%-8kEut*P@ Rբ5qMJjrW&>8T d}jl@tmqm*מ돫i7jZViZkLl%{;w\5;˯{gi]ZB~Qܵzo7c F\>^x.Ӂa:uڷ~!2[Ƨ񭸮~a[i_e/_0n-ھ(4ܽP*7[Fmٸ  +eS \_kFgm콯 =WTj k8=+Up+c'Cyc)v_O?X;)s"Dk6u_Xl\83E?hq_=/>3]kd뷓_N+A_oyUmV߲aksg@FFa?gusX__fhjQL2JUMmvAċ|U9<$wJ8>V_%+RQ%|cܞLy,~2m$UfmFoxﵯ\ 00s +9S=[4&\k9|US_)=P@[*.ʳI["Wś~e0o64CѶ2=/\ۖ;!y{\f=߆}:MlM&Ի:*zz +99]k/dr7 +_m .kvl50ڀWyQ-|]v}a)疒w ]_ +Pkh] xT-Tfεwd>9r^2&D|WM᫼ <-W+\k*N _o>a\?W[&D!~x|U#~Fc-m'5U\*ܸKU/< o@y|V{ (Prۢ(gr6Ep9&|c5`+bIq rݏsyT}*"܇O^NӜ"~W%_ek>We|N)}ף*k/:m뗿.0,kbk7_~SWȿ*[}UM޺x Gׅoaf(ЖH .cS,TX<+; +_چlޱm\k(c|N,yOaF~$6U929:Lҳuټ,|:J0sM{hcqtŧhv=S_ka(5ss(V&,WAi{xXEUqN}=uHRV{ (P"J0a{4oG-u-q>~߿aj?_mH++qG^ cQ:E_33`eڱ<}pR juy{ k2טjdk)`+Uvxرcdh;)o֭w׷G7F_'d[Ԏkhc_c51[[EltڱMW;8Fhmg0~mgkknEEcpkctКj,#mh'?66LoEmMWܟСq۽6B~_>Z~u(P@[|r|roeqrrcp_9!~ej17#2B,OZ\)f Zk&2Xuak(ӎ}C]Жo~lNN˗o|o9rh9a>[ts#HE} Am6!r_g r(:D-;8+Qĉ;ȉ'moN6s՞G +ȧ.oխ zx3S>t +}3F~G[G;aSVkllJ[Yt\%W#W]G2P1+zv7h[0 +hKzfzH7yz9_)jlXh=9ζݔk/^~ߦwcoK?6ᇛd6_sGn٣y۪rm7mo޹6ƞ-G7_Eq3s]ܟ[[!U{. (P n7ktD9e,r<lr|phh9]\_?!MoMAEm|~|[Odwx@͡޴e=ڪ>7Fo~*q{ +Os(W^Sf@m94EiʻI^j^GZ?;h[{V7^H~U71}A[ޟ}:MY?kĈh; +hˠ}Gړ'OGL"D=W=#i5W/SNqvNjnl +Xk>;g'+Qڔ ;L4ɵWk|(P@PJՓ:yWcǦ*pW_ ++RWW<3vLk,j{sxܶ3ዎWݞl/g?Y}ݝ:RWjϩ +Ty#s*؄q9=ӝ 9@gvFrmiӦɢEܶuKW;Ʉ 'ȓ}og-0Co̙pBwjhKW?~'r~t=}\~Kv;f@U:32y-[&k׮;{w_gNkl9fΜ92{doΝ;WVZ%^Z,X0A\o~SVX5ܖVYr9:Žbُ~}^yd]Ì~Gk֬uֹ=m1p׿u߼>7NWW|E.YfH3M/^{Q3f= +h릎ŕW;07!wOw6ɜ_?x7[<~A׾5av3tǟr9縶 6`}c ;Ϯ11v|J+^l)''SO= ' ]}΍~c;?}m;Oo;݃ lF]LO3}c3Ӗfho)J+)29䓝>;N99sF2#~C_Wp.dSW;8wl@mԙs-sI'l%\"^x\uUn1w?3ru90[~WK3LCaw-"_~k.:K,Y&1[-6}a?#ƈ hw~c `pw+ oqr_d~WWUg3O|.]*]to}[kMMNW7'slC ~co94 ?pm';;mg:?>l1O+.gq*·΁ zWN:( msg +Xa|[`#y&bͮrO1;:;X9"C;Ys=ti}'7pw1>|9O{GdK[՞[ +RGJk3L=?'҇zc˝zrM791>/`L6?ab>hg‡y .B`k֍[+>c9[.87tM8iOà{pIpdLppl@mI/$tt"8*>P6`7x&c[p l~b +.%Y|sl)KC,XMÖ}6[:J>a/0\°hGS`iW+ϸg>Um.v_d|UAg|WVg4yFĦ+ַb|pkIۂh1hߐ:_xQȁ}ȴW% }E|`Q#cWWNɫ#^5P@[1m?Ά7,i-vvyR,ΥbrI)>)#ۊ։c ,LJmz<_ wEn~#6 v|l9nS \ҡK\ .ad G <ƘoF +|GX蓱`5]d_bq>u +NFgmh3<Ziqqʿ18w\/̵'T D^|>'>0/Ⱦl@.s`K '` {?Bw[fDN;WousNC l:tvzbC&Sgu 1#p=O|J`*l3";'>J; K~ם/nhidK`~gwsLGmp]a;dȓ#k:}i6F#f^6P@[s^GJٮqAB|fPStjw˽Hyo>Nܐ>dߖ3VgskhN]Dlg(P@B՘;_埕׶^@y{uGm|&Dn:oo +*"Yo{eEZӧfйXoiiIƣ3թשcY (PGQGh; Fa ٚ}厵9u3o4,06820Rʮf둃۷#I1f)$m'r!ϞZ@eh d ņJ+Øgւ2:|8X@uOZ?%O< m䶒7k/y~7ϼ__ b~s,k"y.AmkM2K5xWWW ̉>q%{w^ϼ0~p,#jLEcYZ>5p=f Oˎo'=ɚεk몜|1cc*P@I~̮,_LZ/}-ps5ZkabGcm> َ7>[m& 9߿ŬG&Sͷsq<&ZԼ0͏UNjMkY`f|:7~&pPjHAqRG&jR +78ɖzp38楼NME:ʿׂ (P ;Z:KXwol>:rׅx@;ȊZ|d`!>.g?87}w{}n5)ٲL 5zhYZ&lr8hWWgq;k` u}"Clr@'Ge mŖ>9b[tG p.-ujc؏^hKo5 ]/Y8|/Y }Q.߿+ds!w/~@|7hz=98 %P@&*jllt +;f5+jIaØP üv?Q[NNLJD0 66wem[xj ~l{~l[c}/ˮl2 HWWȃ +|C[k AD`D\!)0+rMc1;Coq^Y]'qUwX8'x>uU|,\v`Od`.-t~K#C +{Y(.>GΜ;X@U]_+#/1O[gy^b؏s6 l&cY/F:x ?|)`s]Ď=0co)xަf0\=/?+ DN6#</f5yuU>V+0*׋\j:k,+:0ΰ +_'}q〜)΋^O/~c 1hp7q~1 NtS+]2`ו+Wݙ_5Gf 1܏|_?g@?ȟ6C%P@$檮<^^b +lck`|zf'=ف87 }%C&F.>cs>lŊx&ϗ㰹;  +;E _[5+0x}+;N# 2rf\ٴkEO 66#{!p>iȇ>c++ bs=#< q"Gg~ZƵsgt܁l#>D0-2^F^Cw{.X@m)\m-_a2dy6ƦL\|b>6|s=&c6-S|A v{džX lorKaP`%pva +u\J+tA#? [dl* e8,8+tFŸ/ݰ2|`)0F1'W`W:h}[*bMqGp2>?|\d^ܹ8s2}=,K@*Eػ"|e|7W=-VnsV_555~b;\mf1{8FWTYc h +_7l5~|/la=l ☬d-+CEQSi:S4a{ɋBv`#䎌 b{PL +٢kLݝ, +xcbq|gL s0x[N.y௤/,/iM[WE_ǽ ۷%緳ƆΉc:~x cc!>}yN1|#U@*Ȉ` 8l*ӳgOw[Z7 |*?v{m#+`wC♘dar6\?±żA02Ĕ%pmh8:7l)`oWoaWۅwpPȶ\C\vT_Vdofs9G̅`* ֥mdKBt 3p<'zD`$Q1 [s`\ Ǡs~alkd#_G071o8 67YI|Ÿ#G +YrMCq_p/W^ % Bw6m~wGdK@*A`!v\ 2/1 >:r~0ayF{_kuͱ̵$[\ml~GFA|O yD{j1HŹ [?0s=4 _ġ5^qm7|U|&uJjˣ[ΏM[ +B&s|?9 *ȟk:4[`+2x#}bm; s2vX\8#ڣ-_Y |QgG?3? ,L^w,K@*A2S۞$^ +{`߂Mg- _-&G`c#l+[|q;dμ̜a'|C`<_Ya/8;`~ l5>w6[ù$ѶC|t}?~StoaKWfژç"_B|X\'9wE!G m d>9gߢ.gh};v`VtM_ ~Ç;1@.dg>+'xaK8-_q9O!;tܳ/܀ slNm q?X@U +_1Ypgc@ `3lݒ~lao]5і_l}=%E'd28Ώ=2;o;h1ù5,AS1|GGt˳6b1cuL.6+W_u"skv-\Kdhv}7{gC5p&dfrtB_&p8ɽ +WÞ /7{?>v/_eNd}C?a~{Y@jwlM \焱^MŕW/}7ߊvwrη\䯭Nw9wmN+|̑_~n>h|ٖs]X}=NC|zhO*P@-9렽9@<+U}=NzhO*P@-9렽9@<+U}=NzhO*P@-9렽9@<+U}=NzhO*P@:'6lO "#F^ƌQ;{QkڹBGn.رce.6idҤIj5؊@m +u&[WC{tܸq2hР [UX+NU[rW +l6Ֆ6|+|O_GU@*\UZm|U}='|(P:-5|p߿_m:i/|E>࿪N _c@mmqWC{4R5(!>(P:-Mɛ l|:!joW4 +3SgW uI_mjS|_m+׀ +PgW5ԸCW/eYA~G +U>N~ѫ  ]"(] {7Fƒ%l(cITfP^{ʝAL~$\w[xг7r*z ݷ)Vܦ=ᾎ_jφ)嘹Mq<Ӑ6m;miKp.u'`7qܕc^k8Zt }=^Mw8&Wj17x [*IφiciK[v߆kw_Z__q|?14SzC4gЮ)q86ݣsQ.αﴥ-mgõq ^Wqڛi=˦ܞܖ={O[Ҷ{^Ҟ^ts`O-mi۽]﹈1)g)fxW藥mJ y6ZmjͫYg?Hnv8;ή*=V{LU,}Gr͵w>ߑczs|Z* ~nO#k\M#vwd{{v-~z1 oﹲkyҖݧ9\;\3=jst[m8t\αn?CfԶ!+oqEm=&~Zuo^Xm;1ٸgXm1Vsu67:q=s.IiV8ڐ ٞy]3=}n2ꈒ^{בHߎ_^Ӗ>W%%%PTTPXX(_U }Ra/)@޸L8&9 ??_///75w7>kڍ)μ1hXcG [[0%oX=\**X¦Ek^gyv3>#gx9ьCeON Mwc\qp(5?vfWCM#qkǎuv8?2z+{o;'6NT7g!.JcS-nmu0|il_^Ӗzk1K:d,ר؞w-:;. $ͼ+RA|657㉟\cR{{[=s Εx롞h^*#} >?疄sYϓ9_3PhB=_gl?>xmqnX7mh|`"b_kY6|E}WԎ%|U|?{r9,~32߱4P 'юcIU" +{ A=.(W)rtQbf1|E6x,-ZN|{8/%I\r.o?D8Jc1"?w׍cS_aߙxn㝒RMBۧ*miKێ`+7wn1*Wb +WWl\̚> |E|fWQnݏ-OnlAsAS"z(TتW c9ܘr^["WZψ_,I* C"b"1YUnx5g w:* my.}_%#'!(݉Җ9\~ _]kP sspǙ(*w*nUI_َI^MOX>J*Lދ̀\X]]aS_T6ԉ(,_8+Gz6Ho+1c$εd|quW&l{ҝ%mi~X&_?L+QD<U1n?~JWjp`!Wv'\**9V# c+bʌb?1>%c 6po +.WY!,ge1rXzIXMɖt`4&n|Ϗ>zBlE6Q?~v|rry1N{?*6c/lB1W.|"s +0=nFuWW#yf0Ɲ0VpWƎK^\d07(b+%tg0s0)VGzp{u.pMtn0 .qq|k:.ߧ~%ԏ 3~+girly SǛmMy~sI[ھ-i/q',OOY(_5}_>C|oQGu ؉sx+ A#5-W.9,5=mlp^[Mk]^ʬEd]0^T?X+̻__);Oz %̒j{x u,VT߈qar}ItΉ1u<_v6UWhS?) #*ʖm8rLӖr3:+\PT|zyk7=wqog3?Kqcg"eZ+**|E =O1j07vu,i6^7pU{3H}GrY{$lǥ^'blvNdK~=qu'|{1 +|Eڰ{Wع9{ǿn6X{=cK{o~׵7g:^.}>g9\~Wy,vWL;uw0?T>DgX0~_wsqKy56Ma'uR^iOd|3$W8WhW8}?@J+ecjXa';= -8.XX y-6x993o~ƣsD5@q`t_YN 6u\Y0Mvis\{{ҖiJ)r˞cetH9?٥#A:_菌$)~ 9\؋u][TCPG>q-p/9aG +C J{3-S^){+*ߕY&FK? }2fS// |\g͓Dž& +8:ir{)L_\WI)_x95q\[Z1ߦ?)W]?'\_G=f$ +ϸM}D|ܗk簵z +s ӽ-mi~Po؏[/ __U?DiC#{||Eh4Ń]׌Wy_NV$nŲtC;*na#*< vBB@4^fsb~,w$Z>y+ߞu?!ӹƦXc4s%~s|6@.2Y#K`S?_'n/Wsv5vNUҖ6l%+&Wg/=jUfYVژ_ _E1WTSs|W1>h/m+/W(h +4mӛf*w>*d13l>&ϳ㫝U{U$0nd$džso5+ܺ.y +/5rusW]_(fv+x:?GJ*s?m&mi5śϟBԡGUV/ _v-K⫤j8|T;NuW<__3;xn/^U*S +y\L0á ACNʡ!1W2j'Fu+4ވ(W~g|u3_9NeX|%|%b|\htH|ek@SC>`W|n:hρvkĸ+_%`Q=c||e-+>tH]Ҏ^ӖI*n?+5"c_1n,ۺWnco0|6QW'D+ܕX2h&0υe} ~gaOއ%,(X?3.wĘ{lW{vMUzF |u8ej^ +|熯J)|F>^_OVǀS*f8Y̜_X8:ߺe8⫴-m(g>rIwc%9V_*_yv_{5?V_1_ xW8jc]_YVE_U9bMA=?-xku-_!cYMS<`_y|ًK Im3dsYߦ_Z3޾j\;74"|%ޗ ]!8JhG͛ +q#a,|-Ϧ +eWW;bx|;OYI:v+|MwH_^1 72jvNJi7_+o% Fr`|gjK4{;W1mvS1+Ud+.EX +JĸyEԃ?sik`F07`I1j.V9>戜cU9_kU؋= W9]Yn+h΢ZF)~_Q>hVc*l +gɹH>uq?(Z.(K]~=zBx +_٘(m_a.tkqk}Җk;JW!y,ʻWu`Opa{I5|+N+O2Ux gLw16eX;{{_) +!KjF]7k +G8mpGK&i@nRJ~OWȁq|%1qWt(ccI)#]/Z0B8rVY|+sn9JdķKZk p=kݒӕMNc7!c6HI^+ZG _cy=u'Ǘ80kLUn\;L7VծՆFBcxZ]SW~WΓw9|Jѿ~xk uWW"}MO^鷪FdֺT^ ,A#`_UBĔc8c^&>F>R daF|Ɣ]i"S)%58-[*}]*'ט{n'u 20î}r-~//orA/` #h%y*;GK<;gpߓM}v;NAz~]A&Ճ눻9 ݣڗ{#/iK[gKIh~ʟc,T)~ŭ('˸iC4> |[I8a<\+[qp=x1jX3*v[1:;&}( L {.rm? +f^}pG6Xf,zfQ +}J*ϐ4*НsTG,+MgRγ;j|2?"Gk1?ԙ9?,rY)]ՀtB]Y[8-_\%&#7<~Wl'@!ڴnOV./KܬO=7vzQ9!d]xA䯹o3kB]qq/_-mi~4I"Y[wsHW֙^8H]it>R+G,l -W6U>_굠M s_OƁ1uƯERS*mi~4w,+݈>El-W9W*dž[+  +z\:6o_{uýJY}A*Գ.’`ScD\GkJ:0/_q#ZSp{npGѦY_Y}s\`2{:NEWs8u+*)͚˘am+߾VaZGJ};xٳ\g V]4 +5޲q X37"[=.V-mm9Fa_Lu+/t+g]\Ʒq\+ϗe l5g1|e(掑WتT]2>_&S.ԿJWWlB>\c9WWZ&)1{Z͹+צbx-kW߷IY{EW?w5Qa5aW7(|՜+@^Wo=xvg.lJ+X섿$+_s)J[fy?⯒ +|s%XNXѡ5΍oOcY8?[|ŷ{&,WWv9aG[*opYWO~#0 UyY,bcu yS1v'ַ-l9cK_+'Vh i0d"\z5p۠Mcaʟ?WNL|D8|'/u[jt_9й>EuWJ|mciK[ruWm>aL/_G91)4bH]#|^$|37z7I+6Yr Lob Q|%pQVeebn/}5R-u0e&"c1`yR+nb_~c5WgJ݋/ +Hs تۺ}'8So>I|_MW%ߞvXK{WC!ϓǷ\?ti=*y=g*>\DZ+ӽ-mis[LNi@5_IJO{T+T*Pibu?Ok[Re_plLE|7tE߫;M/=50D\s1 YDڱAI'x/AJrNW)36ר3[s8f}\Ƚ]Q_s>uſ7wQG3Fzm?miK[nZ]c>ԪKzox58Xw1?бczI?AI_s]Gsd 0zWLF#5 +MyQm\#qE8fw\2^ovp_/a૦1ΊZB]%SSpoupTj(K* F6?QSSH_7Nm |U!r93ݹǦcO=O/[Qꨧe1=/Zv,yCx/'7CfA|U+>\+BA3aW9#?3sv~to^[H]\A 1_k|ⷜKZ(ד պc7Җ=87 CZԮ~wbbwbKcZ.b')zZ`1*-o"󏑏WPz+B^HsT +qcF%U7m&޽̀}S9ǷG"Lec3WRT0T@iJymrz#7 s mnb䮾3?s5 +u|OuzAmkK_حʹYߓ^lHuÛK_f +\qu +OFmղ8Gdc&1} y0 yAWʩꝯ_{iK[rDv} Y:#c?ׯkkkE1۵̋NW'c_1|<r4Ǫp|b/>M!3L_VJirtةp9װ4&=V *5>a,M]Ox+4"@y> +ag9J8.9hOy,9IvFXӢ.?a.j3LUϓ[мB-Ւ4::쾧 |Ox*WU,^֕+_q.MwmkbQf")(^I+>ϟ+|j>yGb/O&Elݦ+ 8g:^\<՗WjU:EJ]BJv `8p_ ?z߰0fKoPâ#%6XkXJ.+է gӘ9Ł3=Y-\G\ʏ&{-tμ^r?Z[ɎsG\[Y [翂i\Fzҵ2'0Y+>w糄UI0*Gʮ~3;9_(s8gҖr ++_C||07NX!^ϊnSǁY^-^A_>^K=Jin%+"+U-aӪW_f ~J+:Y`1O ZY 3sY.+q.L;b3oU%\'8eUi̞[`f7+KyW +[.ao<[8m0]p1co_aܑg*=a+p}~~Pvb)Gy&+sMjS!zWV=-m{^S8WeIJ~\GAX9Ż&&U^nCXY/Wr !|IRA2j 53*.SU3a|?AO(5j`j8j8j}5,^[?P >JS?l<|%ٽXa++uy+ߞo6V"[V;14/6#o}_p/%g+x띿f\ۇG(:*,w?Oqoxvצ9W̆}AW^qc3]w'H[Ҷ['rZ+))b6:CDLhӦM:ܬY3'ŞUez'Ƒ6qA1aN1I=."W?k؊Z$E;jw]\J2҆|=o#~p0C'k$:KpVgognP3B|ec~\"Wx$a˓_UVZa,!+u~_ԫ"MMsql~?`k<[-yl,%;5*u+y6vU۔c׫AUl$_X⫴mlU-\|nCӬY\poA|/snʻ&Mtۥ'GN̊7k I(UٷOo9r8L0NU`ẏÈA`^0v(GG`Q>l :c9ƏÇd$v(LXaЯOhӺ9V<'yC$SI??hd"Hݤ^A]F-rB: : +\ڢLocG|U!сr'nIMϞMs_!vslmޓΨiFzg1J:_x~ʾ=eoFҖ9)SRg̀ɓV2I0I j^Fu ++L UWXKRh[+k{ig&kk:i_b1}'ժt]>b'LE %OWj W +>=PϾ+jj8ů`{|Ҽ&"άMq{LHx\:wៗܓeMIO鮮(uLnecй{l-!miK.jG /? .Yp#|mp‰'aKP5^`Bߜ)z#%Txt0fbo[ݧXڋ/6q$X4w~Y_‚K%p聳Z4 <7W`*~SφyǞ'LKp||Gصл{'oLjw_zM+|ՑU$΍1& _㫘:ּ֧Ze_kؾ ?zOᎏkxj\ #WyXJ)ϵtUߙD[l+&2W&̾ME'\wk~ظ!a*yZwm_]Ƕ2N>_Z"xٗ£Twu"pa[${MjaܵCe~мY37*̰+gY^؏sL$wKUղmJ5s]9)NZܮq7-miI?iذ0_WXx҅%Wr->l8̙=fϞ%*_B4FpRc'NG CNz=*q~XMZK_[yE9 Sξ F3?p(\cVo苽O/{=RkE%lZ/m +@.=W~=EQ|e.W +o`+eWc3Ɔ[=Q5R(ʔ}[K^V/wj`%Q{ROj_ _Ɏ}Vܠw9W6WWq1S)񕿿kEWٴ';6$)1WXN%'V7oK'<!쵵ބ\aJ'W"VIHB_b +_eqtٙWXjV*mi@?sfLyG3ON8zv + +O 1EoiƈX1G k1=.Xpy0ipN+ƥkUX[ +?Jg^FwP{u6kqA>1C$h}?(1b=+TИ+߯_??r՚\r-˕+\䚿AWa|PQa,⯂s _:t-#πsjpFj؋O(a?Q?] ]ιF/;zl"֧(Cڸ kO9;*{ڍPkIbucPϽA`7.NUs怌u[{\F+Y.@VJ냯PX?'}LXpͯ ߰oZ+jVԵ[a_c//78hߵ?4+/fGA Ux37}=#_R+r'#Zߛ{_yҧ'-Ƥ#*}ڻo7lCM D>r_>ﭐW^gb.i?qWbx9]d|{,tkHZ|9tb)T\{r07!Җz___1tfW&>WDzs#WiKa8:th/-ia'.~V:9vT3RYv9B{RC~Bl0 ck=n.<"}Ь@_IB^PrY$^; +Z ;u>={܋;`~ztpTt + =k|_ W9ʍI{Ƶ>SIĸ%a%| ׼-70噯/|O"$oǎjY뾄+̺ᐅB}Xy0i>gU+s*k|_q^OHN_W~RBPA{ޮ +:G ?rݑg:R%֢DxpxWy#L6_B|eS u`ֲ.tSMӖ=Ii_}bC3`"L;"XzɭP㝕 X>C#~ , +*qm.}s=j9J׵n%bmǵZ +RVUh@#)㞤UF +,9 ڏBYQ~_5P|@eU_=ˍ +k#S=g雍 "/!k9Z8SμVy.?p[Q+8WWUʗu'=_ ߼ +Ͼ|X Ͼ_WfiA#?#؊65d|{,rB{jM38: 1Jk*ĜPqVMʡŰwOl[M7/X_ 3o08\Wd*'WU -l8}_WLxbL=WiK۞TMU4d/?}g>Tלrupڵ ݡ zz: :R1p_>0uGaCVn0ji0ԋe`KeؗJ|Z ͟&O1sC~!G"X+ĞJtا:2/8a/~6%_UGTMS^um7 ^}KkRMԊuZ}q9ksFLaZ#mKCцyTJJF[vvV|y-n6)3ßx~8c0eM5!l8GpVY+cu'zr\Oc;ojW[ӹdDs%d\wF~\ewtIʻ zK7/3x !B/Opc;.o; =:%>^+>fϜ#G1>#"XKefͱr y(?XpPcPjNf->OԾJ +*H2F}+~C9 ZfKQeu}._jr FWa x'TLB:Ͼes.lqe~Ʒ๯aWUl_W1۟Gިƿn_1Xs[AC+[Xw}<2^Ho4:XhWG7IhM]}J9}_\ʚ0nn7Lڡwu^7e3-T>IkF~KXx-Lj:C7au-L\)uceLA{Y}->T !nO|nM^M|ъߧ8V.UN~ly*\'?GCuBҖlsKV OX%k8Ჟgx7L?%\#HGfjf|(!Ĥ\sͺFOs*t֋ŷ8͚iœȱT5NOw.=3눱ax\rmvs#Ņ_k\A<$-oo +R`Nݠipo?pKljl|~R5Iwhx8ryrar"1%(!U+cK/֙᫘B2W +WN;WJAy GL%jJS#x +]fWWIKcKfXl+6ur@jp` c{U;>+gj j +kxl1_.b ڨr{nV/~['| '< fKp=f27E[׮iqx+s<|eR[bڮ|?9U +G+Y ?R+;y}oaӛe@] X¸{߄~gL榔棆u$~[obYk,朵VθW'6|\K;Xwp|R|J5DmƃUҹ ux vHXRu'->Ƭ +^:J`_fxxjXV5<-?xa1Yӟ +o<z-9U8LΝW%_i-_0ׂ翗5`{qò5RO_]|EM;|j c.~;X7HeRm0'ˢS"Ʒ[kGx5W<XF[=)|ծ.&JWn+vsH[Ҷ}kn<+7_rTƁI\QTNL+Nm'W ˆ1Mκ%&J;oW֦T1#,E? 1,{.i +|kޭZf5\y!5a6+i|:M:PW rcX[KګAlꉯ}}eX'qE mFcq9825wU=J깶d6{WC .޸w5k ^;50Y்F*xuKS;ժŦs#ؾ[+yV5z?jX.?:J^ !΅ w>?}CM{5_kd m/5de "_-lfϽ Z&cv(f)=~+_ŰiP8W ,nSp.=_9&̳%b}eo 3G۝5qWe*nrکo|?#7up&I~ +{R4gyY1WzʮIm9C3Ԗ=;fW-miێVqWTK@YEI寨vEL:lo%^[8|j긖QU80/v6gpi *WTkH +}G":ir?߇;zcܰV+"FkI [ȇuޑRTuļv5c>>]'W1PAfO?sߥ{_X~'.gT cA ^ǵ[׿_6kaZ&z՟ݔCNGr+O8>_w|}??>kTG|~k#4J3pS9Osrk$2C}斶}7ԄֺJ.1՝Qz:k}j><,,c3{Ǡ4sA*'[sO`KMr-Į''oiSm=r@׮z +͋ypc7ja7l5a5W)|5쵟z@Kl^RV %"V<;Y9sg+>ONJ+?Wq*3嗽G:teP$oa +XjW%_f +;O-(l*p K+gvTCܖg:P ,hx)?-9\+܏q,w83OVW@C +l#Wssp\aSj1'Ð_ƈ=xĚͲժY/]󃟨:VƘ{{];đvW;Gי4õ)i +_!0kb=i+HwIׯ\_}͕F~ڽGO5ԇzk z8J{יkĕ[AIZk’F<{7q&Pה~0-M^?qFܱ{uFYPEx}TQҖݼBa,w <>ա$+\ *[USTM?6#2.@rW 7XS97gwfHlS=x~#7߽IѴ[d$]yΔ~kz0`w*yF1BeK _#:_4q5ҹ1w_K-[˹HؠYSi3ހ .{Q !X5czjt'wշr?~nW X̞_e}gsjl%;;>Z|MY|ʵR /l}#miK[ݍ;q!?c;7ξb 3E"]OkN[Ðk*&!u6vh Xʹ _e=דC Wcs| `2֢{ (ikj`ؗI|U-Ǽ/ߙ7*lմب '-r/ewqW8׌·Wn ll/F+?*)ynbe(Wff\;KV(~a7w>{DՏ6lajDN|Vƴc\QŒ{RΛv$Κ:~7nܓt=\0fh/W>-UR=Җݻ9cPppOcc;cqL6v,bBs'W32_9\WW | CA㓹|{|u"b jWDL'B|T'z=,UaU%ƪ)O| #z:M]ܗ$+ } _EpF6y_Xg oQ+`JRm_-ms b<|O +9Xꉯ,FsA|=c98Y~>,Wv\qL+woVU<&+ʉ'RUq[0Zķ/|r WR# di/d[5j p_~A$M巴WX׺΋D-##9/ ++ԍ+{sgҳұl_eh%g] w}O8]laMROVSpUi^3/(WW+Ʃ8wt\|\;u|{l-S| %[fɢkjUL[v{>׋:bWb0?ub53-v(Pr8q|?-r}8?hOZ3|5FÞJ~ܟw6!X Mf|-d)Ȇ/Wg\IWV&L2]RKJI`r-}OZdttE4Gƀ5k?Lj>|U˺oZk}QeK_Gu=Mչp_W3־z_-?iMYS{d b|#{ЫVhC#J]Ѱua=x=ɖ]3oIWԞ3#"8ו})Ρ(+;غf5b#Zy I[Җи.'x>uFY-3n^ $kіoԺXi6%?u}yɏ͍="_׫u u 2~\ZXR拽Ud|*x[Sl^ + }p[aʏ}S5,+ZIJ6mEA5|T :_{毪mwk篾N s***M<;-':5HH7/Z6x_Ms \sϡW[!gf,\W U_a;HD2V㺥Q+ s8ǧ?5s]mM~i?Y32ޚ]ivW_Vl -~g 5s5J7mk\50c|?7G뻊hf`R$cvN<|UkW#P&UWL'>C{TW'c@g|o@n֔+-h*;>z_|Wgn׾ Sgu5_}~&}(W_Z6?v{=u< iujn-8'(jNZa"wv +8u*Q3٦*k1\=pV: +|ZW5qF}pd:~Rsqn8zN }WqDC߮+<䶾3쎯%1}XqPha!Ŵ`Ҷ6c8bH1x2<b+Ejʎx _zj*^!i + +Ic%I|Wn[_N~V|tfOĞ\.Ÿ|Un `?z⸲>-e0Ψ[S|LhSĨWͳWAzIJ&}JؠDCu? T]$†s4Xc])|qbZ[544s0_T:Ǣ{xl됩C|%5IuWMu<יhiZ9mi[.]8&C4kǛ83~2W <BsXE*&zߙ`5;J-V:O\޲L\ +0Ux;Xj-y(w0bJIm< Ò}C +_}<{k^ f9UbU-E4h(h+)ѰJc?-M 20\he_i<`^9cusCI=kBYC< {[Ҷ{5Jmq} `_VC8WNzɅ\||LU:vi+p<'q~ yHJ/bosunitT{Y3| VBO+۵B^*US*mi}["'U z}𕭵LlB|dzڋ1Ka0q'՗Mx\X|{0IYew_Q_tBB߄&.zn{iՐ[COVzg*gU2R:j6֊p|es|lܹE R{):"U2{p-"jogXFt=Bi`>j'v>5owz!˰>(˜#w%/vc|fWڹZu+5Xg|q/h{?K$fYyß@aSu렺~_2|Ǩ:1ќ:_>B{A3 _1۪\Z_l*c%/l𱕋0P3T{)\{32JD|%b\U=oSij|~E71.a]Ұ+|u9=J_%az+/=lƷ*mi]Ze 3$Fy9rNNs6x 2@<}Yjk^82[pXӋŗy͵bPWӿfO|e^毢Jw[*bj|5npWƯ w·hX.U!?6ΫoY/>e W5-:Hy6ϕ8W +c5M5Yl{yܤqԲ45I1u"u3.ZHzLSDG\H +GaNZَ vu_Vk{Qm}{lXյvp4].⺄JC_.$֕Ӛe爵Q#] x&Bow-DW/oհ[CۙTJR!9҄l=~.5UL:җ=:o'E#A6U⤤n0)2|`4:ܞ\sR9V #{t׶TNeSϓiҳ{H-LX% O~2֎Ȏ)dmi05W6$aiLxEΗ}D|e?qk׵q-6j)]Wѽ_8&M %1:u%TpW?) Q(>H+z Wr?Ǻލh%+?P+6/6m |넯bqYE/xR"yߊ]2ήT@W2 _9vtƾ1j䘙ԮGWL4WcPq]8\ώqTߙk{kָ͏7Ovv_>GkU|esXcXW'$+ +p|le?]}*z^^?^)\3eqh'fۡ\lc%F-U*'>P5q벧r[h$N0;BD,0׾p=߿ Ebb]If||1~qXLhH۴1q6=AkP7*-m-g>w +cBF+v?4;^W hk?۴ؤ"Jw|qbmi/0ám֒ے +yid<|qDOubCW؜u;$٘ d<-~᫲H\RՐC+knu5uCPGcXK޳Tƅ9̮<%k)\ͦ9o+>8𹖟+(/-mZXwx}3?˗JOnxv%DMк1b~)?Zp5\$|ep+~g-Rk7<!hײF ̟'&_ yk̻e*M폜#i#+vΔY_R+ cҔM#8, ύp?+~ڱgʠ<wCQޣ X:>׶F>,;4W-"qs.z8u_ctwM[qM$k9 ;A'$<¿u|&6ԕΏΎ=ϲ[ }+ŻZpMR:$9J{PW2_ EF up4G6MZI{::6`^8RwOEGzNA l?ɒ(?o,>|1mճjFg{=֬|Wj>X>̾V.+'j+ʪOu9tR[Wy~W['<8eⶊ}Tsۜc'LX͛Ǐ-oWi ݳEerB*glo)dl)}uy8>壬-^z:zRo׸6wc3=%7ޣS׃w)3uzH/zy,ZCpT:#Q,D1,/?8;=["~;5O_:!&׾]ZUq<u\Zڳ881P'9uٝpz?8[fC3 #z&@g1H69u60?m6?!C`hczH;s@_e6Ä#O޽]|o\7ﻞJx܁*ۓĞ73vĢmM)~wYWg!u.>_^g{d/|O )|4{[ੇ?8/9%YF/HSUunm 9w :qm_<߼N[d1a. ķ.8F|nb_[VgoL圷uY3pT|sg2і_/Φc_+wf>o|Uu\xt:_YP5~/wR\Ww~$Hl=+ߝ4mUf|eua>&&^ypFua=n͹Vwi}FыI`+#Pb9@\8ſB1vWE?nӓF*jegƄ]C6N2^>0>:&߁5\/㫲0Cv =볿z[cv@.qmͧ૝{9K֗_c%D ~^]{uc^^PU&2T12zֶbXֹq{믶;>yvw+ wc-:/sz{[YW<O>xu})J[m.5b)^ymdIqȹ,:`ʭh.~1v'ۗyR}Em˒'I_&psƦ\x2Y*2m\f!z?/js8!:[)qsAYC}{92&YCc-Ÿ'vz>yrOW7|+V~β8SԦߴƒ|7j}[|U+goXvўqorwTgB4.._]__AeW|ԘϺVF5!]5u^#Cܣxt' Y_AC/K`^Hw( hODѳà\IDO~<۟^MRYw$l#]K{瑎o9,K[V/C.41Vg,y ]YիTug'q^GOEǼrⲔǣ2r)!O&c) g_G[_XK9~s<֡^k+KTu.]LNn2;ufݺufv07o6Vm剉]-Xlmluw*v-O3|Wʛm:MJMXҥ&3=ѬYlذf]v1{Ǭ^[6ho'~<27ho=T/Wr8թ}D)bE揯v}O+;~|xҹWiocd|SNH߾~ +9T~~<֒2Ph*ǿ>}̝wn0W\q愯vay34oyXg˝ϡ̗N<kgLl}6mL2qK5NN/3o=c--0ݶ5;mO߱\m̖6zYby4=6bnqwW:6nbS#!nj|?I'a~噟 ܃hYz//[\wkłi9bs _wz{s_w?..g/O|bG~ sW[̳b]3?nOv5wݵZ'z[,.b?OS۔mkN9>vav"Aۏ0W_y3w4CCk2s[7gE<ܪyu5o}3֜ru:q9ϲ`wvxռg[ᙗ|Y9᫽޿-ρSy_WڼaN텟y_ptnN9StuQ1s6=R;XsAϰy+aZ=m<%f3kB炯{:bs۬1v+jn>jA![֜r]:䐃(+kX?:c@Wzh~|Gt:r珯gsm)ޥnW\SN]oA_Ln=;69xJ[ll3Χh>~u{/;zlj.< xmkN9&SسvLNN:[!8xa5k876}C|s 4:>wKݤlqx ^`zsg]}:蠃M[b\J`R|xx,]ԬZ՛~2_PO<Ѽ0'|{Wܯg|VY~9r}ҏ7ɞB;;>cyee˖˗kVeϗ&s9Ǽe/3}sݵmW_;>]IS sq\0+Vpr~QٶSXy{9W_m.B?]=sEN;a]8L\y3W]u \ o W]^m\7I8_x +{\r9S\]D>;·S?s)~O߹pg;Ӝ}3_ve ׾/v|>^=^ių"~5טk\~O~:GwUqs~;7|W\qO_Jh#!||c3/xl~ּ5guҏ??4>vlҳ,w_+O|5/}KͳJ]nf;r?_W;|.Cstwmַ:gwhMS%2)e|Srems +x5f텆{^񖷼.kG _yk_ꁌ#`n|7yk5'>K/ٟ__!dzHnRى|>vyo^+aHƥoV5E26~x +y{FN8G'?c7ٜtI_Sz= xl$E_K~_0s Ad/9e$Dٔ-?#Uzr?kK}( _ +l:CFGry +|0~3V~^ľńh/倏.gs) ?Bh?uDG) l VFsM<$|~C? 5 +R-/SbW;|̵!f_~;q8 +>d{qrGY|U b,{5БNu0s8d90WUgSSt&ea'w_LУ|v&܇f ?7Ǹy|^l9z t2`O[xz ;y-ZLJx WSC=d>O:,`Kzzӛkmy?^ us<psxouO1Go^|` _d +A^Ћ>O~]#!;?}p4d|+qp)W~Wt:0$~q@pv+ -bx!~7|+_q4F2Ӊz/^'{q9}lBWUБ!w3@ёL~c?OqƶucCςsПD ݌`ƶ`}sn +{.rp73(,ƒ?,A ߨ7nc/4|E L|I4a=U@Sa"3M9~vˢIނ ԓX捰`t~3{SgE~W?M*6h1+OhhbASh -\~_amdL'ʃK't2(߅',Lc"0um a+)#.149 )בâ mkN9_c)[\/269dŌ||nCW`6T'@{ M6. +c6N=6[ݍ78ŘP ۈ^b#OS~WaPƻk0 !+"~| 1vƁb "<Įʥ,t=w ;x,w7|&Z5}ס9ѫ+ ih"`0 8g;+05"y##}^Od G?WOt +S'g/Cawԕgg/˘*=% z>Ѕc-dYe9ՙ:׾5'Z_L9}lW!zyKZNsQ5})|%[ ">] Abs'\?-_c3)3zqu [<,Vda[xI alؖ#_Acl1|ݤpB,6v[ݠ.llX-'vy<-0,O_O̓.d{Qw3!bWd~_W"`Kd +KEp0񌕑G-|}2Dj:}@YW-:na`LƻWRx1/z<_vwa]`DG9Gѵm_`QxO+x=Kc4g^.|Kx68l/a);m7~CYW[ |g+g椸2i{Gx+͑ B6م f5!jKk/f62A~(Ͻk3"kW+|͌s{O>zCa{<J} r3t.<3hF_r%|%h,t #P ϶5R +=M@"`Ar uƧQt^va)<cn +5t8kx ]o =BPg\7@ů.'&AvvOs|.Qm_&c#0߆ ZŐCG +<@3߇~t;bOXhF9"`}*^ ჂԓzVO&>) FylVdN>?x_H/8o'm|V+BSd%!4? ϩ2W>!ϼ{v ƅ`v_1"'|p⯨|V.?KbqQ{l =Ѝ6څB^zL̘>O(4W fsO4o@xqK9|n10:elc#o)>'&H%cl+{|'?E< ~Cgp|"``fh)b_8Ї&BG1nk³!CR`Y5r9|%=#;BakmN֙4Tt!2 +7b~qy|F `<\vwq0>CigQa l+@烷l/\Hw4ʤ^EȶSl$И`Pp 8;O ~.t:n2ע/J߀:.O0u/6e|`E͑PWGʡU_E{{E75o) ekL@sd~_H_`?}yȗbޠ- +~44`B AfiS`? +٢5/ k^_ˆ$#0}НLdKk)} VC6 ?m)cN<{Yf23yl:z]5A>Qt<*9=/[{i.M¶u.:SvPoEߩ ;C@m}ǿk|%?y8'tT[ vD[ S):Z?9R6x@_lxʻTVQ~7)XxKFY>ߡm;~zR.^h/}Cx ֒$GFF?)Ki <ݔͻ/0 /Tg9?m x`iNEQ iQ6:KZ!:gNx[ksZtD9|%۪=3j|||I۾CZdt#r}Oש.z֯7>s$!=r|{&>):>>Y%~Q? /1qېcOjO3>mbyQ׉)ًjW:q&c$ƽNE3墋5h+Kuƺmo'. ʟ/[|E['SƼ1mÒ~l|d<{gLo]9',ڷAq=^+kݶ1On< M}~[~Be\4:me4OyQT~|itNc=xʳ)erà/<ȶ5$3'DƘo0R(_oӏo_@N=G|($9Kl + ɯs\'AtsWWĽWb4Tpذ~x%5|U6OY@kфO"&Xf#n*3S(sGKܾJsG\54&y_>cl%_3>xO?}&~ +~|N>H ʣ\?FnWwBS+x:tȠƤЄ>Klx +=x8Ubq,yVfۚSNV)|Yl'ĥU#%>XKJx;rM]31F%>Lb}%C\Z5nYQڢ=57Ą;opqO<-__.4b(&iY<@lqW^l?;@W֧b{M۸bK'qϬ5 fH6w/Z3byb[⢹Z*)2(~)uƌ/k0kX3l{x'xķ2ư$֓v\gqS>;cgh#~~([g~Zu&9h M<2T ?Кw +d*^:`xlt6-f? S؃R6ۢ/IFeO|C/;+||_g͘U{,G Z?i,^wg c:/e7㹮xA]}e0`-|#[t,e`bŽb'6 ˱{ >Ў{,%q } }*x]#ϞH'X\?Alμ+;?>Kp=?Yv>&w)e78߼l1zu3'Ԟ.ӷҿ<00e_Dq2 - ,&h),AHS۠!4fݡL|Lݔ-턯 ?UJs@'~ἃwmx'gTއτkʣBwl}{ntN{SWx@Uʞu$ڋ kZ{9>Hich]xoCr_hm+ER_ދCc93=S^ZI{:zҾKL:O:P{~N2H|\qv2VY ^#A̽54Cw2 ?aLt kN9TeIEs4aƢ+uct zSgGx>kd{.^fSэ:zli[C`\T i+ =B ޥcV_) +?S8/5|_/Ag+DZO< ~Dl ?}7̈́]Yb$ _+śPg8u^5~MCR<6KoyZ| b?!0+Y֢]@i<.auf(( _-Czd'h G:x[OCCev6 |EYyhzp s~ٳW iRv6zbGt~1b27F:8M煃[? + Ig߽^?5?)zi_w0!>d:siOvϘ{EWK @i/2,/5[u6 +Qgs)Z7-@n㱱1vѨ?u/9:1FߢPc?|%5/S OEF{܋#S=Qs_SCl}%Ykw-t!~.z=FcM _Jp1-٧> l52;uދm$kN{S-B&־W~zw^l+h@ol;/DS_ Yw[hN RoI A߄]ЯW~+!SڤvxRO?i +{O9x}:; 9WͻțB+`>2<;b||3Bn)P$ ] +Aw>+LR~>ڂ^-W|2nR'ڂ7C dRg$_1sұ66ytY'692;ݍ(2+ݛWhe\glDORb/ +] X]ozJǘ>R<;:xt:i;ȩyY}cֻ_)6PCRw)߇s@Kh^g8 ^6|~MY\BܕJֆ>y{ӆOOӏ[0 \2ݡTVJkh J0p ؃{ mdx:CGV`>>+?=_ +G ӯt 2~pßC=71&RhD?36|otJc .}M:k w9Eє +|Ż_hm)+dE2NE7} Z¶aZχ^Cbz[VcCo+)it1z [}#}t6){s-`=CdsR0'i:6x?z}Ҟ _l|_LmCwSz`Wc1 +ɯMޔ }G#6>׼CaAOo^LN+jkmFA;Y/);Ev wF:ϯLQo7S8Κ =xLx8ӐU0 ۄW<P-Oԋk~pd <FMw)2O|@sUB7GcRWa zP+/YʹMq }~x#d + vA3%h9SU +_ߏ~BO d}Y?t/ZtӢGw7_t%au>8K>t؆z3C/aOx@g2nCR&c뵮0%mYDw7b5Gg3Nobv^+2æ*xz6C{`&Uh`(AgCwe+;-DžI1vӾԑ>aO_.bc$Sw[?3{d>{5dw`Nmkz9Ac;ÇAۡ E nŬc|4dc}VOЈL\WYqW [ufu/5~+ մL;WxwauFG7QGǂ-~:ߧ ԅj/J>C?ʇ:G| ԃԝvb+e .yk  2OU$9^ɸ|eВkk:Hy"'GcE:#-9h|ӾZq VI]_ox5DGdZFh@uuYQЈ{F:ׯGxȶ5i>Vd?oQq3'qD !|u΃ԉߩ3Hge ?c?WtFolѢW _tHϧx## ;z㺈GѾZ1dc,oow3?O%o߳Ž^?(:y|YeP/dpd9Wq\]~=f{/UJv|΋)|˺϶5fVOk>Έu u\\nϥ?to;Sw]izBW>mh_i?;{&Ǽ*=q_j/Ɔ~;~ϖW)~hO>0xe[]:3Nkamb 6W,kqɶ5fZGZ9l'wζ5fNߓu\cޞb_2kjq49㫜rk2ڶmq_->f|5x>t3g|SN_j۷_->f|5x>t3g|SNW"V'mg|x͜UN9m_)a3Z|f|5x>t3g|SNSmg|x͜UN9mO)b3Z|f|5x>t3g|SN?e|lqW_ _O_->[iWC7sW9崸/^d|8զMիݞ7o6ᄏٲeV=zo_%5_V)|i:9j᫹f|p\|9jaƷ _u]QW9pRWZvmW_ _e|SN_0o[W _lqqbja+|Z=y)OX|W[_cՎ_q?C_ Xowq栃r|-jYk|3ڶվ|ZUN9mVV\1bn:vWmyyl\}>nf=^3?S$~fs{%ʠ[N9 *۫,**r1s̒e _ g>s6i{VZoTM^3t^=W[Lfx,O1GW8ӌ¸6[^b‡+,`ViN~FG0999Xt-&D^f^-V3?;>O:y=øv<)A]5dzU獘+[ivٴ,][Lysvˌ&+[b̳4fmgC{KdXy+Wih Zl#}|ރ)H-";Oc2;nȔCOui{K;^tuЯOa,xB+ԃy4o/y]~=2?SE} 3?Sh/x*_|JS,y} -r7p}uVrzxK~4'ȼvx[ݓ?ޕ|SFui{J-$׽m2/]}0hSNJ3 r} d-g9g>@W)~׭<ك[[b}u.kLef_5+vx\o. \O~Νn9+tL4eUe͛6ny:_˳;_vMxlsZi||܌++|GƂ1 +=f˧.a/>ypY!b!}tt?Y<{TeyYׇG̘S=[dO{mddցzgTF]NQ!w1wOⳮ'm5^׉kUYR<]>55|1|n Wf˗'y /jW3e-+hr76&:ϊdF|uv LY[x*٢A*~udXt+q +LauS[ڗrYQ{_|5B ʲ:A!S}z S/!GlSVna,+|f{l2u[Gaa#~=G* 3Y=UãEQKn8czEeeRr+/ M|+򤳅<^ӹƹb:S[gin~yPY8U[k#.~R{>Q^<-J\?e5kWᵨNAyUKL+rE&O?ׁSDXT?HPߩФnW>ɂOr[dw< [&') +y,Isջ+|`[ +=UG rEui1'ɔWN;PʏZ~C2.]H +j&j +o^~:_#r& +[_|ڛg[g<\hW<_P_E¹ߩmJU>6m B\_<-^+L1sG>)0pLΉx~ +F*䧏"E{)lB_q_ `v9\}XWC[<-7AۿrUsJ+)v_^!h*SSz,5Uf_y>,n_?Wgg69Wcԕ_)<-uW[T*cJQQ^_$۹*fl{WQ}}٪-I|Նk[>I+|0wj79ItR_'/*kejɻ|IF9_ ʏfaʏƘ0i5*㫜qe_rU7֞HO _%ƏEUlK]Y>|MGr+{_gk|g]zm>ӏtPT%+-#ysS _X_Y;bf>&}uWrm|sW+ +} |{XH⫸5/R9Vy+WG3V/LX9Z#>Ҙ`_qMWs̶܊|3O['k,t )^B||"+Jz{`' +|JM>xMJ|?ϦǴAUoq~P@o64/fz3:7v8&xG|jˍoU9{[ь]_؏fϋd|^A~&jb[\tWs5?1XW7*o$d )yp#)L%XypU||Ub|[2r_㫈VOc|O_C2*z~Pcjܣ *;UN8cf QF+dӗ+b&nh~'GmøM**\pt}ljo-WvƶxɡM|rml1>n`v|5pܙ* f~bcv8Lŷ'^nF6nF5Z +1CvZ*%<fnWp7CyFhϟ-}-I"Os:b,5O G7ߴn=t9?b4JsEl_OKrUwt<*,u5>6Tr`"Vg*m<|2N͕>vZƵR!~|_&-^LXY;O xkWSuY|<"}6~a|5B>}<|ǷӉ66x^myZ*,1{d=Ŗ}vƝ q(53/¸9w-l1Yn+'~D.[Gm]Gvl\nsS>Fd>+kLUoWvĴ>3:%}fⶕu*<]re_3k 2QU?sq5)/S%]T?QW4ll4jzc͑oyg~l63ǁf96[Q_l'Yx&mnܬr5+|%鍰n/5O*^_g|2QX><^UdjڼUxۗQ ]-xj?}|溆_yZpZ.ӜQ| 害tO!v _!߭:el\Csk/:B65g_aumrZ!x9eݟb3~6bq!aV~n/\Sl[-ϛNL9%Ij~߷zź\(bKFG$[SJkp|%_Gwպz/MV* Sʏ%td_6WScC78 +O%Y;lC6jz9Ə|)97]u\z8rL4b\B:Qi7 ,\j3\5o}ت_jjzZ;9nf3zG|ųֱrVD9W|gk*}|i0R܊K!CWJq(m_K-|>iۻWa 받]r}~nUsO|[\(:#[c0s | rM[ |b;%ey0ǭ6 ;|5n1efjw|ټ+?7/'f+d&6 GF🄘-W>Nh +jǐ^ܒ7Jk +~j>s[WMY(|JdAU4U +/#W.v}ʋ[W^Vᣴ*}GEgsw4G]{޼g_7OX#6?b C]Ҋ>OWn ľ Or,ڑnp/lsZ,W6 2_[X̴_BBOӜ_{=K|mosȯqR/7wYch|;1q_1V忊:G\l/]MM]NoӾ9 |u՛.%* ٧f\~W W)9rgQ_ xR܇)WӳW7qn oNߙs?s7~bFr]K|UAUX*#<몒zUj~9h1* L~|{#ާ>W⫄W>mfAѷaTl2e%Ƿ1WR~"V'I4af|* +0[*_>Ĉfuf$yk>œo9'͋- Jeg'j*:3 + _i\Lŷό8үk:<i?d/lV6=6u9y/s#6̡]o&m|{1?*>ts=;_4fngq.ZJ<-U|u_I}%Y0NĭZ~_g?F|{SsQzm>ϟlj|ԆRX-/ X}0_'˷/}Vc{O~jyvWi3 ~|{4vqvsߛL%q4cvhl&ԫ͵sWN-d +;7ᄆ?[k̈́%COrkyW#9Z`F|{3bl]lU*VU沍g_%#+n2Y3|is=?2O,Peh~0l=?᫔^ ?|ÆOi7O1 x"Jm9_"==?:XWX/◃xT􊱱|m0fԹ%ƪ ="ݝӔ~me%WJ:F<|Ó{zl&}|CoX1;u9/=?3g_w-og-|/>+^. my2~PJ$1G`-c99Bs~V]bVVs<>͙{ԜoYۼ_~l82dr~pc|w͍f>k$1{BmJ`2*Xa~P<])r=z_Ώubgk_Ǵe- %a? _ =Rߢx5?'++_gz!/H3?/~_B1użӍ]Kj(@E_}`.֝9+sWg/sOkד/{70gx93}߻V֯oo=#oVK sa[jgSxN_ TXN|qWJ;U.xR[`_1:m14)_c/ +>\|'F윝`b{7|4/{W敏\b慟}f*3} U-yyI+|CrV/6!cn A囪\T|c夳ui[Nή7yuEQ%zCq~ci՟EVy㵝B=+~^.++W4&J~ES;Ա*8dC½W|V3Ϊ8ٛKįʏK_K{ʏy)s|K|%0G;'xٷ4>ssCOS,[%wxf7GlŞY _Qnϊu]6bتf̺h. ˀ9r<( +|eWCˇܜ3/x~fn1ևζ :3t2ҶxMCSY|0U31qaSx5WΫVS<# Xӈӊ֖TsF})&?ؠ։^6δ8 if+?FCX^]S^cʏc@ +;2cCʏUZϫYz_%\W=Dٚ-ܫ,\%r*wr͝؏?jβ1o sC?WW i=kGU\c_kU'q/9!< |c`J+b?Sߘ;-_9ߙ35xz|s|}j!Z=Vg5qs5"dbWuľń"Y*XaYlÒ"U`bnڃm3/U".e2އ+ľvũ+9*_%W)\vL=J?'RpQyG*K911UU^N*3!Kzj ra6SYsm2ϸ_.⫽*UK#=mL{yzs`j?ktVu : IwV>K:ɼ{yWWZ<Ş zWW/\O!sN<^eJױx37yvta9, }[7?؆6 +}s 9XړS{|YapKzɤ2(8PSDm ++O܂(Էa95 8\cI{V,1+͎Ͽ5Ͽ_i+-ot/̖^mv#UF%W9\:k.wjMg8]l>Jw}tre]MڼtŘӁ[<ӧ;c%د,3? sǿi6{Xy+ח6| 9=x'ν6|Uwh<錯XsVvݬnͬ߸/ޒq{E{e +\.`'Tsj_N{Ǧ +,·Tb# +F9yT}w37|f,(._eMT{*_T}AbCs`COҾށ:7s{*{P{ +u=Ck%=7{{Y_f}#+&'\a#o7c{1yX /夒R,BM -3/:s}̷I2X+x[\uO;|?sw~a GЌwWS`rJh?H޼AyZb=/w<ߊ}dcJ&}yoCy2.]tK.6W^qꪫ̕W^i.Rs瘓N<}NX14Rm\8:vM#6(_OtI~23$5FU^b6ݜ-sб7iLLţW1/bzVWF9U;?UA.z~>N-ކ^88Xy;>e ˹VOw>#s7-zI7?=eWy5{~_W#)a]X=U q;{3n>>:ȣMA|OI/U?8oSϡ||N6'}sG>gne>ia9-~n\jՋY߬;9n?ҡeVF>aj_CZ|jo@|Gcm{Tk*]lsږsg>|G>|>h߼1z榛^kN8Vc; |J{)W|ܤpŀ`**Wҏ:FnQឡ Z_+|ҡjuf +8n#1ØXoUV2 lbLL8l­bxa\d:Z# 1,p0>olźuޯ? sk9?3gv;23jݹ5 0CQۛon9~鯶㸽_5"`5k(T_Uضs A̼-//CG4>ZUo|is~9_2k=WA-9bO⩞' :}bcu3$.mi +wM$(Wň! f! 9+Y IErF$("I$5+&2n~]ϻw>ޟg]תufݽ>SO* +r[Mr9V|C^ׯw[eL.S +HHLBBBq$% %% iHZ*}.[%QbETPʕCٲeqn7ߌn ~j7t*UDUq OH9Q&OVi%DX EhSQp1)Rhs<)(_ƟQ+*~hѢf~Uh< +*bhOMD:Wŋ{+)9EPƔb车"9㝊RJr9:7x#nOn+WYL+V ?[jzM}NQ"2ŎI\ t]{tlߩ~NMKCqF^(F|VǨ {/!{E=~ϼ#< ztˈOϻe&O<:XtSDvRR;R9|O/_^{Y˭b4_ %JץO/œc]^犏?_(%z_}&`\}UИ=lIbt[1Ϗ1zJo\l +-@ݺOҍ7[X̩bRJ__x볂;L}w@<x"}kq:'<Kcr8˔œY=Qs_Pwd칄{. +2zlDjMG-3|N@ْs3 |Y |ǚg<ڱL _`󽈧W>h@qW"9.ϡ+2(?]Z2_}dxp+]QUB~+O9/|e\(9G:҈WMk.V׼]^=V"WfkN~lGәMh8qNل6K㒝h>b +(sW._lm*'JޏD$rRJXkiܼM4NT2Wj#;+ _z' 8#èa$5ƏūÚWc<L4sgLt|zc 묩1mxL0^_I/`0s=%Ixu4,ze^?YS1wD̙2}6o.[e?Ϧ6nI/H0_ 'nǎĐPZUq_>o_yװu*WiH,QV󞎃lQt8w4w O캈y'/bO$j|<FQ**?І>k8o Eh_p_)rdUEϤkT,RQ4y{ܢWU+ȾBʹnuLS_9ֽ`3MHb[[wU%*,WJrC\NuE)=L+qʭoK}{ <W.ˤHvW?b衳x۶c7ZU|ꄆ>mxhU1Fт_ASOrauP|d*8+ ,h陆ϖGEs_4_q4Wݨ=WW/џmCYQ4hH\}IRϞ{+Z R>ύiGSR?7NwU>K4vק[ջ~@_Ij2ozWPfucgJ3*o/]⺖Wg"ԗs۳gWh̜6sIĺukVbX*lٴ۷n>7q,MYS^9G0ej?z=돣ct%"jY% 2)WEzLղmqgeєjIMh\nc3UCzm>/*c;7m|UgCUP]bw9<E&[4* CZW>3q-hJ+2tZ[?sn|K}ի7^^Z)w|UJ篦F9AKEvMe6K rdNo⬹y~OR] ѷ򲭅յ$9%D.L뾎<8W8ŀklWzrެOyjEUjϗ"ȡ{=8DaTtwkEׁyͺe0?l̼z׬z3wӴzqWd;>rL G,1}ߵ;c[loOcy=+a1WhLIc2箘]Dq+WY{b~koƍ5n|_yXh{,A1NtcF`KO5}fSgNWbe&_ ;1hW?1KCA_F#WИة 閛m־~{΍U~Tdz/_*YF-ODG;sG.1_]DSV"jIvOX2<&YUȒ8S+n}nqׂݰ/Ŕ|%پG83Ǽvc2υ? 06ǶxRA:,WiSo*ހԇ'0(Β3ḵ2o̚ +kTPuB=j{_Aa~us6"c4s(- r f_,bl~AK'npGwm}/7 Y6^A39W|5yt^#KC:Nh_#գ}K|Ք^SS_ &=GΡ +}3bۈ6˼$~kl`"=6UZې(ZJT{=ΙVϙitW;z~?s_cvx^vgԭ9y:̉⼑?/PsP33FQ=u0$lޘ>Hϱj ެ.+禒#󴏅'x^3ŶzT{ګH|Ey'k㧸H ]|2)(9ܷgP=[;~J]+L:m j9EܙNڧ\_sPLDm5GJ|Օޏ&=s6#.kɹ*{TvLUSaQlPl2'W~OAj:#L|5j*nͨƿÐF:W|i_5g|غn9VImQSɨWV0=jC^[w>:LGlŌ%냗(Eߢq2>rΚ:|)ʅ革%Zq8_9Z&|%sqQ+^Ob1k+&_6_ÚdNyRcmGkƸ;zYG~Ac)ـbz3OW|߷=wimRB(J?._Q9sly9Vv}y6 6W| qB0ѹ<° b|c6_+oE;>QL%$Q_V2,I.RU[ e;-#>cohx/$鐪o& )StN霤_զfyy#n`nje𕗻T/|V/dܲ(g%}G_5am6}!rXߣWըw/_%D|'Wu9[՜nFl;;EoR|U+cR'_#rnp>_J32e1F&,J[] 阵-F<`Ʈ܃}/D|{.`ʃG[Jas5?]Ro 4=G.'h<=WݱX4j)GqiVC?сьI>y S>_ \DyQꁧvc"{ltZĀn=*|6/|7̾WhEW":~LCc[l˯6Xk;dC_դq1ߍihc<(JM+_9zng I7=G/DU~kjAM::Vt.j9a:Mڑfk~ha˓hW9J ElB_|՜)U 5Bb*b _QOWJ~o0j{c0W|\^Nq\xa_vV*}_W-__Jp7ZFӬV'OwO`F5Vx\n,1_w# + RZjޭ6kaidJ +g.XCt8R_w::F]-Ke+92YZֈ)qS38ox&œYkqௗ9,^݆2HL~\3]0WN5uc|Hu?>H ._9LeԱ,!9I +y+e br8WGv: )0$(WF_z.TA97H谸+{; /soN_9+V|%^[(lVn@[oRkK45%JUC$[է\T-Aj>Wgq+Ql~WrI* ZܚnzwbePA鯼|G/D+X  | +*yk?Qy%͚9 w~@9 x+yU}шywɹz +Sh0? yAß US|e诼 +ROW[ǫ>о/<%4KϓtO2#7#iuesz]W?'즜rm)m*Ռ5W)9iop._櫰UbvxCU0c_Wz*ݜߠozˏf=HGQ0e~Sk+h}И]_%33$}rsNsj*sUWm9<|a1_qӹ+f4-!=W=5 ҷo6 jW_V|UN_q[!ni"2䫫1Xט _U\ϛb8-3gcΏ0\CE}WЀ*})TRy6U4akKWN'9+1ԟBUEo7e+[SMR[ũuFkLXV_Uczef@s|"WWН5uM;Hn.QVh꼼f*J>R; \b +^{ܶUhsJ-Gh VKjq=ۉAGjYHOArBWW%qo㫒X<`&_1|+t]s9kWm_ \VYj~?P*2WrΠҶ+bUSEVDJy30[`1M7=c_|_N||%[RWf}q$2f}6>cO7u6ڢ}0iR/y|ϔgwu+8s|P3g9b=,V5?sWL]f]OC +U]ΠX0E0oqkZS@d-j5.Kxet}uAfΘS4uDޡ!$b r]$7cc Lcl']wlxmiüvX:<õ(5:7qxFz*wᆇ =+O㰗}bnܷF G=/#30_'W%_@Dk_q*8~EWznxj|\'T|%WgTKW^}>LՈo_=EqaP]f_诸>X ګA5ͻQV+|tw%+is@j#5$Fз|SףPN?Iu$9CFވ+U+??e$kjڈɿI6_qC3P󒟹*|=ymhP|GuhwڼOpJԸۄ].a+3ht7Tk鮸V̱M9YԜU"2_)]?_fJSOgj]u&u\WWa͋6+}/ؾzޒ_q\c|3L.5::!aA)иg7`3 +߁Eðws4g# ߃|ݫIqEm'e⩍WTNꯪ[+X.(sW³A,l tZQkU3+վ[AlO,:WbH۾GwCb'KN|5j 寘83Zw3'Pig$'sS0Pԁ^(|sCR5| O~;g&ٔo&>7ūsScDt3Zy3w| "SK{F&1\Gqu>>}?McU7ƹ?1G_JOUcٴ8o|jOQDs眯ikB5ǐGɖG|lHix] ev w^¬~-ڡP:H"bN:^{1k~R&DkUWׅqG&\ }WG1o~cjr_9N!c;Ƕmԑ_dAP ʓ4?Nѐ>kL[r>;E?!f~V׷B%߮ky'.^n_=W؇dZ.WGFbm{snzqFirZ7s_\clF}9WS._E,:FNJц!3xӒfTݷs3j)8nhI0H]͋x̌8b~SgZ[yhfOd>U9/T3W3jNbGydAʷ`IXu:yF:j`"}W%H|(o蜥-N1?';h/?^ d+0Z^<}U(8YCXg;;|kT9/y">k;wb˨uzP +V1bbdwWJ]q-go o>u\c4Y}n+&9-Z|wRCS+ﱒy*3k9+r`iq۬Wֈ^s4_cu_k; -y}D BCPkDeA0h'rAOz&S?/':'`O0rWT~yv?X*b=?F*?皠xzU/Jkz-f9~mC诨G5b>_5a}QhAbSdL}_^S'! :לfoZn$ +9wcz]k3e[ۏ;Ǎs\๻i% +_S@i4_v|ZRW_IS<TYrRҷ -櫝"rG{Wz{jK<Ֆ ;{b wV&ꃗX\1W{b+шjuHׇdJ|u3_J|5]|uQWT'\rG|Q|9Z|U;'LeyOc{7FAZSVؗAs^Zy&L> G.OTcWj+?-Ig{nOWNEzp_h99}Z:Ob<R񕙏 w6t-́x|eM磙s^_OF '{G4>y XF^Y&bkBwJ/QfRfQiC;5s.30޺H꼙sW}De;s7Y/$R9O8!Qc[llj|vY8̾uh\z׷ѷK"\r^΅rRT4Wbz?5_=U٣W._i{]TXhw'9%2w9+;|LCB.(b];UԺvW"Ft_ֱw%>ȞWP/8`S1 "Eʟfyc+;ut=-2xD𕧯 I *!B&Xa< FEG nm'tW)JXg4?Pq{!oXt,<w\ͨTk×W/HrQu5 }/$yy5<0s73k0rL3Α#E Q㫒%$'7c t8kD W2wU/ uGG~F; j~We/YX.{J0ZZ~&0=-U6Dru#y5Ӟ5u[4wxf+ܟ䳲櫒eʓQvTmMn_q}PxYV53knqyxp~ZH|e1w\|uW$q#,b,j:A\֔`'?1YM䮸&z`Y~XG֩Lj+ lj(ꃬnLz[̍_ɆdghS4zh4"MOU᪏cԧVZ7??Ƃbψ!:ⷨr]ԪGV'xk=O7]wo%E-J_*W݄m*H ++}7WJ,T.'wEd;jP]djd +UjX*B*|gUU8@`5;|B6fk# ^kԭN|΋uz[xi5ܞC׿|kZM|rj{WV׽.los=|vьg3]'k]Hs 'H_Ŷk={L|3TǸ@|A!<5g)+UG;_AVWIBNSTG +j }>4jS-4J|ɏuշKbUS?d>A?+G-7849~@ovd< wnj&r'܉Kkc_Ayh=f>.@ָh1~_&/.ԥk0{ +dXA7ɊhGb?bț&TOG||%W +>XGֳr.[ +NBtP]sTѹs4<(GH2(pkQ ^O˕_}@rW2L.Jmwdзs<au%Łs%nO岕;.GUbΥԷ9+Ϗ쵭lb6ʤSW568KDL\=IZ/9GjK6+{ݸ+s0˫ܚa3KzuxxИj*UJ'~Kw>i*Vr >=o$WoO/>9{r Z`m? ZmߣNizt[&ӬzjI(~;Ȯ8Ru]zVcoN5:/ +" ʵ04cȢcb}WUJ;ϫes%3ijNP=rSHM,=Ꟊ:s +}@_Is0x, 0ώ} C ó³A0^=;Cu`LݳzֻWw݃ZO<ݧg.ѽ3u$l=w `挩x׷7:wlnۡkvB;uEvО8Mhr1/? <ƈHW?88rIr?eΣ*]Y:iw[4oOsdv7va0yT5)wUs6 w r.d~\ _9:m{W-{;輕~aLB>QMIO[}+Ko|eL +?1,8ij>\iǕcPƪ45ۅ_Ŷk1çbX&F#g!o\t7C[96n>r9m5 Dޛ 5hd#x"}֕ϓo"KQoWKH}aaWUS_]EkP}g2|x HK?@%wr:cxvhbO1^G Q#`3jƍ?G?C0dP?b>O,x^xZ}1d =&I^ GaĈa2Y ~v0y~ /ysgaтWŘ1} +߳;X'tVUr35rb蜗-2^9@s>lx~koOogh;}{UKE^{ߛآ_> oiKH7+"'{52_5W57|;n׮_goOMzK^{JZ1~ɦx/ϙ&Fѿ~<}.Mў']}ϷsO@-}r^/ByF<ד&3iwuZ!oCqxG>,^ճfEVT;lٮ</'sţ>^AS^^}=yۻl`X_?15h:+Wn3m5%,]8/vj{um]dRvԱ 샜#jdQ{sӥ֭-b4K^XsEw62V+)nn&zlQ'%_U}Yp!j;45&lE[u7e>xCvǰ0OK!~7^b{n`c`䨑8h :t-Zi&h֬)f)22;w@o_=Lѝ~ѦM.rrsѶmuȣVo> #1|(Mnƍa 4hPuꤣfH] M6F.ѭkgjOU*qǐΣ[q:נ|,wWbAwڟX).!{k:l\]{_vBxj!=h:U:Xz4'|eqD^Kʺ]׎3xr=K6Ҟ$ o6|w +yQ; C{aX^D<3i 2Ç_y/MxQ䕺)7'Zkt^pP^=wFvlenFk} +HG(wVbև\OR;)-it+|e]2י|;^p'bKH|žWU2uw59OVB*)WnV1u5YX&|Ucp՞_qc^u ^Ulf[. Zx૎I O׎B|ՙ~G3VǼG9v9'iEl6-Iԝr(]Tj>N(OX\TU K+c[ M}:O[cgӔ7j]۷`焆jEy_ 6mڠi&!ԩZ?m 7]ːkg?q؋'[_:wabxNRrҢTG}X~nǝo|ejqܕZDGM'ϢUݒjnw*/1ab{򱶴(1ќqY+ s>z+WϸOo,VaGqўJnjmGW_8NŭѲ=z2:rԡ{AֱKDyKK;5ySsnɘ~Uy_3玏1lA+c.CAUI_96(J +_s(4|y~q/9v{Y{%TẔㆊYC2~f,_BA6":ƹ +h_UL5cq9._EI׫̿fu{|Uw܏Q_B9H^ފ[wg q5M+wmA;Z8.µNnR3qL~W5pFznU0+cˣfnxX%ܹ-WlS57xwc? - O3ʻzUNT/Zf Nzu5Ց~a1c+;~U|E5{ бeұfuN+m]{!`tݗ:KIn];ӽ1`@}i7j-ǔuù@-jJReĺ%YL7W6{擿5E{[.K&W!|l5@濠ٱ_e<WB%}XQ k_eRƘŸ=26+o y3+OS_ϟceRܦn2._޾xDpJ7݆O㕏눵vM6*GZ(LM=wV$X# +6'Aճ5= nm.x @p?[8f/կ[ H[!ڵjh9Xs֭Stš"Gՙx̞5j<.sR*OxHbI3sN=Ow^߭AI?lgzUb~||JUv֘ W:_i;ŚcNЬn2nU:*UW4kiJ֒Bإ*j|БW|"}=^X$]tC:,3G>|ujuXweԦX7 >!vJݕ:S;wAK?WN.ַc>c0_E5_A +׷"૊7߇C'aΏ1m_Ŷk%؞L,ͿP9?MrS1'78>_9}ƿ}W9yxO=$2RSuFZWx?޿=irfخ};ԭWc*>oǵ=}W,exj7Tk^0Ul9+3_l|yM泵Ưܫ5,;ߋf^/O?Uo_랞k?c_3hJC?۟ :Hq&_Vlݽo._E`Hܼ嫂з1B#Pߊ:cKϢqqОWξw AZۄ7^HU`6qu4׳ _E*T|eBbP|S_k.9Jx6j63Jyf`Ķܴ!-Ѿ:7|Wg(zjLDZyP3KR/=mZ+T(J97R2Tjo3@7Z$Cs~̹k~+N6y4T'5OIH*L:ycWh4~F՟ OP~+,vyڍY>NՏ&}5{>nbNn=&ྥ⫺"gYu^|H +y#* >k_4~Jqݏ} {-Rʑ5H/koFLłP_Ջ3ĶܬqͪV$;9sɯ;:y}go}9w#?X,k +/J|}ɟׇski|-T:~oR)iQM| ^ȳ[z_=׭XU24<:b2Q_X1߷9wjyM0Yx_4[7B:})9%lJUߢ (tmblNB"hBk~~ ݙ;QԜYb,v>Ko"iW$MYlTYo9KAlUW2w>L7_8#{џO4rłt ]sG83G}q1+UF^ :pW7o\Wc5'͛;fX6L.B]Pɺ R#2.{uuax:_kv z5yF_iIy~TH'7>_50:oHp9>(k5}u]e*su?CO|h=,O?!fk+o\gnn]qTUpuD;󕷟V { g\FhJ1_D +RSJງQèJ|Ug9dn blku™ |%ϻ{OEyYYP}${y{ +7M;{ޥ-V~ c̕s#\x&ޛVO*PU8wE+i<r|Zk^Fa\2*s"{0RAKAs%f,pm~~F׸u4,u5 BbwpSt^deqw6|33IX\Bjy|>dGSϠ6Ֆ[5}5}۞+NP\xU0⹅_vnԍWsybf 8&W]0l U+!3?ǷF#[o,cA +(Ҿzq5(Jf|Uбз}w<Ww|e3we]Ss{*. $]=[=i}{J$J)@ +mkJ +_I+]ps~-BKg軉ꃤ:o'nLUFW8/oX*T$:_|ݩ- ^+t=.RKt/y_v G<~_Eϟ,}d]!TwfJV +.= +mJU6۞ƹRkm~FxkW W_c޷{F0aq G?ǰ_qØHA>:H ٬Y +Rak g,#ts%Bg_*3G|匘;Xs wXo~v9SFڥznX3?jT{v B*7| D1'{Cx%_*WM'0YyՈcۢV伂nm#[U{>6X9`6M5+  Hy‹Eyxey.AMzwX1W>]_74|tʌCV|CRj<=(|%PMx^I|>ý9M}`D!jXl!k~ }&g}c{7r_ dsTھIm%c!h,6sF+ַ\qiHH--6WΌK]y>d^?dz׭D ͙5Sgڟ!&^VRgZqg|QsR:Q+qu1L עϙyڕQ2cʯ>XtWr}gQ[G98jrm5mmiG|0(F ]G{P({ g%m=Y/h/~9Sfb" :ưZsC=HsRzAW{CBL9ch:?ؔ? +dž[PP{e{uKPJW`6w}C=jޏ.pF#]!w\?sq7Ԫq|ɳ..Ly醱_)~1m"͕W$M{+Ssb,V|и_8} =JkL25N+l~x8_m.F+w^d>8C*;4RL6ȏ[FX,0t_ 1?wPgDmPS)cSj|ݢBq +_YԡE+p&@\Ρ㲚g`+lbh飩T}7H]ґCε}KLnw1cW?.Op8YCU&_I94_j #Ǡ(U*[E+Wf:#l^yi +n5+ǰ\s +&hˣx纄y}|M.cy43# ) nyϚ!O깚^ٝe, +_%Wa$q@L.DՀ_=t A::,&_:jس?g1a q5wJi,V^H 5yxZsfSXۇQ3>F-YUoL^6qglg4L2 +.D* +|u Jk80/A}f9y3N˝3|%֛*]ZuðuWGQ_Ŷl=&_Y9[6gn1x.s\^%_P|ZkQ<窝9޽: :nRo2;L7+S"4ɌxlQ/s $_}902軍:;'Ϳ"t CU^0g_(tY}c/5ֳ>F^S>!|İ$?+3&UiFx>Pz-gACs˨{?a5oH庭a6j~ܬ\oG5ϵ\Ub9fLPkJ&_IsKM~rD]<ꆁNX~E3:*Eš4alU )~0c\UA8~+/!W^aM&5M">;y|~ÎH]q =쾟Z@m̼Z0"aVmLb|5pKטw/>K|cS#x|n\gutT:ס_7$_Wb}A/{qշ^@5_Q;%_n.q{_k9\Oan?YT|S;ֵĨ򕡿 +5%ԃ$p >-x8;l 7}j#P1zlm,[YV[WDf_7S# +|w +D{WW+9Oz NWLMX )!5=/~- _SW3M J +=_#WGվ&r.̛9Ufը i\ Zuq)b3`]$_]#>hM_i*M(_v8,]]9{p}(RƫnWX:/V3П|^u#]_1SvWq+244sfXU 97!,Ԩ=>oAMLCV|ȿ|oD0-;6y_K2x˛$t1D𕙫 +x &^>O9̧*oyU238G_1M_wwENϝ2^BϛX0_}D|v:h^rfvE⫄Wa9W *'M ^ G]_E|v|%ft_UYx OBg `z3GpX{=E+n1,US:b_ vLs*gй!|K XML=UaȜ2s oɭϺiCW!\еsUW(|}~':š/L7 \}&?C8cڎ fX~ߎitVH#>wj, G%믂r_cɎ[M_c;PuG/ejڝ%Y96}67i֖"5WOx6c@9DW2X)~~Yb)6V`J#0ټ08I"r`[2ǔ+?wbd +"|]o^g|5ޢi!)ע$! +1 Om"! +1 Om"  +dGQ`l #TR73"9R2jSD[_|/WWmJ 0薹S XpGzs_rvjע]9m&۬ 8z*J{+3tZpghY۝estS/sTSP_.>{Qs-mZR[[: 8[StVuZ دf~0xV׌YjFK6Ӫ3Z +VwHSTk~Й]5tfԌ~j3zzVvjvKegV4WFZKgMmYͤ3ò5Z: UѦʴԬn8xfuXFZ+;2Un=Q>aHkY]=mh};xftXV̙U٦T"mIg"[i͞gUS4wHE[[Kg*V쮪5U2'4WjuDgepFgMLG:OOOE[֪ʹt]TvT}Wj5Z,8*z*U=HSz*QaVsz+Z3Vgi,nϙhWjZOE[ju~pdfUY̞ڻV׌*ʹ"Rga^۞mfv#]5TwiVkEvΊ a;Y\׬ںk5kfF4TGUzkugj,"k;gvZ[=U:v7k,+{' 5V}`vfUc̨ͷZÑ Kvw[eљUiMug{[;Ù,V=3.UWU?aջ3lXVٻV}`]iwuie:+3c3UkYvUO?o糺]3z[γfYg K{cmu}tؚYE̪jMV3;˭3iE;+3j}/ 5ٳVGfV]6FzjVמYLc}xp 㜭"ޙU&wUOOzW:wzgY[,p}WmҳUf8"miznY22w#,j>wGWefUўw?8iV7gw=#X? +p;W+*zjkXYWwUoSe[L{Ezkz>LgK/Xsݵ*a1vۂ4UV']tl1Y=ڝ,ZVѻV=uU"Z=.#']ϫw{'k3sG[ZEљ̮"=F?ÑZov֊]nvU>X,mmZyf5U9U=VZekr[Gά]ꩭ>ztkvWJ3lg\:gY;YXuֶj=#uתgfvUthK=hwZlO'lwB{o~fgrV{#]UUE[=jhgwlj,kNVh}7k;U];ͬ"]m,%s"͵ZVX;YG6g +<'Xj3ߵ}}/'iHWeZ։6W}a}a~\X+۪U~zOF:XwpDޟ#uwz7=!{}j>Y9ښY՚j'ZΊ68"}U묞{ f߹m^VUknV}zNJ,g{񹳳Nhs,λ<+V'Uw޶}Vz|njyVcmͲFceWHcX6p{2mI*U3j"l[6UZ՚cec;~ܻz(P[=/V"ܚyWUO_eAi]ϑ6dp`}3UlS_JwvJ[wKg{gW3+繭}5Xou}.=!{蝫*m5sTYk5XGܪ6:cce)Ų'w^>ڪw쿞ο+Dw6 +흉}vf0jsQG8!8QTq65Nm龷?~^k}y}YUWE};qh }lc}7N}ĠA6C}H>0W4=ſphzKw<1}/=NJn;X 7>fS8ks`2oBi}NWc'>7JIPe:***j.W/{){.7ߏMߛP~\<_yz=ym=WZϩ5/|?Kgc:>[lt@mP+V\ՖrՅkX# +~X+_cEEE5[ [)_aa=zLeqO>Z_jy붚*KUղ07|zşCKc5Z +ߓT>9:acJ\D铌+wopVq cpցWhVYUyL5,GjZsBsẎ4' +***j|s )dRJJ9'gߎJyX>Ϫr<CMFŚ0***j.Uվf.3Y`+yӫXby +ԧT4ޖ gYZV_UUܹRz5Jyrd0QQQQծa.8J?{_lU;du*xԧ>=xVX1Vɻcq*T}8nDgYKW/V}а&]ysʛytlų@U)pL}xݙmeƲ8c\阹r/wیiг1'8k\s.^аSM*K患b+^\L:oпM}~L'& +,uG˚ߞ-Y7$- ZXc +Ʋ0mX9<'^ u|]>a5B K=XaEEEͅvՕF]-I\vη:=VZ<5+T ;;RWݛ I q.ˬBf,ֲ '**hUTaV_j䂁gVJK|eU.c`>5t5LiӮF6ηaguCWlp=Sz~/[Yw6Lz +/LzN.'d/ WNee"kao1gzܐVkߌpD5w sTT\}38MUse˞cҼIkbA?ѬMz㉩L qЛyOKoNۘ>]k͌"%oMRbzOjͯ*)WAQxf]E2Wg]g0X3>v9rŌfN8* +|QQQs ':. [y?veeC⹠/N`V`IzFM~.mZO%>{<1֣XcH[wXz38YVtjz;ejN9`Ȩhy`z=_Y|l|}h[UXаtyDY 55[Ӥ]&]YsA\]8 [#zB +Lz[[଍0/ĬpubSvX:'XV'8KۺcsnAn /dzyXcu1#,S<8+X5siӰsTTԬרgm}햇]-![y|iW޾e'j LL91ҖM C0ҠS7)%qXkV%ƃfNh1G4+*WAbB}1վȵa\*fWJwsl|X]b,YO k>#lk&8ʛ +:V+κ4+pA2F^8eYBbRlP|VY"~gϹiZ Tߏ૓3|eyK XykN86YX55g5]f kWgbйmyJ깤A@\0ԫ9׌ޙ^#K[>Ū*cmdn糺Ѭ<{4^[MuX:+ffJ5>5FEE͕l _Uw]<d6[lbҢ^I0ԮA>݃~#u۽X[߻<]'xC,oˋ <{䮂Tm?D_YcaVŔW9AcmqX90WX]cF55 IhWVA,=bhb'f3qA5S7~++Xϧ攓GcNxOb3XB_@\]`;rn͊ +wRkȰ{Čߙ?,-| ;+ Z\q29a_V>FEEz(f]|ðU]uѮN MTbe+hV'nS3wSG~'c=uNߘ]dpօr0^yUhX߅=#=**j>8r}ҳ6lUWzxowl hg+hW<[4 ~a!ҰuǭKiۓ^tw[alA*e~8  _Ovd3Xvh:% c4\8gr +**jzkf+_A|ř\{{+ +'[_99!4,tҰp4Vqaʴ癠y'-{;^ܚ(6rU/|Avkf9\1V޷]f|5 ޫ 9m|iWx.'2%o_-g+^ ]a&l/,0_hNv U;2ޘf,V+ypܯ8f`5MES8^e,kNuF8k`6aѐ=UMm_ + wj/3v wьp[!othX9 ޽6OX[1c4>܌򹫆ZF؊:dʚ "ʶ!\p"78 :tzy_5|un?szTbz{f +Ɗ꫺zoWveQ=}uIzG62jX{ 3Bk3BcqLl sṂ +lj橻ke,k clW_!];hyKU c%sF8NVUTTԴԤ}xWm~V1y|u\Gj3_o R Kg=m53U9!a+hծ0ĉhp sBw(r|A{Ub+e>4.3.r2V{>W]qoV᱕X_5|6+f+fo(7{#d;4, X`+ ]\ĠW5&X3B/K _YqJ AG.iXmauEX`EEEMf^Rb_!j|3K|3¶,V [a.*3hiW v[5A-bF,ޓ%Ol}dk;]aFƭeU{&`=**jVk.xۻjW9ҟ~6W|/s۹/W?h1V_,i!9aS`+KtgphW j;_MfjWK+E9fc_|ma͢=***js$F]{SUm~ǹ?8[sw,ο+`-d{,oF _ U ;zWVv{)!*X}Gv{TTT05]*VcY;?"{}g>RC,/gmW9,[vl;]1[5#nf7Ń~c}.WW +_Adkg3xvwԆڟwj.|e1V|9QQQsmwp6#+!<9.;[d4\!1!|uWl`d^3wҌ!d*cFȌu{Uy6B[|f|uW8WW]=Xu8 QQQQ0A, #,uкss_[- b>N1X`'~hXЮ{y7e٠3w/ Ұ^NW sB07է|B%_!3ݎ*;?0_<_3_ӓ}[ {hŚ˻%RR lևk5,jհ8_ly_Y^ +V6抲Ү8?uXp|OGlCp3BG5^ޖ,ƪհ~֣yg}|CVWz +zmոv2\竒b1V>Z 7 + ka霰)K|;cxj}|vgOjWq1: Y> Zle1֤|X7"o4aY5ퟤoW*ʚlPʾ\+TWNghW}S_EEEꓯfc\|caq +7ZҰV Wg{++K嫗D{9WV~5jXUv=Ul0V^w:2F97 !jCoWWd粯r|qvFqg) +5i{Xo;UJ9KfNtѰfr3Հ9aaNȌxWd +34xƍ1/U)=Wc#VNJkE5n>F:Ӷ[}7Ӱ]aw|WX: _mhW}0w-_?xtKr#g+F{I4ew0Rb+.jXg+oTgiXX,-8y| g + WzY⫵ZWKS_@|0=RP +i|uT|_t0***j_7|?]1G5v co?t{>k37WWuG2|= _yW{ +|^_=-s|89IwIGOŌs6ß +|g3p`Nv;|e~|WQ׺~NwS傯.":;3w'&ֶ2Fs82dΌ`TTԬUW[ҁxNXk}܌#dP5,=Cƍg+ְXJo;!&vl=6WՉ/JsNO*vK36e|5jU+{iWy^xNșXנ25Z# K1?c5s3Vnl%Q9+jd[|/fnN%__Zޯ"14_]L|+FC2~UUFEE_~>=R+7'd :bFx5͒bQs(n;o==Ff,+cTj yr|u}ʅ<}f`Y63BW5 +rsBχyF{95ZIt+dNFEE͵:v|Ub+iQQQ^WGO_1G,ƲT<,bP=XW$`[a g!u_3Q{V{I<,/5,+iW}xm૨a*2X}ա ,:U.'82kX%9! ͉KYa&0]a.l?S~ay>->uay^woN/XgU߳a|WQQQ^WhK|+xۭsV|9!3Xϐ +3A\b]1'q\٪0 Uz!iayy L,oNYUU7GW]_ЯW!_3}MW;ip2[qFFԞ0+Xcg# +~x.h]1 +l7jo rYXs(}>XBVCe5X^w'lX ZjW]WU8tWWQQQӐ1:w#hhW?W z.qWȊ N,%sBe/vcm#k[I؊ʈx <djx ΂aY>w1ka5iXj@h5'v5l+_ _EEEMsM2aA|hR0v/}yIar8k :]9Bωs[9sOz].ݛ[w9@z>,yy}sy}qmG߻'1OOso;lO47~WVK|~f_NsV3Ҝ4'9>qiNsT| Ҽ'ͻisww\;ߞoq* Fjy|AWotM|Oj{|c,.%ci=W :NӜ _ݯiW/o ge?3#g=FG38ﱕ|ܯcSF3__gqi~4K4Im}=ͅi#>^9siN'?a^=az;[b15>ߜ65}ȿ+<kaܚM[s2_m|z =\f#6xXa]1[sY0c>XeIZg'۟3 l [b,D>O)u4?Lss\jFjwf|~I4 X_3v)1xWW|W-CFWjj{<,w櫏 L_Lw +g_錍j3DoYb 1+Y˚_*zV 8Isf,aN5WVeFl.dSpCJgV*Vc{W*PB +e䫹uf5=?Jڒ#|uWS|uWpJ +swĬ%\'kYZ[STUY lu<W=UFv:Xw`5u)iM%q@P-ƊUwU-C͹.$g1WɊ.P|uM#_Y9uOGsZaI,:^+K|Fr|1 B<gi/Krże 3{VVl0] ;_՜!<~Οg5_ygcMxWcdcv 6r%#l2֞fdZ| +Wvw"Ge8YK+ʳ{#VJ۽X4cyӻ*D +e`k<,Y`+W5~{4[r7g g1k1oKlcH_Aߞ5A++_351VSu = 5=ZZjAWe<Ҍeq-9g֮S !]~Qog(ۭo/~{ruUNڽ oѽ hNKǩ0soe,˻W|մ&p;y*An_D@rd-o:5c3]zWV~Qo#9'V֌%ӻZd6AjOZ%+|~i=㏨y;=sw08;g#l+{-ޕ*_sjhnU<VcxWceu A*tJψ5f,=C/շ'fy)w_=H]~>᫜<Ҽ%J^.yQv,35[^{.Eݏ۽qN܏W -8D +e*fQu [WGRǙ$38җw(^SfV.3xy{O+V; g+]sD6^n;xPV[lJ AԚ{FsxX0Xn(_}^_"?ځ;Vay=,rZŒ]zǩ=(;}`\w2UɻuÝlPwjV|Ut`˹Ad-s/yX^N(Ks|R|;^C?@!ʎ`Ɍ4ssBJ;Gslu<)!CK(}չG9=g(CLPս"3V)`.{ۭU _f+d!#Ê0}ށ;VKfrOvu~I> zۇ`M [yޓM}P3Ջg}oV; l]zܠ J2|[kƻZNQd-5Zs_ΔV,ڷNoG== |Pܥ%#9!k+;={ ݅KIL9sW=J^[Ylŝ+ sAyfPJUܠսʛ-#UO + A139yX냗ze~|eh`=:#s֏YBb,[Ek%WYLGȻ-U9bYŝg9H?nR3}SY/eÊTL*n%({`+[t=.]+GJsXzAsa2,k<:b%؇]k.f,cBn^sw^(3}^"L|KY<߽MLu'u9*8|B{lu\2l4-l +v6[v5LaK%A8E3£ KD] (`$cq~b%9kSl_OX>SSI>3{UWqJ{V d+}^AٻyW`+켂 h#h=g kXgk~I2BÒg y /2XO~Gc$gqnȬžهr/S,u)n-n$:W1W]NʞE>JvO5Je* ZU֖ԻzW{aEKcZ<,>KtN(Xc}>Wx ˲8)Z(ʍ>W,SKmW<OLu-vYqeAvsludrP +v66ag;X:#yXPΤsB>O(X~cDm*/Y5Ž,Y OAb!歇=\G/uuw-L0pՙgy˷lU\mZb(+VUa PXΒn +òrBKv/#1Ŝu;<.FyPT$Rwr)).6+_eqթg]+JLc+ݹjJl˻{wѻ _.%ʍWK焺e1K-Ž,[i~Hs`{oX)7Q}2Hw$O}CSS]*Wq$Yqxt>a+ Z|։ 0EJk=/A2hJ֮{-c8KWrBŪa ',eYu𴘵nGw+#>qJ}dw$Oq'}WS2\%=+kegV 65U=ȻH"eUòrBŒ}wXy?Xl^,g]O|YV[|XvO<ۃd)SڛyŸb:[d_TL^J\=+c ˞c(_MVp A{XcQҬ߾9aqNh1ŝωX_$^w/18*bb_&[7#RnnqdMgYO|*TܯJfg]+j[!}); h1V^E^/%Y%Ҍup&'.X|c"Bnqe2W0k]Cs# sT>/>rޔS*:a*gpgA'v0԰lգ + h~Vm}̜0iλ>me2C, /=xf+o]}.kϯd*ݯzUUk.]Auy] [X5gyXV^(23ԜŹ!5kliZg$9_$tNz.uKI,jJ:WCr): ZUMuz}X[3%9K2tf(9KsXD[xa<ޔKix*T\UfJj+=8AyX0r^UYkƊzY:3%YӒu-\]{j;T>\A_*R42UU\AAAeH6/rV5ZZj=ylA4c՜-l sg h{\5'TCg5n+VA^5V^ra gMZ[`1*w60YͩkAr*ߓxYplݬaUbLU_U, XCHfY^v8kM]SSC'WE[AA`h'5j=ܽs96Ky8 +IߊJ-Cgt<9;B6?#굸zVIo묻fMj}M5W1JB[imچuTB=UTwU +kDghXo+js[S=}O5fVR͕^Z{֝{pS*p7:Ru\9`QJTmS*سZZrZdM=W4[묷rFNz9tY=Z+JkJJWջr{f7XrSS*m=(9MkAkVMs(mҞUpΪm]#v_9}nbrF\h*޿={kQaoҌ>_`o[5k +WgˊXaVw]@k)`@@OFKOKOmD|! +1 Om"! +1 Om" J +ɒ,G@Q@MHL3ڬi2=<<:,vou=2G?~}nu//Y=QYfYh̪bVuT==O_L8U[Wi,ьU*S-A_쭖ZYe0ݺꪩ"LKtW魳ie;rg;̬Fw٬uFRꭳ:iYfY<̪֦ji^}2魖δ:+r~>ifꬩ=iLwEz3Κ=ʶZUYUKSTs|U[{v,3]`kWEΦjih~mHse[+?\YfYlfu<`oW6UuK};jLk}ZU1/์n>0::FWvUOSO3zfZu3le_ʻVUvlSezju#hoEgZ=uv7lg3r' VUdXUS 8;֨vVdgezVU讪cez묵z:W转գV3.vMvjY=,G3*<]:z]ٙV됝eU ͱ%V lgfV̴̪z:",VUUUW}}qbVucã:zްzF-t `O*s*;UwnZXG,p_Um}FG]UTϳjmG3a0|mkU=:cUUўl+::Y£;YvmU <=W~YuVvg `/VU3ʮjֶZS5MvV,KcʶU>ef[WE۪LTw gZw:+/y@m}B>lfU#*Vٮlzeen,mx޽`O[ݵFfVꪭF4wi,+/<,]*r=Y}a[6U>mg;lmXiu[c1|gTUKSE*{Z{+Y}u7oVXmջꪳjޙUΚiHg4Vܰwz'fuj*TmHWe[ˏN*:kDce;+2Xaf[=zCE[͚Ye*WUmʜHk]uVzgR/aB=\h/chޮ]UOSm3'Z-5h_zキ~{~5ͮռVtU쪲Z{mVxt2<{?,kr `Uʶh[eꬥZN{ZfY{ƺz}C+*UL[Eo=֊vVkcGee-X)Վmku;zϝ}z{*[:kfcU}~>gm,lșu2_mfmUO[9WhX=aehj,siO'ͭF|L[u{ޟΊ̲b5cgj6^>{K[}1zw{Q[E{*rZ:+XW{}mG;_$w|V l֮ʶVEcEhvuVٮj鬣:U1-3zЙ6p͂j^Logl,[}jZUꫪ:묪jV6V}w{B63\i|OG.Wjig5V}X~/}V0ОU ꯝ'}@o_w諳`YfEc]N3b5ymR:묞YH޺4V۫ᆍeO3Uk㜵֣Ί6VtUW-g]5VoBlw W7s/XujF[誳^{9뭏GYHK_}hv˽ƪw'{ȝgWUκs[z}}=;~ngoG}UXg Ͼꨛx5:ꬣz+KochV3QXg +gŲ'^Qj^w.m骫WUg ,aWmUUFŪxl仮;W9EE[tU}p|w[7*"14 +흉UVQ$$A!Ȥ2"A`0& ! !?]}jvW9{߫Z2]pڵ먣QgLI=&}ڤOMaK>eSIzͤW^5鯦> DO"N?*ӏ>6}^S$=떞~/~nc)?~]L~nsß>.Jc&_JGBCLGEEE-jym!>_' +**j%PU\%[hVUS!S1GG}1]Zg2͜54JK⟝Gd+ƘOQQQ^-\pU`_?{y` WNL%t>2-Cb8KӲJK{.VyZUp@_YCU _ 1' +***j>RO{n_pLU_bJjZ30/!4Bx窔,D*a*aR_?;=}ZJ=Y֜L}\ŊUyiX9V^аVj ծJfi8n^+Yf\ZյS7LԿpZspYC<2/,dswKv/er+g{y%k|i=ֲZÊEЮe+ZE1|틦]u kuYi\L%,IoJ}BBΒhYc'v|>9aU^z%J+'&bjVTTJl]y +grҬ9 r3Քn-d-Abƺsq^Ŭ0-J'w:Գ.jUjYְ}QQQR]u׮p.1ߊ[iկB2ԖIߝN̥q&вx^193;R˳GU1s_W7֍F[22հƪ12R:m \YluYPE*d)?;魓 oiu+hY5Że1kq'TY\Zg9-ǜ{a-#Tmvjتfa2ݿ\aEVCTTԢԘ* j+kl[l&ӶIߟz{R˟oBκ` ;3[Zud~X1W}ZiKb!k( uV\VTTJ ֮ UngPӮx.qOb+fuURNS溟8k hYü=Yca~z8C 3\2짟dJRhy?+̵xFXW^k2VFEE-j5kqK ѮUvU2F1d0]c+.LUTS1GR烙z8^ҲjKc-CjΪҼUWt̳~Vck_+ >e= KK7o4|QQQR'*]u^Y{ڵ}A.˵ +Jw'JjPNI2=k[U٠]ivb/k9 +[=t&Ѭv%FBzfN9M z֣nXU˜LZB +oqdKc4ƺѰxF؂J}XQQQ>ЗJ9vboIV[P \%Lg{'ӷMy8Iв&? +X8Gla-+*dUU|#H´rn̲ }X2#DVniWN i݅J3H[e5soWcEEE-`=]૒ ] kWiڜvJ4+*a&/= ,f,Kvcy9(9|ևDL{$_/٪x/vUˌP=F8 ,dg` XWy!6f@2WFn} ̮n$Ń|H]X]43!"k4**j9kW_X`)_]^C|1pg}W]\kl\6eB:s ^@75+W[tRO*[wF8aUTTԘ5v.Ø{]:gc7%Jvw l%\ޤWzcބY~+9SsBb1c%71{][p1Ǩ5ߺfb k35V0Erl\]X]5f=¡ܣ{5V|٠eg}W]xJ?phYc c˛ZŒw*YV8d + +o\';Xm :e|$WLiXCUphV1a>|%J5A֮d.F",a??#gi^,%sB2.JƵhV:wޢX;=WeW|e1V ka{cx{Uzop|uZ!_y9o归A`$a+dCZX%{8'}BsB@ǻZTx3l\Vn]K m-e^@+avYaG8VNCTTTX|5ffF|u+A-W-g{%{ ;b +jR}(X2'԰,kMFǎl%'Z?K%:hsZ*{^Wv٠VPXCkX53Yx<ƊZ+-A{oVgu᫚A-ge` >v?k qӰsBpXV2|R= o7[y|=Xqgsl[>%*cYV wp,૨yY^Xb0WTnⰷ]p.{d6hiWWF?{4'4,-|9 &jnb+mXly.;ѐs+^4*k, kV,KÊjX|ee3h7Aێ+ jڕ0'X%wzs XwS!WmVY W OMo>f,Va-_]W MӮDA5KPr{5&_y7|%W{ f]1[1c焚%`M;㇌u)[ɞ ,ڗpycO"p0jܸAbަU\Pq3cFǝ +Wg6+VyܑmwpHo{-_հXڌ! s9w'xGo|Q-A|udFyᾂhXX}A ]c)r3c])2_y+֭jόp{ܱew=W9ZavkS/R-) W[A#_'cFXPӰ'¹`vsA\ȍ|Ce+++3VɌpyq/a1V4,˃w+aQ+~Ѓ\N4 +W![zQjyc#c3B|} jgexN}૨EV_rXUyܯq]!<:`dk3B԰l%sAvsNjyc4,o_*+~H|vfv=B8aƒH;_EEEA`]x+``x+=q,`pXV]QJc,W43#lZ૨;XK=5,mF׃`mykX3B+Kf4K\W|J!܅v+̻:+d+᪃ +cWrΤ9Β,bqoC8DFCVJ6C ʱ4YYb$w,Mbb2ԴsFzK`+A8_jWڼ|uQ&j|53ZJ +,J5  K|7U#8A4,>&cf>WǝgX8AdaY> :TWo*|ee4  [^u3_![izܵQ+MI&ʼnO}e"_i;K|Ն>?gR٠W`{󺣆7sg*j3֡Jz, ^i|yq6l5:QwjJp(`xܻzaNògaRo{>/WCw嫥|c+w}ְnZ,ykw3B԰ƹ`vVdBb gb{KrFeFx3#հj [g3 W_Wm5KbcZ;g|%?XQ3#.6'1Vm-k ***j^_1cu+-S~Oji:bF4ǝw9d%YsBf^Tʻc.l'Db5#|ÌF7Œ将Żت9_}IU0A42FKJV΃yܽCX]ʚdEis·9XY/fjǹV43 QzP#QGXaq7'z*z6ؗ0>***jg%|utC14cكUChe`i- +u+Ή o+sBaWdW@\b?+͌sBѰ-#,bhiX^Ʋت%_7e_1~U-Yz>?7rځ&9^P|ˋqݾRﰕu[Eif,bVC!g=B7 9K4,K٪'_IVqUTT<"ȩ/- _ވ3}gYZ'EPcΒFbzʺBr_f9e{sai^w^Hn.t{K o}yp*c-.5wk㫋ի2c,l`?kY`Jc+aQb,O5-;g5а߷Yk +٪v5=***jQ3Fk3dEk/܀r;^Fgj|7rZؼ;ỉ`3: +g!kaN4bϕVSW>34,=B/krsQKb{ cy}rcjC!E{VZѰ|W_*伨YAFz`+slo%aݛZ,,niXgugcJS +j2Ⱦ +***j|U5FFCWV+:'_q{_8WoPU.D/X?*%%-<%ջl)x52մ=ΒYai>W.(3§>w-5,jXV ****jd`u!}|xp3p/> +YB +ۂl%\5?af{!˹²|b{تvad3_EEEcVmF<v`u+'Ͱ<:!S>y.%%<%˗VWgjpNܵ{93B- o>k>w+oTfai^w߻Yڹ|[_EEE-zFX;.w畯'zvqv0S1g b,5ԮռNa+{dFxRf6(|%vkL.gΒ +]7݅;5rX8'D }{3 u`jiw0***jjF1Cwp{^S +WTrUG:s73kX<##QGX3#xpC}[բaTTTԼœtFc+-O>U|% i ߓr9 +B=Kz)7>d+1 }Xڌ~KbVkXڜEsñ`# +dUv@BxUՈHk(*   +"b$ + 2B8cɵN+8ӽ73^+~/W}>q89Qq8ü3a094SC¼-[ü%aMq aAqd TG^k5SkZx]ܛx|k6®o5Gkڻ;^Li~tymO]OufZ9:_=a9&̱a0'P0xO0g9/B/"Ua0 +sk3'a +H<0τ0 |ü0moV%ݗ?ͳbWsy^0^!ka^0 +0Wec=>X0Oy4,a sw^[z_ sur/8?|6̧0 0'ǟ syyƟws7ѿskڿ7JjW|uW|E#V^C^|b|uWUꚟb,5^?Xp${_K:~"EՊ9`,$kɡ=pKlyol8c_1>j/״>:Sa~WQ;߇qS|=|%w0sN)|ujYXLg=aE|bܴmw hS _՛zWp>xX|9ƒlUWY_|%+bmJ9XWI+VR<^9 0Fq{"_6k\0D:?̹Vasz8%Gœjs01:VJ[e6[E jՁJB-dFv##$RUZ59):_*f]2z10pֲ8K-94ϪW gTFjѧ#_=a|uU|V|ui\j~&Ya>"_2B+Xg+OJ+>W+6$c2qe] '+-{#YkYZ\%+W +_=.>W滌n呯.|u^>`ia*ս*< +%Ւ:X%W0]Y|7|BJv_Mp5,"ŹKU5lw6t|H䫟2ժ~3+__idH{e] !ֹuhVMFe,{@ ⫯|u{%_`hqk->[_O-[i||u/ J!| [,XGf%vWMM`t-4z>&Ww$_QFgXeRùb=A^WVj_}W:JvrV԰һW%+%vz{X)⯹_jd|u{b'|"i)Z)OKlU]i|;?_s2|eA|5jUK +|AЦtZ45cwLW - Ϡ%_Nc+,⬥1l]W^gv_Y|KX)v"f3 \Z +A:(#aI҆^kU)_}/W3^#AW|Rеʍ~Ib/lbzS𕗱v +WQ{R?AWSd;VƲ|5WߌӉ{`|X_TCNb{aegȝl+ykFrFesw +|A:jВzX$g?g鑯Ί{ω/w_}7z(wN>.G.ea~-#굊+ohl=?ޕ]Q6uqh_TU{~+V|U NqAA4zXciymrW{$Ov:Mwm +_юGَ߰W|YkYg'cysKRޕֽv~gyA?]/ZWpOGн:b!{rl +M`K=zgGh3DuWײX9A9CYY^\|dޕ uo`n|_ h|ВdcIΒ xNPcLXZ1ZXvsii'4JfVJ;;x! ,?XWciZ e+Za.'KͻJ2|;7=uwS,ɾx. +ܠ UjZW h+#,<XjJ븟9BszX9a\ +gkqJuKxROIj\Ey `![Q&يwN䂲w<`Oڻ  BR{-Q:͛)LŽ*9WQϊ{Van`]v[J:=|%],ͻ*^AM]z}] SxX+q`QFYB7eXta>ap3["ޢ!n~j>-SQUWgu=ouLֹrA̠]y^gk h)Voq|eete\Kp0u<8gQnH DEECg,%>?US?2*%WlxVWFseOY\weyW#]]k ږ<gF򕶣:XZF,!gu/c =E +yz\ZRRT Om0yUԯ~o +RV(gle֙Ao6؃zP|Av2|u<aɜbuXtcQ^x?eg=,<o3羔'S?P|ۘWEg)JzV9`]vV|ϕֹr歹jk] hj=Βl+z<,K.g,cѹBڏE^Yg=$XkC0>5rT/]d(LE^8W,rųݡ)l%VZ]傥վ 񮰓MѮ5a/a-gT5>,+' X<+nU񲨗EE!os炥4oOI[SWE*WWYu>v0H:VZ.]UIAPF:zX%+K5a񜐟';$cQ,/$/Ϣ~h=KRqQQ*WY\Ϊ`+ٹR|a9vc'A۪==9++#yX2'.`1uiwd(7ֽh>KRlw%"/$/jY`oswYC!,5KLu>뭟Ug%@;겧تջ7CAzxX]ڜ0WyJ .X~.ۏE,, +k}'rg./qdڼҽ)U2 #F%ﴣwAжkJs'֔| eb,~P2兒.E!4xxI}5FTʣ2_OU'xV9A/[jr\s]A)<]0m_tR%w7hyeiu(?řϊhǮb%YwR:;Av2wim}*/J[٪7c-J@C3`+Ԯ5b{O/k)szt{ҹ6L_X:[sAW'jᬥU-gU,Ƴt<[AmXNV˪嬥 jIVϪ$L [AmօJ:Y-^w\|Փj 6_5|5rwCj;Y^ՋzV-W_2ȣ;qUϮ + hkՓjY^={jWVjIl>;AZ +czY#9kuVJel*AvhnƚYZS^[AmFpV k-SS*RYy 2A\dZ8*\әzrϪ 6SS0w/j^5SL5zzV%lAyX^zV-o-iFT SpUg5k \-j)9vLr\k<]+AkNƪ K2Z^YSjzyV`+ hjj齷zY#8+iV obV~S|^\[;=z +AXyad5iYV5^=QYfYh̪bVuT==O_L8U[Wi,ьU*S-A_쭖ZYe0ݺꪩ"LKtW魳ie;rg;̬Fw٬uFRꭳ:iYfY<̪֦ji^}2魖δ:+r~>ifꬩ=iLwEz3Κ=ʶZUYUKSTs|U[{v,3]`kWEΦjih~mHse[+?\YfYlfu<`oW6UuK};jLk}ZU1/์n>0::FWvUOSO3zfZu3le_ʻVUvlSezju#hoEgZ=uv7lg3r' VUdXUS 8;֨vVdgezVU讪cez묵z:W转գV3.vMvjY=,G3*<]:z]ٙV됝eU ͱ%V lgfV̴̪z:",VUUUW}}qbVucã:zްzF-t `O*s*;UwnZXG,p_Um}FG]UTϳjmG3a0|mkU=:cUUўl+::Y£;YvmU <=W~YuVvg `/VU3ʮjֶZS5MvV,KcʶU>ef[WE۪LTw gZw:+/y@m}B>lfU#*Vٮlzeen,mx޽`O[ݵFfVꪭF4wi,+/<,]*r=Y}a[6U>mg;lmXiu[c1|gTUKSE*{Z{+Y}u7oVXmջꪳjޙUΚiHg4Vܰwz'fuj*TmHWe[ˏN*:kDce;+2Xaf[=zCE[͚Ye*WUmʜHk]uVzgR/aB=\h/chޮ]UOSm3'Z-5h_zキ~{~5ͮռVtU쪲Z{mVxt2<{?,kr `Uʶh[eꬥZN{ZfY{ƺz}C+*UL[Eo=֊vVkcGee-X)Վmku;zϝ}z{*[:kfcU}~>gm,lșu2_mfmUO[9WhX=aehj,siO'ͭF|L[u{ޟΊ̲b5cgj6^>{K[}1zw{Q[E{*rZ:+XW{}mG;_$w|V l֮ʶVEcEhvuVٮj鬣:U1-3zЙ6p͂j^Logl,[}jZUꫪ:묪jV6V}w{B63\i|OG.Wjig5V}X~/}V0ОU ꯝ'}@o_w諳`YfEc]N3b5ymR:묞YH޺4V۫ᆍeO3Uk㜵֣Ί6VtUW-g]5VoBlw W7s/XujF[誳^{9뭏GYHK_}hv˽ƪw'{ȝgWUκs[z}}=;~ngoG}UXg Ͼꨛx5:ꬣz+KochV3QXg +gŲ'^Qj^w.m骫WUg ,aWmUUFŪxl仮;W9EE[tU}p|w[7*"14 +흉UVQ$$A!Ȥ2"A`0& ! !?]}jvW9{߫Z2]pڵ먣QgLI=&}ڤOMaK>eSIzͤW^5鯦> DO"N?*ӏ>6}^S$=떞~/~nc)?~]L~nsß>.Jc&_JGBCLGEEE-jym!>_' +**j%PU\%[hVUS!S1GG}1]Zg2͜54JK⟝Gd+ƘOQQQ^-\pU`_?{y` WNL%t>2-Cb8KӲJK{.VyZUp@_YCU _ 1' +***j>RO{n_pLU_bJjZ30/!4Bx窔,D*a*aR_?;=}ZJ=Y֜L}\ŊUyiX9V^аVj ծJfi8n^+Yf\ZյS7LԿpZspYC<2/,dswKv/er+g{y%k|i=ֲZÊEЮe+ZE1|틦]u kuYi\L%,IoJ}BBΒhYc'v|>9aU^z%J+'&bjVTTJl]y +grҬ9 r3Քn-d-Abƺsq^Ŭ0-J'w:Գ.jUjYְ}QQQR]u׮p.1ߊ[iկB2ԖIߝN̥q&вx^193;R˳GU1s_W7֍F[22հƪ12R:m \YluYPE*d)?;魓 oiu+hY5Że1kq'TY\Zg9-ǜ{a-#Tmvjتfa2ݿ\aEVCTTԢԘ* j+kl[l&ӶIߟz{R˟oBκ` ;3[Zud~X1W}ZiKb!k( uV\VTTJ ֮ UngPӮx.qOb+fuURNS溟8k hYü=Yca~z8C 3\2짟dJRhy?+̵xFXW^k2VFEE-j5kqK ѮUvU2F1d0]c+.LUTS1GR烙z8^ҲjKc-CjΪҼUWt̳~Vck_+ >e= KK7o4|QQQR'*]u^Y{ڵ}A.˵ +Jw'JjPNI2=k[U٠]ivb/k9 +[=t&Ѭv%FBzfN9M z֣nXU˜LZB +oqdKc4ƺѰxF؂J}XQQQ>ЗJ9vboIV[P \%Lg{'ӷMy8Iв&? +X8Gla-+*dUU|#H´rn̲ }X2#DVniWN i݅J3H[e5soWcEEE-`=]૒ ] kWiڜvJ4+*a&/= ,f,Kvcy9(9|ևDL{$_/٪x/vUˌP=F8 ,dg` XWy!6f@2WFn} ̮n$Ń|H]X]43!"k4**j9kW_X`)_]^C|1pg}W]\kl\6eB:s ^@75+W[tRO*[wF8aUTTԘ5v.Ø{]:gc7%Jvw l%\ޤWzcބY~+9SsBb1c%71{][p1Ǩ5ߺfb k35V0Erl\]X]5f=¡ܣ{5V|٠eg}W]xJ?phYc c˛ZŒw*YV8d + +o\';Xm :e|$WLiXCUphV1a>|%J5A֮d.F",a??#gi^,%sB2.JƵhV:wޢX;=WeW|e1V ka{cx{Uzop|uZ!_y9o归A`$a+dCZX%{8'}BsB@ǻZTx3l\Vn]K m-e^@+avYaG8VNCTTTX|5ffF|u+A-W-g{%{ ;b +jR}(X2'԰,kMFǎl%'Z?K%:hsZ*{^Wv٠VPXCkX53Yx<ƊZ+-A{oVgu᫚A-ge` >v?k qӰsBpXV2|R= o7[y|=Xqgsl[>%*cYV wp,૨yY^Xb0WTnⰷ]p.{d6hiWWF?{4'4,-|9 &jnb+mXly.;ѐs+^4*k, kV,KÊjX|ee3h7Aێ+ jڕ0'X%wzs XwS!WmVY W OMo>f,Va-_]W MӮDA5KPr{5&_y7|%W{ f]1[1c焚%`M;㇌u)[ɞ ,ڗpycO"p0jܸAbަU\Pq3cFǝ +Wg6+VyܑmwpHo{-_հXڌ! s9w'xGo|Q-A|udFyᾂhXX}A ]c)r3c])2_y+֭jόp{ܱew=W9ZavkS/R-) W[A#_'cFXPӰ'¹`vsA\ȍ|Ce+++3VɌpyq/a1V4,˃w+aQ+~Ѓ\N4 +W![zQjyc#c3B|} jgexN}૨EV_rXUyܯq]!<:`dk3B԰l%sAvsNjyc4,o_*+~H|vfv=B8aƒH;_EEEA`]x+``x+=q,`pXV]QJc,W43#lZ૨;XK=5,mF׃`mykX3B+Kf4K\W|J!܅v+̻:+d+᪃ +cWrΤ9Β,bqoC8DFCVJ6C ʱ4YYb$w,Mbb2ԴsFzK`+A8_jWڼ|uQ&j|53ZJ +,J5  K|7U#8A4,>&cf>WǝgX8AdaY> :TWo*|ee4  [^u3_![izܵQ+MI&ʼnO}e"_i;K|Ն>?gR٠W`{󺣆7sg*j3֡Jz, ^i|yq6l5:QwjJp(`xܻzaNògaRo{>/WCw嫥|c+w}ְnZ,ykw3B԰ƹ`vVdBb gb{KrFeFx3#հj [g3 W_Wm5KbcZ;g|%?XQ3#.6'1Vm-k ***j^_1cu+-S~Oji:bF4ǝw9d%YsBf^Tʻc.l'Db5#|ÌF7Œ将Żت9_}IU0A42FKJV΃yܽCX]ʚdEis·9XY/fjǹV43 QzP#QGXaq7'z*z6ؗ0>***jg%|utC14cكUChe`i- +u+Ή o+sBaWdW@\b?+͌sBѰ-#,bhiX^Ʋت%_7e_1~U-Yz>?7rځ&9^P|ˋqݾRﰕu[Eif,bVC!g=B7 9K4,K٪'_IVqUTT<"ȩ/- _ވ3}gYZ'EPcΒFbzʺBr_f9e{sai^w^Hn.t{K o}yp*c-.5wk㫋ի2c,l`?kY`Jc+aQb,O5-;g5а߷Yk +٪v5=***jQ3Fk3dEk/܀r;^Fgj|7rZؼ;ỉ`3: +g!kaN4bϕVSW>34,=B/krsQKb{ cy}rcjC!E{VZѰ|W_*伨YAFz`+slo%aݛZ,,niXgugcJS +j2Ⱦ +***j|U5FFCWV+:'_q{_8WoPU.D/X?*%%-<%ջl)x52մ=ΒYai>W.(3§>w-5,jXV ****jd`u!}|xp3p/> +YB +ۂl%\5?af{!˹²|b{تvad3_EEEcVmF<v`u+'Ͱ<:!S>y.%%<%˗VWgjpNܵ{93B- o>k>w+oTfai^w߻Yڹ|[_EEE-zFX;.w畯'zvqv0S1g b,5ԮռNa+{dFxRf6(|%vkL.gΒ +]7݅;5rX8'D }{3 u`jiw0***jjF1Cwp{^S +WTrUG:s73kX<##QGX3#xpC}[բaTTTԼœtFc+-O>U|% i ߓr9 +B=Kz)7>d+1 }Xڌ~KbVkXڜEsñ`# +dUv@BxUՈHk(*   +"b$ + 2B8cɵN+8ӽ73^+~/W}>q89Qq8ü3a094SC¼-[ü%aMq aAqd TG^k5SkZx]ܛx|k6®o5Gkڻ;^Li~tymO]OufZ9:_=a9&̱a0'P0xO0g9/B/"Ua0 +sk3'a +H<0τ0 |ü0moV%ݗ?ͳbWsy^0^!ka^0 +0Wec=>X0Oy4,a sw^[z_ sur/8?|6̧0 0'ǟ syyƟws7ѿskڿ7JjW|uW|E#V^C^|b|uWUꚟb,5^?Xp${_K:~"EՊ9`,$kɡ=pKlyol8c_1>j/״>:Sa~WQ;߇qS|=|%w0sN)|ujYXLg=aE|bܴmw hS _՛zWp>xX|9ƒlUWY_|%+bmJ9XWI+VR<^9 0Fq{"_6k\0D:?̹Vasz8%Gœjs01:VJ[e6[E jՁJB-dFv##$RUZ59):_*f]2z10pֲ8K-94ϪW gTFjѧ#_=a|uU|V|ui\j~&Ya>"_2B+Xg+OJ+>W+6$c2qe] '+-{#YkYZ\%+W +_=.>W滌n呯.|u^>`ia*ս*< +%Ւ:X%W0]Y|7|BJv_Mp5,"ŹKU5lw6t|H䫟2ժ~3+__idH{e] !ֹuhVMFe,{@ ⫯|u{%_`hqk->[_O-[i||u/ J!| [,XGf%vWMM`t-4z>&Ww$_QFgXeRùb=A^WVj_}W:JvrV԰һW%+%vz{X)⯹_jd|u{b'|"i)Z)OKlU]i|;?_s2|eA|5jUK +|AЦtZ45cwLW - Ϡ%_Nc+,⬥1l]W^gv_Y|KX)v"f3 \Z +A:(#aI҆^kU)_}/W3^#AW|Rеʍ~Ib/lbzS𕗱v +WQ{R?AWSd;VƲ|5WߌӉ{`|X_TCNb{aegȝl+ykFrFesw +|A:jВzX$g?g鑯Ί{ω/w_}7z(wN>.G.ea~-#굊+ohl=?ޕ]Q6uqh_TU{~+V|U NqAA4zXciymrW{$Ov:Mwm +_юGَ߰W|YkYg'cysKRޕֽv~gyA?]/ZWpOGн:b!{rl +M`K=zgGh3DuWײX9A9CYY^\|dޕ uo`n|_ h|ВdcIΒ xNPcLXZ1ZXvsii'4JfVJ;;x! ,?XWciZ e+Za.'KͻJ2|;7=uwS,ɾx. +ܠ UjZW h+#,<XjJ븟9BszX9a\ +gkqJuKxROIj\Ey `![Q&يwN䂲w<`Oڻ  BR{-Q:͛)LŽ*9WQϊ{Van`]v[J:=|%],ͻ*^AM]z}] SxX+q`QFYB7eXta>ap3["ޢ!n~j>-SQUWgu=ouLֹrA̠]y^gk h)Voq|eete\Kp0u<8gQnH DEECg,%>?US?2*%WlxVWFseOY\weyW#]]k ږ<gF򕶣:XZF,!gu/c =E +yz\ZRRT Om0yUԯ~o +RV(gle֙Ao6؃zP|Av2|u<aɜbuXtcQ^x?eg=,<o3羔'S?P|ۘWEg)JzV9`]vV|ϕֹr歹jk] hj=Βl+z<,K.g,cѹBڏE^Yg=$XkC0>5rT/]d(LE^8W,rųݡ)l%VZ]傥վ 񮰓MѮ5a/a-gT5>,+' X<+nU񲨗EE!os炥4oOI[SWE*WWYu>v0H:VZ.]UIAPF:zX%+K5a񜐟';$cQ,/$/Ϣ~h=KRqQQ*WY\Ϊ`+ٹR|a9vc'A۪==9++#yX2'.`1uiwd(7ֽh>KRlw%"/$/jY`oswYC!,5KLu>뭟Ug%@;겧تջ7CAzxX]ڜ0WyJ .X~.ۏE,, +k}'rg./qdڼҽ)U2 #F%ﴣwAжkJs'֔| eb,~P2兒.E!4xxI}5FTʣ2_OU'xV9A/[jr\s]A)<]0m_tR%w7hyeiu(?řϊhǮb%YwR:;Av2wim}*/J[٪7c-J@C3`+Ԯ5b{O/k)szt{ҹ6L_X:[sAW'jᬥU-gU,Ƴt<[AmXNV˪嬥 jIVϪ$L [AmօJ:Y-^w\|Փj 6_5|5rwCj;Y^ՋzV-W_2ȣ;qUϮ + hkՓjY^={jWVjIl>;AZ +czY#9kuVJel*AvhnƚYZS^[AmFpV k-SS*RYy 2A\dZ8*\әzrϪ 6SS0w/j^5SL5zzV%lAyX^zV-o-iFT SpUg5k \-j)9vLr\k<]+AkNƪ K2Z^YSjzyV`+ hjj齷zY#8+iV obV~S|^\[;=z +AXyad5iYV5^=QYfYh̪bVuT==O_L8U[Wi,ьU*S-A_쭖ZYe0ݺꪩ"LKtW魳ie;rg;̬Fw٬uFRꭳ:iYfY<̪֦ji^}2魖δ:+r~>ifꬩ=iLwEz3Κ=ʶZUYUKSTs|U[{v,3]`kWEΦjih~mHse[+?\YfYlfu<`oW6UuK};jLk}ZU1/์n>0::FWvUOSO3zfZu3le_ʻVUvlSezju#hoEgZ=uv7lg3r' VUdXUS 8;֨vVdgezVU讪cez묵z:W转գV3.vMvjY=,G3*<]:z]ٙV됝eU ͱ%V lgfV̴̪z:",VUUUW}}qbVucã:zްzF-t `O*s*;UwnZXG,p_Um}FG]UTϳjmG3a0|mkU=:cUUўl+::Y£;YvmU <=W~YuVvg `/VU3ʮjֶZS5MvV,KcʶU>ef[WE۪LTw gZw:+/y@m}B>lfU#*Vٮlzeen,mx޽`O[ݵFfVꪭF4wi,+/<,]*r=Y}a[6U>mg;lmXiu[c1|gTUKSE*{Z{+Y}u7oVXmջꪳjޙUΚiHg4Vܰwz'fuj*TmHWe[ˏN*:kDce;+2Xaf[=zCE[͚Ye*WUmʜHk]uVzgR/aB=\h/chޮ]UOSm3'Z-5h_zキ~{~5ͮռVtU쪲Z{mVxt2<{?,kr `Uʶh[eꬥZN{ZfY{ƺz}C+*UL[Eo=֊vVkcGee-X)Վmku;zϝ}z{*[:kfcU}~>gm,lșu2_mfmUO[9WhX=aehj,siO'ͭF|L[u{ޟΊ̲b5cgj6^>{K[}1zw{Q[E{*rZ:+XW{}mG;_$w|V l֮ʶVEcEhvuVٮj鬣:U1-3zЙ6p͂j^Logl,[}jZUꫪ:묪jV6V}w{B63\i|OG.Wjig5V}X~/}V0ОU ꯝ'}@o_w諳`YfEc]N3b5ymR:묞YH޺4V۫ᆍeO3Uk㜵֣Ί6VtUW-g]5VoBlw W7s/XujF[誳^{9뭏GYHK_}hv˽ƪw'{ȝgWUκs[z}}=;~ngoG}UXg Ͼꨛx5:ꬣz+KochV3QXg +gŲ'^Qj^w.m骫WUg ,aWmUUFŪxl仮;W9EE[tU}p|w[7*"14 +흉UVQ$$A!Ȥ2"A`0& ! !?]}jvW9{߫Z2]pڵ먣QgLI=&}ڤOMaK>eSIzͤW^5鯦> DO"N?*ӏ>6}^S$=떞~/~nc)?~]L~nsß>.Jc&_JGBCLGEEE-jym!>_' +**j%PU\%[hVUS!S1GG}1]Zg2͜54JK⟝Gd+ƘOQQQ^-\pU`_?{y` WNL%t>2-Cb8KӲJK{.VyZUp@_YCU _ 1' +***j>RO{n_pLU_bJjZ30/!4Bx窔,D*a*aR_?;=}ZJ=Y֜L}\ŊUyiX9V^аVj ծJfi8n^+Yf\ZյS7LԿpZspYC<2/,dswKv/er+g{y%k|i=ֲZÊEЮe+ZE1|틦]u kuYi\L%,IoJ}BBΒhYc'v|>9aU^z%J+'&bjVTTJl]y +grҬ9 r3Քn-d-Abƺsq^Ŭ0-J'w:Գ.jUjYְ}QQQR]u׮p.1ߊ[iկB2ԖIߝN̥q&вx^193;R˳GU1s_W7֍F[22հƪ12R:m \YluYPE*d)?;魓 oiu+hY5Że1kq'TY\Zg9-ǜ{a-#Tmvjتfa2ݿ\aEVCTTԢԘ* j+kl[l&ӶIߟz{R˟oBκ` ;3[Zud~X1W}ZiKb!k( uV\VTTJ ֮ UngPӮx.qOb+fuURNS溟8k hYü=Yca~z8C 3\2짟dJRhy?+̵xFXW^k2VFEE-j5kqK ѮUvU2F1d0]c+.LUTS1GR烙z8^ҲjKc-CjΪҼUWt̳~Vck_+ >e= KK7o4|QQQR'*]u^Y{ڵ}A.˵ +Jw'JjPNI2=k[U٠]ivb/k9 +[=t&Ѭv%FBzfN9M z֣nXU˜LZB +oqdKc4ƺѰxF؂J}XQQQ>ЗJ9vboIV[P \%Lg{'ӷMy8Iв&? +X8Gla-+*dUU|#H´rn̲ }X2#DVniWN i݅J3H[e5soWcEEE-`=]૒ ] kWiڜvJ4+*a&/= ,f,Kvcy9(9|ևDL{$_/٪x/vUˌP=F8 ,dg` XWy!6f@2WFn} ̮n$Ń|H]X]43!"k4**j9kW_X`)_]^C|1pg}W]\kl\6eB:s ^@75+W[tRO*[wF8aUTTԘ5v.Ø{]:gc7%Jvw l%\ޤWzcބY~+9SsBb1c%71{][p1Ǩ5ߺfb k35V0Erl\]X]5f=¡ܣ{5V|٠eg}W]xJ?phYc c˛ZŒw*YV8d + +o\';Xm :e|$WLiXCUphV1a>|%J5A֮d.F",a??#gi^,%sB2.JƵhV:wޢX;=WeW|e1V ka{cx{Uzop|uZ!_y9o归A`$a+dCZX%{8'}BsB@ǻZTx3l\Vn]K m-e^@+avYaG8VNCTTTX|5ffF|u+A-W-g{%{ ;b +jR}(X2'԰,kMFǎl%'Z?K%:hsZ*{^Wv٠VPXCkX53Yx<ƊZ+-A{oVgu᫚A-ge` >v?k qӰsBpXV2|R= o7[y|=Xqgsl[>%*cYV wp,૨yY^Xb0WTnⰷ]p.{d6hiWWF?{4'4,-|9 &jnb+mXly.;ѐs+^4*k, kV,KÊjX|ee3h7Aێ+ jڕ0'X%wzs XwS!WmVY W OMo>f,Va-_]W MӮDA5KPr{5&_y7|%W{ f]1[1c焚%`M;㇌u)[ɞ ,ڗpycO"p0jܸAbަU\Pq3cFǝ +Wg6+VyܑmwpHo{-_հXڌ! s9w'xGo|Q-A|udFyᾂhXX}A ]c)r3c])2_y+֭jόp{ܱew=W9ZavkS/R-) W[A#_'cFXPӰ'¹`vsA\ȍ|Ce+++3VɌpyq/a1V4,˃w+aQ+~Ѓ\N4 +W![zQjyc#c3B|} jgexN}૨EV_rXUyܯq]!<:`dk3B԰l%sAvsNjyc4,o_*+~H|vfv=B8aƒH;_EEEA`]x+``x+=q,`pXV]QJc,W43#lZ૨;XK=5,mF׃`mykX3B+Kf4K\W|J!܅v+̻:+d+᪃ +cWrΤ9Β,bqoC8DFCVJ6C ʱ4YYb$w,Mbb2ԴsFzK`+A8_jWڼ|uQ&j|53ZJ +,J5  K|7U#8A4,>&cf>WǝgX8AdaY> :TWo*|ee4  [^u3_![izܵQ+MI&ʼnO}e"_i;K|Ն>?gR٠W`{󺣆7sg*j3֡Jz, ^i|yq6l5:QwjJp(`xܻzaNògaRo{>/WCw嫥|c+w}ְnZ,ykw3B԰ƹ`vVdBb gb{KrFeFx3#հj [g3 W_Wm5KbcZ;g|%?XQ3#.6'1Vm-k ***j^_1cu+-S~Oji:bF4ǝw9d%YsBf^Tʻc.l'Db5#|ÌF7Œ将Żت9_}IU0A42FKJV΃yܽCX]ʚdEis·9XY/fjǹV43 QzP#QGXaq7'z*z6ؗ0>***jg%|utC14cكUChe`i- +u+Ή o+sBaWdW@\b?+͌sBѰ-#,bhiX^Ʋت%_7e_1~U-Yz>?7rځ&9^P|ˋqݾRﰕu[Eif,bVC!g=B7 9K4,K٪'_IVqUTT<"ȩ/- _ވ3}gYZ'EPcΒFbzʺBr_f9e{sai^w^Hn.t{K o}yp*c-.5wk㫋ի2c,l`?kY`Jc+aQb,O5-;g5а߷Yk +٪v5=***jQ3Fk3dEk/܀r;^Fgj|7rZؼ;ỉ`3: +g!kaN4bϕVSW>34,=B/krsQKb{ cy}rcjC!E{VZѰ|W_*伨YAFz`+slo%aݛZ,,niXgugcJS +j2Ⱦ +***j|U5FFCWV+:'_q{_8WoPU.D/X?*%%-<%ջl)x52մ=ΒYai>W.(3§>w-5,jXV ****jd`u!}|xp3p/> +YB +ۂl%\5?af{!˹²|b{تvad3_EEEcVmF<v`u+'Ͱ<:!S>y.%%<%˗VWgjpNܵ{93B- o>k>w+oTfai^w߻Yڹ|[_EEE-zFX;.w畯'zvqv0S1g b,5ԮռNa+{dFxRf6(|%vkL.gΒ +]7݅;5rX8'D }{3 u`jiw0***jjF1Cwp{^S +WTrUG:s73kX<##QGX3#xpC}[բaTTTԼœtFc+-O>U|% i ߓr9 +B=Kz)7>d+1 }Xڌ~KbVkXڜEsñ`# +dUv@BxUՈHk(*   +"b$ + 2B8cɵN+8ӽ73^+~/W}>q89Qq8ü3a094SC¼-[ü%aMq aAqd TG^k5SkZx]ܛx|k6®o5Gkڻ;^Li~tymO]OufZ9:_=a9&̱a0'P0xO0g9/B/"Ua0 +sk3'a +H<0τ0 |ü0moV%ݗ?ͳbWsy^0^!ka^0 +0Wec=>X0Oy4,a sw^[z_ sur/8?|6̧0 0'ǟ syyƟws7ѿskڿ7JjW|uW|E#V^C^|b|uWUꚟb,5^?Xp${_K:~"EՊ9`,$kɡ=pKlyol8c_1>j/״>:Sa~WQ;߇qS|=|%w0sN)|ujYXLg=aE|bܴmw hS _՛zWp>xX|9ƒlUWY_|%+bmJ9XWI+VR<^9 0Fq{"_6k\0D:?̹Vasz8%Gœjs01:VJ[e6[E jՁJB-dFv##$RUZ59):_*f]2z10pֲ8K-94ϪW gTFjѧ#_=a|uU|V|ui\j~&Ya>"_2B+Xg+OJ+>W+6$c2qe] '+-{#YkYZ\%+W +_=.>W滌n呯.|u^>`ia*ս*< +%Ւ:X%W0]Y|7|BJv_Mp5,"ŹKU5lw6t|H䫟2ժ~3+__idH{e] !ֹuhVMFe,{@ ⫯|u{%_`hqk->[_O-[i||u/ J!| [,XGf%vWMM`t-4z>&Ww$_QFgXeRùb=A^WVj_}W:JvrV԰һW%+%vz{X)⯹_jd|u{b'|"i)Z)OKlU]i|;?_s2|eA|5jUK +|AЦtZ45cwLW - Ϡ%_Nc+,⬥1l]W^gv_Y|KX)v"f3 \Z +A:(#aI҆^kU)_}/W3^#AW|Rеʍ~Ib/lbzS𕗱v +WQ{R?AWSd;VƲ|5WߌӉ{`|X_TCNb{aegȝl+ykFrFesw +|A:jВzX$g?g鑯Ί{ω/w_}7z(wN>.G.ea~-#굊+ohl=?ޕ]Q6uqh_TU{~+V|U NqAA4zXciymrW{$Ov:Mwm +_юGَ߰W|YkYg'cysKRޕֽv~gyA?]/ZWpOGн:b!{rl +M`K=zgGh3DuWײX9A9CYY^\|dޕ uo`n|_ h|ВdcIΒ xNPcLXZ1ZXvsii'4JfVJ;;x! ,?XWciZ e+Za.'KͻJ2|;7=uwS,ɾx. +ܠ UjZW h+#,<XjJ븟9BszX9a\ +gkqJuKxROIj\Ey `![Q&يwN䂲w<`Oڻ  BR{-Q:͛)LŽ*9WQϊ{Van`]v[J:=|%],ͻ*^AM]z}] SxX+q`QFYB7eXta>ap3["ޢ!n~j>-SQUWgu=ouLֹrA̠]y^gk h)Voq|eete\Kp0u<8gQnH DEECg,%>?US?2*%WlxVWFseOY\weyW#]]k ږ<gF򕶣:XZF,!gu/c =E +yz\ZRRT Om0yUԯ~o +RV(gle֙Ao6؃zP|Av2|u<aɜbuXtcQ^x?eg=,<o3羔'S?P|ۘWEg)JzV9`]vV|ϕֹr歹jk] hj=Βl+z<,K.g,cѹBڏE^Yg=$XkC0>5rT/]d(LE^8W,rųݡ)l%VZ]傥վ 񮰓MѮ5a/a-gT5>,+' X<+nU񲨗EE!os炥4oOI[SWE*WWYu>v0H:VZ.]UIAPF:zX%+K5a񜐟';$cQ,/$/Ϣ~h=KRqQQ*WY\Ϊ`+ٹR|a9vc'A۪==9++#yX2'.`1uiwd(7ֽh>KRlw%"/$/jY`oswYC!,5KLu>뭟Ug%@;겧تջ7CAzxX]ڜ0WyJ .X~.ۏE,, +k}'rg./qdڼҽ)U2 #F%ﴣwAжkJs'֔| eb,~P2兒.E!4xxI}5FTʣ2_OU'xV9A/[jr\s]A)<]0m_tR%w7hyeiu(?řϊhǮb%YwR:;Av2wim}*/J[٪7c-J@C3`+Ԯ5b{O/k)szt{ҹ6L_X:[sAW'jᬥU-gU,Ƴt<[AmXNV˪嬥 jIVϪ$L [AmօJ:Y-^w\|Փj 6_5|5rwCj;Y^ՋzV-W_2ȣ;qUϮ + hkՓjY^={jWVjIl>;AZ +czY#9kuVJel*AvhnƚYZS^[AmFpV k-SS*RYy 2A\dZ8*\әzrϪ 6SS0w/j^5SL5zzV%lAyX^zV-o-iFT SpUg5k \-j)9vLr\k<]+AkNƪ K2Z^YSjzyV`+ hjj齷zY#8+iV obV~S|^\[;=z +AXyad5iYV5^z_w_{k˧\s'9ąF}5!n,9{|K%sVµ>~ZŧJԾG?ɗ:MWG:ձ߫;7sΕs=֐ktS}.=*xW ]j]-E:mt]j]F]ե`w>\`'{ɻyɹso|ϵȴv.YՐS)R>>ޑRɣP'ZWw]]ՍQK_n]xoK޵#=v̾WyVTʨY>>\]j%y;iBݮuu]}0j=գBukߋ}e_+ɹs/֡mjYEK0gU}N}">u>|]yJ$gxѓ~iW?F=j}f49C/Zoy~kyyzUΪ|{S)ZqS+KɥY5qɡL/eWzSԛ٪5zxxoI{݋\%:kygKg T_Wjf.Z>}*r(y;iFo%רz}^Wּ^gwɹ[w̷o]HRr-!L}0{^橎j2*n[>}j#TɣܟED='دrRU|7Zl^Kʴ|ʳomJɩnF^s?fU|uK yԻTT^]ٹs|Qw-ۺVp-Wp.iy< U*ϪN}Wp+{|'V˕>'kן6['*zh幖2yڪgtՐWiJ=@U)Ro-fr7OejZoNYܺfՇ47Ur~oR٭ǑkyK+Z97Y5^U.Z̨ PϝQ\V-g^[(UiËŖO϶Jx!\w9|Y>UڟE0Xɫ4[(nYz%1D9UͩZ<#ϊv#n*K +B}gh.C^wDƙ!sOLJ8yBg3zfhū9ʣp 9UO]rno]{OvώҎN 9WͷZ]U|)Ҝz%|aL`71fW<|Wi^]=)RN}j3ʻ:P'9 x\W.hox9^;sIno|^Kʴ|NngOsmCbv;CnUGln}Uw4W*eUnP/0zdi5fW~3,8Rz}_z5s|K{)3}Tn"^wȳ:hena{ijv1|BsW{ucVUs*eT)T>9Iδn +w,fɼ&YW>BbגkE\wͷord?7s3Y>U, celf@ͮkչp+,VP^Yl6]~/|Jjd9It4ow#:k*>^ps29[Ec;Wu/\ͮyLkȳNUf&+dYJs/P3V\uWʟ +_lP-}]0yʥ̥Q١Nd0_#ׁK5/z_ן3s݋ZhZk .V3BZi,[Lz YNl`_f@X݊UVoc`7V|꨹{ԗw' 'PM>ƔewsK|ή;Z7Fٳ4ַ4=M~`JZ/f<#/yns!'y]tމSʥILSM\TϪTerseRO1XcH`LHzi{, ,fc}V}ENaJ=!z[yv+J:vSR٣?#M>gʯW1w] +g[;gLKg[=+7܌Dža-3~! `Ugt.P]*;Tv!%JYs0\-Ϸtg\VQg y=P/ k,?c2L2@g5ogO[f?"=k;js<ᚙ>.92SKej'}^]-Ur.Ϲ[O,zY٣?ֳJ='vΰe\  RajzJ^}: kjSԇtt!*li^3-i-W<ZuK_]YdNsڬG# xl}@N3/{JYj1يses-:Pxgöo sYwç/|d1;e]ft>0oYjUm_CjXju-/K,K~}/\<9[V9vZ|`<JJ*ϪrojתeZ;yݱ3eY7C^y,Nqv/fV|]'@f5tJW eU-N2Ƶ|^4yھ,~ L7#wÿ5̶[Pc?mV:؞9x΍|o>\z +>ʙV<%}>͟+;eozP oފs8̺[錠ϱ3SVOlQm{mA}t[oɫWj]kҮ}m>ee~Fa&Br,uZLsm_j|C\Nt?V +6,=:Z8_sY%7?6\%:[RϹo;3tz\pww;ndeY::_Xɪ9֫cwߏ5tsϟwOw0UiwY+kwYbf5K?NβtRlcítڊ[Hw33l`άLlJɳ/<շ dcX0[}y.snw>^)|O1Շ՝ZޭՒe錡 +jl[k=nXNu -rogڧ<9ϗ#vͅ!>mwYZ?ʬZ=jLYٶ 7X/{k! -bogyC|\˾3>V~>0?tfSr>o_Y1<Bű~z֫Oe= ˩xި0GV7A=|B]­Rk*߳oڣf-T{pɭ*7bW p*ͧq^ۜHIBo=Ź֎JpZSE9j±V˿~c0=]lg}u `0 ^Wki&.zbsc\Lj,i{V1^9P3ڈӱmϯsr#/W1Jc`p{$-hyOl{Dfu`0 6.b of+>jӸU)7/sUNЊZQ[$юܟ|1VǪy]n[cD[k!5L۸{`<_GJ;2 ZMI+`\s1fNNmpnUXQ^sUS jw DcSϷ78 +kQSOInPMѲVk}5ǚb{#z>?kwkX<`0 iDϮeZc;Ej buͫ\UKSm MNo|%z=+<#W,^)\J2cYcmr}Cͱb Z>׶s@%H CӬ jFհzv2Y'epiͫ6w +w>;PxNٓW˩^K]oH\BoV.+ıt_&xXnV'CXr`uB`h\hC {F4Wi: WC{+pת\"/>JWm ڔu %GCXcZoɚHaT[Jy{-u]2eQՎ׳|A<Ŋy6Tki0 xqs#ؿǭ* {?^>ԇsBt45ӉMaߕB-i<\kXXgُjWyxURz.xYϪ/VcTy1h,O+5p=ڛkw[`0n]P&߈ϕ R^-o{rrJ_V嫄W 3bO><<> lKOңU# +Wj-pm ߒ]&Oe!75u7W K|<Nh0 ~\ZϮ{ǵWNs+?g%X^5\xrkbgXBb'hnMZ~P"JrU16R߁RR>iiium{-h3ROh0 Z)uAboLsҳK`@̈́i9+c +749z>\b_bo~f14-JZ^BZvT8$OړQOr?ݕEGӊzǽ׿JOֻǴXQ'5 CM *UJ;sd8$O.wqC]N?g-w泎X7e .ʫ|_J&=}I ? ~1FFϬoOh0 MK.XĀ˦}Ba)UzYIJ)Rɹ WNuc\'q³$+lk9>=#7T)}^%N'7|EZdLbk9C?Z]=_"i5Rz +3~PsW>Oh9,`h|v셽$O]POiU*f {8tWG^hǻ8.tq\'ac9dzV28mv]ku.[_ũtx2dDTTQ_ +,:~?,φ屮 4 Cy52/DXb?ZsU ={jnFxi{UKWLN*QO]: +rsy~FH]7tkDoLsfY<,T*SK;=ŚPN(R+5q,`ܕִKT>gjVt=Þ8.}CH|ԕ.~"*;ũ.~/ۋv- T !sWY9+|U#gŬUeȟ6Kϗwb" Cƴ2 Ƒi}Q_*TAʥgítVǩՎ@_u9k]V> yDx|49@_uKO%6w={'1-~m{QԾ~]XtXP(Wj2_>>]HVS9]əg*guuU z\ȿ_݈FL8٬.Z`CUi*5;;dWU? Wu,=g7/`m/<,Oa_-̈́}).Úg vsWWt'w3 +hv+O]Wn% ON¿R4귢z.^r׉WXOsFSGR܁9W//wULQ>g):g5N1޿%Ww53꧗kq.ybݗ|\6XN(P;{A_O>۴ 6uWՄH״!rWK3f_洮lyԯ&r Yݎ.W%|Mo\cxګCugrWE~G8U*T@Y\cwԋXϤ':5W;~u,ϱ7Z ^?SJΕ13M.Yc9,`=C=Ul M{y+m~\Ii5.$r3:Y@*UtC_No,+L`pl5'_,N9:VxU^}_C+x9<Ԑ xBzɚeiV(Y'5<^,^ꁣYÙ\͚x]o.p{ Ϻ\׉^!^J%Jò9H宴~$*jctJ՞pc\_>c_!7.3|Kf$s4Ōᘺf`y1^祄CIf1x=%/3γUln>@4᫏߻-prM9TyR+1 ]a`0T'BBUV`(w%~ i>]e͊pɷZrNq4쯑z\U§>w9\#jop#-d@{ kG|՚ .@1o|kg-.N%gu~IIQNtXWR< Mx":q:\\B`0TYu =gPs =Q%d8Ω]ՓnQ«I%S4Ggpğa.åqW}@_'ug0pjc[k`1М^j}a:|]tioC{u$cN֧^ a5ʜ \93b:w`nO(>3#}Nګ}N+W@/z䍎Br&Cy"[}xw* ?gtoR{޸kw"ApN_5o]]_̱>=-ɕLn,B.V=,{eW{om6/oRj9\ToԼ u~Ϥ՟1rf?\xe?|/z.FNn/7?C6y'}Q<:Ba~oXڇA}[w/;@j&(v(Ä|Jyc0 u-#Q)J3]51QѳĬf[p+T&4Ӹ<c*qvg?znx;+i׹+=3ɛN}8üف⽠VW{C>VMQs/$/sWWͣ oK{s guҹ2 ΣkOގ>\[)g]P|Fuz3Q~(5uVyJǿh#W.Hv8fLmKg +1_Uǂ?[8Ձp'u{AV/sa'9\Os&{ ŵ8yfTKI&pjSr:r P|օ1]H}i0b~W]ɹΪ.8.#Dsu;Ƚ<~oŭCrXF!2ӹI'f鼠ֻmwmNp8Ήfdv-p-a)*YC Wz߿{ a^VyT +ծ}>Ƙl^XjP}_hBb7^B<\O .t0<|nE$DmO<tMV>_#|{RrgqpǹHiFx +߳al955Ax[8ەj&h(`>!guU^sTN\re)=ZwsW՘)" ܵya C>}]{-di[| Yxɤ@]L07)T']<DVc+GW{r-ya|a .Oy>W5"uXM~A@.5ye5B`Em/E4WI#|o ݢrtQ=_M?SrXkxW~JW󕇽̷YZ >KMoU<<.\[Wլg(S{aw Cc@)j!++VjOܕr,u5~/^/x#9a uȝ=܄WpМǼ5[\gy5ś#5#7irVrV{2/H/$(r+NYM=1g'.j%5uZ5 C5S|+oG-n v'ث#@)^Cy-51]qORZsb5XoI(\=`hL l,`9ffqXjv:wcU3+K~Bzl^@%:=yjXi>XFFiokUC~w~`M݋Si~;l)_0QACRk۟&zZ39J{ŴEՈ}ЯћO`04/7u߬˽hF)/K?R\/[/ Kꄢq;+t2'f&uh;Wz/37K2O'_q=\_GlNhK&4ru߇CL&L]jEȣy %yP4?ߗ)ZP}3u$Kz cofޗeX~NG$Osg|MҮjBͯ;Ѫ #O=^s7ؿJu;É]|p\Ô?Pr ͧ`0W{}F Vo)K׮s1WYoy~רWzpOhyg7B;e.u Ooۅ鳼}i*4W2#ze<=#kE{U>ސo=AWk>`vd$BګcawGfstP`(֑c9S9=uDm*$21;Ğ:7!xGy|p,ҾgɻLsw^ŗSU*ȓLGSsМ߁ W2 ~Wr/q&OW4TE߁M 4di!Me0 s{h! .ky2wOu];[zOQz>^zpΫKVlsiJmHޯ+hSS=|5=\.JxG֗;E|f:7#;5EmFR, V֬ZИ81=m2ijexeU^+~u#|~w=$|KE0O~n's(.7VQԙ~}WyRYSљBC;'}ۮ}(hp^ ]4,Șc>XPr_4 +-TiU4~ǯ>SsNj_|(}ouJɍׯTkPkv_ѓHz<+6}+5#9CiA}rrXЗw-_=yj-l>V c9*[p-HajfL.Z'G2oJ/-z5WP\y_ui$j%uMsj=J4Z^3`5_QO\^ kUG8{N~_gY辴(1¯bsk+`I-W^`02t #WZ{g[{c<)%hx'u*QQ:&?{<${Q[ћuaWF2qR=k.PrfR_Wڻ}ůΒ#E/`>XR#͘.4P_xWl՜Qj^Z<,k}Ln0 CLA HA4#S_UyC9iRաԛ'?%9fphk%{^g]f1^jWWػv q3:m,%z8_MZk +*Di#LА6Rk܇E4`0 My?y4Y>ѡU+~2"#X9p8i?8 8τyvL}j srX+X/azJX[!_TA~uTWEz{csգO`hHQ7ŷ]kg5=ٟduEr1R%Ime*7)h9S1aUWj*{./OP*~# 0 Z ;w3+m=mvHU-\*=+Ğ$8hƐ^AU95sk+W5Y-E`0JS_3gao{9gp6eBf-c-W*Yh !IpͦV<7$5?~pf{4mN`0TBﳻyju w"aΜ֑bS/est^“\}?wWo/qOk!Rή2 "sMۑ-N懌RsZ)~/y.EY^^q6yl8; `OCۼW#On}{S,2_!CG`0TvV &WI%؛w>ϯB8YqAp6~ͯi*p>]U%{P )"m?NfH[\8ep9t&c=nՅuؘ5iMΠK]YhT4Ef5~ڿ}oϿ)4Y~U`04E! ?5\jzC^)V .4VGj|`%TW7UU΂+Q)SWkg0 ]gvZ'mJN:,^/v n!V YuVGPk_UM_%׼E +}^B6#`0Wshs*QsHEGK?|ՙ|W~ǭYiRU2#WW,V0~W5"?5 Z̆+ůo9DkQ_Cͷ][:^V+N=U2UՀF*Tb*mN+`(/<gf4x(;mHtsZϾǭVpac'ה̻ qWDJ,*?:DžYiҽДPW¢Y5uOU+S- + +Z]u1~xf[}<_Kjj_ɬ_*W`04%_+ȯ;9vޣ@[¥ixՆL[ _5,+WX1"*pFuXcO-Efn5'ױN$o2~Uz+W_Ys_ CmRb9,XR+\sy7rp+ WW֍_ Cu1qJA"~V>_ꃦ2 ZAcзq=GWU? &W?C\KxV!{__}DEwзwmbug+g0  _HKxV,g|^w}~%Cgv_ͩ=q?KO/\ گDj߫!~5 +rИVssOj[܃W;Fӳ`_gՔCYEk\+Uk%_ C?~5dtCwagƖjFNQ[Ư_[b~T~PoD9V}zQtW2HyT`0:yjWSW- i`~UƯ_ůS՘ʿB4?|_sW:̲2~e0 Wk؁_%۳L0oЂ=3 W ϯ^y9UL<%9V_{I^.{pޓ!~<6~ֺïZ񜥞n0 M mͯ:;qngWs{8Ӎ~6~Jx'.p5/8?(_{.p\282I췽ٍkN_u+xUhfWV*'J{>~5ε l8ߧPM(z_<\J[Wq~~uJ2ULJN.tc9*W?OYOè)N~/ZpWu`PO;y.<ցmEKreK:5-jq 2X!MЛd@o_W*~5C]k^?hZrct9zsop~z:7rϠsX;AAޢ҃A5`0Ԣ~/p|GöoZ!79WW}TJU1L=W?qq)~ϸx n׹ ]lK?Ek[~? UkU=TjCcwrߑ^Rh;OP1+`."~5/jjWz6><$2jo{W=~G_]]Yqp!O߂ת_K@.j1k7'<6ٕ{,iWjY"3beߛP(K}(C C?[jW.N_}~uN~{{xg^<|WpL!~:WW< N?kS)$$ +Z`;U Mg(-y 'S&ڒLJpу琞;y^.~Ҷ'E`0 G]>6{5\<#>yFl!J=K=]$^.tq+\ʱ<DZ_O~ +v/x8 ;53v䜺Sk6ku6{䁦"_Y֬Z3C,TjN#1 Z!K bFq_ +՟.WV8p=S' =D?-'.8ř.rq\<ϱϱ~ű?)Ků^uУS4'9zhOmם9>pa+Lg Ko. stj!]aUnʼnWXjT~FP7 Z!M]4ݣT+>uWە_Wins3\bo^mKz^h/r[7vb\jx .t~Oh_Cu7]iE#^чdžr0&jC +rjޑ)ޓ0`0_>!jCWC*ȯ>$a"t9m2Oa%{Z/xGuw~ 01/xŇ.> Gw_t\dQT_r[Q7ZDD&9=8ǝ xK2-EN6n/u4v^ j)5CG˯zV_}wy\Q39#"c6fAnyydsF::%=_W#߅W:<; 2=@cU:ymVCx%k5%vfʔZW{եZE CcҸV}Jr<'^9M{0Ugy4W:(¯^-x>~6]yoʱz)q.eIi{FP{gAyfx:r~¯rAx7= BcIrM5_WMML8$_MU/$O!y,'5äޖpхkrc](6 +]eE!IH#!=$I#BB Tt 4M@Ji"P${/*}sz眙37gs=ޙ +WXa&[fkl퐭ogk~ִlMl VluVljZ%[_ײRVr\.W5JnFVꐭ3[5<[dklּlm;maꗭUXܞqVֺ2*[S57[fl,[elݑt^ֻ$[_dl;[1?y>z9[Ofl![dlekl-xclu:Zk]6㺛vlmk,\ |~+us֣z.[g}ߗ_Y<lP֍ٺ4[gfl$[?9_!_ 1[d!:<[Gf(yX~g:dsؾMyܪ?+1\.q}lG'pDl 8ĖT%yy:#|_ h\ _`&U~ۏ3[elCxchL5̯Yg(j^uDNօmn-֋z+[dl=[_B||/[eY۟uK֯uZc v]+plvi#щXǃm98 q[x_,lq5kA7[3-1r O~> rg,Wm?1H/fZ<G~}to{>>z!X`* OW)q~ +F{D8[W]OSs<()|9xP|x?o-: X?':+{LM~7.8[~Gz}?\.ײgv?3sy| uZ{W: \\kY_-F*WXx![}ٺ5[9x܃[j#uzj,n6-w[ ٺ$[7w?7*C|/]Vlr|]~.qM8goٴb77X_O>|Lxh]K>3Nv+媭Bm=3>S&X# iaZ ׹ṣ̌unjJ9M>X_ zds6hQ VT?p>@KUb] Q<WĵWK݂\ҿ, +B[a= Ov"t00ďC _w'΅gu <;p÷lxW)ⱲNHz|2ipcx^K{ {$݁ZuE-Sw7RuCо`Y\.>4~[l6>@Ι`2r:}? dz"f+c*d3_*Su"|_Nv>q9(V/[}`2WU*aC[Icpgs^䶮E8׉wq}ET_z?\.K+_ +#p!Z>W!d ~k7KL𢡊+fCg7>pe]fs\炧.Wu%c1z> g xYdowS,We <`wo^s{ֳ୥`a݁ +NjxW L{]QA>mѶ?J k z_j95%c;3R#^רnzKSWRj=VdQ űcR^B~l|b{1XZxSWw-1nw; g7 ÷p EJ15c\8~;x^ZxV/wu}[ԫ%X+Ei0gbz_|DΝ5sj=k6vӅj3, };x"㿢ئl>P}Ɂz=?>J٪ +1WJnx\νY WH 5weXLL?r\-"o-P7" =^:LcFM dڏ4sWbd6_U*X_83'ѕ2՗%ǎ-wq2V=gA7S^OKxWv^kS"Vt\.W[:hz5L²|]jJ_?k7٫*}8d:(};]_*U}y},o7P挹7M>4s5X@ߐ2U'C/S|reսb"ԫ5<8/ȵ\uyp v9/bg)kB ڞOrӀ/uP>˲8ߡG2>fStjeF<}3<|ȄH*]j'Cl].kʹoVjY;vPm[dlS"'~d`ê `s~--8e{߫֨.bk]՞ qF^r5xXj?ZcK lȷlknO< =@gb0XE^VS8LT0}SΏ@Oجk.&fY7Է"[m`cL_+*oM0wwd~.UbMU}`{{Az319.;_)˔[=ҟEQDwI؃̊Q{xBsonނKedxB6zբ.-3hn.U'].k>7{ YP]V׆ `yꌜ3Ыa,X^Ȫj_+V?|CwFwT +_.p𛖂^Pzr1/<+38ނF{FJ)*3Xg^Ȼ~>kr^ zXaqOr[b/[gq|Ycu5wy5QҹeX֪Vgg,E,ks0s)ʹ]0[W?vW\!' IJzckZcuRƻ~r\j(arXv_7lZ cvQƢl5Xy,һ,Rq6%{b+=nO] +9sCFR/ExQSG]XZ,?j1ha*Sgωի{ݻr\arXvo@z]zXEc +Ƹlq§ +eBlI] ʏLI@3bpb0GOc==,KYwLn{kꄵ6#-cp,"t jYg˺;Jg1i`ƌeЬ3λ"Ve͝܏q{ #sGvk ?/RdoG 8OMDH2S#kb~A[9c'8\.=ݹ׎ܱ>okƢёL-җe,:X5?ÚV`^xjY':L!,Ygis^b^u; bm8NQߚ->L^7~Oe:f] +0`_厁gXnG<k*o ~NCsUS %|POTz3r\ˆG^~>ևuB VX"sqVS'9〜E?I_gpnZ\7g3]D߹;8yyg +xy&o\v2['a/qg: w,n{wgf#Hs_ZB3pBuA1)jO؞H\.kT}Q'jE:al?as2XW2Yoqow6H!zRꞇa-۟S\>8F$ҿS9g"x xg M%W4n)UnÙg=,;k^a5u몱=ZzrZòj@HN顾bŮZa^y-ɞu6B^?g=do#&= W{r} +C/g^t}@{#luh'%73U-z~g*Ģ cJ3].U[r&B=Gmֽ賺'Խ`:35K3[LV/G/k+t;-AVᬳQ7} ~ֹ̐y aRɾ/FN{,;54 0<1 +#ꆻvc&}4Bz/ì՚i`e>o\.Uԛ:ԯaTbbnP"RQ=pqMd.=WC8p9ѯOYk '<t>z 9v)xsgnƾ:lCp$Mw>9t\0>Tu皋3]PSϊ@l֪5f8_rQTK5wr x#W'z(j5z6ZbkK->+T/^sY# ܋?Gje䬃g }5Z:?>Cl=Z ++g'%=#=<7td3dvK^w38HpNSQ⚊HY{D<+Z3Z3FԺCSvۜ5PrgR'~XkʻһXZ,1[rܩ՝ֺ!F=sܥ/PLzxV{ܙV3^wctu? 4ܑH|}[p` zV9j^/ݱ_I jvΎvr\m/^T'LeOt-#'zi ó^VW7pkEG{~}{G#g~*jw +[ס/MߊVQou#z_af mOxkdwvx_uӵycK6~ Cܮp:UKUhͳdtf#Z6X.j=驨{;}PО–`X0Z|$ƣ&na-Z?d.B+P ؋*_Y`Gp3oǢypkM9k6Y]p=p>zzd.Rw$Wa<+t׊1 Axrvs*seІUк\.kU*['oc,{+r)lNY/k55X3=v#q^͔kZ?@O:y +L`/_kAM?yg\~޹VIpVGN897p'L\RU~hjq`gJ rkOczWw6ZesY!c7G-h{v|[G`:u,xpdݑ[ BC<' yh'tc\Ztf7: s5흪.vԘkCPyP+wl꺧֌U*YKPpXxHd-Z:;ln~yഝp_s"SRRZt.LqWҳgJZJb<瘀Zlkt\.WgC Di( 2wܹQjq=P&K<-%žq]<Rg 5gg_EJ{;a?v_ +qk<TZUlFΡ{>?k{1 5~Aj{*'?| +XX6.Xks~ͽvXCޚ(35}\\}6>3scי~9zB" W5thW־>W̕ӭ'r\eNh,WŲX1b߆2.]Y2\Rxѩ8nhdpd+ɸ7pk z}/VE]zZVyϮyΫlQ \.WPoAlޭbپX;?o +ej6E_ 6[C z$\#qo̝Y3a2+t-m [M)`X{Tg\.m+*o2+ZWi,cb6]S8+ mu57'>`* vP*yKy[zNnB e֡[:OQA\.WVZeX]S{loc=oU}RQ, -[q]Ld/]dsgS{7\1TK0UfUJ3Oc+Xo$OC߯r\2YX?Ʊҏ1J1nyu!&ޖ#s-_\QzNrUkZ,[ioNf`YZ+뗫U\\.+uJ +By۰0XCL,;%4_%aF^ů!V' JIx{*ꓻr&˻wy#I1_w=~22ey+S*?>z[5TE\,^W,cg {- RP +ʳs^rb״ +vO!+u Sb0n`n=Ȼ:oK-) V"{VTl߲*Y+dz Sl]~ ensQ\\.j),clyc@ЌznʰVb}TkzVZ9^dz rRˮRlUs%p\.^,ci=5MƚPxMƦ/9k3SnZoAٰ^*Zey*gjJs[,VGHYNe,1FduߐOK뇱bo5sŞWRL^kz’uh0@ n٪Fr]kxhf< +͕lM)e RҺ@oE=-e5 kb,sz*zUZ nfn}#ƸJWISdb9vg+r-ʽ3tFxY6B:ÏaN+kbU\U*?8JY*Sڟ2sUUU:2I,͡88hJϹV.ZsL S6DiQg`>k(dKO1Zki^ewY+ "K?T֫b pXv~^Q 2Wt-PԬ_p\.X6NIsг5"bpKfomYyr\mYU,헥5C;#xDAm*ZR̈́1ep0<_W K-ގ7500<6})eOԣ&6[3!b-rUlog].eٚ!Y-}/'^Vdo75GkLLʟgjvm.C~) EOj6<:vUi Ü {0r՚gr\.WB5gٺadׁ-5oXֲ̡a˲ F#Y='PxlUyJY'^DlֵHP-\.t/+ϰ8#}2w{`b +eSEQK-=I)cϻȟ eTSp.Wz_*\.ռ5"b> G/f5cLkOyT=h)Ę,b[;mb_0X2/;Uok.rN6+Y밞dz5P+kżV_V➼yWKs(&GjΥܯ> \r\.W}1嬐žkINORo+Ϡ(7^VW`Gl أ#L\zU> :U.rgr`=-gN+Z:/=9c8YO}bU +b_y%3H)SiK«b~@U/r\,ڡzZiYwwlLSTW#V#N`?}PC~EY +0պ%**rMJec,цk< ?\/]{}ggYg~ZTؼjzTS1%*X\.z +]CU:VWmq~2g'snF\v_hVMa0]Z$N ]0Z)n8SgJ)ʹr\ebVXCd^ޖ["sZff Y2y:SrRq O)OGվOܯr\.k"=kx\=*E'5P6>WFk=~x K0,z%ɨǘȫ̺r\mV.`5!\ wu3Ead2o pTKu(S)ʽ*r.gUe-[er닖`0RKo'B,ծ$O5|r\mbV,ar. +-{;~?C,Ki,O9S\.ZUx2./.Xj})?,GYj OOr\.*k-ewKrXեC8JYȕٍnܑkϫ"DqVjNYOJM*=Ot"M'E<-8%E#F3򩜋iȟsl#?dϯkA9uI筊}s>s{ȹ}ɟ{\-sx7Y0j22=KW9s硽Ţ{M+^c'"Ϭ;g Xj:ByE+'M9uFEKղkQNMQ΍1Y-g5ʘ]7E.3ھ[}tar-qs漇[>U^~0dW;]wuU8ZLͻ&r\dOjx9*粆cT;A. eQ9\4/w~oZ`njhhlK򫴞ݦq>Gi6s(5Gd$<&ڵھ+EwfWljam]jD#>6m0tXhz$ּbgG)tȧ4G#NtK쯢3m[c){14_f-k}C9K|[r9H[OyiCk&ڙ*]g1w >r=C-ͫ~*]CCDAټ6svѭϕ[quĦ +򪷉R-2Z"2-=j\ts+\`qek#S|DuEn2*V~USbh㱮9Fg[Bb:38D$ޯc5n APoJ̩FYw]r;Z:g'z9lM$XWr5Op~hCbEg޵.LP:08S! ma>kq~,|ĵue۳[_ Csb6ޓmkuBbմhdG۝1w ns}͏pسC#s"᜺jfWlbwT3O416fw|=kgfATr ڽjZ>/9A |w{{5Zьl +V󟑱mvɋ>%Z" }I9DžH-\\_=e ngwk6rʵ0HLx΃e|"*{ϕmե oJhxlM|;c}yбޕrJg#V8Wsm/;{p1pe55^#[z,׵"a[32*`rY啵X)6&}Eɦ?-j?)}DowUe~ub6خq5=Zwlk=2]Ľ ŀDĀ\xKͯ^iRnEOEշhTN;CGV1ag벱[v}v?֠>*vuU @W}Qѿ\{}^N=*{\ewd3K`OFj?cuk6b~]'.Ŀ+Dxi_ukcߋYN_FkP"vSm^qnhM$t{vu7?g|[mNWg; 5G[c~xA[T[w$Lg#aVE>=w6f sUɍ^Tڝ6 ΐ 6M>T +h-QvN<̜kvn;qՆh=HOo 9˪{4[>ǟ_]^=dND{DsrisUfgY{w(uʺk2ﶎsulqwͭ,w!r^d~^B&9(n52n3b?%!v&pPUW}s 7/VW4W޾@ƾ]?bucj9*ؚtnU3'3yפ|A}z|#}}0b.@Zֵo8N + gͯZ.u\󏿯"hU`.^e17 =6/[x O']?xUO"NzgO^E{m xfOOˁOhd>8Gw4)RVS?>_gVO=ݬdw \Cs1۫;ίҳ5f sV mܝ);d2_"Nto }F_u^ݽw8_{ |Kݗs0z_m/K ԻU0{Yq3@qMtPY*gӻμ&C+qp_?=qv`xWNsm{MwSS~Ӈus- @~yO"vodm}!{]9$26ښ=é@~bپ#{-kPg/mwmкAo)zhCۿh"{^Y_4!W nw هkp5Z#eڿykfu]$A҆Js].cM6|;S_p?T4{X~WH^`qpTd۽h)okۈ.vz[x+o6wۗ ȯZEFlޒ~+؞#<]D7<`{_kk-J4DȯZV;%<ъ +FEkG?%%序AMN.^(\d!bUwFA5hx,vuOq]^7j? 6WԿ~~Rw<՜w֟KkJ?U1CCpwPVM_ulVN5ĽOo~LJ[~{ #gS_ILў5u7Wzr$jw$]Lc 9#,+CȕS_rzFAes(w[>9.7<3bS~}մ!b{5H&wm|(ٯ>"12nC~U Wݍ?EmO~ȼNNov,q+E+b,?>L{ +V'-Hρovfnށ<毪ؔJ&sU}nmȽVL_[q޳rG~UW⎜ "5'ה>{aVOsZ5 ]Kښdثʶ{_UUwcpynO +&\~uȝߍ31o7Uu_u_؎ + =99WElD3ɯ1H~w~uLJo>Tu;ɯ*1H~8 ȯt$ D~U^->x/ _sί{+ޟA ȃ_zUWUwc+5*eȯ*1H~w}cTU/!"+!W يWUW lG+(r'9ݏkȯ*jgw^nNvpk_UOvOvt"8w+Dc ي:ȯ.Ϣkw5YϾEz![_UUwյE7[D~{=M;_UǙoq\4!]VHzș ى:_=s/T2Utpg +|v57D~U5jUf9z1P~}~@/=۳M~۝lw擗_qGΛvb:ȯwYm>S}g#1 5ml@~ՠʍ]Mb}׶Aݑro XlH~ƽjΕDuUvq~y's_5)h;.Fd~ںnWn+'}fmg}0ʏe΢_9dǸkyݴʺ͸{hv]wUm|~Q.V3Ծ +LDmQ1=JtnVu?2ͯ5m->whǷmq"vlڊ;GG\x +7d[k2i'Q .h—~>WWnlakO,3ĵ}tveh/GHՃSN*-E3ϼAVc۵ݹ}rۤ~6|Qޮ> +7 }0A9ſ.Em9Aɶ혏c!0_w3WW7 ڛG>M:7DphD}h3u񢹁?/֏[ɯίjf{b\þBח+"}@q~z_{'XT%wD+gƊjk1^6MޫpoMLmkuo:mE9u."] =`??#rl + ]EL 0E@!jD2:(*Ҁ FDifQ "(aaPa $aJ_JQ{o޻TڵwOOπ8w$z6Wr+-2Gtv==Wk]V3ݔX0Xnjz-z?aZ6Wb+ӦGy|3Ds+.U FdB{3qMWG{LjN ,^i~d|f2_M2ygMCLnچbW?msYa1 q0X<Xm*1&yNYgU v:~ҧS<٪|<h-AKKx<뿂iEW=5:<gJ+FKKw;eVtE/Ls $g.* HU7;L|>[J=629qU5^ +xc S7*_k]+XW+j#*i:z4z.cwZ؁`~-3_~ft|'tfxjP + XSSűeysqPkDAcn:ܧs"X~7 gǗ]̃:7drY{5878UAѻ|PAIꍖ>aIU=u :ShN{iQ]+t^!@] O>>& \cc>?zrr$y3e,P;Dх:EĘ60]fztj~GpY?s~NL)kt3{{nՄ]6p?A^lcmrRq @I@Ű^o>%5\^k~H[:-!Ὓ-1l!a0ml9u-aSSuXq"{7>ї/?DlMh_ SVmUc5+8. s9A}sf{vmbsn^\ +[D&]l~49c/jN,3D1IW`zBc=Oo'z@y^]W9Xvoc*q\"_޻u͈U;#͕lj3齸:ss ![|,@`;gz$a;6r9D?gէEd^UKsl̅`L;*kfn;.U]jm6$ LRV: +5;Os0n;>YзnIަkM3jok*14Ws?|rN'ۗwŊ8~h-43aKbWڒWPoܜ౶$Ut"ճZtlz<1wt<@߈l l[mJ~8զw{q%WX\b4*~͸^'V nhQgv!T'lgz 5it_'5SuN#JAkskZf_v*\Kdgcy⑦lSOi[-Wt8jU, 7#c[#`s?gmLVS:]q[=񘻙h[*0-Ӂeh8XNilMoZYu +Fo㣲)ߴ/֋Gd:wAjbjwv۬5L7&U.8|,hѺXڿNq~_N]ncy]lrޡӜ5zc\g Cl{T/(+U \Ek/oգ:ƷeW%7eƇ>@ף_SйDfm#+yyA +^ƾEdOrm~^`4m?^BfZ 7=so(WlG椂uD_s +ruUuίLE?!@FDkgD_TYk .)A<`}\g);^N4 c@Dvw{I~}aQ뫲 ` z8Lc5{WuE۹LS k9YP*tOu}F31QJ}r4m~Db[-}"3}Bg{=a*-OL[ +mZLV+Agúh :엫.hOއa8iæ6ľv}OWdHizsd1}!ύY{ 0U(ߣ 8{Xfwg)gΚcu(<}M?Uݴy 7ztiTlDsUUךL&&Ύ&G"irP!F_A?fz)QS6Y2,bN1 {Z{h85VGf~@{([1Mͮa.683ܯڰ{2=]cR[#W=@+|S5WxSWMZL\RS >3 j:Wϊ9@aM7:V_`N}?u] +],eڤs +E>V4N_G9A^:˶iPmL:c~kn9̄T*y(~zL7p,T\kr5bZ_i(}kly:|BPg66W5ηC|t=xwo+݋̼p/.B gN7{Hm= =?r_kL4}N4+mu2wV{}:C}hiN/?CUux'ZՔcLa7L/N~X׉:bh[Afs7IţN6MT;o7MqFSNU-S*>@Ś6T-ky Xy\o(FG<3u^vӯz^<;I#}GTjQoڵjb|/f&@#f;]'e|VT3T~g_X$pdXۻTK?k\ROsꏎRËun|-{żnS~_M5)+h\^1:7wڟ=-wu"ӑU~hz|=\M_5jO9NoE޹YtUsǚ1 =&O@&~W\+a՜Z +ur]GR<^?>Z;{u&K w: +àS_U *! +1 Om"& +ݱI`&mRd,` dL  +@F19^x}Kf3Ns].VI[ ::$muQt2+mEG퓶zWo&jծz@GQۤ>Ϋt um66I[]Vo(:1髇] ϨgUSm X%m2U m6vI[-t 5}j_ 訛\Vo(:աz@GQ뤭mEG'mT 訋勞GY +kuq{[9]a, ++QPBA22"#E0(JI0+CKYam{}~Gz~u7G5ǡC;sLڒSGLzV{sۺLZ=XWL^Zm(Gݻ&JGU&JG}hVLzrn_W0Q:E[m0MjcnWW0Q:E[ȝܽ `t}E_н `tˊZ{DmuW.Q/mTùuIC{wo(ڽ `CE_}{D7msV6Q?/ݻ&JGh˻LvVέto& uRnWto(ueVm0P}mYվSL]ouo(/0Q:E[{D3rkE_{D騯mܺmӤ-&JG}h0Mj}ngWWuo(ٽ `t/^.Q/)jqn6Q7muo.Q}m>S#IC[W0Q:E[=;{4iE_}{DWmx~6Q?.][+&JG}hVLږOW0Q:ꊢ;{4i ?}m.*꿹uo(uOW0Q:Ꜣto(uSV0Q:﬎m>W՟sLzbE_]ѽ `t{ZIIC0Q:5E[۽ `tO{Dmxu6Q_/J6iP'+=&JG}h0M Gl6Qo+@&JG[Սݻ&JG[&JG}hwLzF`Wo0Q:ꪢvwo& %˻Lh㻷LZkLz]Vk3LiWt(wo(u]VVL:1KLQmӤ6v}ue6Qm?wR6i_}u}6Qm8{D騛{Ds}m.ܺmӤ-C&Z:hvof/pWWwo(uAV;ӻL諛wL:hy&JG]_}ݻ&JG|G{DOm+{4i͹E_0Q:ҢN0Mj%wWuo(u~VLh;wLzvn&JG]SՃmӤ-m./jOnK6iPs;b6Qo)`?[Nc +ۋuagtDL$2KJ"0 $ ER`dP $aaEIdeyh s5,w<Շ={࿜>}ް@{QW%mո֞z5i/m Pjs㺓kOR_H׸hV$}-JoW&mu|_},wǏ:nqhV'}-JoW$mW]hVqےZiKܤM'@>N [꒤WW뒶.cOm5!pWl PzZahV~Mq[ޮJPܩhVI_`KլWȖח(ճI[m'@:9@W m Pzz(i?N'@#(]ݘUo9(gI_jKR[MKڪ1ÞZ&[#I_aOR_-OjWP{nq%}Ԗ{:7Ξ۪%5(]Nڪq=J}VVV%}u=J}:i к[ޮ$mG܉hVCv&}cK-I[;Ӟ2Wl PjI[5W/'m-JmuF՞Iڪ{no5[ޮ$m~X{nh-JoWsꋛbOR_۶(Ԥ P5I[mu[;|[ޮ&m7n=Zи]I_-%@M'@6%}ޖUc=J}!im PjzɞZՎhV%}ؖI[cOm5U(]]U_d{ä6V%m՘eOR_Mjs\Ǟj|ܡ%@걤 '@;e(]Kp{nNܷI_%@jfVK PדĖw,髹(ʤuu[۟"[ޮ%mh{nI_=cK I[5s?מ4l Pj˒j̴'@^L[mN{ꉤ~;jxܞ%@Ꞥō'@mNj-JoWW%mոОz3il Pj֞ZOq]hVc$}u-JoW7ʞjpI_=eKI[ƝeOR_}[WW/%m-Jm51hWWO&m+n=ZՈI_=bKƝbOmՉ>m Pz:i(;I_kKR[Mh=J}\V?uu['l Pzz8iFu[ ۙU-_ 9 +ݿKqa"!P  $ASCPKCSC48DP"BBe/4Yghyﻼ F~Ɲ)]Wm Pj I[5WOzkKR[K=J}u?iOq к}M-JoWwԞ۪+n-y[ޮƓڏ;mOR_L-Jm5U=J}(iոN{ncq;I_Mv5f\=ZUoFWl Pznܠ=J}բ-Jm5U=J}ղ-Jm55{AVq=hVq[I_Mv5v{nI_vu9i_q(뤯VgjW I[}'@:= [ޮfgOm9Y[ޮ&|ghOm>[ޮƒj'@&mƖ7bOR_%ms=ZՑl PzNj+n۪'n=髇(]]Mڪ=J}Ւ-Jmu.iƨ=J}Պ-Jm5-{nVqhVqI_v5N1{nθդvu1iƈ=J}|{௜n3ka_"Z'=֜{BL;GjW)+7[:릥Ywpbװ c 353͖ +[o0zWdI]nzѴ86,vWѼQWtnͺyLLGF"dz/g^fzE9YwLטOx\5XPx:3V Alj?@P)Ce_df5OL6mka3jO ˩i2y2GWj>ոN4/# cmh:C$ gn19=X޻]2]E1RֻҴϪyZ\MH54w1 ~fj6cPdy>XZMզy+=4U}V?ƙ։* U,#g?@X.ci30< @.3CӍ4+߲3 KI:~G3TN].֧yUɦLfj~a;;Xmj&f4_ܬw?4;Un󛘰/Ȼe54tyٚ&i@gNQѬ{ fo>a*w)y,7uѴjaGX*< pX_0|t0M(W+رkYqvi"o٠ g@gvܩLXbO屆dO^"{<(K1V3nL{|@gKkU߷uUt^{yvAj>R7ʙyj83bkݙVaMk}L.7X;~!%>V"@wjlY|scn2{;VPl捆jLӓN>/XsL5>o0ݮϡx,Z1\mk_'t6Ӫ@I$tpMۨ +c7Mk>/l?HN̨ڢ2U|NyyMw@ٽsZ;бH%f~^l,S;kooH3Y՚쪚 .k-Ѿ!AtНy}нYcڍ3X2VNSJs^(~; U8O37@gY3 +2ǚL [jAWhh]is̙ LR!W,ڳyie+4t eAz; |jO({I]|Ms=RV>hz{;M]h|~6)^jl.wp55rrV\y+Kvi5SfރZ_OhQq\0kmbc􆲭|[[ŵBYZ/w[z_Yp#@-^xLYiV/Coi3˄r>TY[2Y}L/zfUrΡ3tHX.kZ"<٣og`;^̟ ի&ozyv*|W|֣-}iWg;r0e۬.z^swUT/H0ZrgkYs6z}{/SU%߼sru]e^yoHFZcWs:Rs/O@3ݓuwn73?g_\3&y:xv[C3͚zoUsPe2J3YAG};ӉJfóXd+!+gu]ܮo8vr5DJTGX3|T<)wefov<]}Gg]7cZq໋X٢sfT9M+#ljOivصs;ˆXdfj:}u//Trj|\_ާs=߳%ϛ[Kk}šzV12]?{h􀂳એa1к+PrMg}G,T*vN\͚m +挮Qnʩn=Xe\o~v~4̪sq.i-7 zO3ZnWQٹ JfnLUIkR0$aL౲>9_+ Tĥ\T%>)9N?zkz1gj#gvfgUrsMsn<8e;g + ysrS]wMWZVRqvMW^~la&_˭ fBZ1 0J|׻CCM_1'; +}z,{n<?zXVhͷ=fQy?P7ߪtٶg_*ٹ,eewpTӷ}.SoM}E AݳtcӼ,XZokML9_tr|Z3)٢O&$ Aw ]on)uV+ޣ;7CzR\fwt.M{skZ}T|T 2סй6iȸ{vYUԷRhX]ՔG}X 洜27kvsQT)eλnʂ|seo W-R{qlNY̢\3t~RJ OhOyMZ?XW=~SަL}٣Tr '-Nج#w  `P3(K#@/%4UWjZGO?*{۴z7_A-wh޻!z`.jen~i-͎T-or]Us[Z| eN^+AP'ﱦ}kKSz@5s<ʡL=Ie}zi'X͔m=>;(WjK̈́o!ﴻ˿MЎIwL3i( +kEʍ=[\v*὏;=J^ՑnLҧF%Zڛv=yos.Ss]U~煚 M.|KҝO[*|>AejXh=î}A=(e\R1sZ"yTks^e8%n3)zV( ~9wfJ$1gьRnRg^|V^Ro/>\9OjFfsvc'_Ӭ 88td޵\;]興?\+3#4S~vܯ܅%Wݺoܾa/v4[C3Z +G<|{WkA\YuqT7H +]rg.jS'߽/T z`%fby | t soi\iMAA濚u-xsO}W=Y5?$^ @"gge27RwFCJcwI_WoKy#yiW^g~Zi^Ne9SI,VeKpc.f[W6ۧn )yMtbNWgM8_+J`gv&>tT׎6Lw,/6CJ}=W)\Բ +AGifk_?Ks/Gӟ53;y3]mK1TDf37{͉-swZΗj7G4C>jCifpuLh(B} ]?2Írwv6maڬY/W LyRsjjhgزǙ@Muإ>b|fFUGyW.6Qok!/F5XaWuKR>ַ9j=?Uܖfɓ4֠XIw2Fo_~XJk.͞+r--'9 Jon_zB̲*Yիi|nWW'*K|m(߫8Tu.[ti;eDjZz^_@#^c~zvuvC[@g#2+{pO/IHm^CԪx+׷tE>`x=}n͈_@gآ^=KKVqRt!V@Zr;q2 +}sxJ~m󈷢nڕ UX*Ufo}3 +΂'몇?1`j㙊Re]eZ?w:>_ |ݺ/K⭚4u|[C< +Se]5Ԯ6]`Y iy[L{kN]h HN{FCMGQOܚUvDHSX,}'4t*g*%o5tjba=7͍yrW5kԭo9PK*WnΓSMe [@'Wu=ALR韦 L۩.U~x6kp}eϨޠsmt SLU>3tiH_Vex,7s43sUhcb7ٓ۞_5n~f4 ={P{zv4czSZ M/k>bg]qOjc GinUL Ls[Z~goV$毺ky_5KyX5c`j:ӴiP쵨[@En.}XɦkLkva}GkmnVLN)siΦMG&hom%Fdo3P#?* +; +@Dbaaao ށy|-RLuoy_MOVQme\|r*eX~nou_VoUmj?{ikm޹~Й+oj! +TՖ}>Ճ9g "*9JH9sJY1I}>qc]k[U^{UgW$I$I$I$I$I$I$I$I.*$_Xm>xI$Irʵ.W!ǰw7I$Ir=Jy*^%bO\y_k|]}ߒ$I$ɕ˷?Wa\K=myWz+Y7s.WkwW'W-}߯=ZI$IruruO.ɕ^׬<\)j\彾Wc?]WYV|$I$I,ݔ/ZYQ{HyeRyhw)}_ֵIs뛷/Kq\}?p_i]|\|c%I$٭.K_fz¸0a-{7ǻtpԿ\s{9twծGݕ16o5Z3_2KyZ/q=T:;mA?ѹis׼o4vaEf-7w]]ƾtch,~yt>qh݋1V$I\R[ttŨx#{ϣvxѽw?{4Q~VNy:+W"qm5Ӻ=xyhоXa<^ +0*caޫi>r_V*wx뛷/ņw<6LtW9.eQ\t$In2$эOFx0F5Fr>{oUSJV4=vJ蠨K1r/vZӥ8Gm;ފk[[_߸fZ-yrw fqr֊vx}T:?]52\u^[>Ʊ6bJ̧5ig}}9t>YaF{'qie}jsurE~cJXI$%/*w@7Uz1ӓ +¨S/F]F2^_=gEz<."ӓzǃo 6U=)=(]Jq\΃9gkG:61˝o(ß[y̿sˮuhP/㵵xouU)\8g+vܟ'>y{k}k5Vfߝct"/cr~^ wN߷_*2/#c%I$(y+n%ݭ~w#_-FѤ0Fh^۸0rݺ耨>FЗE5бwpB2b|'xֵ +6&9k2fC1I~b#)M-K̥ ׺ea.6 kk8F-%{Vzmt9v}֩Rkܘ5hjV k>ԞfAy|9,Q]꺺AhKp3oZj}nqV,P$mVvU8^mtw+EGsYѓڶ)zc?g=ΞlC69gXY0]\q[ےk՘F> +&l}K]~>h_5߹OkKca]Ssxu=r]O$I$ד]{ +p=JB4Bw:ӏ} +a ,11k10p>]F2^Ag6FIoeLu Ѓ5сLg+W1R +ql/kLםukk;L6g]ک"Ƽ/0Ps.mƵf^\ƈk{s8V3]/676pJXb;8o[^5}5ͦ8K}׬S}P.K F?cu.8̧6$^XfJٰ$Iz+]/x6 m~W~=0˜\S +cjaL_#{~ +&853oDMzz](X~زeևU;qܭX̆|I$oPqWwO+qmsOou06R[mdsk7X[9Rt\al1Μ^G' Onx-0bTI1xٙ9F3&nqEv{[7^7MA-B'a6njZe%=J*wLYU5u#z3/ھ0^Xc9vo85EsRrV`\샙e׺"[5ddjd1z/ecTݻL\Y7 +MnM+χ&X6\~T{n$I\RoV@ϡ Z[;~Zo}۽08F}<0v.޳mgWMo ><h1HERLǠD zs Ǚ^T6QpA_fgQR?>-1֣?w^{Yl-笩Mth)fiUr,G EOb/6pM}{yyX1t⃔Nً:'T[X]X_Exk<εW ZX<xm\g{_ +Ha-cc={0=9o_Nitv_-k,3Z0A&c\ҙMz$#N#UTqE+sQ[6s|Ϸ-IZ|Xrj't>0xPj'eǵ8 6 +Zg;~s'aa|\Ƨad0>(w99pn潎,`f}?lEnωqO$I\|7XՃE_F' GW,AG }~?~6d؇ jZR$Yl 9D:~V0īVS9]ye$_ltlX}^V`8\8t.[ПZۏmU8sEfYfin5,M-NaABqipygOsCsW\,[[ݡZeOa_'vwk5[|/69>jW˷Tc#;l]4-F_qM5<כ 1QոcK#ƫk3b&s~6[ޜam;υ=K MS߉0uKq튿JI$ |{:[:&񛿑S蓏+"'Pop PBTF[Q\"t6(o6ck(1WmB_= ?(me&M!zKkr6h.jFXIbgb5t"yh=zG#1x_p]%^2ېsr 1>- kw O 08Ϙlk-A fmB<3)0I$׻⫛_U#):k0آdc"O> #ZSi6yv)N|+rrNXzi&:eu?Hc܀?e7(^T r^;*#b /bg(ŭg,Zr:,nc9`ʇ:XGcW9nqAKf3S,7|&9q]Wyg_N51Tև.ϟ< 7ʆbXs҃<<?WW__p?d_[-Zvv +jԘ}XM&Izb|嵯%7:9c!/ :|ҋ Zezؓ]_W}yիgzi6|bŕ~bzyAf75yL ~/:N{ 1SGG|&j P=c^`|vㄔG0jN s.=iU ^`<7\=h'<}MS+fye=|ه>̜>wxv7Q2}0βZs,9\6omW2ϳj٭&6-{zmԃ0I$׋WGls748`g>9JdQjDM =6N=w0M3֣uA>h +57RϕtaJq]J6f__m8o b>4zŮYTR5T;X̎V7,k1ₖ`y ǗcZ-KYj|e.bwʰa)s4X?imfwE=d5CcZʓT ꨶkh1U{90Hl^]ubOq~Ch{Atᐓs9z s+Y^g96AՇڊ]oST}';k ݇J&R|fʳ7 v=?R_V}1׏y*a-b;*t(l:uyZm3?*f\dVb z̅Ow9qV~8~[?iVMUA |h"?6ޛVu$Vf](Æ\7md~Ӽ)Չc|Yl]v<^`R>(sh&gZR a5E.YMbozP/ņb)K]ל|ZBl@W"gUkTwVS}'=wvζ-z-o0I$׫\_0^ΘR]w3ЁЁfkoX js sn} }|_CG A-@g+M~V_ӷX}PeW pgا78j`\c3Gqe1S\ fk< (9xnRqKoǛ⬽j1;/:7^l:[+CNHJ1JT ?{\`Op&k{[gބ|ʇT民9̰8ݙCSgO!~0`Juۓ$IrJy+)a:>[m2W5W?U޹ +qDzׂɚQzc^߫F8k-\%z V:}݋9. +RΑR<궻V3/ W\žbj|t-/lWI$ZkRNtt1kySex˽sSqe=a4l] +,DTH=`G>:jb1ZMz;[bλ @p@"Nr],wkoץZ +n0)f^Y|rwy1ۃfn;EgtK'W7CSws|>l^;zV~!7!ܼhqPP?Kff}jE7'Fv)0Fgǃxqi9{}z黬NZɼwKl k4vq/+'PWsTP=Ye𕟿6a\خc'{!W[%J$IkWߥث*[ v0ҽTb[IT7W<}i5aF˱E#n`e cx,t)ɋCl^*rX샌k3z)fJ9wFEy|%sŜէ͚Bˣ=Kk2kGnϭľf1-Ĺ.}s)ǯ|f^y0{??~o%$I+ߏD5p'q7$S݄_F~ooxM/cöߙGT>ɿnN~ t CG60d'u;S(6h-zuT碳ajmg1]*ހև:28e,1=b[Xhy +!VlO9)j_U9`[|φܽżokP)+Qz9{{a'k}{ s +g!O0$I,Uq~[O9[@܇ X'|A'd>Eks+_5ɑl&0C l8:iCbܻb)vcנW'zн]XGy_ͩj 55=w0rJ뉽JbXac] 3|y+y |u!B?Ql_CR7o2g7Þ\VCJ$I?f_>evٯT!yWغ$U{W#ڇN\v 2je|j=e\lv\Z\l~瑯dqy!ͱ:i*qjnaj(WM6AVcIΚ- Ip[+|5qZoOֺb x@izreƆUVY,FF? V +W0U$I|?S1v gx6baۙ|#< +_|7}M묯[c˺?I[],'\y+LQrj\:|u_9h]ںt#TYmnf<ŊW6?XyJ50Z;p Y>ls2ک^kn`&pN9#؂o+$IXL_\Vop^zqW7ܭCĞ%5p7uٖ??kK|cyv:7ajq5҆a5՞of M[3O{a;#{78ذ>OqŴ(;?ay`^,P"}Doc95T4/}Tcskm5ω=Hv Zjb૥eu?ޜ Ù[_uk ^U1AlQ0k!{ӏ"_,_=rU$I$:|5Z(~`~#Β=6(.WO\#| +|kaOo`,]ď z8QN[})?sJU+޼+z{Wbb=rb|sD>w^ |՟ >bx%_y]RE~c੼굼9ˮ9>cŖ=#1#.nebW_v3/{z$I$wW~ܺawK =D5b~{N\o >šE|+GCM`ʘ OcwbdMb|G_,WyEJzW`|3,W_y=U2Uáqg,xՆWm-ϼN_Yw=w +]~#(n?\f||lWI$IrmD|ٰ7fų(&=B5W{ړؤǬ3*GX|#\ߟ fv_}Ϣ˶Z R?kQSl]*]}e%99|V~7ja}?ƹ`M|j{=pװ>"6W> /vWì}eW<-V3a%9^?(]WJ4cY?;gv/K~n8\MT`?}^gju۟Ƈn{lgUsRSǏ}5q9NV^w: }>D [ ,0v}NW6'U mCVj#\zȾ"_^A-eNY|"e ;j_7o7htev<>zV V䙾JS}_oWI$IW=;W~ [__rK-yA\э#Bdݪ.>R1XSd7Ի w} ?X[ʏ1K?CΩ^ʱGf㫁@;U̾ة4> RfՃ?g΂M&ᔥ\#1>a|u_&+}ss;{ϧT_p-NXE%'A),Wk_%I$OC_~+D\z,v>+?Jj3CFs| )W[,C>c/7)ٕT?A9ebìPKs29Y,x?۵z"|%ڻZ>-̫}>w`fpl$J$I\kz#1Xy>-uW\">g;SVc~_&_GDWe 7RR>?l5F`,o/WD|,?[~l}|^#z`1U=_%J$I\-*+@,>k#\g.:r$: 6\X?t:^|];?jEV-pf s3XX^J2cV'#2c )zx?_n㫏S1/_?xw$I$G}c8aE{<GaO09{سm+!o_Cŀob|2D( c;m\|7&5{pS? ֬ˤz~|U*17Cl_s 4%23,Ԣ՞~tL$I\_[}?k:j2VS%zWr*gx3 3ރv#a>2{&T}^^s~W r΢O=Wp +Xuk v%Vk*kx'VAyM1׾^9Zd~Bi-읏X_T*I$I8B!uRG/to9s&H`̵T}N"mWEj8SGo9>y8-Ցk4^3G~D|.n_MvA +G3|a91#pߞEν<#^=M$I뻿v/5+כ~XpFo&W~eoJq*/.L8G?EA/ß94Ԅ+Uꏓ<0?簕]ZF[`ƳZ oAu l,ype!q珘7K b[U46`r= z|2rFeL$I䟗@g/BNT}2}(:m'`ᐚ+cTn1_;{l{7>oZb翣86U.OU(lUe l\y|%+V}1lJ` ^YljW5 +\ۈukO湀س#_ m#r b.9c9Z^kd[B wz6.9I$Ijz0=GQw?e08a0k|Nc4>>7/x#{ ^/ƨbܽGN{AX9<c=Miiʫ;O<zfc'u1둫^.RC7f VjNΟYVX w07xw98/Z _%zN|$I$>j;,:#KMkKb;UͰrϮ: W|/yXFpO?>\.+~h=72iȧY+>4-*̋ቩpj-/+\YA]oy 󰍽 ?;_yb,}bƎ~fv3nhƕkygqb,xȱO]M'c!+$I䟟Ts; λgk{a;iWG?WU>OF"}qo]f'ov/ՃP|?a[6!߱2U9;ź~7}t.޳,#,l?ՂóO46g'gW[KTj)|1|btJ|5ZVr7xS ;vz!yI$I䟃T|96Cܳ+>ل^-ȊMU4սr`|`{5c_ J|)z8Zyav&Q}Xs <˺};n''aZ[UPzZǫ6rsW/2ثEj5+|h5ؚ2lN,wyzo61X2:XʏlX_e%J$I~:nU{ Ϻ;V`z '9Wf%`s :anTǏz՜SEzte`ac̺6r̓E*V}YV_` c0!c +_M&Zo1cgثXXQ.88Oۥx֗|Κ*QK{+gM` vN?5^+;ݷ0ߊߏ$I$IrJ1'-Ͳ4Z1v]~R\+e(,Ď:=G}e0* FUd+7x7:m.m78 p&}|hï>Z,a|.?Ev"| مzV +~Wq׵eN-YՏYUuf|;<[nV,5kV4:\NROŲ`طBܠ=+\^w,̹>ڋ];j4$J$I||UJ<*~U,J5 wxo~05sy|u,'FsV:WEUg %c+/_dƃMx܎EF|%$>oB|.!cewӼU]NQ|/{R]9~s[ulXp=*I$I+#Ki9DKSmW?>sئs[iᛝ)=7hW=L#WML"_צv x;qVe`yWy|asl Cl+X9;P\C:j%L6L룸AjcuG8=W2lKtj`%J$I\+ʫzD[TDm+Xf +^}V;Gma#-i8z09n?n}|cLWf"v)kT7]}Ջu-Ԩts%TunWOxS7vs4mɹG|r{{[_Z3>*r|1k&U.WfůٝՆ=ܖ9fS8փtռ +|6!G-rsֳ k4lS9w<$Oa큊ƚN}"U>8+߉ǽyn|aX]027ce'm(Wy~̱װ%ׯci7_2l5ܯ6Dߞ$I$?.:'_ E'%>L>xO޳AS?y&lF `nG!f:y#K5q\pFWw!~g$z9|Ϛ;X.kygfR1V[q\=*ҹ*^k Z;qE)!V.Ovl) 7cM9쮲PKg=1WW^&_m+WyMS$I$w~ve-CW- ,z=T0̧"zQWuOSW)5?mŗt]8+gbh!8bXy:[MX{У\[sJl/a⍼|TUlu/}i) z=N;Ƣ+m1=}9kkXZn|կ|5|||0I$I{Wc?8CV#\kq ˭#Ewy FHLVOxe :rYզRo=9|r-VOdwƣY,U"Lװ3`QpqP *\8j6O/qh,W]_b+Z~u|գH>g^/WI$I૊WVY͏an4 0PM|zmKUzziM\7W>ܵ)'x&{㜂#s=k>V k::z ~nn_)gU]^_XceUS?I>0& 8)fJ`W+uφ [S`pz0O+уX@1ja5%"_yMTՈy3tz2*/_^<֥ٯY {#I$I|{Rls x W7S#ZP!yn.1W}+U+WtU{a?fsM9<beZga?֮=`˾Yb|9y⫪n,bWбzh)3?-Fgk3Njyn#=lWZuձǺq$jk>0^_=g`W-&jΫ2 }oWozdkKՓ|V$I$nJcð^Y|+?)y#:Y6 "؃kg$ixKu0ۃ s9l~~Iσ6afYq*{*6έ7l9ϝm|&?+q ^CX,R`cɛZX*;<^s;x2: +_Ⱦkh 5ub9:×-u_uW.>kПl`Yz +*oTTWl LT)bov;`죜AvͬBql}~UIQ8T+0kwՖ=9 `bo$ϫޫ|ϰRp^m'U$I\[*{jm#9>Cb7^1O̎ g3dk }zײ9uq"8h+z{{?u=7zj3ɏ,ذT -{̑b&sCrlcs⻲==g&pH)/_qbu5p?ϙH%a^5C@q?y=2!6?kyl_o$Ҧ$I$2=ݢ31ݳO૯_/(xȭ*05rdꇞ~Θ2jMc *`+SmX8j9jWKpn;|b'1y~W| fk}e>BgCGx|}^c9m=_@4%U13y^lM7ctfox5ѪGoSXG)/_h6|C[PU^^*I$I>_DtI6S_W_[]CbmOX^Xd Z,ObPt`\VOa;>g ~팂SY͈Elv%|~?'hfcW- fY8סUTnbKkn~Mbh˵5uvk^sx غnȾha=#/v+czHg竺 7꼉ֳǦ%c_p扯$Il d}]> +[KͿC)PS+(ˆNZ/M/;S lzn7<>냮j| A=],f:~ܝ܄_|5W&6bcm86\*~{yc#8Ф;b;~Clt39;ϹO_anV#ZN\ްn@S8I3mz|+ltϫa?ֲ)2v0Tsn=61nt+տʳ}#J$ITM3 Yw>T[M.JqXM%F@7AONBf:m:c*zkf5la ,&z:DT-vf`9ކMdɜ1趁F' K1wUb?l ,?UV1K>_\lXއH=WXǔǦ bW .~ok5lRݎ횩zmix)[q\`{_aGR9n?͎Վ η/OOJi`Gkǹ7=siucYzE31V$Iq7Yj7X9-!gubo'ޞ+OPl6+c{rN]>YN[i\y ] Kb&> ^Mik)Bu`rl<ߋ׏~Ҫ s|{0W:y rn=:Z݋-jOX|Wwaǖ3G+I$IW^[kUsyk?V̇|&۪v h3x c-@-Z[# v +PNV㾾6v-ٰW bYn[>[<íWo!08a͇Wqx<狎b+%>fa~8ǿa)K5}.5=GzkNz؀^ں k隭6g`XU3qRբu {wIvi>_=>c+κ217,{Q5PY^Ɓ4g16O$I$߮{k~n>y:Qʿ)oşɦݺP- pUG3 kffK}yT3zgu+:YmЙpbn.uU+5;]-pz~[yf{m=WyVq6%#_2ذ7 hOMő|H[y|_K}֩c)A[_ʮEg]yb0M}>yK/ &y`gՌze8H|C[f{q* Muxϼv16>+L$Ik4x 5+o/dOGk6 VK6ef#&V,p$zMIb#yNce-γ +z ⌫'>mnA9tn]b;5oM?Uf'nYқ>~fA69n% |pS^sjf6kg+xjs26ܞe~&sF:ͷ6UסD-:r1Q %R}!)yp|O|*il׺L5V]=uoOU~ jz= ݇s p/kipg{YXIusTL?k+jcdu*#Nә7}"ӝ0aaiO(e [zxTol(kxҹqb('k*s҅jx<B/CXR)n],ebح*U"lzyl<<ەH6FW6EUKv>ǰ`b:YKLLcmY~PgyS-JfY])_qW5H z)WχOUOsPnBUp'2\VS">ux\2U]W}s,5uŊ]~k4og5ek\j7|^!ٰ$IX#?{mvB 1-Wt:}႞VjB9xl&hcGv#8bgJ9l2:].=adp-)#"Xh-U{Uh4nAލ-"AUkW(k =GaY_W[=Kh{0?K'쬲G6ڗf>~W ;\ɿh}T B\l9apMfYN߼8kL j^`UmMF&x+ɷ~½[̕x~*٪'j  . RK1r&kPwkjGV$I]*tWP[**?\#JzzDq+=B&P}^7Crj =g]U\֕ls͆:Z-si; mнKgY!R݋?iGzkr69 \-埵o%JdME}v[O!:(i,T;l>7ZqAx"VsaujVqP (񺝼o;oSYOVu'ˋahWƾMs *z߅q؊#oܚ\O>ݯL[hԛf TCEzSn^Qccfe!*I$IWN]qM`+{Dw^݋nN: }dP̙O. ݧ:|7O;.y7FW~s\oչeuV{[]l Ba\q3:C>Zn5Ul]c{jLbݬl>Y}6YCz_v/-pMpm`-昭;naW>Z9sl|\½xnV'V~ly1X)=I$׻+'an +Xׯ9wq, :t9trKEgG%6^냞mfT'[$wug~je?s8Ͼs߃z*m>;o}u}/᮸9T!!;l^#>o*MJz9 C}\Ư F +sӞ=+e7ؓ٧Y|o<: VD_N +흇ewvw's@Ŝs΂YD@D 3ssE'}>ߪo=oC9UOq99_%$䨕_~]j+KR;./JR{Ԟ/NֿFR{ڲR[ZjJm.7Z/R.R\j}JmXMKmc,JR{>(J`-2RIϖڣvqCXj=P5f.]KmPg+îR~oSd&3l=] [R{ڗګ ,[ǖR۝3^-K{={v_ٗͥ~WjKml a3gJRkPj} w(ޥg5Ԗ0FWۘG67Z6o*d&7'JRkȿMgG`łR[͞fӼosk;,`xIw=/unu(T%9|?*OKRԾ9wX|n$/l~OLϮ=3΁6Ɠ(y,Y+|'a,1@bO?8g +6xgbjc*1m]bB;g)$܂e` +g|u w?lbǼ;rj +̳jc),u}SjҲ̞iيmթW*ૐR;.(+_/_V3[ZK^GWd4tKN78х2_1zp'>՚971m{XSp8xgz-Ǿa>4ƛ[:N}X#Ͷ&C֝)k6JU.W2tt EՌ=klFpBxz#l'amkjl)6;}ɾue/sk|a<<0^%+~uvb1-q|Vrh_ux|> kyv-#+>ߗ<s 4j>R΁/૰]3K9UR +&i<5o1kkt32nE|K}Wj?[|"6ᛜza N3f3.޳ 1x;1.[@)}68e1DOٸgq3Nr˜E۝s9s^ĄSX +sJmQ;w=z;XK'X?c洖|S\O`ޫp6Ϲ4c{<|]`YhǞwb  `2A{x.wWŒ{9 -s٘;cgm^WYvrWj~ૐv_]!l],Y<a_^1H_~OF'Aś[5-5٭]ఞ\zN5:O;u6P7[o{WOwt'?{C؟Y0˾f˞\8-™˟9ߝV9Zay~7Yf~~,6@+lly>e{ݟ쯸p _ﰹ9=hg-5. ܽ0Vo1T6ΏlVWw'[]64N󙩏Rx38Ԏ)ૐ|u,7O K~6E^wc?HLd?l }Ș毥Ro:ov ؇:{VUU=x)l4hbVF~ [jco1vQWѝYƳ?=龫OZl%Fz0˖K&qtg:wOla}KxBXi ̰SNs{r͓e;XP|@o~P]<'1bٝUp4a>cͻ`:dzك [vk=6[H]lᇰD̋mUHHHHNYt@t@񷻒XH/NB{7[߇_~n8@|{O,:t7.c+l uBֲ[|~DipÌDvNxE CW$| kwŮ+e)_g{{{ҵ-f*l5lw;eKo뽘=žd0Ϝ=XmX7&VQ<+u/תwbUm ?~c7?:v'A[|-a|Rb՝i=c% lon=Cڛ=kK=k7(U|uL|՜"э9:c:żw/z+8꯰iæ z-R} .|5zS,.y-z~0f_wv}Iv!h8 e&<;؛t7m6 l_Ʋi|S1C{h>XbݳxuWv]Xkۜ~y&܄8|Zv`UO}V&iF^EOEoD/})|'+2Ɨ'>+śM=_D/^Msq`yʳaɦ1s3O k`V`诨ߥ Ȇgov8az-S7g'0LխO&84/$`F4hҦxyK>Öx>WX a۽&VYL:*|!!!!kGJKq8p^_uB/.Ƅh|h{t;>]r(d^I^-CGO}K`W*nt⻖㒗}qy(<1|IE~<^lK#pHqֺ+;bG}8WݕS>͢|anqWJ< ox5xkQP,fUd3גg1qs*XbIhuOІؼbS~QLߏg(cn7g[Ѫ~~GQ +rV0ѽ&9>~|թ<>H#gg++=6>¼X)'[KļVdLycߎ9QD71Xy0~˸U9|eژ +ߞfӷ a1nk)*/]Mt mop]y_Cx#խ|/f}.YN'X)[*$$]j&_)g+/WWI?BYJ33/7zְw,7_)RJj>ZzMw؉C{]õ ]fwgLW6=]Rz/7sKΠ~/AwS7}) __oo:o:3Gυ-|&{RW7)э䙺;|+l6b=k#L,yZ7q/#9=Xv[[<_u|0*?c_y,GrP@}kQI/|~(|%^*S֢چ]xbr^=,n +H^p^ZUm~]gEy=sEΫm^Zg's֝s.rL]C?w%6d^S^(ex_䍌,V=$$$+ŷ{~i4G(c,P+tUGk+y/+{N-P^Hy9wڰ\Zu w +?y̲k΋Ul&Ow$+Z6kaWrrvcOz jվ21調Ӗ9iUŝʉju1&UWr_/|)sY+<;__nOtޕLlo1X~+ձvO[_0$$$WyU_y ̲Tb6]lMll._)ǹcCa&luZj%Ĺ?vEom]=E,#=rR/|~_crd 3m|5^UjCPy>ӜU8vT{W࿻g^?!VqՉg1ם_VbnR;_OW^>XW:i:UiA*alJ1[^LZE|nYyͶsvw0)L;$Ϡ/9WAy*^KT|Ae5 /887Q9⫫ \aٟEQG(vyͩ+CBBB~~|9!pa?#/_G/q_:S8%SfW`jl6Y?~W _ixX [ճܙ ujUMLSZs&T|={=Y.lHwe]Ll0iν7UJ~G/dLm>6` | _}{&?l[_HE)/cuǖ%j?:*{0E 7Z}[ѻ`;q~L~v7^11W+Sa_)5_}_ի_-HJ<>53jk^G gy9y΄.J9܋Rz _vsUHHHH/t~R25k瓄TGEyǠ1@(gvR[x/Gݷ>F_2~|UIJ+^eqR>/`/[~Ug⫇ 㫱GdcuTy~A|ٮܚ[8W||[gc|uk U +*{|R3B72j>/aeox11Pf/Gw3Øs!zۙ-2jN:riݽEWP;0~~&L:>o_=#W:׃<+X33:'wlۭl[;Fs| +ૐ.R|ӇЅ2_aK(b_1#ГyF3|dW;׊)a`?[Ԛq$}iVy +:u{=5W_-~}N:3Y)9J|:>W6{u9Nņu#WYa + U +Jq#¸sIdbϸj|ztoo3?{i)[\T:WW{ʻf_ٔ&ƌI+ 0dcI0\rMN-<⫾ _M7ƺ<}bƱyg69rU3=TEm[e9N _Շ~J !!!!!'t3OއWg EUq,{:>Nmg~oRgy7Ͼ,kT[slW η~f{4SlV'yՏfk|o%6y25-YZoAu'Ms&vswrWW&| + + 9:hn|j\1 ";$f[vl&GŌFٰw__)@VW[s+Ƿ-}RW\Rնv5}mm|UI^r|(EK|=K1Ⱥ}׊cc̚t L;b|c:ީ|BBBBj# +e}S} dR>]s Y< T7.Xm|%{x烄d?S\[7^|5zW'n_46&w.Y7'vS(Q=#ߨZµֱI,WGsd.bՃ'OCW^]Hi|NW0W!!!!Gj_}:1·7Jqg10k, ffk-G)W{fClh,}a5JfW>cW`M} #F>I𣝯LZ=>-T 51=]VRy).S᪣`HHHσz&R̷lVZHdswsa~VsDdCh>rMfjQWZ̜UoycR @97>eNzTOMy[s[lMt>9:BBBBj|5<٤|抃z׸  rG-C.gz֛GuYTKFvۖԱ._V(x/Ҟv־OOj$f=x-Wɉ>Z*^1a, O'KnuW;ؙu>QjZUHHHSV|^/c]d:`yȕr ڂ^v.^_&_ 8|5JU2OXmca9~ ?Ͼ]!ΨUR' +|tUc*$$$vImWsZv3}l 2mHWiߪ>GX|~5g]خyg(A/e͗ql3əbúGا|U +Jna%X{BR7ȟU8[{ +Ç,뿓s=Τ~R;o/W}VdxO'YN*{9_*N~;ZZw?]W+!6[W뢷Ao+,"R|w"_u|G:&_ HjrnU ^.?{S[u.i{z&VsyF7 + rՒrߠj,gW~*Ռc)UR^CߺG(r]|51e1c r))%r=?2|uNvpVCx=:>,e*W~Wؙg*Av|5596N?kguZN=ԟ)؀t!ت!J7LL=I՜Uػ __W\8z1}^wd`e9NNwx +x9_eڈo =RJ7n!g|2=H7Wj~n|%0;aI)WWW`F:Vٹ/5:>Lݽ †N49K3?؃>֓>א3*S57[(sߢ/OO-mί%5$aV|uKblWa + r4U2|4 >lLXkVB:11ny|FC=D~a9cKعN t+8ߢXns셭yl?F|RywFq}P*ڗ9bOƒO-`fs;^q+編WWY))~.\~¯~W~Zl"竵{5:yu἞'_g>DVhs]w.c@u_m/+IїO7ߠ*;gN+Ռ`?)(ε3}Hb[ +]٧1uoiǣu6ǞՊjU'{&j^O3Dz?aoBBBB +*ͭX]Wd`)~ל_13װ֧BϮyi0tZEi+Wg7Wuȳpq2;Ŀկxyn6xʾޕz0e#'<[*}yO1㫶Ð$&cE|խirոa + |5 bk6_)})Y0Gf?3?y?疟 lZku~1[61f΄VTWݬƊU^|t7+Oi|u<HO2V7h +kس"<~=\BB+nTO{ `H_W_)&_|y;k?*$$$W&|<"k|5+ᫍVsP' 0tO1);;W}o#;酌?91f4=0/s;xgr;y0oaN& w4qVؓ9ܖǯ+䫖9|$qA*hMU2|;iW˽4]N#=$$$WWok;7'[S +j< YXO?: cM4]-ۍlWrP:v pؒԝ^^WXsWŌ_[|Uty5rK9<?a-ʳ25}Fa ]3{h)KXb?=[k~ߍOKJiw7˫ +ÙrOW1ss>m>ݪ(6af坟j17uSS a[٧,_|bXv( {T +<? bzìaOarjVW"OlW${$?KqTNj-^ 6Mnto˃@_K-DP~w)bGÎ(`3\hqf st9}lu&/ 0l׎zN+[ӫy^43$$$$9;z~'?oJߕjz0cn 3ǏGXSe)rU:YlIj*B8ҧ{5*>L9+{TNwL֫[s8-6ٰj6yIxRۥ^qb'k@ U{=o/V&EsՕ[=¼2l׍oaagdvknt|s6'G؃gu/9՘[]M`l] [rΠߒ}n +kIU㫌۔qO˛|R=ɳcwO[1RVO1R+XLlMVa + U>*٭<5oX#r7^mv+h-Fl(nlzxݭۘ5zZnձ59*?(*siG N14E|wĆ%= +fQb+̛bg!+LmK2ԾԊ3];ިO}H;<@ K۔+-{Z6%ȼ1^ K9!| #*טj{ܯ\U^HHHHHeROw|c9_Y1gzItY3~+UޅV_E9^ĖӜ7KCʃ7]`o5Fw=k4r#Wy"~r8knWuqpi5aq614ձaRu5ƞ`uJww1z7}v1z6r ggSھ#^k'ά)kO݊fƺj=wb dԽǘǶ*c0|MX[眿Gmz.7g%~吐1Y ]6Q^4-J#tKC)MSnQ/ByA-3йOG +' mk{*9khNS{?RL[ާ $urRw#ך`C6 mXu_|TaCyI+>Muu?a)>OwBcjuixO6=7\^[WNE9enF1ncJ|k֏2߸r6 %i=gƮ]2van'{;_UCBBBB*r60䣸ێx+ҝa $~np6zrr*ƪ c6E_]2} d{RUz= 'eWvŊRzj#TUUR Kw '<?u{߄O#7ٖ}&ߟύ;bV6YU0خaf <,ۘzlLW8>αOsЖsn3qoJSړ{ٰXtܮ6n{̱!뺉ԅOɉʳ{~{sU6ᐧ- z-Ol8 Iw7mr[?Ss;RqL=Uݼ~gzTU-C\*k~뽓~[N^oD,v=v;=fUщoN~a0yRÌw;k\o"X +w ql :8R1nKݛҗx1:nٮbObOO1j=lDyTڽ뚄S`^T_79q)3 =g7 |}+ z~4_v_Lr;tU1l{X1LdiAҏw75Kuvk >~<{nD!UU:2gcOy #&ٷVRZ%kaKr赜]j>bƩK:3l9zs8ÓoeC]fHg.Y?Wu ɦZw]zaY=p^>T +T_:ndαǩRfRkݎx{6Ka:'*i9sW3?Ž0_50ȅS܄`l =h[x&HbVx>%|{u{S/3i\w4$$$$zRpΰҗY,lS-KuuCmם^ nn*ZtԝAɇ#+e,ٱδn+oG5O,(y&R>68dé<@aϨMܕK]wNYqn˺ɞn[ja+WƦa*CTS \|D~Pu鯋,EvdW +~:v.znGGq7^/q5QټLS_Zޗwa뽌{趴Ց}Bq3s1n竟Vd;Ov{6)Fճs>㝑)eϥډ}6cvfb8urs~j„}ρ:kKk+:{Φї1I㮪k@sX7'>ݑ%o0e쇸̄c.6).\w$gvI=<&Ϊd&vZ}XGM[wl+?qsVa + Yӭ)cx {^v}Mj?$P]@9i Oc[8)/Y֗TUjsu[=+gi6G׏n Y3Ny;3d;}Egꚽ2,rUuyiU)å,Au;csTs*Ww.9n"*$$$t~ϧ}}sn)zPOT'ה*J}xLlis֞%'not|ϩƜ4fMcT ǐ~uMخBBBBT[]w}^s~,g{>~]IF*3z+"\Nagcny*x ]sέs>=)}*9"{s*U5n޺BBBBJC{ H_vL5+W܊"w<>+DL9ޮ/6+_s%,s;7^yg1o#*$$$䧑:tL^KuUJf +;+jEQr{X~Y(iR*"JTtJǬ1ΰ_И!!!!!G^~"f(ۿH_Uws>q]Q?~/y"t͕>y;ǚ8Ú\? )~Wti5בHO%UFUlz RP<;!!!!!R}s0Z?JErCYsuH=o55fHHHHO+5jЎ R8T{ёWM:ßsR×ڬ91ʍ󗰩T WU! +1 Om"! +1 Om"! +1 Om" +àS_U *, +UqeS6YdGZQE6mAQFPdPGFew"FddU3VuZ'VUsVuު_էVu.Zū.Ye|UWU]Wuͪu~U] qU6uϻIϒ~Tqk/7}u[(뱷ו?YX^mwM(_~.LP;ion-_۾rtp{ws7!|9c9m]1jվU*JWKչ_]jg_5|92ua:Յ.|ռ+Ww5X k֍j^zg(K-U3js.:K՝|u_aU* +_+%_] ӊto+W=Q޲^De^V4#]jw9X*W Xէ_a3=J]Շ|u+:W/ W#lާ3Ų\LYytYfGwnYy{׈e,/RW :+|xXRKmzȻb؊ejߊ-=o18⮙=hu``g}:LxXQPf_lƽT*A7hyW/J2f,NfbsYʺ.qߟlQֵ9@qVXz3jU*J%{#J5[I*;ٛXoxXVt|{2-.kX :9ʿP˛e QUT*#_e zwe4gdXOiE[\y:GDr rLgfuk?Zg&yKZ"ƒ}B9+fKRizv˻b[~ *Qo~e$+Q 5B[5䊮w{c7Z^,aez_i*J]gzW/[iJrd䄘> X@޻ZP9 D|a z5,fּ%"}B9wGe4cJRV3{Qd_Pfڌ+V +1U6CӳbeAls ._(<>*b)ZNYawB&yK,e!ƒyA]z.BJRi3ud]Vҳ\%qffp">d[}0a9 +1?Q,+/ .tMTѵ^^M3׽^b6eP +0avUT*"{#Jfu_P!g}**Y'ɂdyWEiA< njQ㏑v3&S:#]o SݔX^7e]]K~umؖ %wi֒%,X_P}b{ fRTڗޠkڥw%zl jj*fQ8<ק( =b[fLoQ5uר(KV,eIƺ[+xX3R+b{:nyWQ_𖀭gJr8&#^bs=QNBL ?,PT0e'@=Q5e qHO˒ub,'<,Gf>NKR3ۣ`]E}@JW3qY/51>bcV +y8Q'X<@E`-;4g^y,X9 Qb3JRՒ+7hͻ+:$WIɜ+˜Qc'2de|GiNA`blgUA=? D뉮]Qq.Zc}Ba#,qg_J]g2 P_b*|ܒqrCL,B|'%^kVy,A(Qz륹 ?pQQz[c}]+YB#d7T*%{ޕ>/(3W휠V[fH ,nb2CQ cq} ?h-Gz?بAq[zI.X2òz(e**JWl \]ݠ' +qӇPEe ybQ4hV`ߍm;K^J?]S},b bwIޒ,X2.:zqR+7r(w%3/xVڳj\H9L%L^OY:X +B2bѵxO(1u5[ser]屾2z(4_T*WLJEAkgwe3Jr7{>/ʓ[ԓc,K!n8A2W ?a9ϰz?SjsuG~"\%_e~uuKf-jgJ6lv9J箤w!=s.<*6Sr/@?N`.׶Yk^xK[5[gIjy,aY=vPgZ]!,*J's]g5 \{;3rWͻ}\;;;$S!wV\KO!@xFn߉yPˎQ^ɥ"_ ߒ~d+}GՀ2X_]UKҮ*^s7rm]=JzVb*Ğ=2C^Of 젟fTfPC^Yc>ᣠG>Cg4 _|RMb ~ȵ{ޕdcjleJ`J|y=EYk/WS!0DT5GdloIRV4wE<-2g@h/)Ò9cFKgwW5T*k^yAkGޕ s[{S1}(e=BLb LMۢ~YkIpIB='e[Kg_U|U*NUbh \{;3hyW/hUϞ:ü32_eU/x3{^54o x)ֻp٬B?7zNzǩ8֞xKZo~7̜?KzX:$_eW_JW7^s^o室w%3W[evxVWe1gLϓL"f4_hN,-Qwާf&Yio7M>_VҜe峬3L> X({-%3 a_]:ZoRiib2v9]QoUV|so>u[Yyl +e͙sm(g$bQƫ<1Y?QnGi%, yX:_}y_z`cR4C>;hͽ+ynPA]Yl?gYGc$[Q>*sn^TcxXd8,g>^~ϓ%tF+f0# _e+k~;?f(T*_Y;lَWLoPzW[!bX+ޞVL&p^f$oxeʢ8QR꿈9KYXWhyXK7WY;l=KU[13yYxJĘZ^,ZKr"b˼kk_|WۭW|/dۋJҶfl}vPEvAyfyWh M`@|%[|Kv[ pUoh))3zb?& 欈f{XG8Wr>5w|u|e{T*WlWۍW2{ewec,TtxW,[I*3*o;؊eB?k$+I՟A%,>!aWlJϿ7n5j__iT*_y-.g "JSde|/UAٳ|'VW<7S>WދJ2N!֒,XV0a+4Ao;_WLUUT:|%g3Ƚ8_l;^ޠ-Ya"fLb2\ke*zBL/N!֒e1a-W+k?|&wC6#vkBUUTWlyvPvtn$Ky+ƻ=AffY^rV/oYc3J2_B%9K2aez^˷W~WT|uT]٢Aϻ**J쫛Bv+{Js^z&W7xW[ Yᬙ.|ʫ[\1@Y2a|;K#E[T*|gWl9Wʶq,ƊrXtA]E!?1fxWY{UgUSYe1" VWҳzg^hU&^UT݃L+4A٫Wj|%W7yW\{%3X3j&[E\wB#fa1|_f۽ 8n͠lV,JWϒ|f3XzGJq2{{/Ȱ=0g06f+TX'{3^E[o=;(vcU;;xAml+J],wzq횯 ^= _7ɶ{Aر7&Y}a<ΒyX_Wl9;f3Dg`FJ>U{Png;zd%*:;^oЛ+Ւz=F}[XUb7=,c!b}·BgQ ̾**J'WgO{_}cWN櫈zK;gGJzVhqbJvqxX_1Ű~ ٫zӿZgU*MO+;6gʘW[Eyw,JfLb,v&{[jj̴|dW/+^K{X}9cY*,:7}en_J}>-RT|WOUcVcysWl,_V/[=V:c?KT<,s² q[2=\;DsE_Ro|| R|uS ;#l5Y5ޠ9!b\bW{W΢j.YKsVĪf{-XY!|% +ڭ 3j_eWKgR _] +n+l5WQP3։ +-L]}-޵͹[9c-Ty5WIPo0ˀW8T*T{&urC'Vɲ3U{k >2=gX g1_+7xUw.JڎW;|vUc,.6Jx>W& +\e/B{?G̖b y]9wW+b-kȟ/Lm_]ayj]Ri_iՙO|ue‡Վg~h8+aEUֻBCu[5ZF$gi똝Ӏ<,XgeK&F٪{$_ݷg7Û6JҾՇ&%U|_yWѼ\o|2{]=kp^c,ÊzlK#ʰ\6jz\UulsN%# dv6: aWRi|AB;MU[ewXgddsWYLˍ8 1;[ռ*"8)Z"]eG|ે_՗7|um _]᫫|u&WJRi|uW|ճ>t^j}.w_!ƊɽAϻ^}LJ2{hި5ە{cY=1*eG}Q流zfKz@սs b3XR|ջA7 +qR4WI}jc!bg2}$f7Zò,BZ{a1Ųf*U[yl΄nrW̌LƽT*vWA:ߏ2S\ Xo%{=|MV,e˸gwx֌UbY|5;AxB3FaT*9_S3GLeY +qVVo0Wzk |eeUV,+q᫙y媈w(˸gf4zXQT*fȱf9=|Wd*UPO+/j=Bkqg &g4_J}૙;r hƨ+˻7JU23F֎|,[EޕʚxXyGqw;Ch͸U*e5cČQWhƨz{lIB\ ̌QwHAۗ Wl byXVpcY Ve،3WRif~<9cԛ51 +eu^whX=Xo cY θ9«&!a#2ь]պJRi3ܳ;rW/:|+T{uoÄBzpWL}UVfnFRcW-=> RJ"U<. +oUqe*(8>fq@D0 ADx D3cLZWowWUwyvy**_g'y✯)Oo|oSeS|۔o)Ϙ)9廧|Ny)/))?>US~vk~M)3))_Wsw[̗|ȿR{'w3y~|=3sO͟/z?sG}M{>-G,w8v&397)wf?s{οW<:cڣw)5n~g)6WMS0SS~j~'\9#. wr)1{tzڼ69{eEEE^:NuݸyG~`0_=<3g.#[q-Ψ:e8}=YZ]o)[X9TJ⪥lðkZY3/Ó Ʋ 3_1eUEEa: zO>o +|(Տz\SZ~a+Q։G#ɴcHm:\R⳿$4+<^Z?k!g9KlR㞥|WYkeV$*U/U#L6X#f5]櫏-ͬMUEE~7mnzۥ kWVX#%;ZBzZZ%2WhV⨞ޭYci}¨527#W!WMO|uJ|W{vl*=-$+H5ޒXkż +r6X*ڳ>sf,hX^{ B>ջw͙_𕴛MW_%ڼ=+d1C4+ؑCLdhJLa ykaEz#wAJ|>|u!{V_UTT_B|.W|f{MyQ f\yO2]Q]+YkiT#5噋/^0W |] E{>1G.!:᫶[b8L|%yۑP¾ VWM~+ke9c-_2U7+4,m{/_v^5zP᫻g KS~_]4C3 _Iү***wn!e7z۹7Ⱦ+<&\ke8Ҵ]UQ^ 2 {_}f/%ՍS^7nQqmG*e:}櫳V+ݎ$o;jW[^e?"dWzWY{o;Fw_ewFW]ms8|Żp7CGzv{"-dH?+Yk-mUKvj狟mD+ jjW^oPKnk ****v96|,]Il%1=2ZѾaF9I5zYf]{jv{$mFv3h㊯***_}0g jW[IL%ϟV(siiYKQjqVk__VWEphkc+ofPҮpn^E#g-]_UTTd6Wjޫ+,3\al0X dV={3jXQ؊Jw}n{5fAd⫊*W+_iE%`+]yl%q3pV"~eWv`"3f z|5Bb|+iom^|UQQq|uWt J "_Ileԟf4Ί2+ծjX|eiW[익Kf*,G+g+k3]Y{$m_r`{ymb_IՃ|ey[mO +[-^0+_m{GY\Vz$_i>Ijv7{4Uv***vAf{iW[1Xcle%{5ZWQ*sǤyg +jJc+֮` y,o{q<"-+]Il1_+KEʻ{mfveeW}Խ8WVW8|Ҽ +Jc(Sex+X=|ծlqIf< k-q$o;GkWZo2h+۞^|UQQ$YXKӮl,Ge2Y <*s+ihYD!Ez|砶WTދ^1[~UQQ8_Y+I*Ugi%iXjW֮VV%q\zz5]䫗 xFkWռSvU|UQQ:?_eJӮFUzӮkVUY,aLFYKc,K&_yv^E+Pv2hA{#jΫag$^_'=*RJ\%1҆ 2-< +Zeg_iڕk24m_^;b^#+櫈v展XQjW[v"'feqR3d4Ί0Vփ5j~ϿgvW+%+{ޫmتbϰӮ/$1%6Гoy1#aWZopve{%Žֽ8b_wKmUFZVzWI:׎_Ѩܓs/+lqjUTOzM4֊jYc"cIetPv fڕY+zDg `UEEA|/ |h<֞9{N.Y1W!S= JSE=ϼeqem4gҙvu]qoWmJ4Zm/b?_]2G?4֙.yܽ!kX!XYkY)UKv0H{l*:e{q-ֵ<-kcqP,$b*{&Z]yliW7y$ot/4;hfzۋ***v䫋u3_}6+\R] 4Ʋ8+Kc4(W1OmjO[2oŜiYk2YkqV᪈х+[A ;n6ハ|uz oW7|u3՝Wڎ`i=BMXgi%= WY|-Y%UTe{yK, 8YKJ|ֺQ'U/(iWRo^Ef K w_EEE~ |u{Z;4#4,i_Cc,cYi3Uf|VYy\ZI魮{ٞEB8KҲ~6[1V4ޒx'> Lwy#liWZoPk۞}U|UQQqXo|u-ޑh@{`Ez}KӲ0%*U=6 W!O~k% ~{+YZo1K!Syweklz}]Ӯ7(bUdv00i-ϜW_],U#fك{x5,J~,K˲X+r*U#ߏJ*o~ ީv%ڹ7{{vkvw3,}U|UQQ|#w˃%>,jseiw拾Na?}#WiuVl o}[,If8 Y yK./kJҫc^+x5¾]qo^zGf(;rF5ChyxϨ6G(iXGX~ YkeIKxJc Y$^nuj!oiZV_,yvOsL[IgVA]YA{{ۥ{⫊|cTہFzX%qVn:o[/Ec/uj\%V[=2׽ +gyZV.qZ[we~_:Lf[.Qif@oP^yX*{EEŮ7Cz ¹IO&FΖI!zZYZ/y+Ѽ=o9\uK2a[ZYR3gIż(}$jgcxoP2SY\u-gv FW'zWЃ=¬'d/X/ks'imp?Ix~^$3s,rzrsf7]MzVgݵ H$,~bMJodzu*MN&؊v%5ǃ{9Œ}BbIմuu/~|<k;{Zꭖ[gaߐ{]ҳFj玉\$%??1Ciwa̟QET 6U +#նzWzd+ zUvnbUoIx) XS|;֝w[!N',/Tg[m]{Y+Y39h,&2u7Y;;(OISo;UW +QJ):7XUEE^z4G(% c{J5yKb!"i$h$ɛ{7#jٞieq -ot9#s!{y_̄܄^[,eqSR*q[If5_⫊]`e}BO1iߪè{k #O|9ºôЕ_XٞCbBB灗XKJ~I&V4vG}<%1Y5LЭs}AO|تbGiXKL|N|7tQK]4XۜV<U7UUEEŮ{4,i ًŌ~ ,,-\Gk2g {Hm^[T>5Veke9KS*tL7i%5X +se. b_]k/+'D/XΞeY z?uYͬ_ Y)}=#i?dtUq]꫕92%ikIgI:k4v{Qu)d)$pU=AFjW_UTT|JOh1 +\N5j5 ks>Mg{GOevy\uV_l!o]p֕<4ZzyK3YHw]mhQV,,{YdتiV[WñjE>!l^acK-;}RMF3l +jV琷rUg+vGdO1qI {$"CbzVjveU{UTTTBҰ' c5js/lXki<SV^Z-ͣgo嫲z@XS[][keWci\1;<^bfIc'M6ꪗ ֊hZxK[O'x)N $=J(,j$[uUUEE.jXN[5;#}Y:Az9;L;f.fZKkguC"ԣAI8N) 44+mNcQvWQQQ1*Bb,ip gYims3tK$#Yˌa% JVSdbbDBֲXZ;HFXIK$ cnO~g-MK-%ΑR{f +EyQﵸ* *؏ZeqYGV$AI&[מay0KzI=F~VWՋe1gIXP`m;9YS2V$}n|1RtdtJg*k6[UTTTl#[ZYka=K/o6>g}3dQt22Kpj'7y}^I~$jlUQQQcEΒXKR[kIFffy9 lje叏rϠᥨ7JUV=XYXR[aΞٌVgF d #eX)ꉋr5 Y*U[UTT8nKYQCb.">l-jMrtLFG=|3;E(1:Ҭ'XQQqc YQ.KSXCsGL? Q4[UTTX,<޲+akw<=ªZFyюzzz9pUUEEa^ZeYc.[sە\ZG#㭵Xz[gFyzz-R;דV䪚8(1pV[WvmF^Gh3erk3^nO +2UV5cy}h=+_ĨjykMq6Kh#!{NU[UTT8n9KYOs.Xj sKݫs0ը~`8, d刺5Q6K* dڤw=\UlUQQqXbM:&z}Vw̵Lwms=aW[UTT8n uB $]6Q]?3g:Y' `,WUO(:~P]^RS]3w=VOq.Z?ǯUV5FjYQ #kڵsykXh/>TUr3]$ZF\zٳg+|wGiV)zߓkm.^͵k?]9Pت0ǒ6tm~>|K4Բo +Q +0Ъ D$`M,=[=NYA|S\7;|Tz{έN{WzK٫T>%W͟qz9ߕګjh+ju;~ ;@gz ܁sK\יf !';$)y_v^Vݥ(+! +1 Om"! +1 Om" +àS_U * + ]q؅"S1RilA,D-6BDQRj%%*3a0UKj%d0 9yg}Vd|^R:EgRN:H{Rv#&6,k5 Η:WjRgժqR$~5z;SjONR?(GjӬi!'uT5RGJu8dJdߐT_-"?mUX]` +Yy_\o*}7Οs2ExoH㪜!k= +pEfI*Ͳ%?K6|gq/~\tZ:\k9h⼷9Wƥ|mA)ƽXXcA^WL?eUp~9WІWZr9Wd> kG9WW('_@{Jf\j跆r~A +TAO[8gpޮ=!'jߏKuOzX1Y +֘|mIa}|LnM=!*k)JMӔ=楘7\=zJӌqU$_@2Zpϔ@,_Kg*.Cc[dג{\*k+ ~;4%_ e7)f}ѯ[X[NC࢈׭pt}fVUy{&xiIk/6 /*سj?;5j2sȟoA7nf*黲MީߏN1 +sF,|CEyWI{oj:0랊YnUһ wJ'u'_sU2VKvU9GhCm"]aa9K(uWKW|Xʷ~?-WtmT/[}nBTY|9W kyW_ĸ|Waw׵8_jv_Mdf+]9WώUbfHnF+U˜TKYg/]x\=JrAzCTY|9W:PZךzjt>2_+|(Qkk|0WZ8~ҟ;?9Ws.QU sº+pk + @Mj1+g#__:rTX|9W(GZje|~J9r s!f#_Wo++%_sU dro+&_sU #CEJ~v"_'+,K+p x11Ac߯J/Uk3%듯9KJp_m䫱="_uw*KA*OpDH4:_͋g+z7If}wv? ++P: lg9_l,GCkl|a7|;>nE"_sEWɱ,0-W#kGRktY%_Tw^+8W|5%\"bW]YWzI|){>W\Us_|u_YW:Ӥ6uTx>W\Ur+Zy[35`긔l&:_ʞ8W|%=W4[߂qYW-^ZAꕬՀ8.kd+o]ϓ)y>W\X33FvX8_Ͱ\ICF}: ++B6Xn_G*\z>fm|uԄHԅR'|$_XM9W_I-z])FڿvW57lI9W\AjlQ.ճBcjJw8=_8 i kA:$־(g%_'sݢ ++@j%ff|:ZZϗW\ի0wR6 +d6ڑX|L\. +s.WuP0\. +s>V}ncg#_W/(O+%_s^󕌱efý8 +ӸrTX|9WS#?Pkq>|sº+pq2?0Uʺ+pc2|S!0#~ҟ;+piW1!/1H +dY W9'~ҟ;+pa:!Ƭ/}gl5!_ռ?|_uZf0_if}njq;W+Ѱ5nӏ|Չ|9)_)G=F--hr5V:WW3sʏ|<9WPac^UODqPyo~j]t+`guuE IDd-cAcg0_ 1z)'_u"_@8W6bF0_0:/e5^6_N_U'%_=bW9*Gq ߳H*yüwI}1b^U'1_5®5f5Tqfy5y%RgJ vo>7:żN+'FM#h}P|~r(O?]|qwP|aI9k3,e_a]8|e=j=b+>vX՟v~uW|I}h*!ܱRٌ|5ѿ;. +s&}0M7̞^9W2>뒯<+ g؇a9*94̞e|IU%_s9Ws5 2*~|S>-W]ƺN!_syWk*Wm⵶@3y߮Qޑ8[3վ5Oٺ+p.j\~dDQU 3j8zL70W3U,֥c*Y|8_mi/[/r mO,{HjfixJyUF.Rc'%cԻR/$n ٱsG>TM>~}/] 5qMK*\*ؔuvc;ӱn,c^+ZI~F=~"5BQ&ye̲Yڃ&I=*eT&C!^7HHUm֌8꿥&VXh쫶goey#^'sk=iJIMOY$&H!5 LGgkήaμ:s(Lz9WnY*RO6hpsJ}!5~gg"_@=_maAURXl~u/s +Aؖ<_ٺG'7qސy:W93C&rN4W6Wg`ܧ-w3N-Wߥ3Wȥ_H  +Uuqu#&࿅?-,VӡQX[f%k21M3E,iD@4X?L*Lр% r=<<9wޯ~\|v~@ne[=5p/k/F Yh{GsD1gN7 +5a'ygG2s|]A|?hޱTl0_=3T+_k~ܟy4U’9CE.!_Dż;+x\*Y+Yayɜ"_@EW/zV3y޻NedRPYVWY#6Vssf}ʧ +(|u1 _Q(Ye1.G95:QYPP*4e_hڒ A嫵?Y-s-_qӍujxz^Y[em=-D^(| +,_ݮ8>;|YukT2gSU'LrW?8oK` +})#_w[ +X8~!kP:dNl4yR| +,__y Ws>g:Ez A!+yG㜋똓|MYghᄂ> +`ڤ8~{3RFŜ+Me??lu}h9MsO<_T^dc I\ќ^evZZ-k򺐯*& +`sN^%1hNU5s-+HP$9OqUv/OhE˜jh_+T^$(|=_PjAU8~Wcƶϧ* +hrkaד~Б䫗d=->Sd{ǜW9?N~8f _@WV7kl*#۬1i؇&_ \]渚 +hr>UfZ~X뼡+>M,\R _@WRwK~DqϴOIA2u +hrecvM}M*#c{-K A櫣m͏uʦ c%}W:MmM +c%}WSsqimjl4e_җ| +8_i~杊cFMiBj~I_$(|uܙ>ʗW6MWl_җ| +8_}\qsq+M!_)VR| +8_is6ʆ|5 +ca9!y|._5^J|%_jxz:[7m2٭F~5_}:fΆ竳sf{c&jj@>kMs竷̌2x5WW{Y~URX㜙qӌU$_ _@W[gdaI&Csf2Xj'EM.|e꒚Wz-Meh\Q|5| _=g/7f2㬙yԞK@Eߑ%5fguMu3)f&_ _@"W fo7=_juE/z]~eO+\ju>fU>z㼝0|u>Ĝζ'V9jzk'=|u[z=ܣ/_@"WcW>l:4j߸U-qqKEnWZV]W#<tҗ| $_M1{XQ|~_mz"ͭ0[GU>A=WH~Wv `Tjǎ*)IEݠG͖E?5y|rbEa1,M|ɚ/k &Y~5@! +1 Om"! +1 Om"! +1 Om" +àS_U * \ No newline at end of file diff --git a/docs/promo_pictures/extensions.blender.org/Featured Image.jpg b/docs/promo_pictures/extensions.blender.org/Featured Image.jpg index 5dc1dfdb..0cc5aaaf 100644 Binary files a/docs/promo_pictures/extensions.blender.org/Featured Image.jpg and b/docs/promo_pictures/extensions.blender.org/Featured Image.jpg differ diff --git a/docs/promo_pictures/extensions.blender.org/Icon Image.png b/docs/promo_pictures/extensions.blender.org/Icon Image.png index 37343138..97d8047e 100644 Binary files a/docs/promo_pictures/extensions.blender.org/Icon Image.png and b/docs/promo_pictures/extensions.blender.org/Icon Image.png differ