カメラ機能の追加 改変型

<aside> 💡

ご提示いただいたコードと要望を元に、まずは要件定義を作成し、その後ご要望に合わせたBlender 5以降専用のPythonスクリプトを作成しました。

要件定義

  1. 基本要件
  2. コード構成・初期値定義
  3. UI・画面操作 (パネル上部)
  4. 線分軌道・注視点アニメーション (パネル下部)

Blender アドオン スクリプト

以下のコードをテキストエディタにコピーし、.py ファイルとして保存、またはBlenderのテキストエディタで直接実行・インストールしてください。

</aside>






bl_info = {
    "name": "Sync Line Camera & View Tools",
    "author": "Your Name",
    "version": (3, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "線分軌道カメラの同期移動、アニメ自動設定、包み、確実な背景/Grid設定",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME = "Line Cam 20260710"
PREFIX_NAME = "SyncLineCamRig"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

COLLECTION_NAME = "CamRig_Line_Collection"

OBJ_NAME_CAM_TRACK = "CamTrack_Line"
OBJ_NAME_TGT_TRACK = "TgtTrack_Line"
OBJ_NAME_CAMERA = "TrackingCamera_Line"
OBJ_NAME_TARGET = "CameraTarget_Line"

SHIELD_NAME = "CamShield_Sphere_Line"
CUTTER_NAME = "CamShield_Cutter_Line"
CUTTER_BACK_NAME = "CamShield_CutterBack_Line"

INIT_CAM_START = (-15.0, -15.0, 5.0)
INIT_CAM_END = (15.0, -15.0, 5.0)
INIT_TGT_START = (0.0, 0.0, 0.0)
INIT_TGT_END = (0.0, 0.0, 0.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 view_rotation_euler_get(self):
    rv3d = get_rv3d(bpy.context)
    return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)

def view_rotation_euler_set(self, value):
    rv3d = get_rv3d(bpy.context)
    if rv3d:
        if bpy.context.active_object:
            rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
        rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()

# =========================================================================
# 【カメラ・注視点 制御 関数】
# =========================================================================
def update_line_positions(self, context):
    cam_curve = bpy.data.objects.get(OBJ_NAME_CAM_TRACK)
    if cam_curve and cam_curve.data.splines:
        pts = cam_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.cam_line_start, 1.0)
            pts[1].co = (*self.cam_line_end, 1.0)
            
    tgt_curve = bpy.data.objects.get(OBJ_NAME_TGT_TRACK)
    if tgt_curve and tgt_curve.data.splines:
        pts = tgt_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.tgt_line_start, 1.0)
            pts[1].co = (*self.tgt_line_end, 1.0)

def update_cam_target(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not tgt: return
    
    for c in tgt.constraints:
        if c.type == 'COPY_LOCATION':
            tgt.constraints.remove(c)
            
    fp = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None)
    mode = self.cam_target_mode
    
    if mode == 'LINE_TRACK':
        if fp: fp.mute = False
    else:
        if fp: fp.mute = True
        
        if mode == 'OBJECT' and self.cam_target_obj1:
            c1 = tgt.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
        elif mode == 'POINT':
            tgt.location = self.cam_target_loc
        elif mode == 'MIDPOINT':
            if self.cam_target_obj1:
                c1 = tgt.constraints.new(type='COPY_LOCATION')
                c1.target = self.cam_target_obj1
                c1.influence = 1.0
            if self.cam_target_obj2:
                c2 = tgt.constraints.new(type='COPY_LOCATION')
                c2.target = self.cam_target_obj2
                c2.influence = 0.5

def update_cam_mute(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not cam: return
    
    cam_track = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    cam_path = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    tgt_path = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None) if tgt else None
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        cam.location = eval_cam.matrix_world.to_translation()
        cam.rotation_euler = eval_cam.matrix_world.to_euler(cam.rotation_mode)
        if tgt:
            eval_tgt = tgt.evaluated_get(depsgraph)
            tgt.location = eval_tgt.matrix_world.to_translation()
            
        if cam_track: cam_track.mute = True
        if cam_path: cam_path.mute = True
        if tgt_path: tgt_path.mute = True
    else:
        if cam_track: cam_track.mute = False
        if cam_path: cam_path.mute = False
        if tgt_path and self.cam_target_mode == 'LINE_TRACK': 
            tgt_path.mute = False

def cam_fov_get(self):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        cam.data.angle = math.radians(value)

# =========================================================================
# 【包み & その他環境制御】
# =========================================================================
def update_cam_obj_visibility(self, context):
    cam = self.id_data 
    if cam and cam.type == 'CAMERA': cam.hide_viewport = not self.show_cam_obj

def update_cam_shield_visibility(self, context):
    cam = self.id_data
    if not cam: return
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"):
            child.hide_viewport = not self.show_cam_shield

def update_cam_shield(self, context):
    cam = self.id_data
    if not cam: return
    shield, cutter, cutter_back = None, None, None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"): shield = child
        elif child.name.startswith("CamShield_Cutter_"): cutter = child
        elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
        
    if shield:
        shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
        
    z_scale = self.cam_shield_radius * 1.5
    
    if cutter:
        front_deg = max(0.1, min(179.9, self.cam_shield_hole_angle))
        angle = math.radians(front_deg)
        r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
        cutter.scale = (r_scale, r_scale, z_scale)
        
    if cutter_back:
        r_scale_back = max(0.001, self.cam_shield_hole_radius_back)
        cutter_back.scale = (r_scale_back, r_scale_back, z_scale)

def update_cam_shield_material(self, context):
    cam = self.id_data
    if not cam: return
    shield = next((c for c in cam.children if c.name.startswith("CamShield_Sphere_")), None)
    if not shield: return
    
    if shield.data.materials:
        mat = shield.data.materials[0]
        if mat:
            mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
            if mat.use_nodes:
                bsdf = mat.node_tree.nodes.get("Principled BSDF")
                if bsdf:
                    if 'Base Color' in bsdf.inputs: bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
                    if 'Alpha' in bsdf.inputs: bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
    shield.color = (*self.cam_shield_color, self.cam_shield_alpha)

def update_target_empty_visibility(self, context):
    for obj in bpy.data.objects:
        if obj.name in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK]:
            obj.hide_viewport = not self.show_target_empty

def setup_world_nodes(context):
    world = context.scene.world
    if not world:
        world = bpy.data.worlds.new("World")
        context.scene.world = world
    world.use_nodes = True
    tree = world.node_tree
    
    out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
    if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
    bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
    if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
    sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
    if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
    return tree, out_node, bg_node, sky_node

def update_world_mode(self, context):
    tree, out_node, bg_node, sky_node = setup_world_nodes(context)
    for link in bg_node.inputs['Color'].links: tree.links.remove(link)
    if not bg_node.outputs['Background'].links:
        tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
        
    if self.world_mode == 'SKY':
        context.scene.render.film_transparent = False
        sky_node.sky_type = 'NISHITA'
        tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
        update_sky_texture(self, context)
        update_world_settings(self, context)
    elif self.world_mode == 'COLOR':
        context.scene.render.film_transparent = False
        update_world_settings(self, context)
    elif self.world_mode == 'TRANSPARENT':
        context.scene.render.film_transparent = True
        update_world_settings(self, context)

def update_sky_texture(self, context):
    if self.world_mode != 'SKY': return
    _, _, _, sky_node = setup_world_nodes(context)
    sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
    sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
    sky_node.sun_intensity = self.sky_sun_intensity

def update_world_settings(self, context):
    tree, _, bg_node, _ = setup_world_nodes(context)
    if self.world_mode != 'SKY':
        bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
    bg_node.inputs[1].default_value = self.world_bg_strength

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
    show_cam_obj: bpy.props.BoolProperty(name="カメラ本体を表示", default=True, update=update_cam_obj_visibility)
    show_cam_shield: bpy.props.BoolProperty(name="包みを表示", default=True, update=update_cam_shield_visibility)
    cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=1.0, min=0.1, update=update_cam_shield)
    cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
    cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (円筒の半径)", default=1.0, min=0.0, update=update_cam_shield)
    cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
    cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)

class LineCamRigProperties(bpy.types.PropertyGroup):
    anim_factor: bpy.props.FloatProperty(name="進行度 (0.0 〜 1.0)", default=0.0, min=0.0, max=1.0)
    anim_start_frame: bpy.props.IntProperty(name="開始フレーム", default=1, min=0)
    anim_end_frame: bpy.props.IntProperty(name="終了フレーム", default=250, min=1)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="カメラ始点", default=INIT_CAM_START, update=update_line_positions)
    cam_line_end: bpy.props.FloatVectorProperty(name="カメラ終点", default=INIT_CAM_END, update=update_line_positions)
    tgt_line_start: bpy.props.FloatVectorProperty(name="注視点始点", default=INIT_TGT_START, update=update_line_positions)
    tgt_line_end: bpy.props.FloatVectorProperty(name="注視点終点", default=INIT_TGT_END, update=update_line_positions)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", 
        items=[
            ('LINE_TRACK', "同期線分軌道", ""),
            ('OBJECT', "単一オブジェクト", ""), 
            ('POINT', "指定座標 (手動)", ""), 
            ('MIDPOINT', "2オブジェクトの中間", "")
        ],
        default='LINE_TRACK', update=update_cam_target
    )
    cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
    cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
    cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)

    mute_target_tracking: bpy.props.BoolProperty(name="アドオンから切り離し", default=False, update=update_cam_mute)
    show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)

    obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
    
    custom_grid_scale: bpy.props.FloatProperty(name="指定値", default=10.0, min=0.001)

    world_mode: bpy.props.EnumProperty(
        name="ワールド背景モード",
        items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
        default='COLOR', update=update_world_mode
    )
    sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
    sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
    sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
    world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
    world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)

# =========================================================================
# 【オペレーター群】
# =========================================================================
class LINECAM_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
        rv3d.view_perspective = 'PERSP'
        context.view_layer.update()
        return {'FINISHED'}

class LINECAM_OT_rotate_selected(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
    bl_label = "選択オブジェクトを指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=90.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
        rad = math.radians(self.angle)
        for obj in context.selected_objects:
            if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
            if axis == 'X': obj.rotation_euler.x += rad
            elif axis == 'Y': obj.rotation_euler.y += rad
            elif axis == 'Z': obj.rotation_euler.z += rad
        return {'FINISHED'}

class LINECAM_OT_rotate_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
    bl_label = "画面を指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=15.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
        rv3d = get_rv3d(context)
        if rv3d:
            vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
            rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
        return {'FINISHED'}

class LINECAM_OT_set_grid_scale(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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_SAFE}_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 LINECAM_OT_update_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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 LINECAM_OT_copy_view_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_copy_view_info"
    bl_label = "情報をコピー"
    def execute(self, context):
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        if not rv3d or not space: return {'CANCELLED'}
        
        view_loc = rv3d.view_location
        view_dist = rv3d.view_distance
        view_rot = rv3d.view_rotation
        cam_pos = view_loc + view_rot @ mathutils.Vector((0.0, 0.0, view_dist))
        view_dir = view_rot @ mathutils.Vector((0.0, 0.0, -1.0))
        
        area = context.area
        aspect = area.height / area.width if area.width > 0 else 1.0
        sensor = 32.0 
        
        lines = []
        if rv3d.view_perspective == '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
            lines.append("投影モード: 透視投影 (Perspective)")
            lines.append(f"架空の視座位置 X: {cam_pos.x:.1f}, Y: {cam_pos.y:.1f}, Z: {cam_pos.z:.1f}")
            lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
            lines.append(f"画面中央位置 X: {view_loc.x:.1f}, Y: {view_loc.y:.1f}, Z: {view_loc.z:.1f}")
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")
            lines.append(f"視座からの距離: {view_dist:.1f}")
        else:
            w = view_dist * (sensor / space.lens)
            h = w * aspect
            lines.append("投影モード: 平行投影 (Orthographic)")
            lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
            lines.append(f"画面中央位置 X: {view_loc.x:.1f}, Y: {view_loc.y:.1f}, Z: {view_loc.z:.1f}")
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")

        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
        return {'FINISHED'}

class LINECAM_OT_copy_cam_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_copy_cam_info"
    bl_label = "カメラ情報をコピー"
    def execute(self, context):
        cam = context.scene.camera
        if not cam or OBJ_NAME_CAMERA not in cam.name: return {'CANCELLED'}
        
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        cam_loc = eval_cam.matrix_world.to_translation()
        cam_rot = eval_cam.matrix_world.to_quaternion()
        view_dir = cam_rot @ mathutils.Vector((0.0, 0.0, -1.0))
        
        tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
        tgt_loc = tgt.matrix_world.to_translation() if tgt else mathutils.Vector((0,0,0))
        dist = (cam_loc - tgt_loc).length if tgt else cam.data.dof.focus_distance
        
        res_x = context.scene.render.resolution_x
        res_y = context.scene.render.resolution_y
        aspect = res_y / res_x if res_x > 0 else 1.0
        
        fov_h = cam.data.angle
        w = 2.0 * dist * math.tan(fov_h / 2.0)
        h = w * aspect
        
        lines = []
        lines.append(f"投影モード: カメラ ({cam.name})")
        lines.append(f"架空の視座位置 X: {cam_loc.x:.1f}, Y: {cam_loc.y:.1f}, Z: {cam_loc.z:.1f}")
        lines.append(f"画面中央への視線方向 X: {view_dir.x:.1f}, Y: {view_dir.y:.1f}, Z: {view_dir.z:.1f}")
        
        if tgt:
            lines.append(f"画面中央位置 X: {tgt_loc.x:.1f}, Y: {tgt_loc.y:.1f}, Z: {tgt_loc.z:.1f}")
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")
            lines.append(f"視座からの距離: {dist:.1f}")
        else:
            lines.append(f"画面中央位置 X: なし, Y: なし, Z: なし")
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")
            lines.append(f"視座からの距離 (Focus): {dist:.1f}")

        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "カメラ情報をクリップボードにコピーしました")
        return {'FINISHED'}

