カメラ固定追加 20260709

bl_info = {
    "name": "Camera & View Rig Tools",
    "author": "Your Name",
    "version": (11, 6),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"

PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

# =========================================================================
# 【カメラ軌道制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
    for const in cam.constraints:
        if const.type == 'FOLLOW_PATH' and const.target:
            if const.target.name == track_name:
                return const.target
    return None

def update_cam_circle(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Circle")
    if curve_obj:
        curve_obj.location = self.cam_circle_center
        curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
        curve_obj.rotation_euler = self.cam_circle_rotation

def update_cam_line(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Line")
    if curve_obj:
        curve_obj.location = (0, 0, 0)
        curve_obj.scale = (1, 1, 1)
        curve_obj.rotation_euler = (0, 0, 0)
        if len(curve_obj.data.splines) > 0:
            spline = curve_obj.data.splines[0]
            if spline.type == 'POLY' and len(spline.points) >= 2:
                spline.points[0].co = (*self.cam_line_start, 1.0)
                spline.points[1].co = (*self.cam_line_end, 1.0)

def update_cam_sphere(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_SPHERE" not in cam.name:
        return
        
    track_obj = bpy.data.objects.get("CamTrack_Sphere")
    if track_obj:
        track_obj.location = self.cam_sphere_center
        track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
        track_obj.rotation_euler = self.cam_sphere_rotation
    
    if getattr(self, "mute_target_tracking", False):
        return
        
    r = self.cam_sphere_radius
    lon = math.radians(self.cam_sphere_lon)
    lat = math.radians(self.cam_sphere_lat)
    
    lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
    vec = mathutils.Vector((lx, ly, lz))
    vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
    
    cx, cy, cz = self.cam_sphere_center
    cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)

def update_cam_fixed(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_FIXED" not in cam.name:
        return
        
    cam.location = self.cam_fixed_location
    cam.rotation_mode = 'XYZ'
    cam.rotation_euler = (
        math.radians(self.cam_fixed_pitch),
        math.radians(self.cam_fixed_roll),
        math.radians(self.cam_fixed_yaw)
    )

def update_cam_target(self, context):
    cam = context.scene.camera
    if not cam: return
    target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
    if not target_obj and "FIXED" in cam.name:
        target_obj = cam.data.dof.focus_object
    if not target_obj: return
    
    for c in target_obj.constraints:
        if c.type == 'COPY_LOCATION':
            target_obj.constraints.remove(c)
            
    mode = self.cam_target_mode
    if mode == 'OBJECT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
    elif mode == 'POINT':
        target_obj.location = self.cam_target_loc
    elif mode == 'MIDPOINT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
            c1.influence = 1.0
        if self.cam_target_obj2:
            c2 = target_obj.constraints.new(type='COPY_LOCATION')
            c2.target = self.cam_target_obj2
            c2.influence = 0.5

def update_cam_mute(self, context):
    cam = context.scene.camera
    if not cam: return
    track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        mat = eval_cam.matrix_world.copy()
        loc = mat.to_translation()
        rot = mat.to_euler(cam.rotation_mode)
        
        if track_const: track_const.mute = True
        if path_const: path_const.mute = True
        
        cam.location = loc
        cam.rotation_euler = rot
    else:
        if track_const: track_const.mute = False
        if path_const: path_const.mute = False
        if "SPHERE" in cam.name:
            update_cam_sphere(self, context)

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 = None
    cutter = None
    cutter_back = 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 = self.cam_shield_hole_angle
        if front_deg < 0.1: front_deg = 0.1
        if front_deg > 179.9: front_deg = 179.9
        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 = self.cam_shield_hole_angle_back - 180.0
        if back_deg < 0.1: back_deg = 0.1
        if back_deg > 179.9: back_deg = 179.9
        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 = None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere_"):
            shield = child
            break
            
    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_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 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()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
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 CamRigProperties(bpy.types.PropertyGroup):
    """シーン全体の操作プロパティ"""
    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_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
    cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
    cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
    
    cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
    cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
    cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
    cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
    cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
    
    cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
    cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
    cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
    cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)

    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
        default='OBJECT', 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)
    
    # 背景・ワールド
    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 CAMRIG_OT_create_camera_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
    bl_label = "カメラ軌道セットアップ"
    bl_options = {'REGISTER', 'UNDO'}
    rig_type: bpy.props.StringProperty(default='CIRCLE')
    
    def execute(self, context):
        target = context.active_object
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        target_name = f"CameraTarget_{self.rig_type}"
        track_name = f"CamTrack_{self.rig_type.capitalize()}"
        cam_name = f"TrackingCamera_{self.rig_type}"
        cam_data_name = f"CamData_{self.rig_type}"
        shield_name = f"CamShield_Sphere_{self.rig_type}"
        cutter_name = f"CamShield_Cutter_{self.rig_type}"
        cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
        
        names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name]
        if target and target.name in names_to_delete: target = None
            
        loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
        
        for name in names_to_delete:
            obj = bpy.data.objects.get(name)
            if obj:
                try:
                    data = obj.data
                    bpy.data.objects.remove(obj, do_unlink=True)
                    if data and getattr(data, "users", 1) == 0:
                        if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
                        elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
                        elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
                except ReferenceError: pass
        
        cdata = bpy.data.cameras.get(cam_data_name)
        if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
        
        rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
        if not rig_col:
            rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
            context.scene.collection.children.link(rig_col)
            
        target_empty = bpy.data.objects.new(target_name, None)
        target_empty.empty_display_type = 'PLAIN_AXES'
        target_empty.location = loc
        rig_col.objects.link(target_empty)
        
        curve_obj = None
        if self.rig_type == 'CIRCLE':
            bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
            curve_obj = context.active_object
            curve_obj.name = track_name
            for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'LINE':
            curve_data = bpy.data.curves.new(track_name, type='CURVE')
            curve_data.dimensions = '3D'
            spline = curve_data.splines.new('POLY')
            spline.points.add(1)
            curve_obj = bpy.data.objects.new(track_name, curve_data)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'SPHERE':
            curve_obj = bpy.data.objects.new(track_name, None)
            curve_obj.empty_display_type = 'SPHERE'
            rig_col.objects.link(curve_obj)
            
        cam_data = bpy.data.cameras.new(name=cam_data_name)
        cam_obj = bpy.data.objects.new(cam_name, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        rig_col.objects.link(cam_obj)
        
        # -------------------------------------------------------------
        # ★ カッター同士をめり込ませて空洞を貫通させる
        # -------------------------------------------------------------
        mesh_sphere = bpy.data.meshes.new(shield_name)
        shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
        rig_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)
        rig_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)
        rig_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_name = f"CamShield_Material_{self.rig_type}"
        mat = bpy.data.materials.get(mat_name)
        if not mat:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            mat.blend_method = 'BLEND'
            
        cprops = cam_obj.cam_rig_props
        
        mat.diffuse_color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf:
            if 'Base Color' in bsdf.inputs:
                bsdf.inputs['Base Color'].default_value = (*cprops.cam_shield_color, 1.0)
            if 'Alpha' in bsdf.inputs:
                bsdf.inputs['Alpha'].default_value = cprops.cam_shield_alpha
                
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        shield_obj.color = (*cprops.cam_shield_color, cprops.cam_shield_alpha)
        # -------------------------------------------------------------

        if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
            const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
            const_path.target = curve_obj
            const_path.use_curve_follow = False
            const_path.use_fixed_location = True
            
        if self.rig_type != 'FIXED':
            const_track = cam_obj.constraints.new(type='TRACK_TO')
            const_track.target = target_empty
            const_track.track_axis = 'TRACK_NEGATIVE_Z'
            const_track.up_axis = 'UP_Y'
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = target_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        props.cam_target_mode = 'OBJECT'
        try:
            if target and target.type in ['CAMERA', 'LIGHT']: target = None
        except ReferenceError: target = None
        props.cam_target_obj1 = target
        
        if props.mute_target_tracking: props.mute_target_tracking = False 
        update_cam_target(props, context)
        
        if self.rig_type == 'CIRCLE':
            props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_circle_radius = 15.0
            props.cam_circle_rotation = (0, 0, 0)
            update_cam_circle(props, context)
        elif self.rig_type == 'LINE':
            props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
            props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
            update_cam_line(props, context)
        elif self.rig_type == 'SPHERE':
            props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_sphere_radius = 15.0
            props.cam_sphere_rotation = (0, 0, 0)
            props.cam_sphere_lon = 0.0
            props.cam_sphere_lat = 0.0
            update_cam_sphere(props, context)
        elif self.rig_type == 'FIXED':
            props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
            props.cam_fixed_pitch = 90.0
            props.cam_fixed_yaw = 0.0
            props.cam_fixed_roll = 0.0
            update_cam_fixed(props, context)
            
        # オブジェクト個別のプロパティを初期化
        cprops.cam_shield_radius = 1.0
        cprops.cam_shield_hole_angle = 179.0
        cprops.cam_shield_hole_angle_back = 200.0
        cprops.show_cam_obj = True
        cprops.show_cam_shield = True
            
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
                
        self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
        return {'FINISHED'}

