First Commit
This commit is contained in:
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user