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
gpv2
Pullusb 2021-11-25 17:00:09 +01:00
parent 9389faed22
commit 78b70c8fca
7 changed files with 506 additions and 9 deletions

View File

@ -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

View File

@ -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"

View File

@ -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():

338
OP_palettes_linker.py Normal file
View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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)
# 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