class CAMRIG_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 CAMRIG_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 CAMRIG_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 CAMRIG_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class CAMRIG_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 CAMRIG_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")
        
        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:
            try:
                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
            except ReferenceError:
                box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
        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_cam = layout.box()
        box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
        col_cam_btn = box_cam.column(align=True)
        row1 = col_cam_btn.row(align=True)
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
        row2 = col_cam_btn.row(align=True)
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
        
        cam = context.scene.camera
        if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
            box_cam.separator()
            box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
            
            if "SPHERE" in cam.name:
                box_sp = box_cam.box()
                box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
                col_sp = box_sp.column(align=True)
                col_sp.enabled = not props.mute_target_tracking
                col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
                col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
                col_sp.separator()
                col_sp.prop(props, "cam_sphere_center", text="球の中心")
                col_sp.prop(props, "cam_sphere_radius", text="球の半径")
                col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
            elif "FIXED" in cam.name:
                box_fix = box_cam.box()
                box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
                col_fix = box_fix.column(align=True)
                col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
                col_fix.separator()
                col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
                col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
                col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
            else:
                curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
                if curve_obj:
                    col_offset = box_cam.column(align=True)
                    col_offset.enabled = not props.mute_target_tracking
                    col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
                    
                    box_curve = box_cam.box()
                    box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
                    col_crv = box_curve.column(align=True)
                    col_crv.enabled = not props.mute_target_tracking
                    if "Circle" in curve_obj.name:
                        col_crv.prop(props, "cam_circle_center", text="円の中心")
                        col_crv.prop(props, "cam_circle_radius", text="円の半径")
                        col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
                    elif "Line" in curve_obj.name:
                        col_crv.prop(props, "cam_line_start", text="始点")
                        col_crv.prop(props, "cam_line_end", text="終点")
                        
            target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
            if not target_obj and "FIXED" in cam.name:
                target_obj = cam.data.dof.focus_object
                
            if target_obj:
                box_target = box_cam.box()
                if "FIXED" in cam.name:
                    box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
                else:
                    box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
                col_tgt_main = box_target.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 == '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="カメラ枠外を暗くする (Passepartout)")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
            
            box_cam.separator()

            if "FIXED" not in cam.name:
                box_sight = box_cam.box()
                box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
                box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
                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_cam.separator()
                
            # ★ カメラ本体 & 包みの表示・非表示・前後穴あけ設定 (個別化)
            if hasattr(cam, "cam_rig_props"):
                cprops = cam.cam_rig_props
                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="包みを表示")
                
                col_sh = box_shield.column(align=True)
                col_sh.enabled = cprops.show_cam_shield
                col_sh.separator()
                col_sh.prop(cprops, "cam_shield_radius", text="包みのサイズ (半径)")
                col_sh.separator()
                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)
                
            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, "focus_object", text="ピント対象")
                if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
        else:
            box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
            
        layout.separator(factor=1.5)
        
        box_render = layout.box()
        box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
        row_eng = box_render.row(align=True)
        row_eng.prop(context.scene.render, "engine", expand=True)
        box_render.separator()
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        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="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigObjectProperties, CamRigProperties, CAMRIG_OT_create_camera_rig, 
    CAMRIG_OT_reset_view, CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view, 
    CAMRIG_OT_open_url, CAMRIG_OT_remove_addon, CAMRIG_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=CamRigProperties))

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()
カメラ4種類の包みの 色と大きさと穴の大きさ 独立させて
非表示も 個別

Cameraオブジェクトの非表示も独立

```jsx

```bl_info = {
    "name": "Camera & View Rig Tools",
    "author": "Your Name",
    "version": (11, 5),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"

PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

# =========================================================================
# 【カメラ軌道・シールド制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
    for const in cam.constraints:
        if const.type == 'FOLLOW_PATH' and const.target:
            if const.target.name == track_name:
                return const.target
    return None

def update_cam_circle(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Circle")
    if curve_obj:
        curve_obj.location = self.cam_circle_center
        curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
        curve_obj.rotation_euler = self.cam_circle_rotation

def update_cam_line(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Line")
    if curve_obj:
        curve_obj.location = (0, 0, 0)
        curve_obj.scale = (1, 1, 1)
        curve_obj.rotation_euler = (0, 0, 0)
        if len(curve_obj.data.splines) > 0:
            spline = curve_obj.data.splines[0]
            if spline.type == 'POLY' and len(spline.points) >= 2:
                spline.points[0].co = (*self.cam_line_start, 1.0)
                spline.points[1].co = (*self.cam_line_end, 1.0)

def update_cam_sphere(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_SPHERE" not in cam.name:
        return
        
    track_obj = bpy.data.objects.get("CamTrack_Sphere")
    if track_obj:
        track_obj.location = self.cam_sphere_center
        track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
        track_obj.rotation_euler = self.cam_sphere_rotation
    
    if getattr(self, "mute_target_tracking", False):
        return
        
    r = self.cam_sphere_radius
    lon = math.radians(self.cam_sphere_lon)
    lat = math.radians(self.cam_sphere_lat)
    
    lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
    vec = mathutils.Vector((lx, ly, lz))
    vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
    
    cx, cy, cz = self.cam_sphere_center
    cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)

def update_cam_fixed(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_FIXED" not in cam.name:
        return
        
    cam.location = self.cam_fixed_location
    cam.rotation_mode = 'XYZ'
    cam.rotation_euler = (
        math.radians(self.cam_fixed_pitch),
        math.radians(self.cam_fixed_roll),
        math.radians(self.cam_fixed_yaw)
    )

def update_cam_target(self, context):
    cam = context.scene.camera
    if not cam: return
    target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
    if not target_obj and "FIXED" in cam.name:
        target_obj = cam.data.dof.focus_object
    if not target_obj: return
    
    for c in target_obj.constraints:
        if c.type == 'COPY_LOCATION':
            target_obj.constraints.remove(c)
            
    mode = self.cam_target_mode
    if mode == 'OBJECT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
    elif mode == 'POINT':
        target_obj.location = self.cam_target_loc
    elif mode == 'MIDPOINT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
            c1.influence = 1.0
        if self.cam_target_obj2:
            c2 = target_obj.constraints.new(type='COPY_LOCATION')
            c2.target = self.cam_target_obj2
            c2.influence = 0.5

def update_cam_mute(self, context):
    cam = context.scene.camera
    if not cam: return
    track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        mat = eval_cam.matrix_world.copy()
        loc = mat.to_translation()
        rot = mat.to_euler(cam.rotation_mode)
        
        if track_const: track_const.mute = True
        if path_const: path_const.mute = True
        
        cam.location = loc
        cam.rotation_euler = rot
    else:
        if track_const: track_const.mute = False
        if path_const: path_const.mute = False
        if "SPHERE" in cam.name:
            update_cam_sphere(self, context)

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):
    for obj in bpy.data.objects:
        if obj.name.startswith("TrackingCamera_"):
            obj.hide_viewport = not self.show_cam_obj

def update_cam_shield_visibility(self, context):
    for obj in bpy.data.objects:
        if obj.name.startswith("CamShield_Sphere_"):
            obj.hide_viewport = not self.show_cam_shield

def update_cam_shield(self, context):
    cam = context.scene.camera
    if not cam: return
    
    shield = None
    cutter = None
    cutter_back = 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 = self.cam_shield_hole_angle
        if front_deg < 0.1: front_deg = 0.1
        if front_deg > 179.9: front_deg = 179.9
        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 = self.cam_shield_hole_angle_back - 180.0
        if back_deg < 0.1: back_deg = 0.1
        if back_deg > 179.9: back_deg = 179.9
        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):
    mat_name = "CamShield_Material"
    mat = bpy.data.materials.get(mat_name)
    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
                    
    for obj in bpy.data.objects:
        if obj.name.startswith("CamShield_Sphere_"):
            obj.color = (*self.cam_shield_color, self.cam_shield_alpha)

# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
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 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()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigProperties(bpy.types.PropertyGroup):
    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_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
    cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
    cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
    
    cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
    cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
    cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
    cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
    cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
    
    cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
    cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
    cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
    cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)

    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
        default='OBJECT', 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_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)

    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 CAMRIG_OT_create_camera_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
    bl_label = "カメラ軌道セットアップ"
    bl_options = {'REGISTER', 'UNDO'}
    rig_type: bpy.props.StringProperty(default='CIRCLE')
    
    def execute(self, context):
        target = context.active_object
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        target_name = f"CameraTarget_{self.rig_type}"
        track_name = f"CamTrack_{self.rig_type.capitalize()}"
        cam_name = f"TrackingCamera_{self.rig_type}"
        cam_data_name = f"CamData_{self.rig_type}"
        shield_name = f"CamShield_Sphere_{self.rig_type}"
        cutter_name = f"CamShield_Cutter_{self.rig_type}"
        cutter_back_name = f"CamShield_CutterBack_{self.rig_type}"
        
        names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name, cutter_back_name]
        if target and target.name in names_to_delete: target = None
            
        loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
        
        for name in names_to_delete:
            obj = bpy.data.objects.get(name)
            if obj:
                try:
                    data = obj.data
                    bpy.data.objects.remove(obj, do_unlink=True)
                    if data and getattr(data, "users", 1) == 0:
                        if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
                        elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
                        elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
                except ReferenceError: pass
        
        cdata = bpy.data.cameras.get(cam_data_name)
        if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
        
        rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
        if not rig_col:
            rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
            context.scene.collection.children.link(rig_col)
            
        target_empty = bpy.data.objects.new(target_name, None)
        target_empty.empty_display_type = 'PLAIN_AXES'
        target_empty.location = loc
        rig_col.objects.link(target_empty)
        
        curve_obj = None
        if self.rig_type == 'CIRCLE':
            bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
            curve_obj = context.active_object
            curve_obj.name = track_name
            for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'LINE':
            curve_data = bpy.data.curves.new(track_name, type='CURVE')
            curve_data.dimensions = '3D'
            spline = curve_data.splines.new('POLY')
            spline.points.add(1)
            curve_obj = bpy.data.objects.new(track_name, curve_data)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'SPHERE':
            curve_obj = bpy.data.objects.new(track_name, None)
            curve_obj.empty_display_type = 'SPHERE'
            rig_col.objects.link(curve_obj)
            
        cam_data = bpy.data.cameras.new(name=cam_data_name)
        cam_obj = bpy.data.objects.new(cam_name, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        rig_col.objects.link(cam_obj)
        
        # -------------------------------------------------------------
        # ★ カッター同士をめり込ませて空洞を貫通させる
        # -------------------------------------------------------------
        mesh_sphere = bpy.data.meshes.new(shield_name)
        shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
        rig_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()
        
        # 1. 前方カッター (微細な太さを持たせ、Z側に0.05食い込ませる)
        mesh_cone = bpy.data.meshes.new(cutter_name)
        cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
        rig_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()
        
        # 2. 後方カッター (微細な太さを持たせ、Z側に0.05食い込ませる)
        mesh_cone_back = bpy.data.meshes.new(cutter_back_name)
        cutter_back_obj = bpy.data.objects.new(cutter_back_name, mesh_cone_back)
        rig_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_name = "CamShield_Material"
        mat = bpy.data.materials.get(mat_name)
        if not mat:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            mat.blend_method = 'BLEND'
            
        mat.diffuse_color = (*props.cam_shield_color, props.cam_shield_alpha)
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf:
            if 'Base Color' in bsdf.inputs:
                bsdf.inputs['Base Color'].default_value = (*props.cam_shield_color, 1.0)
            if 'Alpha' in bsdf.inputs:
                bsdf.inputs['Alpha'].default_value = props.cam_shield_alpha
                
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        shield_obj.color = (*props.cam_shield_color, props.cam_shield_alpha)
        # -------------------------------------------------------------

        if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
            const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
            const_path.target = curve_obj
            const_path.use_curve_follow = False
            const_path.use_fixed_location = True
            
        if self.rig_type != 'FIXED':
            const_track = cam_obj.constraints.new(type='TRACK_TO')
            const_track.target = target_empty
            const_track.track_axis = 'TRACK_NEGATIVE_Z'
            const_track.up_axis = 'UP_Y'
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = target_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        props.cam_target_mode = 'OBJECT'
        try:
            if target and target.type in ['CAMERA', 'LIGHT']: target = None
        except ReferenceError: target = None
        props.cam_target_obj1 = target
        
        if props.mute_target_tracking: props.mute_target_tracking = False 
        update_cam_target(props, context)
        
        if self.rig_type == 'CIRCLE':
            props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_circle_radius = 15.0
            props.cam_circle_rotation = (0, 0, 0)
            update_cam_circle(props, context)
        elif self.rig_type == 'LINE':
            props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
            props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
            update_cam_line(props, context)
        elif self.rig_type == 'SPHERE':
            props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_sphere_radius = 15.0
            props.cam_sphere_rotation = (0, 0, 0)
            props.cam_sphere_lon = 0.0
            props.cam_sphere_lat = 0.0
            update_cam_sphere(props, context)
        elif self.rig_type == 'FIXED':
            props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
            props.cam_fixed_pitch = 90.0
            props.cam_fixed_yaw = 0.0
            props.cam_fixed_roll = 0.0
            update_cam_fixed(props, context)
            
        props.cam_shield_radius = 1.0
        props.cam_shield_hole_angle = 179.0
        props.cam_shield_hole_angle_back = 200.0
        
        props.show_cam_obj = True
        props.show_cam_shield = True
        update_cam_shield(props, context)
        update_cam_obj_visibility(props, context)
        update_cam_shield_visibility(props, context)
            
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
                
        self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
        return {'FINISHED'}

class CAMRIG_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 CAMRIG_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 CAMRIG_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 CAMRIG_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class CAMRIG_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 CAMRIG_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")
        
        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:
            try:
                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
            except ReferenceError:
                box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
        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_cam = layout.box()
        box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
        col_cam_btn = box_cam.column(align=True)
        row1 = col_cam_btn.row(align=True)
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
        row2 = col_cam_btn.row(align=True)
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
        
        cam = context.scene.camera
        if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
            box_cam.separator()
            box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
            
            if "SPHERE" in cam.name:
                box_sp = box_cam.box()
                box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
                col_sp = box_sp.column(align=True)
                col_sp.enabled = not props.mute_target_tracking
                col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
                col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
                col_sp.separator()
                col_sp.prop(props, "cam_sphere_center", text="球の中心")
                col_sp.prop(props, "cam_sphere_radius", text="球の半径")
                col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
            elif "FIXED" in cam.name:
                box_fix = box_cam.box()
                box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
                col_fix = box_fix.column(align=True)
                col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
                col_fix.separator()
                col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
                col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
                col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
            else:
                curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
                if curve_obj:
                    col_offset = box_cam.column(align=True)
                    col_offset.enabled = not props.mute_target_tracking
                    col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
                    
                    box_curve = box_cam.box()
                    box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
                    col_crv = box_curve.column(align=True)
                    col_crv.enabled = not props.mute_target_tracking
                    if "Circle" in curve_obj.name:
                        col_crv.prop(props, "cam_circle_center", text="円の中心")
                        col_crv.prop(props, "cam_circle_radius", text="円の半径")
                        col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
                    elif "Line" in curve_obj.name:
                        col_crv.prop(props, "cam_line_start", text="始点")
                        col_crv.prop(props, "cam_line_end", text="終点")
                        
            target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
            if not target_obj and "FIXED" in cam.name:
                target_obj = cam.data.dof.focus_object
                
            if target_obj:
                box_target = box_cam.box()
                if "FIXED" in cam.name:
                    box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
                else:
                    box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
                col_tgt_main = box_target.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 == '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="カメラ枠外を暗くする (Passepartout)")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
            
            box_cam.separator()

            if "FIXED" not in cam.name:
                box_sight = box_cam.box()
                box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
                box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
                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_cam.separator()
                
            # ★ カメラ本体 & 包みの表示・非表示・前後穴あけ設定
            box_shield = box_cam.box()
            box_shield.label(text="ビューポート上の表示設定:", icon='RESTRICT_VIEW_OFF')
            
            row_disp = box_shield.row()
            row_disp.prop(props, "show_cam_obj", text="カメラ本体を表示")
            row_disp.prop(props, "show_cam_shield", text="包みを表示")
            
            col_sh = box_shield.column(align=True)
            col_sh.enabled = props.show_cam_shield
            col_sh.separator()
            col_sh.prop(props, "cam_shield_radius", text="包みのサイズ (半径)")
            col_sh.separator()
            col_sh.prop(props, "cam_shield_hole_angle", text="前方の穴 (0〜179度)")
            col_sh.prop(props, "cam_shield_hole_angle_back", text="後方の穴 (181〜360度)")
            col_sh.separator()
            col_sh.prop(props, "cam_shield_color", text="包みの色")
            col_sh.prop(props, "cam_shield_alpha", text="透明度 (アルファ)", 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, "focus_object", text="ピント対象")
                if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
        else:
            box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
            
        layout.separator(factor=1.5)
        
        box_render = layout.box()
        box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
        row_eng = box_render.row(align=True)
        row_eng.prop(context.scene.render, "engine", expand=True)
        box_render.separator()
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        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="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigProperties, CAMRIG_OT_create_camera_rig, CAMRIG_OT_reset_view,
    CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view, CAMRIG_OT_open_url, 
    CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel, 
]

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

