rapture_20260711004554.png

プロンプト 要件定義

<aside> 💡

プロンプト 要件定義をまず作る

タイトルを自動で最適に

prefix 変名に対応

blender 5 以降専用太陽

コレクション

サブコレクション

オブジェクト名を

初期値を  コード先頭に1行ずつ並べる

選択オブジェクトを画面中央にするボタンを先頭に

画面中央位置の座標を表示 更新ボタンあり

画面水平 画面上下 の大きさ表示

透視投影の場合 視座からの距離を表示

情報をコピーし 1行ずつペーストできるようにする

情報は 小数点1桁に揃える

作成ボタンと アドオンからの切り離しボタンを

上から2番目に作る

リンクパネルと

アドオン削除パネルを最後に設置

=========================================================================

【基本設定】 年月日 秒を ここに

=========================================================================

</aside>

bl_info = {
    "name": "Viewport & Grid Master",
    "author": "Your Name",
    "version": (2, 1),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "画面中央化、ビュー詳細情報取得、Grid・確実な背景色変更",
    "category": "3D View",
}

import bpy
import math
import mathutils
import webbrowser

# =========================================================================
# 【初期設定・定数定義】 (1行ずつ定義)
# =========================================================================
PREFIX = "view_grid_master"
TAB_NAME = "View Grid Tools"
COLLECTION_MAIN = "Master_Collection"
COLLECTION_SUB = "Sub_Collection"
OBJ_DEFAULT_NAME = "Target_Object"
GRID_DEFAULT_1 = 10.0
GRID_DEFAULT_2 = 100.0
LINK_URL = "<https://app.notion.com/p/20260710-399f5dacaf4380f3bd2bcea570b2b686>"

# =========================================================================
# 【ビュー情報取得 関数】
# =========================================================================
def get_rv3d(context):
    for a in context.window.screen.areas:
        if a.type == 'VIEW_3D':
            return a.spaces.active.region_3d
    return None

def get_view_info_data(context):
    rv3d = None
    area = None
    space = None
    for a in context.window.screen.areas:
        if a.type == 'VIEW_3D':
            rv3d = a.spaces.active.region_3d
            area = a
            space = a.spaces.active
            break
            
    if not rv3d or not area or not space: return None
        
    mode = rv3d.view_perspective
    target_pos = rv3d.view_location
    view_dist = rv3d.view_distance
    
    # 視座位置
    cam_pos = rv3d.view_matrix.inverted().translation
    
    # 視線方向 (カメラのローカル -Z方向)
    view_dir = rv3d.view_rotation @ mathutils.Vector((0.0, 0.0, -1.0))
    
    aspect = area.height / area.width if area.width > 0 else 1.0
    sensor = 32.0 
    
    if mode == 'PERSP':
        proj_text = "透視投影"
        is_persp = True
    elif mode == 'ORTHO':
        proj_text = "平行投影"
        is_persp = False
    elif mode == 'CAMERA':
        proj_text = "カメラビュー"
        is_persp = True
    else:
        proj_text = "不明"
        is_persp = False
    
    if is_persp:
        fov_h = 2.0 * math.atan(sensor / (2.0 * space.lens))
        w = 2.0 * view_dist * math.tan(fov_h / 2.0)
        h = w * aspect
    else: 
        w = view_dist * (sensor / space.lens)
        h = w * aspect
        
    return {
        'proj_text': proj_text,
        'cam_pos': cam_pos,
        'view_dir': view_dir,
        'target_pos': target_pos,
        'dist': view_dist,
        'w': w,
        'h': h,
        'is_persp': is_persp
    }

# =========================================================================
# 【オペレーター】
# =========================================================================
class VGM_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_reset_view"
    bl_label = "選択オブジェクトを画面中央にする"
    def execute(self, context):
        rv3d = get_rv3d(context)
        if not rv3d: return {'CANCELLED'}
        target = context.active_object
        rv3d.view_location = target.matrix_world.to_translation() if target else rv3d.view_location.copy()
        rv3d.view_distance = max(target.dimensions.length * 1.5, 10.0) if target else 30.0
        rv3d.view_perspective = 'PERSP'
        context.view_layer.update()
        return {'FINISHED'}

class VGM_OT_update_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_update_info"
    bl_label = "情報を更新"
    def execute(self, context):
        for area in context.screen.areas:
            if area.type == 'VIEW_3D': area.tag_redraw()
        return {'FINISHED'}

