From 066c431ad6476ee69e5cee465fbef6246fd579ee Mon Sep 17 00:00:00 2001 From: justo46 <63810219+justo46@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:11:46 +0100 Subject: [PATCH] Updated GUI (#26) * Added times of last load * obj files now load with meshio loader * create_meshio_obj now uses default blender obj importer * create_meshio_obj now only checks for the ending * Update (not working atm) * Current not wokring version (but mzd is now fixed) * Better (not working) obj loader * Some debugging lines * Added functionality to use from_pydata and objloader now works * Minor updates (with old mesh loading) * Updated submodule extern/fileseq * Updated submodule extern/meshio * Updated submodule extern/python-future * Updated submodule extern/rich * Renamed symbol * Added files of submodules * A lot of UI changes * Revert "Revert "A lot of UI changes"" This reverts commit 34d179c9f0205f547db932b1fb7d71fd0dbb9b35. * Fixed obj. loading duplicate bug * Purges all old meshes (during .obj sequences) * Changed class names to fix warnings * Uncommented seperate obj import * Uncommented seperate obj import * Update meshio * Small fix * Added compatibility for split vertex normals with objs * Added obj.py * Default normals now work with .obj and .vtk * Added import zip operator * Import + Delete Zips works now * Lowered number of shown seqs, because it's not the main function anymore * Imported extracted zip folders are stored in tmp_zips in a custom location and this whole folder is then deleted * Made addon ready for release * Updated submodule extern/fileseq * Updated submodule extern/meshio * Updated submodule extern/python-future * Updated submodule extern/rich * Minor GUI adjustments --- __init__.py | 27 ++- additional_file_formats/__init__.py | 3 +- additional_file_formats/mzd.py | 3 +- additional_file_formats/obj.py | 135 +++++++++++ bseq/__init__.py | 22 +- bseq/callback.py | 2 +- bseq/globals.py | 21 +- bseq/importer.py | 128 +++++++---- bseq/messenger.py | 5 +- bseq/operators.py | 339 +++++++++++++++++++++++----- bseq/panels.py | 308 +++++++++++++++---------- bseq/properties.py | 108 ++++++--- bseq/utils.py | 40 ++-- extern/fileseq | 2 +- extern/meshio | 2 +- extern/python-future | 2 +- extern/rich | 2 +- 17 files changed, 849 insertions(+), 300 deletions(-) create mode 100644 additional_file_formats/obj.py diff --git a/__init__.py b/__init__.py index 2b0824c..6fee449 100644 --- a/__init__.py +++ b/__init__.py @@ -2,8 +2,8 @@ "name": "Sequence Loader", "description": "Loader for meshio supported mesh files/ simulation sequences", "author": "Interactive Computer Graphics", - "version": (0, 1, 5), - "blender": (3, 4, 0), + "version": (0, 2, 0), + "blender": (3, 6, 0), "warning": "", "support": "COMMUNITY", "category": "Import-Export", @@ -21,7 +21,7 @@ bpy.context.preferences.filepaths.use_relative_paths = False from bseq import * -from bseq.operators import menu_func_import +from bseq.operators import menu_func_import, add_keymap, delete_keymap classes = [ BSEQ_obj_property, @@ -32,10 +32,14 @@ BSEQ_OT_resetpt, BSEQ_OT_resetins, BSEQ_OT_resetmesh, - BSEQ_Import, + BSEQ_PT_Import, + BSEQ_PT_Import_Child1, + BSEQ_PT_Import_Child2, + BSEQ_Globals_Panel, BSEQ_List_Panel, BSEQ_UL_Obj_List, BSEQ_Settings, + BSEQ_Advanced_Panel, BSEQ_Templates, BSEQ_UL_Att_List, BSEQ_OT_set_as_split_norm, @@ -47,11 +51,15 @@ BSEQ_OT_enable_all, BSEQ_OT_refresh_sequences, BSEQ_OT_set_start_end_frames, - WM_OT_batchSequences, - WM_OT_MeshioObject + 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 ] - def register(): bpy.app.handlers.load_post.append(BSEQ_initialize) for cls in classes: @@ -60,8 +68,8 @@ def register(): bpy.types.Scene.BSEQ = bpy.props.PointerProperty(type=BSEQ_scene_property) bpy.types.Object.BSEQ = bpy.props.PointerProperty(type=BSEQ_obj_property) bpy.types.Mesh.BSEQ = bpy.props.PointerProperty(type=BSEQ_mesh_property) - bpy.types.TOPBAR_MT_file_import.append(menu_func_import) + add_keymap() # manually call this function once # so when addon being installed, it can run correctly @@ -76,10 +84,9 @@ def unregister(): del bpy.types.Object.BSEQ bpy.app.handlers.load_post.remove(BSEQ_initialize) bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) + delete_keymap() unsubscribe_to_selected() - if __name__ == "__main__": - # unregister() register() diff --git a/additional_file_formats/__init__.py b/additional_file_formats/__init__.py index 4d0b7e7..66f512f 100644 --- a/additional_file_formats/__init__.py +++ b/additional_file_formats/__init__.py @@ -1,2 +1,3 @@ from . import bgeo -from . import mzd \ No newline at end of file +from . import mzd +from . import obj \ No newline at end of file diff --git a/additional_file_formats/mzd.py b/additional_file_formats/mzd.py index 23a0029..af4ff3c 100644 --- a/additional_file_formats/mzd.py +++ b/additional_file_formats/mzd.py @@ -22,6 +22,7 @@ def readMZD_to_meshio(filepath): with open(filepath, 'rb') as file: byte = file.read(24) + # check if mzd file is empty if byte != head: return -4 while 1: @@ -49,7 +50,7 @@ def readMZD_to_meshio(filepath): if out_numVertices < 0: return -127 if out_numVertices == 0: - break + return meshio.Mesh(points=np.array([]), cells={}) byte = file.read(12 * out_numVertices) out_vertPositions = np.frombuffer(byte, dtype=np.float32) diff --git a/additional_file_formats/obj.py b/additional_file_formats/obj.py new file mode 100644 index 0000000..57437b3 --- /dev/null +++ b/additional_file_formats/obj.py @@ -0,0 +1,135 @@ +""" +I/O for the Wavefront .obj file format, cf. +. +""" +import datetime + +import numpy as np + +# from .._files import open_file +# from .._helpers import register_format +# from .._mesh import CellBlock, Mesh + +import meshio + + +def read(filename): + with open(filename, "r") as f: + mesh = read_buffer(f) + return mesh + + +def read_buffer(f): + points = [] + vertex_normals = [] + texture_coords = [] + face_groups = [] + face_normals = [] + face_texture_coords = [] + face_group_ids = [] + face_group_id = -1 + while True: + line = f.readline() + + if not line: + # EOF + break + + strip = line.strip() + + if len(strip) == 0 or strip[0] == "#": + continue + + split = strip.split() + + if split[0] == "v": + points.append([float(item) for item in split[1:]]) + elif split[0] == "vn": + vertex_normals.append([float(item) for item in split[1:]]) + elif split[0] == "vt": + texture_coords.append([float(item) for item in split[1:]]) + elif split[0] == "s": + # "s 1" or "s off" controls smooth shading + pass + elif split[0] == "f": + # old: dat = [int(item.split("/")[0]) for item in split[1:]] + # A face in obj has one of the following formats: 1, 1/2, 1//3, 1/2/3 + # We want to support all formats now amd store the texture and normal indices in other arrays + face_indices = [] + face_texture_indices = [] + face_normal_indices = [] + + for item in split[1:]: + indices = item.split("/") + face_indices.append(int(indices[0])) + if len(indices) > 1 and indices[1] != "": + face_texture_indices.append(int(indices[1])) + if len(indices) > 2: + face_normal_indices.append(int(indices[2])) + + if len(face_groups) == 0 or ( + len(face_groups[-1]) > 0 and len(face_groups[-1][-1]) != len(face_indices) + ): + face_groups.append([]) + face_group_ids.append([]) + face_texture_coords.append([]) + face_normals.append([]) + face_groups[-1].append(face_indices) + face_group_ids[-1].append(face_group_id) + if face_texture_indices: + face_texture_coords[-1].append(face_texture_indices) + if face_normal_indices: + face_normals[-1].append(face_normal_indices) + elif split[0] == "g": + # new group + face_groups.append([]) + face_group_ids.append([]) + face_texture_coords.append([]) + face_normals.append([]) + face_group_id += 1 + else: + # who knows + pass + + # There may be empty groups, too. + # Remove them. + face_groups = [f for f in face_groups if len(f) > 0] + face_group_ids = [g for g in face_group_ids if len(g) > 0] + face_normals = [n for n in face_normals if len(n) > 0] + face_texture_coords = [t for t in face_texture_coords if len(t) > 0] + + # convert to numpy arrays and remove + points = np.array(points) + face_groups = [np.array(f) for f in face_groups] + texture_coords = [np.array(t) for t in texture_coords] + vertex_normals = [np.array(n) for n in vertex_normals] + point_data = {} + cell_data = {} + field_data = {} + + if face_texture_coords and len(texture_coords) == max([max(max(face)) for face in face_texture_coords]): + field_data["obj:vt"] = texture_coords + cell_data["obj:vt_face_idx"] = face_texture_coords + elif len(texture_coords) == len(points): + point_data["obj:vt"] = texture_coords + + if face_normals and len(vertex_normals) == max([max(max(face)) for face in face_normals]): + field_data["obj:vn"] = vertex_normals + cell_data["obj:vn_face_idx"] = face_normals + elif len(vertex_normals) == len(points): + point_data["obj:vn"] = vertex_normals + + cell_data["obj:group_ids"] = [] + cells = [] + for f, gid in zip(face_groups, face_group_ids): + if f.shape[1] == 3: + cells.append(meshio.CellBlock("triangle", f - 1)) + elif f.shape[1] == 4: + cells.append(meshio.CellBlock("quad", f - 1)) + else: + cells.append(meshio.CellBlock("polygon", f - 1)) + cell_data["obj:group_ids"].append(gid) + + return meshio.Mesh(points, cells, point_data=point_data, cell_data=cell_data, field_data=field_data) + +meshio.register_format("obj", [".obj"], read, {"obj": None}) diff --git a/bseq/__init__.py b/bseq/__init__.py index d039423..864fe88 100644 --- a/bseq/__init__.py +++ b/bseq/__init__.py @@ -1,7 +1,7 @@ from bseq.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, WM_OT_batchSequences, WM_OT_MeshioObject +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 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_Import, BSEQ_Templates, BSEQ_UL_Att_List, draw_template +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 @@ -12,7 +12,8 @@ @persistent def BSEQ_initialize(scene): if update_obj not in bpy.app.handlers.frame_change_post: - bpy.app.handlers.frame_change_post.append(auto_refresh) + bpy.app.handlers.frame_change_post.append(auto_refresh_active) + bpy.app.handlers.frame_change_post.append(auto_refresh_all) bpy.app.handlers.frame_change_post.append(update_obj) subscribe_to_selected() if print_information not in bpy.app.handlers.render_init: @@ -24,12 +25,16 @@ def BSEQ_initialize(scene): "BSEQ_OT_load", "BSEQ_obj_property", "BSEQ_initialize", - "BSEQ_Import", + "BSEQ_PT_Import", + "BSEQ_PT_Import_Child1", + "BSEQ_PT_Import_Child2", + "BSEQ_Globals_Panel", "BSEQ_List_Panel", "BSEQ_UL_Obj_List", "BSEQ_scene_property", "BSEQ_Templates", "BSEQ_Settings", + "BSEQ_Advanced_Panel", "BSEQ_UL_Att_List", "subscribe_to_selected", "BSEQ_OT_resetpt", @@ -47,6 +52,11 @@ def BSEQ_initialize(scene): "BSEQ_OT_enable_all", "BSEQ_OT_refresh_sequences", "BSEQ_OT_set_start_end_frames", - "WM_OT_batchSequences", - "WM_OT_MeshioObject" + "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" ] diff --git a/bseq/callback.py b/bseq/callback.py index e89bb6b..6aaff62 100644 --- a/bseq/callback.py +++ b/bseq/callback.py @@ -25,7 +25,7 @@ def update_path(self, context): return [("None", "No sequence detected", "", 1)] file_sequences.clear() - if len(f) >= 20: + if len(f) >= 30: file_sequences.append(("None", "Too much sequence detected, could be false detection, please use pattern below", "", 1)) else: count = 1 diff --git a/bseq/globals.py b/bseq/globals.py index e875ff0..5536bfe 100644 --- a/bseq/globals.py +++ b/bseq/globals.py @@ -23,13 +23,26 @@ def print_information(scene): if bseq_prop.init: file.write("Object name: {}\n".format(obj.name)) file.write("Is it being animated: {}\n".format(bseq_prop.enabled)) - file.write("Filepath: {}\n".format(bseq_prop.pattern)) - file.write("Is it relative path: {}\n".format(bseq_prop.use_relative)) + file.write("Filepath: {}\n".format(bseq_prop.path)) + file.write("Pattern: {}\n".format(bseq_prop.pattern)) + file.write("Current file: {}\n".format(bseq_prop.current_file)) file.write("\n\n") -def auto_refresh(scene, depsgraph=None): - if not bpy.context.scene.BSEQ.auto_refresh: +def auto_refresh_all(scene, depsgraph=None): + if not bpy.context.scene.BSEQ.auto_refresh_all: + return + for obj in bpy.data.objects: + if obj.BSEQ.init == False: + continue + if obj.BSEQ.enabled == False: + continue + if obj.mode != "OBJECT": + continue + refresh_obj(obj, scene) + +def auto_refresh_active(scene, depsgraph=None): + if not bpy.context.scene.BSEQ.auto_refresh_active: return for obj in bpy.data.objects: if obj.BSEQ.init == False: diff --git a/bseq/importer.py b/bseq/importer.py index 71b8c60..6e2e5cd 100644 --- a/bseq/importer.py +++ b/bseq/importer.py @@ -4,7 +4,7 @@ import traceback import fileseq import os -from .utils import show_message_box +from .utils import show_message_box, get_relative_path, get_absolute_path import numpy as np from mathutils import Matrix import time @@ -80,12 +80,38 @@ def apply_transformation(meshio_mesh, obj, depsgraph): # evaluate the rigid body transformations (only relevant for .bin format) rigid_body_transformation = mathutils.Matrix.Identity(4) - if meshio_mesh.field_data.get("transformation_matrix") is not None: - rigid_body_transformation = meshio_mesh.field_data["transformation_matrix"] + if meshio_mesh is not None: + if "transformation_matrix" in meshio_mesh.field_data: + rigid_body_transformation = meshio_mesh.field_data["transformation_matrix"] # multiply everything together (with custom transform matrix) obj.matrix_world = rigid_body_transformation @ eval_transform_matrix +# function to create a single custom Blender mesh attribute +def create_or_retrieve_attribute(mesh, k, v): + if k not in mesh.attributes: + if len(v) == 0: + return mesh.attributes.new(k, "FLOAT", "POINT") + if len(v.shape) == 1: + # one dimensional attribute + return mesh.attributes.new(k, "FLOAT", "POINT") + if len(v.shape) == 2: + dim = v.shape[1] + if dim > 3: + show_message_box('higher than 3 dimensional attribue, ignored') + return None + if dim == 1: + return mesh.attributes.new(k, "FLOAT", "POINT") + if dim == 2: + return mesh.attributes.new(k, "FLOAT2", "POINT") + if dim == 3: + return mesh.attributes.new(k, "FLOAT_VECTOR", "POINT") + if len(v.shape) > 2: + show_message_box('more than 2 dimensional tensor, ignored') + return None + else: + return mesh.attributes[k] + def update_mesh(meshio_mesh, mesh): # extract information from the meshio mesh mesh_vertices = meshio_mesh.points @@ -94,6 +120,9 @@ def update_mesh(meshio_mesh, mesh): n_loop = 0 n_verts = len(mesh_vertices) if n_verts == 0: + mesh.clear_geometry() + mesh.update() + mesh.validate() return faces_loop_start = np.array([], dtype=np.uint64) faces_loop_total = np.array([], dtype=np.uint64) @@ -115,7 +144,7 @@ def update_mesh(meshio_mesh, mesh): # Add a zero as first entry faces_loop_start = np.roll(faces_loop_start, 1) faces_loop_start[0] = 0 - + if len(mesh.vertices) == n_verts and len(mesh.polygons) == n_poly and len(mesh.loops) == n_loop: pass else: @@ -130,34 +159,25 @@ def update_mesh(meshio_mesh, mesh): mesh.polygons.foreach_set("loop_total", faces_loop_total) mesh.polygons.foreach_set("use_smooth", [shade_scheme] * len(faces_loop_total)) + # newer function but is about 4 times slower + # mesh.clear_geometry() + # mesh.from_pydata(mesh_vertices, [], data) + mesh.update() mesh.validate() + if bpy.context.scene.BSEQ.use_imported_normals: + if "obj:vn" in meshio_mesh.point_data: + mesh.BSEQ.split_norm_att_name = "bseq_obj:vn" + elif "normals" in meshio_mesh.point_data and len(meshio_mesh.point_data["normals"]) == len(mesh.vertices): + mesh.BSEQ.split_norm_att_name = "bseq_normals" + elif "obj:vn" in meshio_mesh.field_data and "obj:vn_face_idx" in meshio_mesh.cell_data: + mesh.BSEQ.split_norm_att_name = "obj:vn" + # copy attributes - attributes = mesh.attributes for k, v in meshio_mesh.point_data.items(): k = "bseq_" + k - attribute = None - if k not in attributes: - if len(v.shape) == 1: - # one dimensional attribute - attribute = mesh.attributes.new(k, "FLOAT", "POINT") - if len(v.shape) == 2: - dim = v.shape[1] - if dim > 3: - show_message_box('higher than 3 dimensional attribue, ignored') - continue - if dim == 1: - attribute = mesh.attributes.new(k, "FLOAT", "POINT") - if dim == 2: - attribute = mesh.attributes.new(k, "FLOAT2", "POINT") - if dim == 3: - attribute = mesh.attributes.new(k, "FLOAT_VECTOR", "POINT") - if len(v.shape) > 2: - show_message_box('more than 2 dimensional tensor, ignored') - continue - else: - attribute = attributes[k] + attribute = create_or_retrieve_attribute(mesh, k, v) name_string = None if attribute.data_type == "FLOAT": name_string = "value" @@ -166,11 +186,22 @@ def update_mesh(meshio_mesh, mesh): attribute.data.foreach_set(name_string, v.ravel()) - # set as split norm + # set as split normal per vertex if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == k: mesh.use_auto_smooth = True mesh.normals_split_custom_set_from_vertices(v) + for k, v in meshio_mesh.field_data.items(): + if k not in mesh.attributes: + attribute = create_or_retrieve_attribute(mesh, k, []) + + # 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 + 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]) + # function to create a single meshio object def create_meshio_obj(filepath): meshio_mesh = None @@ -180,7 +211,7 @@ def create_meshio_obj(filepath): show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), "Meshio Loading Error" + str(e), icon="ERROR") - + return # create the object name = os.path.basename(filepath) mesh = bpy.data.meshes.new(name) @@ -190,8 +221,7 @@ def create_meshio_obj(filepath): bpy.ops.object.select_all(action="DESELECT") bpy.context.view_layer.objects.active = object - -def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]])): +def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix.Identity(4)): current_frame = bpy.context.scene.frame_current filepath = fileseq[current_frame % len(fileseq)] @@ -202,22 +232,23 @@ def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix([[1, 0, meshio_mesh = meshio.read(filepath) except Exception as e: show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), - "Meshio Loading Error" + str(e), - icon="ERROR") + "Meshio Loading Error" + str(e), + icon="ERROR") enabled = False - # create the object name = fileseq.basename() + "@" + fileseq.extension() mesh = bpy.data.meshes.new(name) object = bpy.data.objects.new(name, mesh) - object.BSEQ.use_relative = use_relative + + # create the object if use_relative: - if root_path != "": - object.BSEQ.pattern = bpy.path.relpath(str(fileseq), start=root_path) - else: - object.BSEQ.pattern = bpy.path.relpath(str(fileseq)) + full_path = get_relative_path(str(fileseq), root_path) else: - object.BSEQ.pattern = str(fileseq) + full_path = str(fileseq) + # 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.current_file = filepath object.BSEQ.init = True object.BSEQ.enabled = enabled object.BSEQ.start_end_frame = (fileseq.start(), fileseq.end()) @@ -230,9 +261,7 @@ def create_obj(fileseq, use_relative, root_path, transform_matrix=Matrix([[1, 0, bpy.ops.object.select_all(action="DESELECT") bpy.context.view_layer.objects.active = object - def update_obj(scene, depsgraph=None): - for obj in bpy.data.objects: start_time = time.perf_counter() @@ -249,17 +278,12 @@ def update_obj(scene, depsgraph=None): show_message_box("Warning: Might not be able load the correct frame because the dependency graph is not available.", "BSEQ Warning") current_frame = obj.BSEQ.frame meshio_mesh = None - pattern = obj.BSEQ.pattern - if obj.BSEQ.use_relative: - if scene.BSEQ.root_path != "": - pattern = bpy.path.abspath(pattern, start=scene.BSEQ.root_path) - else: - pattern = bpy.path.abspath(pattern) - + # in case the blender file was created on windows system, but opened in linux system - pattern = bpy.path.native_pathsep(pattern) - fs = fileseq.FileSequence(pattern) + full_path = get_absolute_path(obj, scene) + fs = fileseq.FileSequence(full_path) + if obj.BSEQ.use_advance and obj.BSEQ.script_name: script = bpy.data.texts[obj.BSEQ.script_name] try: @@ -273,6 +297,7 @@ def update_obj(scene, depsgraph=None): user_process = locals()['process'] try: user_process(fs, current_frame, obj.data) + obj.BSEQ.current_file = "Controlled by user process" except Exception as e: show_message_box("Error when calling user process: " + traceback.format_exc(), icon="ERROR") del locals()['process'] @@ -283,6 +308,7 @@ def update_obj(scene, depsgraph=None): user_preprocess = locals()['preprocess'] try: meshio_mesh = user_preprocess(fs, current_frame) + obj.BSEQ.current_file = "Controlled by user preprocess" except Exception as e: show_message_box("Error when calling user preprocess: " + traceback.format_exc(), icon="ERROR") # this continue means only if error occures, then goes to next bpy.object @@ -291,8 +317,10 @@ def update_obj(scene, depsgraph=None): del locals()['preprocess'] else: filepath = fs[current_frame % len(fs)] + filepath = os.path.normpath(filepath) try: meshio_mesh = meshio.read(filepath) + obj.BSEQ.current_file = filepath except Exception as e: show_message_box("Error when reading: " + filepath + ",\n" + traceback.format_exc(), "Meshio Loading Error" + str(e), diff --git a/bseq/messenger.py b/bseq/messenger.py index 507bc49..0409779 100644 --- a/bseq/messenger.py +++ b/bseq/messenger.py @@ -6,7 +6,10 @@ def selected_callback(): # seems like that this is not necessary # if not bpy.context.view_layer.objects.active: # return - + + if not bpy.context.active_object: + return + name = bpy.context.active_object.name idx = bpy.data.objects.find(name) if idx >= 0: diff --git a/bseq/operators.py b/bseq/operators.py index ca2b636..20e308e 100644 --- a/bseq/operators.py +++ b/bseq/operators.py @@ -3,14 +3,28 @@ import fileseq from .messenger import * import traceback -from .utils import refresh_obj, show_message_box +from .utils import refresh_obj, show_message_box, get_relative_path from .importer import create_obj, create_meshio_obj import numpy as np +addon_name = "blendersequenceloader" + +def relative_path_error(): + show_message_box("When using relative path, please save file before using it", icon="ERROR") + return {"CANCELLED"} + +def get_transform_matrix(importer_prop): + if importer_prop.use_custom_transform: + return Matrix.LocRotScale(importer_prop.custom_location, importer_prop.custom_rotation, importer_prop.custom_scale) + else: + return Matrix.Identity(4) + +def create_obj_wrapper(seq, importer_prop): + create_obj(seq, importer_prop.use_relative, importer_prop.root_path, transform_matrix=get_transform_matrix(importer_prop)) # Here are load and delete operations class BSEQ_OT_load(bpy.types.Operator): - '''This operator loads a sequnce''' + '''This operator loads a sequence''' bl_label = "Load Sequence" bl_idname = "sequence.load" bl_options = {"UNDO"} @@ -19,10 +33,8 @@ def execute(self, context): scene = context.scene importer_prop = scene.BSEQ - if importer_prop.relative and not bpy.data.is_saved: - # use relative but file not saved - show_message_box("When using relative path, please save file before using it", icon="ERROR") - return {"CANCELLED"} + if importer_prop.use_relative and not bpy.data.is_saved: + return relative_path_error() fs = importer_prop.fileseq use_pattern = importer_prop.use_pattern @@ -42,19 +54,13 @@ def execute(self, context): show_message_box(traceback.format_exc(), "Can't find sequence: " + str(fs), "ERROR") return {"CANCELLED"} - transform_matrix = (Matrix.LocRotScale( - importer_prop.custom_location, - importer_prop.custom_rotation, - importer_prop.custom_scale) - if importer_prop.use_custom_transform else Matrix.Identity(4)) - - create_obj(fs, importer_prop.relative, importer_prop.root_path, transform_matrix=transform_matrix) + create_obj_wrapper(fs, importer_prop) return {"FINISHED"} class BSEQ_OT_edit(bpy.types.Operator): - '''This operator changes a sequnce''' - bl_label = "Edit Sequences Path" + '''This operator changes a sequence''' + bl_label = "Edit Sequence's Path" bl_idname = "sequence.edit" bl_options = {"UNDO"} @@ -62,10 +68,9 @@ def execute(self, context): scene = context.scene importer_prop = scene.BSEQ - if importer_prop.relative and not bpy.data.is_saved: + if importer_prop.use_relative and not bpy.data.is_saved: # use relative but file not saved - show_message_box("When using relative path, please save file before using it", icon="ERROR") - return {"CANCELLED"} + return relative_path_error() fs = importer_prop.fileseq use_pattern = importer_prop.use_pattern @@ -92,16 +97,19 @@ def execute(self, context): obj = sim_loader.edit_obj if not obj: return {"CANCELLED"} - if importer_prop.relative: - obj.BSEQ.pattern = bpy.path.relpath(str(fs)) + if importer_prop.use_relative: + if importer_prop.root_path != "": + object.BSEQ.pattern = bpy.path.relpath(str(fileseq), start=importer_prop.root_path) + else: + object.BSEQ.pattern = bpy.path.relpath(str(fileseq)) + else: obj.BSEQ.pattern = str(fs) - obj.BSEQ.use_relative = importer_prop.relative return {"FINISHED"} class BSEQ_OT_resetpt(bpy.types.Operator): - '''This operator reset the geometry nodes of the sequence as a point cloud''' + '''This operator resets the geometry nodes of the sequence as a point cloud''' bl_label = "Reset Geometry Nodes as Point Cloud" bl_idname = "bseq.resetpt" bl_options = {"UNDO"} @@ -136,7 +144,7 @@ def execute(self, context): class BSEQ_OT_resetmesh(bpy.types.Operator): - '''This operator reset the geometry nodes of the sequence as a point cloud''' + '''This operator resets the geometry nodes of the sequence as a point cloud''' bl_label = "Reset Geometry Nodes as Mesh" bl_idname = "bseq.resetmesh" bl_options = {"UNDO"} @@ -161,7 +169,7 @@ def execute(self, context): class BSEQ_OT_resetins(bpy.types.Operator): - '''This operator reset the geometry nodes of the sequence as a point cloud''' + '''This operator resets the geometry nodes of the sequence as a point cloud''' bl_label = "Reset Geometry Nodes as Instances" bl_idname = "bseq.resetins" bl_options = {"UNDO"} @@ -209,8 +217,8 @@ def execute(self, context): class BSEQ_OT_set_as_split_norm(bpy.types.Operator): - '''This operator set the vertex attribute as vertex split normals''' - bl_label = "Set as split normal per Vertex" + '''This operator sets the vertex attributes as vertex split normals''' + bl_label = "Set as split normal per vertex" bl_idname = "bseq.setsplitnorm" bl_options = {"UNDO"} @@ -228,8 +236,8 @@ def execute(self, context): class BSEQ_OT_remove_split_norm(bpy.types.Operator): - '''This operator remove the vertex attribute as vertex split normals''' - bl_label = "Remove split normal per Vertex" + '''This operator removes the vertex attributes as vertex split normals''' + bl_label = "Remove split normal per vertex" bl_idname = "bseq.removesplitnorm" bl_options = {"UNDO"} @@ -244,8 +252,8 @@ def execute(self, context): class BSEQ_OT_disable_selected(bpy.types.Operator): - '''This operator disable all selected sequence''' - bl_label = "Disable Selected Sequence" + '''This operator disables all selected sequence''' + bl_label = "Disable selected sequence" bl_idname = "bseq.disableselected" bl_options = {"UNDO"} @@ -257,8 +265,8 @@ def execute(self, context): class BSEQ_OT_enable_selected(bpy.types.Operator): - '''This operator enable all selected sequence''' - bl_label = "Enable Selected Sequence" + '''This operator enables all selected sequence''' + bl_label = "Enable selected sequence" bl_idname = "bseq.enableselected" bl_options = {"UNDO"} @@ -270,8 +278,8 @@ def execute(self, context): class BSEQ_OT_refresh_seq(bpy.types.Operator): - '''This operator refresh the sequence''' - bl_label = "Refresh Sequence" + '''This operator refreshes the sequence''' + bl_label = "Refresh sequence" bl_idname = "bseq.refresh" def execute(self, context): @@ -282,8 +290,8 @@ def execute(self, context): return {"FINISHED"} class BSEQ_OT_disable_all(bpy.types.Operator): - '''This operator disable all selected sequence''' - bl_label = "Disable All Sequences" + '''This operator disables all selected sequence''' + bl_label = "Disable all sequences" bl_idname = "bseq.disableall" bl_options = {"UNDO"} @@ -294,8 +302,8 @@ def execute(self, context): return {"FINISHED"} class BSEQ_OT_enable_all(bpy.types.Operator): - '''This operator enable all selected sequence''' - bl_label = "Enable All Sequences" + '''This operator enables all selected sequence''' + bl_label = "Enable all sequences" bl_idname = "bseq.enableall" bl_options = {"UNDO"} @@ -307,8 +315,8 @@ def execute(self, context): class BSEQ_OT_refresh_sequences(bpy.types.Operator): '''This operator refreshes all found sequences''' - bl_label = "" #"Refresh Found Sequences" - bl_idname = "bseq.refreshseqs" + bl_label = "Refresh all sequences" + bl_idname = "bseq.refreshall" bl_options = {"UNDO"} def execute(self, context): @@ -319,7 +327,7 @@ def execute(self, context): return {"FINISHED"} class BSEQ_OT_set_start_end_frames(bpy.types.Operator): - '''This changes the timeline start and end frames to the length of a specific sequence''' + '''This operator changes the timeline start and end frames to the length of a specific sequence''' bl_label = "Set timeline" bl_idname = "bseq.set_start_end_frames" bl_options = {"UNDO"} @@ -333,23 +341,53 @@ def execute(self, context): return {"FINISHED"} - from pathlib import Path import meshio from bpy_extras.io_utils import ImportHelper -class WM_OT_batchSequences(bpy.types.Operator, ImportHelper): +class BSEQ_OT_batch_sequences(bpy.types.Operator, ImportHelper): """Batch Import Sequences""" bl_idname = "wm.seq_import_batch" - bl_label = "Import multiple sequences" + bl_label = "Import Sequences" bl_options = {'PRESET', 'UNDO'} + def update_filter_glob(self, context): + bpy.ops.wm.seq_import_batch('INVOKE_DEFAULT') + + filter_string: bpy.props.StringProperty( + default="*.obj", + options={'HIDDEN'}, + update=update_filter_glob, + ) + + filename_ext='' + filter_glob: bpy.props.StringProperty( + default='*.obj', + options={'HIDDEN', 'LIBRARY_EDITABLE'}, + ) + files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + + def invoke(self, context, event): + scene = context.scene + if scene.BSEQ.filter_string: + self.filter_glob = scene.BSEQ.filter_string + else: + self.filter_glob = "*" + + context.window_manager.fileselect_add(self) + + return {'RUNNING_MODAL'} def execute(self, context): scene = context.scene importer_prop = scene.BSEQ + if importer_prop.use_relative and not bpy.data.is_saved: + return relative_path_error() + + self.filter_glob = '*' + folder = Path(self.filepath) used_seqs = set() @@ -357,19 +395,157 @@ def execute(self, context): # Check if there exists a matching file sequence for every selection fp = str(Path(folder.parent, selection.name)) seqs = fileseq.findSequencesOnDisk(str(folder.parent)) - matching_seqs = [s for s in seqs if fp in list(s) and s not in used_seqs] - - if matching_seqs: - transform_matrix = (Matrix.LocRotScale(importer_prop.custom_location, importer_prop.custom_rotation, importer_prop.custom_scale) - if importer_prop.use_custom_transform else Matrix.Identity(4)) - create_obj(matching_seqs[0], False, importer_prop.root_path, transform_matrix=transform_matrix) - used_seqs.add(matching_seqs[0]) + matching_seq = [s for s in seqs if fp in list(s) and str(s) not in used_seqs] + + if matching_seq: + matching_seq = matching_seq[0] + used_seqs.add(str(matching_seq)) + + create_obj_wrapper(matching_seq, importer_prop) + return {'FINISHED'} + + def draw(self, context): + pass + +class BSEQ_PT_batch_sequences_settings(bpy.types.Panel): + bl_space_type = 'FILE_BROWSER' + bl_region_type = 'TOOL_PROPS' + bl_label = "Settings Panel" + bl_options = {'HIDE_HEADER'} + # bl_parent_id = "FILE_PT_operator" # Optional + + @classmethod + def poll(cls, context): + sfile = context.space_data + operator = sfile.active_operator + return operator.bl_idname == "WM_OT_seq_import_batch" + + def draw(self, context): + layout = self.layout + importer_prop = context.scene.BSEQ + + layout.use_property_split = True + layout.use_property_decorate = False # No animation. + + # # sfile = context.space_data + # # operator = sfile.active_operator + + # layout.prop(importer_prop, 'filter_string') + + # layout.alignment = 'LEFT' + # layout.prop(importer_prop, "relative", text="Relative Path") + # if importer_prop.use_relative: + # layout.prop(importer_prop, "root_path", text="Root Directory") + +class BSEQ_addon_preferences(bpy.types.AddonPreferences): + bl_idname = addon_name + + zips_folder: bpy.props.StringProperty( + name="Zips Folder", + subtype='DIR_PATH', + ) + + def draw(self, context): + # layout = self.layout + # layout.label(text="Please set a folder to store the extracted zip files") + # layout.prop(self, "zips_folder", text="Zips Folder") + pass + +zip_folder_name = '/tmp_zips' + +class BSEQ_OT_import_zip(bpy.types.Operator, ImportHelper): + """Import a zip file""" + bl_idname = "bseq.import_zip" + bl_label = "Import Zip" + bl_options = {'PRESET', 'UNDO'} + + filename_ext = ".zip" + filter_glob: bpy.props.StringProperty( + default="*.zip", + options={'HIDDEN', 'LIBRARY_EDITABLE'}, + ) + + files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) + + def execute(self, context): + importer_prop = context.scene.BSEQ + + import zipfile + zip_file = zipfile.ZipFile(self.filepath) + + addon_prefs = context.preferences.addons[addon_name].preferences + # Check if a string is empty: + if not addon_prefs.zips_folder: + show_message_box("Please set a folder to store the extracted zip files", icon="ERROR") + return {"CANCELLED"} + zips_folder = addon_prefs.zips_folder + zip_folder_name + + valid_files = [info.filename for info in zip_file.infolist() if not info.filename.startswith('__MACOSX/')] + zip_file.extractall(zips_folder, members=valid_files) + zip_file.close() + + folder = str(zips_folder) + '/' + str(Path(self.filepath).name)[:-4] + print(folder) + + seqs = fileseq.findSequencesOnDisk(str(folder)) + if not seqs: + show_message_box("No sequences found in the zip file", icon="ERROR") + return {"CANCELLED"} + + for s in seqs: + # Import it with absolute paths + create_obj(s, False, folder, transform_matrix=get_transform_matrix(importer_prop)) + + # created_folder = context.scene.BSEQ.imported_zips.add() + # created_folder.path = folder + return {'FINISHED'} -class WM_OT_MeshioObject(bpy.types.Operator, ImportHelper): +class BSEQ_OT_delete_zips(bpy.types.Operator): + """Delete a zip file""" + bl_idname = "bseq.delete_zips" + bl_label = "Delete Zip" + bl_options = {'PRESET', 'UNDO'} + + def execute(self, context): + # folders = context.scene.BSEQ.imported_zips + # for folder in folders: + + addon_prefs = context.preferences.addons[addon_name].preferences + zips_folder = addon_prefs.zips_folder + zip_folder_name + + import shutil + shutil.rmtree(zips_folder) + + return {'FINISHED'} + +class BSEQ_OT_load_all(bpy.types.Operator): + """Load all sequences""" + bl_idname = "bseq.load_all" + bl_label = "Load All" + bl_options = {'PRESET', 'UNDO'} + + def execute(self, context): + importer_prop = context.scene.BSEQ + + if importer_prop.use_relative and not bpy.data.is_saved: + return relative_path_error() + + dir = importer_prop.path + seqs = fileseq.findSequencesOnDisk(str(dir)) + + for s in seqs: + print(s) + + for s in seqs: + create_obj_wrapper(s, importer_prop) + return {'FINISHED'} + + +class BSEQ_OT_meshio_object(bpy.types.Operator, ImportHelper): """Batch Import Meshio Objects""" bl_idname = "wm.meshio_import_batch" - bl_label = "Import multiple Meshio objects" + bl_label = "Import Multiple Meshio Objects" bl_options = {'PRESET', 'UNDO'} files: bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) @@ -384,5 +560,60 @@ def execute(self, context): def menu_func_import(self, context): self.layout.operator( - WM_OT_MeshioObject.bl_idname, + BSEQ_OT_meshio_object.bl_idname, text="MeshIO Object") + +# Default Keymap Configuration +addon_keymaps = [] + +def add_keymap(): + wm = bpy.context.window_manager + + # Add new keymap section for BSEQ + + kc = wm.keyconfigs.addon + if kc: + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("sequence.load", type='F', value='PRESS', shift=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.disableselected", type='D', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.enableselected", type='E', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.refresh", type='R', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.disableall", type='D', value='PRESS', shift=True, alt=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.enableall", type='E', value='PRESS', shift=True, alt=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.refreshall", type='R', value='PRESS', shift=True, alt=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("bseq.set_start_end_frames", type='F', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("wm.seq_import_batch", type='I', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + + km = kc.keymaps.new(name='3D View', space_type='VIEW_3D') + kmi = km.keymap_items.new("wm.meshio_import_batch", type='M', value='PRESS', shift=True, ctrl=True) + addon_keymaps.append((km, kmi)) + +def delete_keymap(): + for km, kmi in addon_keymaps: + km.keymap_items.remove(kmi) + addon_keymaps.clear() diff --git a/bseq/panels.py b/bseq/panels.py index 007d084..d7fc270 100644 --- a/bseq/panels.py +++ b/bseq/panels.py @@ -22,19 +22,32 @@ def filter_items(self, context, data, property): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: - row = layout.row() - row.prop(item, "name", text='Name ', emboss=False) + split = layout.split(factor=0.4) + col1 = split.column() + col2 = split.column() + split2 = col2.split(factor=0.25) + col2 = split2.column() + col3 = split2.column() + split3 = col3.split(factor=0.5) + col3 = split3.column() + col4 = split3.column() + col4.alignment = 'EXPAND' + start_frame = item.BSEQ.start_end_frame[0] + end_frame = item.BSEQ.start_end_frame[1] + + col1.prop(item, "name", text='', emboss=False) if item.BSEQ.enabled: - row.prop(item.BSEQ, "enabled", text = "ENABLED", icon="PLAY") - row.prop(item.BSEQ, "frame", text = "Current Frame:") + col2.prop(item.BSEQ, "enabled", text="", icon="PLAY") + col3.prop(item.BSEQ, "frame", text="") + col4.label(text=str(start_frame) + '-' + str(end_frame)) else: - row.prop(item.BSEQ, "enabled", text = "DISABLED", icon="PAUSE") - row.label(text = "Animation Stopped") + col2.prop(item.BSEQ, "enabled", text ="", icon="PAUSE") + col3.label(text="", icon="BLANK1") + col4.label(text=str(start_frame) + '-' + str(end_frame)) else: # actually, I guess this line of code won't be executed? layout.label(text="", translate=False, icon_value=icon) - class BSEQ_UL_Att_List(bpy.types.UIList): ''' This controls the list of attributes available for this sequence @@ -43,84 +56,69 @@ class BSEQ_UL_Att_List(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname): if item: layout.enabled = False - layout.prop(item, "name", text='Name ', emboss=False) + layout.prop(item, "name", text='', emboss=False) obj = bpy.data.objects[context.scene.BSEQ.selected_obj_num] mesh = obj.data if mesh.BSEQ.split_norm_att_name and mesh.BSEQ.split_norm_att_name == item.name: - layout.label(text="using as split norm") + layout.label(text="Use as split norm.") else: # actually, I guess this line of code won't be executed? layout.label(text="", translate=False, icon_value=icon) - -class BSEQ_List_Panel(bpy.types.Panel): - ''' - This is the panel of imported sequences, bottom part of images/9.png - ''' - bl_label = "Imported Sequences" - bl_idname = "BSEQ_PT_list" +class BSEQ_Panel: bl_space_type = 'VIEW_3D' bl_region_type = "UI" bl_category = "Sequence Loader" bl_context = "objectmode" +class BSEQ_Globals_Panel(BSEQ_Panel, bpy.types.Panel): + bl_label = "Global Settings" + bl_idname = "BSEQ_PT_global" + bl_options = {'DEFAULT_CLOSED'} + def draw(self, context): layout = self.layout sim_loader = context.scene.BSEQ - row = layout.row() - row.template_list("BSEQ_UL_Obj_List", "", bpy.data, "objects", sim_loader, "selected_obj_num", rows=2) - row = layout.row() - row.operator("bseq.enableselected", text="Enable Selected") - row.operator("bseq.disableselected", text="Disable Selected") - row.operator("bseq.refresh", text="Refresh") - row = layout.row() - row.operator("bseq.enableall", text="Enable All") - row.operator("bseq.disableall", text="Disable All") - row.operator("bseq.set_start_end_frames", text="Set timeline") - + split = layout.split() + col1 = split.column() + col1.alignment = 'RIGHT' + col2 = split.column() + col1.label(text="Root Directory") + col2.prop(sim_loader, "root_path", text="") + col1.label(text="Print Sequence Information") + col2.prop(sim_loader, "print", text="") + col1.label(text="Auto Refresh Active") + col2.prop(sim_loader, "auto_refresh_active", text="") + col1.label(text="Auto Refresh All") + col2.prop(sim_loader, "auto_refresh_all", text="") -class BSEQ_Settings(bpy.types.Panel): - ''' - This is the panel of settings of selected sequence - ''' - bl_label = "Sequence Settings" - bl_idname = "BSEQ_PT_settings" - bl_space_type = 'VIEW_3D' - bl_region_type = "UI" - bl_category = "Sequence Loader" - bl_context = "objectmode" - bl_options = {"DEFAULT_CLOSED"} +class BSEQ_Advanced_Panel(BSEQ_Panel, bpy.types.Panel): + bl_label = "Advanced Settings" + bl_idname = "BSEQ_PT_advanced" + bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout sim_loader = context.scene.BSEQ + + split = layout.split() + col1 = split.column() + col2 = split.column() + if sim_loader.selected_obj_num >= len(bpy.data.objects): return obj = bpy.data.objects[sim_loader.selected_obj_num] if not obj.BSEQ.init: return - # path settings - layout.label(text="Sequence Information (read-only)") - box = layout.box() - - split = box.split() - col1 = split.column() - col1.alignment = 'RIGHT' - col2 = split.column(align=False) - - col2.enabled = False - col1.label(text='Relative') - col2.prop(obj.BSEQ, 'use_relative', text="") - col1.label(text='Pattern') - col2.prop(obj.BSEQ, 'pattern', text="") - col1.label(text='Last loading time (ms)') - col2.prop(obj.BSEQ, 'last_benchmark', text="") + col1.label(text='Script') + col2.prop_search(obj.BSEQ, 'script_name', bpy.data, 'texts', text="") # geometry nodes settings - layout.label(text="Geometry Nodes") + layout.label(text="Geometry Nodes (select sequence first)") + box = layout.box() box.label(text="Point Cloud and Instances Material") split = box.split() @@ -140,6 +138,67 @@ def draw(self, context): col3.operator('bseq.resetins', text="Instances") +class BSEQ_List_Panel(BSEQ_Panel, bpy.types.Panel): + ''' + This is the panel of imported sequences, bottom part of images/9.png + ''' + bl_label = "Sequences" + bl_idname = "BSEQ_PT_list" + bl_options = {'DEFAULT_CLOSED'} + + def draw(self, context): + layout = self.layout + sim_loader = context.scene.BSEQ + row = layout.row() + row.template_list("BSEQ_UL_Obj_List", "", bpy.data, "objects", sim_loader, "selected_obj_num", rows=2) + row = layout.row() + row.operator("bseq.enableselected", text="Activate") + row.operator("bseq.disableselected", text="Deactivate") + row.operator("bseq.refresh", text="Refresh") + row = layout.row() + row.operator("bseq.enableall", text="Activate All") + row.operator("bseq.disableall", text="Deactivate All") + row.operator("bseq.set_start_end_frames", text="Set timeline") + +class BSEQ_Settings(BSEQ_Panel, bpy.types.Panel): + ''' + This is the panel of settings of selected sequence + ''' + bl_label = "Sequence Properties" + bl_idname = "BSEQ_PT_settings" + bl_options = {"DEFAULT_CLOSED"} + + def draw(self, context): + layout = self.layout + sim_loader = context.scene.BSEQ + importer_prop = context.scene.BSEQ + + if sim_loader.selected_obj_num >= len(bpy.data.objects): + return + obj = bpy.data.objects[sim_loader.selected_obj_num] + if not obj.BSEQ.init: + return + + split = layout.split() + col1 = split.column() + col1.alignment = 'RIGHT' + col2 = split.column(align=False) + + col1.label(text='Path') + col2.prop(obj.BSEQ, 'path', text="") + col1.label(text='Pattern') + col2.prop(obj.BSEQ, 'pattern', text="") + # Read-only + col1.label(text='Current File') + # make it read-only + row1 = col2.row() + row1.enabled = False + row1.prop(obj.BSEQ, 'current_file', text="") + col1.label(text='Last loading time (ms)') + row2 = col2.row() + row2.enabled = False + row2.prop(obj.BSEQ, 'last_benchmark', text="", ) + # attributes settings layout.label(text="Attributes") box = layout.box() @@ -148,39 +207,69 @@ def draw(self, context): box.operator("bseq.setsplitnorm", text="Set selected as normal") box.operator("bseq.removesplitnorm", text="Clear normal") - # advance settings - layout.label(text="Advanced") - box = layout.box() - split = box.split() +class BSEQ_PT_Import(BSEQ_Panel, bpy.types.Panel): + ''' + This is the panel of main addon interface. see images/1.jpg + ''' + bl_label = "Import" + bl_idname = "BSEQ_PT_panel" + + def draw(self, context): + layout = self.layout + scene = context.scene + importer_prop = scene.BSEQ + + row = layout.row() + + row.scale_y = 1.5 + row.operator("wm.seq_import_batch") + + split = layout.split() + col1 = split.column() + col2 = split.column() + + split = layout.split(factor=0.5) col1 = split.column() col1.alignment = 'RIGHT' col2 = split.column(align=False) - col1.label(text="Show Settings") - col2.prop(obj.BSEQ, 'use_advance', text="") - if obj.BSEQ.use_advance: - col1.label(text='Script') - col2.prop_search(obj.BSEQ, 'script_name', bpy.data, 'texts', text="") + # col2.prop(importer_prop, "filter_string", text="Filter String") -class BSEQ_Import(bpy.types.Panel): - ''' - This is the panel of main addon interface. see images/1.jpg - ''' - bl_label = "Sequence Loader" - bl_idname = "BSEQ_PT_panel" - bl_space_type = "VIEW_3D" - bl_region_type = "UI" - bl_category = "Sequence Loader" - bl_context = "objectmode" + col1.label(text="Relative Path") + col2.prop(importer_prop, "use_relative", text="") + + col1.label(text="Import Default Normals") + col2.prop(importer_prop, "use_imported_normals", text="") + + col1.label(text="Custom Transform") + col2.prop(importer_prop, "use_custom_transform", text="") + + if importer_prop.use_custom_transform: + split = layout.split(factor=0.33) + box_col1 = split.column() + box_col2 = split.column() + box_col3 = split.column() + + box_col1.label(text="Location:") + box_col1.prop(importer_prop, "custom_location", text="") + + box_col2.label(text="Rotation:") + box_col2.prop(importer_prop, "custom_rotation", text="") + + box_col3.label(text="Scale:") + box_col3.prop(importer_prop, "custom_scale", text="") + +class BSEQ_PT_Import_Child1(BSEQ_Panel, bpy.types.Panel): + bl_parent_id = "BSEQ_PT_panel" + bl_label = "Import from folder" + bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout scene = context.scene importer_prop = scene.BSEQ - layout.label(text="Basic Import Settings") - box = layout.box() - split = box.split() + split = layout.split() col1 = split.column() col1.alignment = 'RIGHT' col2 = split.column(align=False) @@ -188,7 +277,7 @@ def draw(self, context): col1.label(text="Directory") col2.prop(importer_prop, "path", text="") - col1.label(text="Use Custom Pattern") + col1.label(text="Custom Pattern") col2.prop(importer_prop, "use_pattern", text="") col1.label(text="Sequence Pattern") if importer_prop.use_pattern: @@ -198,58 +287,35 @@ def draw(self, context): col3 = split2.column() col4 = split2.column() col3.prop(importer_prop, "fileseq", text="") - col4.operator("bseq.refreshseqs", icon="FILE_REFRESH") - - col1.label(text="Use Relative Path") - col2.prop(importer_prop, "relative", text="") - - if importer_prop.relative: - col1.label(text="Root Directory") - col2.prop(importer_prop, "root_path", text="") - - layout.operator("sequence.load") + col4.operator("bseq.refreshall", text='', icon="FILE_REFRESH") - layout.operator("wm.seq_import_batch") - - split = layout.split() + split = layout.split(factor=0.7) col1 = split.column() col2 = split.column() + col1.operator("sequence.load") + col2.operator("bseq.load_all") - # check if edit_obj exist - # if not exist any more, then delete the object manually - # see here https://blender.stackexchange.com/a/164835 for more details - # I personally think this implementation is not a good design, - # but can't think of any better ways now - if importer_prop.edit_obj and context.scene.objects.get(importer_prop.edit_obj.name) == None: - bpy.data.objects.remove(importer_prop.edit_obj) + # split = layout.split(factor=0.5) + # col1 = split.column() + # col2 = split.column() - col1.prop_search(importer_prop, 'edit_obj', bpy.data, 'objects', text="") - col2.operator("sequence.edit") + # col1.operator("bseq.import_zip", text="Import from zip") + # col2.operator("bseq.delete_zips", text="Delete created folders") - layout.label(text="Global Settings") - box = layout.box() - split = box.split() - col1 = split.column() - col1.alignment = 'RIGHT' - col2 = split.column(align=False) - - col1.label(text="Print Sequence Information on Render") - col2.prop(importer_prop, "print", text="") - col1.label(text="Auto refresh all the sequence every frame") - col2.prop(importer_prop, "auto_refresh", text="") - col1.label(text="Use custom transformation matrix") - col2.prop(importer_prop, "use_custom_transform", text="") - if importer_prop.use_custom_transform: - box.label(text="Location:") - box.prop(importer_prop, "custom_location", text="") +class BSEQ_PT_Import_Child2(BSEQ_Panel, bpy.types.Panel): + bl_parent_id = "BSEQ_PT_panel" + bl_label = "Test" + bl_options = {'HIDE_HEADER'} - box.label(text="Rotation:") - box.prop(importer_prop, "custom_rotation", text="") + def draw(self, context): + layout = self.layout + scene = context.scene + importer_prop = scene.BSEQ - box.label(text="Scale:") - box.prop(importer_prop, "custom_scale", text="") - + split = layout.split() + col1 = split.column() + col2 = split.column() class BSEQ_Templates(bpy.types.Menu): ''' diff --git a/bseq/properties.py b/bseq/properties.py index 5a5968c..cbaec85 100644 --- a/bseq/properties.py +++ b/bseq/properties.py @@ -5,34 +5,52 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): path: bpy.props.StringProperty(name="Directory", subtype="DIR_PATH", - description="You need to go to the folder with the sequence, then click \"Accept\". ", - update=update_path) - relative: bpy.props.BoolProperty(name='Use relative path', description="whether or not to use reletive path", default=False) + description="You need to go to the folder with the sequence, then click \"Accept\"", + update=update_path, + ) + + use_relative: bpy.props.BoolProperty(name='Use relative path', + description="Use relative path", + default=False, + ) + + use_imported_normals: bpy.props.BoolProperty(name='Use Imported Normals', + description="Use normals from imported mesh", + default=False, + ) + root_path: bpy.props.StringProperty(name="Root Directory", subtype="DIR_PATH", - description="Select a root folder for all relative paths. When not set the current filename is used.", - update=update_path) + description="Select a root folder for all relative paths. If not set, the current filename is used", + update=update_path, + default="", + ) + fileseq: bpy.props.EnumProperty( name="File Sequences", - description="Please choose the file sequences you want", + description="Choose file sequences.", items=item_fileseq, ) + use_pattern: bpy.props.BoolProperty(name='Use pattern', - description="whether or not to use manually typed pattern", - default=False) + description="Use manually typed pattern, if the sequence can't be deteced", + default=False, + ) + pattern: bpy.props.StringProperty(name="Pattern", - description="You can specify the pattern here, in case the sequence can't be deteced.") + description="Custom pattern.", + ) - file_paths: bpy.props.StringProperty(name="File", - subtype="FILE_PATH", - description="Select a root folder for all relative paths. When not set the current filename is used.") - selected_obj_deselectall_flag: bpy.props.BoolProperty(default=True, - description="the flag to determine whether call deselect all or not ") + description="Flag that determines whether to deselect all items or not", + ) + selected_obj_num: bpy.props.IntProperty(name='imported count', - description='the number of imported sequence, when selecting from ui list', + description='Number of imported sequences, when selecting from UI list', default=0, - update=update_selected_obj_num) + update=update_selected_obj_num, + ) + selected_attribute_num: bpy.props.IntProperty(default=0) material: bpy.props.PointerProperty( @@ -45,46 +63,68 @@ class BSEQ_scene_property(bpy.types.PropertyGroup): poll=poll_edit_obj, ) - print: bpy.props.BoolProperty(name='print', - description="whether or not to print additional information when rendering", - default=True) + print: bpy.props.BoolProperty(name='Print Sequence Information', + description="Print additional information during rendering to a file in the same folder as the render output", + default=True, + ) - auto_refresh: bpy.props.BoolProperty(name='auto refresh', - description="whether or not to auto refresh all the sequence every frame", - default=False) - - use_custom_transform: bpy.props.BoolProperty(name='Use custom transformation matrix', - description="Whether or not to use a custom transformation matrix", - default=False) + auto_refresh_active: bpy.props.BoolProperty(name='Auto Refresh Active Sequences', + description="Auto refresh all active sequences every frame", + default=False, + ) + + auto_refresh_all: bpy.props.BoolProperty(name='Auto Refresh All Sequences', + description="Auto refresh all sequences every frame", + default=False, + ) + + use_custom_transform: bpy.props.BoolProperty(name='Custom Transform', + description="Use a custom transformation matrix when importing", + default=False, + ) custom_location: bpy.props.FloatVectorProperty(name='Custom Location', description='Set custom location vector', size=3, - subtype="TRANSLATION") + subtype="TRANSLATION", + ) custom_rotation: bpy.props.FloatVectorProperty(name='Custom Rotation', description='Set custom rotation vector', size=3, subtype="EULER", - default=[0,0,0]) + default=[0,0,0], + ) custom_scale: bpy.props.FloatVectorProperty(name='Custom Scale', description='Set custom scaling vector', size=3, subtype="COORDINATES", - default=[1,1,1]) - + default=[1,1,1], + ) + + use_blender_obj_import: bpy.props.BoolProperty(name='Blender .obj import', + description="Use Blender's built-in .obj import function (or meshio's .obj import function)", + default=True, + ) + + filter_string: bpy.props.StringProperty(name='Filter String', + description='Filter string for file sequences', + default='', + ) + class BSEQ_obj_property(bpy.types.PropertyGroup): init: bpy.props.BoolProperty(default=False) enabled: bpy.props.BoolProperty(default=True, - description="When disbaled, the sequence won't be updated at each frame. Enabled by default") + description="If disabled, the sequence won't be updated each frame") use_advance: bpy.props.BoolProperty(default=False) script_name: bpy.props.StringProperty() - use_relative: bpy.props.BoolProperty(default=False) + path: bpy.props.StringProperty(subtype="DIR_PATH") pattern: bpy.props.StringProperty() + current_file: bpy.props.StringProperty() frame: bpy.props.IntProperty() - start_end_frame: bpy.props.IntVectorProperty(name="Start and End Frames", size=2, default=(0, 0)) - last_benchmark: bpy.props.FloatProperty(name="Last Loading Time") + start_end_frame: bpy.props.IntVectorProperty(name="Start and end frames", size=2, default=(0, 0)) + last_benchmark: bpy.props.FloatProperty(name="Last loading time") # set this property for mesh, not object (maybe change later?) class BSEQ_mesh_property(bpy.types.PropertyGroup): diff --git a/bseq/utils.py b/bseq/utils.py index 7f6fd35..4e7e957 100644 --- a/bseq/utils.py +++ b/bseq/utils.py @@ -1,5 +1,6 @@ import bpy import fileseq +import os def show_message_box(message="", title="Message Box", icon="INFO"): ''' @@ -24,23 +25,36 @@ def stop_animation(): # if playing animation, then stop it, otherwise it will keep showing message box bpy.ops.screen.animation_cancel() +def get_relative_path(path, root_path): + if root_path != "": + path = bpy.path.relpath(path, start=root_path) + else: + path = bpy.path.relpath(path) + return path + +# convert relative path to absolute path +def convert_to_absolute_path(path, root_path): + if root_path != "": + path = bpy.path.abspath(path, start=root_path) + else: + path = bpy.path.abspath(path) + return path + +def get_absolute_path(obj, scene): + full_path = os.path.join(bpy.path.native_pathsep(obj.BSEQ.path), obj.BSEQ.pattern) + full_path = convert_to_absolute_path(full_path, scene.BSEQ.root_path) + return full_path def refresh_obj(obj, scene): - fs = obj.BSEQ.pattern - if obj.BSEQ.use_relative: - if scene.BSEQ.root_path != "": - fs = bpy.path.abspath(fs, start=scene.BSEQ.root_path) - else: - fs = bpy.path.abspath(fs) - fs = bpy.path.native_pathsep(fs) + 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) - if obj.BSEQ.use_relative: - if scene.BSEQ.root_path != "": - fs = bpy.path.relpath(fs, start=scene.BSEQ.root_path) - else: - fs = bpy.path.relpath(fs) - obj.BSEQ.pattern = fs + 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) diff --git a/extern/fileseq b/extern/fileseq index 9ec0493..584ee22 160000 --- a/extern/fileseq +++ b/extern/fileseq @@ -1 +1 @@ -Subproject commit 9ec049373af37ec21a21d2a5564deb344a96f97f +Subproject commit 584ee2218b74a9c7f4127c922cc2c33dc5a706b4 diff --git a/extern/meshio b/extern/meshio index a6175e0..0138cc8 160000 --- a/extern/meshio +++ b/extern/meshio @@ -1 +1 @@ -Subproject commit a6175e0d9dfb2aa274392d1cd396e991f0487cbc +Subproject commit 0138cc8692b806b44b32d344f7961e8370121ff7 diff --git a/extern/python-future b/extern/python-future index 80523f3..af1db97 160000 --- a/extern/python-future +++ b/extern/python-future @@ -1 +1 @@ -Subproject commit 80523f383fbba1c6de0551e19d0277e73e69573c +Subproject commit af1db970b0879b59e7aeb798c27a623144561cff diff --git a/extern/rich b/extern/rich index 3734ff4..fd98182 160000 --- a/extern/rich +++ b/extern/rich @@ -1 +1 @@ -Subproject commit 3734ff45d7b30541aabdd656efb80c9896d445b3 +Subproject commit fd981823644ccf50d685ac9c0cfe8e1e56c9dd35