class LINECAM_OT_auto_animate(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_auto_animate"
    bl_label = "進行度アニメーションを自動設定"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        prefs = context.preferences
        prev_interp = prefs.edit.keyframe_new_interpolation_type
        try:
            prefs.edit.keyframe_new_interpolation_type = 'LINEAR'
            props.anim_factor = 0.0
            props.keyframe_insert(data_path="anim_factor", frame=props.anim_start_frame)
            props.anim_factor = 1.0
            props.keyframe_insert(data_path="anim_factor", frame=props.anim_end_frame)
        finally:
            prefs.edit.keyframe_new_interpolation_type = prev_interp
        self.report({'INFO'}, f"アニメ設定完了: {props.anim_start_frame}F 〜 {props.anim_end_frame}F")
        return {'FINISHED'}

class LINECAM_OT_create_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_rig"
    bl_label = "リグ作成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        names = [OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME]
        for name in names:
            obj = bpy.data.objects.get(name)
            if obj: bpy.data.objects.remove(obj, do_unlink=True)
                
        main_col = bpy.data.collections.get(COLLECTION_NAME)
        if not main_col:
            main_col = bpy.data.collections.new(COLLECTION_NAME)
            context.scene.collection.children.link(main_col)

        def create_line_curve(name, p1, p2):
            crv_data = bpy.data.curves.new(name, type='CURVE')
            crv_data.dimensions = '3D'
            spline = crv_data.splines.new('POLY')
            spline.points.add(1)
            spline.points[0].co = (*p1, 1.0)
            spline.points[1].co = (*p2, 1.0)
            crv_obj = bpy.data.objects.new(name, crv_data)
            main_col.objects.link(crv_obj)
            return crv_obj

        tgt_curve = create_line_curve(OBJ_NAME_TGT_TRACK, props.tgt_line_start, props.tgt_line_end)
        cam_curve = create_line_curve(OBJ_NAME_CAM_TRACK, props.cam_line_start, props.cam_line_end)
        
        tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
        tgt_empty.empty_display_type = 'PLAIN_AXES'
        main_col.objects.link(tgt_empty)
        
        tgt_path = tgt_empty.constraints.new(type='FOLLOW_PATH')
        tgt_path.target = tgt_curve
        tgt_path.use_curve_follow = False
        tgt_path.use_fixed_location = True
        
        cam_data = bpy.data.cameras.new(name=f"{OBJ_NAME_CAMERA}_Data")
        cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        main_col.objects.link(cam_obj)
        
        cam_path = cam_obj.constraints.new(type='FOLLOW_PATH')
        cam_path.target = cam_curve
        cam_path.use_curve_follow = False
        cam_path.use_fixed_location = True
        
        cam_track = cam_obj.constraints.new(type='TRACK_TO')
        cam_track.target = tgt_empty
        cam_track.track_axis = 'TRACK_NEGATIVE_Z'
        cam_track.up_axis = 'UP_Y'
        
        dp = f"{PREFIX_SAFE}_props.anim_factor"
        def add_offset_driver(constraint):
            d = constraint.driver_add("offset_factor").driver
            d.type = 'AVERAGE'
            v = d.variables.new()
            v.name = "var"
            v.type = 'SINGLE_PROP'
            v.targets[0].id_type = 'SCENE'
            v.targets[0].id = context.scene
            v.targets[0].data_path = dp
            
        add_offset_driver(tgt_path)
        add_offset_driver(cam_path)
        
        mesh_sphere = bpy.data.meshes.new(SHIELD_NAME)
        shield_obj = bpy.data.objects.new(SHIELD_NAME, mesh_sphere)
        main_col.objects.link(shield_obj)
        shield_obj.parent = cam_obj
        shield_obj.hide_render = True
        shield_obj.hide_select = True
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
        bm.to_mesh(mesh_sphere)
        bm.free()
        
        mesh_cone = bpy.data.meshes.new(CUTTER_NAME)
        cutter_obj = bpy.data.objects.new(CUTTER_NAME, mesh_cone)
        main_col.objects.link(cutter_obj)
        cutter_obj.parent = cam_obj
        cutter_obj.hide_viewport = True
        cutter_obj.hide_render = True
        cutter_obj.hide_select = True
        cutter_obj.display_type = 'BOUNDS'
        bm_cone = bmesh.new()
        bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
        for v in bm_cone.verts: v.co.z -= 0.95
        bm_cone.to_mesh(mesh_cone)
        bm_cone.free()
        
        mesh_cone_back = bpy.data.meshes.new(CUTTER_BACK_NAME)
        cutter_back_obj = bpy.data.objects.new(CUTTER_BACK_NAME, mesh_cone_back)
        main_col.objects.link(cutter_back_obj)
        cutter_back_obj.parent = cam_obj
        cutter_back_obj.hide_viewport = True
        cutter_back_obj.hide_render = True
        cutter_back_obj.hide_select = True
        cutter_back_obj.display_type = 'BOUNDS'
        bm_cone_back = bmesh.new()
        bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
        for v in bm_cone_back.verts: v.co.z += 0.95
        bm_cone_back.to_mesh(mesh_cone_back)
        bm_cone_back.free()
        
        mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
        mod_front.operation = 'DIFFERENCE'
        mod_front.object = cutter_obj
        mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
        mod_back.operation = 'DIFFERENCE'
        mod_back.object = cutter_back_obj
        
        mat = bpy.data.materials.new("CamShield_Mat_Line")
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        
        cprops = cam_obj.cam_rig_props
        cprops.cam_shield_radius = 1.0
        cprops.cam_shield_hole_angle = 179.0
        cprops.cam_shield_hole_radius_back = 1.0
        cprops.cam_shield_color = (0.0, 1.0, 1.0)
        cprops.cam_shield_alpha = 0.3
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = tgt_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        
        props.mute_target_tracking = False
        props.cam_target_mode = 'LINE_TRACK'
        
        update_line_positions(props, context)
        update_cam_target(props, context)
        update_cam_shield(cprops, context)
        update_cam_shield_material(cprops, context)
        
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
        
        self.report({'INFO'}, "線分軌道カメラをセットアップしました")
        return {'FINISHED'}

class LINECAM_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "リンク"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル1】 ビュー & 環境操作ツール
# =========================================================================
class LINECAM_PT_view_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_SAFE.upper()}_PT_view_panel"
    bl_label = "ビュー & 環境操作ツール"
    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_SAFE}_props")
        
        box_action = layout.box()
        box_action.label(text="ビューとオブジェクト操作:", icon='VIEW_CAMERA')
        col_action = box_action.column()
        col_action.scale_y = 1.3
        col_action.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択中心にビューを初期化", icon='ZOOM_ALL')
        layout.separator(factor=1.5)

        box_rot = layout.box()
        box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
        box_rot.prop(props, "obj_rot_axis", expand=True)
        if context.active_object:
            axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
            box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
            row = box_rot.row(align=True)
            for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
        else:
            box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
        layout.separator(factor=1.5)

        box_vrot = layout.box()
        box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
        box_vrot.prop(props, "view_rot_axis", expand=True)
        v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
        box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
        row_v = box_vrot.row(align=True)
        for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
        layout.separator(factor=1.5)
        
        # ビュー情報表示
        box_info = layout.box()
        row_info_head = box_info.row()
        row_info_head.label(text="ビュー位置・座標・範囲情報:", icon='INFO')
        row_info_head.operator(f"view3d.{PREFIX_SAFE}_update_info", text="", icon='FILE_REFRESH')
        
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        
        if rv3d and space:
            view_dist = rv3d.view_distance
            view_loc = rv3d.view_location
            view_rot = rv3d.view_rotation
            cam_pos = view_loc + view_rot @ mathutils.Vector((0.0, 0.0, view_dist))
            view_dir = view_rot @ mathutils.Vector((0.0, 0.0, -1.0))
            
            area = context.area
            aspect = area.height / area.width if area.width > 0 else 1.0
            sensor = 32.0 
            
            col_v = box_info.column(align=True)
            if rv3d.view_perspective == '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
                col_v.label(text="[ 透視投影 (Perspective) ]", icon='VIEW_PERSPECTIVE')
                col_v.label(text=f"架空視座: X:{cam_pos.x:.1f} Y:{cam_pos.y:.1f} Z:{cam_pos.z:.1f}")
                col_v.label(text=f"視線方向: X:{view_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
                col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
                col_v.label(text=f"視座からの距離: {view_dist:.1f}")
                col_v.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
            else:
                w = view_dist * (sensor / space.lens)
                h = w * aspect
                col_v.label(text="[ 平行投影 (Orthographic) ]", icon='VIEW_ORTHO')
                col_v.label(text=f"視線方向: X:{view_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
                col_v.label(text=f"画面中央: X:{view_loc.x:.1f} Y:{view_loc.y:.1f} Z:{view_loc.z:.1f}")
                col_v.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
                
            col_v.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="ビュー情報を一括コピー", icon='COPYDOWN')
        
        layout.separator(factor=1.5)
        
        # 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_SAFE}_set_grid_scale", text="間隔 10").scale_val = 10.0
        row_preset.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="間隔 100").scale_val = 100.0
        row_custom = box_grid.row(align=True)
        row_custom.prop(props, "custom_grid_scale")
        op_custom = row_custom.operator(f"view3d.{PREFIX_SAFE}_set_grid_scale", text="指定値にする")
        op_custom.use_custom = True
        
        box_grid.separator()
        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)
        
        box_render = layout.box()
        box_render.label(text="ワールド (環境光・空):", icon='WORLD')
        box_render.prop(props, "world_mode", expand=True)
        col_world = box_render.column(align=True)
        if props.world_mode == 'SKY':
            col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
            col_world.prop(props, "sky_sun_elevation", text="太陽の高さ (昼〜夕焼け)")
            col_world.prop(props, "sky_sun_rotation", text="太陽の向き (回転)")
            col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
            col_world.prop(props, "world_bg_strength", text="空全体の明るさ")
        elif props.world_mode == 'COLOR':
            col_world.label(text="※ Zキー → レンダービューで確認", icon='INFO')
            col_world.prop(props, "world_bg_color", text="背景の色 (スカイブルーなど)")
            col_world.prop(props, "world_bg_strength", text="明るさ")
        elif props.world_mode == 'TRANSPARENT':
            col_world.label(text="※ 背景を透明にして出力(合成用)", icon='INFO')
            col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング (Film -> Transparent)")
        
        layout.separator(factor=1.5)
        
        box_sys = layout.box()
        box_sys.label(text="システム / リンク:", icon='PREFERENCES')
        box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="アドオンのWebリンク", icon='URL').url = LINK_URL
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【UIパネル2】 線分カメラ軌道
# =========================================================================
class LINECAM_PT_camera_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_SAFE.upper()}_PT_camera_panel"
    bl_label = "カメラ軌道 (線分特化)"
    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_SAFE}_props")
        
        box_rig = layout.box()
        box_rig.label(text="専用カメラ (同期線分軌道):", icon='CAMERA_DATA')
        row_rig = box_rig.row(align=True)
        row_rig.operator(f"object.{PREFIX_SAFE}_create_rig", text="線分リグ作成", icon='ADD')
        row_rig.prop(props, "mute_target_tracking", text="アドオンから切り離し", toggle=True, icon='UNLINKED')
        
        cam = context.scene.camera
        if cam and OBJ_NAME_CAMERA in cam.name:
            box_cam = layout.box()
            box_cam.label(text="アニメーション & 軌道の操作:", icon='PLAY')
            
            col_anim = box_cam.column(align=True)
            col_anim.enabled = not props.mute_target_tracking
            col_anim.prop(props, "anim_factor", slider=True)
            
            row_auto = box_cam.row(align=True)
            row_auto.enabled = not props.mute_target_tracking
            row_auto.prop(props, "anim_start_frame", text="開始F")
            row_auto.prop(props, "anim_end_frame", text="終了F")
            row_auto.operator(f"object.{PREFIX_SAFE}_auto_animate", text="アニメ自動設定", icon='REC')
            
            box_cam.separator()
            col_pos = box_cam.column(align=True)
            col_pos.enabled = not props.mute_target_tracking
            col_pos.prop(props, "cam_line_start", text="カメラ 始点(XYZ)")
            col_pos.prop(props, "cam_line_end", text="カメラ 終点(XYZ)")
            
            box_cam.separator()
            box_cam.label(text="注視点の設定:", icon='EMPTY_DATA')
            col_tgt_main = box_cam.column(align=True)
            col_tgt_main.enabled = not props.mute_target_tracking
            col_tgt_main.prop(props, "cam_target_mode", expand=True)
            
            col_tgt = col_tgt_main.column(align=True)
            if props.cam_target_mode == 'LINE_TRACK':
                col_tgt.prop(props, "tgt_line_start", text="注視点 始点(XYZ)")
                col_tgt.prop(props, "tgt_line_end", text="注視点 終点(XYZ)")
            elif props.cam_target_mode == 'OBJECT':
                col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
            elif props.cam_target_mode == 'POINT':
                col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
            elif props.cam_target_mode == 'MIDPOINT':
                col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
                col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
            
            box_cam.separator()
            
            col_lens = box_cam.column(align=True)
            col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
            col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
            col_lens.separator()
            col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
            col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
            
            box_cam.separator()
            col_pp = box_cam.column(align=True)
            col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
                
            box_cam.separator()
            box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)", toggle=True, icon='STYLUS_PRESSURE')
            if cam.data.dof.use_dof:
                col_dof = box_cam.column(align=True)
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
            
            # 手動操作 (Location, Pitch, Roll, Yaw) と情報表示
            box_sight = box_cam.box()
            box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
            col_sight = box_sight.column(align=True)
            col_sight.enabled = props.mute_target_tracking
            col_sight.prop(cam, "location", text="位置 (XYZ)")
            col_sight.separator()
            col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
            col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
            col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
            
            if hasattr(cam, "cam_rig_props"):
                cprops = cam.cam_rig_props
                box_cam.separator()
                box_shield = box_cam.box()
                box_shield.label(text="包み (シールド) 設定:", icon='RESTRICT_VIEW_OFF')
                
                row_disp = box_shield.row()
                row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
                row_disp.prop(cprops, "show_cam_shield", text="包み")
                box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
                
                col_sh = box_shield.column(align=True)
                col_sh.enabled = cprops.show_cam_shield
                col_sh.prop(cprops, "cam_shield_radius", text="半径")
                col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
                col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴 (円筒の半径)")
                col_sh.separator()
                col_sh.prop(cprops, "cam_shield_color", text="色")
                col_sh.prop(cprops, "cam_shield_alpha", text="透明度", slider=True)
            
            layout.separator(factor=1.5)
            
            # 軌道カメラの個別情報
            box_info = layout.box()
            row_info_head = box_info.row()
            row_info_head.label(text="カメラ位置・座標・範囲情報:", icon='CAMERA_DATA')
            row_info_head.operator(f"view3d.{PREFIX_SAFE}_update_info", text="", icon='FILE_REFRESH')
            
            depsgraph = context.evaluated_depsgraph_get()
            eval_cam = cam.evaluated_get(depsgraph)
            cam_loc = eval_cam.matrix_world.to_translation()
            cam_rot = eval_cam.matrix_world.to_quaternion()
            view_dir = cam_rot @ mathutils.Vector((0.0, 0.0, -1.0))
            
            tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
            tgt_loc = tgt.matrix_world.to_translation() if tgt else mathutils.Vector((0,0,0))
            dist = (cam_loc - tgt_loc).length if tgt else cam.data.dof.focus_distance
            
            res_x = context.scene.render.resolution_x
            res_y = context.scene.render.resolution_y
            aspect = res_y / res_x if res_x > 0 else 1.0
            
            fov_h = cam.data.angle
            w = 2.0 * dist * math.tan(fov_h / 2.0)
            h = w * aspect
            
            col_c = box_info.column(align=True)
            col_c.label(text=f"[ 軌道カメラ ({cam.name}) ]", icon='CAMERA_DATA')
            col_c.label(text=f"視座(位置): X:{cam_loc.x:.1f} Y:{cam_loc.y:.1f} Z:{cam_loc.z:.1f}")
            col_c.label(text=f"視線方向: X:{view_dir.x:.1f} Y:{view_dir.y:.1f} Z:{view_dir.z:.1f}")
            
            if tgt:
                col_c.label(text=f"注視点(中央): X:{tgt_loc.x:.1f} Y:{tgt_loc.y:.1f} Z:{tgt_loc.z:.1f}")
                col_c.label(text=f"視座からの距離: {dist:.1f}")
                col_c.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
            else:
                col_c.label(text="注視点: なし")
                col_c.label(text=f"視座からの距離(Focus): {dist:.1f}")
                col_c.label(text=f"画面の広さ: 横 {w:.1f}m × 縦 {h:.1f}m")
                
            col_c.operator(f"view3d.{PREFIX_SAFE}_copy_cam_info", text="カメラ情報を一括コピー", icon='COPYDOWN')
        else:
            layout.label(text="※専用カメラがアクティブではありません", icon='INFO')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigObjectProperties, LineCamRigProperties,
    LINECAM_OT_reset_view, LINECAM_OT_rotate_selected, LINECAM_OT_rotate_view, LINECAM_OT_set_grid_scale,
    LINECAM_OT_update_info, LINECAM_OT_copy_view_info, LINECAM_OT_copy_cam_info,
    LINECAM_OT_auto_animate, LINECAM_OT_create_rig, 
    LINECAM_OT_open_url, LINECAM_OT_remove_addon, 
    LINECAM_PT_view_panel, LINECAM_PT_camera_panel
]

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
    setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=LineCamRigProperties))

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

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

