From 46044aa2d526fc18b1f22d0c85a6fc551ae38bec Mon Sep 17 00:00:00 2001 From: Stefan Jeske Date: Wed, 20 Nov 2024 11:09:30 +0100 Subject: [PATCH 1/9] New Extension Spec Compatibility and Usability Improvements (#36) * improve recursive import by default it will now detect common prefixes and eliminate them. in the future it might be interesting to have an option to keep prefixes and simply use blenders naming to create a new one with an incremented name. * show hidden option that changes the relative root path * add blender manifest for 4.2 marketplace support * change to relative imports to not make blender angry from changing sys.path * update for new spec * change maintainer --- .gitignore | 3 +- __init__.py | 39 ++++----- blender_manifest.toml | 80 +++++++++++++++++++ bseq/__init__.py | 7 +- .../additional_file_formats}/README.md | 0 .../additional_file_formats}/__init__.py | 0 .../additional_file_formats}/bgeo.py | 0 .../additional_file_formats}/mzd.py | 0 .../additional_file_formats}/obj.py | 0 .../additional_file_formats}/table.py | 0 bseq/importer.py | 2 +- bseq/messenger.py | 6 +- bseq/operators.py | 40 ++++++++-- bseq/panels.py | 5 ++ download_wheels.sh | 4 + 15 files changed, 147 insertions(+), 39 deletions(-) create mode 100644 blender_manifest.toml rename {additional_file_formats => bseq/additional_file_formats}/README.md (100%) rename {additional_file_formats => bseq/additional_file_formats}/__init__.py (100%) rename {additional_file_formats => bseq/additional_file_formats}/bgeo.py (100%) rename {additional_file_formats => bseq/additional_file_formats}/mzd.py (100%) rename {additional_file_formats => bseq/additional_file_formats}/obj.py (100%) rename {additional_file_formats => bseq/additional_file_formats}/table.py (100%) create mode 100755 download_wheels.sh diff --git a/.gitignore b/.gitignore index 59a5874..fe9c7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__/ .DS_Store .vscode/ *.zip +*.whl # the following ignores are used to ignore the local softlink files # the extern folder won't be affected by this @@ -10,4 +11,4 @@ meshio future fileseq -docs/_build/* \ No newline at end of file +docs/_build/* diff --git a/__init__.py b/__init__.py index 97535c0..d98c67b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,35 +1,24 @@ -bl_info = { - "name": "Sequence Loader", - "description": "Loader for meshio supported mesh files/ simulation sequences", - "author": "Interactive Computer Graphics", - "version": (0, 3, 2), - "blender": (4, 0, 0), - "warning": "", - "support": "COMMUNITY", - "category": "Import-Export", -} - import bpy import os import sys -current_folder = os.path.dirname(os.path.abspath(__file__)) -if current_folder not in sys.path: - sys.path.append(current_folder) -# add paths of external libraries to sys.path -if os.path.exists(os.path.join(current_folder, "extern")): - external_libs = ["fileseq/src", "meshio/src", "python-future/src", "rich"] - for lib in external_libs: - lib_path = os.path.join(current_folder, "extern", lib) - if lib_path not in sys.path: - sys.path.append(lib_path) +# current_folder = os.path.dirname(os.path.abspath(__file__)) +# if current_folder not in sys.path: +# sys.path.append(current_folder) +# # add paths of external libraries to sys.path +# if os.path.exists(os.path.join(current_folder, "extern")): +# external_libs = ["fileseq/src", "meshio/src", "python-future/src", "rich"] +# for lib in external_libs: +# lib_path = os.path.join(current_folder, "extern", lib) +# if lib_path not in sys.path: +# sys.path.append(lib_path) -if bpy.context.preferences.filepaths.use_relative_paths == True: - bpy.context.preferences.filepaths.use_relative_paths = False +# if bpy.context.preferences.filepaths.use_relative_paths == True: +# bpy.context.preferences.filepaths.use_relative_paths = False -from bseq import * -from bseq.operators import menu_func_import, add_keymap, delete_keymap +from .bseq import * +from .bseq.operators import menu_func_import, add_keymap, delete_keymap classes = [ BSEQ_obj_property, diff --git a/blender_manifest.toml b/blender_manifest.toml new file mode 100644 index 0000000..3867861 --- /dev/null +++ b/blender_manifest.toml @@ -0,0 +1,80 @@ +schema_version = "1.0.0" + +# Example of manifest file for a Blender extension +# Change the values according to your extension +id = "sequence_loader" +version = "0.3.3" +name = "Blender Sequence Loader" +tagline = "Just-in-time loader for meshio-supported mesh file sequences" +maintainer = "Stefan Rhys Jeske " +# Supported types: "add-on", "theme" +type = "add-on" + +# # Optional: link to documentation, support, source files, etc +website = "https://github.com/InteractiveComputerGraphics/blender-sequence-loader" + +# # Optional: tag list defined by Blender and server, see: +# # https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html +tags = ["Animation", "Object"] + +blender_version_min = "4.2.0" +# # Optional: Blender version that the extension does not support, earlier versions are supported. +# # This can be omitted and defined later on the extensions platform if an issue is found. +# blender_version_max = "5.1.0" + +# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix) +# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html +license = [ + "SPDX:MIT", +] +# # Optional: required by some licenses. +# copyright = [ +# "2002-2024 Developer Name", +# "1998 Company Name", +# ] + +# # Optional: list of supported platforms. If omitted, the extension will be available in all operating systems. +# platforms = ["windows-x64", "macos-arm64", "linux-x64"] +# # Other supported platforms: "windows-arm64", "macos-x64" + +# # Optional: bundle 3rd party Python modules. +# # https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html +wheels = [ + "./wheels/Fileseq-1.15.2-py3-none-any.whl", + "./wheels/future-0.18.3-py3-none-any.whl", + "./wheels/meshio-5.3.4-py3-none-any.whl", + "./wheels/rich-13.7.0-py3-none-any.whl", +] + +# # Optional: add-ons can list which resources they will require: +# # * files (for access of any filesystem operations) +# # * network (for internet access) +# # * clipboard (to read and/or write the system clipboard) +# # * camera (to capture photos and videos) +# # * microphone (to capture audio) +# # +# # If using network, remember to also check `bpy.app.online_access` +# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access +# # +# # For each permission it is important to also specify the reason why it is required. +# # Keep this a single short sentence without a period (.) at the end. +# # For longer explanations use the documentation or detail page. +# +[permissions] +files = "Core functionality to load files from disk" + +# # Optional: advanced build settings. +# # https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build +[build] +# These are the default build excluded patterns. +# You only need to edit them if you want different options. +paths_exclude_pattern = [ + "__pycache__/", + "/.git/", + "/*.zip", + "/extern/", + "/docs/", + "/images/", + "build_addon.py", + "download_wheels.sh" +] \ No newline at end of file diff --git a/bseq/__init__.py b/bseq/__init__.py index 17cb3bf..13ca993 100644 --- a/bseq/__init__.py +++ b/bseq/__init__.py @@ -1,13 +1,14 @@ -from bseq.utils import refresh_obj +from .utils import refresh_obj from .operators import BSEQ_OT_load, BSEQ_OT_edit, BSEQ_OT_resetpt, BSEQ_OT_resetmesh, BSEQ_OT_resetins, BSEQ_OT_set_as_split_norm, BSEQ_OT_remove_split_norm, BSEQ_OT_disable_selected, BSEQ_OT_enable_selected, BSEQ_OT_refresh_seq, BSEQ_OT_disable_all, BSEQ_OT_enable_all, BSEQ_OT_refresh_sequences, BSEQ_OT_set_start_end_frames, BSEQ_OT_batch_sequences, BSEQ_PT_batch_sequences_settings, BSEQ_OT_meshio_object, BSEQ_OT_import_zip, BSEQ_OT_delete_zips, BSEQ_addon_preferences, BSEQ_OT_load_all, BSEQ_OT_load_all_recursive from .properties import BSEQ_scene_property, BSEQ_obj_property, BSEQ_mesh_property from .panels import BSEQ_UL_Obj_List, BSEQ_List_Panel, BSEQ_Settings, BSEQ_PT_Import, BSEQ_PT_Import_Child1, BSEQ_PT_Import_Child2, BSEQ_Globals_Panel, BSEQ_Advanced_Panel, BSEQ_Templates, BSEQ_UL_Att_List, draw_template from .messenger import subscribe_to_selected, unsubscribe_to_selected -import bpy -from bpy.app.handlers import persistent from .importer import update_obj from .globals import * +import bpy +from bpy.app.handlers import persistent + @persistent def BSEQ_initialize(scene): diff --git a/additional_file_formats/README.md b/bseq/additional_file_formats/README.md similarity index 100% rename from additional_file_formats/README.md rename to bseq/additional_file_formats/README.md diff --git a/additional_file_formats/__init__.py b/bseq/additional_file_formats/__init__.py similarity index 100% rename from additional_file_formats/__init__.py rename to bseq/additional_file_formats/__init__.py diff --git a/additional_file_formats/bgeo.py b/bseq/additional_file_formats/bgeo.py similarity index 100% rename from additional_file_formats/bgeo.py rename to bseq/additional_file_formats/bgeo.py diff --git a/additional_file_formats/mzd.py b/bseq/additional_file_formats/mzd.py similarity index 100% rename from additional_file_formats/mzd.py rename to bseq/additional_file_formats/mzd.py diff --git a/additional_file_formats/obj.py b/bseq/additional_file_formats/obj.py similarity index 100% rename from additional_file_formats/obj.py rename to bseq/additional_file_formats/obj.py diff --git a/additional_file_formats/table.py b/bseq/additional_file_formats/table.py similarity index 100% rename from additional_file_formats/table.py rename to bseq/additional_file_formats/table.py diff --git a/bseq/importer.py b/bseq/importer.py index 9c67c52..73b1d01 100644 --- a/bseq/importer.py +++ b/bseq/importer.py @@ -9,7 +9,7 @@ from mathutils import Matrix import time # this import is not useless -import additional_file_formats +from .additional_file_formats import * def extract_edges(cell: meshio.CellBlock): if cell.type == "line": diff --git a/bseq/messenger.py b/bseq/messenger.py index 0409779..25cceab 100644 --- a/bseq/messenger.py +++ b/bseq/messenger.py @@ -20,7 +20,8 @@ def selected_callback(): bpy.context.scene.BSEQ.edit_obj = bpy.context.active_object def subscribe_to_selected(): - import bseq + # import bseq + bseq = __loader__ # because current implementation may subscribe twice # so clear once to avoid duplication @@ -37,5 +38,6 @@ def subscribe_to_selected(): def unsubscribe_to_selected(): - import bseq + # import bseq + bseq = __loader__ bpy.msgbus.clear_by_owner(bseq) diff --git a/bseq/operators.py b/bseq/operators.py index 525f407..5028048 100644 --- a/bseq/operators.py +++ b/bseq/operators.py @@ -440,7 +440,7 @@ def draw(self, context): # layout.prop(importer_prop, "root_path", text="Root Directory") class BSEQ_addon_preferences(bpy.types.AddonPreferences): - bl_idname = addon_name + bl_idname = __package__ zips_folder: bpy.props.StringProperty( name="Zips Folder", @@ -556,6 +556,9 @@ def execute(self, context): return relative_path_error() root_dir = importer_prop.path + root_coll = bpy.context.scene.collection + root_layer_collection = bpy.context.view_layer.layer_collection + unlinked_collections = [] # Recurse through subdirectories for root, dirs, files in os.walk(bpy.path.abspath(root_dir)): for dir in sorted(dirs): @@ -570,14 +573,33 @@ def execute(self, context): coll_list = bpy.path.relpath(subdirectory, start=root_dir).strip("//").split("/") # Get or create a nested collection starting from the root - last_coll = bpy.context.scene.collection - layer_collection = bpy.context.view_layer.layer_collection + last_coll = root_coll + layer_collection = root_layer_collection for coll in coll_list: - cur_coll = bpy.data.collections.get(coll) if bpy.data.collections.get(coll) is not None else bpy.data.collections.new(coll) - if last_coll is not None and cur_coll.name not in last_coll.children: + # If it already exists and is not in the children of the last collection, then the prefix has changed + cur_coll = bpy.data.collections.get(coll) + if cur_coll is not None and last_coll is not None: + if cur_coll.name not in last_coll.children: + # Get the old parent of the existing collection and move the children to the old parent + parent = [c for c in bpy.data.collections if bpy.context.scene.user_of_id(cur_coll) and cur_coll.name in c.children] + if len(parent) > 0: + for child in cur_coll.children: + parent[0].children.link(child) + for obj in cur_coll.objects: + parent[0].objects.link(obj) + parent[0].children.unlink(cur_coll) + unlinked_collections.append(cur_coll) + else: + layer_collection = layer_collection.children[cur_coll.name] + last_coll = cur_coll + + + # If it was newly created, link it to the last collection + if cur_coll is None and last_coll is not None: + cur_coll = bpy.data.collections.new(coll) last_coll.children.link(cur_coll) - layer_collection = layer_collection.children[cur_coll.name] - last_coll = cur_coll + layer_collection = layer_collection.children[cur_coll.name] + last_coll = cur_coll # Set the last collection as the active collection by recursing through the collections context.view_layer.active_layer_collection = layer_collection @@ -587,6 +609,10 @@ def execute(self, context): for s in seqs: create_obj_wrapper(s, importer_prop) + + # Make sure unused datablocks are freed + for coll in unlinked_collections: + bpy.data.collections.remove(coll) return {'FINISHED'} diff --git a/bseq/panels.py b/bseq/panels.py index be1c142..5c9484e 100644 --- a/bseq/panels.py +++ b/bseq/panels.py @@ -241,6 +241,11 @@ def draw(self, context): col1.label(text="Relative Paths") col2.prop(importer_prop, "use_relative", text="") + if importer_prop.use_relative: + col1.label(text="Relative Root") + col2.prop(importer_prop, "root_path", text="") + + col1.label(text="Import Normals") col2.prop(importer_prop, "use_imported_normals", text="") diff --git a/download_wheels.sh b/download_wheels.sh new file mode 100755 index 0000000..1abd6b0 --- /dev/null +++ b/download_wheels.sh @@ -0,0 +1,4 @@ +pip wheel fileseq==1.15.2 -w ./wheels --no-deps +pip wheel meshio==5.3.4 -w ./wheels --no-deps +pip wheel future==0.18.3 -w ./wheels --no-deps +pip wheel rich==13.7.0 -w ./wheels --no-deps \ No newline at end of file From daedd1fec72ca532ed0fc09c1cfe3c10be942f5d Mon Sep 17 00:00:00 2001 From: Stefan Jeske Date: Wed, 20 Nov 2024 11:11:23 +0100 Subject: [PATCH 2/9] Update version in docs conf.py (#37) --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index bf923be..d81a6a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'blender-sequence-loader' copyright = '2024, InteractiveComputerGraphics' author = 'InteractiveComputerGraphics' -release = '0.3.2' +release = '0.3.3' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration From 8ad97b52156ac0a364b736b7559df25b3d7a7730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Wed, 15 Jan 2025 11:14:43 +0100 Subject: [PATCH 3/9] Add missing version check for use_auto_smooth (#39) --- bseq/importer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bseq/importer.py b/bseq/importer.py index 73b1d01..87d377a 100644 --- a/bseq/importer.py +++ b/bseq/importer.py @@ -204,7 +204,7 @@ def update_mesh(meshio_mesh, mesh): # set as split normal per vertex if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == k: - # If blender version is less than 4.1.0, then dont set auto smooth. + # If blender version is greater than 4.1.0, then don't set auto smooth. # It has been removed and normals will be used automatically if they are set. # https://developer.blender.org/docs/release_notes/4.1/python_api/#mesh if bpy.app.version < (4, 1, 0): @@ -217,8 +217,9 @@ def update_mesh(meshio_mesh, mesh): # set split normal per loop per vertex if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == k: - # Currently hard-coded for .obj files - mesh.use_auto_smooth = True + if bpy.app.version < (4, 1, 0): + mesh.use_auto_smooth = True + # currently hard-coded for .obj files indices = [item for sublist in meshio_mesh.cell_data["obj:vn_face_idx"][0] for item in sublist] mesh.normals_split_custom_set([meshio_mesh.field_data["obj:vn"][i - 1] for i in indices]) From 994b2bbd7684646331269957ee462583dd7a6d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20L=C3=B6schner?= Date: Tue, 11 Mar 2025 13:00:35 +0100 Subject: [PATCH 4/9] Fix problems with relative paths on Windows (#40) * Fix problems with relative paths on Windows * Fix updating of sequences without pattern in filename --- bseq/callback.py | 24 +++++++----- bseq/importer.py | 11 +++--- bseq/operators.py | 97 +++++++++++++++++++++++------------------------ bseq/utils.py | 30 ++++++++------- 4 files changed, 84 insertions(+), 78 deletions(-) diff --git a/bseq/callback.py b/bseq/callback.py index 6aaff62..079b702 100644 --- a/bseq/callback.py +++ b/bseq/callback.py @@ -1,30 +1,36 @@ import bpy import fileseq +import traceback + +from .utils import show_message_box # Code here are mostly about the callback/update/items functions used in properties.py file_sequences = [] def update_path(self, context): + ''' + Detects all the file sequences in the directory + ''' + # When the path has been changed, reset the selected sequence to None context.scene.BSEQ['fileseq'] = 1 context.scene.BSEQ.use_pattern = False context.scene.BSEQ.pattern = "" - - ''' - Detects all the file sequences in the directory - ''' + file_sequences.clear() p = context.scene.BSEQ.path try: - f = fileseq.findSequencesOnDisk(p) - except: - return [("None", "No sequence detected", "", 1)] + f = fileseq.findSequencesOnDisk(bpy.path.abspath(p)) + except Exception as e: + show_message_box("Error when reading path\n" + traceback.format_exc(), + "fileseq Error" + str(e), + icon="ERROR") + return None if not f: - return [("None", "No sequence detected", "", 1)] + return None - file_sequences.clear() if len(f) >= 30: file_sequences.append(("None", "Too much sequence detected, could be false detection, please use pattern below", "", 1)) else: diff --git a/bseq/importer.py b/bseq/importer.py index 87d377a..3f0f240 100644 --- a/bseq/importer.py +++ b/bseq/importer.py @@ -262,13 +262,14 @@ def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix.Identit object = bpy.data.objects.new(name, mesh) # create the object + full_path = str(fileseq) + path = os.path.dirname(full_path) + pattern = os.path.basename(full_path) if use_relative: - full_path = get_relative_path(str(fileseq), root_path) - else: - full_path = str(fileseq) + path = get_relative_path(path, root_path) # path is only the directory in which the file is located - object.BSEQ.path = os.path.dirname(full_path) - object.BSEQ.pattern = os.path.basename(full_path) + object.BSEQ.path = path + object.BSEQ.pattern = pattern object.BSEQ.current_file = filepath object.BSEQ.init = True object.BSEQ.enabled = enabled diff --git a/bseq/operators.py b/bseq/operators.py index 5028048..7168868 100644 --- a/bseq/operators.py +++ b/bseq/operators.py @@ -50,7 +50,8 @@ def execute(self, context): fs = importer_prop.path + '/' + importer_prop.pattern try: - fs = fileseq.findSequenceOnDisk(fs) + # Call os.path.abspath in addition because findSequenceOnDisk does not support \..\ components on Windows apparently + fs = fileseq.findSequenceOnDisk(os.path.abspath(bpy.path.abspath(fs))) except Exception as e: show_message_box(traceback.format_exc(), "Can't find sequence: " + str(fs), "ERROR") return {"CANCELLED"} @@ -522,7 +523,7 @@ def execute(self, context): return {'FINISHED'} class BSEQ_OT_load_all(bpy.types.Operator): - """Load all sequences from selected folder""" + """Load all sequences from selected folder and its subfolders""" bl_idname = "bseq.load_all" bl_label = "Load All" bl_options = {'PRESET', 'UNDO'} @@ -533,8 +534,8 @@ def execute(self, context): if importer_prop.use_relative and not bpy.data.is_saved: return relative_path_error() - dir = importer_prop.path - seqs = fileseq.findSequencesOnDisk(str(dir)) + p = importer_prop.path + seqs = fileseq.findSequencesOnDisk(bpy.path.abspath(p)) for s in seqs: print(s) @@ -555,60 +556,56 @@ def execute(self, context): if importer_prop.use_relative and not bpy.data.is_saved: return relative_path_error() - root_dir = importer_prop.path + root_dir = bpy.path.abspath(importer_prop.path) root_coll = bpy.context.scene.collection root_layer_collection = bpy.context.view_layer.layer_collection unlinked_collections = [] - # Recurse through subdirectories - for root, dirs, files in os.walk(bpy.path.abspath(root_dir)): - for dir in sorted(dirs): - # Process subdirectory - subdirectory = os.path.join(root, dir) - - seqs = fileseq.findSequencesOnDisk(subdirectory) - if len(seqs) == 0: - continue - - # Get list of directories from the root_dir to the current subdirectory - coll_list = bpy.path.relpath(subdirectory, start=root_dir).strip("//").split("/") - - # Get or create a nested collection starting from the root - last_coll = root_coll - layer_collection = root_layer_collection - for coll in coll_list: - # If it already exists and is not in the children of the last collection, then the prefix has changed - cur_coll = bpy.data.collections.get(coll) - if cur_coll is not None and last_coll is not None: - if cur_coll.name not in last_coll.children: - # Get the old parent of the existing collection and move the children to the old parent - parent = [c for c in bpy.data.collections if bpy.context.scene.user_of_id(cur_coll) and cur_coll.name in c.children] - if len(parent) > 0: - for child in cur_coll.children: - parent[0].children.link(child) - for obj in cur_coll.objects: - parent[0].objects.link(obj) - parent[0].children.unlink(cur_coll) - unlinked_collections.append(cur_coll) - else: - layer_collection = layer_collection.children[cur_coll.name] - last_coll = cur_coll - - - # If it was newly created, link it to the last collection - if cur_coll is None and last_coll is not None: - cur_coll = bpy.data.collections.new(coll) - last_coll.children.link(cur_coll) + # Recurse through directory itself and subdirectories + for current_dir, subdirs, files in os.walk(root_dir): + seqs = fileseq.findSequencesOnDisk(current_dir) + if len(seqs) == 0: + continue + + # Get list of directories from the root_dir to the current directory + coll_list = bpy.path.relpath(current_dir, start=root_dir).strip("//").split("/") + + # Get or create a nested collection starting from the root + last_coll = root_coll + layer_collection = root_layer_collection + for coll in coll_list: + # If it already exists and is not in the children of the last collection, then the prefix has changed + cur_coll = bpy.data.collections.get(coll) + if cur_coll is not None and last_coll is not None: + if cur_coll.name not in last_coll.children: + # Get the old parent of the existing collection and move the children to the old parent + parent = [c for c in bpy.data.collections if bpy.context.scene.user_of_id(cur_coll) and cur_coll.name in c.children] + if len(parent) > 0: + for child in cur_coll.children: + parent[0].children.link(child) + for obj in cur_coll.objects: + parent[0].objects.link(obj) + parent[0].children.unlink(cur_coll) + unlinked_collections.append(cur_coll) + else: layer_collection = layer_collection.children[cur_coll.name] last_coll = cur_coll - # Set the last collection as the active collection by recursing through the collections - context.view_layer.active_layer_collection = layer_collection - # for s in seqs: - # print(s) + # If it was newly created, link it to the last collection + if cur_coll is None and last_coll is not None: + cur_coll = bpy.data.collections.new(coll) + last_coll.children.link(cur_coll) + layer_collection = layer_collection.children[cur_coll.name] + last_coll = cur_coll + + # Set the last collection as the active collection by recursing through the collections + context.view_layer.active_layer_collection = layer_collection + + # for s in seqs: + # print(s) - for s in seqs: - create_obj_wrapper(s, importer_prop) + for s in seqs: + create_obj_wrapper(s, importer_prop) # Make sure unused datablocks are freed for coll in unlinked_collections: diff --git a/bseq/utils.py b/bseq/utils.py index 22f5ad2..161542c 100644 --- a/bseq/utils.py +++ b/bseq/utils.py @@ -21,7 +21,6 @@ def draw(self, context): stop_animation() bpy.context.window_manager.popup_menu(draw, title=title, icon=icon) - def stop_animation(): if bpy.context.screen.is_animation_playing: # if playing animation, then stop it, otherwise it will keep showing message box @@ -29,17 +28,18 @@ def stop_animation(): def get_relative_path(path, root_path): if root_path != "": - path = bpy.path.relpath(path, start=root_path) + rel_path = bpy.path.relpath(path, start=bpy.path.abspath(root_path)) else: - path = bpy.path.relpath(path) - return path + rel_path = bpy.path.relpath(path) + return rel_path # convert relative path to absolute path def convert_to_absolute_path(path, root_path): + # Additional call to os.path.abspath removes any "/../"" in the path (can be a problem on Windows) if root_path != "": - path = bpy.path.abspath(path, start=root_path) + path = os.path.abspath(bpy.path.abspath(path, start=bpy.path.abspath(root_path))) else: - path = bpy.path.abspath(path) + path = os.path.abspath(bpy.path.abspath(path)) return path def get_absolute_path(obj, scene): @@ -47,19 +47,21 @@ def get_absolute_path(obj, scene): full_path = convert_to_absolute_path(full_path, scene.BSEQ.root_path) return full_path - def refresh_obj(obj, scene): is_relative = obj.BSEQ.path.startswith("//") - print("is_relative: ", is_relative) fs = get_absolute_path(obj, scene) fs = fileseq.findSequenceOnDisk(fs) - fs = fileseq.findSequenceOnDisk(fs.dirname() + fs.basename() + "@" + fs.extension()) - obj.BSEQ.start_end_frame = (fs.start(), fs.end()) - fs = str(fs) + #fs = fileseq.findSequenceOnDisk(fs.dirname() + fs.basename() + "@" + fs.extension()) + + full_path = str(fs) + path = os.path.dirname(full_path) + pattern = os.path.basename(full_path) if is_relative: - fs = get_relative_path(fs, scene.BSEQ.root_path) - obj.BSEQ.path = os.path.dirname(fs) - obj.BSEQ.pattern = os.path.basename(fs) + path = get_relative_path(path, scene.BSEQ.root_path) + + obj.BSEQ.path = path + obj.BSEQ.pattern = pattern + obj.BSEQ.start_end_frame = (fs.start(), fs.end()) def load_meshio_from_path(fileseq, filepath, obj = None): try: From 4f8bf38171a4103b56f175eafb4cb4d1848e241a Mon Sep 17 00:00:00 2001 From: Stefan Jeske Date: Tue, 11 Mar 2025 13:05:26 +0100 Subject: [PATCH 5/9] logic error when the mesh does not contain any cells (#41) --- bseq/importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bseq/importer.py b/bseq/importer.py index 3f0f240..523b6f7 100644 --- a/bseq/importer.py +++ b/bseq/importer.py @@ -162,7 +162,7 @@ def update_mesh(meshio_mesh, mesh): else: mesh.clear_geometry() mesh.vertices.add(n_verts) - mesh.edges.add(len(edge_data)) + mesh.edges.add(len(edges)) mesh.loops.add(n_loop) mesh.polygons.add(n_poly) From e0eed7d6f4376486c8ea284fb07fa4aea1b5cfda Mon Sep 17 00:00:00 2001 From: Stefan Jeske Date: Tue, 11 Mar 2025 13:25:17 +0100 Subject: [PATCH 6/9] prepare release (#42) --- blender_manifest.toml | 2 +- docs/conf.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/blender_manifest.toml b/blender_manifest.toml index 3867861..92c19ec 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "sequence_loader" -version = "0.3.3" +version = "0.3.4" name = "Blender Sequence Loader" tagline = "Just-in-time loader for meshio-supported mesh file sequences" maintainer = "Stefan Rhys Jeske " diff --git a/docs/conf.py b/docs/conf.py index d81a6a8..d4e3eb7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,9 +7,9 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'blender-sequence-loader' -copyright = '2024, InteractiveComputerGraphics' +copyright = '2025, InteractiveComputerGraphics' author = 'InteractiveComputerGraphics' -release = '0.3.3' +release = '0.3.4' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration From 8448b8cd319e788bebbba8a57107f604bc34912f Mon Sep 17 00:00:00 2001 From: Stefan Jeske Date: Wed, 10 Sep 2025 13:14:26 +0200 Subject: [PATCH 7/9] add the main frame change handler to always insert at 0 this allows addons to depend on the data provided by the sequence loader --- README.md | 42 +++++++++++++++++++++++++++++------------- blender_manifest.toml | 2 +- bseq/__init__.py | 4 +++- bseq/panels.py | 1 - docs/conf.py | 2 +- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4c12caa..d2f2e77 100644 --- a/README.md +++ b/README.md @@ -46,17 +46,32 @@ All data is loaded *just-in-time* when the Blender frame changes, in order to av - [1.2 Install Addon](#12-install-addon) - [1.3 FAQs](#13-faqs) - [2. How to use](#2-how-to-use) - - [2. Load the animation sequence you want](#2-load-the-animation-sequence-you-want) - - [2.1 Absolute vs. Relative Paths](#21-absolute-vs-relative-paths) - - [2.2 Sequence List View](#22-sequence-list-view) - - [2.2.1 Enable/ Disable](#221-enable-disable) - - [2.2.1 Refresh Sequence](#221-refresh-sequence) - - [2.3 Settings](#23-settings) - - [2.3.1 Geometry Nodes](#231-geometry-nodes) - - [2.3.2 Path Information](#232-path-information) - - [2.3.3 Attributes Settings](#233-attributes-settings) - - [2.3.4 Split Norm per Vertex](#234-split-norm-per-vertex) - - [2.3.5 Advanced Settings](#235-advanced-settings) + - [1. Load the animation sequence you want](#1-load-the-animation-sequence-you-want) + - [1.1 Relative Paths](#11-relative-paths) + - [1.2 Import Default Normals](#12-import-default-normals) + - [1.3 Custom Transformation Matrix](#13-custom-transformation-matrix) + - [1.4 Load sequences from folder (Legacy importer)](#14-load-sequences-from-folder-legacy-importer) + - [2. Global Settings](#2-global-settings) + - [2.1 Root Directory](#21-root-directory) + - [2.2 Print Sequence Information](#22-print-sequence-information) + - [2.3 Auto Refresh Active Sequences](#23-auto-refresh-active-sequences) + - [2.4 Auto Refresh All Sequences](#24-auto-refresh-all-sequences) + - [3. Sequence List View](#3-sequence-list-view) + - [3.1 Activate / Deactivate Sequences](#31-activate--deactivate-sequences) + - [3.2 Refresh Sequence](#32-refresh-sequence) + - [3.3 Activate / Deactivate All](#33-activate--deactivate-all) + - [3.4 Set Timeline](#34-set-timeline) + - [4. Sequence Properties](#4-sequence-properties) + - [4.1 Match Blender Frame Numbers](#41-match-blender-frame-numbers) + - [4.2 Path](#42-path) + - [4.3 Pattern](#43-pattern) + - [4.4 Current File](#44-current-file) + - [4.5 Last Loading Time](#45-last-loading-time) + - [4.6 Attributes Settings](#46-attributes-settings) + - [4.6.1 Split Norm per Vertex](#461-split-norm-per-vertex) + - [5. Advanced Settings](#5-advanced-settings) + - [5.1 Script](#51-script) + - [5.2 Geometry Nodes](#52-geometry-nodes) ## 1. Installation @@ -70,8 +85,9 @@ git clone https://github.com/InteractiveComputerGraphics/blender-sequence-loader 2. Build the installable `.zip` file by simply running the following command. This should produce a file called `blender_sequence_loader_{date}.zip`, where `{date}` is replaced with todays date. No other dependency other than standard python3 libraries are needed to build the addon. -```shell -python3 build_addon.py +```sh +./download_wheels.sh +blender --command extension build ``` ### 1.2 Install Addon diff --git a/blender_manifest.toml b/blender_manifest.toml index 92c19ec..0e265aa 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "sequence_loader" -version = "0.3.4" +version = "0.3.5" name = "Blender Sequence Loader" tagline = "Just-in-time loader for meshio-supported mesh file sequences" maintainer = "Stefan Rhys Jeske " diff --git a/bseq/__init__.py b/bseq/__init__.py index 13ca993..08a8e37 100644 --- a/bseq/__init__.py +++ b/bseq/__init__.py @@ -13,7 +13,9 @@ @persistent def BSEQ_initialize(scene): if update_obj not in bpy.app.handlers.frame_change_post: - bpy.app.handlers.frame_change_post.append(update_obj) + # Insert at the beginning, so that it runs before other frame change handlers. + # The other handlers don't need to be in the first position. + bpy.app.handlers.frame_change_post.insert(0, update_obj) if auto_refresh_active not in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.append(auto_refresh_active) if auto_refresh_all not in bpy.app.handlers.frame_change_post: diff --git a/bseq/panels.py b/bseq/panels.py index 5c9484e..c70cdd2 100644 --- a/bseq/panels.py +++ b/bseq/panels.py @@ -1,7 +1,6 @@ import bpy import os - class BSEQ_UL_Obj_List(bpy.types.UIList): ''' This controls the list of imported sequences. diff --git a/docs/conf.py b/docs/conf.py index d4e3eb7..3069cf1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'blender-sequence-loader' copyright = '2025, InteractiveComputerGraphics' author = 'InteractiveComputerGraphics' -release = '0.3.4' +release = '0.3.5' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration From d50381dc3ecfc8c0b0ede55ff518c0e371987809 Mon Sep 17 00:00:00 2001 From: Stefan Jeske Date: Thu, 18 Sep 2025 13:45:11 +0200 Subject: [PATCH 8/9] add property option flags --- bseq/properties.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bseq/properties.py b/bseq/properties.py index c99903e..dd93647 100644 --- a/bseq/properties.py +++ b/bseq/properties.py @@ -7,6 +7,7 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): subtype="DIR_PATH", description="You need to go to the folder with the sequence, then click \"Accept\"", update=update_path, + options={'PATH_SUPPORTS_BLEND_RELATIVE' if bpy.app.version >= (4, 5, 0) else ''} ) use_relative: bpy.props.BoolProperty(name='Relative Paths', @@ -24,6 +25,7 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): description="Select root folder for all relative paths. If empty, root is folder of the Blender file", update=update_path, default="", + options={'PATH_SUPPORTS_BLEND_RELATIVE' if bpy.app.version >= (4, 5, 0) else ''} ) fileseq: bpy.props.EnumProperty( @@ -120,7 +122,7 @@ class BSEQ_obj_property(bpy.types.PropertyGroup): description="If deactivated, sequence won't be updated each frame") use_advance: bpy.props.BoolProperty(default=False) script_name: bpy.props.StringProperty(name="Script name") - path: bpy.props.StringProperty(name="Path of sequence", subtype="DIR_PATH") + path: bpy.props.StringProperty(name="Path of sequence", subtype="DIR_PATH", options={'PATH_SUPPORTS_BLEND_RELATIVE' if bpy.app.version >= (4, 5, 0) else ''}) pattern: bpy.props.StringProperty(name="Pattern of sequence") current_file: bpy.props.StringProperty(description="File of sequence that is currently loaded") frame: bpy.props.IntProperty(name="Frame") From 2f04623a41e3ceb6b625622f65b0a17f74fd9044 Mon Sep 17 00:00:00 2001 From: Stefan Jeske Date: Thu, 18 Sep 2025 13:50:02 +0200 Subject: [PATCH 9/9] bump versions --- blender_manifest.toml | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/blender_manifest.toml b/blender_manifest.toml index 0e265aa..b54ebff 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" # Example of manifest file for a Blender extension # Change the values according to your extension id = "sequence_loader" -version = "0.3.5" +version = "0.3.6" name = "Blender Sequence Loader" tagline = "Just-in-time loader for meshio-supported mesh file sequences" maintainer = "Stefan Rhys Jeske " diff --git a/docs/conf.py b/docs/conf.py index 3069cf1..cedad4d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'blender-sequence-loader' copyright = '2025, InteractiveComputerGraphics' author = 'InteractiveComputerGraphics' -release = '0.3.5' +release = '0.3.6' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration