From 8e223b9f3a8aa42a886dda20ad36cf9fa9bc2ccf Mon Sep 17 00:00:00 2001 From: pullusb Date: Tue, 14 Feb 2023 18:49:48 +0100 Subject: [PATCH] add view3d utility 2.2.1 - added: class View3D to calculate area 3d related coordinates --- CHANGELOG.md | 3 + OP_helpers.py | 32 ++++++- UI_tools.py | 3 + __init__.py | 2 +- view3d_utils.py | 237 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 view3d_utils.py diff --git a/CHANGELOG.md b/CHANGELOG.md index aed12af..3547d3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +2.2.1 + +- added: class View3D to calculate area 3d related coordinates 2.2.0 diff --git a/OP_helpers.py b/OP_helpers.py index 111beee..47322de 100644 --- a/OP_helpers.py +++ b/OP_helpers.py @@ -5,7 +5,7 @@ from mathutils import Vector#, Matrix from pathlib import Path import math from math import radians - +from .view3d_utils import View3D from . import utils @@ -38,6 +38,35 @@ class GPTB_OT_flipx_view(bpy.types.Operator): context.scene.camera.scale.x *= -1 return {"FINISHED"} +class GPTB_OT_view_camera_frame_fit(bpy.types.Operator): + bl_idname = "view3d.view_camera_frame_fit" + bl_label = "View Fit" + bl_description = "Fit the camera in view (view 1:1)" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return context.area.type == 'VIEW_3D' and \ + context.region_data.view_perspective == 'CAMERA' + + def zoom_from_fac(self, zoomfac): + from math import sqrt + return (sqrt(4 * zoomfac) - sqrt(2)) * 50.0 + + def execute(self, context): + # Calculate zoom level to fit in view considering upper and side panel (Not done by native view 1:1) + # context.space_data.region_3d.view_camera_zoom = 0 # (value range: -30, - 600) + view3d = View3D() + view3d.fit_camera_view() + + ## re-center + # context.space_data.region_3d.view_camera_offset = (0,0) + + # With a margin + # Calculate pan to fit view in viewport + + return {"FINISHED"} + class GPTB_OT_rename_data_from_obj(bpy.types.Operator): bl_idname = "gp.rename_data_from_obj" bl_label = "Rename GP From Object" @@ -635,6 +664,7 @@ class GPTB_OT_open_addon_prefs(bpy.types.Operator): classes = ( GPTB_OT_copy_text, GPTB_OT_flipx_view, +GPTB_OT_view_camera_frame_fit, GPTB_OT_rename_data_from_obj, GPTB_OT_draw_cam, GPTB_OT_set_view_as_cam, diff --git a/UI_tools.py b/UI_tools.py index 353a292..426979b 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -86,6 +86,9 @@ class GPTB_PT_sidebar_panel(Panel): else: row.prop(context.scene.camera.data, 'show_passepartout', text='', icon ='OBJECT_HIDDEN') row.prop(context.scene.camera.data, 'passepartout_alpha', text='') + # row = layout.row(align=True) + # row.operator('view3d.view_camera_frame_fit', text = 'Custom fit', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT + row = layout.row(align=True) row.operator('view3d.zoom_camera_1_to_1', text = 'Zoom 1:1', icon = 'ZOOM_PREVIOUS') # FULLSCREEN_EXIT row.operator('view3d.view_center_camera', text = 'Zoom fit', icon = 'FULLSCREEN_ENTER') diff --git a/__init__.py b/__init__.py index 9be94af..a46be09 100755 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "GP toolbox", "description": "Tool set for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (2, 2, 0), +"version": (2, 2, 1), "blender": (3, 0, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", diff --git a/view3d_utils.py b/view3d_utils.py new file mode 100644 index 0000000..4c10957 --- /dev/null +++ b/view3d_utils.py @@ -0,0 +1,237 @@ +import bpy +from mathutils import Vector + +class Rect: + def __init__(self, x, y, width, height): + self.x = x + self.y = y + self.width = width + self.height = height + + @property + def top_left(self): + return Vector((self.x, self.y)) + + @property + def bottom_left(self): + return Vector((self.x, self.y - self.height)) + + @property + def bottom_right(self): + return Vector((self.x + self.width, self.y - self.height)) + + @property + def top_right(self): + return Vector((self.x + self.width, self.y)) + + def __str__(self): + return f'Rect(x={self.x}, y={self.y}, width={self.width}, height={self.height})' + +class View3D: + def __init__(self, area=None): + if area is None: + area = bpy.context.area + if area.type != 'VIEW_3D': + area = next((area for area in bpy.context.screen.areas if area.type == 'VIEW_3D'), None) + self.area = area + + @property + def sidebar(self): + return self.area.regions[3] + + @property + def toolbar(self): + return self.area.regions[2] + + @property + def tool_header(self): + return self.area.regions[1] + + @property + def header(self): + return self.area.regions[0] + + @property + def space(self): + return self.area.spaces.active + + @property + def region(self): + return self.area.regions[5] + + @property + def region_3d(self): + return self.space.region_3d + + @property + def x(self): + return 0 + + @property + def y(self): + return 0 + + @property + def width(self): + return self.region.width + + @property + def height(self): + return self.region.height + + @property + def rect(self): + return Rect(self.x, self.y, self.width, self.height) + + @property + def reduced_rect(self): + w, h = self.region.width, self.region.height + if not bpy.context.preferences.system.use_region_overlap: + return self.rect + + ## Minus tool leftbar + sidebar right + # top_margin = bottom_margin = 0 + # if self.tool_header.alignment == 'TOP': + # top_margin += self.tool_header.height + # else: + # bottom_margin += self.tool_header.height + + ## Set corner values + # top_left = (self.toolbar.width, h - top_margin - 1) + # top_right = (w - self.sidebar.width, h - top_margin - 1) + # bottom_right = (w - self.sidebar.width, bottom_margin + 2) + # bottom_left = (self.toolbar.width, bottom_margin + 2) + + reduced_y = 0 + if self.tool_header.alignment == 'TOP': + reduced_y = self.tool_header.height + + reduced_width = w - self.sidebar.width - self.toolbar.width + reduced_height = h - self.tool_header.height - 1 + + return Rect(self.toolbar.width, h - reduced_y - 1, reduced_width, reduced_height) + # return Rect(self.toolbar.width, h - reduced_y - 1, right_down, left_down) + + def to_2d(self, coord): + from bpy_extras.view3d_utils import location_3d_to_region_2d + return location_3d_to_region_2d(self.region, self.region_3d, coord) + + def to_3d(self, coord, depth_coord=None): + from bpy_extras.view3d_utils import region_2d_to_location_3d + if depth_coord is None: + depth_coord = bpy.context.scene.cursor.location + return region_2d_to_location_3d(self.region, self.region_3d, coord, depth_coord) + + + def get_camera_frame_3d(self, scene=None, camera=None): + if scene is None: + scene = bpy.context.scene + if camera is None: + camera = scene.camera + + frame = camera.data.view_frame() + mat = camera.matrix_world + + return [mat @ v for v in frame] + + def get_camera_frame_2d(self, scene=None, camera=None): + '''View frame Top_right-CW''' + if scene is None: + scene = bpy.context.scene + frame_3d = self.get_camera_frame_3d(scene=scene, camera=camera) + + frame_2d = [self.to_2d(v) for v in frame_3d] + + rd = scene.render + resolution_x = rd.resolution_x * rd.pixel_aspect_x + resolution_y = rd.resolution_y * rd.pixel_aspect_y + ratio_x = min(resolution_x / resolution_y, 1.0) + ratio_y = min(resolution_y / resolution_x, 1.0) + + ## Top right - CW + + frame_width = (frame_2d[1].x - frame_2d[2].x) # same size (square) + frame_height = (frame_2d[0].y - frame_2d[1].y) # same size (square) + + cam_width = (frame_2d[1].x - frame_2d[2].x) * ratio_x + cam_height = (frame_2d[0].y - frame_2d[1].y) * ratio_y + + cam_x = frame_2d[3].x - ((frame_width - cam_width) / 2) + cam_y = frame_2d[3].y - ((frame_height - cam_height) / 2) + + return Rect(cam_x, cam_y, cam_width, cam_height) + + + def zoom_from_fac(self, zoomfac): + from math import sqrt + return (sqrt(4 * zoomfac) - sqrt(2)) * 50.0 + + def fit_camera_view(self): + ## CENTER + self.region_3d.view_camera_offset = (0,0) + + ## ZOOM + + # rect = self.reduced_rect + rect = self.rect + cam_frame = self.get_camera_frame_2d() + # print('width: ', rect.width) + # print('height: ', rect.height) + + + # xfac = rect.width / (cam_frame.width + 4) + # yfac = rect.height / (cam_frame.height + 4) + # # xfac = rect.width / (rect.width - 4) + # # yfac = rect.height / (rect.height - 4) + + scene = bpy.context.scene + rd = scene.render + # resolution_x = rd.resolution_x * rd.pixel_aspect_x + # resolution_y = rd.resolution_y * rd.pixel_aspect_y + # xfac = min(resolution_x / resolution_y, 1.0) + # yfac = min(resolution_y / resolution_x, 1.0) + + # xfac = rect.width / (cam_frame.width * rect.width + 4) + # yfac = rect.height / (cam_frame.height * rect.height + 4) + + # xfac = rect.width / ((rect.width - cam_frame.width) * 2 + 4) + # yfac = rect.height / (rect.height - cam_frame.height + 4) + + # xfac = rect.width / ((rect.width - cam_frame.width * rd.resolution_x) * 2 + 4) + # xfac = rect.width / ((rect.width / cam_frame.width) * rd.resolution_x) + # xfac = rect.width / rd.resolution_x * 2 + # xfac = rect.width / ((cam_frame.width / rect.width) * rd.resolution_x) + # xfac = self.region.width / (rect.width - cam_frame.width) + # xfac = self.region.width / (rect.width - cam_frame.width) + # xfac = ((cam_frame.width - rect.width) / rd.resolution_x) * self.region.width + + xfac = rect.width / cam_frame.width + # xfac = 0.8 + + # xfac = yfac = 0.8 + # xfac = yfac = 1.0 + # print('xfac: ', xfac) # Dbg + # print('yfac: ', yfac) # Dbg + + + # fac = min([xfac, yfac]) # Dbg + fac = xfac + # fac = rd.resolution_x / self.width + + print('fac: ', fac) + print('zoom before', self.region_3d.view_camera_zoom) # Dbg + self.region_3d.view_camera_zoom = self.zoom_from_fac(fac) + print('zoom after', self.region_3d.view_camera_zoom) # Dbg + + + +if __name__ == "__main__": + ## construct view 3d class + view3d = View3D() + print(view3d.rect) + print(view3d.rect.bottom_left) + print(view3d.get_camera_frame_3d()) + ## construct + rect_frame = view3d.get_camera_frame_2d() + view3d.reduced_rect.bottom_left + bpy.context.scene.cursor.location = view3d.to_3d(view3d.reduced_rect.bottom_right) \ No newline at end of file