bl_info = {
    "name": "Sync Line Camera & View Tools",
    "author": "Your Name",
    "version": (2, 5),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "線分軌道カメラの同期移動、アニメ自動設定、包み(背後円筒)、ビュー操作",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】 2026年7月11日 00:00
# =========================================================================
TAB_NAME = "Line Cam 20260710"
PREFIX_NAME = "SyncLineCamRig"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

COLLECTION_NAME = "CamRig_Line_Collection"

OBJ_NAME_CAM_TRACK = "CamTrack_Line"
OBJ_NAME_TGT_TRACK = "TgtTrack_Line"
OBJ_NAME_CAMERA = "TrackingCamera_Line"
OBJ_NAME_TARGET = "CameraTarget_Line"

SHIELD_NAME = "CamShield_Sphere_Line"
CUTTER_NAME = "CamShield_Cutter_Line"
CUTTER_BACK_NAME = "CamShield_CutterBack_Line"

INIT_CAM_START = (-15.0, -15.0, 5.0)
INIT_CAM_END = (15.0, -15.0, 5.0)
INIT_TGT_START = (0.0, 0.0, 0.0)
INIT_TGT_END = (0.0, 0.0, 0.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

class LINECAM_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
        rv3d.view_perspective = 'PERSP'
        context.view_layer.update()
        return {'FINISHED'}

class LINECAM_OT_update_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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 LINECAM_OT_copy_view_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_copy_view_info"
    bl_label = "情報をコピー"
    def execute(self, context):
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        if not rv3d or not space: return {'CANCELLED'}
        
        view_loc = rv3d.view_location
        view_dist = rv3d.view_distance
        area = context.area
        aspect = area.height / area.width if area.width > 0 else 1.0
        sensor = 32.0 
        
        lines = []
        lines.append(f"画面中央位置 X: {view_loc.x:.1f}")
        lines.append(f"画面中央位置 Y: {view_loc.y:.1f}")
        lines.append(f"画面中央位置 Z: {view_loc.z:.1f}")
        
        if rv3d.view_perspective == '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
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")
            lines.append(f"視座からの距離: {view_dist:.1f}")
        else:
            w = view_dist * (sensor / space.lens)
            h = w * aspect
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")

        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
        return {'FINISHED'}

class LINECAM_OT_copy_cam_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_copy_cam_info"
    bl_label = "カメラ情報をコピー"
    def execute(self, context):
        cam = context.scene.camera
        if not cam: return {'CANCELLED'}
        
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        loc = eval_cam.matrix_world.to_translation()
        rot = eval_cam.matrix_world.to_euler('XYZ')
        
        lines = []
        lines.append(f"カメラ位置 X: {loc.x:.1f}")
        lines.append(f"カメラ位置 Y: {loc.y:.1f}")
        lines.append(f"カメラ位置 Z: {loc.z:.1f}")
        lines.append(f"Pitch (X): {math.degrees(rot.x):.1f}")
        lines.append(f"Roll (Y): {math.degrees(rot.y):.1f}")
        lines.append(f"Yaw (Z): {math.degrees(rot.z):.1f}")
        
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "カメラ情報をクリップボードにコピーしました")
        return {'FINISHED'}

# =========================================================================
# 【カメラアニメーション設定】
# =========================================================================
class LINECAM_OT_auto_animate(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_auto_animate"
    bl_label = "進行度アニメーションを自動設定"
    bl_description = "指定したフレーム間に進行度0.0〜1.0のキーフレームを打ち、等速移動させます"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        prefs = context.preferences
        
        prev_interp = prefs.edit.keyframe_new_interpolation_type
        
        try:
            prefs.edit.keyframe_new_interpolation_type = 'LINEAR'
            
            props.anim_factor = 0.0
            props.keyframe_insert(data_path="anim_factor", frame=props.anim_start_frame)
            
            props.anim_factor = 1.0
            props.keyframe_insert(data_path="anim_factor", frame=props.anim_end_frame)
            
        finally:
            prefs.edit.keyframe_new_interpolation_type = prev_interp
                        
        self.report({'INFO'}, f"アニメーション設定完了: {props.anim_start_frame}F 〜 {props.anim_end_frame}F")
        return {'FINISHED'}

# =========================================================================
# 【カメラ・注視点 制御 & 更新関数】
# =========================================================================
def update_line_positions(self, context):
    cam_curve = bpy.data.objects.get(OBJ_NAME_CAM_TRACK)
    if cam_curve and cam_curve.data.splines:
        pts = cam_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.cam_line_start, 1.0)
            pts[1].co = (*self.cam_line_end, 1.0)
            
    tgt_curve = bpy.data.objects.get(OBJ_NAME_TGT_TRACK)
    if tgt_curve and tgt_curve.data.splines:
        pts = tgt_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.tgt_line_start, 1.0)
            pts[1].co = (*self.tgt_line_end, 1.0)

def update_cam_target(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not tgt: return
    
    for c in tgt.constraints:
        if c.type == 'COPY_LOCATION':
            tgt.constraints.remove(c)
            
    fp = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None)
    mode = self.cam_target_mode
    
    if mode == 'LINE_TRACK':
        if fp: fp.mute = False
    else:
        if fp: fp.mute = True
        
        if mode == 'OBJECT' and self.cam_target_obj1:
            c1 = tgt.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
        elif mode == 'POINT':
            tgt.location = self.cam_target_loc
        elif mode == 'MIDPOINT':
            if self.cam_target_obj1:
                c1 = tgt.constraints.new(type='COPY_LOCATION')
                c1.target = self.cam_target_obj1
                c1.influence = 1.0
            if self.cam_target_obj2:
                c2 = tgt.constraints.new(type='COPY_LOCATION')
                c2.target = self.cam_target_obj2
                c2.influence = 0.5

def update_cam_mute(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not cam: return
    
    cam_track = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    cam_path = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    tgt_path = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None) if tgt else None
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        cam.location = eval_cam.matrix_world.to_translation()
        cam.rotation_euler = eval_cam.matrix_world.to_euler(cam.rotation_mode)
        if tgt:
            eval_tgt = tgt.evaluated_get(depsgraph)
            tgt.location = eval_tgt.matrix_world.to_translation()
            
        if cam_track: cam_track.mute = True
        if cam_path: cam_path.mute = True
        if tgt_path: tgt_path.mute = True
    else:
        if cam_track: cam_track.mute = False
        if cam_path: cam_path.mute = False
        if tgt_path and self.cam_target_mode == 'LINE_TRACK': 
            tgt_path.mute = False

# =========================================================================
# 【包み & その他環境制御】
# =========================================================================
def update_cam_obj_visibility(self, context):
    cam = self.id_data 
    if cam and cam.type == 'CAMERA': cam.hide_viewport = not self.show_cam_obj

def update_cam_shield_visibility(self, context):
    cam = self.id_data
    if not cam: return
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"):
            child.hide_viewport = not self.show_cam_shield

def update_cam_shield(self, context):
    cam = self.id_data
    if not cam: return
    shield, cutter, cutter_back = None, None, None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"): shield = child
        elif child.name.startswith("CamShield_Cutter_"): cutter = child
        elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
        
    if shield:
        shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
        
    z_scale = self.cam_shield_radius * 1.5
    
    # 前方の穴 (円錐)
    if cutter:
        front_deg = max(0.1, min(179.9, self.cam_shield_hole_angle))
        angle = math.radians(front_deg)
        r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
        cutter.scale = (r_scale, r_scale, z_scale)
        
    # 後方の穴 (円筒)
    if cutter_back:
        r_scale_back = max(0.001, self.cam_shield_hole_radius_back)
        cutter_back.scale = (r_scale_back, r_scale_back, z_scale)

def update_cam_shield_material(self, context):
    cam = self.id_data
    if not cam: return
    shield = next((c for c in cam.children if c.name.startswith("CamShield_Sphere_")), None)
    if not shield: return
    
    if shield.data.materials:
        mat = shield.data.materials[0]
        if mat:
            mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
            if mat.use_nodes:
                bsdf = mat.node_tree.nodes.get("Principled BSDF")
                if bsdf:
                    if 'Base Color' in bsdf.inputs:
                        bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
                    if 'Alpha' in bsdf.inputs:
                        bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
    shield.color = (*self.cam_shield_color, self.cam_shield_alpha)

def update_target_empty_visibility(self, context):
    for obj in bpy.data.objects:
        if obj.name in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK]:
            obj.hide_viewport = not self.show_target_empty

def update_viewport_color(self, context):
    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.shading.background_type = 'VIEWPORT'
                        space.shading.background_color = self.viewport_bg_color

def setup_world_nodes(context):
    world = context.scene.world
    if not world:
        world = bpy.data.worlds.new("World")
        context.scene.world = world
    world.use_nodes = True
    tree = world.node_tree
    
    out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
    if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
    bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
    if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
    sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
    if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
    return tree, out_node, bg_node, sky_node

def update_world_mode(self, context):
    tree, out_node, bg_node, sky_node = setup_world_nodes(context)
    for link in bg_node.inputs['Color'].links: tree.links.remove(link)
    if not bg_node.outputs['Background'].links:
        tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
        
    if self.world_mode == 'SKY':
        context.scene.render.film_transparent = False
        sky_node.sky_type = 'NISHITA'
        tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
        update_sky_texture(self, context)
        update_world_settings(self, context)
    elif self.world_mode == 'COLOR':
        context.scene.render.film_transparent = False
        update_world_settings(self, context)
    elif self.world_mode == 'TRANSPARENT':
        context.scene.render.film_transparent = True
        update_world_settings(self, context)

def update_sky_texture(self, context):
    if self.world_mode != 'SKY': return
    _, _, _, sky_node = setup_world_nodes(context)
    sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
    sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
    sky_node.sun_intensity = self.sky_sun_intensity

def update_world_settings(self, context):
    tree, _, bg_node, _ = setup_world_nodes(context)
    if self.world_mode != 'SKY':
        bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
    bg_node.inputs[1].default_value = self.world_bg_strength

def cam_fov_get(self):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        cam.data.angle = math.radians(value)

def view_rotation_euler_get(self):
    rv3d = get_rv3d(bpy.context)
    return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)

def view_rotation_euler_set(self, value):
    rv3d = get_rv3d(bpy.context)
    if rv3d:
        if bpy.context.active_object:
            rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
        rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
    show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
    show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
    
    cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=1.0, min=0.1, update=update_cam_shield)
    cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
    cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=1.0, min=0.0, update=update_cam_shield)
    
    cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
    cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)

class LineCamRigProperties(bpy.types.PropertyGroup):
    anim_factor: bpy.props.FloatProperty(name="進行度 (0.0 〜 1.0)", default=0.0, min=0.0, max=1.0)
    
    anim_start_frame: bpy.props.IntProperty(name="開始フレーム", default=1, min=0)
    anim_end_frame: bpy.props.IntProperty(name="終了フレーム", default=250, min=1)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="カメラ始点 (XYZ)", default=INIT_CAM_START, update=update_line_positions)
    cam_line_end: bpy.props.FloatVectorProperty(name="カメラ終点 (XYZ)", default=INIT_CAM_END, update=update_line_positions)
    tgt_line_start: bpy.props.FloatVectorProperty(name="注視点始点 (XYZ)", default=INIT_TGT_START, update=update_line_positions)
    tgt_line_end: bpy.props.FloatVectorProperty(name="注視点終点 (XYZ)", default=INIT_TGT_END, update=update_line_positions)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", 
        items=[
            ('LINE_TRACK', "同期線分軌道", ""),
            ('OBJECT', "単一オブジェクト", ""), 
            ('POINT', "指定座標 (手動)", ""), 
            ('MIDPOINT', "2オブジェクトの中間", "")
        ],
        default='LINE_TRACK', update=update_cam_target
    )
    cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
    cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
    cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)

    mute_target_tracking: bpy.props.BoolProperty(name="アドオンから切り離し", default=False, update=update_cam_mute)
    show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)

    obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)

    viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
    world_mode: bpy.props.EnumProperty(
        name="ワールド背景モード",
        items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
        default='COLOR', update=update_world_mode
    )
    sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
    sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
    sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
    world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
    world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)

