From 78b70c8fcafcd82c1f95d0c6e763149212cf31fb Mon Sep 17 00:00:00 2001 From: Pullusb Date: Thu, 25 Nov 2021 17:00:09 +0100 Subject: [PATCH] fix: reset draw cam rotation 1.7.8 - fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation) - code: initial enhancement for palette linking --- CHANGELOG.md | 5 + OP_helpers.py | 90 ++++++++++- OP_palettes.py | 7 +- OP_palettes_linker.py | 338 ++++++++++++++++++++++++++++++++++++++++++ UI_tools.py | 53 ++++++- __init__.py | 5 +- utils.py | 17 ++- 7 files changed, 506 insertions(+), 9 deletions(-) create mode 100644 OP_palettes_linker.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b99fd1c..50effd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +1.7.8 + +- fix: reset rotation in draw cam mode keep view in the same place (counter camera rotation) +- code: initial enhancement for palette linking + 1.7.7 - feat: add copy path to `check link` ops with multiple path representation choices diff --git a/OP_helpers.py b/OP_helpers.py index 1f8dce5..d8bf281 100644 --- a/OP_helpers.py +++ b/OP_helpers.py @@ -1,10 +1,14 @@ from time import ctime import bpy +import mathutils from mathutils import Vector#, Matrix from pathlib import Path +import math from math import radians + from . import utils + class GPTB_OT_copy_text(bpy.types.Operator): bl_idname = "wm.copytext" bl_label = "Copy to clipboard" @@ -280,6 +284,89 @@ class GPTB_OT_set_view_as_cam(bpy.types.Operator): return {"FINISHED"} +class GPTB_OT_reset_cam_rot(bpy.types.Operator): + bl_idname = "gp.reset_cam_rot" + bl_label = "Reset rotation" + bl_description = "Reset rotation of the draw manipulation camera" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return context.scene.camera and not context.scene.camera.name.startswith('Cam') + # return context.region_data.view_perspective == 'CAMERA'# check if in camera + + def get_center_view(self, context, cam): + from bpy_extras.view3d_utils import location_3d_to_region_2d + frame = cam.data.view_frame() + mat = cam.matrix_world + frame = [mat @ v for v in frame] + frame_px = [location_3d_to_region_2d(context.region, context.space_data.region_3d, v) for v in frame] + center_x = frame_px[2].x + (frame_px[0].x - frame_px[2].x)/2 + center_y = frame_px[1].y + (frame_px[0].y - frame_px[1].y)/2 + return mathutils.Vector((center_x, center_y)) + + def get_ui_ratio(self, context): + '''correct ui overlap from header/toolbars''' + regs = context.area.regions + if context.preferences.system.use_region_overlap: + w = context.area.width + # minus tool header + h = context.area.height - regs[0].height + else: + # minus tool leftbar + sidebar right + w = context.area.width - regs[2].width - regs[3].width + # minus tool header + header + h = context.area.height - regs[0].height - regs[1].height + + self.ratio = h / w + self.ratio_inv = w / h + + def execute(self, context): + cam = context.scene.camera + + # self.cam_init_euler = self.cam.rotation_euler.copy() + if not cam.parent or cam.parent.type != 'CAMERA': + self.report({'ERROR'}, "No parents to refer to for rotation reset") + return {"CANCELLED"} + + # store original rotation mode + org_rotation_mode = cam.rotation_mode + + # set to euler to works with quaternions, restored at finish + cam.rotation_mode = 'XYZ' + # store camera matrix world + org_cam_matrix = cam.matrix_world.copy() + + org_cam_z = cam.rotation_euler.z + + ## initialize current view_offset in camera + view_cam_offset = mathutils.Vector(context.space_data.region_3d.view_camera_offset) + + # Do the reset to parent transforms + cam.matrix_world = cam.parent.matrix_world # wrong, get the parent rotation offset + + # Get diff angle + angle = cam.rotation_euler.z - org_cam_z + # create rotation matrix with negative angle (we want to counter the move) + neg = -angle + rot_mat2d = mathutils.Matrix([[math.cos(neg), -math.sin(neg)], [math.sin(neg), math.cos(neg)]]) + + # restore original rotation mode + cam.rotation_mode = org_rotation_mode + + self.get_ui_ratio(context) + # apply rotation matrix + new_cam_offset = view_cam_offset.copy() + new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio)) # apply screen ratio + new_cam_offset.rotate(rot_mat2d) + new_cam_offset = mathutils.Vector((new_cam_offset[0], new_cam_offset[1] * self.ratio_inv)) # restore screen ratio + + context.space_data.region_3d.view_camera_offset = new_cam_offset + + + return {"FINISHED"} + +""" # old simple cam draw fit to cam class GPTB_OT_reset_cam_rot(bpy.types.Operator): bl_idname = "gp.reset_cam_rot" bl_label = "Reset rotation" @@ -306,9 +393,8 @@ class GPTB_OT_reset_cam_rot(bpy.types.Operator): else: self.report({'ERROR'}, "No parents to refer to for rotation reset") return {"CANCELLED"} - - return {"FINISHED"} +""" class GPTB_OT_toggle_mute_animation(bpy.types.Operator): bl_idname = "gp.toggle_mute_animation" diff --git a/OP_palettes.py b/OP_palettes.py index 81f917e..de189f4 100644 --- a/OP_palettes.py +++ b/OP_palettes.py @@ -5,7 +5,6 @@ from bpy_extras.io_utils import ImportHelper, ExportHelper from pathlib import Path from .utils import convert_attr, get_addon_prefs - ### --- Json serialized material load/save def load_palette(context, filepath): @@ -58,7 +57,7 @@ class GPTB_OT_load_default_palette(bpy.types.Operator): line.name = 'line' # load json - pfp = Path(bpy.path.abspath(get_addon_prefs().palette_path)) + pfp = Path(bpy.path.abspath(get_addon_prefs().palettes_path)) if not pfp.exists(): self.report({'ERROR'}, f'Palette path not found') return {"CANCELLED"} @@ -166,7 +165,7 @@ class GPTB_OT_save_palette(bpy.types.Operator, ExportHelper): def load_blend_palette(context, filepath): '''Load materials on current active object from current chosen blend''' #from pathlib import Path - #palette_fp = C.preferences.addons['GP_toolbox'].preferences['palette_path'] + #palette_fp = C.preferences.addons['GP_toolbox'].preferences['palettes_path'] #fp = Path(palette_fp) / 'christina.blend' print(f'-- import palette from : {filepath} --') for ob in context.selected_objects: @@ -460,7 +459,6 @@ class GPTB_OT_clean_material_stack(bpy.types.Operator): return {"FINISHED"} - classes = ( GPTB_OT_load_palette, GPTB_OT_save_palette, @@ -468,6 +466,7 @@ GPTB_OT_load_default_palette, GPTB_OT_load_blend_palette, GPTB_OT_copy_active_to_selected_palette, GPTB_OT_clean_material_stack, + ) def register(): diff --git a/OP_palettes_linker.py b/OP_palettes_linker.py new file mode 100644 index 0000000..bb3fc9e --- /dev/null +++ b/OP_palettes_linker.py @@ -0,0 +1,338 @@ +import bpy +import re +import json +import os +from bpy_extras.io_utils import ImportHelper, ExportHelper +from pathlib import Path +from . import utils +# from . import blendfile + +from bpy.types import ( + Panel, + Operator, + PropertyGroup, + UIList, + ) + +from bpy.props import ( + IntProperty, + BoolProperty, + StringProperty, + FloatProperty, + EnumProperty, + PointerProperty, + ) + +class GPTB_UL_blend_list(UIList): + # order_by_distance : BoolProperty(default=True) + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + layout.label(text=item.blend_name) + + def draw_filter(self, context, layout): + row = layout.row() + subrow = row.row(align=True) + subrow.prop(self, "filter_name", text="") # Only show items matching this name (use ‘*’ as wildcard) + + # reverse order + icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC' + subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse + + def filter_items(self, context, data, propname): + # example : https://docs.blender.org/api/blender_python_api_current/bpy.types.UIList.html + # This function gets the collection property (as the usual tuple (data, propname)), and must return two lists: + # * The first one is for filtering, it must contain 32bit integers were self.bitflag_filter_item marks the + # matching item as filtered (i.e. to be shown), and 31 other bits are free for custom needs. Here we use the + # * The second one is for reordering, it must return a list containing the new indices of the items (which + # gives us a mapping org_idx -> new_idx). + # Please note that the default UI_UL_list defines helper functions for common tasks (see its doc for more info). + # If you do not make filtering and/or ordering, return empty list(s) (this will be more efficient than + # returning full lists doing nothing!). + + collec = getattr(data, propname) + helper_funcs = bpy.types.UI_UL_list + + # Default return values. + flt_flags = [] + flt_neworder = [] + + + # Filtering by name #not working damn ! + if self.filter_name: + flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name", + reverse=self.use_filter_sort_reverse)#self.use_filter_name_reverse) + + return flt_flags, flt_neworder + +class GPTB_UL_object_list(UIList): + # order_by_distance : BoolProperty(default=True) + + def draw_item(self, context, layout, data, item, icon, active_data, active_propname): + layout.label(text=item.name) + + def draw_filter(self, context, layout): + row = layout.row() + subrow = row.row(align=True) + subrow.prop(self, "filter_name", text="") # Only show items matching this name (use ‘*’ as wildcard) + # reverse order + icon = 'SORT_DESC' if self.use_filter_sort_reverse else 'SORT_ASC' + subrow.prop(self, "use_filter_sort_reverse", text="", icon=icon) # built-in reverse + + def filter_items(self, context, data, propname): + collec = getattr(data, propname) + helper_funcs = bpy.types.UI_UL_list + # Default return values. + flt_flags = [] + flt_neworder = [] + if self.filter_name: + flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, collec, "name", + reverse=self.use_filter_sort_reverse) + return flt_flags, flt_neworder + + +def reload_blends(self, context): + scn = context.scene + pl_prop = scn.bl_palettes_props + uilist = scn.bl_palettes_props.blends + uilist.clear() + pl_prop['bl_idx'] = 0 + + prefs = utils.get_addon_prefs() + + if pl_prop.use_project_path: + palette_fp = prefs.palette_path + else: + palette_fp = pl_prop.custom_dir + + if not palette_fp: # singular + item = uilist.add() + item.blend_name = 'No Palette Path Specified' + reload_objects(self, context) + return + + palettes_dir = Path(palette_fp) + if not palettes_dir.exists(): + item = uilist.add() + item.blend_name = 'Palette Path not found' + reload_objects(self, context) + return + + blends = [(o.path, Path(o).stem, "") for o in os.scandir(palettes_dir) + if o.is_file() + and o.name.endswith('.blend') + and re.search(r'v\d{3}', o.name, re.I)] + + blends.sort(key=lambda x: x[1], reverse=False) + # print('blends found', len(blends)) + + for bl in blends: # populate list + item = uilist.add() + + scn.bl_palettes_props['bl_idx'] = len(uilist) - 1 # don't trigger updates + item.blend_path = bl[0] + item.blend_name = bl[1] + + scn.bl_palettes_props.bl_idx = len(uilist) - 1 # trigger update () + # reload_objects(self, context) # triggered by above assignation + + # return len(blends) # return value must be None + +class GPTB_OT_palettes_reload_blends(Operator): + bl_idname = "gp.palettes_reload_blends" + bl_label = "Reload Palette Blends" + bl_description = "Reload the blends in UI list of palettes linker" + bl_options = {"REGISTER"} # , "INTERNAL" + + def execute(self, context): + reload_blends(self, context) + # ret = reload_blends(self, context) + # if ret is None: + # self.report({'ERROR'}, 'No blend scanned, check palette path') + # else: + # self.report({'INFO'}, f'{ret} blends found') + return {"FINISHED"} + +def reload_objects(self, context): + scn = context.scene + prefs = utils.get_addon_prefs() + pal_prop = scn.bl_palettes_props + blend_uil = pal_prop.blends + obj_uil = pal_prop.objects + obj_uil.clear() + pal_prop['ob_idx'] = 0 + + if not len(blend_uil) or (len(blend_uil) == 1 and not bool(blend_uil[0].blend_path)): + item = obj_uil.add() + item.name = 'No blend to list object' + return + + if not blend_uil[pal_prop.bl_idx].blend_path: + item = obj_uil.add() + item.name = 'Selected blend has no path' + return + + path_to_blend = Path(blend_uil[pal_prop.bl_idx].blend_path) + + ob_list = utils.check_objects_in_blend(str(path_to_blend)) # get list of string of all object except camera + + ob_list.sort(reverse=False) # filter object by name ? + # remove camera + # print('blends found', len(blends)) + + for ob_name in ob_list: # populate list + item = obj_uil.add() + item.name = ob_name + item.path = str(path_to_blend / 'Object' / ob_name) + + pal_prop.ob_idx = len(obj_uil) - 1 + # return len(ob_list) # must return None if used in update + + +## PROPS + +class GPTB_PG_blend_prop(PropertyGroup): + blend_name : StringProperty() # stem of the path + blend_path : StringProperty() # ful path + # select: BoolProperty(update=update_select) # use and update to set the plane selection + +class GPTB_PG_object_prop(PropertyGroup): + name : StringProperty() # stem of the path + path : StringProperty() # Object / Material ? + +class GPTB_PG_palette_settings(PropertyGroup): + bl_idx : IntProperty(update=reload_objects) # update_on_index_change to reload object + blends : bpy.props.CollectionProperty(type=GPTB_PG_blend_prop) + + ob_idx : IntProperty() + objects : bpy.props.CollectionProperty(type=GPTB_PG_object_prop) + + use_project_path : BoolProperty(name='Use Project Palettes', + default=True, description='Use palettes directory specified in gp toolbox addon preferences', + update=reload_blends) + + show_path : BoolProperty(name='Show path', + default=True, description='Show Palette directoty filepath') + + custom_dir : StringProperty(name='Custom Palettes Directory', subtype='DIR_PATH', + description='Use choosen directory to load blend palettes', + update=reload_blends) + + import_type : EnumProperty( + name="Import Type", description="Choose inmport type: link, append, append reuse (keep existing materials)", + default='LINK', options={'ANIMATABLE'}, update=None, get=None, set=None, + items=( + ('LINK', 'Link', 'Link materials to selected object', 0), + ('APPEND', 'Append', 'Append materials to selected objects', 1), + ('APPEND_REUSE', 'Append (Reuse)', 'Append materials to selected objects\nkeep those already there', 2), + ) + ) + + + + # fav_blend: StringProperty() ## mark a blend as prefered ? (need to be stored in prefereneces to restore in other blend...) + +class GPTB_OT_import_obj_palette(Operator): + bl_idname = "gp.import_obj_palette" + bl_label = "Import Object Palette" + bl_description = "Import object palette from blend" + bl_options = {"REGISTER", "INTERNAL"} + + def execute(self, context): + pl_prop = context.scene.bl_palettes_props + path = pl_prop.objects[pl_prop.ob_idx].path + self.report({'INFO'}, f'Path to object: {path}') + return {"FINISHED"} + +## PANEL + +class GPTB_PT_palettes_linker_ui(Panel): + bl_idname = 'GPTB_PT_palettes_linker_ui' + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "Gpencil"#Dev + bl_label = "Palettes Mat Linker" + + def draw(self, context): + layout = self.layout + scn = bpy.context.scene + pl_prop = scn.bl_palettes_props + col= layout.column() + prefs = utils.get_addon_prefs() + ## Here put the path thing (only to use a non-library) + + # maybe in submenu... + row = col.row() + expand_icon = 'TRIA_DOWN' if pl_prop.show_path else 'TRIA_RIGHT' + row.prop(pl_prop, 'show_path', text='', icon=expand_icon, emboss=False) + row.prop(pl_prop, 'use_project_path', text='Use Project Palettes') + row.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="") + + if pl_prop.use_project_path: + ## gp toolbox addon prefs path + if not prefs.palette_path: + col.label(text='Gp Toolbox Palette Directory Needed') + col.label(text='(saved with preferences)') + if pl_prop.show_path or not prefs.palette_path: + col.prop(prefs, 'palette_path', text='Project Dir') + else: + ## local path + if not pl_prop.custom_dir: + col.label(text='Need to specify directory') + if pl_prop.show_path or not pl_prop.custom_dir: + col.prop(pl_prop, 'custom_dir', text='Custom Dir') + + + row = col.row() + row.template_list("GPTB_UL_blend_list", "", pl_prop, "blends", pl_prop, "bl_idx", + rows=2) + # side panel + # subcol = row.column(align=True) + # subcol.operator("gp.palettes_reload_blends", icon="FILE_REFRESH", text="") + + ## Show object UI list only once blend Uilist is filled ? + if not len(pl_prop.blends) or (len(pl_prop.blends) == 1 and not bool(pl_prop.blends[0].blend_path)): + col.label(text='Select a blend to list available object') + + row = col.row() + row.template_list("GPTB_UL_object_list", "", pl_prop, "objects", pl_prop, "ob_idx", + rows=4) + + ## Show link button in the border of the UI list ? + # col.prop(pl_prop, 'import_type') + split = col.split(align=True, factor=0.4 ) + split.prop(pl_prop, 'import_type', text='') + + split.enabled = len(pl_prop.objects) and bool(pl_prop.objects[pl_prop.ob_idx].path) + split.operator('gp.import_obj_palette', text='Palette') + + # button to launch link with combined props (active only if the two items are valids) + # str(Path(self.blends) / 'Object' / self.objects + + +classes = ( +# blend list +GPTB_PG_blend_prop, +GPTB_UL_blend_list, +GPTB_OT_palettes_reload_blends, + +# object in blend list +GPTB_PG_object_prop, +GPTB_UL_object_list, + +# prop containing two above +GPTB_PG_palette_settings, + +GPTB_OT_import_obj_palette, +GPTB_PT_palettes_linker_ui, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + bpy.types.Scene.bl_palettes_props = bpy.props.PointerProperty(type=GPTB_PG_palette_settings) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.bl_palettes_props \ No newline at end of file diff --git a/UI_tools.py b/UI_tools.py index 44112a2..3bbd44d 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -308,8 +308,10 @@ class GPTB_PT_color(bpy.types.Panel): def draw(self, context): layout = self.layout + col = layout.column() ## Create empty frame on layer - layout.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME') + col.operator('gp.palette_linker', text=f'Link Materials Palette TO Object', icon='COLOR') ## ops + col.operator('gp.create_empty_frames', icon='DECORATE_KEYFRAME') """ # unused : added in Animation Manager class GPTB_PT_extra(bpy.types.Panel): @@ -378,6 +380,48 @@ def expose_use_channel_color_pref(self, context): layout.label(text='Use Channel Colors (User preferences):') layout.prop(context.preferences.edit, 'use_anim_channel_group_colors') +def asset_browser_ui(self, context): + '''Only shows in blender >= 3.0.0''' + + layout = self.layout + asset_file_handle = context.asset_file_handle + if asset_file_handle is None: + # layout.label(text="No asset selected", icon='INFO') + layout.label(text='No object/material selected', icon='INFO') + return + if asset_file_handle.id_type not in ('OBJECT', 'MATERIAL'): + layout.label(text='No object/material selected', icon='INFO') + return + + layout.use_property_split = True + layout.use_property_decorate = False + + asset_library_ref = context.asset_library_ref + ## Path to blend + asset_lib_path = bpy.types.AssetHandle.get_full_library_path(asset_file_handle, asset_library_ref) + path_to_obj = Path(asset_lib_path) / 'Objects' / asset_file_handle.name + + ## respect header choice ? + ## import_type in (LINK, APPEND, APPEND_REUSE) + imp_type = context.space_data.params.import_type + if imp_type == 'APPEND': + imp_txt = 'Append' + elif imp_type == 'APPEND_REUSE': + imp_txt = 'Append (Reuse)' + else: + imp_txt = 'Link' + + if asset_file_handle.id_type == 'MATERIAL': + layout.label(text=f'From Mat: {asset_file_handle.name}') + if asset_file_handle.id_type == 'OBJECT': + layout.label(text=f'From Obj: {asset_file_handle.name}') + layout.label(text=f'{imp_txt} Materials To GP Object') + layout.operator('gp.palette_linker', text=f'{imp_txt} Materials To GP Object') ## ops + + # layout.label(text='Link Materials to GP Object') + + + classes = ( GPTB_PT_sidebar_panel, GPTB_PT_checker, @@ -394,9 +438,16 @@ def register(): bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu) bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref) + # if bpy.app.version >= (3,0,0): + # bpy.types.ASSETBROWSER_PT_metadata.append(asset_browser_ui) + + def unregister(): bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref) bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu) + # if bpy.app.version >= (3,0,0): + # bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui) + for cls in reversed(classes): bpy.utils.unregister_class(cls) diff --git a/__init__.py b/__init__.py index a0483ea..9d2e235 100755 --- a/__init__.py +++ b/__init__.py @@ -15,7 +15,7 @@ bl_info = { "name": "GP toolbox", "description": "Set of tools for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (1, 7, 7), +"version": (1, 7, 8), "blender": (2, 91, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -42,6 +42,7 @@ from . import OP_helpers from . import OP_keyframe_jump from . import OP_cursor_snap_canvas from . import OP_palettes +from . import OP_palettes_linker from . import OP_brushes from . import OP_file_checker from . import OP_copy_paste @@ -632,6 +633,7 @@ def register(): OP_playblast_bg.register() OP_playblast.register() OP_palettes.register() + # OP_palettes_linker.register() OP_brushes.register() OP_cursor_snap_canvas.register() OP_copy_paste.register() @@ -678,6 +680,7 @@ def unregister(): OP_copy_paste.unregister() OP_cursor_snap_canvas.unregister() OP_brushes.unregister() + # OP_palettes_linker.unregister() OP_palettes.unregister() OP_file_checker.unregister() OP_helpers.unregister() diff --git a/utils.py b/utils.py index 6126471..0743e58 100644 --- a/utils.py +++ b/utils.py @@ -855,4 +855,19 @@ def draw_kmi(km, kmi, layout): # kmm = kc.keymaps.find_modal(kmi.idname) # if kmm: # draw_km(display_keymaps, kc, kmm, None, layout + 1) - # layout.context_pointer_set("keymap", km) \ No newline at end of file + # layout.context_pointer_set("keymap", km) + +def link_objects_in_blend(filepath, obj_name, link=True): + '''Link an object by name from a file, if link is False, append instead of linking''' + with bpy.data.libraries.load(filepath, link=link) as (data_from, data_to): + data_to.objects = [o for o in data_from.objects if o == obj_name] # c.startswith(obj_name) + return data_to.objects + +def check_objects_in_blend(filepath, avoid_camera=True): + '''return a list of object name in file''' + with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): + if avoid_camera: + l = [o for o in data_from.objects if not 'camera' in o.lower()] + else: + l = [o for o in data_from.objects] + return l \ No newline at end of file