def unregister():
    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()
カメラ包みんの球体の さらに追加で 初期設定200度から360度
つまりカメラ視線方向の反対側にも 穴を開ける調整作って

最大 181度から360度 穴を開けて

```jsx
bl_info = {
    "name": "Camera & View Rig Tools",
    "author": "Your Name",
    "version": (11, 3),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"

PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

# =========================================================================
# 【カメラ軌道・シールド制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
    for const in cam.constraints:
        if const.type == 'FOLLOW_PATH' and const.target:
            if const.target.name == track_name:
                return const.target
    return None

def update_cam_circle(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Circle")
    if curve_obj:
        curve_obj.location = self.cam_circle_center
        curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
        curve_obj.rotation_euler = self.cam_circle_rotation

def update_cam_line(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Line")
    if curve_obj:
        curve_obj.location = (0, 0, 0)
        curve_obj.scale = (1, 1, 1)
        curve_obj.rotation_euler = (0, 0, 0)
        if len(curve_obj.data.splines) > 0:
            spline = curve_obj.data.splines[0]
            if spline.type == 'POLY' and len(spline.points) >= 2:
                spline.points[0].co = (*self.cam_line_start, 1.0)
                spline.points[1].co = (*self.cam_line_end, 1.0)

def update_cam_sphere(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_SPHERE" not in cam.name:
        return
        
    track_obj = bpy.data.objects.get("CamTrack_Sphere")
    if track_obj:
        track_obj.location = self.cam_sphere_center
        track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
        track_obj.rotation_euler = self.cam_sphere_rotation
    
    if getattr(self, "mute_target_tracking", False):
        return
        
    r = self.cam_sphere_radius
    lon = math.radians(self.cam_sphere_lon)
    lat = math.radians(self.cam_sphere_lat)
    
    lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
    vec = mathutils.Vector((lx, ly, lz))
    vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
    
    cx, cy, cz = self.cam_sphere_center
    cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)

def update_cam_fixed(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_FIXED" not in cam.name:
        return
        
    cam.location = self.cam_fixed_location
    cam.rotation_mode = 'XYZ'
    cam.rotation_euler = (
        math.radians(self.cam_fixed_pitch),
        math.radians(self.cam_fixed_roll),
        math.radians(self.cam_fixed_yaw)
    )

def update_cam_target(self, context):
    cam = context.scene.camera
    if not cam: return
    target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
    if not target_obj and "FIXED" in cam.name:
        target_obj = cam.data.dof.focus_object
    if not target_obj: return
    
    for c in target_obj.constraints:
        if c.type == 'COPY_LOCATION':
            target_obj.constraints.remove(c)
            
    mode = self.cam_target_mode
    if mode == 'OBJECT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
    elif mode == 'POINT':
        target_obj.location = self.cam_target_loc
    elif mode == 'MIDPOINT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
            c1.influence = 1.0
        if self.cam_target_obj2:
            c2 = target_obj.constraints.new(type='COPY_LOCATION')
            c2.target = self.cam_target_obj2
            c2.influence = 0.5

def update_cam_mute(self, context):
    cam = context.scene.camera
    if not cam: return
    track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        mat = eval_cam.matrix_world.copy()
        loc = mat.to_translation()
        rot = mat.to_euler(cam.rotation_mode)
        
        if track_const: track_const.mute = True
        if path_const: path_const.mute = True
        
        cam.location = loc
        cam.rotation_euler = rot
    else:
        if track_const: track_const.mute = False
        if path_const: path_const.mute = False
        if "SPHERE" in cam.name:
            update_cam_sphere(self, context)

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):
    # シーン内のすべての専用カメラ本体の表示/非表示を切り替え
    for obj in bpy.data.objects:
        if obj.name.startswith("TrackingCamera_"):
            obj.hide_viewport = not self.show_cam_obj

def update_cam_shield_visibility(self, context):
    # シーン内のすべての「包み」の表示/非表示を切り替え
    for obj in bpy.data.objects:
        if obj.name.startswith("CamShield_Sphere_"):
            obj.hide_viewport = not self.show_cam_shield

def update_cam_shield(self, context):
    cam = context.scene.camera
    if not cam: return
    
    shield = None
    cutter = None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere"): shield = child
        elif child.name.startswith("CamShield_Cutter"): cutter = child
        
    if shield:
        shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
        
    if cutter:
        # Z方向の深さを十分に取りつつ、角度に応じて半径を算出
        z_scale = self.cam_shield_radius * 1.5
        angle = math.radians(self.cam_shield_hole_angle)
        # 円錐の深さは元の2.0 * z_scale。半角のtanを掛けて半径を計算
        r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
        cutter.scale = (r_scale, r_scale, z_scale)

def update_cam_shield_material(self, context):
    mat_name = "CamShield_Material"
    mat = bpy.data.materials.get(mat_name)
    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
                    
    # ビューポートカラー (Solid表示時) を一括更新
    for obj in bpy.data.objects:
        if obj.name.startswith("CamShield_Sphere_"):
            obj.color = (*self.cam_shield_color, self.cam_shield_alpha)

# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
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 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()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigProperties(bpy.types.PropertyGroup):
    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_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
    cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
    cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
    
    cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
    cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
    cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
    cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
    cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
    
    cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
    cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
    cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
    cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)

    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
        default='OBJECT', 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_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_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)

    # 背景・ワールド
    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 CAMRIG_OT_create_camera_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
    bl_label = "カメラ軌道セットアップ"
    bl_options = {'REGISTER', 'UNDO'}
    rig_type: bpy.props.StringProperty(default='CIRCLE')
    
    def execute(self, context):
        target = context.active_object
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        target_name = f"CameraTarget_{self.rig_type}"
        track_name = f"CamTrack_{self.rig_type.capitalize()}"
        cam_name = f"TrackingCamera_{self.rig_type}"
        cam_data_name = f"CamData_{self.rig_type}"
        shield_name = f"CamShield_Sphere_{self.rig_type}"
        cutter_name = f"CamShield_Cutter_{self.rig_type}"
        
        names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name]
        if target and target.name in names_to_delete: target = None
            
        loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
        
        # 既存リグのクリーンアップ
        for name in names_to_delete:
            obj = bpy.data.objects.get(name)
            if obj:
                try:
                    data = obj.data
                    bpy.data.objects.remove(obj, do_unlink=True)
                    if data and getattr(data, "users", 1) == 0:
                        if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
                        elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
                        elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
                except ReferenceError: pass
        
        cdata = bpy.data.cameras.get(cam_data_name)
        if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
        
        rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
        if not rig_col:
            rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
            context.scene.collection.children.link(rig_col)
            
        target_empty = bpy.data.objects.new(target_name, None)
        target_empty.empty_display_type = 'PLAIN_AXES'
        target_empty.location = loc
        rig_col.objects.link(target_empty)
        
        curve_obj = None
        if self.rig_type == 'CIRCLE':
            bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
            curve_obj = context.active_object
            curve_obj.name = track_name
            for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'LINE':
            curve_data = bpy.data.curves.new(track_name, type='CURVE')
            curve_data.dimensions = '3D'
            spline = curve_data.splines.new('POLY')
            spline.points.add(1)
            curve_obj = bpy.data.objects.new(track_name, curve_data)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'SPHERE':
            curve_obj = bpy.data.objects.new(track_name, None)
            curve_obj.empty_display_type = 'SPHERE'
            rig_col.objects.link(curve_obj)
            
        # カメラ本体の生成
        cam_data = bpy.data.cameras.new(name=cam_data_name)
        cam_obj = bpy.data.objects.new(cam_name, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        rig_col.objects.link(cam_obj)
        
        # -------------------------------------------------------------
        # ★ カメラの包み (球体) と 穴あけ用カッター (円錐) の生成
        # -------------------------------------------------------------
        # 球体メッシュ作成
        mesh_sphere = bpy.data.meshes.new(shield_name)
        shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
        rig_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()
        
        # 円錐カッター作成 (カメラの視線(-Z)方向に広がるように生成)
        mesh_cone = bpy.data.meshes.new(cutter_name)
        cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
        rig_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()
        # 半径1.0、深さ2.0で作成し、Z方向を-1ずらして原点=頂点にする
        bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.0, depth=2.0)
        for v in bm_cone.verts: v.co.z -= 1.0
        bm_cone.to_mesh(mesh_cone)
        bm_cone.free()
        
        # ブーリアン設定で穴を開ける
        mod = shield_obj.modifiers.new(name="Vision_Hole", type='BOOLEAN')
        mod.operation = 'DIFFERENCE'
        mod.object = cutter_obj
        
        # 半透明マテリアルの設定
        mat_name = "CamShield_Material"
        mat = bpy.data.materials.get(mat_name)
        if not mat:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            mat.blend_method = 'BLEND'
            
        mat.diffuse_color = (*props.cam_shield_color, props.cam_shield_alpha)
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf:
            if 'Base Color' in bsdf.inputs:
                bsdf.inputs['Base Color'].default_value = (*props.cam_shield_color, 1.0)
            if 'Alpha' in bsdf.inputs:
                bsdf.inputs['Alpha'].default_value = props.cam_shield_alpha
                
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        shield_obj.color = (*props.cam_shield_color, props.cam_shield_alpha)
        # -------------------------------------------------------------

        # 軌道・追従コンストレイント設定
        if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
            const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
            const_path.target = curve_obj
            const_path.use_curve_follow = False
            const_path.use_fixed_location = True
            
        if self.rig_type != 'FIXED':
            const_track = cam_obj.constraints.new(type='TRACK_TO')
            const_track.target = target_empty
            const_track.track_axis = 'TRACK_NEGATIVE_Z'
            const_track.up_axis = 'UP_Y'
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = target_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        props.cam_target_mode = 'OBJECT'
        try:
            if target and target.type in ['CAMERA', 'LIGHT']: target = None
        except ReferenceError: target = None
        props.cam_target_obj1 = target
        
        if props.mute_target_tracking: props.mute_target_tracking = False 
        update_cam_target(props, context)
        
        if self.rig_type == 'CIRCLE':
            props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_circle_radius = 15.0
            props.cam_circle_rotation = (0, 0, 0)
            update_cam_circle(props, context)
        elif self.rig_type == 'LINE':
            props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
            props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
            update_cam_line(props, context)
        elif self.rig_type == 'SPHERE':
            props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_sphere_radius = 15.0
            props.cam_sphere_rotation = (0, 0, 0)
            props.cam_sphere_lon = 0.0
            props.cam_sphere_lat = 0.0
            update_cam_sphere(props, context)
        elif self.rig_type == 'FIXED':
            props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
            props.cam_fixed_pitch = 90.0
            props.cam_fixed_yaw = 0.0
            props.cam_fixed_roll = 0.0
            update_cam_fixed(props, context)
            
        # シールドサイズの更新と表示初期化
        props.cam_shield_radius = 1.0
        props.cam_shield_hole_angle = 179.0
        props.show_cam_obj = True
        props.show_cam_shield = True
        update_cam_shield(props, context)
        update_cam_obj_visibility(props, context)
        update_cam_shield_visibility(props, context)
            
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
                
        self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
        return {'FINISHED'}

class CAMRIG_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 CAMRIG_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 CAMRIG_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 CAMRIG_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class CAMRIG_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 CAMRIG_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")
        
        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:
            try:
                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
            except ReferenceError:
                box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
        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_cam = layout.box()
        box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
        col_cam_btn = box_cam.column(align=True)
        row1 = col_cam_btn.row(align=True)
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
        row2 = col_cam_btn.row(align=True)
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
        
        cam = context.scene.camera
        if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
            box_cam.separator()
            box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
            
            if "SPHERE" in cam.name:
                box_sp = box_cam.box()
                box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
                col_sp = box_sp.column(align=True)
                col_sp.enabled = not props.mute_target_tracking
                col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
                col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
                col_sp.separator()
                col_sp.prop(props, "cam_sphere_center", text="球の中心")
                col_sp.prop(props, "cam_sphere_radius", text="球の半径")
                col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
            elif "FIXED" in cam.name:
                box_fix = box_cam.box()
                box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
                col_fix = box_fix.column(align=True)
                col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
                col_fix.separator()
                col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
                col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
                col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
            else:
                curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
                if curve_obj:
                    col_offset = box_cam.column(align=True)
                    col_offset.enabled = not props.mute_target_tracking
                    col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
                    
                    box_curve = box_cam.box()
                    box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
                    col_crv = box_curve.column(align=True)
                    col_crv.enabled = not props.mute_target_tracking
                    if "Circle" in curve_obj.name:
                        col_crv.prop(props, "cam_circle_center", text="円の中心")
                        col_crv.prop(props, "cam_circle_radius", text="円の半径")
                        col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
                    elif "Line" in curve_obj.name:
                        col_crv.prop(props, "cam_line_start", text="始点")
                        col_crv.prop(props, "cam_line_end", text="終点")
                        
            target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
            if not target_obj and "FIXED" in cam.name:
                target_obj = cam.data.dof.focus_object
                
            if target_obj:
                box_target = box_cam.box()
                if "FIXED" in cam.name:
                    box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
                else:
                    box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
                col_tgt_main = box_target.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 == '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="カメラ枠外を暗くする (Passepartout)")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
            
            box_cam.separator()

            if "FIXED" not in cam.name:
                box_sight = box_cam.box()
                box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
                box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
                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_cam.separator()
                
            # ★ カメラ本体 & 包みの表示・非表示チェックボックス
            box_shield = box_cam.box()
            box_shield.label(text="ビューポート上の表示設定:", icon='RESTRICT_VIEW_OFF')
            
            row_disp = box_shield.row()
            row_disp.prop(props, "show_cam_obj", text="カメラ本体を表示")
            row_disp.prop(props, "show_cam_shield", text="包みを表示")
            
            col_sh = box_shield.column(align=True)
            col_sh.enabled = props.show_cam_shield
            col_sh.separator()
            col_sh.prop(props, "cam_shield_radius", text="包みのサイズ (半径)")
            col_sh.prop(props, "cam_shield_hole_angle", text="視線穴の広がり角度")
            col_sh.separator()
            col_sh.prop(props, "cam_shield_color", text="包みの色")
            col_sh.prop(props, "cam_shield_alpha", text="透明度 (アルファ)", 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, "focus_object", text="ピント対象")
                if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
        else:
            box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
            
        layout.separator(factor=1.5)
        
        box_render = layout.box()
        box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
        row_eng = box_render.row(align=True)
        row_eng.prop(context.scene.render, "engine", expand=True)
        box_render.separator()
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        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="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigProperties, CAMRIG_OT_create_camera_rig, CAMRIG_OT_reset_view,
    CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view, CAMRIG_OT_open_url, 
    CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel, 
]

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

def unregister():
    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()

```jsx