# =========================================================================
# 【オペレーター】
# =========================================================================
class LINECAM_OT_create_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_rig"
    bl_label = "リグ作成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        # 既存クリーンアップ
        names = [OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME]
        for name in names:
            obj = bpy.data.objects.get(name)
            if obj: bpy.data.objects.remove(obj, do_unlink=True)
                
        main_col = bpy.data.collections.get(COLLECTION_NAME)
        if not main_col:
            main_col = bpy.data.collections.new(COLLECTION_NAME)
            context.scene.collection.children.link(main_col)

        def create_line_curve(name, p1, p2):
            crv_data = bpy.data.curves.new(name, type='CURVE')
            crv_data.dimensions = '3D'
            spline = crv_data.splines.new('POLY')
            spline.points.add(1)
            spline.points[0].co = (*p1, 1.0)
            spline.points[1].co = (*p2, 1.0)
            crv_obj = bpy.data.objects.new(name, crv_data)
            main_col.objects.link(crv_obj)
            return crv_obj

        # カーブ作成
        tgt_curve = create_line_curve(OBJ_NAME_TGT_TRACK, props.tgt_line_start, props.tgt_line_end)
        cam_curve = create_line_curve(OBJ_NAME_CAM_TRACK, props.cam_line_start, props.cam_line_end)
        
        # ターゲット作成
        tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
        tgt_empty.empty_display_type = 'PLAIN_AXES'
        main_col.objects.link(tgt_empty)
        
        tgt_path = tgt_empty.constraints.new(type='FOLLOW_PATH')
        tgt_path.target = tgt_curve
        tgt_path.use_curve_follow = False
        tgt_path.use_fixed_location = True
        
        # カメラ作成
        cam_data = bpy.data.cameras.new(name=f"{OBJ_NAME_CAMERA}_Data")
        cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        main_col.objects.link(cam_obj)
        
        cam_path = cam_obj.constraints.new(type='FOLLOW_PATH')
        cam_path.target = cam_curve
        cam_path.use_curve_follow = False
        cam_path.use_fixed_location = True
        
        cam_track = cam_obj.constraints.new(type='TRACK_TO')
        cam_track.target = tgt_empty
        cam_track.track_axis = 'TRACK_NEGATIVE_Z'
        cam_track.up_axis = 'UP_Y'
        
        # ドライバーを設定
        dp = f"{PREFIX_SAFE}_props.anim_factor"
        
        def add_offset_driver(constraint):
            d = constraint.driver_add("offset_factor").driver
            d.type = 'AVERAGE'
            v = d.variables.new()
            v.name = "var"
            v.type = 'SINGLE_PROP'
            v.targets[0].id_type = 'SCENE'
            v.targets[0].id = context.scene
            v.targets[0].data_path = dp
            
        add_offset_driver(tgt_path)
        add_offset_driver(cam_path)
        
        # 包み(シールド)作成
        mesh_sphere = bpy.data.meshes.new(SHIELD_NAME)
        shield_obj = bpy.data.objects.new(SHIELD_NAME, mesh_sphere)
        main_col.objects.link(shield_obj)
        shield_obj.parent = cam_obj
        shield_obj.hide_render = True
        shield_obj.hide_select = True
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
        bm.to_mesh(mesh_sphere)
        bm.free()
        
        mesh_cone = bpy.data.meshes.new(CUTTER_NAME)
        cutter_obj = bpy.data.objects.new(CUTTER_NAME, mesh_cone)
        main_col.objects.link(cutter_obj)
        cutter_obj.parent = cam_obj
        cutter_obj.hide_viewport = True
        cutter_obj.hide_render = True
        cutter_obj.hide_select = True
        cutter_obj.display_type = 'BOUNDS'
        bm_cone = bmesh.new()
        bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
        for v in bm_cone.verts: v.co.z -= 0.95
        bm_cone.to_mesh(mesh_cone)
        bm_cone.free()
        
        # ★ ここを完全に「円筒 (radius1=1.0, radius2=1.0)」に変更
        mesh_cone_back = bpy.data.meshes.new(CUTTER_BACK_NAME)
        cutter_back_obj = bpy.data.objects.new(CUTTER_BACK_NAME, mesh_cone_back)
        main_col.objects.link(cutter_back_obj)
        cutter_back_obj.parent = cam_obj
        cutter_back_obj.hide_viewport = True
        cutter_back_obj.hide_render = True
        cutter_back_obj.hide_select = True
        cutter_back_obj.display_type = 'BOUNDS'
        bm_cone_back = bmesh.new()
        bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
        for v in bm_cone_back.verts: v.co.z += 0.95
        bm_cone_back.to_mesh(mesh_cone_back)
        bm_cone_back.free()
        
        mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
        mod_front.operation = 'DIFFERENCE'
        mod_front.object = cutter_obj
        mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
        mod_back.operation = 'DIFFERENCE'
        mod_back.object = cutter_back_obj
        
        mat = bpy.data.materials.new("CamShield_Mat_Line")
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        
        cprops = cam_obj.cam_rig_props
        cprops.cam_shield_radius = 1.0
        cprops.cam_shield_hole_angle = 179.0
        cprops.cam_shield_hole_radius_back = 1.0
        cprops.cam_shield_color = (0.0, 1.0, 1.0)
        cprops.cam_shield_alpha = 0.3
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = tgt_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        
        props.mute_target_tracking = False
        props.cam_target_mode = 'LINE_TRACK'
        
        update_line_positions(props, context)
        update_cam_target(props, context)
        update_cam_shield(cprops, context)
        update_cam_shield_material(cprops, context)
        
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
        
        self.report({'INFO'}, "線分軌道カメラをセットアップしました")
        return {'FINISHED'}

class LINECAM_OT_rotate_selected(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
    bl_label = "選択オブジェクトを指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=90.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
        rad = math.radians(self.angle)
        for obj in context.selected_objects:
            if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
            if axis == 'X': obj.rotation_euler.x += rad
            elif axis == 'Y': obj.rotation_euler.y += rad
            elif axis == 'Z': obj.rotation_euler.z += rad
        return {'FINISHED'}

class LINECAM_OT_rotate_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
    bl_label = "画面を指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=15.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
        rv3d = get_rv3d(context)
        if rv3d:
            vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
            rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
        return {'FINISHED'}

class LINECAM_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "リンク"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル】
# =========================================================================
class LINECAM_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
    bl_label = "カメラ & ビュー操作ツール"
    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_SAFE}_props")
        
        # 1. 選択オブジェクトを画面中央にする
        layout.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
        layout.separator(factor=1.5)
        
        # 2. リグ作成 & アドオンから切り離し
        box_rig = layout.box()
        box_rig.label(text="専用カメラ (同期線分軌道):", icon='CAMERA_DATA')
        row_rig = box_rig.row(align=True)
        row_rig.operator(f"object.{PREFIX_SAFE}_create_rig", text="リグ作成", icon='ADD')
        row_rig.prop(props, "mute_target_tracking", text="アドオンから切り離し", toggle=True, icon='UNLINKED')
        
        # 3. ビュー情報 (小数点1桁)
        layout.separator(factor=1.5)
        box_info = layout.box()
        row_info = box_info.row(align=True)
        row_info.label(text="ビュー情報:", icon='INFO')
        row_info.operator(f"view3d.{PREFIX_SAFE}_update_info", text="更新", icon='FILE_REFRESH')
        
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        
        if rv3d and space:
            view_loc = rv3d.view_location
            view_dist = rv3d.view_distance
            aspect = context.area.height / context.area.width if context.area.width > 0 else 1.0
            sensor = 32.0 
            
            col_v = box_info.column(align=True)
            col_v.label(text=f"画面中央位置 X: {view_loc.x:.1f}")
            col_v.label(text=f"画面中央位置 Y: {view_loc.y:.1f}")
            col_v.label(text=f"画面中央位置 Z: {view_loc.z:.1f}")
            
            if rv3d.view_perspective == '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
                col_v.label(text=f"画面水平の大きさ: {w:.1f}")
                col_v.label(text=f"画面上下の大きさ: {h:.1f}")
                col_v.label(text=f"視座からの距離: {view_dist:.1f}")
            else:
                w = view_dist * (sensor / space.lens)
                h = w * aspect
                col_v.label(text=f"画面水平の大きさ: {w:.1f}")
                col_v.label(text=f"画面上下の大きさ: {h:.1f}")
                
            box_info.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="ビュー情報を一括でコピー", icon='COPYDOWN')
        
        layout.separator(factor=1.5)
        
        cam = context.scene.camera
        if cam and OBJ_NAME_CAMERA in cam.name:
            
            # 4. 軌道操作 と 注視点モード
            box_cam = layout.box()
            box_cam.label(text="軌道の操作:", icon='PLAY')
            
            col_anim = box_cam.column(align=True)
            col_anim.enabled = not props.mute_target_tracking
            col_anim.prop(props, "anim_factor", slider=True)
            
            row_auto = box_cam.row(align=True)
            row_auto.enabled = not props.mute_target_tracking
            row_auto.prop(props, "anim_start_frame", text="開始F")
            row_auto.prop(props, "anim_end_frame", text="終了F")
            row_auto.operator(f"object.{PREFIX_SAFE}_auto_animate", text="アニメ自動設定", icon='REC')
            
            box_cam.separator()
            col_pos = box_cam.column(align=True)
            col_pos.enabled = not props.mute_target_tracking
            col_pos.prop(props, "cam_line_start", text="カメラ 始点(XYZ)")
            col_pos.prop(props, "cam_line_end", text="カメラ 終点(XYZ)")
            
            box_cam.separator()
            box_cam.label(text="注視点の設定:", icon='EMPTY_DATA')
            col_tgt_main = box_cam.column(align=True)
            col_tgt_main.enabled = not props.mute_target_tracking
            col_tgt_main.prop(props, "cam_target_mode", expand=True)
            
            col_tgt = col_tgt_main.column(align=True)
            if props.cam_target_mode == 'LINE_TRACK':
                col_tgt.prop(props, "tgt_line_start", text="注視点 始点(XYZ)")
                col_tgt.prop(props, "tgt_line_end", text="注視点 終点(XYZ)")
            elif props.cam_target_mode == 'OBJECT':
                col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
            elif props.cam_target_mode == 'POINT':
                col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
            elif props.cam_target_mode == 'MIDPOINT':
                col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
                col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
            
            # 手動操作 (Location, Pitch, Roll, Yaw) と情報表示
            box_sight = box_cam.box()
            box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
            col_sight = box_sight.column(align=True)
            col_sight.enabled = props.mute_target_tracking
            col_sight.prop(cam, "location", text="位置 (XYZ)")
            col_sight.separator()
            col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
            col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
            col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
            
            box_sight.separator()
            
            # カメラ現在情報の表示・コピー
            depsgraph = context.evaluated_depsgraph_get()
            eval_cam = cam.evaluated_get(depsgraph)
            eval_loc = eval_cam.matrix_world.to_translation()
            eval_rot = eval_cam.matrix_world.to_euler('XYZ')
            
            col_cinfo = box_sight.column(align=True)
            col_cinfo.label(text="[ カメラ現在情報 ]", icon='INFO')
            col_cinfo.label(text=f"X: {eval_loc.x:.1f}  |  Y: {eval_loc.y:.1f}  |  Z: {eval_loc.z:.1f}")
            col_cinfo.label(text=f"Pitch: {math.degrees(eval_rot.x):.1f}  |  Roll: {math.degrees(eval_rot.y):.1f}  |  Yaw: {math.degrees(eval_rot.z):.1f}")
            col_cinfo.operator(f"view3d.{PREFIX_SAFE}_copy_cam_info", text="カメラ情報を一括でコピー", icon='COPYDOWN')

            # 5. カメラ・包み設定
            box_cam.separator()
            col_lens = box_cam.column(align=True)
            col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
            col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
            col_lens.separator()
            col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
            col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
            
            box_cam.separator()
            col_pp = box_cam.column(align=True)
            col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
                
            box_cam.separator()
            box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)", toggle=True, icon='STYLUS_PRESSURE')
            if cam.data.dof.use_dof:
                col_dof = box_cam.column(align=True)
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
            
            if hasattr(cam, "cam_rig_props"):
                cprops = cam.cam_rig_props
                box_cam.separator()
                box_shield = box_cam.box()
                box_shield.label(text="包み (シールド) 設定:", icon='RESTRICT_VIEW_OFF')
                
                row_disp = box_shield.row()
                row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
                row_disp.prop(cprops, "show_cam_shield", text="包み")
                box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
                
                col_sh = box_shield.column(align=True)
                col_sh.enabled = cprops.show_cam_shield
                col_sh.prop(cprops, "cam_shield_radius", text="半径")
                col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
                col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴 (円筒の半径)")
                col_sh.separator()
                col_sh.prop(cprops, "cam_shield_color", text="色")
                col_sh.prop(cprops, "cam_shield_alpha", text="透明度", slider=True)
            
            layout.separator(factor=1.5)
            
        # 6. オブジェクト&画面 回転操作
        box_rot = layout.box()
        box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
        box_rot.prop(props, "obj_rot_axis", expand=True)
        if context.active_object:
            axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
            box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
            row = box_rot.row(align=True)
            for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
        else:
            box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
            
        box_vrot = layout.box()
        box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
        box_vrot.prop(props, "view_rot_axis", expand=True)
        v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
        box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
        row_v = box_vrot.row(align=True)
        for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
        
        layout.separator(factor=1.5)
        
        # 7. ワールド・背景
        box_render = layout.box()
        box_render.label(text="レンダリング & ワールド:", icon='SHADING_RENDERED')
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        box_render.prop(props, "world_mode", expand=True)
        
        col_world = box_render.column(align=True)
        if props.world_mode == 'SKY':
            col_world.prop(props, "sky_sun_elevation", text="太陽の高さ")
            col_world.prop(props, "sky_sun_rotation", text="太陽の向き")
            col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
            col_world.prop(props, "world_bg_strength", text="全体の明るさ")
        elif props.world_mode == 'COLOR':
            col_world.prop(props, "world_bg_color", text="背景の色")
            col_world.prop(props, "world_bg_strength", text="明るさ")
        elif props.world_mode == 'TRANSPARENT':
            col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング")
        
        layout.separator(factor=1.5)
        
        # 8. リンク & 削除パネル
        box_sys = layout.box()
        box_sys.label(text="システム:", icon='PREFERENCES')
        box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="カメラ線分軌道特化 20260710", icon='URL').url = LINK_URL
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオン削除パネル", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigObjectProperties, LineCamRigProperties,
    LINECAM_OT_reset_view, LINECAM_OT_update_info, LINECAM_OT_copy_view_info, LINECAM_OT_copy_cam_info,
    LINECAM_OT_auto_animate, LINECAM_OT_create_rig, LINECAM_OT_rotate_selected, LINECAM_OT_rotate_view,
    LINECAM_OT_open_url, LINECAM_OT_remove_addon, LINECAM_PT_main_panel
]

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
    setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=LineCamRigProperties))

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

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
円筒穴
bl_info = {
    "name": "Sync Line Camera & View Tools",
    "author": "Your Name",
    "version": (2, 4),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "線分軌道カメラの同期移動、アニメ自動設定(ドライバー対応版)、包み、ビュー操作ツール",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】 2026年7月10日 23:25
# =========================================================================
TAB_NAME = "Line Cam 20260710"
PREFIX_NAME = "SyncLineCamRig"
PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

COLLECTION_NAME = "CamRig_Line_Collection"

OBJ_NAME_CAM_TRACK = "CamTrack_Line"
OBJ_NAME_TGT_TRACK = "TgtTrack_Line"
OBJ_NAME_CAMERA = "TrackingCamera_Line"
OBJ_NAME_TARGET = "CameraTarget_Line"

SHIELD_NAME = "CamShield_Sphere_Line"
CUTTER_NAME = "CamShield_Cutter_Line"
CUTTER_BACK_NAME = "CamShield_CutterBack_Line"

INIT_CAM_START = (-15.0, -15.0, 5.0)
INIT_CAM_END = (15.0, -15.0, 5.0)
INIT_TGT_START = (0.0, 0.0, 0.0)
INIT_TGT_END = (0.0, 0.0, 0.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

class LINECAM_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
        rv3d.view_perspective = 'PERSP'
        context.view_layer.update()
        return {'FINISHED'}

class LINECAM_OT_update_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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 LINECAM_OT_copy_view_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_copy_view_info"
    bl_label = "情報をコピー"
    def execute(self, context):
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        if not rv3d or not space: return {'CANCELLED'}
        
        view_loc = rv3d.view_location
        view_dist = rv3d.view_distance
        area = context.area
        aspect = area.height / area.width if area.width > 0 else 1.0
        sensor = 32.0 
        
        lines = []
        lines.append(f"画面中央位置 X: {view_loc.x:.1f}")
        lines.append(f"画面中央位置 Y: {view_loc.y:.1f}")
        lines.append(f"画面中央位置 Z: {view_loc.z:.1f}")
        
        if rv3d.view_perspective == '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
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")
            lines.append(f"視座からの距離: {view_dist:.1f}")
        else:
            w = view_dist * (sensor / space.lens)
            h = w * aspect
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")

        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
        return {'FINISHED'}

class LINECAM_OT_copy_cam_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_copy_cam_info"
    bl_label = "カメラ情報をコピー"
    def execute(self, context):
        cam = context.scene.camera
        if not cam: return {'CANCELLED'}
        
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        loc = eval_cam.matrix_world.to_translation()
        rot = eval_cam.matrix_world.to_euler('XYZ')
        
        lines = []
        lines.append(f"カメラ位置 X: {loc.x:.1f}")
        lines.append(f"カメラ位置 Y: {loc.y:.1f}")
        lines.append(f"カメラ位置 Z: {loc.z:.1f}")
        lines.append(f"Pitch (X): {math.degrees(rot.x):.1f}")
        lines.append(f"Roll (Y): {math.degrees(rot.y):.1f}")
        lines.append(f"Yaw (Z): {math.degrees(rot.z):.1f}")
        
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "カメラ情報をクリップボードにコピーしました")
        return {'FINISHED'}

# =========================================================================
# 【カメラアニメーション設定】
# =========================================================================
class LINECAM_OT_auto_animate(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_auto_animate"
    bl_label = "進行度アニメーションを自動設定"
    bl_description = "指定したフレーム間に進行度0.0〜1.0のキーフレームを打ち、等速移動させます"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        prefs = context.preferences
        
        # 現在のユーザー設定(デフォルトのキーフレーム補間タイプ)を保存
        prev_interp = prefs.edit.keyframe_new_interpolation_type
        
        try:
            # Blender5.0エラー回避のため、一時的に補間モードをLINEAR(等速)にしてからキーを打つ
            prefs.edit.keyframe_new_interpolation_type = 'LINEAR'
            
            props.anim_factor = 0.0
            props.keyframe_insert(data_path="anim_factor", frame=props.anim_start_frame)
            
            props.anim_factor = 1.0
            props.keyframe_insert(data_path="anim_factor", frame=props.anim_end_frame)
            
        finally:
            # ユーザー設定を元に戻す
            prefs.edit.keyframe_new_interpolation_type = prev_interp
                        
        self.report({'INFO'}, f"アニメーション設定完了: {props.anim_start_frame}F 〜 {props.anim_end_frame}F")
        return {'FINISHED'}

# =========================================================================
# 【カメラ・注視点 制御 & 更新関数】
# =========================================================================
def update_line_positions(self, context):
    cam_curve = bpy.data.objects.get(OBJ_NAME_CAM_TRACK)
    if cam_curve and cam_curve.data.splines:
        pts = cam_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.cam_line_start, 1.0)
            pts[1].co = (*self.cam_line_end, 1.0)
            
    tgt_curve = bpy.data.objects.get(OBJ_NAME_TGT_TRACK)
    if tgt_curve and tgt_curve.data.splines:
        pts = tgt_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.tgt_line_start, 1.0)
            pts[1].co = (*self.tgt_line_end, 1.0)

