import re from math import pi import bpy import mathutils from bpy.types import Operator from mathutils import Matrix, Vector from .. import core def set_resolution_from_cam_prop(cam=None): rd = bpy.context.scene.render if not cam: cam = bpy.context.scene.camera if not cam: return ('ERROR', 'No active camera') res = cam.get('resolution') if not res: cam['resolution'] = [rd.resolution_x, rd.resolution_y] return ('INFO', 'Cam resolution set from scene') if rd.resolution_x == res[0] and rd.resolution_y == res[1]: return ('INFO', f'Resolution already at {res[0]}x{res[1]}') else: rd.resolution_x, rd.resolution_y = res[0], res[1] return ('INFO', f'Resolution to {res[0]}x{res[1]}') class BPM_OT_swap_cams(Operator): bl_idname = "bpm.swap_cams" bl_label = "Swap Cameras" bl_description = "Toggle between anim and bg cam" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return True def execute(self, context): anim_cam = bpy.context.scene.objects.get('anim_cam') bg_cam = bpy.context.scene.objects.get('bg_cam') if not anim_cam or not bg_cam: self.report({'ERROR'}, 'anim_cam or bg_cam is missing') return {"CANCELLED"} cam = context.scene.camera if not cam: context.scene.camera = anim_cam set_resolution_from_cam_prop() return {"FINISHED"} in_draw = False if cam.parent and cam.name in ('draw_cam', 'action_cam'): if cam.name == 'draw_cam': draw_cam = cam in_draw = True cam = cam.parent ## swap # context.scene.camera = bg_cam if cam is anim_cam else anim_cam if cam is anim_cam: main = context.scene.camera = bg_cam anim_cam.hide_viewport = True bg_cam.hide_viewport = False else: main = context.scene.camera = anim_cam anim_cam.hide_viewport = False bg_cam.hide_viewport = True if in_draw: draw_cam.parent = main draw_cam.data = main.data # back in draw_cam context.scene.camera = draw_cam bg_cam.hide_viewport = anim_cam.hide_viewport = True # set res ret = set_resolution_from_cam_prop(main) if ret: self.report({ret[0]}, ret[1]) return {"FINISHED"} class BPM_OT_send_gp_to_plane(Operator): bl_idname = "bpm.send_gp_to_plane" bl_label = "Send To Plane" bl_description = "Send the selected GPs to current active layer, adjusting scale to keep size in camera" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type != 'CAMERA' def execute(self, context): offset = 0.005 # 0.001 ob = context.object cam = context.scene.camera settings = context.scene.bg_props plane = settings.planes[settings.index].plane plane_mat = plane.matrix_world plane_co = plane_mat.translation plane_no = Vector((0,0,1)) plane_no.rotate(plane_mat) cam_co = cam.matrix_world.translation ob_co = ob.matrix_world.translation if cam.data.type == 'ORTHO': forward = Vector((0,0,-1)) backward = Vector((0,0,1)) forward.rotate(cam.matrix_world) backward.rotate(cam.matrix_world) new_co = mathutils.geometry.intersect_line_plane(ob_co, ob_co+forward, plane_co, plane_no) if not new_co: new_co = mathutils.geometry.intersect_line_plane(ob_co, ob_co+backward, plane_co, plane_no) if not new_co: self.report({'ERROR'}, 'Could not hit background surface by tracing looking in cam direction from obj\nCheck if BG plane is parallel to camera view') return {"CANCELLED"} new_vec = new_co - ob_co new_vec += backward * offset ob.matrix_world.translation += new_vec self.report({'INFO'}, f'Moved {ob.name} to {plane.name} ({new_vec.length:.3f}m)') return {"FINISHED"} # PERSP mode init_dist = (ob_co - cam_co).length new_co = mathutils.geometry.intersect_line_plane(cam_co, ob_co, plane_co, plane_no) print('new_co: ', new_co) if not new_co: self.report({'ERROR'}, 'Grease pencil object might be behind camera\nCould not hit background surface by tracing from cam to BG') return {"CANCELLED"} new_vec = new_co - cam_co new_vec -= new_vec.normalized() * offset # substract offset from cam to ob vector new_dist = new_vec.length # check distance after offset applied for right scaling dist_percentage = new_dist / init_dist ob.matrix_world.translation = cam_co + new_vec # replace from cam to ob ob.scale = ob.matrix_world.to_scale() * dist_percentage # adjust scale self.report({'INFO'}, f'Moved {ob.name} to {plane.name} ({new_dist:.3f}m)') return {"FINISHED"} class BPM_OT_parent_to_bg(Operator): bl_idname = "bpm.parent_to_bg" bl_label = "Parent To Selected Background" bl_description = "Parent selected active object to active Background in list" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.object# and context.object.type != 'CAMERA' def execute(self, context): settings = context.scene.bg_props plane = settings.planes[settings.index].plane plane_list = [i.plane for i in settings.planes] # plane_mat = plane.matrix_world o = bpy.context.object if o in plane_list: self.report({'ERROR'}, 'Selected object must not be a plane') return {"CANCELLED"} mat = o.matrix_world.copy() if o.parent: parent = o.parent o.parent = None self.report({'INFO'}, f'Object "{o.name}" unparented from {parent.name}') else: o.parent = plane self.report({'INFO'}, f'Object "{o.name}" parented to {plane.name}') o.matrix_world = mat return {"FINISHED"} # TODO make the align to plane orientation (change object rotation without affecting points) # need to make a loop with frame_set on each frame (and change only relevant layers...) class BPM_OT_align_to_plane(Operator): bl_idname = "bpm.align_to_plane" bl_label = "Align to GP plane" bl_description = "Align the current GP object to plane\n(change object orientation while keeping points in place)" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return True def execute(self, context): settings = context.scene.bg_props plane = settings.planes[settings.index].plane plane_mat = plane.matrix_world o = bpy.context.object old_mat = o.matrix_world.copy() # reset or align to needed plane # decompose old matrix loc, rot, scale = old_mat.decompose() matloc = Matrix.Translation(loc) #matrot = Matrix() matrot = rot.to_matrix().to_4x4() # recreate a neutral mat scale matscale_x = Matrix.Scale(scale[0], 4,(1,0,0)) matscale_y = Matrix.Scale(scale[1], 4,(0,1,0)) matscale_z = Matrix.Scale(scale[2], 4,(0,0,1)) matscale = matscale_x @ matscale_y @ matscale_z #C.object.rotation_euler = (0,0,0) # mat_90 = Matrix.Rotation(-pi/2, 4, 'X') print("old_mat", old_mat)#Dbg #new_mat = o.matrix_world.copy() new_mat = matloc @ matscale context.object.matrix_world = new_mat for l in o.data.layers: for f in l.frames: for s in f.strokes: for p in s.points: p.co = matrot @ p.co return {"FINISHED"} def place_object_from_facing_cam(ob=None, cam=None, distance=8): ob = ob or bpy.context.object cam = cam or bpy.context.scene.camera if not cam: return scale = ob.matrix_world.to_scale() mat_scale_x = Matrix.Scale(scale[0], 4,(1,0,0)) mat_scale_y = Matrix.Scale(scale[1], 4,(0,1,0)) mat_scale_z = Matrix.Scale(scale[2], 4,(0,0,1)) mat_scale = mat_scale_x @ mat_scale_y @ mat_scale_z mat = cam.matrix_world.copy() cam_mat_inv = mat.inverted() mat_90 = Matrix.Rotation(-pi/2, 4, 'X') # Offset in object local Y mat.translation -= Vector((0, 0, distance)) @ cam_mat_inv mat = mat @ mat_90 @ mat_scale ob.matrix_world = mat class BPM_OT_create_and_place_in_camera(Operator): bl_idname = "bpm.create_and_place_in_camera" bl_label = "Create and Place Gpencil In Camera" bl_description = "Create GP object\ \nCentered and Rotated so X-Z front axis is facing cam\ \nCtrl + Click to place selected object intead of creating" bl_options = {"REGISTER", "UNDO", "INTERNAL"} @classmethod def poll(cls, context): if not context.scene.camera: cls.poll_message_set("Need a scene camera, object is created facing cam") return False return True create : bpy.props.BoolProperty(name='Create', default=False, options={'SKIP_SAVE'}) name : bpy.props.StringProperty(name='Name', default='', options={'SKIP_SAVE'}) distance : bpy.props.FloatProperty(name='Distance', default=8, subtype='DISTANCE') use_light : bpy.props.BoolProperty(name='Use Light', default=False, options={'SKIP_SAVE'}) edit_line_opacity : bpy.props.FloatProperty(name='Edit Line Opacity', description="Edit line opacity for newly created objects\ \nAdvanced users generally like it at 0 (show only selected line in edit mode)\ \nBlender default is 0.5", default=0.0, min=0.0, max=1.0) def invoke(self, context, event): if event.ctrl: self.create = False ## Set placeholder name (Comment to let an empty string) self.name = core.placeholder_name(self.name, context) prefs = core.get_addon_prefs() self.use_light = prefs.use_light self.edit_line_opacity = prefs.edit_line_opacity if not bpy.context.scene.objects.get('bg_cam'): self.report({'ERROR'}, 'No bg_cam') return {"CANCELLED"} ## match current plane distance settings = context.scene.bg_props if settings.planes and settings.planes[settings.index].plane: plane = settings.planes[settings.index].plane self.distance = core.coord_distance_from_cam_straight(plane.matrix_world.to_translation()) - 0.005 else: self.distance = core.coord_distance_from_cam_straight(context.scene.cursor.location) self.distance = max([1.0, self.distance]) # minimum one meter away from cam if self.create: return context.window_manager.invoke_props_dialog(self, width=250) else: if not context.object: self.report({'ERROR'}, 'No active object') return {"CANCELLED"} return self.execute(context) def draw(self, context): layout = self.layout layout.use_property_split = True layout.prop(self, 'name', icon='OUTLINER_OB_GREASEPENCIL') layout.prop(self, 'distance', icon='DRIVER_DISTANCE') layout.separator() layout.prop(self, 'use_light') layout.prop(self, 'edit_line_opacity') def execute(self, context): if self.create: ob_name = core.placeholder_name(self.name, context) ## Create Object prefs = core.get_addon_prefs() gp_data = bpy.data.grease_pencils.new(ob_name) ob = bpy.data.objects.new(ob_name, gp_data) ob.use_grease_pencil_lights = prefs.use_light gp_data.edit_line_color[3] = prefs.edit_line_opacity l = gp_data.layers.new('GP_Layer') l.frames.new(context.scene.frame_current) core.set_collection(ob, 'GP') # Gpencils # Add to bg_plane collection new_item = context.scene.bg_props.planes.add() new_item.plane = ob new_item.type = 'obj' # Set active on last context.scene.bg_props.index = len(context.scene.bg_props.planes) - 1 core.gp_transfer_mode(ob) loaded_palette = False if hasattr(bpy.types, "GPTB_OT_load_default_palette"): res = bpy.ops.gp.load_default_palette() if res == {"FINISHED"}: loaded_palette = True if not loaded_palette: # Append at least line material mat = bpy.data.materials.get('line') if not mat: ## Create basic GP mat mat = bpy.data.materials.new(name='line') bpy.data.materials.create_gpencil_data(mat) gp_data.materials.append(mat) else: ob = context.object ## Place in centered and front facing camera at given distance cam = context.scene.camera place_object_from_facing_cam(ob, cam, self.distance) return {"FINISHED"} classes=( ## Scene BPM_OT_swap_cams, ## GP related BPM_OT_send_gp_to_plane, BPM_OT_parent_to_bg, BPM_OT_create_and_place_in_camera, # BPM_OT_align_to_plane # << TODO ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)