background_plane_manager/operators/manage_objects.py

407 lines
14 KiB
Python

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):
if not cam:
cam = bpy.context.scene.camera
if not cam:
return ('ERROR', 'No active camera')
res = cam.get('resolution')
if not res:
return ('ERROR', 'Cam has no resolution attribute')
rd = bpy.context.scene.render
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):
rd = bpy.context.scene.render
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"}
if anim_cam:
res = anim_cam.get('resolution')
if not res:
anim_cam['resolution'] = [rd.resolution_x, rd.resolution_y]
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)
gp_col = core.set_collection(ob, 'GP') # Gpencils
## Add Collection to current scene
if not gp_col in context.scene.collection.children_recursive:
context.scene.collection.children.link(gp_col)
# 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)