def update_cam_target(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not tgt: return
    
    for c in tgt.constraints:
        if c.type == 'COPY_LOCATION':
            tgt.constraints.remove(c)
            
    fp = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None)
    mode = self.cam_target_mode
    
    if mode == 'LINE_TRACK':
        if fp: fp.mute = False
    else:
        if fp: fp.mute = True
        
        if mode == 'OBJECT' and self.cam_target_obj1:
            c1 = tgt.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
        elif mode == 'POINT':
            tgt.location = self.cam_target_loc
        elif mode == 'MIDPOINT':
            if self.cam_target_obj1:
                c1 = tgt.constraints.new(type='COPY_LOCATION')
                c1.target = self.cam_target_obj1
                c1.influence = 1.0
            if self.cam_target_obj2:
                c2 = tgt.constraints.new(type='COPY_LOCATION')
                c2.target = self.cam_target_obj2
                c2.influence = 0.5

def update_cam_mute(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not cam: return
    
    cam_track = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    cam_path = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    tgt_path = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None) if tgt else None
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        cam.location = eval_cam.matrix_world.to_translation()
        cam.rotation_euler = eval_cam.matrix_world.to_euler(cam.rotation_mode)
        if tgt:
            eval_tgt = tgt.evaluated_get(depsgraph)
            tgt.location = eval_tgt.matrix_world.to_translation()
            
        if cam_track: cam_track.mute = True
        if cam_path: cam_path.mute = True
        if tgt_path: tgt_path.mute = True
    else:
        if cam_track: cam_track.mute = False
        if cam_path: cam_path.mute = False
        if tgt_path and self.cam_target_mode == 'LINE_TRACK': 
            tgt_path.mute = False

# =========================================================================
# 【包み & その他環境制御】
# =========================================================================
def update_cam_obj_visibility(self, context):
    cam = self.id_data 
    if cam and cam.type == 'CAMERA': cam.hide_viewport = not self.show_cam_obj

def update_cam_shield_visibility(self, context):
    cam = self.id_data
    if not cam: return
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"):
            child.hide_viewport = not self.show_cam_shield

def update_cam_shield(self, context):
    cam = self.id_data
    if not cam: return
    shield, cutter, cutter_back = None, None, None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"): shield = child
        elif child.name.startswith("CamShield_Cutter_"): cutter = child
        elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
        
    if shield:
        shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
        
    z_scale = self.cam_shield_radius * 1.5
    if cutter:
        front_deg = max(0.1, min(179.9, self.cam_shield_hole_angle))
        angle = math.radians(front_deg)
        r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
        cutter.scale = (r_scale, r_scale, z_scale)
    if cutter_back:
        back_deg = max(0.1, min(179.9, self.cam_shield_hole_angle_back - 180.0))
        back_angle = math.radians(back_deg)
        r_scale_back = 2.0 * z_scale * math.tan(back_angle / 2.0)
        cutter_back.scale = (r_scale_back, r_scale_back, z_scale)

def update_cam_shield_material(self, context):
    cam = self.id_data
    if not cam: return
    shield = next((c for c in cam.children if c.name.startswith("CamShield_Sphere_")), None)
    if not shield: return
    
    if shield.data.materials:
        mat = shield.data.materials[0]
        if mat:
            mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
            if mat.use_nodes:
                bsdf = mat.node_tree.nodes.get("Principled BSDF")
                if bsdf:
                    if 'Base Color' in bsdf.inputs:
                        bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
                    if 'Alpha' in bsdf.inputs:
                        bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
    shield.color = (*self.cam_shield_color, self.cam_shield_alpha)

def update_target_empty_visibility(self, context):
    for obj in bpy.data.objects:
        if obj.name in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK]:
            obj.hide_viewport = not self.show_target_empty

def update_viewport_color(self, context):
    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.shading.background_type = 'VIEWPORT'
                        space.shading.background_color = self.viewport_bg_color

def setup_world_nodes(context):
    world = context.scene.world
    if not world:
        world = bpy.data.worlds.new("World")
        context.scene.world = world
    world.use_nodes = True
    tree = world.node_tree
    
    out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
    if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
    bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
    if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
    sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
    if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
    return tree, out_node, bg_node, sky_node

def update_world_mode(self, context):
    tree, out_node, bg_node, sky_node = setup_world_nodes(context)
    for link in bg_node.inputs['Color'].links: tree.links.remove(link)
    if not bg_node.outputs['Background'].links:
        tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
        
    if self.world_mode == 'SKY':
        context.scene.render.film_transparent = False
        sky_node.sky_type = 'NISHITA'
        tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
        update_sky_texture(self, context)
        update_world_settings(self, context)
    elif self.world_mode == 'COLOR':
        context.scene.render.film_transparent = False
        update_world_settings(self, context)
    elif self.world_mode == 'TRANSPARENT':
        context.scene.render.film_transparent = True
        update_world_settings(self, context)

def update_sky_texture(self, context):
    if self.world_mode != 'SKY': return
    _, _, _, sky_node = setup_world_nodes(context)
    sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
    sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
    sky_node.sun_intensity = self.sky_sun_intensity

def update_world_settings(self, context):
    tree, _, bg_node, _ = setup_world_nodes(context)
    if self.world_mode != 'SKY':
        bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
    bg_node.inputs[1].default_value = self.world_bg_strength

def cam_fov_get(self):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        cam.data.angle = math.radians(value)

def view_rotation_euler_get(self):
    rv3d = get_rv3d(bpy.context)
    return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)

def view_rotation_euler_set(self, value):
    rv3d = get_rv3d(bpy.context)
    if rv3d:
        if bpy.context.active_object:
            rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
        rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
    show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
    show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
    
    cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=1.0, min=0.1, update=update_cam_shield)
    cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
    cam_shield_hole_angle_back: bpy.props.FloatProperty(name="後方の穴 (反対側)", default=200.0, min=181.0, max=360.0, update=update_cam_shield)
    cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 0.8, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
    cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)

class LineCamRigProperties(bpy.types.PropertyGroup):
    # ★ ここからは update関数を使わずに、ドライバーを通じて制約を動かします
    anim_factor: bpy.props.FloatProperty(name="進行度 (0.0 〜 1.0)", default=0.0, min=0.0, max=1.0)
    
    anim_start_frame: bpy.props.IntProperty(name="開始フレーム", default=1, min=0)
    anim_end_frame: bpy.props.IntProperty(name="終了フレーム", default=250, min=1)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="カメラ始点 (XYZ)", default=INIT_CAM_START, update=update_line_positions)
    cam_line_end: bpy.props.FloatVectorProperty(name="カメラ終点 (XYZ)", default=INIT_CAM_END, update=update_line_positions)
    tgt_line_start: bpy.props.FloatVectorProperty(name="注視点始点 (XYZ)", default=INIT_TGT_START, update=update_line_positions)
    tgt_line_end: bpy.props.FloatVectorProperty(name="注視点終点 (XYZ)", default=INIT_TGT_END, update=update_line_positions)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", 
        items=[
            ('LINE_TRACK', "同期線分軌道", ""),
            ('OBJECT', "単一オブジェクト", ""), 
            ('POINT', "指定座標 (手動)", ""), 
            ('MIDPOINT', "2オブジェクトの中間", "")
        ],
        default='LINE_TRACK', update=update_cam_target
    )
    cam_target_obj1: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット1", update=update_cam_target)
    cam_target_obj2: bpy.props.PointerProperty(type=bpy.types.Object, name="ターゲット2", update=update_cam_target)
    cam_target_loc: bpy.props.FloatVectorProperty(name="注視点 座標", default=(0,0,0), update=update_cam_target)

    mute_target_tracking: bpy.props.BoolProperty(name="アドオンから切り離し", default=False, update=update_cam_mute)
    show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)

    obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)

    viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
    world_mode: bpy.props.EnumProperty(
        name="ワールド背景モード",
        items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
        default='COLOR', update=update_world_mode
    )
    sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
    sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
    sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
    world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
    world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)

