Prefix 追加 20260227 Code copy
リンク パネル付き
リンクのトップに
進化版 画面中央 透視投影視座位置 20260319bb
<https://www.notion.so/20260319bb-327f5dacaf43801e8e37ce489dc1d593>

角度説明
# Copied: 20260319 15:00:01
# Perspective projection
# viewpoint position
import bpy
import math
from bpy.props import FloatVectorProperty, FloatProperty, PointerProperty
from bpy.types import Operator, Panel, PropertyGroup
from mathutils import Vector
from datetime import datetime
# ==============================================================================
# 設定エリア & ID管理
# ==============================================================================
PREFIX = "Perspective20260319"
TAB_NAME = "[ 透視投影 視座位置 ] "
# --- 透視投影 視座関連の定数パラメーター ---
VIEW_RESET_BTN_TEXT = "Reset View (0, -10, 10)"
VIEW_POS_INIT = (0.0, -10.0, 10.0)
# ### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###
bl_info = {
"name": f"zionad 520 [ Sphere Gen ] {PREFIX}",
"author": "zionadchat",
"version": (3, 7, 0),
"blender": (3, 0, 0),
"location": "3D View > Sidebar",
"description": "View Control Addon with Multi-Angle Calculation",
"category": "3D View",
}
OP_PREFIX = PREFIX.lower()
PROPS_NAME = f"{PREFIX}_props"
SOURCE_ID_TAG = "### ZIONAD_SOURCE_ID: SPHERE_2026_02_27_FIXED ###"
# ==============================================================================
# デフォルト値設定
# ==============================================================================
# <BEGIN_DICT>
CURRENT_DEFAULTS = {
"view_pos": (0.0000, -10.0000, 10.0000),
}
# <END_DICT>
# ==============================================================================
# 透視投影 視点更新ロジック
# ==============================================================================
_is_updating_view = False
def update_view_position(self, context):
global _is_updating_view
if _is_updating_view: return
props = getattr(context.scene, PROPS_NAME, None)
if not props: return
limit = props.slider_limit
v = list(props.view_pos)
clamped = False
for i in range(3):
if v[i] > limit: v[i] = limit; clamped = True
elif v[i] < -limit: v[i] = -limit; clamped = True
if clamped:
_is_updating_view = True
props.view_pos = v
_is_updating_view = False
_is_updating_view = True
try:
cam_pos = Vector(props.view_pos)
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')
finally:
_is_updating_view = False
# ==============================================================================
# PROPERTIES
# ==============================================================================
class PG_SphereProps(PropertyGroup):
slider_limit: FloatProperty(name="Range Limit", default=300.0, min=10.0, max=10000.0)
view_pos: FloatVectorProperty(name="View Position", size=3, soft_min=-10000.0, soft_max=10000.0,
default=CURRENT_DEFAULTS.get('view_pos', VIEW_POS_INIT),
update=update_view_position)
# ==============================================================================
# OPERATORS
# ==============================================================================
class OT_GetCurrentView(Operator):
bl_idname = f"{OP_PREFIX}.get_current_view"
bl_label = "Get Current View & Update"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
if not props: return {'CANCELLED'}
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
actual_cam_pos = r3d.view_matrix.inverted().translation
max_val = max(abs(actual_cam_pos.x), abs(actual_cam_pos.y), abs(actual_cam_pos.z))
if max_val > props.slider_limit: props.slider_limit = max_val + 50.0
props.view_pos = actual_cam_pos
return {'FINISHED'}
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:
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):
bpy.ops.view3d.view_selected()
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
props = getattr(context.scene, PROPS_NAME, None)
if props: props.view_pos = r3d.view_matrix.inverted().translation
return {'FINISHED'}
class OT_CopyActualViewPos(Operator):
bl_idname = f"{OP_PREFIX}.copy_actual_pos"
bl_label = "Copy Position Only"
def execute(self, context):
r3d = context.space_data.region_3d if context.space_data else None
if not r3d: return {'CANCELLED'}
p = r3d.view_matrix.inverted().translation
context.window_manager.clipboard = f"Actual View Pos: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})"
self.report({'INFO'}, "視座位置をコピーしました")
return {'FINISHED'}
class OT_CopyAngles(Operator):
bl_idname = f"{OP_PREFIX}.copy_angles"
bl_label = "Copy Full Info (Pos & Angles)"
def execute(self, context):
props = getattr(context.scene, PROPS_NAME, None)
r3d = context.space_data.region_3d if context.space_data else None
if not r3d or not props: return {'CANCELLED'}
slider_cam_pos = Vector(props.view_pos)
actual_cam_pos = r3d.view_matrix.inverted().translation
target_pos = Vector(r3d.view_location)
vec = target_pos - actual_cam_pos
length = vec.length
if length < 0.0001: return {'CANCELLED'}
# 軸とのなす角 (Direction Cosine)
ang_x = math.degrees(math.acos(vec.x / length))
ang_y = math.degrees(math.acos(vec.y / length))
ang_z = math.degrees(math.acos(vec.z / length))
# 直感的な傾き (Planar Angles)
pl_x = math.degrees(math.asin(vec.x / length))
pl_y = math.degrees(math.asin(vec.y / length))
pl_z = math.degrees(math.asin(vec.z / length))
info_text = (
f"--- View Direction Info ---\n"
f"[ Actual 3D View Status ]\n"
f"Actual View Pos : ({actual_cam_pos.x:.4f}, {actual_cam_pos.y:.4f}, {actual_cam_pos.z:.4f})\n"
f"Target Pos : ({target_pos.x:.4f}, {target_pos.y:.4f}, {target_pos.z:.4f})\n"
f"Distance : {length:.4f}\n\n"
f"[ Direction Angles (軸そのものとの角度 0〜180°) ]\n"
f"Angle from X Axis : {ang_x:.2f} deg\n"
f"Angle from Y Axis : {ang_y:.2f} deg\n"
f"Angle from Z Axis : {ang_z:.2f} deg\n\n"
f"[ Planar Angles (直感的な傾き・ズレ角 -90〜90°) ]\n"
f"X (横のズレ角) : {pl_x:.2f} deg\n"
f"Y (前後の傾き) : {pl_y:.2f} deg\n"
f"Z (仰角・俯角) : {pl_z:.2f} deg\n"
)
context.window_manager.clipboard = info_text
self.report({'INFO'}, "情報全体をクリップボードにコピーしました")
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 = next((t for t in bpy.data.texts if SOURCE_ID_TAG in t.as_string()), None)
if not target_text: return {'CANCELLED'}
code = target_text.as_string()
v = props.view_pos
new_dict = f'CURRENT_DEFAULTS = {{\n "view_pos": ({v[0]:.4f}, {v[1]:.4f}, {v[2]:.4f}),\n}}\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_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
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')
box.prop(props, "slider_limit", text="Range Limit (+/-)")
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_GetCurrentView.bl_idname, icon='RESTRICT_VIEW_OFF')
box.operator(OT_ResetView.bl_idname, icon='LOOP_BACK')
layout.operator(OT_CenterSelected.bl_idname, icon='VIEWZOOM')
layout.separator()
box_info = layout.box()
box_info.label(text="Actual View Status", icon='INFO')
r3d = context.space_data.region_3d if context.space_data else None
if r3d:
target_pos = Vector(r3d.view_location)
actual_cam_pos = r3d.view_matrix.inverted().translation
vec = target_pos - actual_cam_pos
length = vec.length
col_pos = box_info.column(align=True)
col_pos.label(text="[ Actual Position ]", icon='VIEW_CAMERA')
col_pos.label(text=f" X: {actual_cam_pos.x:.4f}")
col_pos.label(text=f" Y: {actual_cam_pos.y:.4f}")
col_pos.label(text=f" Z: {actual_cam_pos.z:.4f}")
box_info.operator(OT_CopyActualViewPos.bl_idname, icon='COPYDOWN')
box_info.separator()
col_ang = box_info.column(align=True)
if length > 0.0001:
# 数学的な方向角
a_x = math.degrees(math.acos(vec.x / length))
a_y = math.degrees(math.acos(vec.y / length))
a_z = math.degrees(math.acos(vec.z / length))
# 直感的な角度(アークサイン)
p_x = math.degrees(math.asin(vec.x / length))
p_y = math.degrees(math.asin(vec.y / length))
p_z = math.degrees(math.asin(vec.z / length))
col_ang.label(text="[ Direction Angles (軸との角度) ]", icon='ORIENTATION_GLOBAL')
col_ang.label(text=f" X: {a_x:.2f}°")
col_ang.label(text=f" Y: {a_y:.2f}°")
col_ang.label(text=f" Z: {a_z:.2f}°")
col_ang.separator()
col_ang.label(text="[ Planar Angles (直感的な傾き) ]", icon='DRIVER_ROTATIONAL_DIFFERENCE')
col_ang.label(text=f" X (ズレ角): {p_x:.2f}°")
col_ang.label(text=f" Y (ズレ角): {p_y:.2f}°")
col_ang.label(text=f" Z (仰俯角): {p_z:.2f}°")
else:
col_ang.label(text=" Target is too close")
box_info.separator()
box_info.operator(OT_CopyAngles.bl_idname, icon='COPYDOWN')
else:
box_info.label(text="Please use in 3D View")
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'}
def draw(self, context):
self.layout.operator(OT_RemoveAddon.bl_idname, icon='CANCEL', text="Remove Addon")
# ==============================================================================
# REGISTER
# ==============================================================================
classes = (PG_SphereProps, OT_GetCurrentView, OT_CopyActualViewPos, OT_CopyAngles,
OT_CopyFullScript, OT_ResetView, OT_CenterSelected, OT_RemoveAddon,
PT_ViewControlPanel, 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()