First Commit

This commit is contained in:
ChristopheSeux
2023-09-28 11:34:41 +02:00
parent d302e8d14b
commit e4ae2be1e8
18 changed files with 3579 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
# SPDX-License-Identifier: GPL-2.0-or-later
from . import (manage_objects, manage_planes, import_planes, convert_planes)
modules = (
manage_objects,
manage_planes,
import_planes,
convert_planes
)
# if 'bpy' in locals():
# import importlib
# for mod in modules:
# importlib.reload(mod)
import bpy
def register():
for mod in modules:
mod.register()
def unregister():
for mod in reversed(modules):
mod.unregister()
+165
View File
@@ -0,0 +1,165 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import bpy
import re
# import bpy_extras
# from bpy_extras.io_utils import ImportHelper
from pathlib import Path
from bpy.types import Operator
from .. import fn
from .. constants import BGCOL, MODULE_DIR
## Plane conversion between multiple types: GP plane / image plane / empty references
def convert_background_image(ob, target = 'GPENCIL'):
'''Target in ['GPENCIL', 'EMPTY', 'MESH']'''
if target == ob.type:
print('WARNING', f'{ob.name} Already of type {target}')
return
scn = bpy.context.scene
cam = scn.objects.get('bg_cam')
if not cam:
cam = ob.parent
if not cam:
cam = scn.camera
if not cam:
print('No "bg_cam", no parent, source object is not parented !')
return
# Get info from Source
img_infos = fn.get_image_infos_from_object(ob)
if not img_infos:
print(f'No could not retrieve image source from {ob}')
return
image_data, opacity = img_infos
### Create new plane type
## RENAME to avoid conflict (always keep same name for list scan ?)
## rename source temporarily
## write type in name ?
## name only holder (if there is a holder)
ob.name = f'{ob.name}_temp'
## TODO: ensure image filepath is ok ?
suffix = ''
if target == 'GPENCIL':
## Create GP plane from Texture source
new = fn.import_image_as_gp_reference(
bpy.context,
image_data,
pack_image=False,
)
suffix = '_texgp'
if target == 'MESH':
## HARDCODED text blend path:
# blend = '/s/blender/blender-2.9_scripts/addons/bg_plane_manager/texture_plane.blend'
blend = str(MODULE_DIR / 'texture_plane.blend')
node_group = fn.link_nodegroup(blend, 'texture_plane', link=False)
new = fn.create_image_plane(image_data, node_group)
suffix = '_texplane'
if target == 'EMPTY':
new = fn.create_empty_image(image_data)
suffix = '_texempty'
# Strip after '.' or get original name ?
print('source name: ', new.name)
new.name = fn.clean_image_name(image_data.name) + suffix
print('final name:', new.name)
new['is_background'] = True
## Without holder
## Transfer attributes
# fn.create_plane_driver(new, cam)
# new.parent = cam
# new['distance'] = ob['distance']
# new['scale'] = ob['scale']
# new.lock_location = ob.lock_location # lock only X,Y ?
# new.hide_select = ob.hide_select
# new.rotation_euler = ob.rotation_euler
## With holder
new.parent = ob.parent
## Loc/scale overrided by driver
# new.location = ob.location
# new.scale = ob.scale
## Transfer current transparency
# Destination
fn.set_opacity(new, opacity)
## Set in collection
if not len(ob.users_collection):
fn.set_collection(new, BGCOL)
else:
fn.set_collection(new, ob.users_collection[0].name)
## Remove old object
bpy.data.objects.remove(ob)
return new
class BPM_OT_convert_planes(Operator):
bl_idname = "bpm.convert_planes"
bl_label = "Convert Planes"
bl_description = "Convert as plane all images inside a folder"
bl_options = {"REGISTER"} # , "UNDO"
target_type : bpy.props.EnumProperty(
name="Convert To", description="", default='GPENCIL', options={'ANIMATABLE'},
items=(
('GPENCIL', 'Gpencil Object', 'Convert bg planes to gpencil objects', 0),
('EMPTY', 'Empty Reference', 'Convert bg planes to empty objects', 1),
('MESH', 'Texture Plane', 'Convert bg planes to mesh objects', 2),
))
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=250)
# return self.execute(context)
def execute(self, context):
print('BPM_OT_convert_planes')
holder_list = fn.scan_backgrounds(all_bg=True)
if not holder_list:
self.report({'ERROR'}, 'No Background found to convert, Structure must have a parent holder')
return {'CANCELLED'}
for holder in holder_list:
if not holder.children:
print(f'{holder.name} has not child')
continue
ob = holder.children[0] # take first child FIXME: what if multiple childrens ?
new_bg = convert_background_image(ob, target=self.target_type)
# if isinstance(new_bg, str):
# self.report({'ERROR'}, new_bg)
return {"FINISHED"}
classes=(
BPM_OT_convert_planes,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+263
View File
@@ -0,0 +1,263 @@
# SPDX-License-Identifier: GPL-3.0-or-later
## Standalone based on SPA code
## Code from create_gp_texture_ref.py (Copyright (C) 2023, The SPA Studios. All rights reserved.)
## adapted to create as a single 'GP as image' and fit in camera with driver
import bpy
from bpy_extras.io_utils import ImportHelper
from bpy.props import (StringProperty,
CollectionProperty,
BoolProperty,
EnumProperty)
import json
import bpy_extras
from mathutils import Vector
import os
from pathlib import Path
from .. import fn
from .. constants import *
from .. export_psd_layers import export_psd_bg
def get_json_infos(json_path) -> tuple((list, tuple)):
'''return a tuple with (image paths list, [x, y] original psd resolution'''
import json
setup_json_path = Path(json_path)
setup_json = json.loads(setup_json_path.read_text())
# psd = Path(setup_json['psd']) # path to psd
# image_size = setup_json['image_size'] # image size (reduced)
# png_dir = Path(setup_json['png_dir']) # path to png directory
layers = setup_json['layers'] # dic : name, lbbox, bbox, index, path
org_image_size = setup_json['org_image_size'] # original PSD dimensions
img_list = [Path(l['path']) for l in layers] # (png_dir /l['name']).with_suffix('.png')
return img_list, org_image_size
def setup_bg_camera(image_size, context=None, scn=None, cam_type=None):
'''Create background camera from scene active camera according to background image resolution
image_size ::
cam_type :: wether the BG camera should be set to perspective or orthographic
'''
context = context or bpy.context
scn=scn or context.scene
movie_resolution = scn.render.resolution_x, scn.render.resolution_y
anim_cam = scn.camera
anim_cam_scale = 6 # anim_cam.data.ortho_scale
anim_cam_focale = 50 # anim_cam.data.lens
cam_type = cam_type or anim_cam.data.type
## scale up plane scale and bg_cam ortho_scale according to film res
bg_factor = (anim_cam_scale * image_size[0]) / movie_resolution[0]
#print(f'({anim_cam_scale} * {image_size[0]}) / {movie_resolution[0]} = {bg_factor}')
## scale down the focal according to film res
bg_focale = (anim_cam_focale * movie_resolution[0]) / image_size[0]
#print(f'({anim_cam_focale} * {movie_resolution[0]}) / {image_size[0]} = {bg_focale}')
bg_cam = scn.objects.get('bg_cam')
if not bg_cam:
bg_cam = fn.create_cam(name='bg_cam', type=cam_type, size=bg_factor, focale=bg_focale)
bg_cam['resolution'] = image_size
bg_cam.matrix_world = anim_cam.matrix_world
bg_cam.hide_select = True
bg_cam.lock_location = bg_cam.lock_rotation = bg_cam.lock_scale = [True]*3
fn.set_collection(bg_cam, 'Camera')
fn.set_collection(anim_cam, 'Camera')
return bg_cam
class BPM_OT_import_bg_images(bpy.types.Operator, ImportHelper):
bl_idname = "bpm.import_bg_images"
bl_label = "Import Background images"
bl_description = "Import either a Json, a PSD, multiple files"
bl_options = {"REGISTER"} # , "INTERNAL"
# filename_ext = '.json'
filter_glob: StringProperty(
default=';'.join([f'*{i}' for i in bpy.path.extensions_image]) + ';*.json',
options={'HIDDEN'} )
## Active selection
filepath : StringProperty(
name="File Path",
description="File path used for import",
maxlen= 1024) # the active file
## Handle multi-selection
files: CollectionProperty(
name="File Path",
type=bpy.types.OperatorFileListElement,
) # The filelist collection
## Choice to place before or after ?
import_type : EnumProperty(
name="Import As", description="Type of import to ", default='GPENCIL', options={'ANIMATABLE'},
items=(
('GPENCIL', 'Gpencil Object', 'Import bg planes as gpencil objects', 0),
('EMPTY', 'Empty Reference', 'Import bg planes as empty objects', 1),
('MESH', 'Texture Plane', 'Import bg planes as mesh objects', 2),
))
mode : EnumProperty(
name="Mode", description="", default='REPLACE', options={'ANIMATABLE'},
items=(
('REPLACE', 'Replace Existing', 'Replace the image if already exists', 0),
('SKIP', 'Skip Existing', 'Skip the import if the image alreaady exists in planes', 1),
# ('PURGE', 'Purge', 'When object exists, fully delete it before linking, even associated collection and holder', 2),
))
def execute(self, context):
org_image_size = None
scn = context.scene
active_file = Path(self.filepath)
if len(self.files) == 1 and active_file.suffix.lower() in ('.json', '.psd'):
json_path = None
print('active_file.suffix.lower(): ', active_file.suffix.lower())
if active_file.suffix.lower() == '.psd':
print('Is a PSD')
## Export layers and create json setup file
# Export passes in a 'render' or 'render_hd' folder aside psd
json_path = export_psd_bg(str(active_file))
elif active_file.suffix.lower() == '.json':
print('Is a json')
# Use json data to batch import
json_path = active_file
if not json_path:
self.report({'ERROR'}, 'No json path to load, you can try loading from psd or selecting image file directly')
return {'CANCELLED'}
file_list, org_image_size = get_json_infos(json_path)
folder = Path(json_path).parent # render folder
else:
folder = active_file.parent
# Filter out json (we may want ot import the PSD as imagelisted in bpy.path.extensions_image)
file_list = [folder / f.name for f in self.files if Path(f.name).suffix in bpy.path.extensions_image]
if not file_list:
self.report({'ERROR'}, 'Image file list is empty')
return {'CANCELLED'}
## simple_list
# existing_backgrounds = [o for o in context.scene.objects if o.get('is_background')]
# existing_holders = [o for o in context.scene.objects if o.name.startswith(PREFIX) and o.type == 'MESH' and o.children]
## Has dict
# existing_backgrounds = {o : img_info for o in context.scene.objects if o.get('is_background') and (img_info := o.get('is_background'))}
# FIXME: Use existing background custom prop? : o.get('is_background')
## list of Tuples : [(plane_object, img_data, transparency_value), ...]
existing_backgrounds = [(o, *img_info) for o in context.scene.objects \
if o.get('is_background') and (img_info := fn.get_image_infos_from_object(o)) and o.get('is_background') and o.parent]
existing_backgrounds.sort(key=lambda x: x[0].parent.location.z, reverse=True) # sort by parent (holder) location Z
far_plane = INIT_POS
if existing_backgrounds:
far_plane = existing_backgrounds[-1][0].parent.location.z
## Ensure bg_cam setup (option to use active camera ?)
bg_cam = scn.objects.get('bg_cam')
if not bg_cam:
if not org_image_size:
## Get image size from first file
img = bpy.data.images.load(file_list[0], check_existing=True)
org_image_size = (img.size[0], img.size[1])
bg_cam = setup_bg_camera(
org_image_size, context=context, scn=scn, cam_type=scn.camera.data.type)
## Ensure Background collection
backgrounds = fn.get_col(BGCOL) # Create if needed
for fp in file_list:
print(f'Importing {fp.name}')
current_holder = None
file_stem = Path(fp).stem
# current_bg = next((o for o if o.parent and fn.get_image_infos_from_object(o)), None)
current_bg = next((o for o in existing_backgrounds if o[1].filepath == fp), None)
if current_bg:
current_holder = current_bg.parent
# TODO store opacity or delete existing objects only after loop
if current_bg and self.mode == 'SKIP':
print(f' - SKIP: existing {current_bg.name}')
continue
if current_bg and self.mode == 'REPLACE':
print(f' - DEL: existing {current_bg.name}')
bpy.data.objects.remove(current_bg)
# Import image
if self.import_type == 'GPENCIL':
bg_img = fn.import_image_as_gp_reference(
context=context,
image=fp,
pack_image=False,
)
elif self.import_type == 'MESH':
bg_img = fn.create_image_plane(fp)
elif self.import_type == 'EMPTY':
bg_img = fn.create_empty_image(fp)
bg_img.name = file_stem + '_texgp'
bg_img.hide_select = True
if current_holder:
bg_img.parent = current_holder
fn.set_collection(bg_img, current_holder.users_collection[0].name)
continue
# img, opacity = fn.get_image_infos_from_object(bg_img)
print('bg_img: ', bg_img)
img = fn.get_image(bg_img)
# bg_name = fn.clean_image_name(img.name)
file_stem = file_stem
# Get create collection from image clean name
bg_col = fn.get_col(file_stem, parent=backgrounds)
## Set in collection
bg_col.objects.link(bg_img)
print('img: ', img.name, bg_img.name, bg_img.users_collection)
## create the holder, parent to camera, set driver and set collection. Could also pass 'fp'
holder = fn.create_plane_holder(img, name=PREFIX + file_stem, parent=bg_cam, link_in_col=bg_col)
print('holder: ', holder.name)
fn.create_plane_driver(holder, bg_cam, distance=far_plane)
bg_img.parent = holder
far_plane += 2
fn.reload_bg_list(scene=scn)
self.report({'INFO'}, f'Settings loaded from: {os.path.basename(self.filepath)}')
return {"FINISHED"}
classes=(
BPM_OT_import_bg_images,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
+397
View File
@@ -0,0 +1,397 @@
import re
from math import pi
import bpy
import mathutils
from bpy.types import Operator
from mathutils import Matrix, Vector
from .. import fn
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):
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 = fn.placeholder_name(self.name, context)
prefs = fn.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 = fn.coord_distance_from_cam_straight(plane.matrix_world.to_translation()) - 0.005
else:
self.distance = fn.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 = fn.placeholder_name(self.name, context)
## Create Object
prefs = fn.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)
fn.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
fn.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)
+319
View File
@@ -0,0 +1,319 @@
from pathlib import Path
import bpy
from bpy.types import Operator
from mathutils import Vector
from os.path import abspath
from .. import fn
from .. constants import BGCOL
## Open image folder
## Open change material alpha mode when using texture
## Selection ops (select toggle / all)
## Set distance modal
## Move plane by index
## Rebuild collection (Scan Background collection)
class BPM_OT_open_bg_folder(Operator):
bl_idname = "bpm.open_bg_folder"
bl_label = "Open bg folder"
bl_description = "Open folder of active element"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
if context.scene.bg_props.planes and context.scene.bg_props.planes[context.scene.bg_props.index].type == 'bg':
return True
else:
cls.poll_message_set("Only available when a background image planes is selected")
return False
def execute(self, context):
item = context.scene.bg_props.planes[context.scene.bg_props.index]
ob = item.plane
# name = f'{item.plane.name}_texempty'
tex_obj = next((o for o in ob.children), None)
if not tex_obj:
self.report({'ERROR'}, f'Could not found child for holder: {ob.name}')
return {"CANCELLED"}
img = fn.get_image(tex_obj)
fp = Path(abspath(bpy.path.abspath(img.filepath)))
if not fp.exists():
self.report({'ERROR'}, f'Not found: {fp}')
return {"CANCELLED"}
bpy.ops.wm.path_open(filepath=str(fp.parent))
return {"FINISHED"}
class BPM_OT_change_material_alpha_mode(Operator):
bl_idname = "bpm.change_material_alpha_mode"
bl_label = "Swap Material Aplha"
bl_description = "Swap alpha : blend <-> Clip"
bl_options = {"REGISTER"} # , "UNDO"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
print('----')
# maybe add a shift to act on selection only
on_selection = False
ct = 0
mode = None
for col in bpy.context.scene.collection.children:
if col.name != BGCOL:
continue
for bgcol in col.children:
for o in bgcol.all_objects:
if o.type == 'MESH' and len(o.data.materials):
if on_selection and o.parent and not o.parent.select_get():
continue
if mode is None:
mode = 'BLEND' if o.data.materials[0].blend_method == 'CLIP' else 'CLIP'
print(o.name, '>>', mode)
ct += 1
o.data.materials[0].blend_method = mode
self.report({'INFO'}, f'{ct} swapped to alpha {mode}')
return {"FINISHED"}
class BPM_OT_select_swap(Operator):
bl_idname = "bpm.select_swap_active_bg"
bl_label = "Select / Deselect"
bl_description = "Select/Deselect Active Background"
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
if not plane:
self.report({'ERROR'}, 'could not found current plane\ntry a refresh')
return{'CANCELLED'}
plane.select_set(not plane.select_get())
return {"FINISHED"}
class BPM_OT_select_all(Operator):
bl_idname = "bpm.select_all"
bl_label = "Select All Background"
bl_description = "Select all Background"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
bg_col = bpy.data.collections.get('Background')
if bg_col:
bg_col.hide_select = False
settings = context.scene.bg_props
for p in settings.planes:
if p and p.type == 'bg':
if p.plane.visible_get():
p.plane.select_set(True)
return {"FINISHED"}
# class BPM_OT_frame_backgrounds(Operator):
# bl_idname = "bpm.frame_backgrounds"
# bl_label = "Frame Backgrounds"
# bl_description = "Get out of camera and frame backgrounds from a quarter point or view"
# bl_options = {"REGISTER"}
# @classmethod
# def poll(cls, context):
# return True
# def execute(self, context):
# if context.space_data.region_3d.view_perspective != 'CAMERA':
# context.space_data.region_3d.view_perspective = 'CAMERA'
# return {"FINISHED"}
# ## go out of camera, frame all bg in view at wauter angle
# settings = context.scene.bg_props
# visible_planes = [p.plane for p in settings.planes if p and p.type == 'bg' and p.plane.visible_get()]
# # TODO get out of camera and view frame planes
# context.space_data.region_3d.view_perspective = 'PERSPECTIVE'
# return {"FINISHED"}
class BPM_OT_set_distance(Operator):
bl_idname = "bpm.set_distance"
bl_label = "Distance"
bl_description = "Set distance of the active plane object\
\nShift move all further plane\
\nCtrl move all closer planes\
\nAlt move All"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return True
def invoke(self, context, event):
self.init_mouse_x = event.mouse_x
context.window_manager.modal_handler_add(self)
self.active = context.scene.bg_props.planes[context.scene.bg_props.index].plane
if context.scene.bg_props.move_hided:
self.plane_list = [i.plane for i in context.scene.bg_props.planes if i.type == 'bg']
else:
# o.visible_get() << if not get_view_layer_col(o.users_collection[0]).exclude
self.plane_list = [i.plane for i in context.scene.bg_props.planes if i.type == 'bg' and i.plane.visible_get()]
if not self.plane_list:
self.report({'ERROR'}, f'No plane found in list with current filter')
return {"CANCELLED"}
self.plane_list.sort(key=lambda o: o.get('distance'), reverse=True)
self.active_index = self.plane_list.index(self.active)
self.offset = 0.0
self.further = self.plane_list[:self.active_index+1] # need +1 to include active
self.closer = self.plane_list[self.active_index:]
self.init_dist = [o.get('distance') for o in self.plane_list]
# context.area.header_text_set(f'{self.mode}')
return {'RUNNING_MODAL'}
def modal(self, context, event):
context.area.header_text_set(f'Shift : move active and all further planes |\
Ctrl : move active and all closer planes | Shift + Ctrl :move all| Offset : {self.offset:.3f}m')
if event.type in {'LEFTMOUSE'} and event.value == 'PRESS':
# VALID
context.area.header_text_set(None)
return {"FINISHED"}
if event.type in {'ESC', 'RIGHTMOUSE'} and event.value == 'PRESS':
# CANCEL
context.area.header_text_set(None)
for plane, init_dist in zip(self.plane_list, self.init_dist):
plane['distance'] = init_dist
plane.location = plane.location
return {"CANCELLED"}
if event.type in {'MOUSEMOVE'}:
self.mouse = Vector((event.mouse_x, event.mouse_y))
self.offset = (event.mouse_x - self.init_mouse_x) * 0.1
for plane, init_dist in zip(self.plane_list, self.init_dist):
# reset to init distance dist
plane['distance'] = init_dist
if event.ctrl and event.shift or event.alt:
# all
plane['distance'] = init_dist + self.offset
elif event.shift:
if plane in self.further:
plane['distance'] = init_dist + self.offset
elif event.ctrl:
if plane in self.closer:
plane['distance'] = init_dist + self.offset
else:
if plane == self.active:
plane['distance'] = init_dist + self.offset
## needed esle no update...
plane.location = plane.location
return {"RUNNING_MODAL"}
class BPM_OT_move_plane(Operator):
bl_idname = "bpm.move_plane"
bl_label = "Move Plane"
bl_description = "Move active plane up or down"
bl_options = {"REGISTER", "UNDO"}
direction : bpy.props.StringProperty()
@classmethod
def poll(cls, context):
props = context.scene.bg_props
return props \
and props.planes \
and props.index >= 0 \
and props.index < len(props.planes) \
and props.planes[props.index].type == 'bg'
def execute(self, context):
props = context.scene.bg_props
planes = props.planes
plane_ob = planes[props.index].plane
plane_objects = sorted([p.plane for p in planes if p.type == 'bg'], key=lambda x :x['distance'])
index = plane_objects.index(plane_ob)
if self.direction == 'UP':
if index == 0:
return {"FINISHED"}
other_plane = plane_objects[index - 1]
elif self.direction == 'DOWN':
# If index == len(planes)-1: # Invalid when there are GP as well
if index == len(planes) - 1 or planes[index + 1].type != 'bg':
# End of list or avoid going below a GP object item.
return {"FINISHED"}
other_plane = plane_objects[index + 1]
other_plane['distance'], plane_ob['distance'] = plane_ob['distance'], other_plane['distance']
plane_ob.location = plane_ob.location
other_plane.location = other_plane.location
return {"FINISHED"}
class BPM_OT_reload(Operator):
bl_idname = "bpm.reload_list"
bl_label = "Refresh Bg List"
bl_description = "Refresh the background list (scan for objects using a distance prop)"
bl_options = {"REGISTER"} # , "UNDO"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
error = fn.reload_bg_list()
if error:
if isinstance(error, list):
self.report({'WARNING'}, 'Wrong name for some object, see console:' + '\n'.join(error))
else:
self.report({'ERROR'}, error)
return{'CANCELLED'}
return {"FINISHED"}
classes=(
# BPM_OT_change_background_type,
BPM_OT_select_swap,
BPM_OT_change_material_alpha_mode,
BPM_OT_select_all,
BPM_OT_reload,
BPM_OT_open_bg_folder,
BPM_OT_set_distance,
BPM_OT_move_plane
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)