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 right_num = re.compile(r'(\d+)(?!.*\d)') 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.search(r'\d+$', img.stem): print(f'SKIP: object "{bg_obj.name}" > image "{image.name}" does not end with version') img_folder = img.parent # List in folder : only file versionned, with same suffix and same basename images_list = [i for i in img_folder.iterdir() if i.is_file()\ and re.search(r'\d+$', i.stem)\ and i.suffix == img.suffix\ and right_num.sub('', img.stem) == right_num.sub('', i.stem)] ## images_list should neveer be empty (source img should be listed) # for i in images_list: # print(i.stem) 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') 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)