Prefix 追加 20260227 Code copy

リンク パネル付き

リンクのトップに
進化版 画面中央 透視投影視座位置 20260319bb
<https://www.notion.so/20260319bb-327f5dacaf43801e8e37ce489dc1d593>

rapture_20260318154359.png

角度説明

# 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()