Add an unloaded operator (for potential later use) to adjust scale custom prop of the plane to fit camera frame.
418 lines
14 KiB
Python
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_hidden:
|
|
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)
|