import bpy from mathutils import Vector class ODM_OT_depth_move(bpy.types.Operator): bl_idname = "object.depth_proportional_move" bl_label = "Depth move" bl_description = "Move object in the depth from camera POV while retaining same size in framing" bl_options = {"REGISTER", "UNDO"} @classmethod def poll(cls, context): return context.object and context.object.type != 'CAMERA' # and context.scene.camera def invoke(self, context, event): self.init_mouse_x = event.mouse_x self.cam = bpy.context.scene.camera if not self.cam: self.report({'ERROR'}, 'No active camera') return {"CANCELLED"} self.cam_pos = self.cam.matrix_world.translation self.mode = 'distance' self.objects = [o for o in context.selected_objects if o.type != 'CAMERA'] self.init_mats = [o.matrix_world.copy() for o in self.objects] if self.cam.data.type == 'ORTHO': context.area.header_text_set(f'Move factor: 0.00') # distance is view vector based self.view_vector = Vector((0,0,-1)) self.view_vector.rotate(self.cam.matrix_world) else: self.init_vecs = [o.matrix_world.translation - self.cam_pos for o in self.objects] self.init_dists = [v.length for v in self.init_vecs] context.area.header_text_set(f'Move factor: 0.00 | Mode: {self.mode} (M to switch)') context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): if self.mode == 'distance': factor = 0.1 if event.shift: factor = 0.01 else: # Smaller factor for proportional dist factor = 0.01 if event.shift: factor = 0.001 if event.type in {'MOUSEMOVE'}: diff = (event.mouse_x - self.init_mouse_x) * factor if self.cam.data.type == 'ORTHO': # just push in view vector direction context.area.header_text_set(f'Move factor: {diff:.2f}') for i, obj in enumerate(self.objects): new_vec = self.init_mats[i].translation + (self.view_vector * diff) obj.matrix_world.translation = new_vec else: # Push from camera point and scale accordingly context.area.header_text_set(f'Move factor: {diff:.2f} | Mode: {self.mode} (M to switch)') for i, obj in enumerate(self.objects): if self.mode == 'distance': ## move with the same length for everyone new_vec = self.init_vecs[i] + (self.init_vecs[i].normalized() * diff) else: ## move with proportional factor from individual distance vector to camera new_vec = self.init_vecs[i] + (self.init_vecs[i] * diff) obj.matrix_world.translation = self.cam_pos + new_vec dist_percentage = new_vec.length / self.init_dists[i] obj.scale = self.init_mats[i].to_scale() * dist_percentage if event.type in {'M'} and event.value == 'PRESS': # Switch mode self.mode = 'distance' if self.mode == 'proportional' else 'proportional' if event.type in {'LEFTMOUSE'} and event.value == 'PRESS': context.area.header_text_set(None) return {"FINISHED"} if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS': for i, obj in enumerate(self.objects): obj.matrix_world = self.init_mats[i] context.area.header_text_set(None) return {"CANCELLED"} return {"RUNNING_MODAL"} """ # Own standalone panel class ODM_PT_sudden_depth_panel(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Gpencil" bl_label = "Depth move" def draw(self, context): layout = self.layout row = layout.row() row.operator('object.depth_proportional_move', text='Depth move', icon='TRANSFORM_ORIGINS') """ ### --- REGISTER --- classes=( ODM_OT_depth_move, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)