# =========================================================================
# 【オペレーター】
# =========================================================================
class LINECAM_OT_create_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_rig"
    bl_label = "リグ作成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        # 既存クリーンアップ
        names = [OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME]
        for name in names:
            obj = bpy.data.objects.get(name)
            if obj: bpy.data.objects.remove(obj, do_unlink=True)
                
        main_col = bpy.data.collections.get(COLLECTION_NAME)
        if not main_col:
            main_col = bpy.data.collections.new(COLLECTION_NAME)
            context.scene.collection.children.link(main_col)

        def create_line_curve(name, p1, p2):
            crv_data = bpy.data.curves.new(name, type='CURVE')
            crv_data.dimensions = '3D'
            spline = crv_data.splines.new('POLY')
            spline.points.add(1)
            spline.points[0].co = (*p1, 1.0)
            spline.points[1].co = (*p2, 1.0)
            crv_obj = bpy.data.objects.new(name, crv_data)
            main_col.objects.link(crv_obj)
            return crv_obj

        # カーブ作成
        tgt_curve = create_line_curve(OBJ_NAME_TGT_TRACK, props.tgt_line_start, props.tgt_line_end)
        cam_curve = create_line_curve(OBJ_NAME_CAM_TRACK, props.cam_line_start, props.cam_line_end)
        
        # ターゲット作成
        tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
        tgt_empty.empty_display_type = 'PLAIN_AXES'
        main_col.objects.link(tgt_empty)
        
        tgt_path = tgt_empty.constraints.new(type='FOLLOW_PATH')
        tgt_path.target = tgt_curve
        tgt_path.use_curve_follow = False
        tgt_path.use_fixed_location = True
        
        # カメラ作成
        cam_data = bpy.data.cameras.new(name=f"{OBJ_NAME_CAMERA}_Data")
        cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        main_col.objects.link(cam_obj)
        
        cam_path = cam_obj.constraints.new(type='FOLLOW_PATH')
        cam_path.target = cam_curve
        cam_path.use_curve_follow = False
        cam_path.use_fixed_location = True
        
        cam_track = cam_obj.constraints.new(type='TRACK_TO')
        cam_track.target = tgt_empty
        cam_track.track_axis = 'TRACK_NEGATIVE_Z'
        cam_track.up_axis = 'UP_Y'
        
        # ★ドライバーを設定して、アニメーション再生中もスライダーの値と完全に同期させる
        dp = f"{PREFIX_SAFE}_props.anim_factor"
        
        def add_offset_driver(constraint):
            d = constraint.driver_add("offset_factor").driver
            d.type = 'AVERAGE'
            v = d.variables.new()
            v.name = "var"
            v.type = 'SINGLE_PROP'
            v.targets[0].id_type = 'SCENE'
            v.targets[0].id = context.scene
            v.targets[0].data_path = dp
            
        add_offset_driver(tgt_path)
        add_offset_driver(cam_path)
        
        # 包み(シールド)作成
        mesh_sphere = bpy.data.meshes.new(SHIELD_NAME)
        shield_obj = bpy.data.objects.new(SHIELD_NAME, mesh_sphere)
        main_col.objects.link(shield_obj)
        shield_obj.parent = cam_obj
        shield_obj.hide_render = True
        shield_obj.hide_select = True
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
        bm.to_mesh(mesh_sphere)
        bm.free()
        
        mesh_cone = bpy.data.meshes.new(CUTTER_NAME)
        cutter_obj = bpy.data.objects.new(CUTTER_NAME, mesh_cone)
        main_col.objects.link(cutter_obj)
        cutter_obj.parent = cam_obj
        cutter_obj.hide_viewport = True
        cutter_obj.hide_render = True
        cutter_obj.hide_select = True
        cutter_obj.display_type = 'BOUNDS'
        bm_cone = bmesh.new()
        bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
        for v in bm_cone.verts: v.co.z -= 0.95
        bm_cone.to_mesh(mesh_cone)
        bm_cone.free()
        
        mesh_cone_back = bpy.data.meshes.new(CUTTER_BACK_NAME)
        cutter_back_obj = bpy.data.objects.new(CUTTER_BACK_NAME, mesh_cone_back)
        main_col.objects.link(cutter_back_obj)
        cutter_back_obj.parent = cam_obj
        cutter_back_obj.hide_viewport = True
        cutter_back_obj.hide_render = True
        cutter_back_obj.hide_select = True
        cutter_back_obj.display_type = 'BOUNDS'
        bm_cone_back = bmesh.new()
        bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=0.001, radius2=1.0, depth=2.0)
        for v in bm_cone_back.verts: v.co.z += 0.95
        bm_cone_back.to_mesh(mesh_cone_back)
        bm_cone_back.free()
        
        mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
        mod_front.operation = 'DIFFERENCE'
        mod_front.object = cutter_obj
        mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
        mod_back.operation = 'DIFFERENCE'
        mod_back.object = cutter_back_obj
        
        mat = bpy.data.materials.new("CamShield_Mat_Line")
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        
        cprops = cam_obj.cam_rig_props
        cprops.cam_shield_radius = 1.0
        cprops.cam_shield_hole_angle = 179.0
        cprops.cam_shield_hole_angle_back = 200.0
        cprops.cam_shield_color = (0.0, 1.0, 1.0)
        cprops.cam_shield_alpha = 0.3
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = tgt_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        
        props.mute_target_tracking = False
        props.cam_target_mode = 'LINE_TRACK'
        
        update_line_positions(props, context)
        update_cam_target(props, context)
        update_cam_shield(cprops, context)
        update_cam_shield_material(cprops, context)
        
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
        
        self.report({'INFO'}, "線分軌道カメラをセットアップしました")
        return {'FINISHED'}

class LINECAM_OT_rotate_selected(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
    bl_label = "選択オブジェクトを指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=90.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
        rad = math.radians(self.angle)
        for obj in context.selected_objects:
            if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
            if axis == 'X': obj.rotation_euler.x += rad
            elif axis == 'Y': obj.rotation_euler.y += rad
            elif axis == 'Z': obj.rotation_euler.z += rad
        return {'FINISHED'}

class LINECAM_OT_rotate_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
    bl_label = "画面を指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=15.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
        rv3d = get_rv3d(context)
        if rv3d:
            vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
            rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
        return {'FINISHED'}

class LINECAM_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "リンク"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル】
# =========================================================================
class LINECAM_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
    bl_label = "カメラ & ビュー操作ツール"
    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_SAFE}_props")
        
        # 1. 選択オブジェクトを画面中央にする
        layout.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
        layout.separator(factor=1.5)
        
        # 2. リグ作成 & アドオンから切り離し
        box_rig = layout.box()
        box_rig.label(text="専用カメラ (同期線分軌道):", icon='CAMERA_DATA')
        row_rig = box_rig.row(align=True)
        row_rig.operator(f"object.{PREFIX_SAFE}_create_rig", text="リグ作成", icon='ADD')
        row_rig.prop(props, "mute_target_tracking", text="アドオンから切り離し", toggle=True, icon='UNLINKED')
        
        # 3. ビュー情報 (小数点1桁)
        layout.separator(factor=1.5)
        box_info = layout.box()
        row_info = box_info.row(align=True)
        row_info.label(text="ビュー情報:", icon='INFO')
        row_info.operator(f"view3d.{PREFIX_SAFE}_update_info", text="更新", icon='FILE_REFRESH')
        
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        
        if rv3d and space:
            view_loc = rv3d.view_location
            view_dist = rv3d.view_distance
            aspect = context.area.height / context.area.width if context.area.width > 0 else 1.0
            sensor = 32.0 
            
            col_v = box_info.column(align=True)
            col_v.label(text=f"画面中央位置 X: {view_loc.x:.1f}")
            col_v.label(text=f"画面中央位置 Y: {view_loc.y:.1f}")
            col_v.label(text=f"画面中央位置 Z: {view_loc.z:.1f}")
            
            if rv3d.view_perspective == '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
                col_v.label(text=f"画面水平の大きさ: {w:.1f}")
                col_v.label(text=f"画面上下の大きさ: {h:.1f}")
                col_v.label(text=f"視座からの距離: {view_dist:.1f}")
            else:
                w = view_dist * (sensor / space.lens)
                h = w * aspect
                col_v.label(text=f"画面水平の大きさ: {w:.1f}")
                col_v.label(text=f"画面上下の大きさ: {h:.1f}")
                
            box_info.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="ビュー情報を一括でコピー", icon='COPYDOWN')
        
        layout.separator(factor=1.5)
        
        cam = context.scene.camera
        if cam and OBJ_NAME_CAMERA in cam.name:
            
            # 4. 軌道操作 と 注視点モード
            box_cam = layout.box()
            box_cam.label(text="軌道の操作:", icon='PLAY')
            
            col_anim = box_cam.column(align=True)
            col_anim.enabled = not props.mute_target_tracking
            col_anim.prop(props, "anim_factor", slider=True)
            
            row_auto = box_cam.row(align=True)
            row_auto.enabled = not props.mute_target_tracking
            row_auto.prop(props, "anim_start_frame", text="開始F")
            row_auto.prop(props, "anim_end_frame", text="終了F")
            row_auto.operator(f"object.{PREFIX_SAFE}_auto_animate", text="アニメ自動設定", icon='REC')
            
            box_cam.separator()
            col_pos = box_cam.column(align=True)
            col_pos.enabled = not props.mute_target_tracking
            col_pos.prop(props, "cam_line_start", text="カメラ 始点(XYZ)")
            col_pos.prop(props, "cam_line_end", text="カメラ 終点(XYZ)")
            
            box_cam.separator()
            box_cam.label(text="注視点の設定:", icon='EMPTY_DATA')
            col_tgt_main = box_cam.column(align=True)
            col_tgt_main.enabled = not props.mute_target_tracking
            col_tgt_main.prop(props, "cam_target_mode", expand=True)
            
            col_tgt = col_tgt_main.column(align=True)
            if props.cam_target_mode == 'LINE_TRACK':
                col_tgt.prop(props, "tgt_line_start", text="注視点 始点(XYZ)")
                col_tgt.prop(props, "tgt_line_end", text="注視点 終点(XYZ)")
            elif props.cam_target_mode == 'OBJECT':
                col_tgt.prop(props, "cam_target_obj1", text="追従オブジェクト")
            elif props.cam_target_mode == 'POINT':
                col_tgt.prop(props, "cam_target_loc", text="指定座標(XYZ)")
            elif props.cam_target_mode == 'MIDPOINT':
                col_tgt.prop(props, "cam_target_obj1", text="オブジェクト 1")
                col_tgt.prop(props, "cam_target_obj2", text="オブジェクト 2")
            
            # 手動操作 (Location, Pitch, Roll, Yaw) と情報表示
            box_sight = box_cam.box()
            box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
            col_sight = box_sight.column(align=True)
            col_sight.enabled = props.mute_target_tracking
            col_sight.prop(cam, "location", text="位置 (XYZ)")
            col_sight.separator()
            col_sight.prop(cam, "rotation_euler", index=0, text="Pitch (上下・X)")
            col_sight.prop(cam, "rotation_euler", index=1, text="Roll (傾き・Y)")
            col_sight.prop(cam, "rotation_euler", index=2, text="Yaw (左右・Z)")
            
            box_sight.separator()
            
            # カメラ現在情報の表示・コピー
            depsgraph = context.evaluated_depsgraph_get()
            eval_cam = cam.evaluated_get(depsgraph)
            eval_loc = eval_cam.matrix_world.to_translation()
            eval_rot = eval_cam.matrix_world.to_euler('XYZ')
            
            col_cinfo = box_sight.column(align=True)
            col_cinfo.label(text="[ カメラ現在情報 ]", icon='INFO')
            col_cinfo.label(text=f"X: {eval_loc.x:.1f}  |  Y: {eval_loc.y:.1f}  |  Z: {eval_loc.z:.1f}")
            col_cinfo.label(text=f"Pitch: {math.degrees(eval_rot.x):.1f}  |  Roll: {math.degrees(eval_rot.y):.1f}  |  Yaw: {math.degrees(eval_rot.z):.1f}")
            col_cinfo.operator(f"view3d.{PREFIX_SAFE}_copy_cam_info", text="カメラ情報を一括でコピー", icon='COPYDOWN')

            # 5. カメラ・包み設定
            box_cam.separator()
            col_lens = box_cam.column(align=True)
            col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
            col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
            col_lens.separator()
            col_lens.prop(cam.data, "clip_start", text="クリップ開始 (Clip Start)")
            col_lens.prop(cam.data, "clip_end", text="クリップ終了 (Clip End)")
            
            box_cam.separator()
            col_pp = box_cam.column(align=True)
            col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
                
            box_cam.separator()
            box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)", toggle=True, icon='STYLUS_PRESSURE')
            if cam.data.dof.use_dof:
                col_dof = box_cam.column(align=True)
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
            
            if hasattr(cam, "cam_rig_props"):
                cprops = cam.cam_rig_props
                box_cam.separator()
                box_shield = box_cam.box()
                box_shield.label(text="包み (シールド) 設定:", icon='RESTRICT_VIEW_OFF')
                
                row_disp = box_shield.row()
                row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
                row_disp.prop(cprops, "show_cam_shield", text="包み")
                box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
                
                col_sh = box_shield.column(align=True)
                col_sh.enabled = cprops.show_cam_shield
                col_sh.prop(cprops, "cam_shield_radius", text="半径")
                col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
                col_sh.prop(cprops, "cam_shield_hole_angle_back", text="後方の穴 (181〜360度)")
                col_sh.separator()
                col_sh.prop(cprops, "cam_shield_color", text="色")
                col_sh.prop(cprops, "cam_shield_alpha", text="透明度", slider=True)
            
            layout.separator(factor=1.5)
            
        # 6. オブジェクト&画面 回転操作
        box_rot = layout.box()
        box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
        box_rot.prop(props, "obj_rot_axis", expand=True)
        if context.active_object:
            axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
            box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
            row = box_rot.row(align=True)
            for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
        else:
            box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
            
        box_vrot = layout.box()
        box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
        box_vrot.prop(props, "view_rot_axis", expand=True)
        v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
        box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
        row_v = box_vrot.row(align=True)
        for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
        
        layout.separator(factor=1.5)
        
        # 7. ワールド・背景
        box_render = layout.box()
        box_render.label(text="レンダリング & ワールド:", icon='SHADING_RENDERED')
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        box_render.prop(props, "world_mode", expand=True)
        
        col_world = box_render.column(align=True)
        if props.world_mode == 'SKY':
            col_world.prop(props, "sky_sun_elevation", text="太陽の高さ")
            col_world.prop(props, "sky_sun_rotation", text="太陽の向き")
            col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
            col_world.prop(props, "world_bg_strength", text="全体の明るさ")
        elif props.world_mode == 'COLOR':
            col_world.prop(props, "world_bg_color", text="背景の色")
            col_world.prop(props, "world_bg_strength", text="明るさ")
        elif props.world_mode == 'TRANSPARENT':
            col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング")
        
        layout.separator(factor=1.5)
        
        # 8. リンク & 削除パネル
        box_sys = layout.box()
        box_sys.label(text="システム:", icon='PREFERENCES')
        box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="カメラ線分軌道特化 20260710", icon='URL').url = LINK_URL
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオン削除パネル", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigObjectProperties, LineCamRigProperties,
    LINECAM_OT_reset_view, LINECAM_OT_update_info, LINECAM_OT_copy_view_info, LINECAM_OT_copy_cam_info,
    LINECAM_OT_auto_animate, LINECAM_OT_create_rig, LINECAM_OT_rotate_selected, LINECAM_OT_rotate_view,
    LINECAM_OT_open_url, LINECAM_OT_remove_addon, LINECAM_PT_main_panel
]

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
    setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=LineCamRigProperties))

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

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

