import bpy, os import numpy as np import bmesh import mathutils from mathutils import Vector import math from math import sqrt from sys import platform import subprocess """ def get_gp_parent(layer) : if layer.parent_type == "BONE" and layer.parent_bone : return layer.parent.pose.bones.get(layer.parent_bone) else : return layer.parent """ def translate_range(OldValue, OldMin, OldMax, NewMax, NewMin): return (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin def get_matrix(ob) : '''return a copy of the world_matrix, applied object matrix if its a bone''' if isinstance(ob, bpy.types.PoseBone) : return ob.id_data.matrix_world @ ob.matrix.copy()# * ? else : return ob.matrix_world.copy() def set_matrix(gp_frame,mat): for stroke in gp_frame.strokes : for point in stroke.points : point.co = mat @ point.co # get view vector location (the 2 methods work fine) def get_view_origin_position(): ## method 1 # from bpy_extras import view3d_utils # region = bpy.context.region # rv3d = bpy.context.region_data # view_loc = view3d_utils.region_2d_to_origin_3d(region, rv3d, (region.width/2.0, region.height/2.0)) ## method 2 r3d = bpy.context.space_data.region_3d view_loc2 = r3d.view_matrix.inverted().translation return view_loc2 def location_to_region(worldcoords): from bpy_extras import view3d_utils return view3d_utils.location_3d_to_region_2d(bpy.context.region, bpy.context.space_data.region_3d, worldcoords) def region_to_location(viewcoords, depthcoords): from bpy_extras import view3d_utils return view3d_utils.region_2d_to_location_3d(bpy.context.region, bpy.context.space_data.region_3d, viewcoords, depthcoords) def vector_len_from_coord(a, b): '''get either two points or world coordinates and return length''' from mathutils import Vector if type(a) is Vector: return (a - b).length else: return (a.co - b.co).length def transfer_value(Value, OldMin, OldMax, NewMin, NewMax): '''map a value from a range to another (transfer/translate value)''' return (((Value - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin def object_derived_get(ob, scene): if ob.dupli_type != 'NONE' : ob.dupli_list_create(scene) ob_matrix_pairs = [(dob.object, dob.matrix.copy()) for dob in ob.dupli_list] ob.dupli_list_clear() else: ob_matrix_pairs = [(ob, ob.matrix_world.copy())] return ob_matrix_pairs # ----------------- ### Bmesh # ----------------- def link_vert(v,ordered_vert) : for e in v.link_edges : other_vert = e.other_vert(v) if other_vert not in ordered_vert : ordered_vert.append(other_vert) link_vert(other_vert,ordered_vert) return ordered_vert def find_loops(bm) : verts = [] loops = [] print([v for v in bm.verts if len(v.link_edges)==1]) for v in [v for v in bm.verts if len(v.link_edges)==1] : if v not in verts : loop = link_vert(v,[v]) loops.append(loop) for vert in loop : verts.append(vert) return loops def get_perimeter(points) : perimeter = 0 print('pointlen',len(points)) for i,point in enumerate(points) : if i != 0 : perimeter += (Vector(point) -Vector(points[i-1])).length return perimeter def points_to_bm_face(points,depth=0) : bm = bmesh.new() for point in points : bm.verts.new((point[0],point[1],depth)) bm.faces.new(bm.verts) bm.faces.ensure_lookup_table() return bm def gp_stroke_to_bmesh(strokes): strokes_info = [] for stroke in strokes : info = {'stroke' : stroke ,'color':stroke.colorname,'line_width':stroke.line_width} bm = bmesh.new() strength = bm.verts.layers.float.new('strength') pressure = bm.verts.layers.float.new('pressure') select = bm.verts.layers.int.new('select') verts = [] for i,point in enumerate(stroke.points) : v = bm.verts.new(point.co) v[strength] = point.strength v[pressure] = point.pressure v[select] = point.select verts.append(v) if i > 0 : e = bm.edges.new([verts[-1],verts[-2]]) info['bmesh']= bm strokes_info.append(info) return strokes_info # ----------------- ### GP Drawing # ----------------- def simple_draw_gp_stroke(pts, frame, width = 2, mat_id = 0): ''' draw basic stroke by passing list of point 3D coordinate the frame to draw on and optional width parameter (default = 2) ''' stroke = frame.strokes.new() stroke.line_width = width stroke.display_mode = '3DSPACE' stroke.material_index = mat_id # readonly -> stroke.is_nofill_stroke# boundary_stroke stroke.points.add(len(pts)) seq = [i for vec in pts for i in vec]## foreach_set flatlist for speed stroke.points.foreach_set('co', seq) ## one by one # for i, pt in enumerate(pts): # stroke.points.add() # dest_point = stroke.points[i] # dest_point.co = pt return stroke ## OLD - need update def draw_gp_stroke(loop_info, frame, palette, width = 2) : stroke = frame.strokes.new(palette) stroke.line_width = width stroke.display_mode = '3DSPACE'# old -> draw_mode for i,info in enumerate(loop_info) : stroke.points.add() dest_point = stroke.points[i] for attr,value in info.items() : setattr(dest_point,attr,value) return stroke def get_camera_frame_info(cam, distance = 1): ''' return a list with 4 screen corner top-right first rotating CC 4-------1 | | 3-------2 ''' cam_coord = cam.matrix_world.to_translation() scene = bpy.context.scene #shift_x = cam.data.shift_x #shift_y = cam.data.shift_y #cam.data.shift_x = 0 #cam.data.shift_y = 0 frame = cam.data.view_frame(scene) frame = [cam.matrix_world * corner for corner in frame] #frame = [corner+(corner-cam_coord).normalized()*distance for corner in frame] #cam.data.shift_x = shift_x #cam.data.shift_y = shift_y # bpy.context.scene.cursor_location = frame[0]# test by placing cursor return frame def id_convert(fimg,id,operation = 'EQUAL',border = True): new_img = fimg.copy() width = len(new_img[0]) if operation == 'EQUAL' : thresh_mask = new_img[...] == id elif operation == 'GREATER' : thresh_mask = new_img[...] > id elif operation == 'LOWER' : thresh_mask = new_img[...] < id else : return new_img[:] = 1.0 new_img[thresh_mask] = 0.0 if border : # Adding black around the image new_img = np.concatenate((new_img,[[1]*width])) new_img = np.concatenate(([[1]*width],new_img)) new_img = np.insert(new_img,width,1,axis = 1) new_img = np.insert(new_img,0,1,axis = 1) return new_img def remapping(value, leftMin, leftMax, rightMin, rightMax): # Figure out how 'wide' each range is leftSpan = leftMax - leftMin rightSpan = rightMax - rightMin # Convert the left range into a 0-1 range (float) valueScaled = float(value - leftMin) / float(leftSpan) # Convert the 0-1 range into a value in the right range. return rightMin + (valueScaled * rightSpan) # ----------------- ### GP funcs # ----------------- def get_gp_draw_plane(context, obj=None): ''' return tuple with plane coordinate and normal of the curent drawing accordign to geometry''' settings = context.scene.tool_settings orient = settings.gpencil_sculpt.lock_axis #'VIEW', 'AXIS_Y', 'AXIS_X', 'AXIS_Z', 'CURSOR' loc = settings.gpencil_stroke_placement_view3d #'ORIGIN', 'CURSOR', 'SURFACE', 'STROKE' if obj: mat = obj.matrix_world else: mat = context.object.matrix_world if context.object else None # -> placement if loc == "CURSOR": plane_co = context.scene.cursor.location else: # ORIGIN (also on origin if set to 'SURFACE', 'STROKE') if not context.object: plane_co = None else: plane_co = context.object.matrix_world.to_translation()# context.object.location # -> orientation if orient == 'VIEW': #only depth is important, no need to get view vector plane_no = None elif orient == 'AXIS_Y':#front (X-Z) plane_no = Vector((0,1,0)) plane_no.rotate(mat) elif orient == 'AXIS_X':#side (Y-Z) plane_no = Vector((1,0,0)) plane_no.rotate(mat) elif orient == 'AXIS_Z':#top (X-Y) plane_no = Vector((0,0,1)) plane_no.rotate(mat) elif orient == 'CURSOR': plane_no = Vector((0,0,1)) plane_no.rotate(context.scene.cursor.matrix) return plane_co, plane_no def check_angle_from_view(obj=None, plane_no=None, context=None): '''Return angle to obj according to chosen drawing axis''' import math from bpy_extras import view3d_utils if not context: context = bpy.context if not plane_no: _plane_co, plane_no = get_gp_draw_plane(context, obj=obj) view_direction = view3d_utils.region_2d_to_vector_3d(context.region, context.region_data, (context.region.width/2.0, context.region.height/2.0)) angle = math.degrees(view_direction.angle(plane_no)) # correct angle value when painting from other side (seems a bit off...) if angle > 90: angle = abs(90 - (angle - 90)) # over angle warning if angle > 75: return angle, 'ERROR', f"Canvas not aligned at all! (angle: {angle:.2f}°), use Realign GP or change draw axis" if angle > 6: return angle, 'ERROR', f'Canvas not aligned! angle: {angle:.2f}°, use Realign GP' if angle > 3: # below 3 is reasonable (camera seem to view induce error !) return angle, 'INFO', f'Canvas almost aligned (angle: {angle:.2f}°)' if angle == 0: return angle, 'INFO', f'Canvas perfectly aligned (angle: {angle:.2f}°)' # below 1 consider aligned return angle, 'INFO', f'Canvas alignement seem ok (angle: {angle:.2f}°)' ## need big update def create_gp_palette(gp_data_block,info) : palette = gp_data_block.palettes.active name = info["name"] if palette.colors.get(name) : return palette.colors.get(name) else : p = palette.colors.new() for attr,value in info.items() : setattr(p,attr,value) return p """ def get_gp_data_block() : scene = bpy.context.scene if scene.tool_settings.grease_pencil_source == 'OBJECT' and ob and ob.grease_pencil: gp_data_block = ob.grease_pencil elif scene.grease_pencil : gp_data_block = scene.grease_pencil else : gp_data_block =bpy.data.grease_pencil.new('GPencil') scene.grease_pencil = gp_data_block palette = gp_data_block.palettes.active if not palette : palette = gp_data_block.palettes.new("palette") return gp_data_block,palette """ def get_gp_objects(selection=True): '''return selected objects or only the active one''' if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL': print('No active GP object') return [] active = bpy.context.active_object if selection: selection = [o for o in bpy.context.selected_objects if o.type == 'GPENCIL'] if not active in selection: selection += [active] return selection if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL': return [active] return [] def get_gp_datas(selection=True): '''return selected objects or only the active one''' if not bpy.context.active_object or bpy.context.active_object.type != 'GPENCIL': print('No active GP object') return [] active_data = bpy.context.active_object.data if selection: selected = [] for o in bpy.context.selected_objects: if o.type == 'GPENCIL': if o.data not in selected: selected.append(o.data) # selected = [o.data for o in bpy.context.selected_objects if o.type == 'GPENCIL'] if not active_data in selected: selected += [active_data] return selected if bpy.context.active_object and bpy.context.active_object.type == 'GPENCIL': return [active_data] print('EOL. No active GP object') return [] def get_gp_layer(gp_data_block,name) : gp_layer = gp_data_block.layers.get(name) if not gp_layer : gp_layer = gp_data_block.layers.new(name) return gp_layer def get_gp_frame(layer,frame_nb = None) : scene = bpy.context.scene if not frame_nb : frame_nb = scene.frame_current frames={} for i,f in enumerate(layer.frames) : frames[f.frame_number]=i if not scene.frame_current in frames.keys(): dest_frame = layer.frames.new(frame_nb) else : dest_frame = layer.frames[frames[frame_nb]] dest_frame.clear() return dest_frame def get_active_frame(layer_name=None): '''Return active frame of active layer or from layer name passed''' if layer_name: lay = bpy.context.scene.grease_pencil.layers.get(layer_name) if lay: frame = lay.active_frame if frame: return frame else: print ('no active frame for layer', layer_name) else: print('no layers named', layer_name, 'in scene layers') else:#active layer frame = bpy.context.scene.grease_pencil.layers.active.active_frame if frame: return frame else: print ('no active frame on active layer') def get_stroke_2D_coords(stroke): '''return a list containing points 2D coordinates of passed gp stroke object''' return [location_to_region(p.co) for p in stroke.points] '''#foreach method for retreiving multiple other attribute quickly and stack them point_nb = len(stroke.points) seq = [0]*(point_nb*3) stroke.points.foreach_get("co",seq) print("raw_list", seq)#Dbg import numpy as np #can use np.stack to add points infos on same index (on different line/dimension) #https://docs.scipy.org/doc/numpy-1.14.0/reference/generated/numpy.stack.html ''' def get_all_stroke_2D_coords(frame): '''return a list of lists with all strokes's points 2D location''' ## using modification from get_stroke_2D_coords func' return [get_stroke_2D_coords(s) for s in frame.strokes] ## direct #return[[location_to_region(p.co) for p in s.points] for s in frame.strokes] def selected_strokes(frame): '''return all stroke having a point selected as a list of strokes objects''' stlist = [] for i, s in enumerate(frame.strokes): if any(pt.select for pt in s.points): stlist.append(s) return stlist from math import sqrt from mathutils import Vector # ----------------- ### Vector utils 3d # ----------------- def single_vector_length(v): return sqrt((v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2])) def vector_length(A,B): ''''take two Vector3 and return length''' return sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2 + (A[2] - B[2])**2) def vector_length_coeff(size, A, B): ''' Calculate the vector lenght return the coefficient to multiply this vector to obtain a vector of the size given in paramerter ''' Vlength = sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2 + (A[2] - B[2])**2) if Vlength == 0: print('problem Vector lenght == 0 !') return (1) return (size / Vlength) def cross_vector_coord(foo, bar, size): '''Return the coord in space of a cross vector between the two point with specified size''' between = foo - bar #create a generic Up vector (on Y or Z) up = Vector([1.0,0,0]) new = Vector.cross(up, between)#the cross product return a 90 degree Vector if new == Vector([0.0000, 0.0000, 0.0000]): #new == 0 if up vector and between are aligned ! (so change up vector) up = Vector([0,-1.0,0]) new = Vector.cross(up, between)#the cross product return a 90 degree Vector perpendicular = foo + new coeff = vector_length_coeff(size, foo, perpendicular) #position the point in space by adding the new vector multiplied by coeff value to get wanted lenght return (foo + (new * coeff)) def midpoint(p1, p2): '''middle location between 2 vector is calculated by adding the two vector and divide by two''' ##mid = (foo + bar) / 2 return (Vector([(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2, (p1[2] + p2[2]) / 2])) def extrapolate_points_by_length(a,b, length): ''' Return a third point C from by continuing in AB direction Length define BC distance. both vector2 and vector3 ''' # return b + ((b - a).normalized() * length)# one shot ab = b - a if not ab: return None return b + (ab.normalized() * length) # ----------------- ### Vector utils 2d # ----------------- def is_vector_close(a, b, rel_tol=1e-03): '''compare Vector or sequence of value by default tolerance is set on 1e-03 (0.001) ''' return all([math.isclose(i, j, rel_tol=rel_tol) for i, j in zip(a,b)]) def single_vector_length_2d(v): return sqrt((v[0] * v[0]) + (v[1] * v[1])) def vector_length_2d(A,B): ''''take two Vector and return length''' return sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2) def vector_length_coeff_2d(size, A, B): ''' Calculate the vector lenght return the coefficient to multiply this vector to obtain a vector of the size given in paramerter ''' Vlength = sqrt((A[0] - B[0])**2 + (A[1] - B[1])**2) if Vlength == 0: print('problem Vector lenght == 0 !') return (1) return (size / Vlength) def cross_vector_coord_2d(foo, bar, size): '''Return the coord in space of a cross vector between the two point with specified size''' ##middle location between 2 vector is calculated by adding the two vector and divide by two ##mid = (foo + bar) / 2 between = foo - bar #create a generic Up vector (on Y or Z) up = Vector([0,1.0]) new = Vector.cross(up, between)#the cross product return a 90 degree Vector if new == Vector([0.0000, 0.0000]): #new == 0 if up vector and between are aligned ! (so change up vector) up = Vector([0,-1.0,0]) new = Vector.cross(up, between)#the cross product return a 90 degree Vector perpendicular = foo + new coeff = vector_length_coeff(size, foo, perpendicular) #position the point in space by adding the new vector multiplied by coeff value to get wanted lenght return (foo + (new * coeff)) def midpoint_2d(p1, p2): return (Vector([(p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2])) # ----------------- ### Collection management # ----------------- def set_collection(ob, collection, unlink=True) : ''' link an object in a collection and create it if necessary, if unlink object is removed from other collections''' scn = bpy.context.scene col = None visible = False linked = False # check if collection exist or create it for c in bpy.data.collections : if c.name == collection : col = c if not col : col = bpy.data.collections.new(name=collection) # link the collection to the scene's collection if necessary for c in scn.collection.children : if c.name == col.name : visible = True if not visible : scn.collection.children.link(col) # check if the object is already in the collection and link it if necessary for o in col.objects : if o == ob : linked = True if not linked : col.objects.link(ob) # remove object from scene's collection for o in scn.collection.objects : if o == ob : scn.collection.objects.unlink(ob) # if unlink flag we remove the object from other collections if unlink : for c in ob.users_collection : if c.name != collection : c.objects.unlink(ob) # ----------------- ### Path utils # ----------------- def get_addon_prefs(): ''' function to read current addon preferences properties access a prop like this : prefs = get_addon_prefs() option_state = prefs.super_special_option oneliner : get_addon_prefs().super_special_option ''' import os addon_name = os.path.splitext(__name__)[0] preferences = bpy.context.preferences addon_prefs = preferences.addons[addon_name].preferences return (addon_prefs) def open_addon_prefs(): '''Open addon prefs windows with focus on current addon''' from .__init__ import bl_info wm = bpy.context.window_manager wm.addon_filter = 'All' if not 'COMMUNITY' in wm.addon_support: # reactivate community wm.addon_support = set([i for i in wm.addon_support] + ['COMMUNITY']) wm.addon_search = bl_info['name'] bpy.context.preferences.active_section = 'ADDONS' bpy.ops.preferences.addon_expand(module=__package__) bpy.ops.screen.userpref_show('INVOKE_DEFAULT') def open_file(file_path) : '''Open filepath with default browser''' if platform.lower() == 'darwin': subprocess.call(('open', file_path)) elif platform.lower().startswith('win'): os.startfile(file_path) # subprocess.call(('start', file_path)) else:#linux subprocess.call(('xdg-open', file_path)) def open_folder(folderpath): '''Open the folder at given path with default browser''' myOS = platform if myOS.startswith('linux') or myOS.startswith('freebsd'): cmd = 'xdg-open' elif myOS.startswith('win'): cmd = 'explorer' if not folderpath: return('/') else:#elif myOS == "darwin": cmd = 'open' if not folderpath: return('//') folderpath = os.path.normpath(folderpath)# to prevent bad path string fullcmd = [cmd, folderpath] print(fullcmd) # subprocess.call(fullcmd) subprocess.Popen(fullcmd) return ' '.join(fullcmd)#back to string to return and print def detect_OS(): """return str of os name : Linux, Windows, Mac (None if undetected)""" myOS = platform if myOS.startswith('linux') or myOS.startswith('freebsd'):# linux # print("operating system : Linux") return ("Linux") elif myOS.startswith('win'):# Windows # print("operating system : Windows") return ("Windows") elif myOS == "darwin":# OS X # print("operating system : Mac") return ('Mac') else:# undetected print("Cannot detect OS, python 'sys.platform' give :", myOS) return None def fuzzy_match(s1, s2, tol=0.8, case_sensitive=False): '''Tell if two strings are similar using a similarity ratio (0 to 1) value passed as third arg''' from difflib import SequenceMatcher # can also use difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6) if case_sensitive: similarity = SequenceMatcher(None, s1, s2) else: similarity = SequenceMatcher(None, s1.lower(), s2.lower()) return similarity.ratio() > tol def fuzzy_match_ratio(s1, s2, case_sensitive=False): '''Tell how much two passed strings are similar 1.0 being exactly similar''' from difflib import SequenceMatcher if case_sensitive: similarity = SequenceMatcher(None, s1, s2) else: similarity = SequenceMatcher(None, s1.lower(), s2.lower()) return similarity.ratio() def convert_attr(Attr): '''Convert given value to a Json serializable format''' if isinstance(Attr, (mathutils.Vector,mathutils.Color)): return Attr[:] elif isinstance(Attr, mathutils.Matrix): return [v[:] for v in Attr] elif isinstance(Attr,bpy.types.bpy_prop_array): return [Attr[i] for i in range(0,len(Attr))] else: return(Attr) ## confirm pop-up message: def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): '''Show message box with element passed as string or list if _message if a list of lists: if sublist have 2 element: considered a label [text,icon] if sublist have 3 element: considered as an operator [ops_id_name, text, icon] ''' def draw(self, context): for l in _message: if isinstance(l, str): self.layout.label(text=l) else: if len(l) == 2: # label with icon self.layout.label(text=l[0], icon=l[1]) elif len(l) == 3: # ops self.layout.operator_context = "INVOKE_DEFAULT" self.layout.operator(l[0], text=l[1], icon=l[2], emboss=False) # <- highligh the entry ## offset pnale when using row... # row = self.layout.row() # row.label(text=l[1]) # row.operator(l[0], icon=l[2]) if isinstance(_message, str): _message = [_message] bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) # ----------------- ### UI utils # ----------------- ## kmi draw for addon without delete button def draw_kmi(km, kmi, layout): map_type = kmi.map_type ## col = _indented_layout(layout) # layout.label(text=f'{km.name} - {km.space_type} - {km.region_type}') # debug # layout.label(text=f'{km.name}') col = layout.column() if kmi.show_expanded: col = col.column(align=True) box = col.box() else: box = col.column() split = box.split() # header bar row = split.row(align=True) row.prop(kmi, "show_expanded", text="", emboss=False) row.prop(kmi, "active", text="", emboss=False) if km.is_modal: row.separator() row.prop(kmi, "propvalue", text="") else: row.label(text=kmi.name) row = split.row() row.prop(kmi, "map_type", text="") if map_type == 'KEYBOARD': row.prop(kmi, "type", text="", full_event=True) elif map_type == 'MOUSE': row.prop(kmi, "type", text="", full_event=True) elif map_type == 'NDOF': row.prop(kmi, "type", text="", full_event=True) elif map_type == 'TWEAK': subrow = row.row() subrow.prop(kmi, "type", text="") subrow.prop(kmi, "value", text="") elif map_type == 'TIMER': row.prop(kmi, "type", text="") else: row.label() ### / Hided delete button if (not kmi.is_user_defined) and kmi.is_user_modified: row.operator("preferences.keyitem_restore", text="", icon='BACK').item_id = kmi.id else: pass ### NO REMOVE # row.operator( # "preferences.keyitem_remove", # text="", # # Abusing the tracking icon, but it works pretty well here. # icon=('TRACKING_CLEAR_BACKWARDS' if kmi.is_user_defined else 'X') # ).item_id = kmi.id ### Hided delete button / # Expanded, additional event settings if kmi.show_expanded: box = col.box() split = box.split(factor=0.4) sub = split.row() if km.is_modal: sub.prop(kmi, "propvalue", text="") else: # One day... # sub.prop_search(kmi, "idname", bpy.context.window_manager, "operators_all", text="") sub.prop(kmi, "idname", text="") if map_type not in {'TEXTINPUT', 'TIMER'}: sub = split.column() subrow = sub.row(align=True) if map_type == 'KEYBOARD': subrow.prop(kmi, "type", text="", event=True) subrow.prop(kmi, "value", text="") subrow_repeat = subrow.row(align=True) subrow_repeat.active = kmi.value in {'ANY', 'PRESS'} subrow_repeat.prop(kmi, "repeat", text="Repeat") elif map_type in {'MOUSE', 'NDOF'}: subrow.prop(kmi, "type", text="") subrow.prop(kmi, "value", text="") subrow = sub.row() subrow.scale_x = 0.75 subrow.prop(kmi, "any", toggle=True) subrow.prop(kmi, "shift", toggle=True) subrow.prop(kmi, "ctrl", toggle=True) subrow.prop(kmi, "alt", toggle=True) subrow.prop(kmi, "oskey", text="Cmd", toggle=True) subrow.prop(kmi, "key_modifier", text="", event=True) # Operator properties box.template_keymap_item_properties(kmi) ## Modal key maps attached to this operator # if not km.is_modal: # kmm = kc.keymaps.find_modal(kmi.idname) # if kmm: # draw_km(display_keymaps, kc, kmm, None, layout + 1) # layout.context_pointer_set("keymap", km) # ----------------- ### linking utility # ----------------- def link_objects_in_blend(filepath, obj_name_list, link=True): '''Link an object by name from a file, if link is False, append instead of linking''' if isinstance(obj_name_list, str): obj_name_list = [obj_name_list] 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 in obj_name_list] return data_to.objects def check_materials_in_blend(filepath): '''Return a list of all material in remote blend file''' with bpy.data.libraries.load(filepath, link=False) as (data_from, data_to): l = [m for m in data_from.materials] return l def check_objects_in_blend(filepath, avoid_camera=True): '''Return a list of object name in remote blend 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 any(x in o.lower() for x in ('camera', 'draw_cam', 'obj_cam'))] else: l = [o for o in data_from.objects] return l # ----------------- ### props handling # ----------------- def iterate_selector(zone, attr, state, info_attr = None, active_access='active'): '''Iterate with given attribute''' item_number = len(zone) if item_number <= 1: return if getattr(zone, attr) == None: print('no', attr, 'in', zone) return if state: # swap info = None bottom = None new_index = getattr(zone, attr) + state setattr(zone, attr, new_index % item_number) if new_index == item_number: bottom = 1 # bottom reached, cycle to first elif new_index < 0: bottom = -1 # up reached, cycle to last if info_attr: active_item = getattr(zone, active_access) # active by default if active_item: info = getattr(active_item, info_attr) return info, bottom # ----------------- ### Curve handle # ----------------- def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None): '''Create curve at provided location and direction vector''' ## option to create nurbs instaed of bezier ? context = context or bpy.context ## using ops (dirty) # bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=enter_edit, align='WORLD', location=location, scale=(1, 1, 1)) # curve = context.object # curve.name = 'curve_path' # # fast straighten # bpy.ops.curve.handle_type_set(type='VECTOR') # bpy.ops.curve.handle_type_set(type='ALIGNED') # bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL', # orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL', # constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False) ## using data curve_data = bpy.data.curves.new(name, 'CURVE') # ('CURVE', 'SURFACE', 'FONT') curve_data.dimensions = '3D' curve_data.use_path = True curve = bpy.data.objects.new(name, curve_data) spl = curve_data.splines.new('BEZIER') # ('POLY', 'BEZIER', 'NURBS') spl.bezier_points.add(1) # One point already exists for i in range(2): spl.bezier_points[i].handle_left_type = 'VECTOR' # ('FREE', 'VECTOR', 'ALIGNED', 'AUTO') spl.bezier_points[i].handle_right_type = 'VECTOR' spl.bezier_points[1].co = direction # Back to aligned mode for i in range(2): spl.bezier_points[i].handle_right_type = spl.bezier_points[i].handle_left_type = 'ALIGNED' # Select second point spl.bezier_points[1].select_control_point = True spl.bezier_points[1].select_left_handle = True spl.bezier_points[1].select_right_handle = True # link context.scene.collection.objects.link(curve) # curve object settings curve.location = location curve.show_in_front = True # enter edit if enter_edit and context.mode == 'OBJECT': curve.select_set(True) context.view_layer.objects.active = curve bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE ## set viewport overlay visibility for better view if context.space_data.type == 'VIEW_3D': context.space_data.overlay.show_curve_normals = True context.space_data.overlay.normals_length = 0.2 return curve def get_direction_vector_from_enum(string) -> Vector: orient_vectors = { 'FORWARD_X' : Vector((1,0,0)), 'FORWARD_Y' : Vector((0,1,0)), 'FORWARD_Z' : Vector((0,0,1)), 'TRACK_NEGATIVE_X' : Vector((-1,0,0)), 'TRACK_NEGATIVE_Y' : Vector((0,-1,0)), 'TRACK_NEGATIVE_Z' : Vector((0,0,-1)) } return orient_vectors[string] def orentation_track_from_vector(input_vector) -> str: '''return closest world track orientation name from passed vector direction''' orient_vectors = { 'FORWARD_X' : Vector((1,0,0)), 'FORWARD_Y' : Vector((0,1,0)), 'FORWARD_Z' : Vector((0,0,1)), 'TRACK_NEGATIVE_X' : Vector((-1,0,0)), 'TRACK_NEGATIVE_Y' : Vector((0,-1,0)), 'TRACK_NEGATIVE_Z' : Vector((0,0,-1)) } orient = None min_angle = 10000 for track, v in orient_vectors.items(): angle = input_vector.angle(v) if angle < min_angle: min_angle = angle orient = track return orient def create_follow_path_constraint(ob, curve, follow_curve=False): '''return create constraint''' # # Clear bone follow path constraint exiting_fp_constraints = [c for c in ob.constraints if c.type == 'FOLLOW_PATH'] for c in exiting_fp_constraints: ob.constraints.remove(c) # loc = ob.matrix_world @ ob.matrix.to_translation() if ob.location != (0,0,0): old_loc = ob.location ob.location = (0,0,0) print(f'ob moved from {old_loc} to (0,0,0) to counter follow curve offset') const = ob.constraints.new('FOLLOW_PATH') const.target = curve if follow_curve: const.use_curve_follow = True return const ## on_bones: # prefs = get_addon_prefs() # root_name = prefs.tgt_bone # root = ob.pose.bones.get(root_name) # if not root: # return ('ERROR', f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name') # # Clear bone follow path constraint # exiting_fp_constraints = [c for c in root.constraints if c.type == 'FOLLOW_PATH'] # for c in exiting_fp_constraints: # root.constraints.remove(c) # # loc = ob.matrix_world @ root.matrix.to_translation() # if root.name == ('world', 'root') and root.location != (0,0,0): # old_loc = root.location # root.location = (0,0,0) # print(f'root moved from {old_loc} to (0,0,0) to counter follow curve offset') # const = root.constraints.new('FOLLOW_PATH') # const.target = curve # # axis only in this case, should be in addon to prefs # ## determine which axis to use... maybe found orientation in world space from matrix_basis ? # root_world_base_direction = root.bone.matrix_local @ get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis) # const.forward_axis = orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X' # print('const.forward_axis: ', const.forward_axis) # const.use_curve_follow = True # return curve, const