```カメラ 包み 初期値179度 表示非表示のチェックボックス作る
専用カメラ4つの 包と カメラオブジェクト
bl_info = {
    "name": "Camera & View Rig Tools",
    "author": "Your Name",
    "version": (11, 2),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"

PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

# =========================================================================
# 【カメラ軌道・シールド制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
    for const in cam.constraints:
        if const.type == 'FOLLOW_PATH' and const.target:
            if const.target.name == track_name:
                return const.target
    return None

def update_cam_circle(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Circle")
    if curve_obj:
        curve_obj.location = self.cam_circle_center
        curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
        curve_obj.rotation_euler = self.cam_circle_rotation

def update_cam_line(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Line")
    if curve_obj:
        curve_obj.location = (0, 0, 0)
        curve_obj.scale = (1, 1, 1)
        curve_obj.rotation_euler = (0, 0, 0)
        if len(curve_obj.data.splines) > 0:
            spline = curve_obj.data.splines[0]
            if spline.type == 'POLY' and len(spline.points) >= 2:
                spline.points[0].co = (*self.cam_line_start, 1.0)
                spline.points[1].co = (*self.cam_line_end, 1.0)

def update_cam_sphere(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_SPHERE" not in cam.name:
        return
        
    track_obj = bpy.data.objects.get("CamTrack_Sphere")
    if track_obj:
        track_obj.location = self.cam_sphere_center
        track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
        track_obj.rotation_euler = self.cam_sphere_rotation
    
    if getattr(self, "mute_target_tracking", False):
        return
        
    r = self.cam_sphere_radius
    lon = math.radians(self.cam_sphere_lon)
    lat = math.radians(self.cam_sphere_lat)
    
    lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
    vec = mathutils.Vector((lx, ly, lz))
    vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
    
    cx, cy, cz = self.cam_sphere_center
    cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)

def update_cam_fixed(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_FIXED" not in cam.name:
        return
        
    cam.location = self.cam_fixed_location
    cam.rotation_mode = 'XYZ'
    cam.rotation_euler = (
        math.radians(self.cam_fixed_pitch),
        math.radians(self.cam_fixed_roll),
        math.radians(self.cam_fixed_yaw)
    )

def update_cam_target(self, context):
    cam = context.scene.camera
    if not cam: return
    target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
    if not target_obj and "FIXED" in cam.name:
        target_obj = cam.data.dof.focus_object
    if not target_obj: return
    
    for c in target_obj.constraints:
        if c.type == 'COPY_LOCATION':
            target_obj.constraints.remove(c)
            
    mode = self.cam_target_mode
    if mode == 'OBJECT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
    elif mode == 'POINT':
        target_obj.location = self.cam_target_loc
    elif mode == 'MIDPOINT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
            c1.influence = 1.0
        if self.cam_target_obj2:
            c2 = target_obj.constraints.new(type='COPY_LOCATION')
            c2.target = self.cam_target_obj2
            c2.influence = 0.5

def update_cam_mute(self, context):
    cam = context.scene.camera
    if not cam: return
    track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        mat = eval_cam.matrix_world.copy()
        loc = mat.to_translation()
        rot = mat.to_euler(cam.rotation_mode)
        
        if track_const: track_const.mute = True
        if path_const: path_const.mute = True
        
        cam.location = loc
        cam.rotation_euler = rot
    else:
        if track_const: track_const.mute = False
        if path_const: path_const.mute = False
        if "SPHERE" in cam.name:
            update_cam_sphere(self, context)

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_shield(self, context):
    cam = context.scene.camera
    if not cam: return
    
    shield = None
    cutter = None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere"): shield = child
        elif child.name.startswith("CamShield_Cutter"): cutter = child
        
    if shield:
        shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
        
    if cutter:
        # Z方向の深さを十分に取りつつ、角度に応じて半径を算出
        z_scale = self.cam_shield_radius * 1.5
        angle = math.radians(self.cam_shield_hole_angle)
        # 円錐の深さは元の2.0 * z_scale。半角のtanを掛けて半径を計算
        r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
        cutter.scale = (r_scale, r_scale, z_scale)

def update_cam_shield_visibility(self, context):
    cam = context.scene.camera
    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_material(self, context):
    mat_name = "CamShield_Material"
    mat = bpy.data.materials.get(mat_name)
    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
                    
    # ビューポートカラー (Solid表示時) を更新
    for obj in bpy.data.objects:
        if obj.name.startswith("CamShield_Sphere"):
            obj.color = (*self.cam_shield_color, self.cam_shield_alpha)

# =========================================================================
# 【その他環境・ビュー制御関数】
# =========================================================================
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 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()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigProperties(bpy.types.PropertyGroup):
    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_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
    cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
    cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
    
    cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
    cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
    cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
    cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
    cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
    
    cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
    cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
    cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
    cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)

    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
        default='OBJECT', 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_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=60.0, min=0.0, max=170.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)

    # 背景・ワールド
    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 CAMRIG_OT_create_camera_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
    bl_label = "カメラ軌道セットアップ"
    bl_options = {'REGISTER', 'UNDO'}
    rig_type: bpy.props.StringProperty(default='CIRCLE')
    
    def execute(self, context):
        target = context.active_object
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        
        target_name = f"CameraTarget_{self.rig_type}"
        track_name = f"CamTrack_{self.rig_type.capitalize()}"
        cam_name = f"TrackingCamera_{self.rig_type}"
        cam_data_name = f"CamData_{self.rig_type}"
        shield_name = f"CamShield_Sphere_{self.rig_type}"
        cutter_name = f"CamShield_Cutter_{self.rig_type}"
        
        names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name]
        if target and target.name in names_to_delete: target = None
            
        loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
        
        # 既存リグのクリーンアップ
        for name in names_to_delete:
            obj = bpy.data.objects.get(name)
            if obj:
                try:
                    data = obj.data
                    bpy.data.objects.remove(obj, do_unlink=True)
                    if data and getattr(data, "users", 1) == 0:
                        if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
                        elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
                        elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
                except ReferenceError: pass
        
        cdata = bpy.data.cameras.get(cam_data_name)
        if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
        
        rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
        if not rig_col:
            rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
            context.scene.collection.children.link(rig_col)
            
        target_empty = bpy.data.objects.new(target_name, None)
        target_empty.empty_display_type = 'PLAIN_AXES'
        target_empty.location = loc
        rig_col.objects.link(target_empty)
        
        curve_obj = None
        if self.rig_type == 'CIRCLE':
            bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
            curve_obj = context.active_object
            curve_obj.name = track_name
            for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'LINE':
            curve_data = bpy.data.curves.new(track_name, type='CURVE')
            curve_data.dimensions = '3D'
            spline = curve_data.splines.new('POLY')
            spline.points.add(1)
            curve_obj = bpy.data.objects.new(track_name, curve_data)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'SPHERE':
            curve_obj = bpy.data.objects.new(track_name, None)
            curve_obj.empty_display_type = 'SPHERE'
            rig_col.objects.link(curve_obj)
            
        # カメラ本体の生成
        cam_data = bpy.data.cameras.new(name=cam_data_name)
        cam_obj = bpy.data.objects.new(cam_name, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        rig_col.objects.link(cam_obj)
        
        # -------------------------------------------------------------
        # ★ カメラ位置シールド (球体) と 穴あけ用カッター (円錐) の生成
        # -------------------------------------------------------------
        # 球体メッシュ作成
        mesh_sphere = bpy.data.meshes.new(shield_name)
        shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
        rig_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()
        
        # 円錐カッター作成 (カメラの視線(-Z)方向に広がるように生成)
        mesh_cone = bpy.data.meshes.new(cutter_name)
        cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
        rig_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()
        # 半径1.0、深さ2.0で作成し、Z方向を-1ずらして原点=頂点にする
        bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.0, depth=2.0)
        for v in bm_cone.verts: v.co.z -= 1.0
        bm_cone.to_mesh(mesh_cone)
        bm_cone.free()
        
        # ブーリアン設定で穴を開ける
        mod = shield_obj.modifiers.new(name="Vision_Hole", type='BOOLEAN')
        mod.operation = 'DIFFERENCE'
        mod.object = cutter_obj
        
        # 半透明マテリアルの設定
        mat_name = "CamShield_Material"
        mat = bpy.data.materials.get(mat_name)
        if not mat:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            mat.blend_method = 'BLEND'
            
        mat.diffuse_color = (*props.cam_shield_color, props.cam_shield_alpha)
        bsdf = mat.node_tree.nodes.get("Principled BSDF")
        if bsdf:
            if 'Base Color' in bsdf.inputs:
                bsdf.inputs['Base Color'].default_value = (*props.cam_shield_color, 1.0)
            if 'Alpha' in bsdf.inputs:
                bsdf.inputs['Alpha'].default_value = props.cam_shield_alpha
                
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        shield_obj.color = (*props.cam_shield_color, props.cam_shield_alpha)
        # -------------------------------------------------------------

        # 軌道・追従コンストレイント設定
        if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
            const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
            const_path.target = curve_obj
            const_path.use_curve_follow = False
            const_path.use_fixed_location = True
            
        if self.rig_type != 'FIXED':
            const_track = cam_obj.constraints.new(type='TRACK_TO')
            const_track.target = target_empty
            const_track.track_axis = 'TRACK_NEGATIVE_Z'
            const_track.up_axis = 'UP_Y'
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = target_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        props.cam_target_mode = 'OBJECT'
        try:
            if target and target.type in ['CAMERA', 'LIGHT']: target = None
        except ReferenceError: target = None
        props.cam_target_obj1 = target
        
        if props.mute_target_tracking: props.mute_target_tracking = False 
        update_cam_target(props, context)
        
        if self.rig_type == 'CIRCLE':
            props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_circle_radius = 15.0
            props.cam_circle_rotation = (0, 0, 0)
            update_cam_circle(props, context)
        elif self.rig_type == 'LINE':
            props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
            props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
            update_cam_line(props, context)
        elif self.rig_type == 'SPHERE':
            props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_sphere_radius = 15.0
            props.cam_sphere_rotation = (0, 0, 0)
            props.cam_sphere_lon = 0.0
            props.cam_sphere_lat = 0.0
            update_cam_sphere(props, context)
        elif self.rig_type == 'FIXED':
            props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
            props.cam_fixed_pitch = 90.0
            props.cam_fixed_yaw = 0.0
            props.cam_fixed_roll = 0.0
            update_cam_fixed(props, context)
            
        # シールドサイズの更新
        props.cam_shield_radius = 1.0
        props.cam_shield_hole_angle = 60.0
        update_cam_shield(props, context)
        update_cam_shield_visibility(props, context)
            
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
                
        self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
        return {'FINISHED'}

class CAMRIG_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 CAMRIG_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 CAMRIG_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 CAMRIG_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class CAMRIG_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 CAMRIG_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")
        
        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:
            try:
                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
            except ReferenceError:
                box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
        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_cam = layout.box()
        box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
        col_cam_btn = box_cam.column(align=True)
        row1 = col_cam_btn.row(align=True)
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
        row2 = col_cam_btn.row(align=True)
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
        
        cam = context.scene.camera
        if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
            box_cam.separator()
            box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
            
            if "SPHERE" in cam.name:
                box_sp = box_cam.box()
                box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
                col_sp = box_sp.column(align=True)
                col_sp.enabled = not props.mute_target_tracking
                col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
                col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
                col_sp.separator()
                col_sp.prop(props, "cam_sphere_center", text="球の中心")
                col_sp.prop(props, "cam_sphere_radius", text="球の半径")
                col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
            elif "FIXED" in cam.name:
                box_fix = box_cam.box()
                box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
                col_fix = box_fix.column(align=True)
                col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
                col_fix.separator()
                col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
                col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
                col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
            else:
                curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
                if curve_obj:
                    col_offset = box_cam.column(align=True)
                    col_offset.enabled = not props.mute_target_tracking
                    col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
                    
                    box_curve = box_cam.box()
                    box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
                    col_crv = box_curve.column(align=True)
                    col_crv.enabled = not props.mute_target_tracking
                    if "Circle" in curve_obj.name:
                        col_crv.prop(props, "cam_circle_center", text="円の中心")
                        col_crv.prop(props, "cam_circle_radius", text="円の半径")
                        col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
                    elif "Line" in curve_obj.name:
                        col_crv.prop(props, "cam_line_start", text="始点")
                        col_crv.prop(props, "cam_line_end", text="終点")
                        
            target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
            if not target_obj and "FIXED" in cam.name:
                target_obj = cam.data.dof.focus_object
                
            if target_obj:
                box_target = box_cam.box()
                if "FIXED" in cam.name:
                    box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
                else:
                    box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
                col_tgt_main = box_target.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 == '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="カメラ枠外を暗くする (Passepartout)")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
            
            box_cam.separator()

            if "FIXED" not in cam.name:
                box_sight = box_cam.box()
                box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
                box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
                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_cam.separator()
                
            # ★ カメラ位置シールドUI
            box_shield = box_cam.box()
            row_sh = box_shield.row(align=True)
            row_sh.prop(props, "show_cam_shield", text="", icon='RESTRICT_VIEW_OFF' if props.show_cam_shield else 'RESTRICT_VIEW_ON')
            row_sh.label(text="カメラの位置シールド (球体)", icon='MESH_UVSPHERE')
            
            col_sh = box_shield.column(align=True)
            col_sh.enabled = props.show_cam_shield
            col_sh.prop(props, "cam_shield_radius", text="球体のサイズ (半径)")
            col_sh.prop(props, "cam_shield_hole_angle", text="視線穴の広がり角度")
            col_sh.separator()
            col_sh.prop(props, "cam_shield_color", text="シールドの色")
            col_sh.prop(props, "cam_shield_alpha", text="透明度 (アルファ)", 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, "focus_object", text="ピント対象")
                if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
        else:
            box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
            
        layout.separator(factor=1.5)
        
        box_render = layout.box()
        box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
        row_eng = box_render.row(align=True)
        row_eng.prop(context.scene.render, "engine", expand=True)
        box_render.separator()
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        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="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigProperties, CAMRIG_OT_create_camera_rig, CAMRIG_OT_reset_view,
    CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view, CAMRIG_OT_open_url, 
    CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel, 
]

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

def unregister():
    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()

```jsx
カメラを包む球体を表示 非表示できるようにして
色設定 透明度設置もできるようにして