bl_info = {
    "name": "Line Camera Rig & View Tools",
    "author": "Your Name",
    "version": (2, 0),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "線分軌道の同期移動と残存機能、ビュー情報取得",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】 2026年7月10日 22時26分00秒
# =========================================================================
COLLECTION_NAME = "CamRig_Collection"
SUB_COLLECTION_NAME = "CamRig_Line_Sub"

OBJ_NAME_CAM_TRACK = "CamTrack_Line"
OBJ_NAME_TGT_TRACK = "TgtTrack_Line"
OBJ_NAME_CAMERA = "TrackingCamera_Line"
OBJ_NAME_TARGET = "CameraTarget_Line"

SHIELD_NAME = "CamShield_Sphere_Line"
CUTTER_NAME = "CamShield_Cutter_Line"
CUTTER_BACK_NAME = "CamShield_CutterBack_Line"

INIT_CAM_START = (-15.0, -15.0, 5.0)
INIT_CAM_END = (15.0, -15.0, 5.0)
INIT_TGT_START = (0.0, 0.0, 0.0)
INIT_TGT_END = (0.0, 0.0, 0.0)

PREFIX_SAFE = "linecamrig"

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

class LINECAM_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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_rotation = mathutils.Euler((math.radians(90.0), 0.0, 0.0), 'XYZ').to_quaternion()
        rv3d.view_perspective = 'PERSP'
        context.view_layer.update()
        return {'FINISHED'}

class LINECAM_OT_update_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_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 LINECAM_OT_copy_view_info(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_copy_view_info"
    bl_label = "情報をコピー"
    
    def execute(self, context):
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        if not rv3d or not space: return {'CANCELLED'}
        
        view_loc = rv3d.view_location
        view_dist = rv3d.view_distance
        area = context.area
        aspect = area.height / area.width if area.width > 0 else 1.0
        sensor = 32.0 
        
        lines = []
        lines.append(f"画面中央位置 X: {view_loc.x:.1f}")
        lines.append(f"画面中央位置 Y: {view_loc.y:.1f}")
        lines.append(f"画面中央位置 Z: {view_loc.z:.1f}")
        
        if rv3d.view_perspective == '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
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")
            lines.append(f"視座からの距離: {view_dist:.1f}")
        else:
            w = view_dist * (sensor / space.lens)
            h = w * aspect
            lines.append(f"画面水平の大きさ: {w:.1f}")
            lines.append(f"画面上下の大きさ: {h:.1f}")

        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "ビュー情報をクリップボードにコピーしました")
        return {'FINISHED'}

# =========================================================================
# 【カメラ軌道・注視点 同期制御】
# =========================================================================
def update_line_positions(self, context):
    cam_curve = bpy.data.objects.get(OBJ_NAME_CAM_TRACK)
    if cam_curve and cam_curve.data.splines:
        pts = cam_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.cam_line_start, 1.0)
            pts[1].co = (*self.cam_line_end, 1.0)
            
    tgt_curve = bpy.data.objects.get(OBJ_NAME_TGT_TRACK)
    if tgt_curve and tgt_curve.data.splines:
        pts = tgt_curve.data.splines[0].points
        if len(pts) >= 2:
            pts[0].co = (*self.tgt_line_start, 1.0)
            pts[1].co = (*self.tgt_line_end, 1.0)

def update_anim_factor(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    
    if cam:
        for c in cam.constraints:
            if c.type == 'FOLLOW_PATH' and c.target and c.target.name == OBJ_NAME_CAM_TRACK:
                c.offset_factor = self.anim_factor
    if tgt:
        for c in tgt.constraints:
            if c.type == 'FOLLOW_PATH' and c.target and c.target.name == OBJ_NAME_TGT_TRACK:
                c.offset_factor = self.anim_factor

def update_cam_mute(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not cam: return
    
    cam_track = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    cam_path = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    tgt_path = next((c for c in tgt.constraints if c.type == 'FOLLOW_PATH'), None) if tgt else None
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        cam.location = eval_cam.matrix_world.to_translation()
        cam.rotation_euler = eval_cam.matrix_world.to_euler(cam.rotation_mode)
        
        if tgt:
            eval_tgt = tgt.evaluated_get(depsgraph)
            tgt.location = eval_tgt.matrix_world.to_translation()
            
        if cam_track: cam_track.mute = True
        if cam_path: cam_path.mute = True
        if tgt_path: tgt_path.mute = True
    else:
        if cam_track: cam_track.mute = False
        if cam_path: cam_path.mute = False
        if tgt_path: tgt_path.mute = False

# =========================================================================
# 【包み(シールド)& 残存機能関連】
# =========================================================================
def update_cam_obj_visibility(self, context):
    cam = self.id_data 
    if cam and cam.type == 'CAMERA':
        cam.hide_viewport = not self.show_cam_obj

def update_cam_shield_visibility(self, context):
    cam = self.id_data
    if not cam: return
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"):
            child.hide_viewport = not self.show_cam_shield

def update_cam_shield(self, context):
    cam = self.id_data
    if not cam: return
    shield, cutter, cutter_back = None, None, None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"): shield = child
        elif child.name.startswith("CamShield_Cutter_"): cutter = child
        elif child.name.startswith("CamShield_CutterBack_"): cutter_back = child
        
    if shield:
        shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
    z_scale = self.cam_shield_radius * 1.5
    if cutter:
        front_deg = max(0.1, min(179.9, self.cam_shield_hole_angle))
        angle = math.radians(front_deg)
        r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
        cutter.scale = (r_scale, r_scale, z_scale)
    if cutter_back:
        r_back = max(0.001, self.cam_shield_hole_radius_back)
        cutter_back.scale = (r_back, r_back, z_scale)

def update_cam_shield_material(self, context):
    cam = self.id_data
    if not cam: return
    shield = next((c for c in cam.children if c.name.startswith("CamShield_Sphere_")), None)
    if not shield: return
    if shield.data.materials:
        mat = shield.data.materials[0]
        if mat:
            mat.diffuse_color = (*self.cam_shield_color, self.cam_shield_alpha)
            if mat.use_nodes:
                bsdf = mat.node_tree.nodes.get("Principled BSDF")
                if bsdf:
                    if 'Base Color' in bsdf.inputs:
                        bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color, 1.0)
                    if 'Alpha' in bsdf.inputs:
                        bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha
    shield.color = (*self.cam_shield_color, self.cam_shield_alpha)

def update_target_empty_visibility(self, context):
    for obj in bpy.data.objects:
        if obj.name in [OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK]:
            obj.hide_viewport = not self.show_target_empty

def update_viewport_color(self, context):
    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.shading.background_type = 'VIEWPORT'
                        space.shading.background_color = self.viewport_bg_color

def setup_world_nodes(context):
    world = context.scene.world
    if not world:
        world = bpy.data.worlds.new("World")
        context.scene.world = world
    world.use_nodes = True
    tree = world.node_tree
    
    out_node = next((n for n in tree.nodes if n.type == 'OUTPUT_WORLD'), None)
    if not out_node: out_node = tree.nodes.new("ShaderNodeOutputWorld")
    bg_node = next((n for n in tree.nodes if n.type == 'BACKGROUND'), None)
    if not bg_node: bg_node = tree.nodes.new("ShaderNodeBackground")
    sky_node = next((n for n in tree.nodes if n.type == 'TEX_SKY'), None)
    if not sky_node: sky_node = tree.nodes.new("ShaderNodeTexSky")
    
    return tree, out_node, bg_node, sky_node

def update_world_mode(self, context):
    tree, out_node, bg_node, sky_node = setup_world_nodes(context)
    for link in bg_node.inputs['Color'].links: tree.links.remove(link)
    if not bg_node.outputs['Background'].links:
        tree.links.new(bg_node.outputs['Background'], out_node.inputs['Surface'])
        
    if self.world_mode == 'SKY':
        context.scene.render.film_transparent = False
        sky_node.sky_type = 'NISHITA'
        tree.links.new(sky_node.outputs['Color'], bg_node.inputs['Color'])
        update_sky_texture(self, context)
        update_world_settings(self, context)
    elif self.world_mode == 'COLOR':
        context.scene.render.film_transparent = False
        update_world_settings(self, context)
    elif self.world_mode == 'TRANSPARENT':
        context.scene.render.film_transparent = True
        update_world_settings(self, context)

def update_sky_texture(self, context):
    if self.world_mode != 'SKY': return
    _, _, _, sky_node = setup_world_nodes(context)
    sky_node.sun_elevation = math.radians(self.sky_sun_elevation)
    sky_node.sun_rotation = math.radians(self.sky_sun_rotation)
    sky_node.sun_intensity = self.sky_sun_intensity

def update_world_settings(self, context):
    tree, _, bg_node, _ = setup_world_nodes(context)
    if self.world_mode != 'SKY':
        bg_node.inputs[0].default_value = (*self.world_bg_color, 1.0)
    bg_node.inputs[1].default_value = self.world_bg_strength

def cam_fov_get(self):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.context.scene.camera
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        cam.data.angle = math.radians(value)

def view_rotation_euler_get(self):
    rv3d = get_rv3d(bpy.context)
    return rv3d.view_rotation.to_euler('XYZ') if rv3d else (0.0, 0.0, 0.0)

def view_rotation_euler_set(self, value):
    rv3d = get_rv3d(bpy.context)
    if rv3d:
        if bpy.context.active_object:
            rv3d.view_location = bpy.context.active_object.matrix_world.to_translation()
        rv3d.view_rotation = mathutils.Euler((value[0], value[1], value[2]), 'XYZ').to_quaternion()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigObjectProperties(bpy.types.PropertyGroup):
    show_cam_obj: bpy.props.BoolProperty(name="専用カメラ本体を表示", default=True, update=update_cam_obj_visibility)
    show_cam_shield: bpy.props.BoolProperty(name="包み (シールド) を表示", default=True, update=update_cam_shield_visibility)
    cam_shield_radius: bpy.props.FloatProperty(name="包みの半径", default=10.0, min=0.1, update=update_cam_shield)
    cam_shield_hole_angle: bpy.props.FloatProperty(name="前方の穴 (角度)", default=179.0, min=0.0, max=179.9, update=update_cam_shield)
    cam_shield_hole_radius_back: bpy.props.FloatProperty(name="後方の穴 (半径)", default=5.0, min=0.0, update=update_cam_shield)
    cam_shield_color: bpy.props.FloatVectorProperty(name="包みの色", subtype='COLOR', size=3, default=(0.0, 1.0, 1.0), min=0.0, max=1.0, update=update_cam_shield_material)
    cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=0.3, min=0.0, max=1.0, update=update_cam_shield_material)

