background_plane_manager/operators/manage_planes.py

418 lines
14 KiB
Python

from pathlib import Path
import bpy
import re
from bpy.types import Operator
from mathutils import Vector
from os.path import abspath
from .. import core
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 = core.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 = core.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"}
class BPM_OT_update_bg_images(Operator):
bl_idname = "bpm.update_bg_images"
bl_label = "Update Bg Planes Images"
bl_description = "Update the loaded images if there are newer version\
\n(source files name needs to end with a version number)"
bl_options = {"REGISTER"} # , "UNDO"
@classmethod
def poll(cls, context):
return True
def execute(self, context):
print('Updating barckground images:')
ct = 0
pb = 0
# TODO: remove '?' after v on clean project
re_version = re.compile(r'_v?\d{3}') # _001 # Underscored 3 digit
for item in context.scene.bg_props.planes:
if item.type != 'bg':
continue
holder = item.plane
if not holder:
continue
for bg_obj in holder.children:
image = core.get_image(bg_obj)
# print(f'holder:{holder.name} > obj({bg_obj.type}):{bg_obj.name} > img:{image.name} > {image.filepath}')
img = Path(bpy.path.abspath(image.filepath))
if not re_version.search(img.stem):
print(f'SKIP: object "{bg_obj.name}" > image "{image.name}" does not have version in name')
continue
img_folder = img.parent
# List in folder : only file versionned, with same suffix and same basename (and suffix after version)
images_list = [i for i in img_folder.iterdir() if i.is_file()\
and re_version.search(i.stem)\
and i.suffix == img.suffix\
and re_version.sub('', img.stem) == re_version.sub('', i.stem)]
if not images_list:
## images_list should never be empty (at least source img should be listed)
print(f'PROBLEM: Empty list with image {image.name} -> not found at: {image.filepath}')
pb += 1
continue
images_list.sort()
last = images_list[-1]
if last == img:
print(f'Already up to date: {image.name}')
continue
## Update with last number
print(f'Update: {img.name} >> {last.name}')
## keep current fielpath head (to not affect relative/absolute)
new_path = Path(image.filepath).parent / last.name
print('New path: ', new_path)
# Update image filepath and name
image.filepath = str(new_path)
image.name = new_path.name
### Propagate new names
## Rename collection
parent_col = core.get_parent_collection(holder)
parent_col.name = new_path.stem
# Rename holder keeping prefix
holder.name = holder.data.name = f'BG_{new_path.stem}'
## Rename object keeping type suffix
if bg_obj.name.endswith(('_texgp', '_texplane', '_texempty')):
type_suffix = f"_tex{bg_obj.name.split('_tex')[-1]}"
else:
type_suffix = ''
bg_obj.name = bg_obj.data.name = f'{new_path.stem}{type_suffix}'
ct += 1
if ct:
self.report({'INFO'}, f'Updated {ct} background images')
else:
self.report({'INFO'}, 'All background images are up to date')
if pb:
self.report({'WARNING'}, f'{pb} images filepaths are not valid!')
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,
BPM_OT_update_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)