From 150bae5920a057f6fa2c583448a54c2c467c1268 Mon Sep 17 00:00:00 2001 From: ChristopheSeux Date: Mon, 26 Feb 2024 11:26:49 +0100 Subject: [PATCH] Add menu and some cleanup --- area.py | 87 +++++- constants.py | 29 +- core/addon_utils.py | 19 +- core/bl_utils.py | 47 +++ core/picker.py | 61 ++-- core/shape.py | 174 ++++++----- draw_handlers.py | 56 +--- operators/picker.py | 635 +++++++++++++++++++++++---------------- operators/shape.py | 24 +- panels.py | 117 -------- shaders/dash_shader.frag | 19 ++ shaders/dash_shader.vert | 15 + ui.py | 66 ++-- 13 files changed, 778 insertions(+), 571 deletions(-) delete mode 100644 panels.py create mode 100644 shaders/dash_shader.frag create mode 100644 shaders/dash_shader.vert diff --git a/area.py b/area.py index b510c4a..3ada21c 100644 --- a/area.py +++ b/area.py @@ -1,5 +1,5 @@ import bpy -from bpy.types import NodeTree, NODE_PT_tools_active, NODE_HT_header +from bpy.types import NodeTree, NODE_PT_tools_active, NODE_HT_header, Menu from .constants import PICKERS @@ -14,6 +14,69 @@ class RigPickerTree(NodeTree): bl_icon = 'OUTLINER_DATA_ARMATURE' +class RP_MT_picker(Menu): + """Picker""" + bl_label = "Picker" + + def draw(self, context): + layout = self.layout + scn = context.scene + + + layout.operator('rigpicker.reload_picker', icon='FILE_REFRESH') + row = layout.row(align=True) + + # Has at least one picker collection in the scene + if not [c.rig_picker.enabled for c in scn.collection.children_recursive]: + row.enabled = False + row.prop(scn.rig_picker, 'use_pick_bone', text='Auto Bone Assign') + + +class RP_MT_animation(Menu): + """Picker""" + bl_label = "Animation" + + def draw(self, context): + layout = self.layout + + layout.operator("rigpicker.toogle_bone_layer", icon="MOUSE_LMB_DRAG") + + row = layout.row() + op = row.operator("node.context_menu_picker", icon="MOUSE_RMB") + if not context.active_pose_bone: + row.enabled = False + + layout.separator() + op = layout.operator("rigpicker.call_operator", text='Select All') + op.operator = "pose.select_all" + op.arguments = '{"action": "SELECT"}' + + op = layout.operator("rigpicker.call_operator", text='Select All') + op.operator = "pose.select_all" + op.arguments = '{"action": "DESELECT"}' + + op = layout.operator("rigpicker.call_operator", text='Frame Selected') + op.operator = "view3d.view_selected" + op.view_3d = True + + layout.separator() + layout.operator("rigpicker.call_operator", text='Insert Keyframe').operator="animtoolbox.insert_keyframe" + layout.operator("anim.keyframe_delete_v3d", text='Delete Keyframe') + + layout.separator() + op = layout.operator("rigpicker.call_operator", text='Move') + op.operator="transform.translate" + op.invoke = True + op.view_3d = True + + layout.operator("node.picker_transform", text='Rotate').mode='ROTATE' + layout.operator("node.picker_transform", text='Scale').mode='SCALE' + layout.separator() + layout.operator("rigpicker.call_operator", text='Reset Bone').operator="animtoolbox.reset_bone" + layout.operator("rigpicker.call_operator", text='Clear Location').operator='pose.loc_clear' + layout.operator("rigpicker.call_operator", text='Clear Rotation').operator='pose.rot_clear' + layout.operator("rigpicker.call_operator", text='Clear Scale').operator='pose.scale_clear' + def draw_header(self, context): if not context.space_data.tree_type == 'RigPickerTree': self._draw(context) @@ -23,7 +86,6 @@ def draw_header(self, context): layout = self.layout layout.template_header() - #layout.separator_spacer() if not context.space_data.node_tree: ntree = bpy.data.node_groups.get('.rig_picker') @@ -32,10 +94,10 @@ def draw_header(self, context): context.space_data.node_tree = ntree - #layout.template_ID(context.space_data, "node_tree", new="node.new_node_tree") - #layout.separator_spacer() + row = layout.row(align=True) + row.menu("RP_MT_picker") + row.menu("RP_MT_animation") - #layout.operator('rigpicker.reload_picker', icon='FILE_REFRESH', text='') ob = context.object if not ob or not ob.type == 'ARMATURE': return @@ -50,22 +112,17 @@ def draw_header(self, context): layout.prop(scn.rig_picker, 'use_pick_bone', icon='PANEL_CLOSE', text='', emboss=False) layout.separator_spacer() - layout.label(text=ob.name) + row = layout.row() + row.enabled = False + row.label(text=ob.name) layout.separator_spacer() - #row.alignment = 'RIGHT' - row = layout.row(align=True) - - for i, picker in enumerate(picker_group.pickers): row.operator('rigpicker.fit_picker', text=f'{i+1}').index=i - #row.separator_spacer() + row.operator('rigpicker.fit_picker', text='', icon='FULLSCREEN_ENTER').index = -1 - - #layout.prop('rigpicker.reload_picker', icon='FILE_REFRESH', text='') - def tools_from_context(context, mode=None): @@ -92,6 +149,8 @@ def poll(cls, context): classes = ( RigPickerTree, + RP_MT_picker, + RP_MT_animation ) diff --git a/constants.py b/constants.py index c560c52..36fecdc 100644 --- a/constants.py +++ b/constants.py @@ -1,2 +1,29 @@ +import gpu +from pathlib import Path -PICKERS = {} \ No newline at end of file +class LazyDict(dict): + def __getitem__(self, k): + v = super().__getitem__(k) + if callable(v): + v = v() + super().__setitem__(k, v) + return v + + def get(self, k, default=None): + if k in self: + return self.__getitem__(k) + return default + + +PICKERS = {} + + +MODULE_DIR = Path(__file__).parent +SHADER_DIR = MODULE_DIR / 'shaders' + +SHADERS = LazyDict() + +vertex_shader = Path(SHADER_DIR, "dash_shader.vert").read_text(encoding='utf-8') +fragment_shader = Path(SHADER_DIR, "dash_shader.frag").read_text(encoding='utf-8') + +SHADERS['dashed_line'] = lambda : gpu.types.GPUShader(vertex_shader, fragment_shader) diff --git a/core/addon_utils.py b/core/addon_utils.py index 9470719..0dfdba7 100644 --- a/core/addon_utils.py +++ b/core/addon_utils.py @@ -1,6 +1,21 @@ import bpy -from .bl_utils import get_mat +from .bl_utils import get_mat, get_collection_parents + + +def get_picker_collection(ob=None): + """Return the picker collection of an object""" + + if not ob: + ob = bpy.context.object + + for col in ob.users_collection: + if col.rig_picker.enabled: + return col + + if picker_col := next((c for c in get_collection_parents(col) if c.rig_picker.enabled), None): + return picker_col + def is_shape(ob): scn = bpy.context.scene @@ -15,6 +30,7 @@ def is_shape(ob): return False + def get_object_color(ob): if not ob.data.materials: return @@ -29,6 +45,7 @@ def get_object_color(ob): return emit_node.inputs['Color'].default_value + def get_operator_from_id(idname): if not '.' in idname: return diff --git a/core/bl_utils.py b/core/bl_utils.py index 1f858ac..7fd4619 100644 --- a/core/bl_utils.py +++ b/core/bl_utils.py @@ -2,6 +2,34 @@ import bpy +def get_collection_parents(col, root=None, cols=None): + """Return all direct collection parents + + Args: + col (bpy.types.Collection): collection to get parents from + root (bpy.types.Collection, optional): collection to search in (recursive) instead of the scene collection. + Defaults to None. + cols (_type_, optional): for recursivity, store the parent collections. + Defaults to None. + + Returns: + list[bpy.types.Collection]: a list of direct parents + """ + + if cols is None: + cols = [] + + if root is None: + root = bpy.context.scene.collection + + for sub in root.children: + if sub == col: + cols.append(root) + + if len(sub.children): + cols = get_collection_parents(col, root=sub, cols=cols) + return cols + def get_view_3d_override(): windows = bpy.context.window_manager.windows areas = [a for w in windows for a in w.screen.areas if a.type == 'VIEW_3D'] @@ -31,6 +59,12 @@ def link_mat_to_object(ob): sl.link = 'OBJECT' sl.material = m +def eval_attr(ob, name): + resolved = ob + for o in name.split("."): + resolved = getattr(resolved, o) + return resolved + def flip_name(name): if not name: return @@ -41,6 +75,19 @@ def flip_name(name): else: return bpy.utils.flip_name(name) +def split_path(path) : + try : + bone_name = path.split('["')[1].split('"]')[0] + except Exception: + bone_name = None + + try : + prop_name = path.split('["')[2].split('"]')[0] + except Exception: + prop_name = None + + return bone_name, prop_name + def hide_layers(args): """ """ ob = bpy.context.object diff --git a/core/picker.py b/core/picker.py index 4a5f1b6..ed84ddc 100644 --- a/core/picker.py +++ b/core/picker.py @@ -232,7 +232,7 @@ class BoneShape(Shape): if self.hide: return self.draw_hided() - elif self.bone.custom_shape_scale_xyz.length < 0.00001: + elif self.bone and self.bone.custom_shape_scale_xyz.length < 0.00001: return self.draw_zero_scaled() # Draw Fill @@ -258,8 +258,9 @@ class BoneShape(Shape): def assign_bone_event(self): #print('assign_bone_event', self) + scn = bpy.context.scene - rig = scn.rig_picker.rig + rig = self.picker.rig source_object = scn.objects.get(self.source_name) if not source_object: print(f'Source object {self.source_name} not found') @@ -270,6 +271,9 @@ class BoneShape(Shape): print('You need to have an active bone') return + #print(active_bone, source_object) + #print(rig) + source_object.name = rig.data.bones.active.name source_object.rig_picker.name = rig.data.bones.active.name def release_event(self, mode='SET'): @@ -452,10 +456,10 @@ class Picker: def assign_bone_event(self): for shape in self.shapes: - if shape.type=='bone' and shape.hover: + if shape.type == 'bone' and shape.hover: shape.assign_bone_event() - bpy.ops.rigpicker.save_picker(index=self.index) + bpy.ops._rigpicker.save_picker(index=self.index) def press_event(self, mode='SET'): for shape in self.shapes: @@ -626,11 +630,11 @@ class PickerGroup: if self.timer: self.timer.cancel() - self.timer = threading.Timer(0.5, self.tooltip_event) + self.timer = threading.Timer(0.4, self.tooltip_event) self.timer.start() def press_event(self, mode='SET'): - self.clear_tooltip() + #self.clear_tooltip() for picker in self.pickers: if picker.under_mouse(self.location): @@ -678,7 +682,7 @@ class PickerGroup: #print(self.hover_shape, self.hover_shape.type) if self.hover_shape and self.hover_shape.type != 'display': - if self.hover_shape.type == 'bone': + if self.hover_shape.type == 'bone' and self.hover_shape.bone: self.tooltip = self.hover_shape.bone.name else: self.tooltip = self.hover_shape.tooltip @@ -706,28 +710,47 @@ class PickerGroup: return center -def get_picker_path(rig, start=None): - picker_path = rig.data.get('rig_picker', {}).get('source') - if not picker_path: - return +def load_picker_data(rig): + if 'pickers' in rig.data.rig_picker: + picker_datas = [p.to_dict() for p in rig.rig_picker['pickers']] + else: + picker_datas = [] - picker_path = bpy.path.abspath(picker_path, library=rig.data.library, start=start) + for picker in rig.data.rig_picker.sources: + picker_path = Path(bpy.path.abspath(picker.source, library=rig.data.library)) + if not picker_path.exists(): + print(f'Picker path not exists: {picker_path.resolve()}') + continue + + print('Load picker from', picker_path.resolve()) + picker_data = json.loads(picker_path.read_text(encoding='utf-8')) + picker_datas.append(picker_data) + + PICKERS[rig] = PickerGroup(rig, picker_datas) + + +def get_picker_path(rig, source, start=None): + picker_path = bpy.path.abspath(source, library=rig.data.library, start=start) return Path(os.path.abspath(picker_path)) def pack_picker(rig, start=None): - picker_path = get_picker_path(rig, start=start) - if picker_path and picker_path.exists(): - if 'rig_picker' not in rig.data.keys(): - rig.data['rig_picker'] = {} + pickers = [] + for picker_source in rig.data.get('rig_picker', {}).get('sources', {}): + picker_path = get_picker_path(rig, picker_source['source'], start) + if not picker_path.exists(): + print(f'{picker_path} not exists') + continue + picker_data = json.loads(picker_path.read_text(encoding='utf-8')) + pickers.append(picker_data) - rig.data['rig_picker']['picker'] = json.loads(picker_path.read_text(encoding='utf-8')) + rig.data['rig_picker']['pickers'] = pickers def unpack_picker(rig): if 'rig_picker' not in rig.data.keys(): return - if 'picker' in rig.data['rig_picker'].keys(): - del rig.data['rig_picker']['picker'] \ No newline at end of file + if 'pickers' in rig.data['rig_picker'].keys(): + del rig.data['rig_picker']['pickers'] \ No newline at end of file diff --git a/core/shape.py b/core/shape.py index 91690e4..a6cbb10 100644 --- a/core/shape.py +++ b/core/shape.py @@ -61,99 +61,119 @@ def contour_loops(bm, vert_index=0, loops=None, vert_indices=None): return loops -def get_picker_data(objects, canvas, rig): - picker_datas = [] - gamma = 1 / 2.2 - if canvas.type =='CURVE': +def get_shape_data(ob, matrix=None, depsgraph=None): + if not matrix: + matrix = Matrix() + if not depsgraph: + depsgraph = bpy.context.evaluated_depsgraph_get() + + gamma = 1 / 2.2 + eval_ob = ob.evaluated_get(depsgraph) + mesh = eval_ob.to_mesh() + + bm = bmesh.new() + bm.from_mesh(mesh) + + + bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.002) + bmesh.ops.dissolve_limit(bm, angle_limit=0.001745, verts=bm.verts, edges=bm.edges) + + bmesh.ops.connect_verts_concave(bm, faces=bm.faces) + bmesh.ops.triangulate(bm, faces=bm.faces) + + edges = [[v.index for v in e.verts] for e in bm.edges if len(e.link_faces)<=1] + bm.to_mesh(mesh) + mesh.update() + bm.clear() + + points = [] + polygons = [] + depths = [] + + for vert in mesh.vertices: + co = matrix @ (ob.matrix_world @ Vector(vert.co)) + points.append([round(co[0]), round(co[1])]) + + depths += [co[2]] + + if depths: + depth = max(depths) + else: + print(f'{ob.name} has no vertices') + depth = 0 + + for face in mesh.polygons: + polygons.append([v for v in face.vertices]) + + color = get_object_color(ob) + if color: + color = [round(pow(c, gamma), 4) for c in color] + else: + color = [0.5, 0.5, 0.5] + + shape = { + 'source_name': ob.name, + 'tooltip': ob.rig_picker.name, + 'depth': depth, + 'points': points, + 'polygons': polygons, + 'edges': edges, + 'color': color, + 'type': ob.rig_picker.shape_type + } + + if shape['type'] =='OPERATOR': + shape['operator'] = ob.rig_picker.operator + shape['shortcut'] = ob.rig_picker.shortcut + + elif shape['type'] =='BONE': + shape['bone'] = ob.rig_picker.name + + eval_ob.to_mesh_clear() + + return shape + + +def get_picker_data(collection): + picker_data = [] + + depsgraph = bpy.context.evaluated_depsgraph_get() + canvas = collection.rig_picker.canvas + + if canvas.type == 'CURVE': canvas_points = canvas.data.splines[0].points else: canvas_points = canvas.data.vertices canvas_coords = [canvas.matrix_world@Vector((p.co)) for p in canvas_points] - height = max(canvas_coords).y - min(canvas_coords).y - width = max(canvas_coords).x - min(canvas_coords).x + height = abs(max(canvas_coords).y - min(canvas_coords).y) + width = abs(max(canvas_coords).x - min(canvas_coords).x) - canvas_center = sum(canvas_coords, Vector()) / len(canvas_coords) - canvas_scale_fac = 4096 / max(height, width)# Reference height for the canvas + center = sum(canvas_coords, Vector()) / len(canvas_coords) + scale = 2048 / max(height, width)# Reference height for the canvas - objects.append(canvas) - - dg = bpy.context.evaluated_depsgraph_get() + matrix = Matrix.Translation(-center) + matrix = Matrix.Scale(scale, 4) @ matrix #sorted by their z axes - for ob in sorted(objects, key=lambda x: bound_box_center(x)[2]): + for ob in collection.all_objects: + if ob.instance_collection: + for shape in ob.instance_collection.all_objects: + picker_data.append(get_shape_data(shape, matrix=matrix@ob.matrix_world, depsgraph=depsgraph)) + elif ob.type in ('MESH', 'CURVE', 'FONT'): + picker_data.append(get_shape_data(ob, matrix=matrix, depsgraph=depsgraph)) - print('Storing shape', ob.name) - mesh = bpy.data.meshes.new_from_object(ob.evaluated_get(dg)) - - bm = bmesh.new() - bm.from_mesh(mesh) - bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.002) - bmesh.ops.dissolve_limit(bm, angle_limit=0.001745, verts=bm.verts, edges=bm.edges) - - bmesh.ops.connect_verts_concave(bm, faces=bm.faces) - bmesh.ops.triangulate(bm, faces=bm.faces) - - #bm_loops = contour_loops(bm) - #loops = [[l.index for l in loop] for loop in bm_loops] - loops = [] - - edges = [[v.index for v in e.verts] for e in bm.edges if len(e.link_faces)<=1] - - bm.to_mesh(mesh) - mesh.update() - bm.clear() - - points = [] - #edges = [] - polygons = [] - - for p in mesh.vertices: - point = ob.matrix_world@Vector((p.co)) - point = (point - canvas_center) * canvas_scale_fac - points.append([round(point[0]), round(point[1])]) - - for f in mesh.polygons: - polygons.append([v for v in f.vertices]) - - #for e in mesh.edges: - # - # edges.append([v for v in e.vertices]) - - color = get_object_color(ob) - if color: - color = [round(pow(c, gamma), 4) for c in color] else: - color = [0.5, 0.5, 0.5] - - shape = { - 'source_name': ob.name, - 'tooltip': ob.rig_picker.name, - 'points': points, - 'polygons': polygons, - 'edges': edges, - 'loops': loops, - 'color': color, - 'type': 'CANVAS' if ob == canvas else ob.rig_picker.shape_type - } - - if shape['type'] =='OPERATOR': - shape['operator'] = ob.rig_picker.operator - #if ob.rig_picker.arguments: - #shape['arguments'] = ob.rig_picker.arguments - #if ob.rig_picker.shortcut: - shape['shortcut'] = ob.rig_picker.shortcut - - elif shape['type'] =='BONE': - shape['bone'] = ob.rig_picker.name - - picker_datas.append(shape) + #print(f'{ob.name} of type {ob.type} not supported') + continue + + picker_data.sort(key=lambda x : x['depth']) #print(picker_datas) - return picker_datas + return picker_data #rig.data.rig_picker['shapes'] = picker_datas diff --git a/draw_handlers.py b/draw_handlers.py index 938cb7c..6f19ae1 100644 --- a/draw_handlers.py +++ b/draw_handlers.py @@ -5,7 +5,7 @@ from gpu_extras.batch import batch_for_shader import blf from pathlib import Path from .constants import PICKERS -from .core.picker import Picker, PickerGroup +from .core.picker import Picker, PickerGroup, load_picker_data import json @@ -38,55 +38,27 @@ def draw_rect_2d(position, width, height, color): def draw_callback_view(): - sp = bpy.context.space_data + space_data = bpy.context.space_data - if not sp or not sp.tree_type == 'RigPickerTree': + if not space_data or not space_data.tree_type == 'RigPickerTree': return - if not sp.pin: - sp.pin = True - sp.show_region_ui = False + # Use the pin to know if this is the first time this picker window in created to hide the n panel + if not space_data.pin: + space_data.pin = True + space_data.show_region_ui = False ob = bpy.context.object if not ob or ob.type !='ARMATURE' or not ob.data.rig_picker.sources: return if ob not in PICKERS: - if 'pickers' in ob.data.rig_picker: - picker_datas = [s.to_dict() for s in ob.data.rig_picker['pickers']] - else: - picker_datas = [] - for picker in ob.data.rig_picker.sources: - picker_path = Path(bpy.path.abspath(picker.source, library=ob.data.library)) + load_picker_data(ob) - if not picker_path.exists(): - print(f'Picker path not exists: {picker_path.resolve()}') - continue - - print('Load picker from', picker_path.resolve()) - picker_data = json.loads(picker_path.read_text(encoding='utf-8')) - picker_datas.append(picker_data) - #shapes = [s.to_dict() for s in ob.data.rig_picker['shapes']] - - PICKERS[ob] = PickerGroup(ob, picker_datas)#[Picker(ob, shapes=picker_data) for picker_data in picker_datas] - - # if 'picker' in ob.data.rig_picker: - # picker_datas = [s.to_dict() for s in ob.data.rig_picker['picker']] - # else: - # picker_path = Path(bpy.path.abspath(ob.data.rig_picker.source, library=ob.data.library)) - - # if not picker_path.exists(): - # print(f'Picker path not exists: {picker_path.resolve()}') - # return - - # print('Load picker from', picker_path.resolve()) - # picker_datas = json.loads(picker_path.read_text()) - # #shapes = [s.to_dict() for s in ob.data.rig_picker['shapes']] - # PICKERS[ob] = Picker(ob, shapes=picker_datas) - - picker_group = PICKERS.get(ob) + picker_group = PICKERS[ob] picker_group.draw() + def draw_callback_px(): sp = bpy.context.space_data if not sp.tree_type == 'RigPickerTree': @@ -100,23 +72,21 @@ def draw_callback_px(): region = bpy.context.region text = picker_group.tooltip - mouse = picker_group.tooltip_mouse ui_scale = bpy.context.preferences.system.ui_scale #print('Draw text', text) font_id = 0 blf.size(font_id, int(13 * ui_scale)) - margins = [12, 4] + margins = [12, 5] text_size = blf.dimensions(font_id, text) - #text_pos = (mouse[0] - text_size[0]*0.33, mouse[1] - (34*ui_scale)) text_pos = (region.width - text_size[0]-margins[0], margins[1]) bg_pos = (text_pos[0] - margins[0], text_pos[1] - margins[1]-1) bg_size = (text_size[0] + 2*margins[0], text_size[1] + 2*margins[1]) gpu.state.blend_set('ALPHA') - draw_rect_2d(bg_pos, *bg_size, (0.1, 0.1, 0.1, 0.75)) + draw_rect_2d(bg_pos, *bg_size, (0.1, 0.1, 0.1, 0.66)) gpu.state.blend_set('NONE') @@ -126,7 +96,7 @@ def draw_callback_px(): # BLF drawing routine blf.position(font_id, round(text_pos[0]), round(text_pos[1]), 0) - blf.color(font_id, 1, 1, 1, 1) + blf.color(font_id, 0.85, 0.85, 0.85, 1) blf.shadow(font_id , 5, 0.0, 0.0, 0.0, 1) blf.shadow_offset(font_id, 2, -2) diff --git a/operators/picker.py b/operators/picker.py index 0a94619..ae884ae 100644 --- a/operators/picker.py +++ b/operators/picker.py @@ -1,9 +1,11 @@ import bpy -from bpy.props import EnumProperty, IntProperty, BoolProperty -from ..constants import PICKERS -from ..core.bl_utils import get_view_3d_override +from bpy.props import EnumProperty, IntProperty, BoolProperty, StringProperty +from bpy.types import Operator, Menu +from ..constants import PICKERS, SHADERS +from ..core.addon_utils import get_picker_collection +from ..core.bl_utils import get_view_3d_override, split_path, eval_attr from ..core.geometry_utils import bounding_rect -from ..core.picker import get_picker_path, pack_picker, unpack_picker +from ..core.picker import get_picker_path, pack_picker, unpack_picker, load_picker_data #from .func_bgl import draw_callback_px #from .func_bgl import select_bone #from core.picker import * @@ -16,95 +18,28 @@ import json import os -vertex_shader = ''' - layout (location = 0) in vec2 pos; - - flat out vec2 startPos; - out vec2 vertPos; - - uniform mat4 viewMatrix; - - void main() - { - vec4 outPos = viewMatrix * vec4(pos.x, pos.y, 0.0, 1.0); - gl_Position = outPos; - vertPos = pos.xy / outPos.w; - startPos = vertPos; - } - -''' - -fragment_shader = ''' - flat in vec2 startPos; - in vec2 vertPos; - - out vec4 fragColor; - - uniform vec4 color; - uniform float dashSize; - uniform float gapSize; - - void main() - { - vec2 dir = (vertPos.xy - startPos.xy); - float dist = length(dir); - - if (fract(dist / (dashSize + gapSize)) > dashSize/(dashSize + gapSize)) - discard; - fragColor = color; - } -''' - -def draw_callback(self): - #print('draw callback border') - if not self.draw_border: - return - - gpu.state.blend_set('ALPHA') - - #print('DRAW BORDER') - - self.color_shader.bind() - self.color_shader.uniform_float("color", self.bg_color) - self.bg_batch.draw(self.color_shader) - - self.dash_shader.bind() - matrix = gpu.matrix.get_projection_matrix() - - self.dash_shader.uniform_float("color", self.border_color) - self.dash_shader.uniform_float("viewMatrix", matrix) - self.dash_shader.uniform_float("dashSize", 5) - self.dash_shader.uniform_float("gapSize", 4) - - self.contour_batch.draw(self.dash_shader) - - gpu.state.blend_set('NONE') - - -def is_picker_space(context): - sp = context.space_data - if sp and (sp.type == 'NODE_EDITOR' and sp.tree_type == 'RigPickerTree'): +def is_picker_space(space_data=None): + if not space_data: + space_data = bpy.context.space_data + if space_data and (space_data.type == 'NODE_EDITOR' and space_data.tree_type == 'RigPickerTree'): return True return False -class RP_OT_box_select(bpy.types.Operator): - """Tooltip""" +class RP_OT_box_select(Operator): + """Box Select bones in the picker view""" bl_idname = "node.rp_box_select" bl_label = "Picker Box Select" mode: EnumProperty(items=[(i, i.title(), '') for i in ('SET', 'EXTEND', 'SUBSTRACT')]) - color_shader = gpu.shader.from_builtin('UNIFORM_COLOR') - dash_shader = gpu.types.GPUShader(vertex_shader, fragment_shader) - @classmethod def poll(cls, context): - if not is_picker_space(context): + if not is_picker_space(context.space_data): return ob = context.object @@ -120,6 +55,31 @@ class RP_OT_box_select(bpy.types.Operator): return 'SET' ''' + def draw_callback(self): + #print('draw callback border') + if not self.draw_border: + return + + gpu.state.blend_set('ALPHA') + + #print('DRAW BORDER') + + self.color_shader.bind() + self.color_shader.uniform_float("color", self.bg_color) + self.bg_batch.draw(self.color_shader) + + self.dash_shader.bind() + matrix = gpu.matrix.get_projection_matrix() + + self.dash_shader.uniform_float("color", self.border_color) + self.dash_shader.uniform_float("viewMatrix", matrix) + self.dash_shader.uniform_float("dashSize", 5) + self.dash_shader.uniform_float("gapSize", 4) + + self.contour_batch.draw(self.dash_shader) + + gpu.state.blend_set('NONE') + def invoke(self, context, event): #print(f'invoke: type={event.type}, value={event.value}, ctrl={event.ctrl}, shift={event.shift}, alt={event.alt}') @@ -140,7 +100,9 @@ class RP_OT_box_select(bpy.types.Operator): self.bg_color = [1, 1, 1, 0.05] #args = (self, context) - self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback, (self,), 'WINDOW', 'POST_PIXEL') + self.color_shader = gpu.shader.from_builtin('UNIFORM_COLOR') + self.dash_shader = SHADERS['dashed_line'] + self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(self.draw_callback, (), 'WINDOW', 'POST_PIXEL') context.window_manager.modal_handler_add(self) @@ -163,12 +125,12 @@ class RP_OT_box_select(bpy.types.Operator): self.region.tag_redraw() if event.value == 'RELEASE': - return self.release_event() + return self.release_event(context) return {'RUNNING_MODAL'} - def release_event(self): - scn = bpy.context.scene + def release_event(self, context): + scn = context.scene if scn.rig_picker.use_pick_bone: self.picker.assign_bone_event() @@ -181,173 +143,246 @@ class RP_OT_box_select(bpy.types.Operator): bpy.ops.ed.undo_push(message="Box Select") - return self.exit() - - def exit(self): - #print('Border Select Finished') + return self.exit(context) + def exit(self, context): bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW') - self.region.tag_redraw() + context.region.tag_redraw() return {'FINISHED'} - -class RP_OT_picker_transform(bpy.types.Operator): - """Tooltip""" +class RP_OT_picker_transform(Operator): + """Transform Bones in the picker view""" bl_idname = "node.picker_transform" - bl_label = "Move Bone in Picker View" + bl_label = "Transform Bone in Picker View" - mode : EnumProperty(items=[(m, m.title(), '') for m in ('TRANSLATE', 'ROTATE', 'SCALE')]) + mode : EnumProperty(items=[(m, m.title(), '') for m in ('ROTATE', 'SCALE')]) @classmethod def poll(cls, context): - return is_picker_space(context) + return context.selected_pose_bones and is_picker_space(context.space_data) ''' - def execute(self, context): - with context.temp_override(**get_view_3d_override()): - if self.mode == 'TRANSLATE': - bpy.ops.transform.translate("INVOKE_DEFAULT") - elif self.mode == 'ROTATE': - bpy.ops.transform.rotate("INVOKE_DEFAULT") - elif self.mode == 'SCALE': - bpy.ops.transform.resize("INVOKE_DEFAULT") + def draw_callback(self): + gpu.state.blend_set('ALPHA') + gpu.state.line_width_set(2) - return {"FINISHED"} + self.dash_shader.bind() + matrix = gpu.matrix.get_projection_matrix() + + self.dash_shader.uniform_float("color", [0, 0, 0, 1]) + self.dash_shader.uniform_float("viewMatrix", matrix) + self.dash_shader.uniform_float("dashSize", 5) + self.dash_shader.uniform_float("gapSize", 4) + + self.batch = batch_for_shader(self.dash_shader, 'LINE_LOOP', {"pos": [self.view_center, self.mouse]}) + self.batch.draw(self.dash_shader) + + gpu.state.line_width_set(1) + gpu.state.blend_set('NONE') ''' def invoke(self, context, event): self.override = get_view_3d_override() - if self.mode == 'TRANSLATE': - with context.temp_override(**self.override): - if event.alt: - bpy.ops.pose.loc_clear() - else: - bpy.ops.transform.translate("INVOKE_DEFAULT") - return {"FINISHED"} - - elif self.mode == 'ROTATE' and event.alt: - with context.temp_override(**self.override): - bpy.ops.pose.rot_clear() - return {"FINISHED"} - - elif self.mode == 'SCALE' and event.alt: - with context.temp_override(**self.override): - bpy.ops.pose.scale_clear() - return {"FINISHED"} - - self.mouse = event.mouse_region_x, event.mouse_region_y - - self.center = context.active_pose_bone.matrix.translation.copy() + self.view_center = Vector((int(context.region.width / 2), int(context.region.height / 2))) + self.mouse_start = Vector((event.mouse_region_x, event.mouse_region_y)) + self.mouse = Vector((0, 0)) + transform_type = context.scene.tool_settings.transform_pivot_point + self.center = context.active_pose_bone.matrix.to_translation() + if transform_type == 'MEDIAN_POINT': + origins = [b.matrix.to_translation() for b in context.selected_pose_bones] + self.center = sum(origins, Vector()) / len(context.selected_pose_bones) + # self.bone_data = {} + # for bone in context.selected_pose_bones: + # self.bone_data[bone] = bone.matrix.copy() self.bone_data = {b: b.matrix.copy() for b in context.selected_pose_bones} + space = self.override['area'].spaces.active + + view_matrix = space.region_3d.view_matrix + if space.region_3d.view_perspective == 'CAMERA': + view_matrix = context.scene.camera.matrix_world + + context.window.cursor_modal_set('MOVE_X') + self.view_vector = Vector((0, 0, 1)) + if self.mode == 'ROTATE': + self.view_vector.rotate(view_matrix) + + + elif self.mode == 'SCALE': + self.view_vector = Vector((0, 0, 0)) + + self.transform_type = "VIEW" + self.transform_types = ["VIEW", 'GLOBAL', 'LOCAL'] + if context.scene.transform_orientation_slots[0].type == 'LOCAL': + self.transform_types = ["VIEW", 'LOCAL', 'GLOBAL'] + + self.transform_orientation = context.scene.transform_orientation_slots[0].type + + #self.dash_shader = SHADERS['dashed_line'] + + + #self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(self.draw_callback, (), 'WINDOW', 'POST_PIXEL') context.window_manager.modal_handler_add(self) + return {'RUNNING_MODAL'} + def change_transform_type(self): + indices = {0: 1, 1:2, 2:1} + index = self.transform_types.index(self.transform_type) + new_index = indices[index] + self.transform_type = self.transform_types[new_index] + + def release_event(self, context): + scn = bpy.context.scene + + # Insert keyframe + #bpy.ops.ed.undo_push(message="Transform") + if scn.tool_settings.use_keyframe_insert_auto: + try: + bpy.ops.animtoolbox.insert_keyframe() + except Exception: + self.report({"WARNING"}, 'You need animtoolbox addon for inserting keyframe') + + self.exit(context) + + def exit(self, context): + #bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW') + context.window.cursor_modal_restore() + #context.region.tag_redraw() + def modal(self, context, event): - - delta_x = (event.mouse_region_x - self.mouse[0]) / 100 - #delta_y = (event.mouse_region_y - self.mouse[1]) / 1000 - - #for bone, matrix in self.bone_data.items(): - # bone.matrix.#translation = matrix.translation + Vector((delta_x, 0, delta_y)) - center = context.active_pose_bone.tail - scn = context.scene - cam = scn.camera + self.mouse = Vector((event.mouse_region_x, event.mouse_region_y)) + if self.mode == 'ROTATE': + delta = (event.mouse_region_x - self.mouse_start[0]) / 100 + elif self.mode == 'SCALE': + delta = 1 + (event.mouse_region_x - self.mouse_start[0]) / 100 + #delta = (self.mouse - self.view_center).length / (self.mouse_start - self.view_center).length + + + transform_type = self.transform_type + #self.batch = batch_for_shader(self.dash_shader, 'LINE_STRIP', {"pos": [self.mouse_start, self.mouse]}) + #context.area.tag_redraw() - - #rotation_axis = cam.matrix_world.to_3x3().transposed().col[2] - #rotation_matrix = Matrix.Rotation(delta_x, 4, Vector((0, 0, -1)) @ cam.matrix_world) - view_vec = Vector((0, 0, 1)) - view_vec.rotate(cam.matrix_world) - rotation_matrix = Matrix.Rotation(delta_x, 4, view_vec) - - #rot_mat = Matrix.Rotation(delta_x, 4, "Z") - #rot_mat = scn.camera.matrix_world.inverted() @ rot_mat - #rot_mat = Matrix.LocRotScale(center, Euler((0, 0, delta_x)), (1, 1, 1)) - - if event.type == "MOUSEMOVE": - for bone, matrix in self.bone_data.items(): - #center = - - mat = matrix.copy() - mat.translation -= self.center - mat = rotation_matrix @ mat - mat.translation += self.center - - bone.matrix = mat - #rotation_matrix.translation = matrix.translation - - #mat = matrix.copy().to_3x3() - #mat.rotate(rotation_matrix) - #mat = mat.to_4x4() - #mat.translation = matrix.translation - - #bone.matrix = mat - - #bone.matrix = matrix.inverted() @ (rotation_matrix @ bone_mat) - #bone.matrix.translation = 0, 0, 0 - #bone.matrix = rotation_matrix @ matrix - #bone.matrix = matrix @ (matrix.inverted() @ rotation_matrix) - #bone.matrix.translation = matrix.translation - - #print('Rotate', delta_x) - #with context.temp_override(**get_view_3d_override()): - #for bone, matrix in self.bone_data.items(): - # bone.rotation_euler.x = delta_x - #bpy.ops.transform.rotate(value=delta_x) - - #print(delta_x, delta_y) - - if event.type=="LEFTMOUSE" and event.value == 'RELEASE': + #print(event.type, event.value) + if self.mode == 'ROTATE' and event.type == "R" and event.value == 'PRESS': + with context.temp_override(**self.override): + self.exit(context) + bpy.ops.transform.trackball('INVOKE_DEFAULT') return {'FINISHED'} - if event.type == "RIGHTMOUSE": + if event.type == "LEFTMOUSE" and event.value == 'RELEASE': + self.release_event(context) + return {'FINISHED'} + + if event.type in {"RIGHTMOUSE", "ESC"}: for bone, matrix in self.bone_data.items(): bone.matrix = matrix - + self.exit(context) return {'CANCELLED'} + elif event.type == "X" and event.value == 'PRESS': + self.change_transform_type() + self.view_vector = Vector((1, 0, 0)) + #transform_type = self.transform_orientation + + elif event.type == "Y" and event.value == 'PRESS': + self.change_transform_type() + self.view_vector = Vector((0, 1, 0)) + #transform_type = self.transform_orientation + + elif event.type == "Z" and event.value == 'PRESS': + self.change_transform_type() + self.view_vector = Vector((0, 0, 1)) + #transform_type = self.transform_orientation + + elif event.type == "MOUSEMOVE": + + if self.mode == 'ROTATE': + transform_matrix = Matrix.Rotation(delta, 4, self.view_vector) + elif self.mode == 'SCALE': + if self.view_vector.length: + transform_matrix = Matrix.Scale(delta, 4, self.view_vector) + else: + transform_matrix = Matrix.Scale(delta, 4) + + for bone, matrix in self.bone_data.items(): + center = self.center + if scn.tool_settings.transform_pivot_point == "INDIVIDUAL_ORIGINS": + center = matrix.to_translation() + + if self.mode == 'ROTATE': + if transform_type == 'LOCAL': + view_vector = self.view_vector.copy() + view_vector.rotate(matrix) + transform_matrix = Matrix.Rotation(delta, 4, view_vector) + + elif self.mode == 'SCALE': + if transform_type == 'LOCAL': + view_vector = self.view_vector.copy() + view_vector.rotate(matrix) + transform_matrix = Matrix.Scale(delta, 4, view_vector) + + mat = matrix.copy() + mat.translation -= center + + mat = transform_matrix @ mat + + mat.translation += center + + bone.matrix = mat + return {'RUNNING_MODAL'} -class RP_OT_function_execute(bpy.types.Operator): - bl_idname = "rigpicker.function_execute" - bl_label = 'Function Execute' +class RP_OT_toogle_property(Operator): + """Invert a bone custom property""" - shape_index = IntProperty() + bl_idname = "rigpicker.toogle_property" + bl_label = 'Toogle Property' + + data_path : StringProperty() @classmethod def poll(cls, context): - if not is_picker_space(context): - return + return is_picker_space(context.space_data) def execute(self,context): - event = self.event ob = context.object - shape = ob.data.rig_picker['shapes'][self.shape_index] + + try: + value = ob.path_resolve(self.data_path) + except Exception: + return {'CANCELLED'} - function = shape['function'] - if shape.get('variables'): - variables=shape['variables'].to_dict() - else: - variables={} + data = ob + prop_name = self.data_path + #if '.' in self.data_path: + # data, prop = prop_name.rsplit('.', 1) + + value = type(value)(not value) + exec(f'{repr(ob)}.{self.data_path} = {value}') - variables['event']=event - globals()[function](variables) + #setattr(data, prop_name, value) + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='POSE') + + if context.scene.tool_settings.use_keyframe_insert_auto: + bone_name, _ = split_path(self.data_path) + ob.keyframe_insert(data_path=self.data_path, group=bone_name) + + context.area.tag_redraw() return {'FINISHED'} - def invoke(self,context,event): - self.event = event - return self.execute(context) +class RP_OT_reload_picker(Operator): + """Reload the picker shapes""" -class RP_OT_reload_picker(bpy.types.Operator): bl_idname = "rigpicker.reload_picker" bl_label = 'Reload Picker' @@ -356,22 +391,37 @@ class RP_OT_reload_picker(bpy.types.Operator): # if not is_picker_space(context): # return - def execute(self,context): - PICKERS.clear() + def execute(self, context): + #PICKERS.clear() + if context.object.type == 'ARMATURE': + rig = context.object + else: + collection = get_picker_collection(context.object) + rig = collection.rig_picker.rig - for a in context.screen.areas: - a.tag_redraw() + load_picker_data(rig) + + ''' + for area in context.screen.areas: + #print(area.type, is_picker_space(area.spaces.active)) + if is_picker_space(area.spaces.active): + + print('Tag Redraw Region', area.type) + area.regions[0].tag_redraw() + area.tag_redraw() + ''' return {"FINISHED"} -class RP_OT_toogle_bone_layer(bpy.types.Operator): +class RP_OT_toogle_bone_layer(Operator): + """Toogle bone layer visibility when double clicking on a bone""" bl_idname = "rigpicker.toogle_bone_layer" bl_label = 'Toogle Bone Layer' @classmethod def poll(cls, context): - if not is_picker_space(context): + if not is_picker_space(context.space_data): return ob = context.object @@ -396,13 +446,14 @@ class RP_OT_toogle_bone_layer(bpy.types.Operator): return {"FINISHED"} -class RP_OT_context_menu_picker(bpy.types.Operator): +class RP_OT_context_menu_picker(Operator): + """Display Menu with the custom properties of the hovered bone if any or the active bone""" bl_idname = "node.context_menu_picker" bl_label = 'Context Menu Picker' @classmethod def poll(cls, context): - return is_picker_space(context) + return is_picker_space(context.space_data) def execute(self, context): bpy.ops.wm.call_menu(name='RP_MT_context_menu') @@ -410,75 +461,91 @@ class RP_OT_context_menu_picker(bpy.types.Operator): return {"FINISHED"} -class RP_OT_pack_picker(bpy.types.Operator): +class RP_OT_pack_picker(Operator): """Pack Unpack the picker on the rig""" bl_idname = "rigpicker.pack_picker" - bl_label = 'Toogle Bone Layer' + bl_label = 'Pack Picker' @classmethod def poll(cls, context): ob = context.object - return (ob and ob.type == 'ARMATURE' and ob.data.rig_picker.source) + return (ob and ob.type == 'ARMATURE' and ob.data.rig_picker.sources) def execute(self, context): - print('Pack Picker') + print('Pack Pickers...') rig = context.object - picker_path = get_picker_path(rig) - if 'picker' in rig.data.rig_picker.keys(): + if 'pickers' in rig.data.rig_picker.keys(): unpack_picker(rig) self.report({"INFO"}, f'The picker is unpacked') return {"FINISHED"} - if not picker_path.exists(): - self.report({"ERROR"}, f'The path of the picker not exist: {picker_path}') - return {"CANCELLED"} - pack_picker(rig) + + if not rig.data.rig_picker['pickers']: + self.report({"ERROR"}, f'No picker have been packed') + return {"CANCELLED"} + + elif len(rig.data.rig_picker['pickers']) < len(rig.data.rig_picker.sources): + self.report({"WARNING"}, f'No all pickers have been packed') + return {"FINISHED"} + self.report({"INFO"}, f'The picker is packed') return {"FINISHED"} -class RP_OT_call_operator(bpy.types.Operator): +class RP_OT_call_operator(Operator): bl_idname = "rigpicker.call_operator" bl_label = 'Call operator' - operator : bpy.props.StringProperty() - view_3d : bpy.props.BoolProperty() + operator : StringProperty() + arguments : StringProperty() + invoke: BoolProperty() + view_3d : BoolProperty() @classmethod def poll(cls, context): - return is_picker_space(context) + return is_picker_space(context.space_data) - def execute(self, context): - if not self.operator.startswith('bpy.ops'): - print("Operator call_operator can only execute operator") - return {"FINISHED"} - - print('CAll Operator') + @classmethod + def description(cls, context, properties): + try: + op = eval_attr(bpy.ops, properties.operator).get_rna_type() + return op.description + except AttributeError: + return 'Call operator' - context.area.tag_redraw() + def execute(self, context): + #context.area.tag_redraw() override = {} if self.view_3d: override = get_view_3d_override() + + arguments = json.loads(self.arguments or "{}") with context.temp_override(**override): try: - exec(self.operator) + ops = eval_attr(bpy.ops, self.operator) + if self.invoke: + ops('INVOKE_DEFAULT', **arguments) + else: + ops(**arguments) + except Exception as e: - self.report({"ERROR"}, e) + print(e) + self.report({"ERROR"}, f'The operator {self.operator} cannot be called') + return {"CANCELLED"} context.area.tag_redraw() return {"FINISHED"} -class RP_MT_context_menu(bpy.types.Menu): +class RP_MT_context_menu(Menu): bl_label = "Context Menu" - #bl_idname = "RP_MT_context_menu" # Set the menu operators and draw functions def draw(self, context): @@ -491,7 +558,10 @@ class RP_MT_context_menu(bpy.types.Menu): if picker.hover_shape and picker.hover_shape.type == 'bone': bone = picker.hover_shape.bone - + else: + bone = context.active_pose_bone + + if bone: for key in bone.keys(): layout.prop(bone,f'["{key}"]', slider=True) @@ -499,7 +569,51 @@ class RP_MT_context_menu(bpy.types.Menu): #layout.operator("rigidbody.objects_add", text="B Add Passive").type = 'PASSIVE' -class RP_OT_add_picker_source(bpy.types.Operator): +class RP_OT_add_picker_collection(Operator): + bl_idname = "rigpicker.add_picker_collection" + bl_label = "Add a Picker Collection" + bl_description = "Add a Picker Collection" + bl_options = {'UNDO', 'REGISTER'} + + @classmethod + def poll(cls, context): + ob = context.object + return (ob and ob.type == 'ARMATURE') + + def execute(self, context): + scn = context.scene + ob = context.object + + name = ob.name + if name.endswith('_rig'): + name = name[:-4] + + canvas_name = f'canevas_{name}' + canvas_points = [(-0.5, 0.5, 0), (0.5, 0.5, 0), (0.5, -0.5, 0), (-0.5, -0.5, 0)] + canvas_faces = [(0, 1, 2, 3)] + + canvas_mesh = bpy.data.meshes.new(canvas_name) + canvas_mesh.from_pydata(canvas_points, [], canvas_faces) + canvas_mesh.update(calc_edges=True) + + canvas_ob = bpy.data.objects.new(canvas_name, canvas_mesh) + canvas_ob.rig_picker.shape_type = 'DISPLAY' + + col = bpy.data.collections.new(f'Picker {name}') + col.rig_picker.enabled = True + col.rig_picker.rig = ob + col.rig_picker.canvas = canvas_ob + + col.objects.link(canvas_ob) + + scn.collection.children.link(col) + + self.report({"INFO"}, f"New Picker Collection {col.name} Created") + + return {'FINISHED'} + + +class RP_OT_add_picker_source(Operator): bl_idname = "rigpicker.add_picker_source" bl_label = "Add a Picker source" bl_description = "Add a Picker source" @@ -512,7 +626,7 @@ class RP_OT_add_picker_source(bpy.types.Operator): return {'FINISHED'} -class RP_OT_remove_picker_source(bpy.types.Operator): +class RP_OT_remove_picker_source(Operator): bl_idname = "rigpicker.remove_picker_source" bl_label = "Delete a Picker source" bl_description = "Delete a Picker source" @@ -527,7 +641,7 @@ class RP_OT_remove_picker_source(bpy.types.Operator): return {'FINISHED'} -class RP_OT_fit_picker(bpy.types.Operator): +class RP_OT_fit_picker(Operator): bl_idname = "rigpicker.fit_picker" bl_label = "Fit Picker" bl_description = "Fit Picker in 2d view" @@ -537,7 +651,7 @@ class RP_OT_fit_picker(bpy.types.Operator): @classmethod def poll(cls, context): - return is_picker_space(context) and PICKERS.get(context.object) + return is_picker_space(context.space_data) and PICKERS.get(context.object) def execute(self, context): ob = context.object @@ -585,43 +699,58 @@ def register_keymaps(): keymaps.append((km, kmi)) kmi = km.keymap_items.new("rigpicker.call_operator", type="NUMPAD_PERIOD", value="PRESS") - kmi.properties.operator = "bpy.ops.view3d.view_selected()" + kmi.properties.operator = "view3d.view_selected" kmi.properties.view_3d = True keymaps.append((km, kmi)) kmi = km.keymap_items.new("rigpicker.call_operator", type="A", value="PRESS") - kmi.properties.operator = "bpy.ops.pose.select_all(action='SELECT')" + kmi.properties.operator = "pose.select_all" + kmi.properties.arguments = '{"action": "SELECT"}' keymaps.append((km, kmi)) kmi = km.keymap_items.new("rigpicker.call_operator", type="A", value="PRESS", alt=True) - kmi.properties.operator = "bpy.ops.pose.select_all(action='DESELECT')" + kmi.properties.operator = "pose.select_all" + kmi.properties.arguments = '{"action": "DESELECT"}' keymaps.append((km, kmi)) kmi = km.keymap_items.new("rigpicker.toogle_bone_layer", type="LEFTMOUSE", value="DOUBLE_CLICK") keymaps.append((km, kmi)) - kmi = km.keymap_items.new("node.picker_transform", type="G", value="PRESS") - kmi.properties.mode = 'TRANSLATE' + kmi = km.keymap_items.new("rigpicker.call_operator", type="X", value="PRESS") + kmi.properties.operator = "animtoolbox.reset_bone" keymaps.append((km, kmi)) - kmi = km.keymap_items.new("node.picker_transform", type="G", value="PRESS", alt=True) - kmi.properties.mode = 'TRANSLATE' + kmi = km.keymap_items.new("rigpicker.call_operator", type="K", value="PRESS") + kmi.properties.operator = "animtoolbox.insert_keyframe" + keymaps.append((km, kmi)) + + kmi = km.keymap_items.new("anim.keyframe_delete_v3d", type="K", value="PRESS", alt=True) + keymaps.append((km, kmi)) + + kmi = km.keymap_items.new("rigpicker.call_operator", type="G", value="PRESS") + kmi.properties.operator = 'transform.translate' + kmi.properties.view_3d = True + kmi.properties.invoke = True + keymaps.append((km, kmi)) + + kmi = km.keymap_items.new("rigpicker.call_operator", type="G", value="PRESS", alt=True) + kmi.properties.operator = 'pose.loc_clear' keymaps.append((km, kmi)) kmi = km.keymap_items.new("node.picker_transform", type="R", value="PRESS") kmi.properties.mode = 'ROTATE' keymaps.append((km, kmi)) - kmi = km.keymap_items.new("node.picker_transform", type="R", value="PRESS", alt=True) - kmi.properties.mode = 'ROTATE' + kmi = km.keymap_items.new("rigpicker.call_operator", type="R", value="PRESS", alt=True) + kmi.properties.operator = 'pose.rot_clear' keymaps.append((km, kmi)) kmi = km.keymap_items.new("node.picker_transform", type="S", value="PRESS") kmi.properties.mode = 'SCALE' keymaps.append((km, kmi)) - kmi = km.keymap_items.new("node.picker_transform", type="S", value="PRESS", alt=True) - kmi.properties.mode = 'SCALE' + kmi = km.keymap_items.new("rigpicker.call_operator", type="S", value="PRESS", alt=True) + kmi.properties.operator = 'pose.scale_clear' keymaps.append((km, kmi)) kmi = km.keymap_items.new("node.context_menu_picker", type="RIGHTMOUSE", value="PRESS") @@ -641,9 +770,9 @@ def register_keymaps(): keymaps.append((km, kmi)) #km = wm.keyconfigs.addon.keymaps.new(name="View2D") - kmi = km.keymap_items.new("rigpicker.call_operator", type="MIDDLEMOUSE", value="PRESS") - kmi.properties.operator = "bpy.ops.view2d.pan('INVOKE_DEFAULT')" - keymaps.append((km, kmi)) + #kmi = km.keymap_items.new("rigpicker.call_operator", type="MIDDLEMOUSE", value="PRESS") + #kmi.properties.operator = "bpy.ops.view2d.pan('INVOKE_DEFAULT')" + #keymaps.append((km, kmi)) def unregister_keymaps(): @@ -653,7 +782,7 @@ def unregister_keymaps(): classes = ( RP_OT_box_select, - RP_OT_function_execute, + RP_OT_toogle_property, RP_OT_reload_picker, RP_OT_toogle_bone_layer, RP_OT_call_operator, @@ -663,7 +792,8 @@ classes = ( RP_OT_pack_picker, RP_OT_add_picker_source, RP_OT_remove_picker_source, - RP_OT_fit_picker + RP_OT_fit_picker, + RP_OT_add_picker_collection #RP_OT_ui_draw ) @@ -674,6 +804,7 @@ def register(): register_keymaps() + def unregister(): unregister_keymaps() for cls in reversed(classes): diff --git a/operators/shape.py b/operators/shape.py index 8687b58..c74d8c3 100644 --- a/operators/shape.py +++ b/operators/shape.py @@ -8,7 +8,7 @@ from bpy.props import IntProperty from mathutils import Matrix from ..core.shape import get_picker_data -from ..core.addon_utils import is_shape +from ..core.addon_utils import is_shape, get_picker_collection from ..core.bl_utils import link_mat_to_object, flip_name @@ -77,7 +77,7 @@ class RP_OT_name_from_bone(Operator): class RP_OT_mirror_shape(Operator): bl_label = 'Mirror UI shape' bl_idname = 'rigpicker.mirror_shape' - #bl_options = {'REGISTER', 'UNDO'} + #bl_options = g @classmethod def poll(cls, context): @@ -86,7 +86,7 @@ class RP_OT_mirror_shape(Operator): def execute(self,context): scn = context.scene ob = context.object - collection = next((c for c in ob.users_collection if c.rig_picker.enabled), None) + collection = get_picker_collection(ob) objects = context.selected_objects # Remove mirror object: @@ -192,39 +192,33 @@ class RP_OT_save_picker(Operator): scn = context.scene ob = context.object - print('SAve Picker', self.index) - if self.index == -1: - collection = next((c for c in ob.users_collection if c.rig_picker.enabled), None) + collection = get_picker_collection(ob) else: source = context.active_object.data.rig_picker.sources[self.index].source source = Path(bpy.path.abspath(source, library=ob.data.library)).resolve() collection = next((c for c in set(scn.collection.children_recursive) if c.rig_picker.enabled and Path(bpy.path.abspath(c.rig_picker.destination)).resolve() == source), None) - print('source', source) - print('colleciton', collection) if not collection: self.report({"ERROR"}, 'No Picker found') return {'CANCELLED'} canvas = collection.rig_picker.canvas - rig = collection.rig_picker.rig + #rig = collection.rig_picker.rig shapes = [o for o in collection.all_objects if o != canvas and o.type in ('MESH', 'CURVE', 'FONT')] - if not rig: - self.report({'ERROR'}, 'Choose a Rig') - return {'CANCELLED'} - if not canvas: self.report({'ERROR'}, 'Choose a Canvas') return {'CANCELLED'} - data = get_picker_data(shapes, canvas, rig) + picker_data = get_picker_data(collection) picker_path = Path(bpy.path.abspath(collection.rig_picker.destination)) - picker_path.write_text(json.dumps(data)) + + print(f'Save Picker to {picker_path}') + picker_path.write_text(json.dumps(picker_data)) bpy.ops.rigpicker.reload_picker() diff --git a/panels.py b/panels.py deleted file mode 100644 index 32635ed..0000000 --- a/panels.py +++ /dev/null @@ -1,117 +0,0 @@ -import bpy -#import collections -#import inspect -from .core.addon_utils import get_operator_from_id -from .core.bl_utils import get_mat - -import re - - - - -class RP_PT_picker_maker_panel(bpy.types.Panel): - bl_label = 'Rig Picker' - bl_category = 'Rigging' - bl_space_type = 'VIEW_3D' - bl_region_type = 'UI' - - @classmethod - def poll(cls, context): - return context.object - - def draw(self, context): - ob = context.object - scn = context.scene - - layout = self.layout - col = layout.column(align=False) - col.prop_search(scn.rig_picker, 'rig', scn, 'objects', text='Rig ') - col.prop_search(scn.rig_picker, 'canvas', scn, 'objects', text='Canvas ') - col.prop_search(scn.rig_picker, 'symmetry', scn, 'objects', text='Symmetry ') - - if ob.type == 'ARMATURE': - row = layout.row(align=True) - is_packed = ('picker' in ob.data.rig_picker.keys()) - sub_row = row.row(align=True) - sub_row.prop(ob.data.rig_picker, 'source', text='Picker') - sub_row.enabled = not is_packed - row.operator('rigpicker.pack_picker', icon='PACKAGE' if is_packed else 'UGLYPACKAGE', text='') - - - col = layout.column(align=True) - row = col.row(align=True) - row.operator('rigpicker.create_shape', icon='MESH_DATA', text='Create shape') - row.operator('rigpicker.mirror_shape', icon='ARROW_LEFTRIGHT', text='Mirror shape') - col.operator('rigpicker.name_from_bone', icon='SORTALPHA' , text='Name from bones') - col.prop(scn.rig_picker, 'use_pick_bone', icon='EYEDROPPER', text='Auto bone assign') - - - if ob.type !='ARMATURE': - box = layout.box() - col = box.column(align=False) - - material_row = col.row(align=True) - material_row.operator('rigpicker.remove_mat', icon='REMOVE', text='') - material_row.operator('rigpicker.add_mat', icon='ADD', text='') - mat = False - if ob.type in ('MESH', 'CURVE', 'FONT') and ob.data.materials: - mat = get_mat(ob) - if mat and mat.node_tree: - emission_nodes = [n for n in mat.node_tree.nodes if n.type =='EMISSION'] - if emission_nodes: - material_row.prop(emission_nodes[0].inputs[0], 'default_value', text='') - mat = True - if not mat: - material_row.label(text='No Material') - material_row.operator('rigpicker.eyedropper_mat', icon='EYEDROPPER', text='') - - shape_type_row = col.row(align=True) - shape_type_row.prop(ob.rig_picker,'shape_type',expand = True) - shape_type_row.operator('rigpicker.select_shape_type', text='', icon='RESTRICT_SELECT_OFF') - - - if ob.rig_picker.shape_type == 'OPERATOR': - - op_row = col.row(align=True) - op_row.prop(ob.rig_picker, 'operator', text='') - op_row.operator('rigpicker.operator_selector', text='', icon='COLLAPSEMENU') - - if ob.rig_picker.operator: - col.prop(ob.rig_picker, 'name', text='Tooltip') - col.prop(ob.rig_picker,'shortcut', text='Shortcut') - - else: - col.prop(ob.rig_picker, 'name', text='Tooltip') - - ''' - if ob.rig_picker.operator: - op = get_operator_from_id(ob.rig_picker.idname) - if op: - doc = re.findall(r'\(([^\)]+)\)', op.__doc__)[0] - else: - doc = 'Operator not found' - - col.prop(ob.rig_picker,'name', text='Tooltip') - if op: - col.prop(ob.rig_picker,'shortcut', text='Shortcut') - col.prop(ob.rig_picker,'arguments', text='') - col.label(text=doc) - else: - col.label(text=doc) - ''' - - - if ob.rig_picker.shape_type == 'BONE': - if scn.rig_picker.rig: - col.prop_search(ob.rig_picker, 'name', scn.rig_picker.rig.pose, 'bones', text='Bone') - - #layout.separator() - layout.prop(scn.rig_picker, 'destination', text='Filepath') - layout.operator('rigpicker.save_picker', icon='PASTEDOWN', text='Save Picker') - - -classes = ( - RP_PT_picker_maker_panel, -) - -register, unregister = bpy.utils.register_classes_factory(classes) \ No newline at end of file diff --git a/shaders/dash_shader.frag b/shaders/dash_shader.frag new file mode 100644 index 0000000..42ddf80 --- /dev/null +++ b/shaders/dash_shader.frag @@ -0,0 +1,19 @@ + +flat in vec2 startPos; +in vec2 vertPos; + +out vec4 fragColor; + +uniform vec4 color; +uniform float dashSize; +uniform float gapSize; + +void main() +{ + vec2 dir = (vertPos.xy - startPos.xy); + float dist = length(dir); + + if (fract(dist / (dashSize + gapSize)) > dashSize/(dashSize + gapSize)) + discard; + fragColor = color; +} \ No newline at end of file diff --git a/shaders/dash_shader.vert b/shaders/dash_shader.vert new file mode 100644 index 0000000..705e629 --- /dev/null +++ b/shaders/dash_shader.vert @@ -0,0 +1,15 @@ + +layout (location = 0) in vec2 pos; + +flat out vec2 startPos; +out vec2 vertPos; + +uniform mat4 viewMatrix; + +void main() +{ + vec4 outPos = viewMatrix * vec4(pos.x, pos.y, 0.0, 1.0); + gl_Position = outPos; + vertPos = pos.xy / outPos.w; + startPos = vertPos; +} \ No newline at end of file diff --git a/ui.py b/ui.py index 8215ab5..e06e535 100644 --- a/ui.py +++ b/ui.py @@ -2,7 +2,7 @@ import bpy from bpy.types import UIList #import collections #import inspect -from .core.addon_utils import get_operator_from_id +from .core.addon_utils import get_operator_from_id, get_picker_collection from .core.bl_utils import get_mat import re @@ -29,10 +29,13 @@ class RP_PT_picker_maker_panel(bpy.types.Panel): def poll(cls, context): return context.object + def draw_header_preset(self, context): + self.layout.operator('rigpicker.add_picker_collection', text="", icon='ADD', emboss=False) + def draw(self, context): ob = context.object scn = context.scene - collection = next((c for c in ob.users_collection if c.rig_picker.enabled), None) + collection = get_picker_collection(ob) layout = self.layout col = layout.column(align=False) @@ -51,24 +54,45 @@ class RP_PT_picker_maker_panel(bpy.types.Panel): row.separator(factor=0.5) row.label(text="Sources") - is_packed = ('picker' in ob.data.rig_picker.keys()) + is_packed = ('pickers' in ob.data.rig_picker.keys()) row.operator('rigpicker.pack_picker', icon='PACKAGE' if is_packed else 'UGLYPACKAGE', text='', emboss=False) row.operator("rigpicker.add_picker_source", icon ='ADD', text="", emboss=False) for i, item in enumerate(ob.data.rig_picker.sources): row = col.row(align=True) - is_packed = ('pickers' in ob.data.rig_picker.keys()) + row.enabled = not is_packed row.prop(item, 'source', text='') row.operator("rigpicker.remove_picker_source", icon ='PANEL_CLOSE', text="", emboss=False).index=i + if collection: + #layout.separator() + layout.prop(collection.rig_picker, 'destination', text='Filepath') + layout.operator('rigpicker.save_picker', icon='PASTEDOWN', text='Save Picker') + + +class RP_PT_shape(bpy.types.Panel): + bl_label = 'Shape' + bl_category = 'Rigging' + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_parent_id = "RP_PT_picker_maker_panel" + + def draw(self, context): + ob = context.object + scn = context.scene + collection = get_picker_collection(ob) + + layout = self.layout + col = layout.column(align=False) + if collection: #if context.collection and context.collection.rig_picker.enabled: col = layout.column(align=True) row = col.row(align=True) - row.operator('rigpicker.create_shape', icon='MESH_DATA', text='Create shape') - row.operator('rigpicker.mirror_shape', icon='MOD_MIRROR', text='Mirror shape') - col.operator('rigpicker.name_from_bone', icon='SORTALPHA' , text='Name from bones') - col.prop(scn.rig_picker, 'use_pick_bone', icon='EYEDROPPER', text='Auto bone assign') + row.operator('rigpicker.create_shape', icon='MESH_DATA', text='Create Shape') + row.operator('rigpicker.mirror_shape', icon='MOD_MIRROR', text='Mirror Shape') + col.operator('rigpicker.name_from_bone', icon='SORTALPHA' , text='Name From Bones') + col.prop(scn.rig_picker, 'use_pick_bone', icon='EYEDROPPER', text='Auto Bone Assign') if ob.type != 'ARMATURE': box = layout.box() @@ -107,36 +131,14 @@ class RP_PT_picker_maker_panel(bpy.types.Panel): else: col.prop(ob.rig_picker, 'name', text='Tooltip') - ''' - if ob.rig_picker.operator: - op = get_operator_from_id(ob.rig_picker.idname) - if op: - doc = re.findall(r'\(([^\)]+)\)', op.__doc__)[0] - else: - doc = 'Operator not found' - - col.prop(ob.rig_picker,'name', text='Tooltip') - if op: - col.prop(ob.rig_picker,'shortcut', text='Shortcut') - col.prop(ob.rig_picker,'arguments', text='') - col.label(text=doc) - else: - col.label(text=doc) - ''' - - - if ob.rig_picker.shape_type == 'BONE': + elif ob.rig_picker.shape_type == 'BONE': if collection and collection.rig_picker.rig: col.prop_search(ob.rig_picker, 'name', collection.rig_picker.rig.pose, 'bones', text='Bone') - #layout.separator() - layout.prop(collection.rig_picker, 'destination', text='Filepath') - layout.operator('rigpicker.save_picker', icon='PASTEDOWN', text='Save Picker') - - classes = ( #RP_UL_picker_source, RP_PT_picker_maker_panel, + RP_PT_shape ) register, unregister = bpy.utils.register_classes_factory(classes) \ No newline at end of file