class VGM_OT_copy_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_copy_info"
    bl_label = "情報をコピー"
    def execute(self, context):
        info = get_view_info_data(context)
        if not info: return {'CANCELLED'}
        
        lines = []
        lines.append(f"投影モード: {info['proj_text']}")
        
        if info['is_persp']:
            lines.append(f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}")
            
        lines.append(f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
        lines.append(f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
        
        lines.append(f"画面水平の大きさ: {info['w']:.1f}")
        lines.append(f"画面上下の大きさ: {info['h']:.1f}")
        
        if info['is_persp']:
            lines.append(f"視座からの距離: {info['dist']:.1f}")

        # 1行ずつペーストできるように改行(\n)で連結
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
        return {'FINISHED'}

class VGM_OT_set_grid_scale(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_set_grid_scale"
    bl_label = "Grid間隔を設定"
    scale_val: bpy.props.FloatProperty(name="Scale")
    use_custom: bpy.props.BoolProperty(name="カスタム値", default=False)
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        val = props.custom_grid_scale if self.use_custom else self.scale_val
            
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.overlay.grid_scale = val
                        
        self.report({'INFO'}, f"Grid間隔を {val:.1f} に設定しました")
        return {'FINISHED'}

class VGM_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX}_open_url"
    bl_label = "リンク"
    def execute(self, context): webbrowser.open(LINK_URL); return {'FINISHED'}

class VGM_OT_remove_addon(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX}_remove_addon"
    bl_label = "アドオン削除"
    def execute(self, context): unregister(); return {'FINISHED'}

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class VGM_Properties(bpy.types.PropertyGroup):
    custom_grid_scale: bpy.props.FloatProperty(name="指定値", default=50.0, min=0.001)

# =========================================================================
# 【UIパネル】
# =========================================================================
class VGM_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX.upper()}_PT_main_panel"
    bl_label = "ビュー / Grid ツール"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        
        # --- 1. 選択オブジェクト 操作 ---
        layout.operator(f"view3d.{PREFIX}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
        layout.separator(factor=1.5)
        
        # --- 2. ビュー情報表示 ---
        box_info = layout.box()
        row_info = box_info.row(align=True)
        row_info.label(text="ビュー情報:", icon='INFO')
        row_info.operator(f"view3d.{PREFIX}_update_info", text="更新", icon='FILE_REFRESH')
        
        info = get_view_info_data(context)
        if info:
            col = box_info.column(align=True)
            col.label(text=f"[{info['proj_text']}]")
            col.separator()
            
            if info['is_persp']:
                col.label(text=f"架空の視座位置 X: {info['cam_pos'].x:.1f}, Y: {info['cam_pos'].y:.1f}, Z: {info['cam_pos'].z:.1f}")
                
            col.label(text=f"画面中央への視線方向 X: {info['view_dir'].x:.1f}, Y: {info['view_dir'].y:.1f}, Z: {info['view_dir'].z:.1f}")
            col.label(text=f"画面中央位置 X: {info['target_pos'].x:.1f}, Y: {info['target_pos'].y:.1f}, Z: {info['target_pos'].z:.1f}")
            
            col.separator()
            col.label(text=f"画面水平の大きさ: {info['w']:.1f}")
            col.label(text=f"画面上下の大きさ: {info['h']:.1f}")
            
            if info['is_persp']:
                col.separator()
                col.label(text=f"視座からの距離: {info['dist']:.1f}")
                
            box_info.separator()
            box_info.operator(f"view3d.{PREFIX}_copy_info", text="情報をコピー (1行ずつペースト可能)", icon='COPYDOWN')
        else:
            box_info.label(text="3Dビューが見つかりません")
        
        layout.separator(factor=1.5)
            
        # --- 3. Grid / 背景 設定 ---
        box_grid = layout.box()
        box_grid.label(text="Grid / 背景 設定", icon='GRID')
        
        row_preset = box_grid.row(align=True)
        row_preset.operator(f"view3d.{PREFIX}_set_grid_scale", text=f"間隔 {GRID_DEFAULT_1}").scale_val = GRID_DEFAULT_1
        row_preset.operator(f"view3d.{PREFIX}_set_grid_scale", text=f"間隔 {GRID_DEFAULT_2}").scale_val = GRID_DEFAULT_2
        
        row_custom = box_grid.row(align=True)
        row_custom.prop(props, "custom_grid_scale")
        op_custom = row_custom.operator(f"view3d.{PREFIX}_set_grid_scale", text="指定値にする")
        op_custom.use_custom = True
        
        box_grid.separator()
        
        # テーマ設定を直接参照してGrid色・背景色をUIに表示
        prefs = context.preferences
        if len(prefs.themes) > 0:
            theme_view3d = prefs.themes[0].view_3d
            
            col_theme = box_grid.column(align=True)
            col_theme.prop(theme_view3d, "grid", text="Grid 色変更")
            col_theme.separator()
            col_theme.prop(theme_view3d.space.gradients, "high_gradient", text="背景色 (上/単色)")
            col_theme.prop(theme_view3d.space.gradients, "low_gradient", text="背景色 (下)")

        layout.separator(factor=1.5)

        # --- 4. システム ---
        box_sys = layout.box()
        box_sys.label(text="システム", icon='PREFERENCES')
        box_sys.operator(f"wm.{PREFIX}_open_url", text="リンクを開く", icon='URL')
        box_sys.operator(f"wm.{PREFIX}_remove_addon", text="アドオン削除", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    VGM_Properties,
    VGM_OT_reset_view, VGM_OT_update_info, VGM_OT_copy_info, 
    VGM_OT_set_grid_scale, VGM_OT_open_url, VGM_OT_remove_addon, 
    VGM_PT_main_panel
]

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=VGM_Properties))

def unregister():
    if hasattr(bpy.types.Scene, f"{PREFIX}_props"): delattr(bpy.types.Scene, f"{PREFIX}_props")
    for c in reversed(classes):
        try: bpy.utils.unregister_class(c)
        except: pass

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()