class LineCamRigProperties(bpy.types.PropertyGroup):
    anim_factor: bpy.props.FloatProperty(name="進行度 (0.0 〜 1.0)", default=0.0, min=0.0, max=1.0, update=update_anim_factor)
    cam_line_start: bpy.props.FloatVectorProperty(name="カメラ始点 (XYZ)", default=INIT_CAM_START, update=update_line_positions)
    cam_line_end: bpy.props.FloatVectorProperty(name="カメラ終点 (XYZ)", default=INIT_CAM_END, update=update_line_positions)
    tgt_line_start: bpy.props.FloatVectorProperty(name="注視点始点 (XYZ)", default=INIT_TGT_START, update=update_line_positions)
    tgt_line_end: bpy.props.FloatVectorProperty(name="注視点終点 (XYZ)", default=INIT_TGT_END, update=update_line_positions)
    
    mute_target_tracking: bpy.props.BoolProperty(name="アドオンから切り離し", default=False, update=update_cam_mute)
    show_target_empty: bpy.props.BoolProperty(name="軌道と注視点を表示", default=True, update=update_target_empty_visibility)

    obj_rot_axis: bpy.props.EnumProperty(name="回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rot_axis: bpy.props.EnumProperty(name="画面の回転軸", items=[('X', "X軸", ""), ('Y', "Y軸", ""), ('Z', "Z軸", "")], default='Z')
    view_rotation_euler: bpy.props.FloatVectorProperty(name="画面回転", subtype='EULER', unit='ROTATION', size=3, get=view_rotation_euler_get, set=view_rotation_euler_set)
    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
    viewport_bg_color: bpy.props.FloatVectorProperty(name="ビューポート背景色", subtype='COLOR', size=3, default=(0.05, 0.05, 0.05), min=0.0, max=1.0, update=update_viewport_color)
    world_mode: bpy.props.EnumProperty(
        name="ワールド背景モード",
        items=[('SKY', "大気 (青空)", ""), ('COLOR', "単色 (カラー)", ""), ('TRANSPARENT', "透過 (合成用)", "")],
        default='COLOR', update=update_world_mode
    )
    sky_sun_elevation: bpy.props.FloatProperty(name="太陽の高さ", default=15.0, min=-90.0, max=90.0, update=update_sky_texture)
    sky_sun_rotation: bpy.props.FloatProperty(name="太陽の向き", default=0.0, min=-360.0, max=360.0, update=update_sky_texture)
    sky_sun_intensity: bpy.props.FloatProperty(name="太陽の強さ", default=1.0, min=0.0, update=update_sky_texture)
    world_bg_color: bpy.props.FloatVectorProperty(name="ワールド背景色", subtype='COLOR', size=3, default=(0.53, 0.81, 0.92), min=0.0, max=1.0, update=update_world_settings)
    world_bg_strength: bpy.props.FloatProperty(name="全体の明るさ", default=1.0, min=0.0, update=update_world_settings)

# =========================================================================
# 【オペレーター群】
# =========================================================================
class LINECAM_OT_create_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_rig"
    bl_label = "リグ作成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        # 既存クリーンアップ
        names_to_delete = [OBJ_NAME_CAMERA, OBJ_NAME_TARGET, OBJ_NAME_CAM_TRACK, OBJ_NAME_TGT_TRACK, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME]
        for name in names_to_delete:
            obj = bpy.data.objects.get(name)
            if obj: bpy.data.objects.remove(obj, do_unlink=True)
                
        main_col = bpy.data.collections.get(COLLECTION_NAME)
        if not main_col:
            main_col = bpy.data.collections.new(COLLECTION_NAME)
            context.scene.collection.children.link(main_col)
            
        def create_line_curve(name, p1, p2):
            crv_data = bpy.data.curves.new(name, type='CURVE')
            crv_data.dimensions = '3D'
            spline = crv_data.splines.new('POLY')
            spline.points.add(1)
            spline.points[0].co = (*p1, 1.0)
            spline.points[1].co = (*p2, 1.0)
            crv_obj = bpy.data.objects.new(name, crv_data)
            main_col.objects.link(crv_obj)
            return crv_obj

        # カーブ生成
        tgt_curve = create_line_curve(OBJ_NAME_TGT_TRACK, props.tgt_line_start, props.tgt_line_end)
        cam_curve = create_line_curve(OBJ_NAME_CAM_TRACK, props.cam_line_start, props.cam_line_end)
        
        # ターゲット生成
        tgt_empty = bpy.data.objects.new(OBJ_NAME_TARGET, None)
        tgt_empty.empty_display_type = 'CROSS'
        main_col.objects.link(tgt_empty)
        
        tgt_path = tgt_empty.constraints.new(type='FOLLOW_PATH')
        tgt_path.target = tgt_curve
        tgt_path.use_curve_follow = False
        tgt_path.use_fixed_location = True
        
        # カメラ生成
        cam_data = bpy.data.cameras.new(name=f"{OBJ_NAME_CAMERA}_Data")
        cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        main_col.objects.link(cam_obj)
        
        cam_path = cam_obj.constraints.new(type='FOLLOW_PATH')
        cam_path.target = cam_curve
        cam_path.use_curve_follow = False
        cam_path.use_fixed_location = True
        
        cam_track = cam_obj.constraints.new(type='TRACK_TO')
        cam_track.target = tgt_empty
        cam_track.track_axis = 'TRACK_NEGATIVE_Z'
        cam_track.up_axis = 'UP_Y'
        
        # 包み(シールド)生成
        mesh_sphere = bpy.data.meshes.new(SHIELD_NAME)
        shield_obj = bpy.data.objects.new(SHIELD_NAME, mesh_sphere)
        main_col.objects.link(shield_obj)
        shield_obj.parent = cam_obj
        shield_obj.hide_render = True
        shield_obj.hide_select = True
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
        bm.to_mesh(mesh_sphere)
        bm.free()
        
        mesh_cone = bpy.data.meshes.new(CUTTER_NAME)
        cutter_obj = bpy.data.objects.new(CUTTER_NAME, mesh_cone)
        main_col.objects.link(cutter_obj)
        cutter_obj.parent = cam_obj
        cutter_obj.hide_viewport = True
        cutter_obj.hide_render = True
        cutter_obj.hide_select = True
        cutter_obj.display_type = 'BOUNDS'
        bm_cone = bmesh.new()
        bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.001, depth=2.0)
        for v in bm_cone.verts: v.co.z -= 0.95
        bm_cone.to_mesh(mesh_cone)
        bm_cone.free()
        
        mesh_cone_back = bpy.data.meshes.new(CUTTER_BACK_NAME)
        cutter_back_obj = bpy.data.objects.new(CUTTER_BACK_NAME, mesh_cone_back)
        main_col.objects.link(cutter_back_obj)
        cutter_back_obj.parent = cam_obj
        cutter_back_obj.hide_viewport = True
        cutter_back_obj.hide_render = True
        cutter_back_obj.hide_select = True
        cutter_back_obj.display_type = 'BOUNDS'
        bm_cone_back = bmesh.new()
        bmesh.ops.create_cone(bm_cone_back, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=1.0, depth=2.0)
        for v in bm_cone_back.verts: v.co.z += 0.95
        bm_cone_back.to_mesh(mesh_cone_back)
        bm_cone_back.free()
        
        mod_front = shield_obj.modifiers.new(name="Vision_Hole_Front", type='BOOLEAN')
        mod_front.operation = 'DIFFERENCE'
        mod_front.object = cutter_obj
        
        mod_back = shield_obj.modifiers.new(name="Vision_Hole_Back", type='BOOLEAN')
        mod_back.operation = 'DIFFERENCE'
        mod_back.object = cutter_back_obj
        
        mat = bpy.data.materials.new(f"CamShield_Mat_Line")
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        
        cprops = cam_obj.cam_rig_props
        cprops.cam_shield_color = (0.0, 1.0, 1.0)
        cprops.cam_shield_alpha = 0.3
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = tgt_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        
        props.mute_target_tracking = False
        update_line_positions(props, context)
        update_anim_factor(props, context)
        update_cam_shield(cprops, context)
        update_cam_shield_material(cprops, context)
        
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
        
        return {'FINISHED'}

class LINECAM_OT_rotate_selected(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_rotate_selected"
    bl_label = "選択オブジェクトを指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=90.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").obj_rot_axis
        rad = math.radians(self.angle)
        for obj in context.selected_objects:
            if obj.rotation_mode != 'XYZ': obj.rotation_mode = 'XYZ'
            if axis == 'X': obj.rotation_euler.x += rad
            elif axis == 'Y': obj.rotation_euler.y += rad
            elif axis == 'Z': obj.rotation_euler.z += rad
        return {'FINISHED'}

class LINECAM_OT_rotate_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX_SAFE}_rotate_view"
    bl_label = "画面を指定軸で回転"
    angle: bpy.props.FloatProperty(name="Angle", default=15.0)
    def execute(self, context):
        axis = getattr(context.scene, f"{PREFIX_SAFE}_props").view_rot_axis
        rv3d = get_rv3d(context)
        if rv3d:
            vec = (1,0,0) if axis == 'X' else ((0,1,0) if axis == 'Y' else (0,0,1))
            rv3d.view_rotation = mathutils.Quaternion(vec, math.radians(self.angle)) @ rv3d.view_rotation
        return {'FINISHED'}

class LINECAM_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "リンク"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル】
# =========================================================================
class LINECAM_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_SAFE.upper()}_PT_main_panel"
    bl_label = "カメラ & ビュー操作ツール"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Camera 20260710"
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        # --- 1. 選択オブジェクトを画面中央にする ---
        layout.operator(f"view3d.{PREFIX_SAFE}_reset_view", text="選択オブジェクトを画面中央にする", icon='ZOOM_ALL')
        layout.separator(factor=1.5)
        
        # --- 2. リグ作成 & アドオンから切り離し ---
        box_rig = layout.box()
        box_rig.label(text="専用カメラ (同期線分軌道):", icon='CAMERA_DATA')
        row_rig = box_rig.row(align=True)
        row_rig.operator(f"object.{PREFIX_SAFE}_create_rig", text="リグ作成", icon='ADD')
        row_rig.prop(props, "mute_target_tracking", text="アドオンから切り離し", toggle=True, icon='UNLINKED')
        
        # --- 3. 情報表示・更新・コピー (小数点1桁) ---
        layout.separator(factor=1.5)
        box_info = layout.box()
        row_info = box_info.row(align=True)
        row_info.label(text="ビュー情報:", icon='INFO')
        row_info.operator(f"view3d.{PREFIX_SAFE}_update_info", text="更新", icon='FILE_REFRESH')
        
        rv3d = get_rv3d(context)
        space = context.space_data if context.area and context.area.type == 'VIEW_3D' else None
        
        if rv3d and space:
            view_loc = rv3d.view_location
            view_dist = rv3d.view_distance
            aspect = context.area.height / context.area.width if context.area.width > 0 else 1.0
            sensor = 32.0 
            
            col_v = box_info.column(align=True)
            col_v.label(text=f"画面中央位置 X: {view_loc.x:.1f}")
            col_v.label(text=f"画面中央位置 Y: {view_loc.y:.1f}")
            col_v.label(text=f"画面中央位置 Z: {view_loc.z:.1f}")
            
            if rv3d.view_perspective == '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
                col_v.label(text=f"画面水平の大きさ: {w:.1f}")
                col_v.label(text=f"画面上下の大きさ: {h:.1f}")
                col_v.label(text=f"視座からの距離: {view_dist:.1f}")
            else:
                w = view_dist * (sensor / space.lens)
                h = w * aspect
                col_v.label(text=f"画面水平の大きさ: {w:.1f}")
                col_v.label(text=f"画面上下の大きさ: {h:.1f}")
                
            box_info.operator(f"view3d.{PREFIX_SAFE}_copy_view_info", text="情報をコピー (1行ずつ)", icon='COPYDOWN')
        
        layout.separator(factor=1.5)
        
        cam = context.scene.camera
        if cam and OBJ_NAME_CAMERA in cam.name:
            # --- 4. 軌道操作 ---
            box_cam = layout.box()
            box_cam.label(text="軌道の操作:", icon='PLAY')
            
            col_anim = box_cam.column(align=True)
            col_anim.enabled = not props.mute_target_tracking
            col_anim.prop(props, "anim_factor", slider=True)
            
            box_cam.separator()
            col_pos = box_cam.column(align=True)
            col_pos.enabled = not props.mute_target_tracking
            col_pos.prop(props, "cam_line_start", text="カメラ 始点(XYZ)")
            col_pos.prop(props, "cam_line_end", text="カメラ 終点(XYZ)")
            col_pos.separator()
            col_pos.prop(props, "tgt_line_start", text="注視点 始点(XYZ)")
            col_pos.prop(props, "tgt_line_end", text="注視点 終点(XYZ)")
            
            box_cam.separator()
            col_lens = box_cam.column(align=True)
            col_lens.prop(cam.data, "lens", text="ズーム (焦点距離 mm)")
            col_lens.prop(props, "cam_fov", text="水平視野角 (度)")
            col_lens.prop(cam.data, "clip_start", text="Clip Start")
            col_lens.prop(cam.data, "clip_end", text="Clip End")
            
            box_cam.separator()
            col_pp = box_cam.column(align=True)
            col_pp.prop(cam.data, "show_passepartout", text="カメラ枠外を暗くする")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="暗さ (Opacity)", slider=True)
                
            box_cam.separator()
            box_cam.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)", toggle=True, icon='STYLUS_PRESSURE')
            if cam.data.dof.use_dof:
                col_dof = box_cam.column(align=True)
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
            
            # --- 包み(シールド) ---
            if hasattr(cam, "cam_rig_props"):
                cprops = cam.cam_rig_props
                box_cam.separator()
                box_shield = box_cam.box()
                box_shield.label(text="包み (シールド) 設定:", icon='RESTRICT_VIEW_OFF')
                
                row_disp = box_shield.row()
                row_disp.prop(cprops, "show_cam_obj", text="カメラ本体")
                row_disp.prop(cprops, "show_cam_shield", text="包み")
                box_shield.prop(props, "show_target_empty", text="軌道 & 注視点 を表示", toggle=True, icon='EMPTY_DATA')
                
                col_sh = box_shield.column(align=True)
                col_sh.enabled = cprops.show_cam_shield
                col_sh.prop(cprops, "cam_shield_radius", text="半径")
                col_sh.prop(cprops, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
                col_sh.prop(cprops, "cam_shield_hole_radius_back", text="後方の穴半径")
                col_sh.prop(cprops, "cam_shield_color", text="色")
                col_sh.prop(cprops, "cam_shield_alpha", text="透明度", slider=True)
            
            layout.separator(factor=1.5)
            
        # --- 5. オブジェクト&画面 回転操作 ---
        box_rot = layout.box()
        box_rot.label(text="選択オブジェクトの回転:", icon='ORIENTATION_GIMBAL')
        box_rot.prop(props, "obj_rot_axis", expand=True)
        if context.active_object:
            axis_idx = {'X':0, 'Y':1, 'Z':2}[props.obj_rot_axis]
            box_rot.prop(context.active_object, "rotation_euler", index=axis_idx, text=f"{props.obj_rot_axis} 回転角度")
            row = box_rot.row(align=True)
            for ang in [-90, -15, 15, 90]: row.operator(f"object.{PREFIX_SAFE}_rotate_selected", text=f"{ang:+}°").angle = ang
        else:
            box_rot.label(text="※オブジェクトを選択してください", icon='INFO')
            
        box_vrot = layout.box()
        box_vrot.label(text="画面(ビュー)自体の回転:", icon='VIEW3D')
        box_vrot.prop(props, "view_rot_axis", expand=True)
        v_idx = {'X':0, 'Y':1, 'Z':2}[props.view_rot_axis]
        box_vrot.prop(props, "view_rotation_euler", index=v_idx, text=f"画面 {props.view_rot_axis} 回転")
        row_v = box_vrot.row(align=True)
        for ang in [-90, -15, 15, 90]: row_v.operator(f"view3d.{PREFIX_SAFE}_rotate_view", text=f"{ang:+}°").angle = ang
        
        layout.separator(factor=1.5)
        
        # --- 6. ワールド・背景 ---
        box_render = layout.box()
        box_render.label(text="レンダリング & ワールド:", icon='SHADING_RENDERED')
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        box_render.prop(props, "world_mode", expand=True)
        
        col_world = box_render.column(align=True)
        if props.world_mode == 'SKY':
            col_world.prop(props, "sky_sun_elevation", text="太陽の高さ")
            col_world.prop(props, "sky_sun_rotation", text="太陽の向き")
            col_world.prop(props, "sky_sun_intensity", text="太陽の強さ")
            col_world.prop(props, "world_bg_strength", text="全体の明るさ")
        elif props.world_mode == 'COLOR':
            col_world.prop(props, "world_bg_color", text="背景の色")
            col_world.prop(props, "world_bg_strength", text="明るさ")
        elif props.world_mode == 'TRANSPARENT':
            col_world.prop(context.scene.render, "film_transparent", text="透過レンダリング")
        
        layout.separator(factor=1.5)
        
        # --- 7. リンク & 削除パネル ---
        box_sys = layout.box()
        box_sys.label(text="システム:", icon='PREFERENCES')
        box_sys.operator(f"wm.{PREFIX_SAFE}_open_url", text="リンクパネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオン削除パネル", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigObjectProperties, LineCamRigProperties,
    LINECAM_OT_reset_view, LINECAM_OT_update_info, LINECAM_OT_copy_view_info,
    LINECAM_OT_create_rig, LINECAM_OT_rotate_selected, LINECAM_OT_rotate_view,
    LINECAM_OT_open_url, LINECAM_OT_remove_addon, LINECAM_PT_main_panel
]

def register():
    for c in classes: bpy.utils.register_class(c)
    setattr(bpy.types.Object, "cam_rig_props", bpy.props.PointerProperty(type=CamRigObjectProperties))
    setattr(bpy.types.Scene, f"{PREFIX_SAFE}_props", bpy.props.PointerProperty(type=LineCamRigProperties))

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

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
画面中央位置 X: 438.590
画面中央位置 Y: -134.445
画面中央位置 Z: -87.028
画面水平の大きさ: 332.554
画面上下の大きさ: 184.658
視座からの距離: 519.615

画面中央位置 X: 438.590
画面中央位置 Y: -134.445
画面中央位置 Z: -87.028
画面水平の大きさ: 689.584
画面上下の大きさ: 382.907
視座からの距離: 1077.474