```jsx
bl_info = {
    "name": "Camera & View Rig Tools",
    "author": "Your Name",
    "version": (11, 2),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "カメラ軌道のセットアップとビュー・オブジェクト・背景の操作ツール",
    "category": "Object",
}

import bpy
import bmesh
import webbrowser
import math
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "Camera 20260704"
PREFIX_NAME = "camrigdon"
RIG_COLLECTION_NAME = "CamRig_Collection"

PREFIX_SAFE = PREFIX_NAME.strip().lower().replace(" ", "_").replace("-", "_")

# =========================================================================
# 【カメラ軌道・シールド制御用アップデート関数】
# =========================================================================
def get_curve_target(cam, track_name):
    for const in cam.constraints:
        if const.type == 'FOLLOW_PATH' and const.target:
            if const.target.name == track_name:
                return const.target
    return None

def update_cam_circle(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Circle")
    if curve_obj:
        curve_obj.location = self.cam_circle_center
        curve_obj.scale = (self.cam_circle_radius, self.cam_circle_radius, self.cam_circle_radius)
        curve_obj.rotation_euler = self.cam_circle_rotation

def update_cam_line(self, context):
    cam = context.scene.camera
    if not cam: return
    curve_obj = get_curve_target(cam, "CamTrack_Line")
    if curve_obj:
        curve_obj.location = (0, 0, 0)
        curve_obj.scale = (1, 1, 1)
        curve_obj.rotation_euler = (0, 0, 0)
        if len(curve_obj.data.splines) > 0:
            spline = curve_obj.data.splines[0]
            if spline.type == 'POLY' and len(spline.points) >= 2:
                spline.points[0].co = (*self.cam_line_start, 1.0)
                spline.points[1].co = (*self.cam_line_end, 1.0)

def update_cam_sphere(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_SPHERE" not in cam.name:
        return
        
    track_obj = bpy.data.objects.get("CamTrack_Sphere")
    if track_obj:
        track_obj.location = self.cam_sphere_center
        track_obj.scale = (self.cam_sphere_radius, self.cam_sphere_radius, self.cam_sphere_radius)
        track_obj.rotation_euler = self.cam_sphere_rotation
    
    if getattr(self, "mute_target_tracking", False):
        return
        
    r = self.cam_sphere_radius
    lon = math.radians(self.cam_sphere_lon)
    lat = math.radians(self.cam_sphere_lat)
    
    lx, ly, lz = r * math.cos(lat) * math.cos(lon), r * math.cos(lat) * math.sin(lon), r * math.sin(lat)
    vec = mathutils.Vector((lx, ly, lz))
    vec.rotate(mathutils.Euler(self.cam_sphere_rotation, 'XYZ'))
    
    cx, cy, cz = self.cam_sphere_center
    cam.location = (cx + vec.x, cy + vec.y, cz + vec.z)

def update_cam_fixed(self, context):
    cam = context.scene.camera
    if not cam or "TrackingCamera_FIXED" not in cam.name:
        return
        
    cam.location = self.cam_fixed_location
    cam.rotation_mode = 'XYZ'
    cam.rotation_euler = (
        math.radians(self.cam_fixed_pitch),
        math.radians(self.cam_fixed_roll),
        math.radians(self.cam_fixed_yaw)
    )

def update_cam_target(self, context):
    cam = context.scene.camera
    if not cam: return
    target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
    if not target_obj and "FIXED" in cam.name:
        target_obj = cam.data.dof.focus_object
    if not target_obj: return
    
    for c in target_obj.constraints:
        if c.type == 'COPY_LOCATION':
            target_obj.constraints.remove(c)
            
    mode = self.cam_target_mode
    if mode == 'OBJECT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
    elif mode == 'POINT':
        target_obj.location = self.cam_target_loc
    elif mode == 'MIDPOINT':
        if self.cam_target_obj1:
            c1 = target_obj.constraints.new(type='COPY_LOCATION')
            c1.target = self.cam_target_obj1
            c1.influence = 1.0
        if self.cam_target_obj2:
            c2 = target_obj.constraints.new(type='COPY_LOCATION')
            c2.target = self.cam_target_obj2
            c2.influence = 0.5

def update_cam_mute(self, context):
    cam = context.scene.camera
    if not cam: return
    track_const = next((c for c in cam.constraints if c.type == 'TRACK_TO'), None)
    path_const = next((c for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
    
    if self.mute_target_tracking:
        depsgraph = context.evaluated_depsgraph_get()
        eval_cam = cam.evaluated_get(depsgraph)
        mat = eval_cam.matrix_world.copy()
        loc = mat.to_translation()
        rot = mat.to_euler(cam.rotation_mode)
        
        if track_const: track_const.mute = True
        if path_const: path_const.mute = True
        
        cam.location = loc
        cam.rotation_euler = rot
    else:
        if track_const: track_const.mute = False
        if path_const: path_const.mute = False
        if "SPHERE" in cam.name:
            update_cam_sphere(self, context)

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_shield(self, context):
    cam = context.scene.camera
    if not cam: return
    
    shield = None
    cutter = None
    for child in cam.children:
        if child.name.startswith("CamShield_Sphere"): shield = child
        elif child.name.startswith("CamShield_Cutter"): cutter = child
        
    if shield:
        shield.scale = (self.cam_shield_radius, self.cam_shield_radius, self.cam_shield_radius)
        
    if cutter:
        # Z方向の深さを十分に取りつつ、角度に応じて半径を算出
        z_scale = self.cam_shield_radius * 1.5
        angle = math.radians(self.cam_shield_hole_angle)
        # 円錐の深さは元の2.0 * z_scale。半角のtanを掛けて半径を計算
        r_scale = 2.0 * z_scale * math.tan(angle / 2.0)
        cutter.scale = (r_scale, r_scale, z_scale)

def update_cam_shield_visibility(self, context):
    cam = context.scene.camera
    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_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 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()

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class CamRigProperties(bpy.types.PropertyGroup):
    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_circle_center: bpy.props.FloatVectorProperty(name="円の中心", default=(0,0,5), update=update_cam_circle)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=15.0, min=0.1, update=update_cam_circle)
    cam_circle_rotation: bpy.props.FloatVectorProperty(name="円の傾き", subtype='EULER', default=(0,0,0), update=update_cam_circle)
    
    cam_line_start: bpy.props.FloatVectorProperty(name="始点", default=(-15,-15,5), update=update_cam_line)
    cam_line_end: bpy.props.FloatVectorProperty(name="終点", default=(15,-15,5), update=update_cam_line)
    
    cam_sphere_center: bpy.props.FloatVectorProperty(name="球の中心", default=(0,0,5), update=update_cam_sphere)
    cam_sphere_radius: bpy.props.FloatProperty(name="球の半径", default=15.0, min=0.1, update=update_cam_sphere)
    cam_sphere_rotation: bpy.props.FloatVectorProperty(name="球の傾き", subtype='EULER', default=(0,0,0), update=update_cam_sphere)
    cam_sphere_lon: bpy.props.FloatProperty(name="U軸 (経度・左右)", default=0.0, update=update_cam_sphere)
    cam_sphere_lat: bpy.props.FloatProperty(name="V軸 (緯度・上下)", default=0.0, update=update_cam_sphere)
    
    cam_fixed_location: bpy.props.FloatVectorProperty(name="設置座標", default=(0,-10,5), update=update_cam_fixed)
    cam_fixed_pitch: bpy.props.FloatProperty(name="Pitch (上下)", default=90.0, update=update_cam_fixed)
    cam_fixed_yaw: bpy.props.FloatProperty(name="Yaw (左右)", default=0.0, update=update_cam_fixed)
    cam_fixed_roll: bpy.props.FloatProperty(name="Roll (傾き)", default=0.0, update=update_cam_fixed)

    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点の指定方法", items=[('OBJECT', "単一オブジェクト", ""), ('POINT', "指定座標 (手動)", ""), ('MIDPOINT', "2オブジェクトの中間", "")],
        default='OBJECT', 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_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=60.0, min=0.0, max=170.0, update=update_cam_shield)

    # 背景・ワールド
    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 CAMRIG_OT_create_camera_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_SAFE}_create_camera_rig"
    bl_label = "カメラ軌道セットアップ"
    bl_options = {'REGISTER', 'UNDO'}
    rig_type: bpy.props.StringProperty(default='CIRCLE')
    
    def execute(self, context):
        target = context.active_object
        
        target_name = f"CameraTarget_{self.rig_type}"
        track_name = f"CamTrack_{self.rig_type.capitalize()}"
        cam_name = f"TrackingCamera_{self.rig_type}"
        cam_data_name = f"CamData_{self.rig_type}"
        shield_name = f"CamShield_Sphere_{self.rig_type}"
        cutter_name = f"CamShield_Cutter_{self.rig_type}"
        
        names_to_delete = [cam_name, target_name, track_name, shield_name, cutter_name]
        if target and target.name in names_to_delete: target = None
            
        loc = target.matrix_world.to_translation() if target else mathutils.Vector((0,0,0))
        
        # 既存リグのクリーンアップ
        for name in names_to_delete:
            obj = bpy.data.objects.get(name)
            if obj:
                try:
                    data = obj.data
                    bpy.data.objects.remove(obj, do_unlink=True)
                    if data and getattr(data, "users", 1) == 0:
                        if isinstance(data, bpy.types.Camera): bpy.data.cameras.remove(data)
                        elif isinstance(data, bpy.types.Curve): bpy.data.curves.remove(data)
                        elif isinstance(data, bpy.types.Mesh): bpy.data.meshes.remove(data)
                except ReferenceError: pass
        
        cdata = bpy.data.cameras.get(cam_data_name)
        if cdata and cdata.users == 0: bpy.data.cameras.remove(cdata)
        
        rig_col = bpy.data.collections.get(RIG_COLLECTION_NAME)
        if not rig_col:
            rig_col = bpy.data.collections.new(RIG_COLLECTION_NAME)
            context.scene.collection.children.link(rig_col)
            
        target_empty = bpy.data.objects.new(target_name, None)
        target_empty.empty_display_type = 'PLAIN_AXES'
        target_empty.location = loc
        rig_col.objects.link(target_empty)
        
        curve_obj = None
        if self.rig_type == 'CIRCLE':
            bpy.ops.curve.primitive_nurbs_circle_add(radius=1.0, location=(0,0,0))
            curve_obj = context.active_object
            curve_obj.name = track_name
            for col in curve_obj.users_collection: col.objects.unlink(curve_obj)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'LINE':
            curve_data = bpy.data.curves.new(track_name, type='CURVE')
            curve_data.dimensions = '3D'
            spline = curve_data.splines.new('POLY')
            spline.points.add(1)
            curve_obj = bpy.data.objects.new(track_name, curve_data)
            rig_col.objects.link(curve_obj)
        elif self.rig_type == 'SPHERE':
            curve_obj = bpy.data.objects.new(track_name, None)
            curve_obj.empty_display_type = 'SPHERE'
            rig_col.objects.link(curve_obj)
            
        # カメラ本体の生成
        cam_data = bpy.data.cameras.new(name=cam_data_name)
        cam_obj = bpy.data.objects.new(cam_name, cam_data)
        cam_obj.rotation_mode = 'XYZ'
        rig_col.objects.link(cam_obj)
        
        # -------------------------------------------------------------
        # ★ カメラ位置シールド (球体) と 穴あけ用カッター (円錐) の生成
        # -------------------------------------------------------------
        # 球体メッシュ作成
        mesh_sphere = bpy.data.meshes.new(shield_name)
        shield_obj = bpy.data.objects.new(shield_name, mesh_sphere)
        rig_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()
        
        # 円錐カッター作成 (カメラの視線(-Z)方向に広がるように生成)
        mesh_cone = bpy.data.meshes.new(cutter_name)
        cutter_obj = bpy.data.objects.new(cutter_name, mesh_cone)
        rig_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()
        # 半径1.0、深さ2.0で作成し、Z方向を-1ずらして原点=頂点にする
        bmesh.ops.create_cone(bm_cone, cap_ends=True, cap_tris=False, segments=32, radius1=1.0, radius2=0.0, depth=2.0)
        for v in bm_cone.verts: v.co.z -= 1.0
        bm_cone.to_mesh(mesh_cone)
        bm_cone.free()
        
        # ブーリアン設定で穴を開ける
        mod = shield_obj.modifiers.new(name="Vision_Hole", type='BOOLEAN')
        mod.operation = 'DIFFERENCE'
        mod.object = cutter_obj
        # mod.solver = 'FAST'  ←Blender 4.x以降でエラーになるため削除。Blenderのデフォルト設定に任せます。
        
        # 半透明マテリアルの設定
        mat_name = "CamShield_Material"
        mat = bpy.data.materials.get(mat_name)
        if not mat:
            mat = bpy.data.materials.new(mat_name)
            mat.use_nodes = True
            mat.blend_method = 'BLEND'
            mat.diffuse_color = (0.0, 0.8, 1.0, 0.3)
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                bsdf.inputs['Base Color'].default_value = (0.0, 0.8, 1.0, 1.0)
                bsdf.inputs['Alpha'].default_value = 0.3
        shield_obj.data.materials.append(mat)
        shield_obj.show_transparent = True
        shield_obj.color = (0.0, 0.8, 1.0, 0.3)
        # -------------------------------------------------------------

        # 軌道・追従コンストレイント設定
        if curve_obj and self.rig_type not in ['SPHERE', 'FIXED']:
            const_path = cam_obj.constraints.new(type='FOLLOW_PATH')
            const_path.target = curve_obj
            const_path.use_curve_follow = False
            const_path.use_fixed_location = True
            
        if self.rig_type != 'FIXED':
            const_track = cam_obj.constraints.new(type='TRACK_TO')
            const_track.target = target_empty
            const_track.track_axis = 'TRACK_NEGATIVE_Z'
            const_track.up_axis = 'UP_Y'
        
        context.scene.camera = cam_obj
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = target_empty
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        props = getattr(context.scene, f"{PREFIX_SAFE}_props")
        props.cam_target_mode = 'OBJECT'
        try:
            if target and target.type in ['CAMERA', 'LIGHT']: target = None
        except ReferenceError: target = None
        props.cam_target_obj1 = target
        
        if props.mute_target_tracking: props.mute_target_tracking = False 
        update_cam_target(props, context)
        
        if self.rig_type == 'CIRCLE':
            props.cam_circle_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_circle_radius = 15.0
            props.cam_circle_rotation = (0, 0, 0)
            update_cam_circle(props, context)
        elif self.rig_type == 'LINE':
            props.cam_line_start = (loc.x - 15.0, loc.y - 15.0, loc.z + 5.0)
            props.cam_line_end = (loc.x + 15.0, loc.y - 15.0, loc.z + 5.0)
            update_cam_line(props, context)
        elif self.rig_type == 'SPHERE':
            props.cam_sphere_center = (loc.x, loc.y, loc.z + 5.0)
            props.cam_sphere_radius = 15.0
            props.cam_sphere_rotation = (0, 0, 0)
            props.cam_sphere_lon = 0.0
            props.cam_sphere_lat = 0.0
            update_cam_sphere(props, context)
        elif self.rig_type == 'FIXED':
            props.cam_fixed_location = (loc.x, loc.y - 10.0, loc.z + 5.0)
            props.cam_fixed_pitch = 90.0
            props.cam_fixed_yaw = 0.0
            props.cam_fixed_roll = 0.0
            update_cam_fixed(props, context)
            
        # シールドサイズの更新
        props.cam_shield_radius = 1.0
        props.cam_shield_hole_angle = 60.0
        update_cam_shield(props, context)
        update_cam_shield_visibility(props, context)
            
        rv3d = get_rv3d(context)
        if rv3d: rv3d.view_perspective = 'CAMERA'
                
        self.report({'INFO'}, f"専用コレクション({RIG_COLLECTION_NAME})に {self.rig_type} カメラをセットアップしました。")
        return {'FINISHED'}

class CAMRIG_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 CAMRIG_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 CAMRIG_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 CAMRIG_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_SAFE}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class CAMRIG_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 CAMRIG_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")
        
        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:
            try:
                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
            except ReferenceError:
                box_rot.label(text="※オブジェクトが無効です", icon='ERROR')
        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_cam = layout.box()
        box_cam.label(text="専用カメラリグ作成:", icon='CAMERA_DATA')
        col_cam_btn = box_cam.column(align=True)
        row1 = col_cam_btn.row(align=True)
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="円周", icon='MESH_CIRCLE').rig_type = 'CIRCLE'
        row1.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="線分", icon='CURVE_PATH').rig_type = 'LINE'
        row2 = col_cam_btn.row(align=True)
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="球面", icon='MESH_UVSPHERE').rig_type = 'SPHERE'
        row2.operator(f"object.{PREFIX_SAFE}_create_camera_rig", text="固定 (定点)", icon='CAMERA_DATA').rig_type = 'FIXED'
        
        cam = context.scene.camera
        if cam and cam.type == 'CAMERA' and "TrackingCamera_" in cam.name:
            box_cam.separator()
            box_cam.label(text=f"操作中: {cam.name}", icon='VIEW_CAMERA')
            
            if "SPHERE" in cam.name:
                box_sp = box_cam.box()
                box_sp.label(text="球面軌道の設定:", icon='MESH_UVSPHERE')
                col_sp = box_sp.column(align=True)
                col_sp.enabled = not props.mute_target_tracking
                col_sp.prop(props, "cam_sphere_lon", text="U軸 (経度・左右)")
                col_sp.prop(props, "cam_sphere_lat", text="V軸 (緯度・上下)")
                col_sp.separator()
                col_sp.prop(props, "cam_sphere_center", text="球の中心")
                col_sp.prop(props, "cam_sphere_radius", text="球の半径")
                col_sp.prop(props, "cam_sphere_rotation", text="球の傾き (XYZ)")
            elif "FIXED" in cam.name:
                box_fix = box_cam.box()
                box_fix.label(text="固定(定点)カメラの設定:", icon='CAMERA_DATA')
                col_fix = box_fix.column(align=True)
                col_fix.prop(props, "cam_fixed_location", text="設置座標 (XYZ)")
                col_fix.separator()
                col_fix.prop(props, "cam_fixed_pitch", text="Pitch (上下)")
                col_fix.prop(props, "cam_fixed_yaw", text="Yaw (左右)")
                col_fix.prop(props, "cam_fixed_roll", text="Roll (傾き)")
            else:
                curve_obj = next((c.target for c in cam.constraints if c.type == 'FOLLOW_PATH'), None)
                if curve_obj:
                    col_offset = box_cam.column(align=True)
                    col_offset.enabled = not props.mute_target_tracking
                    col_offset.prop(cam.constraints['Follow Path'], "offset_factor", text="軌道上の移動 (0~1)", slider=True)
                    
                    box_curve = box_cam.box()
                    box_curve.label(text="軌道の調整:", icon='CURVE_DATA')
                    col_crv = box_curve.column(align=True)
                    col_crv.enabled = not props.mute_target_tracking
                    if "Circle" in curve_obj.name:
                        col_crv.prop(props, "cam_circle_center", text="円の中心")
                        col_crv.prop(props, "cam_circle_radius", text="円の半径")
                        col_crv.prop(props, "cam_circle_rotation", text="円の傾き (XYZ)")
                    elif "Line" in curve_obj.name:
                        col_crv.prop(props, "cam_line_start", text="始点")
                        col_crv.prop(props, "cam_line_end", text="終点")
                        
            target_obj = next((c.target for c in cam.constraints if c.type == 'TRACK_TO'), None)
            if not target_obj and "FIXED" in cam.name:
                target_obj = cam.data.dof.focus_object
                
            if target_obj:
                box_target = box_cam.box()
                if "FIXED" in cam.name:
                    box_target.label(text=f"ピント基準点({target_obj.name}):", icon='EMPTY_DATA')
                else:
                    box_target.label(text=f"注視点({target_obj.name})の設定:", icon='EMPTY_DATA')
                col_tgt_main = box_target.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 == '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="カメラ枠外を暗くする (Passepartout)")
            if cam.data.show_passepartout:
                col_pp.prop(cam.data, "passepartout_alpha", text="枠外の暗さ (Opacity)", slider=True)
            
            box_cam.separator()

            if "FIXED" not in cam.name:
                box_sight = box_cam.box()
                box_sight.label(text="視線と位置 (手動操作):", icon='ORIENTATION_GIMBAL')
                box_sight.prop(props, "mute_target_tracking", text="軌道と注視を解除 (現在位置を維持)", toggle=True, icon='UNLINKED')
                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_cam.separator()
                
            # ★ カメラ位置シールドUI
            box_shield = box_cam.box()
            row_sh = box_shield.row(align=True)
            row_sh.prop(props, "show_cam_shield", text="", icon='RESTRICT_VIEW_OFF' if props.show_cam_shield else 'RESTRICT_VIEW_ON')
            row_sh.label(text="カメラの位置シールド (球体)", icon='MESH_UVSPHERE')
            col_sh = box_shield.column(align=True)
            col_sh.enabled = props.show_cam_shield
            col_sh.prop(props, "cam_shield_radius", text="球体のサイズ (半径)")
            col_sh.prop(props, "cam_shield_hole_angle", text="視線穴の広がり角度")
            
            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, "focus_object", text="ピント対象")
                if not cam.data.dof.focus_object: col_dof.prop(cam.data.dof, "focus_distance", text="ピント距離")
                col_dof.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
        else:
            box_cam.label(text="※専用カメラがアクティブではありません", icon='INFO')
            
        layout.separator(factor=1.5)
        
        box_render = layout.box()
        box_render.label(text="レンダリング & ビューポート & ワールド:", icon='SHADING_RENDERED')
        row_eng = box_render.row(align=True)
        row_eng.prop(context.scene.render, "engine", expand=True)
        box_render.separator()
        box_render.prop(props, "viewport_bg_color", text="ビューポート背景色 (Solid時)")
        box_render.separator()
        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="アドオン削除パネル", icon='URL').url = "<https://app.notion.com/p/20260704-390f5dacaf4380e6939dd28e6e2ff91d>"
        box_sys.operator(f"wm.{PREFIX_SAFE}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    CamRigProperties, CAMRIG_OT_create_camera_rig, CAMRIG_OT_reset_view,
    CAMRIG_OT_rotate_selected, CAMRIG_OT_rotate_view, CAMRIG_OT_open_url, 
    CAMRIG_OT_remove_addon, CAMRIG_PT_main_panel, 
]

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

def unregister():
    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()

```jsx
専用カメラオブジェクトを包む半径1初期値の球体を作って
カメラの視線方向に大きな穴を作って 調整できるように
専用カメラのpitch yaw roll に追随して穴の方向が動く
カメラ視線を妨害しないように

それでいて カメラが どこにあるか わかるようにこの球体で
カメラを包む