
プロンプト 要件定義
<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()