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_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)
|