進化版 画面中央 透視投影視座位置 20260319bb
# Copied: 15:00:01
import bpy
import webbrowser
from bpy.props import FloatVectorProperty, PointerProperty, StringProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
# ★ PREFIXに大文字が含まれていても、内部で自動的に小文字に変換して処理します
PREFIX = "Sphere20260227"
TAB_NAME = "[ Sphere copy ] "
# --- 透視投影 視座関連の定数パラメーター ---
VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"
VIEW_POS_INIT = (0.0, -10.0, 10.0)
# ★ このスクリプト自身のID (コピー機能で使用)
# ### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###
bl_info = {
"name": f"zionad 520 [ Sphere Gen ] {PREFIX}",
"author": "zionadchat",
"version": (3, 2, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "View Control Addon (Sphere generator removed)",
"category": "3D View",
}
# 内部変数
# オペレーターID用には小文字化したPrefixを使用 (エラー回避のため)
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###"
ADDON_LINKS = (
{"label": "進化版 画面中央 20260319aa", "url": "<https://www.notion.so/20260319aa-327f5dacaf4380ef8a80c686f1e28ea1>"},
{"label": "Code Copy Template", "url": "<https://www.notion.so/Code-copy-20260221>"},
{"label": "Theory Background", "url": "<https://www.notion.so/Einstein-from-20260119>"},
)
# ==============================================================================
# デフォルト値設定 (コピー機能でここが書き換わります)
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"view_pos": (0.0000, -10.0000, 10.0000),
}
# <END_DICT>
# ==============================================================================
# 透視投影 視点更新ロジック
# ==============================================================================
def update_view_position(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
cam_pos = Vector(props.view_pos)
# ウィンドウ内の全ての3Dビューポートの視点を更新
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
r3d = space.region_3d
r3d.view_perspective = 'PERSP' # 透視投影にする
# 現在の注視点を基準にカメラ位置を計算
target_pos = Vector(r3d.view_location)
rel_pos = cam_pos - target_pos
dist = rel_pos.length
if dist > 0.001:
r3d.view_distance = dist
# 注視点へ向けるための回転行列を設定
r3d.view_rotation = rel_pos.to_track_quat('Z', 'Y')
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_SphereProps(PropertyGroup):
# 視座位置
view_pos: FloatVectorProperty(
name="View Position",
size=3,
soft_min=-100.0, soft_max=100.0,
default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT),
update=update_view_position
)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_ResetView(Operator):
bl_idname = f"{OP_PREFIX}.reset_view"
bl_label = VIEW_RESET_BTN_TEXT
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if props:
# リセット時は全3Dビューの注視点(フォーカス位置)も (0, 0, 0) に戻す
for window in context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.region_3d.view_location = (0.0, 0.0, 0.0)
props.view_pos = VIEW_POS_INIT # 初期値定数を適用
return {'FINISHED'}
class OT_CenterSelected(Operator):
bl_idname = f"{OP_PREFIX}.center_selected"
bl_label = "Center Selected Object"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if context.area.type != 'VIEW_3D':
self.report({'WARNING'}, "Please run in 3D Viewport")
return {'CANCELLED'}
# 1. 選択オブジェクトを注視点としてフォーカス
bpy.ops.view3d.view_selected()
# 2. フォーカス後のカメラのワールド座標を取得する
r3d = None
if hasattr(context, "region_data") and context.region_data:
r3d = context.region_data
else:
for space in context.area.spaces:
if space.type == 'VIEW_3D':
r3d = space.region_3d
break
if r3d:
cam_matrix = r3d.view_matrix.inverted()
cam_pos = cam_matrix.translation
# 3. 取得したカメラ座標を view_pos にセット (スライダーと同期させる)
props = getattr(context.scene, PROPS_NAME, None)
if props:
props.view_pos = cam_pos
return {'FINISHED'}
class OT_CopyFullScript(Operator):
bl_idname = f"{OP_PREFIX}.copy_script"
bl_label = "Copy Script"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
target_text = None
for t in bpy.data.texts:
if SOURCE_ID_TAG in t.as_string():
target_text = t; break
if not target_text:
self.report({'ERROR'}, "Script source not found.")
return {'CANCELLED'}
code = target_text.as_string()
v = props.view_pos
# 保存対象は view_pos のみに変更
new_dict = "CURRENT_DEFAULTS = {\n"
new_dict += f' "view_pos": ({v[0]:.4f}, {v[1]:.4f}, {v[2]:.4f}),\n'
new_dict += "}\n"
try:
start, end = "# <BEGIN" + "_DICT>", "# <END" + "_DICT>"
pre, post = code.split(start)[0], code.split(end)[1]
final = f"# Copied: {datetime.now().strftime('%H:%M:%S')}\n" + pre + start + "\n" + new_dict + end + post
context.window_manager.clipboard = final
self.report({'INFO'}, "Code copied!")
except: return {'CANCELLED'}
return {'FINISHED'}
class OT_OpenUrl(Operator):
bl_idname = f"{OP_PREFIX}.open_url"; bl_label = "Open URL"; url: StringProperty()
def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}
class OT_RemoveAddon(Operator):
bl_idname = f"{OP_PREFIX}.remove_addon"; bl_label = "Remove Addon"
def execute(self, context):
bpy.app.timers.register(lambda: unregister(), first_interval=0.1)
return {'FINISHED'}
# ==============================================================================
# PANELS
# ==============================================================================
class PT_ViewControlPanel(Panel):
bl_label = "View Control"
bl_idname = f"{PREFIX}_PT_view_control"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_order = 0
def draw(self, context):
layout = self.layout
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
# コピー機能はここに追加
row = layout.row()
row.scale_y = 1.2
row.operator(OT_CopyFullScript.bl_idname, icon='COPY_ID', text="Copy Code with Values")
layout.separator()
box = layout.box()
box.label(text="Perspective Viewpoint", icon='VIEW_CAMERA')
col = box.column(align=True)
col.prop(props, "view_pos", text="X", index=0)
col.prop(props, "view_pos", text="Y", index=1)
col.prop(props, "view_pos", text="Z", index=2)
box.separator()
box.operator(OT_ResetView.bl_idname, icon='LOOP_BACK', text=VIEW_RESET_BTN_TEXT)
layout.separator()
layout.operator(OT_CenterSelected.bl_idname, icon='VIEWZOOM', text="Center Selected Object")
class PT_LinksPanel(Panel):
bl_label = "Links"
bl_idname = f"{PREFIX}_PT_links"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
bl_order = 1
def draw(self, context):
for l in ADDON_LINKS:
self.layout.operator(OT_OpenUrl.bl_idname, text=l["label"]).url = l["url"]
class PT_RemovePanel(Panel):
bl_label = "System"
bl_idname = f"{PREFIX}_PT_remove"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = TAB_NAME
bl_options = {'DEFAULT_CLOSED'}
bl_order = 2
def draw(self, context):
self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (
PG_SphereProps,
OT_CopyFullScript,
OT_ResetView,
OT_CenterSelected,
OT_OpenUrl,
OT_RemoveAddon,
PT_ViewControlPanel,
PT_LinksPanel,
PT_RemovePanel
)
def open_sidebar():
if not bpy.context:
return None
for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for space in area.spaces:
if space.type == 'VIEW_3D':
space.show_region_ui = True # パネルを展開
return None
def register():
for c in classes: bpy.utils.register_class(c)
setattr(bpy.types.Scene, PROPS_NAME, PointerProperty(type=PG_SphereProps))
bpy.app.timers.register(open_sidebar, first_interval=0.1)
def unregister():
if hasattr(bpy.types.Scene, PROPS_NAME): delattr(bpy.types.Scene, PROPS_NAME)
for c in reversed(classes): bpy.utils.unregister_class(c)
if __name__ == "__main__": register()