Cam 固定 blender アドオン









bl_info = {
    "name": "Fixed Camera & Automation Master",
    "author": "Your Name",
    "version": (4, 23),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "固定位置カメラ(カメラ円周移動追加・軌道設定拡充)",
    "category": "3D View",
}

import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import sys

# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed & View Master"
PREFIX_VAL = "CAM_FVIEW_MASTER"
ADDON_CATEGORY = " Cam Fixed "
PREFIX = PREFIX_VAL.lower()

# ===================================
# 5. リンク定義
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"

LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"

LINK_NAME_4 = ""
LINK_URL_4 = ""

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================
# 6. 初期設定
# ===================================
COLLECTION_NAME_CAM = "FixedCam_Collection"
COLLECTION_SUB_NAME_CAM = "FixedCam_SubCollection"
OBJ_NAME_ROOT = "FixedCamera_Root"
OBJ_NAME_CAMERA = "FixedCamera"
OBJ_NAME_CAMERA_BACK = "FixedCamera_Observer"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"

SHIELD_NAME = "FixedCam_Shield_Sphere"
CUTTER_NAME = "FixedCam_Shield_Cutter"
CUTTER_BACK_NAME = "FixedCam_Shield_CutterBack"
MAT_NAME_SHIELD = "FixedCam_Shield_Mat"

BACK_SHIELD_NAME = "FixedCam_BackShield_Sphere"
BACK_CUTTER_NAME = "FixedCam_BackShield_Cutter"
BACK_CUTTER_BACK_NAME = "FixedCam_BackShield_CutterBack"
MAT_NAME_BACK_SHIELD = "FixedCam_BackShield_Mat"

MAT_NAME_PUPIL = "Pupil_Window_Mat"
MAT_NAME_TARGET = "FixedCam_Target_Mat"

CAMERA_PRESETS = [
    ("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
    ("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
    ("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]

# --- 選択・表示状態の初期値 ---
CAM_MOVE_MODE_DEFAULT = 'FIXED'
CAM_TARGET_MODE_DEFAULT = 'NONE'
ACTIVE_CAMERA_DEFAULT = 'MAIN'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = True
SHOW_CAM_SHIELD_DEFAULT = True
SHOW_BACK_CAM_SHIELD_DEFAULT = True

CAMERA_COLOR_DEFAULT = (0.1483, 1.0000, 0.1110)

CAM_LOC_U_DEFAULT = 0.0000
CAM_LOC_V_DEFAULT = 0.0000
CAM_LOC_W_DEFAULT = 0.0000

# カメラの移動用
CAM_START_LOC_DEFAULT = (-50.0, 0.0, 0.0)
CAM_END_LOC_DEFAULT = (50.0, 0.0, 0.0)
CAM_CIRCLE_CENTER_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_RADIUS_DEFAULT = 20.0
CAM_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_START_ANGLE_DEFAULT = 0.0
CAM_CIRCLE_END_ANGLE_DEFAULT = 360.0
CAM_ANIM_SPEED_MULT_DEFAULT = 1.0

CAM_TGT_U_DEFAULT = 0.0000
CAM_TGT_V_DEFAULT = 0.0000
CAM_TGT_W_DEFAULT = 0.0000

# ターゲット(線分・円周移動用)
TARGET_START_DEFAULT = (-50.0, 5.0, 0.0)
TARGET_END_DEFAULT = (50.0, 5.0, 0.0)
TARGET_CIRCLE_CENTER_DEFAULT = (0.0, 5.0, 0.0)
TARGET_CIRCLE_RADIUS_DEFAULT = 10.0
TARGET_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
TARGET_CIRCLE_START_ANGLE_DEFAULT = 0.0
TARGET_CIRCLE_END_ANGLE_DEFAULT = 360.0

TARGET_SIZE_DEFAULT = 1.0
TARGET_COLOR_DEFAULT = (0.0000, 0.8000, 0.2000, 1.0)
TARGET_RANDOM_COLOR_DEFAULT = False
TARGET_ANIM_SPEED_MULT_DEFAULT = 1.0

CAM_SHIELD_RADIUS_DEFAULT = 1.7600
CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000

BACK_CAM_SHIELD_RADIUS_DEFAULT = 1.7600
BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000

CAM_BACK_DIST_DEFAULT = 10.0000

PUPIL_SHOW_DEFAULT = True
PUPIL_DIST_DEFAULT = 5.0000
PUPIL_RADIUS_DEFAULT = 2.0000
PUPIL_THICKNESS_DEFAULT = 0.0500
PUPIL_SEGMENTS_DEFAULT = 4
COLOR_PUPIL = (1.0000, 1.0000, 1.0000, 1.0)
PUPIL_ROT_X_DEFAULT = 0.0000
PUPIL_ROT_Y_DEFAULT = 0.0000
PUPIL_ROT_Z_DEFAULT = 0.7854

ANIM_START_FRAME_DEFAULT = 1
ANIM_END_FRAME_DEFAULT = 500

CUSTOM_GRID_SCALE_DEFAULT = 50.0

CAM_ROT_U_START = 1.5708
CAM_ROT_V_START = 0.0000
CAM_ROT_W_START = 0.0000

CAM_ROT_SPEED_U_DEFAULT = 0.0000
CAM_ROT_SPEED_V_DEFAULT = 0.0000
CAM_ROT_SPEED_W_DEFAULT = 0.3000

CAM_OFFSET_U_DEFAULT = 0.0000
CAM_OFFSET_V_DEFAULT = 0.0000
CAM_OFFSET_W_DEFAULT = 0.0000

ALPHA_SHIELD_DEFAULT = 1.0000
ALPHA_BACK_SHIELD_DEFAULT = 1.0000

COLOR_SHIELD = (0.2565, 0.0141, 0.1115, 1.0)
COLOR_BACK_SHIELD = (0.0141, 0.2565, 0.1115, 1.0)

LABEL_SIZE = "サイズ"
LABEL_U = "U"
LABEL_V = "V"
LABEL_W = "W"
LABEL_LOC = "座標"
LABEL_START = "開始"
LABEL_END = "終了"

# ===================================
# 7. 共通クラス
# ===================================
class CP_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX}_open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        if self.url: webbrowser.open(self.url)
        return {'FINISHED'}

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

# ===================================
# 8. アドオン固有機能
# ===================================

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 get_addon_source():
    source = ""
    try:
        if '__file__' in globals() and os.path.exists(__file__):
            with open(__file__, 'r', encoding='utf-8') as f:
                source = f.read()
    except Exception: pass
    if not source:
        for t in bpy.data.texts:
            if f'ADDON_TITLE = "{ADDON_TITLE}"' in t.as_string():
                source = t.as_string()
                break
    return source

def cam_fov_get(self):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        cam.data.angle = math.radians(value)

def update_visibility(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam:
        cam.hide_viewport = not self.show_cam_obj
        
    for name in [OBJ_NAME_ROOT, CUTTER_NAME, CUTTER_BACK_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            obj.hide_viewport = not self.show_target_empty

def update_target_visuals(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if tgt:
        tgt.scale = (self.target_size, self.target_size, self.target_size)
        mesh = tgt.data
        
        if len(mesh.materials) < 6:
            mesh.materials.clear()
            for i in range(6):
                mat = bpy.data.materials.new(name=f"{MAT_NAME_TARGET}_{i}")
                mat.use_nodes = True
                mesh.materials.append(mat)
                
        colors = [
            (1.0, 0.0, 0.0, 1.0), # 赤
            (0.0, 1.0, 0.0, 1.0), # 緑
            (0.0, 0.5, 1.0, 1.0), # 青
            (1.0, 1.0, 0.0, 1.0), # 黄
            (0.0, 1.0, 1.0, 1.0), # シアン
            (1.0, 0.0, 1.0, 1.0), # マゼンタ
        ]

        for i in range(6):
            mat = mesh.materials[i]
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf and 'Base Color' in bsdf.inputs:
                if self.target_random_color:
                    bsdf.inputs['Base Color'].default_value = colors[i]
                else:
                    bsdf.inputs['Base Color'].default_value = self.target_color
            
        for i, poly in enumerate(mesh.polygons):
            poly.material_index = i % 6 if self.target_random_color else 0

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_target_anim(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not tgt: return
    
    if tgt.animation_data:
        tgt.animation_data.action = None
        
    start_f = self.anim_start_frame
    end_f = self.anim_end_frame
    delta_f = end_f - start_f
    speed = self.target_anim_speed_mult
        
    if self.cam_target_mode == 'POINT':
        tgt.location = self.cam_target_loc
        
    elif self.cam_target_mode == 'LINE':
        s_loc = mathutils.Vector(self.target_start_loc)
        e_loc = mathutils.Vector(self.target_end_loc)
        
        if delta_f <= 0:
            tgt.location = s_loc
            tgt.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                pp = progress % 2.0
                if pp < 0: pp += 2.0 # マイナス対応
                ratio = pp if pp <= 1.0 else 2.0 - pp
                
                tgt.location = s_loc.lerp(e_loc, ratio)
                tgt.keyframe_insert(data_path="location", frame=f)
                
        if tgt.animation_data and tgt.animation_data.action:
            act = tgt.animation_data.action
            fcs = getattr(act, "fcurves", None)
            if fcs:
                for fc in fcs:
                    if fc.data_path == 'location':
                        for kp in fc.keyframe_points:
                            kp.interpolation = 'LINEAR'
                            
    elif self.cam_target_mode == 'CIRCLE':
        rot_mat = mathutils.Euler(self.target_circle_rot, 'XYZ').to_matrix()
        center = mathutils.Vector(self.target_circle_center)
        radius = self.target_circle_radius
        start_rad = math.radians(self.target_circle_start_angle)
        end_rad = math.radians(self.target_circle_end_angle)
        
        if delta_f <= 0:
            local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
            tgt.location = center + rot_mat @ local_pos
            tgt.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                t = start_rad + (end_rad - start_rad) * progress
                local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
                tgt.location = center + rot_mat @ local_pos
                tgt.keyframe_insert(data_path="location", frame=f)
                
        if tgt.animation_data and tgt.animation_data.action:
            act = tgt.animation_data.action
            fcs = getattr(act, "fcurves", None)
            if fcs:
                for fc in fcs:
                    if fc.data_path == 'location':
                        for kp in fc.keyframe_points:
                            kp.interpolation = 'LINEAR'
                            
    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_anim(self, context):
    update_target_anim(self, context)
    
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    if not root: return
        
    if root.animation_data:
        root.animation_data.action = None
            
    start_f = self.anim_start_frame
    end_f = self.anim_end_frame
    delta_f = end_f - start_f
    
    # 1. カメラ(Root)の位置ベイク
    if self.cam_move_mode == 'FIXED':
        root.location = self.cam_fixed_loc
        root.keyframe_insert(data_path="location", frame=start_f)
        
    elif self.cam_move_mode == 'LINE':
        s_loc = mathutils.Vector(self.cam_start_loc)
        e_loc = mathutils.Vector(self.cam_end_loc)
        speed = self.cam_anim_speed_mult
        
        if delta_f <= 0:
            root.location = s_loc
            root.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                pp = progress % 2.0
                if pp < 0: pp += 2.0 # マイナス対応
                ratio = pp if pp <= 1.0 else 2.0 - pp
                
                root.location = s_loc.lerp(e_loc, ratio)
                root.keyframe_insert(data_path="location", frame=f)
                
    elif self.cam_move_mode == 'CIRCLE':
        rot_mat = mathutils.Euler(self.cam_circle_rot, 'XYZ').to_matrix()
        center = mathutils.Vector(self.cam_circle_center)
        radius = self.cam_circle_radius
        start_rad = math.radians(self.cam_circle_start_angle)
        end_rad = math.radians(self.cam_circle_end_angle)
        speed = self.cam_anim_speed_mult
        
        if delta_f <= 0:
            local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
            root.location = center + rot_mat @ local_pos
            root.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                t = start_rad + (end_rad - start_rad) * progress
                local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
                root.location = center + rot_mat @ local_pos
                root.keyframe_insert(data_path="location", frame=f)

    # 2. カメラ(Root)の回転ベイク (ターゲットが無い場合のみ)
    if self.cam_target_mode == 'NONE':
        root.rotation_mode = 'XYZ'
        if delta_f <= 0:
            root.rotation_euler = self.cam_rot_start
            root.keyframe_insert(data_path="rotation_euler", frame=start_f)
        else:
            speed_u = math.radians(self.cam_rot_speed[0])
            speed_v = math.radians(self.cam_rot_speed[1])
            speed_w = math.radians(self.cam_rot_speed[2])

            start_rot = self.cam_rot_start
            end_rot = (
                start_rot[0] + speed_u * delta_f,
                start_rot[1] + speed_v * delta_f,
                start_rot[2] + speed_w * delta_f
            )

            root.rotation_euler = start_rot
            root.keyframe_insert(data_path="rotation_euler", frame=start_f)
            root.rotation_euler = end_rot
            root.keyframe_insert(data_path="rotation_euler", frame=end_f)

    # Fcurveの線形補間適用
    if root.animation_data and root.animation_data.action:
        act = root.animation_data.action
        fcs = getattr(act, "fcurves", None)
        if fcs:
            for fc in fcs:
                for kp in fc.keyframe_points:
                    kp.interpolation = 'LINEAR'

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_root_constraint(self, context):
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    tgt_obj = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not root: return

    tt = next((c for c in root.constraints if c.type == 'TRACK_TO'), None)
    
    if self.cam_target_mode == 'NONE':
        if tt: root.constraints.remove(tt)
        update_cam_anim(self, context) 
    else:
        if not tt: tt = root.constraints.new(type='TRACK_TO')
        tt.track_axis = 'TRACK_NEGATIVE_Z'
        tt.up_axis = 'UP_Y'
        
        if self.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
            if tgt_obj:
                tt.target = tgt_obj
            update_target_anim(self, context)
        elif self.cam_target_mode == 'OBJECT':
            tt.target = self.cam_target_obj

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_root_loc(self, context):
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    if root: root.location = self.cam_fixed_loc

def update_cam_local_offset_rot(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam: cam.rotation_euler = self.cam_local_offset_rot

def update_cam_shield(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if not cam: return
    shield = next((c for c in cam.children if SHIELD_NAME in c.name), None)
    cutter = next((c for c in cam.children if CUTTER_NAME in c.name), None)
    cutter_back = next((c for c in cam.children if CUTTER_BACK_NAME in c.name), None)
    
    if shield:
        shield.scale = (self.cam_shield_radius,) * 3
        shield.hide_viewport = not self.show_cam_shield
    
    z_scale = self.cam_shield_radius * 3.0 
    
    if cutter:
        r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_front)
        cutter.scale = (r, r, z_scale)
        cutter.display_type = 'WIRE'
        cutter.hide_render = True
        
    if cutter_back:
        r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_back)
        cutter_back.scale = (r, r, z_scale)
        cutter_back.display_type = 'WIRE'
        cutter_back.hide_render = True
        
    if shield and shield.data.materials:
        mat = shield.data.materials[0]
        mat.diffuse_color = (*self.cam_shield_color[:3], self.cam_shield_alpha)
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs: 
                    bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color[:3], 1.0)
                if 'Alpha' in bsdf.inputs: 
                    bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha

def update_back_cam_shield(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
    if not cam: return
    shield = next((c for c in cam.children if BACK_SHIELD_NAME in c.name), None)
    cutter = next((c for c in cam.children if BACK_CUTTER_NAME in c.name), None)
    cutter_back = next((c for c in cam.children if BACK_CUTTER_BACK_NAME in c.name), None)
    
    if shield:
        shield.scale = (self.back_cam_shield_radius,) * 3
        shield.hide_viewport = not self.show_back_cam_shield
    
    z_scale = self.back_cam_shield_radius * 3.0 
    
    if cutter:
        r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_front)
        cutter.scale = (r, r, z_scale)
        cutter.display_type = 'WIRE'
        cutter.hide_render = True
        
    if cutter_back:
        r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_back)
        cutter_back.scale = (r, r, z_scale)
        cutter_back.display_type = 'WIRE'
        cutter_back.hide_render = True
        
    if shield and shield.data.materials:
        mat = shield.data.materials[0]
        mat.diffuse_color = (*self.back_cam_shield_color[:3], self.back_cam_shield_alpha)
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs: 
                    bsdf.inputs['Base Color'].default_value = (*self.back_cam_shield_color[:3], 1.0)
                if 'Alpha' in bsdf.inputs: 
                    bsdf.inputs['Alpha'].default_value = self.back_cam_shield_alpha

def update_cam_back_dist(self, context):
    cam_back = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
    if cam_back:
        cam_back.location.z = self.cam_back_dist

def update_active_camera(self, context):
    cam = None
    if self.active_camera == 'MAIN':
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    elif self.active_camera == 'BACK':
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
        
    if cam:
        context.scene.camera = cam
        cam.data.show_passepartout = True
        cam.data.passepartout_alpha = 0.8
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                area.spaces.active.region_3d.view_perspective = 'CAMERA'

def update_camera_color(self, context):
    try:
        bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
    except Exception:
        pass

def update_camera_preset_func(self, context):
    for p in CAMERA_PRESETS:
        if p[0] == self.camera_preset: 
            self.camera_color = p[3]
            break

def update_pupil_window(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if not obj: return
    
    obj.hide_viewport = not self.pupil_window_show
    obj.hide_render = not self.pupil_window_show
    
    if not self.pupil_window_show:
        return
        
    obj.location = (0.0, 0.0, -self.pupil_window_dist)
    
    mesh = obj.data
    mesh.clear_geometry()
    
    maj_s = self.pupil_window_segments
    min_s = 16
    
    val = self.pupil_window_radius
    if maj_s == 4:
        R = val / math.sqrt(2.0)
    else:
        R = val
        
    r = self.pupil_window_thickness
    
    verts = []
    faces = []
    
    for i in range(maj_s):
        u = i * 2.0 * math.pi / maj_s
        for j in range(min_s):
            v = j * 2.0 * math.pi / min_s
            x = (R + r * math.cos(v)) * math.cos(u)
            y = (R + r * math.cos(v)) * math.sin(u)
            z = r * math.sin(v)
            verts.append((x, y, z))
            
    for i in range(maj_s):
        for j in range(min_s):
            next_i = (i + 1) % maj_s
            next_j = (j + 1) % min_s
            v0 = i * min_s + j
            v1 = i * min_s + next_j
            v2 = next_i * min_s + next_j
            v3 = next_i * min_s + j
            faces.append((v0, v1, v2, v3))
            
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_pupil_segments(self, context):
    if self.pupil_window_segments == 4:
        self.pupil_window_rot = (0.0, 0.0, math.radians(45.0))
    else:
        self.pupil_window_rot = (0.0, 0.0, math.radians(90.0))
    update_pupil_window(self, context)

def update_pupil_window_rot(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if obj:
        obj.rotation_euler = self.pupil_window_rot

def update_pupil_window_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = self.pupil_window_color
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs:
                    bsdf.inputs['Base Color'].default_value = self.pupil_window_color

# --- Property Group ---
class ADDON_Properties(bpy.types.PropertyGroup):
    custom_grid_scale: bpy.props.FloatProperty(name=LABEL_SIZE, default=CUSTOM_GRID_SCALE_DEFAULT, min=0.001)
    
    # カメラの移動設定
    cam_move_mode: bpy.props.EnumProperty(
        name="カメラ(Root)の移動",
        items=[('FIXED', "固定位置", ""), ('LINE', "線分移動 (アニメ)", ""), ('CIRCLE', "円周移動 (アニメ)", "")],
        default=CAM_MOVE_MODE_DEFAULT, update=update_cam_anim
    )
    cam_fixed_loc: bpy.props.FloatVectorProperty(name=f"固定座標", default=(CAM_LOC_U_DEFAULT, CAM_LOC_V_DEFAULT, CAM_LOC_W_DEFAULT), update=update_cam_anim)
    cam_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=CAM_START_LOC_DEFAULT, update=update_cam_anim)
    cam_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=CAM_END_LOC_DEFAULT, update=update_cam_anim)
    
    cam_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=CAM_CIRCLE_CENTER_DEFAULT, update=update_cam_anim)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=CAM_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_cam_anim)
    cam_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=CAM_CIRCLE_ROT_DEFAULT, update=update_cam_anim)
    cam_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=CAM_CIRCLE_START_ANGLE_DEFAULT, update=update_cam_anim)
    cam_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=CAM_CIRCLE_END_ANGLE_DEFAULT, update=update_cam_anim)
    
    cam_anim_speed_mult: bpy.props.FloatProperty(name="カメラ移動速度(倍率)", default=CAM_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_cam_anim)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点設定",
        items=[
            ('NONE', "オフ (速度ベース回転)", ""), 
            ('POINT', "指定座標 (固定)", ""), 
            ('LINE', "線分移動 (アニメ)", ""), 
            ('CIRCLE', "円周移動 (アニメ)", ""), 
            ('OBJECT', "別オブジェクト", "")
        ],
        default=CAM_TARGET_MODE_DEFAULT, update=update_cam_root_constraint
    )
    cam_target_loc: bpy.props.FloatVectorProperty(name=f"注視点", default=(CAM_TGT_U_DEFAULT, CAM_TGT_V_DEFAULT, CAM_TGT_W_DEFAULT), update=update_cam_root_constraint)
    cam_target_obj: bpy.props.PointerProperty(type=bpy.types.Object, name="対象オブジェクト", update=update_cam_root_constraint)
    
    target_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=TARGET_START_DEFAULT, update=update_target_anim)
    target_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=TARGET_END_DEFAULT, update=update_target_anim)
    
    target_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=TARGET_CIRCLE_CENTER_DEFAULT, update=update_target_anim)
    target_circle_radius: bpy.props.FloatProperty(name="円の半径", default=TARGET_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_target_anim)
    target_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=TARGET_CIRCLE_ROT_DEFAULT, update=update_target_anim)
    target_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=TARGET_CIRCLE_START_ANGLE_DEFAULT, update=update_target_anim)
    target_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=TARGET_CIRCLE_END_ANGLE_DEFAULT, update=update_target_anim)

    target_size: bpy.props.FloatProperty(name="立方体のサイズ", default=TARGET_SIZE_DEFAULT, min=0.01, update=update_target_visuals)
    target_color: bpy.props.FloatVectorProperty(name="立方体の色", subtype='COLOR', size=4, default=TARGET_COLOR_DEFAULT, min=0.0, max=1.0, update=update_target_visuals)
    target_random_color: bpy.props.BoolProperty(name="6面をランダムな色にする", default=TARGET_RANDOM_COLOR_DEFAULT, update=update_target_visuals)
    target_anim_speed_mult: bpy.props.FloatProperty(name="立方体移動速度 (倍率)", description="マイナス値で逆行/逆回転します", default=TARGET_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_target_anim)

    cam_local_offset_rot: bpy.props.FloatVectorProperty(name="角度オフセット", subtype='EULER', default=(CAM_OFFSET_U_DEFAULT, CAM_OFFSET_V_DEFAULT, CAM_OFFSET_W_DEFAULT), update=update_cam_local_offset_rot)
    
    anim_start_frame: bpy.props.IntProperty(name=f"{LABEL_START} F", default=ANIM_START_FRAME_DEFAULT, min=0, update=update_cam_anim)
    anim_end_frame: bpy.props.IntProperty(name=f"{LABEL_END} F", default=ANIM_END_FRAME_DEFAULT, min=1, update=update_cam_anim)
    cam_rot_start: bpy.props.FloatVectorProperty(name="初期回転", subtype='EULER', default=(CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START), update=update_cam_anim)
    cam_rot_speed: bpy.props.FloatVectorProperty(name="回転速度", default=(CAM_ROT_SPEED_U_DEFAULT, CAM_ROT_SPEED_V_DEFAULT, CAM_ROT_SPEED_W_DEFAULT), update=update_cam_anim)
    
    show_cam_obj: bpy.props.BoolProperty(name="カメラ本体表示", default=SHOW_CAM_OBJ_DEFAULT, update=update_visibility)
    show_target_empty: bpy.props.BoolProperty(name="Empty", default=SHOW_TARGET_EMPTY_DEFAULT, update=update_visibility)
    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
    
    cam_back_dist: bpy.props.FloatProperty(name="背後カメラの距離", default=CAM_BACK_DIST_DEFAULT, min=1.0, max=100.0, update=update_cam_back_dist)
    
    active_camera: bpy.props.EnumProperty(
        name="表示カメラ切り替え",
        items=[('MAIN', "メインカメラ", "包み内 カメラ"), ('BACK', "背後霊カメラ", "背後 観察カメラ")],
        default=ACTIVE_CAMERA_DEFAULT, update=update_active_camera
    )
    
    camera_preset: bpy.props.EnumProperty(
        name="カメラ色プリセット", 
        items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS], 
        default=CAMERA_PRESET_DEFAULT, 
        update=update_camera_preset_func
    )
    camera_color: bpy.props.FloatVectorProperty(
        name="カメラテーマ色", 
        subtype='COLOR', size=3, min=0.0, max=1.0, 
        default=CAMERA_COLOR_DEFAULT, 
        update=update_camera_color
    )
    
    show_cam_shield: bpy.props.BoolProperty(name="包み表示", default=SHOW_CAM_SHIELD_DEFAULT, update=update_cam_shield)
    cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_cam_shield)
    cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
    cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_cam_shield)
    cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_SHIELD[:3], min=0.0, max=1.0, update=update_cam_shield)
    cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)

    show_back_cam_shield: bpy.props.BoolProperty(name="背後 包み表示", default=SHOW_BACK_CAM_SHIELD_DEFAULT, update=update_back_cam_shield)
    back_cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=BACK_CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_back_cam_shield)
    back_cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
    back_cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_back_cam_shield)
    back_cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_BACK_SHIELD[:3], min=0.0, max=1.0, update=update_back_cam_shield)
    back_cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_BACK_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)

    pupil_window_show: bpy.props.BoolProperty(name="瞳孔窓面の表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
    pupil_window_dist: bpy.props.FloatProperty(name="距離 (カメラ前方)", default=PUPIL_DIST_DEFAULT, min=1.0, max=100.0, update=update_pupil_window)
    pupil_window_radius: bpy.props.FloatProperty(name="サイズ", default=PUPIL_RADIUS_DEFAULT, min=0.1, update=update_pupil_window)
    pupil_window_thickness: bpy.props.FloatProperty(name="枠の太さ", default=PUPIL_THICKNESS_DEFAULT, min=0.01, update=update_pupil_window)
    pupil_window_segments: bpy.props.IntProperty(name="角数 (3=三角, 4=四角)", default=PUPIL_SEGMENTS_DEFAULT, min=3, max=32, update=update_pupil_segments)
    pupil_window_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_PUPIL, min=0.0, max=1.0, update=update_pupil_window_mat)
    pupil_window_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(PUPIL_ROT_X_DEFAULT, PUPIL_ROT_Y_DEFAULT, PUPIL_ROT_Z_DEFAULT), update=update_pupil_window_rot)

    info_proj_mode: bpy.props.StringProperty(name="投影モード", default="-")
    info_active_camera_name: bpy.props.StringProperty(name="表示カメラ", default="-")
    info_center_loc: bpy.props.FloatVectorProperty(name="画面中央位置")
    info_view_loc: bpy.props.FloatVectorProperty(name="視座位置")
    info_view_dir: bpy.props.FloatVectorProperty(name="視線方向")
    info_view_width: bpy.props.FloatProperty(name="画面水平の大きさ")
    info_view_height: bpy.props.FloatProperty(name="画面上下の大きさ")
    info_view_dist: bpy.props.FloatProperty(name="視座からの距離")
    
    info_target_path: bpy.props.StringProperty(name="ターゲット軌道情報", default="軌道: なし")

# --- Operators ---
class VIEW3D_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_reset_view"
    bl_label = "選択物を中央にする"
    def execute(self, context):
        rv3d = get_rv3d(context)
        if rv3d and context.active_object:
            rv3d.view_location = context.active_object.location
        return {'FINISHED'}

class OBJECT_OT_create_cam_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_create_cam_rig"
    bl_label = "カメラリグ作成"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        for n in [OBJ_NAME_ROOT, OBJ_NAME_CAMERA, OBJ_NAME_CAMERA_BACK, OBJ_NAME_TARGET, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME, OBJ_NAME_PUPIL, BACK_SHIELD_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
            o = bpy.data.objects.get(n)
            if o: bpy.data.objects.remove(o, do_unlink=True)
            
        main_col = bpy.data.collections.get(COLLECTION_NAME_CAM) or bpy.data.collections.new(COLLECTION_NAME_CAM)
        if main_col.name not in context.scene.collection.children: context.scene.collection.children.link(main_col)
        
        sub_col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM) or bpy.data.collections.new(COLLECTION_SUB_NAME_CAM)
        if sub_col.name not in main_col.children: main_col.children.link(sub_col)

        m_tgt = bpy.data.meshes.new(f"{OBJ_NAME_TARGET}_Mesh")
        bm_t = bmesh.new()
        bmesh.ops.create_cube(bm_t, size=1.0)
        bm_t.to_mesh(m_tgt)
        bm_t.free()
        
        tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, m_tgt)
        sub_col.objects.link(tgt_obj)
        
        update_target_visuals(props, context)

        root_obj = bpy.data.objects.new(OBJ_NAME_ROOT, None)
        root_obj.empty_display_type = 'PLAIN_AXES'
        root_obj.location = props.cam_fixed_loc
        root_obj.rotation_mode = 'XYZ'
        root_obj.rotation_euler = props.cam_rot_start
        sub_col.objects.link(root_obj)

        cam_data = bpy.data.cameras.new(OBJ_NAME_CAMERA)
        cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
        cam_obj.parent = root_obj
        cam_obj.location = (0, 0, 0)
        cam_obj.rotation_euler = props.cam_local_offset_rot
        sub_col.objects.link(cam_obj)
        context.scene.camera = cam_obj
        
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = tgt_obj
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        cam_back_data = bpy.data.cameras.new(OBJ_NAME_CAMERA_BACK)
        cam_back_data.show_passepartout = True
        cam_back_data.passepartout_alpha = 0.8
        cam_back_obj = bpy.data.objects.new(OBJ_NAME_CAMERA_BACK, cam_back_data)
        cam_back_obj.parent = cam_obj
        cam_back_obj.location = (0.0, 0.0, props.cam_back_dist)
        cam_back_obj.rotation_mode = 'XYZ'
        cam_back_obj.rotation_euler = (0.0, 0.0, 0.0) 
        sub_col.objects.link(cam_back_obj)

        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
        m = bpy.data.meshes.new(SHIELD_NAME)
        bm.to_mesh(m)
        bm.free()
        s_obj = bpy.data.objects.new(SHIELD_NAME, m)
        s_obj.parent = cam_obj
        sub_col.objects.link(s_obj)

        bm_c = bmesh.new()
        bmesh.ops.create_cone(bm_c, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_c.verts: v.co.z -= 0.5
        m_c = bpy.data.meshes.new(CUTTER_NAME)
        bm_c.to_mesh(m_c)
        bm_c.free()
        c_obj = bpy.data.objects.new(CUTTER_NAME, m_c)
        c_obj.parent = cam_obj
        c_obj.hide_viewport = not props.show_target_empty
        sub_col.objects.link(c_obj)

        bm_b = bmesh.new()
        bmesh.ops.create_cone(bm_b, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_b.verts: v.co.z += 0.5
        m_b = bpy.data.meshes.new(CUTTER_BACK_NAME)
        bm_b.to_mesh(m_b)
        bm_b.free()
        cb_obj = bpy.data.objects.new(CUTTER_BACK_NAME, m_b)
        cb_obj.parent = cam_obj
        cb_obj.hide_viewport = not props.show_target_empty
        sub_col.objects.link(cb_obj)

        mod_front = s_obj.modifiers.new("Hole_Front", 'BOOLEAN')
        mod_front.object = c_obj
        mod_front.solver = 'FLOAT'
        
        mod_back = s_obj.modifiers.new("Hole_Back", 'BOOLEAN')
        mod_back.object = cb_obj
        mod_back.solver = 'FLOAT'

        mat = bpy.data.materials.new(MAT_NAME_SHIELD)
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
        s_obj.data.materials.append(mat)
        s_obj.show_transparent = True

        bm_bs = bmesh.new()
        bmesh.ops.create_uvsphere(bm_bs, u_segments=32, v_segments=16, radius=1.0)
        m_bs = bpy.data.meshes.new(BACK_SHIELD_NAME)
        bm_bs.to_mesh(m_bs)
        bm_bs.free()
        s_obj_b = bpy.data.objects.new(BACK_SHIELD_NAME, m_bs)
        s_obj_b.parent = cam_back_obj
        sub_col.objects.link(s_obj_b)

        bm_bc = bmesh.new()
        bmesh.ops.create_cone(bm_bc, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_bc.verts: v.co.z -= 0.5
        m_bc = bpy.data.meshes.new(BACK_CUTTER_NAME)
        bm_bc.to_mesh(m_bc)
        bm_bc.free()
        c_obj_b = bpy.data.objects.new(BACK_CUTTER_NAME, m_bc)
        c_obj_b.parent = cam_back_obj
        c_obj_b.hide_viewport = not props.show_target_empty
        sub_col.objects.link(c_obj_b)

        bm_bb = bmesh.new()
        bmesh.ops.create_cone(bm_bb, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_bb.verts: v.co.z += 0.5
        m_bb = bpy.data.meshes.new(BACK_CUTTER_BACK_NAME)
        bm_bb.to_mesh(m_bb)
        bm_bb.free()
        cb_obj_b = bpy.data.objects.new(BACK_CUTTER_BACK_NAME, m_bb)
        cb_obj_b.parent = cam_back_obj
        cb_obj_b.hide_viewport = not props.show_target_empty
        sub_col.objects.link(cb_obj_b)

        mod_front_b = s_obj_b.modifiers.new("Hole_Front", 'BOOLEAN')
        mod_front_b.object = c_obj_b
        mod_front_b.solver = 'FLOAT'
        
        mod_back_b = s_obj_b.modifiers.new("Hole_Back", 'BOOLEAN')
        mod_back_b.object = cb_obj_b
        mod_back_b.solver = 'FLOAT'

        mat_b = bpy.data.materials.new(MAT_NAME_BACK_SHIELD)
        mat_b.use_nodes = True
        mat_b.blend_method = 'BLEND'
        s_obj_b.data.materials.append(mat_b)
        s_obj_b.show_transparent = True

        m_pupil = bpy.data.meshes.new(f"{OBJ_NAME_PUPIL}_Mesh")
        o_pupil = bpy.data.objects.new(OBJ_NAME_PUPIL, m_pupil)
        o_pupil.parent = cam_obj
        o_pupil.rotation_mode = 'XYZ'
        sub_col.objects.link(o_pupil)
        
        mat_pupil = bpy.data.materials.new(MAT_NAME_PUPIL)
        mat_pupil.use_nodes = True
        mat_pupil.blend_method = 'BLEND'
        o_pupil.data.materials.append(mat_pupil)
        
        update_pupil_window(props, context)
        update_pupil_window_rot(props, context)
        update_pupil_window_mat(props, context)

        update_cam_root_constraint(props, context)
        update_visibility(props, context)
        update_cam_shield(props, context)
        update_back_cam_shield(props, context)
        update_cam_anim(props, context)
        
        update_camera_color(props, context)
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
                        space.region_3d.view_perspective = 'CAMERA'
                        space.lock_camera = False
        
        self.report({'INFO'}, "カメラを作成し、カメラビュー&マテリアルプレビューに切り替えました")
        return {'FINISHED'}

class OBJECT_OT_sync_from_scene(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_sync_from_scene"
    bl_label = "シーンから取得"
    bl_description = "Blender本体のタイムライン(開始/終了フレーム)をこのアドオンの設定にコピーします"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.anim_start_frame = context.scene.frame_start
        props.anim_end_frame = context.scene.frame_end
        self.report({'INFO'}, f"シーンから取得しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
        return {'FINISHED'}

class OBJECT_OT_sync_to_scene(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_sync_to_scene"
    bl_label = "シーンへ適用"
    bl_description = "このアドオンで設定した開始/終了フレームを、Blender本体のタイムラインに適用します"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        context.scene.frame_start = props.anim_start_frame
        context.scene.frame_end = props.anim_end_frame
        self.report({'INFO'}, f"タイムラインのフレーム範囲を更新しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
        return {'FINISHED'}

class OBJECT_OT_apply_cam_anim(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_apply_cam_anim"
    bl_label = "アニメーションを適用(更新)"
    bl_description = "現在の設定で回転・移動アニメーションのキーフレームを打ち直します"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        update_cam_anim(props, context)
        self.report({'INFO'}, "アニメーションキーフレームを更新しました")
        return {'FINISHED'}

class OBJECT_OT_switch_cam_main(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_switch_cam_main"
    bl_label = "包み内 カメラ視点"
    bl_description = "本来の固定カメラをアクティブにしてカメラビューに切り替えます"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "包み内カメラ視点に切り替えました")
        else:
            self.report({'ERROR'}, "包み内カメラが見つかりません")
        return {'FINISHED'}

class OBJECT_OT_switch_cam_back(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_switch_cam_back"
    bl_label = "背後 観察視点"
    bl_description = "全体を見渡す背後カメラをアクティブにしてカメラビューに切り替えます"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "背後からの観察視点に切り替えました")
        else:
            self.report({'ERROR'}, "背後カメラが見つかりません。リグを再作成してください")
        return {'FINISHED'}

class OBJECT_OT_set_active_camera(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_set_active_camera"
    bl_label = "専用カメラをアクティブにする"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "専用カメラをアクティブに設定しました")
        else:
            self.report({'ERROR'}, "専用カメラが見つかりません。リグを作成してください")
        return {'FINISHED'}

class OBJECT_OT_update_camera_info(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_update_camera_info"
    bl_label = "情報更新"
    bl_description = "現在のビューから画面の情報を取得・更新します"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        rv3d = get_rv3d(context)
        
        if not rv3d:
            self.report({'WARNING'}, "3Dビューが見つかりません")
            return {'CANCELLED'}

        if rv3d.view_perspective == 'CAMERA': props.info_proj_mode = "カメラビュー"
        elif rv3d.view_perspective == 'PERSP': props.info_proj_mode = "透視投影"
        elif rv3d.view_perspective == 'ORTHO': props.info_proj_mode = "平行投影"
        else: props.info_proj_mode = rv3d.view_perspective
            
        dist = rv3d.view_distance
        props.info_view_dist = dist
        
        res_x = context.scene.render.resolution_x
        res_y = context.scene.render.resolution_y
        aspect = (res_x / res_y) if res_y != 0 else 1.0

        if rv3d.view_perspective == 'CAMERA' and context.scene.camera:
            cam = context.scene.camera
            
            if cam.name == OBJ_NAME_CAMERA:
                props.info_active_camera_name = "メインカメラ"
            elif cam.name == OBJ_NAME_CAMERA_BACK:
                props.info_active_camera_name = "背後霊カメラ"
            else:
                props.info_active_camera_name = f"その他 ({cam.name})"
                
            view_loc = cam.matrix_world.translation
            view_dir = cam.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
            center_loc = view_loc + view_dir * dist
            fov = cam.data.angle
            width = 2.0 * dist * math.tan(fov / 2.0)
            height = width / aspect
        else:
            props.info_active_camera_name = "なし (ビューポート)"
            center_loc = rv3d.view_location
            view_rot = rv3d.view_rotation
            view_dir = view_rot @ mathutils.Vector((0, 0, -1))
            view_loc = center_loc - view_dir * dist
            space = None
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    space = area.spaces.active
                    break
            lens = space.lens if space and hasattr(space, "lens") else 50.0
            fov = 2.0 * math.atan(36.0 / (2.0 * lens))
            width = 2.0 * dist * math.tan(fov / 2.0)
            height = width / aspect
            
        props.info_center_loc = center_loc
        props.info_view_loc = view_loc
        props.info_view_dir = view_dir
        props.info_view_width = width
        props.info_view_height = height
        
        props.info_target_path = "ターゲット軌道: なし"
        if props.cam_target_mode == 'LINE':
            s = props.target_start_loc
            e = props.target_end_loc
            props.info_target_path = f"ターゲット線分(往復 {props.target_anim_speed_mult:.1f}倍速): Start({s[0]:.1f}, {s[1]:.1f}, {s[2]:.1f}) -> End({e[0]:.1f}, {e[1]:.1f}, {e[2]:.1f})"
        elif props.cam_target_mode == 'CIRCLE':
            rot_mat = mathutils.Euler(props.target_circle_rot, 'XYZ').to_matrix()
            normal = rot_mat @ mathutils.Vector((0, 0, 1))
            c = props.target_circle_center
            d = -(normal.x * c[0] + normal.y * c[1] + normal.z * c[2])
            props.info_target_path = f"ターゲット平面({props.target_anim_speed_mult:.1f}倍速): {normal.x:.2f}x + {normal.y:.2f}y + {normal.z:.2f}z + ({d:.2f}) = 0"
            
        if props.cam_move_mode == 'LINE':
            cs = props.cam_start_loc
            ce = props.cam_end_loc
            props.info_target_path += f"\nカメラ移動(往復 {props.cam_anim_speed_mult:.1f}倍速): Start({cs[0]:.1f}, {cs[1]:.1f}, {cs[2]:.1f}) -> End({ce[0]:.1f}, {ce[1]:.1f}, {ce[2]:.1f})"
        elif props.cam_move_mode == 'CIRCLE':
            rot_mat_c = mathutils.Euler(props.cam_circle_rot, 'XYZ').to_matrix()
            normal_c = rot_mat_c @ mathutils.Vector((0, 0, 1))
            cc = props.cam_circle_center
            dc = -(normal_c.x * cc[0] + normal_c.y * cc[1] + normal_c.z * cc[2])
            props.info_target_path += f"\nカメラ平面({props.cam_anim_speed_mult:.1f}倍速): {normal_c.x:.2f}x + {normal_c.y:.2f}y + {normal_c.z:.2f}z + ({dc:.2f}) = 0"

        
        self.report({'INFO'}, "画面情報を更新しました")
        return {'FINISHED'}

class OBJECT_OT_copy_camera_info(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_copy_camera_info"
    bl_label = "情報をコピー"
    bl_description = "更新された画面情報をクリップボードにコピーします"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        info = (
            f"投影モード: {props.info_proj_mode}\n"
            f"表示カメラ: {props.info_active_camera_name}\n"
            f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}\n"
            f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}\n"
            f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}\n"
            f"画面水平の大きさ: {props.info_view_width:.1f}\n"
            f"画面上下の大きさ: {props.info_view_height:.1f}\n"
            f"視座からの距離: {props.info_view_dist:.1f}\n"
            f"{props.info_target_path}"
        )
        
        context.window_manager.clipboard = info
        self.report({'INFO'}, "画面情報をクリップボードにコピーしました")
        return {'FINISHED'}

class OBJECT_OT_export_addon(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_export_addon"
    bl_label = "現在の設定でアドオンを書き出す"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        source = get_addon_source()
        if not source:
            self.report({'ERROR'}, "ソースコードを取得できませんでした")
            return {'CANCELLED'}
            
        lines = source.split('\n')
        for i, line in enumerate(lines):
            # モードや表示の選択状態
            if line.startswith("CAM_MOVE_MODE_DEFAULT = "): lines[i] = f"CAM_MOVE_MODE_DEFAULT = '{props.cam_move_mode}'"
            elif line.startswith("CAM_TARGET_MODE_DEFAULT = "): lines[i] = f"CAM_TARGET_MODE_DEFAULT = '{props.cam_target_mode}'"
            elif line.startswith("ACTIVE_CAMERA_DEFAULT = "): lines[i] = f"ACTIVE_CAMERA_DEFAULT = '{props.active_camera}'"
            elif line.startswith("CAMERA_PRESET_DEFAULT = "): lines[i] = f"CAMERA_PRESET_DEFAULT = '{props.camera_preset}'"
            
            elif line.startswith("SHOW_CAM_OBJ_DEFAULT = "): lines[i] = f"SHOW_CAM_OBJ_DEFAULT = {props.show_cam_obj}"
            elif line.startswith("SHOW_TARGET_EMPTY_DEFAULT = "): lines[i] = f"SHOW_TARGET_EMPTY_DEFAULT = {props.show_target_empty}"
            elif line.startswith("SHOW_CAM_SHIELD_DEFAULT = "): lines[i] = f"SHOW_CAM_SHIELD_DEFAULT = {props.show_cam_shield}"
            elif line.startswith("SHOW_BACK_CAM_SHIELD_DEFAULT = "): lines[i] = f"SHOW_BACK_CAM_SHIELD_DEFAULT = {props.show_back_cam_shield}"
            
            # 各種設定値
            elif line.startswith("CAM_LOC_U_DEFAULT = "): lines[i] = f"CAM_LOC_U_DEFAULT = {props.cam_fixed_loc[0]:.4f}"
            elif line.startswith("CAM_LOC_V_DEFAULT = "): lines[i] = f"CAM_LOC_V_DEFAULT = {props.cam_fixed_loc[1]:.4f}"
            elif line.startswith("CAM_LOC_W_DEFAULT = "): lines[i] = f"CAM_LOC_W_DEFAULT = {props.cam_fixed_loc[2]:.4f}"
            
            elif line.startswith("CAM_START_LOC_DEFAULT = "): lines[i] = f"CAM_START_LOC_DEFAULT = ({props.cam_start_loc[0]:.4f}, {props.cam_start_loc[1]:.4f}, {props.cam_start_loc[2]:.4f})"
            elif line.startswith("CAM_END_LOC_DEFAULT = "): lines[i] = f"CAM_END_LOC_DEFAULT = ({props.cam_end_loc[0]:.4f}, {props.cam_end_loc[1]:.4f}, {props.cam_end_loc[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"CAM_CIRCLE_CENTER_DEFAULT = ({props.cam_circle_center[0]:.4f}, {props.cam_circle_center[1]:.4f}, {props.cam_circle_center[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"CAM_CIRCLE_RADIUS_DEFAULT = {props.cam_circle_radius:.4f}"
            elif line.startswith("CAM_CIRCLE_ROT_DEFAULT = "): lines[i] = f"CAM_CIRCLE_ROT_DEFAULT = ({props.cam_circle_rot[0]:.4f}, {props.cam_circle_rot[1]:.4f}, {props.cam_circle_rot[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_START_ANGLE_DEFAULT = {props.cam_circle_start_angle:.4f}"
            elif line.startswith("CAM_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_END_ANGLE_DEFAULT = {props.cam_circle_end_angle:.4f}"
            elif line.startswith("CAM_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"CAM_ANIM_SPEED_MULT_DEFAULT = {props.cam_anim_speed_mult:.4f}"
            
            elif line.startswith("CAM_TGT_U_DEFAULT = "): lines[i] = f"CAM_TGT_U_DEFAULT = {props.cam_target_loc[0]:.4f}"
            elif line.startswith("CAM_TGT_V_DEFAULT = "): lines[i] = f"CAM_TGT_V_DEFAULT = {props.cam_target_loc[1]:.4f}"
            elif line.startswith("CAM_TGT_W_DEFAULT = "): lines[i] = f"CAM_TGT_W_DEFAULT = {props.cam_target_loc[2]:.4f}"
            
            elif line.startswith("TARGET_START_DEFAULT = "): lines[i] = f"TARGET_START_DEFAULT = ({props.target_start_loc[0]:.4f}, {props.target_start_loc[1]:.4f}, {props.target_start_loc[2]:.4f})"
            elif line.startswith("TARGET_END_DEFAULT = "): lines[i] = f"TARGET_END_DEFAULT = ({props.target_end_loc[0]:.4f}, {props.target_end_loc[1]:.4f}, {props.target_end_loc[2]:.4f})"
            
            elif line.startswith("TARGET_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_CENTER_DEFAULT = ({props.target_circle_center[0]:.4f}, {props.target_circle_center[1]:.4f}, {props.target_circle_center[2]:.4f})"
            elif line.startswith("TARGET_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_RADIUS_DEFAULT = {props.target_circle_radius:.4f}"
            elif line.startswith("TARGET_CIRCLE_ROT_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_ROT_DEFAULT = ({props.target_circle_rot[0]:.4f}, {props.target_circle_rot[1]:.4f}, {props.target_circle_rot[2]:.4f})"
            elif line.startswith("TARGET_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_START_ANGLE_DEFAULT = {props.target_circle_start_angle:.4f}"
            elif line.startswith("TARGET_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_END_ANGLE_DEFAULT = {props.target_circle_end_angle:.4f}"

            elif line.startswith("TARGET_SIZE_DEFAULT = "): lines[i] = f"TARGET_SIZE_DEFAULT = {props.target_size:.4f}"
            elif line.startswith("TARGET_COLOR_DEFAULT = "): lines[i] = f"TARGET_COLOR_DEFAULT = ({props.target_color[0]:.4f}, {props.target_color[1]:.4f}, {props.target_color[2]:.4f}, 1.0)"
            elif line.startswith("TARGET_RANDOM_COLOR_DEFAULT = "): lines[i] = f"TARGET_RANDOM_COLOR_DEFAULT = {props.target_random_color}"
            elif line.startswith("TARGET_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"

            elif line.startswith("CAM_BACK_DIST_DEFAULT = "): lines[i] = f"CAM_BACK_DIST_DEFAULT = {props.cam_back_dist:.4f}"
            
            elif line.startswith("CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"CAM_SHIELD_RADIUS_DEFAULT = {props.cam_shield_radius:.4f}"
            elif line.startswith("CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.cam_shield_hole_ratio_front:.4f}"
            elif line.startswith("CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.cam_shield_hole_ratio_back:.4f}"
            elif line.startswith("ALPHA_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_SHIELD_DEFAULT = {props.cam_shield_alpha:.4f}"
            elif line.startswith("COLOR_SHIELD = "): lines[i] = f"COLOR_SHIELD = ({props.cam_shield_color[0]:.4f}, {props.cam_shield_color[1]:.4f}, {props.cam_shield_color[2]:.4f}, 1.0)"

            elif line.startswith("BACK_CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_RADIUS_DEFAULT = {props.back_cam_shield_radius:.4f}"
            elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.back_cam_shield_hole_ratio_front:.4f}"
            elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.back_cam_shield_hole_ratio_back:.4f}"
            elif line.startswith("ALPHA_BACK_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_BACK_SHIELD_DEFAULT = {props.back_cam_shield_alpha:.4f}"
            elif line.startswith("COLOR_BACK_SHIELD = "): lines[i] = f"COLOR_BACK_SHIELD = ({props.back_cam_shield_color[0]:.4f}, {props.back_cam_shield_color[1]:.4f}, {props.back_cam_shield_color[2]:.4f}, 1.0)"

            elif line.startswith("CAMERA_COLOR_DEFAULT = "): lines[i] = f"CAMERA_COLOR_DEFAULT = ({props.camera_color[0]:.4f}, {props.camera_color[1]:.4f}, {props.camera_color[2]:.4f})"

            elif line.startswith("ANIM_START_FRAME_DEFAULT = "): lines[i] = f"ANIM_START_FRAME_DEFAULT = {props.anim_start_frame}"
            elif line.startswith("ANIM_END_FRAME_DEFAULT = "): lines[i] = f"ANIM_END_FRAME_DEFAULT = {props.anim_end_frame}"
            
            elif line.startswith("CAM_ROT_U_START = "): lines[i] = f"CAM_ROT_U_START = {props.cam_rot_start[0]:.4f}"
            elif line.startswith("CAM_ROT_V_START = "): lines[i] = f"CAM_ROT_V_START = {props.cam_rot_start[1]:.4f}"
            elif line.startswith("CAM_ROT_W_START = "): lines[i] = f"CAM_ROT_W_START = {props.cam_rot_start[2]:.4f}"
            
            elif line.startswith("CAM_ROT_SPEED_U_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_U_DEFAULT = {props.cam_rot_speed[0]:.4f}"
            elif line.startswith("CAM_ROT_SPEED_V_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_V_DEFAULT = {props.cam_rot_speed[1]:.4f}"
            elif line.startswith("CAM_ROT_SPEED_W_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_W_DEFAULT = {props.cam_rot_speed[2]:.4f}"
            
            elif line.startswith("CAM_OFFSET_U_DEFAULT = "): lines[i] = f"CAM_OFFSET_U_DEFAULT = {props.cam_local_offset_rot[0]:.4f}"
            elif line.startswith("CAM_OFFSET_V_DEFAULT = "): lines[i] = f"CAM_OFFSET_V_DEFAULT = {props.cam_local_offset_rot[1]:.4f}"
            elif line.startswith("CAM_OFFSET_W_DEFAULT = "): lines[i] = f"CAM_OFFSET_W_DEFAULT = {props.cam_local_offset_rot[2]:.4f}"
            
            elif line.startswith("PUPIL_SHOW_DEFAULT = "): lines[i] = f"PUPIL_SHOW_DEFAULT = {props.pupil_window_show}"
            elif line.startswith("PUPIL_DIST_DEFAULT = "): lines[i] = f"PUPIL_DIST_DEFAULT = {props.pupil_window_dist:.4f}"
            elif line.startswith("PUPIL_RADIUS_DEFAULT = "): lines[i] = f"PUPIL_RADIUS_DEFAULT = {props.pupil_window_radius:.4f}"
            elif line.startswith("PUPIL_THICKNESS_DEFAULT = "): lines[i] = f"PUPIL_THICKNESS_DEFAULT = {props.pupil_window_thickness:.4f}"
            elif line.startswith("PUPIL_SEGMENTS_DEFAULT = "): lines[i] = f"PUPIL_SEGMENTS_DEFAULT = {props.pupil_window_segments}"
            elif line.startswith("COLOR_PUPIL = "): lines[i] = f"COLOR_PUPIL = ({props.pupil_window_color[0]:.4f}, {props.pupil_window_color[1]:.4f}, {props.pupil_window_color[2]:.4f}, 1.0)"
            elif line.startswith("PUPIL_ROT_X_DEFAULT = "): lines[i] = f"PUPIL_ROT_X_DEFAULT = {props.pupil_window_rot[0]:.4f}"
            elif line.startswith("PUPIL_ROT_Y_DEFAULT = "): lines[i] = f"PUPIL_ROT_Y_DEFAULT = {props.pupil_window_rot[1]:.4f}"
            elif line.startswith("PUPIL_ROT_Z_DEFAULT = "): lines[i] = f"PUPIL_ROT_Z_DEFAULT = {props.pupil_window_rot[2]:.4f}"

        new_text = '\n'.join(lines)
        
        import time
        t_name = f"Custom_Camera_Addon_{int(time.time())}.py"
        text_data = bpy.data.texts.new(name=t_name)
        text_data.write(new_text)
        
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR':
                area.spaces.active.text = text_data
                break
                
        self.report({'INFO'}, f"現在の設定を {t_name} に書き出しました")
        return {'FINISHED'}

# ===================================
# 9. パネル構成
# ===================================

class VIEW3D_PT_main(bpy.types.Panel):
    bl_label = "Viewport & Grid"
    bl_idname = f"{PREFIX.upper()}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    
    def draw(self, context):
        layout = self.layout
        layout.operator(f"view3d.{PREFIX}_reset_view", icon='ZOOM_ALL')

class VIEW3D_PT_camera(bpy.types.Panel):
    bl_label = "Fixed Camera Rig (一体化)"
    bl_idname = f"{PREFIX.upper()}_PT_camera"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        
        box = layout.box()
        box.label(text="1. カメラリグの配置 (移動)", icon='CAMERA_DATA')
        box.prop(props, "cam_move_mode", expand=True)
        col_loc = box.column(align=True)
        
        if props.cam_move_mode == 'FIXED':
            col_loc.prop(props, "cam_fixed_loc", index=0, text=f"{LABEL_LOC} {LABEL_U}")
            col_loc.prop(props, "cam_fixed_loc", index=1, text=f"{LABEL_LOC} {LABEL_V}")
            col_loc.prop(props, "cam_fixed_loc", index=2, text=f"{LABEL_LOC} {LABEL_W}")
        elif props.cam_move_mode == 'LINE':
            col_loc.label(text="出発点:")
            col_loc.prop(props, "cam_start_loc", index=0, text="X")
            col_loc.prop(props, "cam_start_loc", index=1, text="Y")
            col_loc.prop(props, "cam_start_loc", index=2, text="Z")
            col_loc.separator()
            col_loc.label(text="到達点:")
            col_loc.prop(props, "cam_end_loc", index=0, text="X")
            col_loc.prop(props, "cam_end_loc", index=1, text="Y")
            col_loc.prop(props, "cam_end_loc", index=2, text="Z")
            col_loc.separator()
            col_loc.prop(props, "cam_anim_speed_mult", slider=True)
        elif props.cam_move_mode == 'CIRCLE':
            col_loc.label(text="中心位置:")
            col_loc.prop(props, "cam_circle_center", index=0, text="X")
            col_loc.prop(props, "cam_circle_center", index=1, text="Y")
            col_loc.prop(props, "cam_circle_center", index=2, text="Z")
            col_loc.separator()
            col_loc.prop(props, "cam_circle_radius")
            col_loc.separator()
            col_loc.label(text="平面の傾き (XYZ):")
            col_loc.prop(props, "cam_circle_rot", index=0, text="X (Pitch)")
            col_loc.prop(props, "cam_circle_rot", index=1, text="Y (Roll)")
            col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Yaw)")
            col_loc.separator()
            col_loc.prop(props, "cam_circle_start_angle")
            col_loc.prop(props, "cam_circle_end_angle")
            col_loc.separator()
            col_loc.prop(props, "cam_anim_speed_mult", slider=True)
            
        box.separator()
        row_cam = box.row(align=True)
        row_cam.operator(f"object.{PREFIX}_create_cam_rig", icon='ADD')
        row_cam.operator(f"object.{PREFIX}_set_active_camera", text="", icon='RESTRICT_RENDER_OFF')
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if root and cam:
            box_tgt = layout.box()
            box_tgt.label(text="2. 注視点 (Track To) 設定", icon='TRACKING')
            box_tgt.prop(props, "cam_target_mode", expand=False)
            
            if props.cam_target_mode == 'POINT':
                col_tgt = box_tgt.column(align=True)
                col_tgt.prop(props, "cam_target_loc", index=0, text=f"注視点 {LABEL_U}")
                col_tgt.prop(props, "cam_target_loc", index=1, text=f"注視点 {LABEL_V}")
                col_tgt.prop(props, "cam_target_loc", index=2, text=f"注視点 {LABEL_W}")
                
            elif props.cam_target_mode == 'LINE':
                col_tgt = box_tgt.column(align=True)
                col_tgt.label(text="出発点:")
                col_tgt.prop(props, "target_start_loc", index=0, text="X")
                col_tgt.prop(props, "target_start_loc", index=1, text="Y")
                col_tgt.prop(props, "target_start_loc", index=2, text="Z")
                col_tgt.separator()
                col_tgt.label(text="到達点:")
                col_tgt.prop(props, "target_end_loc", index=0, text="X")
                col_tgt.prop(props, "target_end_loc", index=1, text="Y")
                col_tgt.prop(props, "target_end_loc", index=2, text="Z")
                
            elif props.cam_target_mode == 'CIRCLE':
                col_tgt = box_tgt.column(align=True)
                col_tgt.label(text="中心位置:")
                col_tgt.prop(props, "target_circle_center", index=0, text="X")
                col_tgt.prop(props, "target_circle_center", index=1, text="Y")
                col_tgt.prop(props, "target_circle_center", index=2, text="Z")
                col_tgt.separator()
                col_tgt.prop(props, "target_circle_radius")
                col_tgt.separator()
                col_tgt.label(text="平面の傾き (XYZ):")
                col_tgt.prop(props, "target_circle_rot", index=0, text="X (Pitch)")
                col_tgt.prop(props, "target_circle_rot", index=1, text="Y (Roll)")
                col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Yaw)")
                col_tgt.separator()
                col_tgt.prop(props, "target_circle_start_angle")
                col_tgt.prop(props, "target_circle_end_angle")
                
            elif props.cam_target_mode == 'OBJECT':
                box_tgt.prop(props, "cam_target_obj")

            if props.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
                col_tgt = box_tgt.column(align=True)
                col_tgt.separator()
                col_tgt.prop(props, "target_size")
                col_tgt.prop(props, "target_random_color")
                if not props.target_random_color:
                    col_tgt.prop(props, "target_color")
                    
            if props.cam_target_mode in ['LINE', 'CIRCLE']:
                col_tgt = box_tgt.column(align=True)
                col_tgt.separator()
                col_tgt.prop(props, "target_anim_speed_mult", slider=True)

            box_offset = layout.box()
            box_offset.label(text="補助: 注視点ベースの角度ズレ", icon='ORIENTATION_LOCAL')
            col_off = box_offset.column(align=True)
            col_off.prop(props, "cam_local_offset_rot", index=0, text=f"角度ズレ {LABEL_U} (Pitch)")
            col_off.prop(props, "cam_local_offset_rot", index=1, text=f"角度ズレ {LABEL_V} (Roll)")
            col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Yaw)")

            box_rot = layout.box()
            box_rot.label(text="3. アニメーション設定", icon='ANIM_DATA')
            if props.cam_target_mode != 'NONE':
                box_rot.label(text="※速度ベースのRoot回転は無効化されています", icon='INFO')
            
            if props.cam_target_mode == 'NONE':
                col_rot = box_rot.column(align=True)
                col_rot.label(text="Rootの速度ベース回転:")
                col_rot.prop(props, "cam_rot_start", index=0, text=f"初期 {LABEL_U} (Pitch)")
                col_rot.prop(props, "cam_rot_start", index=1, text=f"初期 {LABEL_V} (Roll)")
                col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Yaw)")
                col_rot.separator()
                col_rot.prop(props, "cam_rot_speed", index=0, text=f"速度 {LABEL_U} (度/F)")
                col_rot.prop(props, "cam_rot_speed", index=1, text=f"速度 {LABEL_V} (度/F)")
                col_rot.prop(props, "cam_rot_speed", index=2, text=f"速度 {LABEL_W} (度/F)")
                row_hint = box_rot.row()
                row_hint.label(text="方向ガイド (UVW):", icon='QUESTION')
                col_hint = box_rot.column(align=True)
                col_hint.scale_y = 0.8
                col_hint.label(text=f"{LABEL_U}+: 下向 / {LABEL_U}-: 上向")
                col_hint.label(text=f"{LABEL_V}+: 左傾 / {LABEL_V}-: 右傾")
                col_hint.label(text=f"{LABEL_W}+: 左向 / {LABEL_W}-: 右向")
                box_rot.separator()
            
            col_f = box_rot.column(align=True)
            row_f = col_f.row(align=True)
            row_f.prop(props, "anim_start_frame")
            row_f.prop(props, "anim_end_frame")
            
            row_sync = col_f.row(align=True)
            row_sync.operator(f"object.{PREFIX}_sync_from_scene", icon='IMPORT')
            row_sync.operator(f"object.{PREFIX}_sync_to_scene", icon='EXPORT')
            
            box_rot.separator()
            
            col_btn = box_rot.column(align=True)
            col_btn.operator(f"object.{PREFIX}_apply_cam_anim", icon='KEYINGSET')
            if not context.screen.is_animation_playing:
                col_btn.operator("screen.animation_play", text="アニメーションを再生", icon='PLAY')
            else:
                col_btn.operator("screen.animation_play", text="アニメーションを停止", icon='PAUSE')
                
            box_rot.separator()

            # --- 視点切り替えボタン & 背後カメラの距離 ---
            col_switch = box_rot.column(align=True)
            row_switch = col_switch.row(align=True)
            row_switch.prop(props, "active_camera", expand=True)
            col_switch.prop(props, "cam_back_dist", text="背後カメラの距離", slider=True)
            box_rot.separator()

            # --- 画面情報表示と更新・コピー機能 ---
            box_info = box_rot.box()
            row_info_head = box_info.row(align=True)
            row_info_head.label(text="画面情報", icon='INFO')
            row_info_head.operator(f"object.{PREFIX}_update_camera_info", text="情報更新", icon='FILE_REFRESH')
            
            box_info.label(text=f"投影モード: {props.info_proj_mode}")
            box_info.label(text=f"表示カメラ: {props.info_active_camera_name}")
            box_info.label(text=f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}")
            box_info.label(text=f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}")
            box_info.label(text=f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}")
            box_info.label(text=f"画面水平の大きさ: {props.info_view_width:.1f}")
            box_info.label(text=f"画面上下の大きさ: {props.info_view_height:.1f}")
            box_info.label(text=f"視座からの距離: {props.info_view_dist:.1f}")
            box_info.separator()
            
            for line in props.info_target_path.split('\n'):
                box_info.label(text=line, icon='EDITMODE_HLT')
            
            box_info.operator(f"object.{PREFIX}_copy_camera_info", icon='COPYDOWN')

            # --- 4. メインカメラ 包み・線画設定 ---
            box_vis = layout.box()
            box_vis.label(text="4. カメラ色(共通テーマ) & メイン包み", icon='RESTRICT_VIEW_OFF')
            
            row_disp = box_vis.row(align=True)
            row_disp.prop(props, "show_cam_obj", text="カメラ")
            row_disp.prop(props, "show_target_empty", text="Empty") 
            row_disp.prop(props, "show_cam_shield", text="包み表示")
            
            col_vis = box_vis.column(align=True)
            row_cam_col = col_vis.row(align=True)
            row_cam_col.prop(props, "camera_preset", text="")
            row_cam_col.prop(props, "camera_color", text="")
            col_vis.separator()

            if props.show_cam_shield:
                col_vis.label(text="メインカメラ 包み設定", icon='MESH_UVSPHERE')
                col_vis.prop(props, "cam_shield_radius")
                col_vis.prop(props, "cam_shield_hole_ratio_front")
                col_vis.prop(props, "cam_shield_hole_ratio_back")
                col_vis.separator()
                col_vis.prop(props, "cam_shield_color")
                col_vis.prop(props, "cam_shield_alpha", slider=True)

            # --- 5. 背後霊カメラ 包み設定 ---
            box_back = layout.box()
            box_back.label(text="5. 背後霊カメラ 包み設定", icon='OUTLINER_OB_CAMERA')
            
            row_back_disp = box_back.row(align=True)
            row_back_disp.prop(props, "show_back_cam_shield", text="背後包み表示")
            
            col_back = box_back.column(align=True)

            if props.show_back_cam_shield:
                col_back.separator()
                col_back.prop(props, "back_cam_shield_radius")
                col_back.prop(props, "back_cam_shield_hole_ratio_front")
                col_back.prop(props, "back_cam_shield_hole_ratio_back")
                col_back.separator()
                col_back.prop(props, "back_cam_shield_color")
                col_back.prop(props, "back_cam_shield_alpha", slider=True)
                
            # --- 6. 瞳孔窓面 設定 ---
            box_pupil = layout.box()
            box_pupil.label(text="6. 瞳孔窓面 (前方の枠)", icon='MESH_TORUS')
            box_pupil.prop(props, "pupil_window_show", text="表示する")
            if props.pupil_window_show:
                col_p = box_pupil.column(align=True)
                col_p.prop(props, "pupil_window_dist", text="距離 (1-100)", slider=True)
                col_p.prop(props, "pupil_window_segments", text="角数 (3-32)")
                
                if props.pupil_window_segments == 4:
                    col_p.prop(props, "pupil_window_radius", text="1辺の長さ")
                else:
                    col_p.prop(props, "pupil_window_radius", text="半径")
                    
                col_p.prop(props, "pupil_window_thickness")
                col_p.prop(props, "pupil_window_color")
                col_p.separator()
                
                col_p.label(text="ローカル回転 (XYZ):")
                col_p.prop(props, "pupil_window_rot", index=0, text="X (Pitch)")
                col_p.prop(props, "pupil_window_rot", index=1, text="Y (Roll)")
                col_p.prop(props, "pupil_window_rot", index=2, text="Z (Yaw)")
                
                
            # --- 7. レンズ・カメラ設定 (統合) ---
            box_lens = layout.box()
            box_lens.label(text="7. レンズ・カメラ設定", icon='VIEW_CAMERA')
            col_lens = box_lens.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="クリップ開始")
            col_lens.prop(cam.data, "clip_end", text="クリップ終了")
            col_lens.separator()
            col_lens.prop(cam.data, "show_passepartout", text="枠外を暗くする")
            if cam.data.show_passepartout:
                col_lens.prop(cam.data, "passepartout_alpha", text="枠外の暗さ", slider=True)
            col_lens.separator()
            col_lens.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)")
            if cam.data.dof.use_dof:
                if cam.data.dof.focus_object:
                    col_lens.prop(cam.data.dof, "focus_object", text="ピント対象")
                col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
                
            box_exp = layout.box()
            box_exp.label(text="8. 設定値の保存 (自己書き出し)", icon='TEXT')
            box_exp.operator(f"object.{PREFIX}_export_addon", icon='EXPORT')

class VIEW3D_PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX.upper()}_PT_links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        if LINK_NAME_1 and LINK_URL_1: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL').url = LINK_URL_1
        if LINK_NAME_2 and LINK_URL_2: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        if LINK_NAME_3 and LINK_URL_3: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        if LINK_NAME_4 and LINK_URL_4: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_4, icon='URL').url = LINK_URL_4
        if LINK_NAME_5 and LINK_URL_5: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_5, icon='URL').url = LINK_URL_5

class VIEW3D_PT_remove(bpy.types.Panel):
    bl_label = "Addon Remove"
    bl_idname = f"{PREFIX.upper()}_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')

# ===================================
# 10. register()
# ===================================
classes = [
    CP_OT_open_url,
    CP_OT_remove_addon,
    ADDON_Properties,
    VIEW3D_OT_reset_view,
    OBJECT_OT_create_cam_rig,
    OBJECT_OT_sync_from_scene,
    OBJECT_OT_sync_to_scene,
    OBJECT_OT_apply_cam_anim,
    OBJECT_OT_switch_cam_main,
    OBJECT_OT_switch_cam_back,
    OBJECT_OT_set_active_camera,
    OBJECT_OT_update_camera_info,
    OBJECT_OT_copy_camera_info,
    OBJECT_OT_export_addon,
    VIEW3D_PT_main,
    VIEW3D_PT_camera,
    VIEW3D_PT_links,
    VIEW3D_PT_remove
]

def register():
    for c in classes:
        bpy.utils.register_class(c)
    setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=ADDON_Properties))
    
    def apply_initial_settings():
        if bpy.context.scene and hasattr(bpy.context.scene, f"{PREFIX}_props"):
            props = getattr(bpy.context.scene, f"{PREFIX}_props")
            try:
                bpy.context.preferences.themes[0].view_3d.camera = props.camera_color
            except Exception:
                pass

        if bpy.context.window_manager:
            for window in bpy.context.window_manager.windows:
                for area in window.screen.areas:
                    if area.type == 'VIEW_3D':
                        for space in area.spaces:
                            if space.type == 'VIEW_3D':
                                space.show_region_ui = True
                                space.shading.type = 'MATERIAL'
                                
        return None

    bpy.app.timers.register(apply_initial_settings, first_interval=0.1)

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

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
記憶
bl_info = {
    "name": "Fixed Camera & Automation Master",
    "author": "Your Name",
    "version": (4, 22),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "固定位置カメラ(カメラ円周移動追加・軌道設定拡充)",
    "category": "3D View",
}

import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import sys

# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed & View Master"
PREFIX_VAL = "CAM_FVIEW_MASTER"
ADDON_CATEGORY = " Cam Fixed "
PREFIX = PREFIX_VAL.lower()

# ===================================
# 5. リンク定義
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"

LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"

LINK_NAME_4 = ""
LINK_URL_4 = ""

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================
# 6. 初期設定
# ===================================
COLLECTION_NAME_CAM = "FixedCam_Collection"
COLLECTION_SUB_NAME_CAM = "FixedCam_SubCollection"
OBJ_NAME_ROOT = "FixedCamera_Root"
OBJ_NAME_CAMERA = "FixedCamera"
OBJ_NAME_CAMERA_BACK = "FixedCamera_Observer"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"

SHIELD_NAME = "FixedCam_Shield_Sphere"
CUTTER_NAME = "FixedCam_Shield_Cutter"
CUTTER_BACK_NAME = "FixedCam_Shield_CutterBack"
MAT_NAME_SHIELD = "FixedCam_Shield_Mat"

BACK_SHIELD_NAME = "FixedCam_BackShield_Sphere"
BACK_CUTTER_NAME = "FixedCam_BackShield_Cutter"
BACK_CUTTER_BACK_NAME = "FixedCam_BackShield_CutterBack"
MAT_NAME_BACK_SHIELD = "FixedCam_BackShield_Mat"

MAT_NAME_PUPIL = "Pupil_Window_Mat"
MAT_NAME_TARGET = "FixedCam_Target_Mat"

CAMERA_PRESETS = [
    ("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
    ("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
    ("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
CAMERA_COLOR_DEFAULT = (0.1483, 1.0000, 0.1110)

CAM_LOC_U_DEFAULT = 0.0000
CAM_LOC_V_DEFAULT = 0.0000
CAM_LOC_W_DEFAULT = 0.0000

# カメラの移動用
CAM_START_LOC_DEFAULT = (-50.0, 0.0, 0.0)
CAM_END_LOC_DEFAULT = (50.0, 0.0, 0.0)
CAM_CIRCLE_CENTER_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_RADIUS_DEFAULT = 20.0
CAM_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_START_ANGLE_DEFAULT = 0.0
CAM_CIRCLE_END_ANGLE_DEFAULT = 360.0
CAM_ANIM_SPEED_MULT_DEFAULT = 1.0

CAM_TGT_U_DEFAULT = 0.0000
CAM_TGT_V_DEFAULT = 0.0000
CAM_TGT_W_DEFAULT = 0.0000

# ターゲット(線分・円周移動用)
TARGET_START_DEFAULT = (-50.0, 5.0, 0.0)
TARGET_END_DEFAULT = (50.0, 5.0, 0.0)
TARGET_CIRCLE_CENTER_DEFAULT = (0.0, 5.0, 0.0)
TARGET_CIRCLE_RADIUS_DEFAULT = 10.0
TARGET_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
TARGET_CIRCLE_START_ANGLE_DEFAULT = 0.0
TARGET_CIRCLE_END_ANGLE_DEFAULT = 360.0

TARGET_SIZE_DEFAULT = 1.0
TARGET_COLOR_DEFAULT = (0.0000, 0.8000, 0.2000, 1.0)
TARGET_RANDOM_COLOR_DEFAULT = False
TARGET_ANIM_SPEED_MULT_DEFAULT = 1.0

CAM_SHIELD_RADIUS_DEFAULT = 1.7600
CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000

BACK_CAM_SHIELD_RADIUS_DEFAULT = 1.7600
BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000

CAM_BACK_DIST_DEFAULT = 10.0000

PUPIL_SHOW_DEFAULT = True
PUPIL_DIST_DEFAULT = 5.0000
PUPIL_RADIUS_DEFAULT = 2.0000
PUPIL_THICKNESS_DEFAULT = 0.0500
PUPIL_SEGMENTS_DEFAULT = 4
COLOR_PUPIL = (1.0000, 1.0000, 1.0000, 1.0)
PUPIL_ROT_X_DEFAULT = 0.0000
PUPIL_ROT_Y_DEFAULT = 0.0000
PUPIL_ROT_Z_DEFAULT = 0.7854

ANIM_START_FRAME_DEFAULT = 1
ANIM_END_FRAME_DEFAULT = 500

CUSTOM_GRID_SCALE_DEFAULT = 50.0

CAM_ROT_U_START = 1.5708
CAM_ROT_V_START = 0.0000
CAM_ROT_W_START = 0.0000

CAM_ROT_SPEED_U_DEFAULT = 0.0000
CAM_ROT_SPEED_V_DEFAULT = 0.0000
CAM_ROT_SPEED_W_DEFAULT = 0.3000

CAM_OFFSET_U_DEFAULT = 0.0000
CAM_OFFSET_V_DEFAULT = 0.0000
CAM_OFFSET_W_DEFAULT = 0.0000

ALPHA_SHIELD_DEFAULT = 1.0000
ALPHA_BACK_SHIELD_DEFAULT = 1.0000

COLOR_SHIELD = (0.2565, 0.0141, 0.1115, 1.0)
COLOR_BACK_SHIELD = (0.0141, 0.2565, 0.1115, 1.0)

LABEL_SIZE = "サイズ"
LABEL_U = "U"
LABEL_V = "V"
LABEL_W = "W"
LABEL_LOC = "座標"
LABEL_START = "開始"
LABEL_END = "終了"

# ===================================
# 7. 共通クラス
# ===================================
class CP_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX}_open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        if self.url: webbrowser.open(self.url)
        return {'FINISHED'}

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

# ===================================
# 8. アドオン固有機能
# ===================================

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 get_addon_source():
    source = ""
    try:
        if '__file__' in globals() and os.path.exists(__file__):
            with open(__file__, 'r', encoding='utf-8') as f:
                source = f.read()
    except Exception: pass
    if not source:
        for t in bpy.data.texts:
            if f'ADDON_TITLE = "{ADDON_TITLE}"' in t.as_string():
                source = t.as_string()
                break
    return source

def cam_fov_get(self):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        cam.data.angle = math.radians(value)

def update_visibility(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam:
        cam.hide_viewport = not self.show_cam_obj
        
    for name in [OBJ_NAME_ROOT, CUTTER_NAME, CUTTER_BACK_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            obj.hide_viewport = not self.show_target_empty

def update_target_visuals(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if tgt:
        tgt.scale = (self.target_size, self.target_size, self.target_size)
        mesh = tgt.data
        
        if len(mesh.materials) < 6:
            mesh.materials.clear()
            for i in range(6):
                mat = bpy.data.materials.new(name=f"{MAT_NAME_TARGET}_{i}")
                mat.use_nodes = True
                mesh.materials.append(mat)
                
        colors = [
            (1.0, 0.0, 0.0, 1.0), # 赤
            (0.0, 1.0, 0.0, 1.0), # 緑
            (0.0, 0.5, 1.0, 1.0), # 青
            (1.0, 1.0, 0.0, 1.0), # 黄
            (0.0, 1.0, 1.0, 1.0), # シアン
            (1.0, 0.0, 1.0, 1.0), # マゼンタ
        ]

        for i in range(6):
            mat = mesh.materials[i]
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf and 'Base Color' in bsdf.inputs:
                if self.target_random_color:
                    bsdf.inputs['Base Color'].default_value = colors[i]
                else:
                    bsdf.inputs['Base Color'].default_value = self.target_color
            
        for i, poly in enumerate(mesh.polygons):
            poly.material_index = i % 6 if self.target_random_color else 0

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_target_anim(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not tgt: return
    
    if tgt.animation_data:
        tgt.animation_data.action = None
        
    start_f = self.anim_start_frame
    end_f = self.anim_end_frame
    delta_f = end_f - start_f
    speed = self.target_anim_speed_mult
        
    if self.cam_target_mode == 'POINT':
        tgt.location = self.cam_target_loc
        
    elif self.cam_target_mode == 'LINE':
        s_loc = mathutils.Vector(self.target_start_loc)
        e_loc = mathutils.Vector(self.target_end_loc)
        
        if delta_f <= 0:
            tgt.location = s_loc
            tgt.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                pp = progress % 2.0
                if pp < 0: pp += 2.0 # マイナス対応
                ratio = pp if pp <= 1.0 else 2.0 - pp
                
                tgt.location = s_loc.lerp(e_loc, ratio)
                tgt.keyframe_insert(data_path="location", frame=f)
                
        if tgt.animation_data and tgt.animation_data.action:
            act = tgt.animation_data.action
            fcs = getattr(act, "fcurves", None)
            if fcs:
                for fc in fcs:
                    if fc.data_path == 'location':
                        for kp in fc.keyframe_points:
                            kp.interpolation = 'LINEAR'
                            
    elif self.cam_target_mode == 'CIRCLE':
        rot_mat = mathutils.Euler(self.target_circle_rot, 'XYZ').to_matrix()
        center = mathutils.Vector(self.target_circle_center)
        radius = self.target_circle_radius
        start_rad = math.radians(self.target_circle_start_angle)
        end_rad = math.radians(self.target_circle_end_angle)
        
        if delta_f <= 0:
            local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
            tgt.location = center + rot_mat @ local_pos
            tgt.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                t = start_rad + (end_rad - start_rad) * progress
                local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
                tgt.location = center + rot_mat @ local_pos
                tgt.keyframe_insert(data_path="location", frame=f)
                
        if tgt.animation_data and tgt.animation_data.action:
            act = tgt.animation_data.action
            fcs = getattr(act, "fcurves", None)
            if fcs:
                for fc in fcs:
                    if fc.data_path == 'location':
                        for kp in fc.keyframe_points:
                            kp.interpolation = 'LINEAR'
                            
    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_anim(self, context):
    update_target_anim(self, context)
    
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    if not root: return
        
    if root.animation_data:
        root.animation_data.action = None
            
    start_f = self.anim_start_frame
    end_f = self.anim_end_frame
    delta_f = end_f - start_f
    
    # 1. カメラ(Root)の位置ベイク
    if self.cam_move_mode == 'FIXED':
        root.location = self.cam_fixed_loc
        root.keyframe_insert(data_path="location", frame=start_f)
        
    elif self.cam_move_mode == 'LINE':
        s_loc = mathutils.Vector(self.cam_start_loc)
        e_loc = mathutils.Vector(self.cam_end_loc)
        speed = self.cam_anim_speed_mult
        
        if delta_f <= 0:
            root.location = s_loc
            root.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                pp = progress % 2.0
                if pp < 0: pp += 2.0 # マイナス対応
                ratio = pp if pp <= 1.0 else 2.0 - pp
                
                root.location = s_loc.lerp(e_loc, ratio)
                root.keyframe_insert(data_path="location", frame=f)
                
    elif self.cam_move_mode == 'CIRCLE':
        rot_mat = mathutils.Euler(self.cam_circle_rot, 'XYZ').to_matrix()
        center = mathutils.Vector(self.cam_circle_center)
        radius = self.cam_circle_radius
        start_rad = math.radians(self.cam_circle_start_angle)
        end_rad = math.radians(self.cam_circle_end_angle)
        speed = self.cam_anim_speed_mult
        
        if delta_f <= 0:
            local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
            root.location = center + rot_mat @ local_pos
            root.keyframe_insert(data_path="location", frame=start_f)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                t = start_rad + (end_rad - start_rad) * progress
                local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
                root.location = center + rot_mat @ local_pos
                root.keyframe_insert(data_path="location", frame=f)

    # 2. カメラ(Root)の回転ベイク (ターゲットが無い場合のみ)
    if self.cam_target_mode == 'NONE':
        root.rotation_mode = 'XYZ'
        if delta_f <= 0:
            root.rotation_euler = self.cam_rot_start
            root.keyframe_insert(data_path="rotation_euler", frame=start_f)
        else:
            speed_u = math.radians(self.cam_rot_speed[0])
            speed_v = math.radians(self.cam_rot_speed[1])
            speed_w = math.radians(self.cam_rot_speed[2])

            start_rot = self.cam_rot_start
            end_rot = (
                start_rot[0] + speed_u * delta_f,
                start_rot[1] + speed_v * delta_f,
                start_rot[2] + speed_w * delta_f
            )

            root.rotation_euler = start_rot
            root.keyframe_insert(data_path="rotation_euler", frame=start_f)
            root.rotation_euler = end_rot
            root.keyframe_insert(data_path="rotation_euler", frame=end_f)

    # Fcurveの線形補間適用
    if root.animation_data and root.animation_data.action:
        act = root.animation_data.action
        fcs = getattr(act, "fcurves", None)
        if fcs:
            for fc in fcs:
                for kp in fc.keyframe_points:
                    kp.interpolation = 'LINEAR'

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_root_constraint(self, context):
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    tgt_obj = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not root: return

    tt = next((c for c in root.constraints if c.type == 'TRACK_TO'), None)
    
    if self.cam_target_mode == 'NONE':
        if tt: root.constraints.remove(tt)
        update_cam_anim(self, context) 
    else:
        if not tt: tt = root.constraints.new(type='TRACK_TO')
        tt.track_axis = 'TRACK_NEGATIVE_Z'
        tt.up_axis = 'UP_Y'
        
        if self.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
            if tgt_obj:
                tt.target = tgt_obj
            update_target_anim(self, context)
        elif self.cam_target_mode == 'OBJECT':
            tt.target = self.cam_target_obj

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_root_loc(self, context):
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    if root: root.location = self.cam_fixed_loc

def update_cam_local_offset_rot(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam: cam.rotation_euler = self.cam_local_offset_rot

def update_cam_shield(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if not cam: return
    shield = next((c for c in cam.children if SHIELD_NAME in c.name), None)
    cutter = next((c for c in cam.children if CUTTER_NAME in c.name), None)
    cutter_back = next((c for c in cam.children if CUTTER_BACK_NAME in c.name), None)
    
    if shield:
        shield.scale = (self.cam_shield_radius,) * 3
        shield.hide_viewport = not self.show_cam_shield
    
    z_scale = self.cam_shield_radius * 3.0 
    
    if cutter:
        r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_front)
        cutter.scale = (r, r, z_scale)
        cutter.display_type = 'WIRE'
        cutter.hide_render = True
        
    if cutter_back:
        r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_back)
        cutter_back.scale = (r, r, z_scale)
        cutter_back.display_type = 'WIRE'
        cutter_back.hide_render = True
        
    if shield and shield.data.materials:
        mat = shield.data.materials[0]
        mat.diffuse_color = (*self.cam_shield_color[:3], self.cam_shield_alpha)
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs: 
                    bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color[:3], 1.0)
                if 'Alpha' in bsdf.inputs: 
                    bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha

def update_back_cam_shield(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
    if not cam: return
    shield = next((c for c in cam.children if BACK_SHIELD_NAME in c.name), None)
    cutter = next((c for c in cam.children if BACK_CUTTER_NAME in c.name), None)
    cutter_back = next((c for c in cam.children if BACK_CUTTER_BACK_NAME in c.name), None)
    
    if shield:
        shield.scale = (self.back_cam_shield_radius,) * 3
        shield.hide_viewport = not self.show_back_cam_shield
    
    z_scale = self.back_cam_shield_radius * 3.0 
    
    if cutter:
        r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_front)
        cutter.scale = (r, r, z_scale)
        cutter.display_type = 'WIRE'
        cutter.hide_render = True
        
    if cutter_back:
        r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_back)
        cutter_back.scale = (r, r, z_scale)
        cutter_back.display_type = 'WIRE'
        cutter_back.hide_render = True
        
    if shield and shield.data.materials:
        mat = shield.data.materials[0]
        mat.diffuse_color = (*self.back_cam_shield_color[:3], self.back_cam_shield_alpha)
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs: 
                    bsdf.inputs['Base Color'].default_value = (*self.back_cam_shield_color[:3], 1.0)
                if 'Alpha' in bsdf.inputs: 
                    bsdf.inputs['Alpha'].default_value = self.back_cam_shield_alpha

def update_cam_back_dist(self, context):
    cam_back = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
    if cam_back:
        cam_back.location.z = self.cam_back_dist

def update_active_camera(self, context):
    cam = None
    if self.active_camera == 'MAIN':
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    elif self.active_camera == 'BACK':
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
        
    if cam:
        context.scene.camera = cam
        cam.data.show_passepartout = True
        cam.data.passepartout_alpha = 0.8
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                area.spaces.active.region_3d.view_perspective = 'CAMERA'

def update_camera_color(self, context):
    try:
        bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
    except Exception:
        pass

def update_camera_preset_func(self, context):
    for p in CAMERA_PRESETS:
        if p[0] == self.camera_preset: 
            self.camera_color = p[3]
            break

def update_pupil_window(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if not obj: return
    
    obj.hide_viewport = not self.pupil_window_show
    obj.hide_render = not self.pupil_window_show
    
    if not self.pupil_window_show:
        return
        
    obj.location = (0.0, 0.0, -self.pupil_window_dist)
    
    mesh = obj.data
    mesh.clear_geometry()
    
    maj_s = self.pupil_window_segments
    min_s = 16
    
    val = self.pupil_window_radius
    if maj_s == 4:
        R = val / math.sqrt(2.0)
    else:
        R = val
        
    r = self.pupil_window_thickness
    
    verts = []
    faces = []
    
    for i in range(maj_s):
        u = i * 2.0 * math.pi / maj_s
        for j in range(min_s):
            v = j * 2.0 * math.pi / min_s
            x = (R + r * math.cos(v)) * math.cos(u)
            y = (R + r * math.cos(v)) * math.sin(u)
            z = r * math.sin(v)
            verts.append((x, y, z))
            
    for i in range(maj_s):
        for j in range(min_s):
            next_i = (i + 1) % maj_s
            next_j = (j + 1) % min_s
            v0 = i * min_s + j
            v1 = i * min_s + next_j
            v2 = next_i * min_s + next_j
            v3 = next_i * min_s + j
            faces.append((v0, v1, v2, v3))
            
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_pupil_segments(self, context):
    if self.pupil_window_segments == 4:
        self.pupil_window_rot = (0.0, 0.0, math.radians(45.0))
    else:
        self.pupil_window_rot = (0.0, 0.0, math.radians(90.0))
    update_pupil_window(self, context)

def update_pupil_window_rot(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if obj:
        obj.rotation_euler = self.pupil_window_rot

def update_pupil_window_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = self.pupil_window_color
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs:
                    bsdf.inputs['Base Color'].default_value = self.pupil_window_color

# --- Property Group ---
class ADDON_Properties(bpy.types.PropertyGroup):
    custom_grid_scale: bpy.props.FloatProperty(name=LABEL_SIZE, default=CUSTOM_GRID_SCALE_DEFAULT, min=0.001)
    
    # カメラの移動設定
    cam_move_mode: bpy.props.EnumProperty(
        name="カメラ(Root)の移動",
        items=[('FIXED', "固定位置", ""), ('LINE', "線分移動 (アニメ)", ""), ('CIRCLE', "円周移動 (アニメ)", "")],
        default='FIXED', update=update_cam_anim
    )
    cam_fixed_loc: bpy.props.FloatVectorProperty(name=f"固定座標", default=(CAM_LOC_U_DEFAULT, CAM_LOC_V_DEFAULT, CAM_LOC_W_DEFAULT), update=update_cam_anim)
    cam_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=CAM_START_LOC_DEFAULT, update=update_cam_anim)
    cam_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=CAM_END_LOC_DEFAULT, update=update_cam_anim)
    
    cam_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=CAM_CIRCLE_CENTER_DEFAULT, update=update_cam_anim)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=CAM_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_cam_anim)
    cam_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=CAM_CIRCLE_ROT_DEFAULT, update=update_cam_anim)
    cam_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=CAM_CIRCLE_START_ANGLE_DEFAULT, update=update_cam_anim)
    cam_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=CAM_CIRCLE_END_ANGLE_DEFAULT, update=update_cam_anim)
    
    cam_anim_speed_mult: bpy.props.FloatProperty(name="カメラ移動速度(倍率)", default=CAM_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_cam_anim)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点設定",
        items=[
            ('NONE', "オフ (速度ベース回転)", ""), 
            ('POINT', "指定座標 (固定)", ""), 
            ('LINE', "線分移動 (アニメ)", ""), 
            ('CIRCLE', "円周移動 (アニメ)", ""), 
            ('OBJECT', "別オブジェクト", "")
        ],
        default='NONE', update=update_cam_root_constraint
    )
    cam_target_loc: bpy.props.FloatVectorProperty(name=f"注視点", default=(CAM_TGT_U_DEFAULT, CAM_TGT_V_DEFAULT, CAM_TGT_W_DEFAULT), update=update_cam_root_constraint)
    cam_target_obj: bpy.props.PointerProperty(type=bpy.types.Object, name="対象オブジェクト", update=update_cam_root_constraint)
    
    target_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=TARGET_START_DEFAULT, update=update_target_anim)
    target_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=TARGET_END_DEFAULT, update=update_target_anim)
    
    target_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=TARGET_CIRCLE_CENTER_DEFAULT, update=update_target_anim)
    target_circle_radius: bpy.props.FloatProperty(name="円の半径", default=TARGET_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_target_anim)
    target_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=TARGET_CIRCLE_ROT_DEFAULT, update=update_target_anim)
    target_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=TARGET_CIRCLE_START_ANGLE_DEFAULT, update=update_target_anim)
    target_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=TARGET_CIRCLE_END_ANGLE_DEFAULT, update=update_target_anim)

    target_size: bpy.props.FloatProperty(name="立方体のサイズ", default=TARGET_SIZE_DEFAULT, min=0.01, update=update_target_visuals)
    target_color: bpy.props.FloatVectorProperty(name="立方体の色", subtype='COLOR', size=4, default=TARGET_COLOR_DEFAULT, min=0.0, max=1.0, update=update_target_visuals)
    target_random_color: bpy.props.BoolProperty(name="6面をランダムな色にする", default=TARGET_RANDOM_COLOR_DEFAULT, update=update_target_visuals)
    target_anim_speed_mult: bpy.props.FloatProperty(name="立方体移動速度 (倍率)", description="マイナス値で逆行/逆回転します", default=TARGET_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_target_anim)

    cam_local_offset_rot: bpy.props.FloatVectorProperty(name="角度オフセット", subtype='EULER', default=(CAM_OFFSET_U_DEFAULT, CAM_OFFSET_V_DEFAULT, CAM_OFFSET_W_DEFAULT), update=update_cam_local_offset_rot)
    
    anim_start_frame: bpy.props.IntProperty(name=f"{LABEL_START} F", default=ANIM_START_FRAME_DEFAULT, min=0, update=update_cam_anim)
    anim_end_frame: bpy.props.IntProperty(name=f"{LABEL_END} F", default=ANIM_END_FRAME_DEFAULT, min=1, update=update_cam_anim)
    cam_rot_start: bpy.props.FloatVectorProperty(name="初期回転", subtype='EULER', default=(CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START), update=update_cam_anim)
    cam_rot_speed: bpy.props.FloatVectorProperty(name="回転速度", default=(CAM_ROT_SPEED_U_DEFAULT, CAM_ROT_SPEED_V_DEFAULT, CAM_ROT_SPEED_W_DEFAULT), update=update_cam_anim)
    
    show_cam_obj: bpy.props.BoolProperty(name="カメラ本体表示", default=True, update=update_visibility)
    show_target_empty: bpy.props.BoolProperty(name="Empty", default=True, update=update_visibility)
    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
    
    cam_back_dist: bpy.props.FloatProperty(name="背後カメラの距離", default=CAM_BACK_DIST_DEFAULT, min=1.0, max=100.0, update=update_cam_back_dist)
    
    active_camera: bpy.props.EnumProperty(
        name="表示カメラ切り替え",
        items=[('MAIN', "メインカメラ", "包み内 カメラ"), ('BACK', "背後霊カメラ", "背後 観察カメラ")],
        default='MAIN', update=update_active_camera
    )
    
    camera_preset: bpy.props.EnumProperty(
        name="カメラ色プリセット", 
        items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS], 
        default='Cam', 
        update=update_camera_preset_func
    )
    camera_color: bpy.props.FloatVectorProperty(
        name="カメラテーマ色", 
        subtype='COLOR', size=3, min=0.0, max=1.0, 
        default=CAMERA_COLOR_DEFAULT, 
        update=update_camera_color
    )
    
    show_cam_shield: bpy.props.BoolProperty(name="包み表示", default=True, update=update_cam_shield)
    cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_cam_shield)
    cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
    cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_cam_shield)
    cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_SHIELD[:3], min=0.0, max=1.0, update=update_cam_shield)
    cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)

    show_back_cam_shield: bpy.props.BoolProperty(name="背後 包み表示", default=True, update=update_back_cam_shield)
    back_cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=BACK_CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_back_cam_shield)
    back_cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
    back_cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_back_cam_shield)
    back_cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_BACK_SHIELD[:3], min=0.0, max=1.0, update=update_back_cam_shield)
    back_cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_BACK_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)

    pupil_window_show: bpy.props.BoolProperty(name="瞳孔窓面の表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
    pupil_window_dist: bpy.props.FloatProperty(name="距離 (カメラ前方)", default=PUPIL_DIST_DEFAULT, min=1.0, max=100.0, update=update_pupil_window)
    pupil_window_radius: bpy.props.FloatProperty(name="サイズ", default=PUPIL_RADIUS_DEFAULT, min=0.1, update=update_pupil_window)
    pupil_window_thickness: bpy.props.FloatProperty(name="枠の太さ", default=PUPIL_THICKNESS_DEFAULT, min=0.01, update=update_pupil_window)
    pupil_window_segments: bpy.props.IntProperty(name="角数 (3=三角, 4=四角)", default=PUPIL_SEGMENTS_DEFAULT, min=3, max=32, update=update_pupil_segments)
    pupil_window_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_PUPIL, min=0.0, max=1.0, update=update_pupil_window_mat)
    pupil_window_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(PUPIL_ROT_X_DEFAULT, PUPIL_ROT_Y_DEFAULT, PUPIL_ROT_Z_DEFAULT), update=update_pupil_window_rot)

    info_proj_mode: bpy.props.StringProperty(name="投影モード", default="-")
    info_active_camera_name: bpy.props.StringProperty(name="表示カメラ", default="-")
    info_center_loc: bpy.props.FloatVectorProperty(name="画面中央位置")
    info_view_loc: bpy.props.FloatVectorProperty(name="視座位置")
    info_view_dir: bpy.props.FloatVectorProperty(name="視線方向")
    info_view_width: bpy.props.FloatProperty(name="画面水平の大きさ")
    info_view_height: bpy.props.FloatProperty(name="画面上下の大きさ")
    info_view_dist: bpy.props.FloatProperty(name="視座からの距離")
    
    info_target_path: bpy.props.StringProperty(name="ターゲット軌道情報", default="軌道: なし")

# --- Operators ---
class VIEW3D_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_reset_view"
    bl_label = "選択物を中央にする"
    def execute(self, context):
        rv3d = get_rv3d(context)
        if rv3d and context.active_object:
            rv3d.view_location = context.active_object.location
        return {'FINISHED'}

class OBJECT_OT_create_cam_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_create_cam_rig"
    bl_label = "カメラリグ作成"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        for n in [OBJ_NAME_ROOT, OBJ_NAME_CAMERA, OBJ_NAME_CAMERA_BACK, OBJ_NAME_TARGET, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME, OBJ_NAME_PUPIL, BACK_SHIELD_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
            o = bpy.data.objects.get(n)
            if o: bpy.data.objects.remove(o, do_unlink=True)
            
        main_col = bpy.data.collections.get(COLLECTION_NAME_CAM) or bpy.data.collections.new(COLLECTION_NAME_CAM)
        if main_col.name not in context.scene.collection.children: context.scene.collection.children.link(main_col)
        
        sub_col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM) or bpy.data.collections.new(COLLECTION_SUB_NAME_CAM)
        if sub_col.name not in main_col.children: main_col.children.link(sub_col)

        m_tgt = bpy.data.meshes.new(f"{OBJ_NAME_TARGET}_Mesh")
        bm_t = bmesh.new()
        bmesh.ops.create_cube(bm_t, size=1.0)
        bm_t.to_mesh(m_tgt)
        bm_t.free()
        
        tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, m_tgt)
        sub_col.objects.link(tgt_obj)
        
        update_target_visuals(props, context)

        root_obj = bpy.data.objects.new(OBJ_NAME_ROOT, None)
        root_obj.empty_display_type = 'PLAIN_AXES'
        root_obj.location = props.cam_fixed_loc
        root_obj.rotation_mode = 'XYZ'
        root_obj.rotation_euler = props.cam_rot_start
        sub_col.objects.link(root_obj)

        cam_data = bpy.data.cameras.new(OBJ_NAME_CAMERA)
        cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
        cam_obj.parent = root_obj
        cam_obj.location = (0, 0, 0)
        cam_obj.rotation_euler = props.cam_local_offset_rot
        sub_col.objects.link(cam_obj)
        context.scene.camera = cam_obj
        
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = tgt_obj
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        cam_back_data = bpy.data.cameras.new(OBJ_NAME_CAMERA_BACK)
        cam_back_data.show_passepartout = True
        cam_back_data.passepartout_alpha = 0.8
        cam_back_obj = bpy.data.objects.new(OBJ_NAME_CAMERA_BACK, cam_back_data)
        cam_back_obj.parent = cam_obj
        cam_back_obj.location = (0.0, 0.0, props.cam_back_dist)
        cam_back_obj.rotation_mode = 'XYZ'
        cam_back_obj.rotation_euler = (0.0, 0.0, 0.0) 
        sub_col.objects.link(cam_back_obj)

        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
        m = bpy.data.meshes.new(SHIELD_NAME)
        bm.to_mesh(m)
        bm.free()
        s_obj = bpy.data.objects.new(SHIELD_NAME, m)
        s_obj.parent = cam_obj
        sub_col.objects.link(s_obj)

        bm_c = bmesh.new()
        bmesh.ops.create_cone(bm_c, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_c.verts: v.co.z -= 0.5
        m_c = bpy.data.meshes.new(CUTTER_NAME)
        bm_c.to_mesh(m_c)
        bm_c.free()
        c_obj = bpy.data.objects.new(CUTTER_NAME, m_c)
        c_obj.parent = cam_obj
        c_obj.hide_viewport = not props.show_target_empty
        sub_col.objects.link(c_obj)

        bm_b = bmesh.new()
        bmesh.ops.create_cone(bm_b, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_b.verts: v.co.z += 0.5
        m_b = bpy.data.meshes.new(CUTTER_BACK_NAME)
        bm_b.to_mesh(m_b)
        bm_b.free()
        cb_obj = bpy.data.objects.new(CUTTER_BACK_NAME, m_b)
        cb_obj.parent = cam_obj
        cb_obj.hide_viewport = not props.show_target_empty
        sub_col.objects.link(cb_obj)

        mod_front = s_obj.modifiers.new("Hole_Front", 'BOOLEAN')
        mod_front.object = c_obj
        mod_front.solver = 'FLOAT'
        
        mod_back = s_obj.modifiers.new("Hole_Back", 'BOOLEAN')
        mod_back.object = cb_obj
        mod_back.solver = 'FLOAT'

        mat = bpy.data.materials.new(MAT_NAME_SHIELD)
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
        s_obj.data.materials.append(mat)
        s_obj.show_transparent = True

        bm_bs = bmesh.new()
        bmesh.ops.create_uvsphere(bm_bs, u_segments=32, v_segments=16, radius=1.0)
        m_bs = bpy.data.meshes.new(BACK_SHIELD_NAME)
        bm_bs.to_mesh(m_bs)
        bm_bs.free()
        s_obj_b = bpy.data.objects.new(BACK_SHIELD_NAME, m_bs)
        s_obj_b.parent = cam_back_obj
        sub_col.objects.link(s_obj_b)

        bm_bc = bmesh.new()
        bmesh.ops.create_cone(bm_bc, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_bc.verts: v.co.z -= 0.5
        m_bc = bpy.data.meshes.new(BACK_CUTTER_NAME)
        bm_bc.to_mesh(m_bc)
        bm_bc.free()
        c_obj_b = bpy.data.objects.new(BACK_CUTTER_NAME, m_bc)
        c_obj_b.parent = cam_back_obj
        c_obj_b.hide_viewport = not props.show_target_empty
        sub_col.objects.link(c_obj_b)

        bm_bb = bmesh.new()
        bmesh.ops.create_cone(bm_bb, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_bb.verts: v.co.z += 0.5
        m_bb = bpy.data.meshes.new(BACK_CUTTER_BACK_NAME)
        bm_bb.to_mesh(m_bb)
        bm_bb.free()
        cb_obj_b = bpy.data.objects.new(BACK_CUTTER_BACK_NAME, m_bb)
        cb_obj_b.parent = cam_back_obj
        cb_obj_b.hide_viewport = not props.show_target_empty
        sub_col.objects.link(cb_obj_b)

        mod_front_b = s_obj_b.modifiers.new("Hole_Front", 'BOOLEAN')
        mod_front_b.object = c_obj_b
        mod_front_b.solver = 'FLOAT'
        
        mod_back_b = s_obj_b.modifiers.new("Hole_Back", 'BOOLEAN')
        mod_back_b.object = cb_obj_b
        mod_back_b.solver = 'FLOAT'

        mat_b = bpy.data.materials.new(MAT_NAME_BACK_SHIELD)
        mat_b.use_nodes = True
        mat_b.blend_method = 'BLEND'
        s_obj_b.data.materials.append(mat_b)
        s_obj_b.show_transparent = True

        m_pupil = bpy.data.meshes.new(f"{OBJ_NAME_PUPIL}_Mesh")
        o_pupil = bpy.data.objects.new(OBJ_NAME_PUPIL, m_pupil)
        o_pupil.parent = cam_obj
        o_pupil.rotation_mode = 'XYZ'
        sub_col.objects.link(o_pupil)
        
        mat_pupil = bpy.data.materials.new(MAT_NAME_PUPIL)
        mat_pupil.use_nodes = True
        mat_pupil.blend_method = 'BLEND'
        o_pupil.data.materials.append(mat_pupil)
        
        update_pupil_window(props, context)
        update_pupil_window_rot(props, context)
        update_pupil_window_mat(props, context)

        update_cam_root_constraint(props, context)
        update_visibility(props, context)
        update_cam_shield(props, context)
        update_back_cam_shield(props, context)
        update_cam_anim(props, context)
        
        update_camera_color(props, context)
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
                        space.region_3d.view_perspective = 'CAMERA'
                        space.lock_camera = False
        
        self.report({'INFO'}, "カメラを作成し、カメラビュー&マテリアルプレビューに切り替えました")
        return {'FINISHED'}

class OBJECT_OT_sync_from_scene(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_sync_from_scene"
    bl_label = "シーンから取得"
    bl_description = "Blender本体のタイムライン(開始/終了フレーム)をこのアドオンの設定にコピーします"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.anim_start_frame = context.scene.frame_start
        props.anim_end_frame = context.scene.frame_end
        self.report({'INFO'}, f"シーンから取得しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
        return {'FINISHED'}

class OBJECT_OT_sync_to_scene(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_sync_to_scene"
    bl_label = "シーンへ適用"
    bl_description = "このアドオンで設定した開始/終了フレームを、Blender本体のタイムラインに適用します"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        context.scene.frame_start = props.anim_start_frame
        context.scene.frame_end = props.anim_end_frame
        self.report({'INFO'}, f"タイムラインのフレーム範囲を更新しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
        return {'FINISHED'}

class OBJECT_OT_apply_cam_anim(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_apply_cam_anim"
    bl_label = "アニメーションを適用(更新)"
    bl_description = "現在の設定で回転・移動アニメーションのキーフレームを打ち直します"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        update_cam_anim(props, context)
        self.report({'INFO'}, "アニメーションキーフレームを更新しました")
        return {'FINISHED'}

class OBJECT_OT_switch_cam_main(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_switch_cam_main"
    bl_label = "包み内 カメラ視点"
    bl_description = "本来の固定カメラをアクティブにしてカメラビューに切り替えます"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "包み内カメラ視点に切り替えました")
        else:
            self.report({'ERROR'}, "包み内カメラが見つかりません")
        return {'FINISHED'}

class OBJECT_OT_switch_cam_back(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_switch_cam_back"
    bl_label = "背後 観察視点"
    bl_description = "全体を見渡す背後カメラをアクティブにしてカメラビューに切り替えます"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "背後からの観察視点に切り替えました")
        else:
            self.report({'ERROR'}, "背後カメラが見つかりません。リグを再作成してください")
        return {'FINISHED'}

class OBJECT_OT_set_active_camera(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_set_active_camera"
    bl_label = "専用カメラをアクティブにする"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "専用カメラをアクティブに設定しました")
        else:
            self.report({'ERROR'}, "専用カメラが見つかりません。リグを作成してください")
        return {'FINISHED'}

class OBJECT_OT_update_camera_info(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_update_camera_info"
    bl_label = "情報更新"
    bl_description = "現在のビューから画面の情報を取得・更新します"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        rv3d = get_rv3d(context)
        
        if not rv3d:
            self.report({'WARNING'}, "3Dビューが見つかりません")
            return {'CANCELLED'}

        if rv3d.view_perspective == 'CAMERA': props.info_proj_mode = "カメラビュー"
        elif rv3d.view_perspective == 'PERSP': props.info_proj_mode = "透視投影"
        elif rv3d.view_perspective == 'ORTHO': props.info_proj_mode = "平行投影"
        else: props.info_proj_mode = rv3d.view_perspective
            
        dist = rv3d.view_distance
        props.info_view_dist = dist
        
        res_x = context.scene.render.resolution_x
        res_y = context.scene.render.resolution_y
        aspect = (res_x / res_y) if res_y != 0 else 1.0

        if rv3d.view_perspective == 'CAMERA' and context.scene.camera:
            cam = context.scene.camera
            
            if cam.name == OBJ_NAME_CAMERA:
                props.info_active_camera_name = "メインカメラ"
            elif cam.name == OBJ_NAME_CAMERA_BACK:
                props.info_active_camera_name = "背後霊カメラ"
            else:
                props.info_active_camera_name = f"その他 ({cam.name})"
                
            view_loc = cam.matrix_world.translation
            view_dir = cam.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
            center_loc = view_loc + view_dir * dist
            fov = cam.data.angle
            width = 2.0 * dist * math.tan(fov / 2.0)
            height = width / aspect
        else:
            props.info_active_camera_name = "なし (ビューポート)"
            center_loc = rv3d.view_location
            view_rot = rv3d.view_rotation
            view_dir = view_rot @ mathutils.Vector((0, 0, -1))
            view_loc = center_loc - view_dir * dist
            space = None
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    space = area.spaces.active
                    break
            lens = space.lens if space and hasattr(space, "lens") else 50.0
            fov = 2.0 * math.atan(36.0 / (2.0 * lens))
            width = 2.0 * dist * math.tan(fov / 2.0)
            height = width / aspect
            
        props.info_center_loc = center_loc
        props.info_view_loc = view_loc
        props.info_view_dir = view_dir
        props.info_view_width = width
        props.info_view_height = height
        
        props.info_target_path = "ターゲット軌道: なし"
        if props.cam_target_mode == 'LINE':
            s = props.target_start_loc
            e = props.target_end_loc
            props.info_target_path = f"ターゲット線分(往復 {props.target_anim_speed_mult:.1f}倍速): Start({s[0]:.1f}, {s[1]:.1f}, {s[2]:.1f}) -> End({e[0]:.1f}, {e[1]:.1f}, {e[2]:.1f})"
        elif props.cam_target_mode == 'CIRCLE':
            rot_mat = mathutils.Euler(props.target_circle_rot, 'XYZ').to_matrix()
            normal = rot_mat @ mathutils.Vector((0, 0, 1))
            c = props.target_circle_center
            d = -(normal.x * c[0] + normal.y * c[1] + normal.z * c[2])
            props.info_target_path = f"ターゲット平面({props.target_anim_speed_mult:.1f}倍速): {normal.x:.2f}x + {normal.y:.2f}y + {normal.z:.2f}z + ({d:.2f}) = 0"
            
        if props.cam_move_mode == 'LINE':
            cs = props.cam_start_loc
            ce = props.cam_end_loc
            props.info_target_path += f"\nカメラ移動(往復 {props.cam_anim_speed_mult:.1f}倍速): Start({cs[0]:.1f}, {cs[1]:.1f}, {cs[2]:.1f}) -> End({ce[0]:.1f}, {ce[1]:.1f}, {ce[2]:.1f})"
        elif props.cam_move_mode == 'CIRCLE':
            rot_mat_c = mathutils.Euler(props.cam_circle_rot, 'XYZ').to_matrix()
            normal_c = rot_mat_c @ mathutils.Vector((0, 0, 1))
            cc = props.cam_circle_center
            dc = -(normal_c.x * cc[0] + normal_c.y * cc[1] + normal_c.z * cc[2])
            props.info_target_path += f"\nカメラ平面({props.cam_anim_speed_mult:.1f}倍速): {normal_c.x:.2f}x + {normal_c.y:.2f}y + {normal_c.z:.2f}z + ({dc:.2f}) = 0"

        
        self.report({'INFO'}, "画面情報を更新しました")
        return {'FINISHED'}

class OBJECT_OT_copy_camera_info(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_copy_camera_info"
    bl_label = "情報をコピー"
    bl_description = "更新された画面情報をクリップボードにコピーします"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        info = (
            f"投影モード: {props.info_proj_mode}\n"
            f"表示カメラ: {props.info_active_camera_name}\n"
            f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}\n"
            f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}\n"
            f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}\n"
            f"画面水平の大きさ: {props.info_view_width:.1f}\n"
            f"画面上下の大きさ: {props.info_view_height:.1f}\n"
            f"視座からの距離: {props.info_view_dist:.1f}\n"
            f"{props.info_target_path}"
        )
        
        context.window_manager.clipboard = info
        self.report({'INFO'}, "画面情報をクリップボードにコピーしました")
        return {'FINISHED'}

class OBJECT_OT_export_addon(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_export_addon"
    bl_label = "現在の設定でアドオンを書き出す"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        source = get_addon_source()
        if not source:
            self.report({'ERROR'}, "ソースコードを取得できませんでした")
            return {'CANCELLED'}
            
        lines = source.split('\n')
        for i, line in enumerate(lines):
            if line.startswith("CAM_LOC_U_DEFAULT = "): lines[i] = f"CAM_LOC_U_DEFAULT = {props.cam_fixed_loc[0]:.4f}"
            elif line.startswith("CAM_LOC_V_DEFAULT = "): lines[i] = f"CAM_LOC_V_DEFAULT = {props.cam_fixed_loc[1]:.4f}"
            elif line.startswith("CAM_LOC_W_DEFAULT = "): lines[i] = f"CAM_LOC_W_DEFAULT = {props.cam_fixed_loc[2]:.4f}"
            
            elif line.startswith("CAM_START_LOC_DEFAULT = "): lines[i] = f"CAM_START_LOC_DEFAULT = ({props.cam_start_loc[0]:.4f}, {props.cam_start_loc[1]:.4f}, {props.cam_start_loc[2]:.4f})"
            elif line.startswith("CAM_END_LOC_DEFAULT = "): lines[i] = f"CAM_END_LOC_DEFAULT = ({props.cam_end_loc[0]:.4f}, {props.cam_end_loc[1]:.4f}, {props.cam_end_loc[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"CAM_CIRCLE_CENTER_DEFAULT = ({props.cam_circle_center[0]:.4f}, {props.cam_circle_center[1]:.4f}, {props.cam_circle_center[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"CAM_CIRCLE_RADIUS_DEFAULT = {props.cam_circle_radius:.4f}"
            elif line.startswith("CAM_CIRCLE_ROT_DEFAULT = "): lines[i] = f"CAM_CIRCLE_ROT_DEFAULT = ({props.cam_circle_rot[0]:.4f}, {props.cam_circle_rot[1]:.4f}, {props.cam_circle_rot[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_START_ANGLE_DEFAULT = {props.cam_circle_start_angle:.4f}"
            elif line.startswith("CAM_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_END_ANGLE_DEFAULT = {props.cam_circle_end_angle:.4f}"
            elif line.startswith("CAM_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"CAM_ANIM_SPEED_MULT_DEFAULT = {props.cam_anim_speed_mult:.4f}"
            
            elif line.startswith("CAM_TGT_U_DEFAULT = "): lines[i] = f"CAM_TGT_U_DEFAULT = {props.cam_target_loc[0]:.4f}"
            elif line.startswith("CAM_TGT_V_DEFAULT = "): lines[i] = f"CAM_TGT_V_DEFAULT = {props.cam_target_loc[1]:.4f}"
            elif line.startswith("CAM_TGT_W_DEFAULT = "): lines[i] = f"CAM_TGT_W_DEFAULT = {props.cam_target_loc[2]:.4f}"
            
            elif line.startswith("TARGET_START_DEFAULT = "): lines[i] = f"TARGET_START_DEFAULT = ({props.target_start_loc[0]:.4f}, {props.target_start_loc[1]:.4f}, {props.target_start_loc[2]:.4f})"
            elif line.startswith("TARGET_END_DEFAULT = "): lines[i] = f"TARGET_END_DEFAULT = ({props.target_end_loc[0]:.4f}, {props.target_end_loc[1]:.4f}, {props.target_end_loc[2]:.4f})"
            
            elif line.startswith("TARGET_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_CENTER_DEFAULT = ({props.target_circle_center[0]:.4f}, {props.target_circle_center[1]:.4f}, {props.target_circle_center[2]:.4f})"
            elif line.startswith("TARGET_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_RADIUS_DEFAULT = {props.target_circle_radius:.4f}"
            elif line.startswith("TARGET_CIRCLE_ROT_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_ROT_DEFAULT = ({props.target_circle_rot[0]:.4f}, {props.target_circle_rot[1]:.4f}, {props.target_circle_rot[2]:.4f})"
            elif line.startswith("TARGET_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_START_ANGLE_DEFAULT = {props.target_circle_start_angle:.4f}"
            elif line.startswith("TARGET_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_END_ANGLE_DEFAULT = {props.target_circle_end_angle:.4f}"

            elif line.startswith("TARGET_SIZE_DEFAULT = "): lines[i] = f"TARGET_SIZE_DEFAULT = {props.target_size:.4f}"
            elif line.startswith("TARGET_COLOR_DEFAULT = "): lines[i] = f"TARGET_COLOR_DEFAULT = ({props.target_color[0]:.4f}, {props.target_color[1]:.4f}, {props.target_color[2]:.4f}, 1.0)"
            elif line.startswith("TARGET_RANDOM_COLOR_DEFAULT = "): lines[i] = f"TARGET_RANDOM_COLOR_DEFAULT = {props.target_random_color}"
            elif line.startswith("TARGET_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"

            elif line.startswith("CAM_BACK_DIST_DEFAULT = "): lines[i] = f"CAM_BACK_DIST_DEFAULT = {props.cam_back_dist:.4f}"
            
            elif line.startswith("CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"CAM_SHIELD_RADIUS_DEFAULT = {props.cam_shield_radius:.4f}"
            elif line.startswith("CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.cam_shield_hole_ratio_front:.4f}"
            elif line.startswith("CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.cam_shield_hole_ratio_back:.4f}"
            elif line.startswith("ALPHA_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_SHIELD_DEFAULT = {props.cam_shield_alpha:.4f}"
            elif line.startswith("COLOR_SHIELD = "): lines[i] = f"COLOR_SHIELD = ({props.cam_shield_color[0]:.4f}, {props.cam_shield_color[1]:.4f}, {props.cam_shield_color[2]:.4f}, 1.0)"

            elif line.startswith("BACK_CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_RADIUS_DEFAULT = {props.back_cam_shield_radius:.4f}"
            elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.back_cam_shield_hole_ratio_front:.4f}"
            elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.back_cam_shield_hole_ratio_back:.4f}"
            elif line.startswith("ALPHA_BACK_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_BACK_SHIELD_DEFAULT = {props.back_cam_shield_alpha:.4f}"
            elif line.startswith("COLOR_BACK_SHIELD = "): lines[i] = f"COLOR_BACK_SHIELD = ({props.back_cam_shield_color[0]:.4f}, {props.back_cam_shield_color[1]:.4f}, {props.back_cam_shield_color[2]:.4f}, 1.0)"

            elif line.startswith("CAMERA_COLOR_DEFAULT = "): lines[i] = f"CAMERA_COLOR_DEFAULT = ({props.camera_color[0]:.4f}, {props.camera_color[1]:.4f}, {props.camera_color[2]:.4f})"

            elif line.startswith("ANIM_START_FRAME_DEFAULT = "): lines[i] = f"ANIM_START_FRAME_DEFAULT = {props.anim_start_frame}"
            elif line.startswith("ANIM_END_FRAME_DEFAULT = "): lines[i] = f"ANIM_END_FRAME_DEFAULT = {props.anim_end_frame}"
            
            elif line.startswith("CAM_ROT_U_START = "): lines[i] = f"CAM_ROT_U_START = {props.cam_rot_start[0]:.4f}"
            elif line.startswith("CAM_ROT_V_START = "): lines[i] = f"CAM_ROT_V_START = {props.cam_rot_start[1]:.4f}"
            elif line.startswith("CAM_ROT_W_START = "): lines[i] = f"CAM_ROT_W_START = {props.cam_rot_start[2]:.4f}"
            
            elif line.startswith("CAM_ROT_SPEED_U_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_U_DEFAULT = {props.cam_rot_speed[0]:.4f}"
            elif line.startswith("CAM_ROT_SPEED_V_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_V_DEFAULT = {props.cam_rot_speed[1]:.4f}"
            elif line.startswith("CAM_ROT_SPEED_W_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_W_DEFAULT = {props.cam_rot_speed[2]:.4f}"
            
            elif line.startswith("CAM_OFFSET_U_DEFAULT = "): lines[i] = f"CAM_OFFSET_U_DEFAULT = {props.cam_local_offset_rot[0]:.4f}"
            elif line.startswith("CAM_OFFSET_V_DEFAULT = "): lines[i] = f"CAM_OFFSET_V_DEFAULT = {props.cam_local_offset_rot[1]:.4f}"
            elif line.startswith("CAM_OFFSET_W_DEFAULT = "): lines[i] = f"CAM_OFFSET_W_DEFAULT = {props.cam_local_offset_rot[2]:.4f}"
            
            elif line.startswith("PUPIL_SHOW_DEFAULT = "): lines[i] = f"PUPIL_SHOW_DEFAULT = {props.pupil_window_show}"
            elif line.startswith("PUPIL_DIST_DEFAULT = "): lines[i] = f"PUPIL_DIST_DEFAULT = {props.pupil_window_dist:.4f}"
            elif line.startswith("PUPIL_RADIUS_DEFAULT = "): lines[i] = f"PUPIL_RADIUS_DEFAULT = {props.pupil_window_radius:.4f}"
            elif line.startswith("PUPIL_THICKNESS_DEFAULT = "): lines[i] = f"PUPIL_THICKNESS_DEFAULT = {props.pupil_window_thickness:.4f}"
            elif line.startswith("PUPIL_SEGMENTS_DEFAULT = "): lines[i] = f"PUPIL_SEGMENTS_DEFAULT = {props.pupil_window_segments}"
            elif line.startswith("COLOR_PUPIL = "): lines[i] = f"COLOR_PUPIL = ({props.pupil_window_color[0]:.4f}, {props.pupil_window_color[1]:.4f}, {props.pupil_window_color[2]:.4f}, 1.0)"
            elif line.startswith("PUPIL_ROT_X_DEFAULT = "): lines[i] = f"PUPIL_ROT_X_DEFAULT = {props.pupil_window_rot[0]:.4f}"
            elif line.startswith("PUPIL_ROT_Y_DEFAULT = "): lines[i] = f"PUPIL_ROT_Y_DEFAULT = {props.pupil_window_rot[1]:.4f}"
            elif line.startswith("PUPIL_ROT_Z_DEFAULT = "): lines[i] = f"PUPIL_ROT_Z_DEFAULT = {props.pupil_window_rot[2]:.4f}"

        new_text = '\n'.join(lines)
        
        import time
        t_name = f"Custom_Camera_Addon_{int(time.time())}.py"
        text_data = bpy.data.texts.new(name=t_name)
        text_data.write(new_text)
        
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR':
                area.spaces.active.text = text_data
                break
                
        self.report({'INFO'}, f"現在の設定を {t_name} に書き出しました")
        return {'FINISHED'}

# ===================================
# 9. パネル構成
# ===================================

class VIEW3D_PT_main(bpy.types.Panel):
    bl_label = "Viewport & Grid"
    bl_idname = f"{PREFIX.upper()}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    
    def draw(self, context):
        layout = self.layout
        layout.operator(f"view3d.{PREFIX}_reset_view", icon='ZOOM_ALL')

class VIEW3D_PT_camera(bpy.types.Panel):
    bl_label = "Fixed Camera Rig (一体化)"
    bl_idname = f"{PREFIX.upper()}_PT_camera"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        
        box = layout.box()
        box.label(text="1. カメラリグの配置 (移動)", icon='CAMERA_DATA')
        box.prop(props, "cam_move_mode", expand=True)
        col_loc = box.column(align=True)
        
        if props.cam_move_mode == 'FIXED':
            col_loc.prop(props, "cam_fixed_loc", index=0, text=f"{LABEL_LOC} {LABEL_U}")
            col_loc.prop(props, "cam_fixed_loc", index=1, text=f"{LABEL_LOC} {LABEL_V}")
            col_loc.prop(props, "cam_fixed_loc", index=2, text=f"{LABEL_LOC} {LABEL_W}")
        elif props.cam_move_mode == 'LINE':
            col_loc.label(text="出発点:")
            col_loc.prop(props, "cam_start_loc", index=0, text="X")
            col_loc.prop(props, "cam_start_loc", index=1, text="Y")
            col_loc.prop(props, "cam_start_loc", index=2, text="Z")
            col_loc.separator()
            col_loc.label(text="到達点:")
            col_loc.prop(props, "cam_end_loc", index=0, text="X")
            col_loc.prop(props, "cam_end_loc", index=1, text="Y")
            col_loc.prop(props, "cam_end_loc", index=2, text="Z")
            col_loc.separator()
            col_loc.prop(props, "cam_anim_speed_mult", slider=True)
        elif props.cam_move_mode == 'CIRCLE':
            col_loc.label(text="中心位置:")
            col_loc.prop(props, "cam_circle_center", index=0, text="X")
            col_loc.prop(props, "cam_circle_center", index=1, text="Y")
            col_loc.prop(props, "cam_circle_center", index=2, text="Z")
            col_loc.separator()
            col_loc.prop(props, "cam_circle_radius")
            col_loc.separator()
            col_loc.label(text="平面の傾き (XYZ):")
            col_loc.prop(props, "cam_circle_rot", index=0, text="X (Pitch)")
            col_loc.prop(props, "cam_circle_rot", index=1, text="Y (Roll)")
            col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Yaw)")
            col_loc.separator()
            col_loc.prop(props, "cam_circle_start_angle")
            col_loc.prop(props, "cam_circle_end_angle")
            col_loc.separator()
            col_loc.prop(props, "cam_anim_speed_mult", slider=True)
            
        box.separator()
        row_cam = box.row(align=True)
        row_cam.operator(f"object.{PREFIX}_create_cam_rig", icon='ADD')
        row_cam.operator(f"object.{PREFIX}_set_active_camera", text="", icon='RESTRICT_RENDER_OFF')
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if root and cam:
            box_tgt = layout.box()
            box_tgt.label(text="2. 注視点 (Track To) 設定", icon='TRACKING')
            box_tgt.prop(props, "cam_target_mode", expand=False)
            
            if props.cam_target_mode == 'POINT':
                col_tgt = box_tgt.column(align=True)
                col_tgt.prop(props, "cam_target_loc", index=0, text=f"注視点 {LABEL_U}")
                col_tgt.prop(props, "cam_target_loc", index=1, text=f"注視点 {LABEL_V}")
                col_tgt.prop(props, "cam_target_loc", index=2, text=f"注視点 {LABEL_W}")
                
            elif props.cam_target_mode == 'LINE':
                col_tgt = box_tgt.column(align=True)
                col_tgt.label(text="出発点:")
                col_tgt.prop(props, "target_start_loc", index=0, text="X")
                col_tgt.prop(props, "target_start_loc", index=1, text="Y")
                col_tgt.prop(props, "target_start_loc", index=2, text="Z")
                col_tgt.separator()
                col_tgt.label(text="到達点:")
                col_tgt.prop(props, "target_end_loc", index=0, text="X")
                col_tgt.prop(props, "target_end_loc", index=1, text="Y")
                col_tgt.prop(props, "target_end_loc", index=2, text="Z")
                
            elif props.cam_target_mode == 'CIRCLE':
                col_tgt = box_tgt.column(align=True)
                col_tgt.label(text="中心位置:")
                col_tgt.prop(props, "target_circle_center", index=0, text="X")
                col_tgt.prop(props, "target_circle_center", index=1, text="Y")
                col_tgt.prop(props, "target_circle_center", index=2, text="Z")
                col_tgt.separator()
                col_tgt.prop(props, "target_circle_radius")
                col_tgt.separator()
                col_tgt.label(text="平面の傾き (XYZ):")
                col_tgt.prop(props, "target_circle_rot", index=0, text="X (Pitch)")
                col_tgt.prop(props, "target_circle_rot", index=1, text="Y (Roll)")
                col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Yaw)")
                col_tgt.separator()
                col_tgt.prop(props, "target_circle_start_angle")
                col_tgt.prop(props, "target_circle_end_angle")
                
            elif props.cam_target_mode == 'OBJECT':
                box_tgt.prop(props, "cam_target_obj")

            if props.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
                col_tgt = box_tgt.column(align=True)
                col_tgt.separator()
                col_tgt.prop(props, "target_size")
                col_tgt.prop(props, "target_random_color")
                if not props.target_random_color:
                    col_tgt.prop(props, "target_color")
                    
            if props.cam_target_mode in ['LINE', 'CIRCLE']:
                col_tgt = box_tgt.column(align=True)
                col_tgt.separator()
                col_tgt.prop(props, "target_anim_speed_mult", slider=True)

            box_offset = layout.box()
            box_offset.label(text="補助: 注視点ベースの角度ズレ", icon='ORIENTATION_LOCAL')
            col_off = box_offset.column(align=True)
            col_off.prop(props, "cam_local_offset_rot", index=0, text=f"角度ズレ {LABEL_U} (Pitch)")
            col_off.prop(props, "cam_local_offset_rot", index=1, text=f"角度ズレ {LABEL_V} (Roll)")
            col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Yaw)")

            box_rot = layout.box()
            box_rot.label(text="3. アニメーション設定", icon='ANIM_DATA')
            if props.cam_target_mode != 'NONE':
                box_rot.label(text="※速度ベースのRoot回転は無効化されています", icon='INFO')
            
            if props.cam_target_mode == 'NONE':
                col_rot = box_rot.column(align=True)
                col_rot.label(text="Rootの速度ベース回転:")
                col_rot.prop(props, "cam_rot_start", index=0, text=f"初期 {LABEL_U} (Pitch)")
                col_rot.prop(props, "cam_rot_start", index=1, text=f"初期 {LABEL_V} (Roll)")
                col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Yaw)")
                col_rot.separator()
                col_rot.prop(props, "cam_rot_speed", index=0, text=f"速度 {LABEL_U} (度/F)")
                col_rot.prop(props, "cam_rot_speed", index=1, text=f"速度 {LABEL_V} (度/F)")
                col_rot.prop(props, "cam_rot_speed", index=2, text=f"速度 {LABEL_W} (度/F)")
                row_hint = box_rot.row()
                row_hint.label(text="方向ガイド (UVW):", icon='QUESTION')
                col_hint = box_rot.column(align=True)
                col_hint.scale_y = 0.8
                col_hint.label(text=f"{LABEL_U}+: 下向 / {LABEL_U}-: 上向")
                col_hint.label(text=f"{LABEL_V}+: 左傾 / {LABEL_V}-: 右傾")
                col_hint.label(text=f"{LABEL_W}+: 左向 / {LABEL_W}-: 右向")
                box_rot.separator()
            
            col_f = box_rot.column(align=True)
            row_f = col_f.row(align=True)
            row_f.prop(props, "anim_start_frame")
            row_f.prop(props, "anim_end_frame")
            
            row_sync = col_f.row(align=True)
            row_sync.operator(f"object.{PREFIX}_sync_from_scene", icon='IMPORT')
            row_sync.operator(f"object.{PREFIX}_sync_to_scene", icon='EXPORT')
            
            box_rot.separator()
            
            col_btn = box_rot.column(align=True)
            col_btn.operator(f"object.{PREFIX}_apply_cam_anim", icon='KEYINGSET')
            if not context.screen.is_animation_playing:
                col_btn.operator("screen.animation_play", text="アニメーションを再生", icon='PLAY')
            else:
                col_btn.operator("screen.animation_play", text="アニメーションを停止", icon='PAUSE')
                
            box_rot.separator()

            # --- 視点切り替えボタン & 背後カメラの距離 ---
            col_switch = box_rot.column(align=True)
            row_switch = col_switch.row(align=True)
            row_switch.prop(props, "active_camera", expand=True)
            col_switch.prop(props, "cam_back_dist", text="背後カメラの距離", slider=True)
            box_rot.separator()

            # --- 画面情報表示と更新・コピー機能 ---
            box_info = box_rot.box()
            row_info_head = box_info.row(align=True)
            row_info_head.label(text="画面情報", icon='INFO')
            row_info_head.operator(f"object.{PREFIX}_update_camera_info", text="情報更新", icon='FILE_REFRESH')
            
            box_info.label(text=f"投影モード: {props.info_proj_mode}")
            box_info.label(text=f"表示カメラ: {props.info_active_camera_name}")
            box_info.label(text=f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}")
            box_info.label(text=f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}")
            box_info.label(text=f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}")
            box_info.label(text=f"画面水平の大きさ: {props.info_view_width:.1f}")
            box_info.label(text=f"画面上下の大きさ: {props.info_view_height:.1f}")
            box_info.label(text=f"視座からの距離: {props.info_view_dist:.1f}")
            box_info.separator()
            
            for line in props.info_target_path.split('\n'):
                box_info.label(text=line, icon='EDITMODE_HLT')
            
            box_info.operator(f"object.{PREFIX}_copy_camera_info", icon='COPYDOWN')

            # --- 4. メインカメラ 包み・線画設定 ---
            box_vis = layout.box()
            box_vis.label(text="4. カメラ色(共通テーマ) & メイン包み", icon='RESTRICT_VIEW_OFF')
            
            row_disp = box_vis.row(align=True)
            row_disp.prop(props, "show_cam_obj", text="カメラ")
            row_disp.prop(props, "show_target_empty", text="Empty") 
            row_disp.prop(props, "show_cam_shield", text="包み表示")
            
            col_vis = box_vis.column(align=True)
            row_cam_col = col_vis.row(align=True)
            row_cam_col.prop(props, "camera_preset", text="")
            row_cam_col.prop(props, "camera_color", text="")
            col_vis.separator()

            if props.show_cam_shield:
                col_vis.label(text="メインカメラ 包み設定", icon='MESH_UVSPHERE')
                col_vis.prop(props, "cam_shield_radius")
                col_vis.prop(props, "cam_shield_hole_ratio_front")
                col_vis.prop(props, "cam_shield_hole_ratio_back")
                col_vis.separator()
                col_vis.prop(props, "cam_shield_color")
                col_vis.prop(props, "cam_shield_alpha", slider=True)

            # --- 5. 背後霊カメラ 包み設定 ---
            box_back = layout.box()
            box_back.label(text="5. 背後霊カメラ 包み設定", icon='OUTLINER_OB_CAMERA')
            
            row_back_disp = box_back.row(align=True)
            row_back_disp.prop(props, "show_back_cam_shield", text="背後包み表示")
            
            col_back = box_back.column(align=True)

            if props.show_back_cam_shield:
                col_back.separator()
                col_back.prop(props, "back_cam_shield_radius")
                col_back.prop(props, "back_cam_shield_hole_ratio_front")
                col_back.prop(props, "back_cam_shield_hole_ratio_back")
                col_back.separator()
                col_back.prop(props, "back_cam_shield_color")
                col_back.prop(props, "back_cam_shield_alpha", slider=True)
                
            # --- 6. 瞳孔窓面 設定 ---
            box_pupil = layout.box()
            box_pupil.label(text="6. 瞳孔窓面 (前方の枠)", icon='MESH_TORUS')
            box_pupil.prop(props, "pupil_window_show", text="表示する")
            if props.pupil_window_show:
                col_p = box_pupil.column(align=True)
                col_p.prop(props, "pupil_window_dist", text="距離 (1-100)", slider=True)
                col_p.prop(props, "pupil_window_segments", text="角数 (3-32)")
                
                if props.pupil_window_segments == 4:
                    col_p.prop(props, "pupil_window_radius", text="1辺の長さ")
                else:
                    col_p.prop(props, "pupil_window_radius", text="半径")
                    
                col_p.prop(props, "pupil_window_thickness")
                col_p.prop(props, "pupil_window_color")
                col_p.separator()
                
                col_p.label(text="ローカル回転 (XYZ):")
                col_p.prop(props, "pupil_window_rot", index=0, text="X (Pitch)")
                col_p.prop(props, "pupil_window_rot", index=1, text="Y (Roll)")
                col_p.prop(props, "pupil_window_rot", index=2, text="Z (Yaw)")
                
                
            # --- 7. レンズ・カメラ設定 (統合) ---
            box_lens = layout.box()
            box_lens.label(text="7. レンズ・カメラ設定", icon='VIEW_CAMERA')
            col_lens = box_lens.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="クリップ開始")
            col_lens.prop(cam.data, "clip_end", text="クリップ終了")
            col_lens.separator()
            col_lens.prop(cam.data, "show_passepartout", text="枠外を暗くする")
            if cam.data.show_passepartout:
                col_lens.prop(cam.data, "passepartout_alpha", text="枠外の暗さ", slider=True)
            col_lens.separator()
            col_lens.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)")
            if cam.data.dof.use_dof:
                if cam.data.dof.focus_object:
                    col_lens.prop(cam.data.dof, "focus_object", text="ピント対象")
                col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
                
            box_exp = layout.box()
            box_exp.label(text="8. 設定値の保存 (自己書き出し)", icon='TEXT')
            box_exp.operator(f"object.{PREFIX}_export_addon", icon='EXPORT')

class VIEW3D_PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX.upper()}_PT_links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        if LINK_NAME_1 and LINK_URL_1: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL').url = LINK_URL_1
        if LINK_NAME_2 and LINK_URL_2: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        if LINK_NAME_3 and LINK_URL_3: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        if LINK_NAME_4 and LINK_URL_4: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_4, icon='URL').url = LINK_URL_4
        if LINK_NAME_5 and LINK_URL_5: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_5, icon='URL').url = LINK_URL_5

class VIEW3D_PT_remove(bpy.types.Panel):
    bl_label = "Addon Remove"
    bl_idname = f"{PREFIX.upper()}_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')

# ===================================
# 10. register()
# ===================================
classes = [
    CP_OT_open_url,
    CP_OT_remove_addon,
    ADDON_Properties,
    VIEW3D_OT_reset_view,
    OBJECT_OT_create_cam_rig,
    OBJECT_OT_sync_from_scene,
    OBJECT_OT_sync_to_scene,
    OBJECT_OT_apply_cam_anim,
    OBJECT_OT_switch_cam_main,
    OBJECT_OT_switch_cam_back,
    OBJECT_OT_set_active_camera,
    OBJECT_OT_update_camera_info,
    OBJECT_OT_copy_camera_info,
    OBJECT_OT_export_addon,
    VIEW3D_PT_main,
    VIEW3D_PT_camera,
    VIEW3D_PT_links,
    VIEW3D_PT_remove
]

def register():
    for c in classes:
        bpy.utils.register_class(c)
    setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=ADDON_Properties))
    
    def apply_initial_settings():
        if bpy.context.scene and hasattr(bpy.context.scene, f"{PREFIX}_props"):
            props = getattr(bpy.context.scene, f"{PREFIX}_props")
            try:
                bpy.context.preferences.themes[0].view_3d.camera = props.camera_color
            except Exception:
                pass

        if bpy.context.window_manager:
            for window in bpy.context.window_manager.windows:
                for area in window.screen.areas:
                    if area.type == 'VIEW_3D':
                        for space in area.spaces:
                            if space.type == 'VIEW_3D':
                                space.show_region_ui = True
                                space.shading.type = 'MATERIAL'
                                
        return None

    bpy.app.timers.register(apply_initial_settings, first_interval=0.1)

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

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
bl_info = {
    "name": "Fixed Camera & Automation Master",
    "author": "Your Name",
    "version": (4, 23),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "固定位置カメラ(カメラ&ターゲットの移動軌跡描画機能追加)",
    "category": "3D View",
}

import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import sys

# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed & View Master"
PREFIX_VAL = "CAM_FVIEW_MASTER"
ADDON_CATEGORY = " Cam Fixed "
PREFIX = PREFIX_VAL.lower()

# ===================================
# 5. リンク定義
# ===================================
LINK_NAME_1 = "背後霊カメラ 最新版リンク"
LINK_URL_1 = "<https://app.notion.com/p/Cam-blender-39f347b3454a806490a6f0107c2347dd>"

LINK_NAME_2 = "posfie"
LINK_URL_2 = "<https://posfie.com/@timekagura/t/zionad2022?sort=0>"

LINK_NAME_3 = "zionadchat"
LINK_URL_3 = "<https://x.com/zionadchat>"

LINK_NAME_4 = ""
LINK_URL_4 = ""

LINK_NAME_5 = ""
LINK_URL_5 = ""

# ===================================
# 6. 初期設定
# ===================================
COLLECTION_NAME_CAM = "FixedCam_Collection"
COLLECTION_SUB_NAME_CAM = "FixedCam_SubCollection"
OBJ_NAME_ROOT = "FixedCamera_Root"
OBJ_NAME_CAMERA = "FixedCamera"
OBJ_NAME_CAMERA_BACK = "FixedCamera_Observer"
OBJ_NAME_TARGET = "FixedCamera_Target"
OBJ_NAME_PUPIL = "瞳孔窓面"

OBJ_NAME_CAM_PATH = "FixedCamera_CamPath"
OBJ_NAME_TGT_PATH = "FixedCamera_TargetPath"

SHIELD_NAME = "FixedCam_Shield_Sphere"
CUTTER_NAME = "FixedCam_Shield_Cutter"
CUTTER_BACK_NAME = "FixedCam_Shield_CutterBack"
MAT_NAME_SHIELD = "FixedCam_Shield_Mat"

BACK_SHIELD_NAME = "FixedCam_BackShield_Sphere"
BACK_CUTTER_NAME = "FixedCam_BackShield_Cutter"
BACK_CUTTER_BACK_NAME = "FixedCam_BackShield_CutterBack"
MAT_NAME_BACK_SHIELD = "FixedCam_BackShield_Mat"

MAT_NAME_PUPIL = "Pupil_Window_Mat"
MAT_NAME_TARGET = "FixedCam_Target_Mat"

CAMERA_PRESETS = [
    ("Cam", "Cam", "Cam camera color", (0.47, 0.55, 1.00)),
    ("Cam 4.4.0", "Cam 4.4.0", "Cam 4.4.0 color", (0.00, 0.00, 0.00)),
    ("YELLOW", "Yellow", "Yellow camera", (1.00, 1.00, 0.00)),
]
CAMERA_COLOR_DEFAULT = (0.1483, 1.0000, 0.1110)

CAM_LOC_U_DEFAULT = 0.0000
CAM_LOC_V_DEFAULT = 0.0000
CAM_LOC_W_DEFAULT = 0.0000

# カメラの移動用
CAM_START_LOC_DEFAULT = (-50.0, 0.0, 0.0)
CAM_END_LOC_DEFAULT = (50.0, 0.0, 0.0)
CAM_CIRCLE_CENTER_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_RADIUS_DEFAULT = 20.0
CAM_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
CAM_CIRCLE_START_ANGLE_DEFAULT = 0.0
CAM_CIRCLE_END_ANGLE_DEFAULT = 360.0
CAM_ANIM_SPEED_MULT_DEFAULT = 1.0

CAM_TGT_U_DEFAULT = 0.0000
CAM_TGT_V_DEFAULT = 0.0000
CAM_TGT_W_DEFAULT = 0.0000

# ターゲット(線分・円周移動用)
TARGET_START_DEFAULT = (-50.0, 5.0, 0.0)
TARGET_END_DEFAULT = (50.0, 5.0, 0.0)
TARGET_CIRCLE_CENTER_DEFAULT = (0.0, 5.0, 0.0)
TARGET_CIRCLE_RADIUS_DEFAULT = 10.0
TARGET_CIRCLE_ROT_DEFAULT = (0.0, 0.0, 0.0)
TARGET_CIRCLE_START_ANGLE_DEFAULT = 0.0
TARGET_CIRCLE_END_ANGLE_DEFAULT = 360.0

TARGET_SIZE_DEFAULT = 1.0
TARGET_COLOR_DEFAULT = (0.0000, 0.8000, 0.2000, 1.0)
TARGET_RANDOM_COLOR_DEFAULT = False
TARGET_ANIM_SPEED_MULT_DEFAULT = 1.0

# 軌跡用初期設定
SHOW_CAM_PATH_DEFAULT = True
CAM_PATH_THICKNESS_DEFAULT = 0.05
CAM_PATH_COLOR_DEFAULT = (1.0, 0.2, 0.2, 1.0)

SHOW_TARGET_PATH_DEFAULT = True
TARGET_PATH_THICKNESS_DEFAULT = 0.05
TARGET_PATH_COLOR_DEFAULT = (0.2, 0.8, 1.0, 1.0)

CAM_SHIELD_RADIUS_DEFAULT = 1.7600
CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000

BACK_CAM_SHIELD_RADIUS_DEFAULT = 1.7600
BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = 0.9400
BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = 0.4000

CAM_BACK_DIST_DEFAULT = 10.0000

PUPIL_SHOW_DEFAULT = True
PUPIL_DIST_DEFAULT = 5.0000
PUPIL_RADIUS_DEFAULT = 2.0000
PUPIL_THICKNESS_DEFAULT = 0.0500
PUPIL_SEGMENTS_DEFAULT = 4
COLOR_PUPIL = (1.0000, 1.0000, 1.0000, 1.0)
PUPIL_ROT_X_DEFAULT = 0.0000
PUPIL_ROT_Y_DEFAULT = 0.0000
PUPIL_ROT_Z_DEFAULT = 0.7854

ANIM_START_FRAME_DEFAULT = 1
ANIM_END_FRAME_DEFAULT = 500

CUSTOM_GRID_SCALE_DEFAULT = 50.0

CAM_ROT_U_START = 1.5708
CAM_ROT_V_START = 0.0000
CAM_ROT_W_START = 0.0000

CAM_ROT_SPEED_U_DEFAULT = 0.0000
CAM_ROT_SPEED_V_DEFAULT = 0.0000
CAM_ROT_SPEED_W_DEFAULT = 0.3000

CAM_OFFSET_U_DEFAULT = 0.0000
CAM_OFFSET_V_DEFAULT = 0.0000
CAM_OFFSET_W_DEFAULT = 0.0000

ALPHA_SHIELD_DEFAULT = 1.0000
ALPHA_BACK_SHIELD_DEFAULT = 1.0000

COLOR_SHIELD = (0.2565, 0.0141, 0.1115, 1.0)
COLOR_BACK_SHIELD = (0.0141, 0.2565, 0.1115, 1.0)

LABEL_SIZE = "サイズ"
LABEL_U = "U"
LABEL_V = "V"
LABEL_W = "W"
LABEL_LOC = "座標"
LABEL_START = "開始"
LABEL_END = "終了"

# ===================================
# 7. 共通クラス
# ===================================
class CP_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX}_open_url"
    bl_label = "リンクを開く"
    url: bpy.props.StringProperty()
    def execute(self, context):
        if self.url: webbrowser.open(self.url)
        return {'FINISHED'}

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

# ===================================
# 8. アドオン固有機能
# ===================================

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 get_addon_source():
    source = ""
    try:
        if '__file__' in globals() and os.path.exists(__file__):
            with open(__file__, 'r', encoding='utf-8') as f:
                source = f.read()
    except Exception: pass
    if not source:
        for t in bpy.data.texts:
            if f'ADDON_TITLE = "{ADDON_TITLE}"' in t.as_string():
                source = t.as_string()
                break
    return source

def cam_fov_get(self):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam and cam.type == 'CAMERA': return math.degrees(cam.data.angle)
    return 50.0

def cam_fov_set(self, value):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam and cam.type == 'CAMERA':
        cam.data.lens_unit = 'FOV'
        cam.data.angle = math.radians(value)

def update_visibility(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam:
        cam.hide_viewport = not self.show_cam_obj
        
    for name in [OBJ_NAME_ROOT, OBJ_NAME_TARGET, CUTTER_NAME, CUTTER_BACK_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME]:
        obj = bpy.data.objects.get(name)
        if obj:
            obj.hide_viewport = not self.show_target_empty

def create_or_update_curve(obj_name, points, thickness, color, show):
    col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM)
    if not col: return
    
    obj = bpy.data.objects.get(obj_name)
    if not obj:
        curve_data = bpy.data.curves.new(f"{obj_name}_Curve", type='CURVE')
        curve_data.dimensions = '3D'
        curve_data.fill_mode = 'FULL'
        obj = bpy.data.objects.new(obj_name, curve_data)
        col.objects.link(obj)
    else:
        curve_data = obj.data
        
    obj.hide_viewport = not show
    obj.hide_render = not show
    curve_data.bevel_depth = thickness
    
    if not show or not points:
        curve_data.splines.clear()
        return

    mat_name = f"{obj_name}_Mat"
    mat = bpy.data.materials.get(mat_name)
    if not mat:
        mat = bpy.data.materials.new(mat_name)
        mat.use_nodes = True
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf and 'Base Color' in bsdf.inputs:
        bsdf.inputs['Base Color'].default_value = color
        
    if len(curve_data.materials) == 0:
        curve_data.materials.append(mat)
    else:
        curve_data.materials[0] = mat

    spline = curve_data.splines[0] if curve_data.splines else curve_data.splines.new('POLY')
    if len(spline.points) != len(points):
        curve_data.splines.clear()
        spline = curve_data.splines.new('POLY')
        spline.points.add(len(points) - 1)
        
    for i, p in enumerate(points):
        spline.points[i].co = (p.x, p.y, p.z, 1.0)

def update_target_visuals(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if tgt:
        tgt.scale = (self.target_size, self.target_size, self.target_size)
        mesh = tgt.data
        
        if len(mesh.materials) < 6:
            mesh.materials.clear()
            for i in range(6):
                mat = bpy.data.materials.new(name=f"{MAT_NAME_TARGET}_{i}")
                mat.use_nodes = True
                mesh.materials.append(mat)
                
        colors = [
            (1.0, 0.0, 0.0, 1.0), # 赤
            (0.0, 1.0, 0.0, 1.0), # 緑
            (0.0, 0.5, 1.0, 1.0), # 青
            (1.0, 1.0, 0.0, 1.0), # 黄
            (0.0, 1.0, 1.0, 1.0), # シアン
            (1.0, 0.0, 1.0, 1.0), # マゼンタ
        ]

        for i in range(6):
            mat = mesh.materials[i]
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf and 'Base Color' in bsdf.inputs:
                if self.target_random_color:
                    bsdf.inputs['Base Color'].default_value = colors[i]
                else:
                    bsdf.inputs['Base Color'].default_value = self.target_color
            
        for i, poly in enumerate(mesh.polygons):
            poly.material_index = i % 6 if self.target_random_color else 0

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_target_anim(self, context):
    tgt = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not tgt: return
    
    if tgt.animation_data:
        tgt.animation_data.action = None
        
    start_f = self.anim_start_frame
    end_f = self.anim_end_frame
    delta_f = end_f - start_f
    speed = self.target_anim_speed_mult
    
    pts = []
        
    if self.cam_target_mode == 'POINT':
        tgt.location = self.cam_target_loc
        pts.append(self.cam_target_loc)
        
    elif self.cam_target_mode == 'LINE':
        s_loc = mathutils.Vector(self.target_start_loc)
        e_loc = mathutils.Vector(self.target_end_loc)
        
        if delta_f <= 0:
            tgt.location = s_loc
            tgt.keyframe_insert(data_path="location", frame=start_f)
            pts.append(s_loc)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                pp = progress % 2.0
                if pp < 0: pp += 2.0
                ratio = pp if pp <= 1.0 else 2.0 - pp
                
                loc = s_loc.lerp(e_loc, ratio)
                tgt.location = loc
                tgt.keyframe_insert(data_path="location", frame=f)
                pts.append(loc)
                
        if tgt.animation_data and tgt.animation_data.action:
            act = tgt.animation_data.action
            fcs = getattr(act, "fcurves", None)
            if fcs:
                for fc in fcs:
                    if fc.data_path == 'location':
                        for kp in fc.keyframe_points:
                            kp.interpolation = 'LINEAR'
                            
    elif self.cam_target_mode == 'CIRCLE':
        rot_mat = mathutils.Euler(self.target_circle_rot, 'XYZ').to_matrix()
        center = mathutils.Vector(self.target_circle_center)
        radius = self.target_circle_radius
        start_rad = math.radians(self.target_circle_start_angle)
        end_rad = math.radians(self.target_circle_end_angle)
        
        if delta_f <= 0:
            local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
            loc = center + rot_mat @ local_pos
            tgt.location = loc
            tgt.keyframe_insert(data_path="location", frame=start_f)
            pts.append(loc)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                t = start_rad + (end_rad - start_rad) * progress
                local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
                loc = center + rot_mat @ local_pos
                tgt.location = loc
                tgt.keyframe_insert(data_path="location", frame=f)
                pts.append(loc)
                
        if tgt.animation_data and tgt.animation_data.action:
            act = tgt.animation_data.action
            fcs = getattr(act, "fcurves", None)
            if fcs:
                for fc in fcs:
                    if fc.data_path == 'location':
                        for kp in fc.keyframe_points:
                            kp.interpolation = 'LINEAR'
    
    # ターゲット軌跡の描画
    create_or_update_curve(OBJ_NAME_TGT_PATH, pts, self.target_path_thickness, self.target_path_color, self.show_target_path)
                            
    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_anim(self, context):
    update_target_anim(self, context)
    
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    if not root: return
        
    if root.animation_data:
        root.animation_data.action = None
            
    start_f = self.anim_start_frame
    end_f = self.anim_end_frame
    delta_f = end_f - start_f
    
    pts = []
    
    # 1. カメラ(Root)の位置ベイク
    if self.cam_move_mode == 'FIXED':
        loc = mathutils.Vector(self.cam_fixed_loc)
        root.location = loc
        root.keyframe_insert(data_path="location", frame=start_f)
        pts.append(loc)
        
    elif self.cam_move_mode == 'LINE':
        s_loc = mathutils.Vector(self.cam_start_loc)
        e_loc = mathutils.Vector(self.cam_end_loc)
        speed = self.cam_anim_speed_mult
        
        if delta_f <= 0:
            root.location = s_loc
            root.keyframe_insert(data_path="location", frame=start_f)
            pts.append(s_loc)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                pp = progress % 2.0
                if pp < 0: pp += 2.0
                ratio = pp if pp <= 1.0 else 2.0 - pp
                
                loc = s_loc.lerp(e_loc, ratio)
                root.location = loc
                root.keyframe_insert(data_path="location", frame=f)
                pts.append(loc)
                
    elif self.cam_move_mode == 'CIRCLE':
        rot_mat = mathutils.Euler(self.cam_circle_rot, 'XYZ').to_matrix()
        center = mathutils.Vector(self.cam_circle_center)
        radius = self.cam_circle_radius
        start_rad = math.radians(self.cam_circle_start_angle)
        end_rad = math.radians(self.cam_circle_end_angle)
        speed = self.cam_anim_speed_mult
        
        if delta_f <= 0:
            local_pos = mathutils.Vector((radius * math.cos(start_rad), radius * math.sin(start_rad), 0.0))
            loc = center + rot_mat @ local_pos
            root.location = loc
            root.keyframe_insert(data_path="location", frame=start_f)
            pts.append(loc)
        else:
            for f in range(start_f, end_f + 1):
                progress = ((f - start_f) / delta_f) * speed
                t = start_rad + (end_rad - start_rad) * progress
                local_pos = mathutils.Vector((radius * math.cos(t), radius * math.sin(t), 0.0))
                loc = center + rot_mat @ local_pos
                root.location = loc
                root.keyframe_insert(data_path="location", frame=f)
                pts.append(loc)

    # カメラ軌跡の描画
    create_or_update_curve(OBJ_NAME_CAM_PATH, pts, self.cam_path_thickness, self.cam_path_color, self.show_cam_path)

    # 2. カメラ(Root)の回転ベイク (ターゲットが無い場合のみ)
    if self.cam_target_mode == 'NONE':
        root.rotation_mode = 'XYZ'
        if delta_f <= 0:
            root.rotation_euler = self.cam_rot_start
            root.keyframe_insert(data_path="rotation_euler", frame=start_f)
        else:
            speed_u = math.radians(self.cam_rot_speed[0])
            speed_v = math.radians(self.cam_rot_speed[1])
            speed_w = math.radians(self.cam_rot_speed[2])

            start_rot = self.cam_rot_start
            end_rot = (
                start_rot[0] + speed_u * delta_f,
                start_rot[1] + speed_v * delta_f,
                start_rot[2] + speed_w * delta_f
            )

            root.rotation_euler = start_rot
            root.keyframe_insert(data_path="rotation_euler", frame=start_f)
            root.rotation_euler = end_rot
            root.keyframe_insert(data_path="rotation_euler", frame=end_f)

    # Fcurveの線形補間適用
    if root.animation_data and root.animation_data.action:
        act = root.animation_data.action
        fcs = getattr(act, "fcurves", None)
        if fcs:
            for fc in fcs:
                for kp in fc.keyframe_points:
                    kp.interpolation = 'LINEAR'

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_root_constraint(self, context):
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    tgt_obj = bpy.data.objects.get(OBJ_NAME_TARGET)
    if not root: return

    tt = next((c for c in root.constraints if c.type == 'TRACK_TO'), None)
    
    if self.cam_target_mode == 'NONE':
        if tt: root.constraints.remove(tt)
        update_cam_anim(self, context) 
    else:
        if not tt: tt = root.constraints.new(type='TRACK_TO')
        tt.track_axis = 'TRACK_NEGATIVE_Z'
        tt.up_axis = 'UP_Y'
        
        if self.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
            if tgt_obj:
                tt.target = tgt_obj
            update_target_anim(self, context)
        elif self.cam_target_mode == 'OBJECT':
            tt.target = self.cam_target_obj

    if bpy.context.scene:
        bpy.context.scene.frame_set(bpy.context.scene.frame_current)

def update_cam_root_loc(self, context):
    root = bpy.data.objects.get(OBJ_NAME_ROOT)
    if root: root.location = self.cam_fixed_loc

def update_cam_local_offset_rot(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if cam: cam.rotation_euler = self.cam_local_offset_rot

def update_cam_shield(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    if not cam: return
    shield = next((c for c in cam.children if SHIELD_NAME in c.name), None)
    cutter = next((c for c in cam.children if CUTTER_NAME in c.name), None)
    cutter_back = next((c for c in cam.children if CUTTER_BACK_NAME in c.name), None)
    
    if shield:
        shield.scale = (self.cam_shield_radius,) * 3
        shield.hide_viewport = not self.show_cam_shield
    
    z_scale = self.cam_shield_radius * 3.0 
    
    if cutter:
        r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_front)
        cutter.scale = (r, r, z_scale)
        cutter.display_type = 'WIRE'
        cutter.hide_render = True
        
    if cutter_back:
        r = max(0.001, self.cam_shield_radius * self.cam_shield_hole_ratio_back)
        cutter_back.scale = (r, r, z_scale)
        cutter_back.display_type = 'WIRE'
        cutter_back.hide_render = True
        
    if shield and shield.data.materials:
        mat = shield.data.materials[0]
        mat.diffuse_color = (*self.cam_shield_color[:3], self.cam_shield_alpha)
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs: 
                    bsdf.inputs['Base Color'].default_value = (*self.cam_shield_color[:3], 1.0)
                if 'Alpha' in bsdf.inputs: 
                    bsdf.inputs['Alpha'].default_value = self.cam_shield_alpha

def update_back_cam_shield(self, context):
    cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
    if not cam: return
    shield = next((c for c in cam.children if BACK_SHIELD_NAME in c.name), None)
    cutter = next((c for c in cam.children if BACK_CUTTER_NAME in c.name), None)
    cutter_back = next((c for c in cam.children if BACK_CUTTER_BACK_NAME in c.name), None)
    
    if shield:
        shield.scale = (self.back_cam_shield_radius,) * 3
        shield.hide_viewport = not self.show_back_cam_shield
    
    z_scale = self.back_cam_shield_radius * 3.0 
    
    if cutter:
        r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_front)
        cutter.scale = (r, r, z_scale)
        cutter.display_type = 'WIRE'
        cutter.hide_render = True
        
    if cutter_back:
        r = max(0.001, self.back_cam_shield_radius * self.back_cam_shield_hole_ratio_back)
        cutter_back.scale = (r, r, z_scale)
        cutter_back.display_type = 'WIRE'
        cutter_back.hide_render = True
        
    if shield and shield.data.materials:
        mat = shield.data.materials[0]
        mat.diffuse_color = (*self.back_cam_shield_color[:3], self.back_cam_shield_alpha)
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs: 
                    bsdf.inputs['Base Color'].default_value = (*self.back_cam_shield_color[:3], 1.0)
                if 'Alpha' in bsdf.inputs: 
                    bsdf.inputs['Alpha'].default_value = self.back_cam_shield_alpha

def update_cam_back_dist(self, context):
    cam_back = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
    if cam_back:
        cam_back.location.z = self.cam_back_dist

def update_active_camera(self, context):
    cam = None
    if self.active_camera == 'MAIN':
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
    elif self.active_camera == 'BACK':
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
        
    if cam:
        context.scene.camera = cam
        cam.data.show_passepartout = True
        cam.data.passepartout_alpha = 0.8
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                area.spaces.active.region_3d.view_perspective = 'CAMERA'

def update_camera_color(self, context):
    try:
        bpy.context.preferences.themes[0].view_3d.camera = self.camera_color
    except Exception:
        pass

def update_camera_preset_func(self, context):
    for p in CAMERA_PRESETS:
        if p[0] == self.camera_preset: 
            self.camera_color = p[3]
            break

def update_pupil_window(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if not obj: return
    
    obj.hide_viewport = not self.pupil_window_show
    obj.hide_render = not self.pupil_window_show
    
    if not self.pupil_window_show:
        return
        
    obj.location = (0.0, 0.0, -self.pupil_window_dist)
    
    mesh = obj.data
    mesh.clear_geometry()
    
    maj_s = self.pupil_window_segments
    min_s = 16
    
    val = self.pupil_window_radius
    if maj_s == 4:
        R = val / math.sqrt(2.0)
    else:
        R = val
        
    r = self.pupil_window_thickness
    
    verts = []
    faces = []
    
    for i in range(maj_s):
        u = i * 2.0 * math.pi / maj_s
        for j in range(min_s):
            v = j * 2.0 * math.pi / min_s
            x = (R + r * math.cos(v)) * math.cos(u)
            y = (R + r * math.cos(v)) * math.sin(u)
            z = r * math.sin(v)
            verts.append((x, y, z))
            
    for i in range(maj_s):
        for j in range(min_s):
            next_i = (i + 1) % maj_s
            next_j = (j + 1) % min_s
            v0 = i * min_s + j
            v1 = i * min_s + next_j
            v2 = next_i * min_s + next_j
            v3 = next_i * min_s + j
            faces.append((v0, v1, v2, v3))
            
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_pupil_segments(self, context):
    if self.pupil_window_segments == 4:
        self.pupil_window_rot = (0.0, 0.0, math.radians(45.0))
    else:
        self.pupil_window_rot = (0.0, 0.0, math.radians(90.0))
    update_pupil_window(self, context)

def update_pupil_window_rot(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if obj:
        obj.rotation_euler = self.pupil_window_rot

def update_pupil_window_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_PUPIL)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = self.pupil_window_color
        if mat.use_nodes:
            bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
            if bsdf:
                if 'Base Color' in bsdf.inputs:
                    bsdf.inputs['Base Color'].default_value = self.pupil_window_color

# --- Property Group ---
class ADDON_Properties(bpy.types.PropertyGroup):
    custom_grid_scale: bpy.props.FloatProperty(name=LABEL_SIZE, default=CUSTOM_GRID_SCALE_DEFAULT, min=0.001)
    
    # カメラの移動設定
    cam_move_mode: bpy.props.EnumProperty(
        name="カメラ(Root)の移動",
        items=[('FIXED', "固定位置", ""), ('LINE', "線分移動 (アニメ)", ""), ('CIRCLE', "円周移動 (アニメ)", "")],
        default='FIXED', update=update_cam_anim
    )
    cam_fixed_loc: bpy.props.FloatVectorProperty(name=f"固定座標", default=(CAM_LOC_U_DEFAULT, CAM_LOC_V_DEFAULT, CAM_LOC_W_DEFAULT), update=update_cam_anim)
    cam_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=CAM_START_LOC_DEFAULT, update=update_cam_anim)
    cam_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=CAM_END_LOC_DEFAULT, update=update_cam_anim)
    
    cam_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=CAM_CIRCLE_CENTER_DEFAULT, update=update_cam_anim)
    cam_circle_radius: bpy.props.FloatProperty(name="円の半径", default=CAM_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_cam_anim)
    cam_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=CAM_CIRCLE_ROT_DEFAULT, update=update_cam_anim)
    cam_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=CAM_CIRCLE_START_ANGLE_DEFAULT, update=update_cam_anim)
    cam_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=CAM_CIRCLE_END_ANGLE_DEFAULT, update=update_cam_anim)
    
    cam_anim_speed_mult: bpy.props.FloatProperty(name="カメラ移動速度(倍率)", default=CAM_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_cam_anim)

    show_cam_path: bpy.props.BoolProperty(name="軌跡を描画する", default=SHOW_CAM_PATH_DEFAULT, update=update_cam_anim)
    cam_path_thickness: bpy.props.FloatProperty(name="太さ", default=CAM_PATH_THICKNESS_DEFAULT, min=0.001, update=update_cam_anim)
    cam_path_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=4, default=CAM_PATH_COLOR_DEFAULT, update=update_cam_anim)

    cam_target_mode: bpy.props.EnumProperty(
        name="注視点設定",
        items=[
            ('NONE', "オフ (速度ベース回転)", ""), 
            ('POINT', "指定座標 (固定)", ""), 
            ('LINE', "線分移動 (アニメ)", ""), 
            ('CIRCLE', "円周移動 (アニメ)", ""), 
            ('OBJECT', "別オブジェクト", "")
        ],
        default='NONE', update=update_cam_root_constraint
    )
    cam_target_loc: bpy.props.FloatVectorProperty(name=f"注視点", default=(CAM_TGT_U_DEFAULT, CAM_TGT_V_DEFAULT, CAM_TGT_W_DEFAULT), update=update_cam_root_constraint)
    cam_target_obj: bpy.props.PointerProperty(type=bpy.types.Object, name="対象オブジェクト", update=update_cam_root_constraint)
    
    target_start_loc: bpy.props.FloatVectorProperty(name="出発点", default=TARGET_START_DEFAULT, update=update_target_anim)
    target_end_loc: bpy.props.FloatVectorProperty(name="到達点", default=TARGET_END_DEFAULT, update=update_target_anim)
    
    target_circle_center: bpy.props.FloatVectorProperty(name="中心座標", default=TARGET_CIRCLE_CENTER_DEFAULT, update=update_target_anim)
    target_circle_radius: bpy.props.FloatProperty(name="円の半径", default=TARGET_CIRCLE_RADIUS_DEFAULT, min=0.01, update=update_target_anim)
    target_circle_rot: bpy.props.FloatVectorProperty(name="平面の傾斜(XYZ)", subtype='EULER', default=TARGET_CIRCLE_ROT_DEFAULT, update=update_target_anim)
    target_circle_start_angle: bpy.props.FloatProperty(name="開始角度", default=TARGET_CIRCLE_START_ANGLE_DEFAULT, update=update_target_anim)
    target_circle_end_angle: bpy.props.FloatProperty(name="終了角度", default=TARGET_CIRCLE_END_ANGLE_DEFAULT, update=update_target_anim)

    target_size: bpy.props.FloatProperty(name="立方体のサイズ", default=TARGET_SIZE_DEFAULT, min=0.01, update=update_target_visuals)
    target_color: bpy.props.FloatVectorProperty(name="立方体の色", subtype='COLOR', size=4, default=TARGET_COLOR_DEFAULT, min=0.0, max=1.0, update=update_target_visuals)
    target_random_color: bpy.props.BoolProperty(name="6面をランダムな色にする", default=TARGET_RANDOM_COLOR_DEFAULT, update=update_target_visuals)
    target_anim_speed_mult: bpy.props.FloatProperty(name="立方体移動速度 (倍率)", description="マイナス値で逆行/逆回転します", default=TARGET_ANIM_SPEED_MULT_DEFAULT, min=-100.0, max=100.0, update=update_target_anim)

    show_target_path: bpy.props.BoolProperty(name="軌跡を描画する", default=SHOW_TARGET_PATH_DEFAULT, update=update_target_anim)
    target_path_thickness: bpy.props.FloatProperty(name="太さ", default=TARGET_PATH_THICKNESS_DEFAULT, min=0.001, update=update_target_anim)
    target_path_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=4, default=TARGET_PATH_COLOR_DEFAULT, update=update_target_anim)

    cam_local_offset_rot: bpy.props.FloatVectorProperty(name="角度オフセット", subtype='EULER', default=(CAM_OFFSET_U_DEFAULT, CAM_OFFSET_V_DEFAULT, CAM_OFFSET_W_DEFAULT), update=update_cam_local_offset_rot)
    
    anim_start_frame: bpy.props.IntProperty(name=f"{LABEL_START} F", default=ANIM_START_FRAME_DEFAULT, min=0, update=update_cam_anim)
    anim_end_frame: bpy.props.IntProperty(name=f"{LABEL_END} F", default=ANIM_END_FRAME_DEFAULT, min=1, update=update_cam_anim)
    cam_rot_start: bpy.props.FloatVectorProperty(name="初期回転", subtype='EULER', default=(CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START), update=update_cam_anim)
    cam_rot_speed: bpy.props.FloatVectorProperty(name="回転速度", default=(CAM_ROT_SPEED_U_DEFAULT, CAM_ROT_SPEED_V_DEFAULT, CAM_ROT_SPEED_W_DEFAULT), update=update_cam_anim)
    
    show_cam_obj: bpy.props.BoolProperty(name="カメラ本体表示", default=True, update=update_visibility)
    show_target_empty: bpy.props.BoolProperty(name="注視点/カッター表示", default=True, update=update_visibility)
    cam_fov: bpy.props.FloatProperty(name="水平視野角", min=1.0, max=359.0, default=50.0, get=cam_fov_get, set=cam_fov_set)
    
    cam_back_dist: bpy.props.FloatProperty(name="背後カメラの距離", default=CAM_BACK_DIST_DEFAULT, min=1.0, max=100.0, update=update_cam_back_dist)
    
    active_camera: bpy.props.EnumProperty(
        name="表示カメラ切り替え",
        items=[('MAIN', "メインカメラ", "包み内 カメラ"), ('BACK', "背後霊カメラ", "背後 観察カメラ")],
        default='MAIN', update=update_active_camera
    )
    
    camera_preset: bpy.props.EnumProperty(
        name="カメラ色プリセット", 
        items=[(p[0], p[1], p[2]) for p in CAMERA_PRESETS], 
        default='Cam', 
        update=update_camera_preset_func
    )
    camera_color: bpy.props.FloatVectorProperty(
        name="カメラテーマ色", 
        subtype='COLOR', size=3, min=0.0, max=1.0, 
        default=CAMERA_COLOR_DEFAULT, 
        update=update_camera_color
    )
    
    show_cam_shield: bpy.props.BoolProperty(name="包み表示", default=True, update=update_cam_shield)
    cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_cam_shield)
    cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)
    cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_cam_shield)
    cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_SHIELD[:3], min=0.0, max=1.0, update=update_cam_shield)
    cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_cam_shield)

    show_back_cam_shield: bpy.props.BoolProperty(name="背後 包み表示", default=True, update=update_back_cam_shield)
    back_cam_shield_radius: bpy.props.FloatProperty(name="大きさ (半径)", default=BACK_CAM_SHIELD_RADIUS_DEFAULT, min=0.1, update=update_back_cam_shield)
    back_cam_shield_hole_ratio_front: bpy.props.FloatProperty(name="前の穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)
    back_cam_shield_hole_ratio_back: bpy.props.FloatProperty(name="後ろの穴 (比率)", default=BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT, min=0.0, max=1.01, update=update_back_cam_shield)
    back_cam_shield_color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', size=3, default=COLOR_BACK_SHIELD[:3], min=0.0, max=1.0, update=update_back_cam_shield)
    back_cam_shield_alpha: bpy.props.FloatProperty(name="透明度", default=ALPHA_BACK_SHIELD_DEFAULT, min=0.0, max=1.0, update=update_back_cam_shield)

    pupil_window_show: bpy.props.BoolProperty(name="瞳孔窓面の表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
    pupil_window_dist: bpy.props.FloatProperty(name="距離 (カメラ前方)", default=PUPIL_DIST_DEFAULT, min=1.0, max=100.0, update=update_pupil_window)
    pupil_window_radius: bpy.props.FloatProperty(name="サイズ", default=PUPIL_RADIUS_DEFAULT, min=0.1, update=update_pupil_window)
    pupil_window_thickness: bpy.props.FloatProperty(name="枠の太さ", default=PUPIL_THICKNESS_DEFAULT, min=0.01, update=update_pupil_window)
    pupil_window_segments: bpy.props.IntProperty(name="角数 (3=三角, 4=四角)", default=PUPIL_SEGMENTS_DEFAULT, min=3, max=32, update=update_pupil_segments)
    pupil_window_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_PUPIL, min=0.0, max=1.0, update=update_pupil_window_mat)
    pupil_window_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(PUPIL_ROT_X_DEFAULT, PUPIL_ROT_Y_DEFAULT, PUPIL_ROT_Z_DEFAULT), update=update_pupil_window_rot)

    info_proj_mode: bpy.props.StringProperty(name="投影モード", default="-")
    info_active_camera_name: bpy.props.StringProperty(name="表示カメラ", default="-")
    info_center_loc: bpy.props.FloatVectorProperty(name="画面中央位置")
    info_view_loc: bpy.props.FloatVectorProperty(name="視座位置")
    info_view_dir: bpy.props.FloatVectorProperty(name="視線方向")
    info_view_width: bpy.props.FloatProperty(name="画面水平の大きさ")
    info_view_height: bpy.props.FloatProperty(name="画面上下の大きさ")
    info_view_dist: bpy.props.FloatProperty(name="視座からの距離")
    
    info_target_path: bpy.props.StringProperty(name="ターゲット軌道情報", default="軌道: なし")

# --- Operators ---
class VIEW3D_OT_reset_view(bpy.types.Operator):
    bl_idname = f"view3d.{PREFIX}_reset_view"
    bl_label = "選択物を中央にする"
    def execute(self, context):
        rv3d = get_rv3d(context)
        if rv3d and context.active_object:
            rv3d.view_location = context.active_object.location
        return {'FINISHED'}

class OBJECT_OT_create_cam_rig(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_create_cam_rig"
    bl_label = "カメラリグ作成"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        for n in [OBJ_NAME_ROOT, OBJ_NAME_CAMERA, OBJ_NAME_CAMERA_BACK, OBJ_NAME_TARGET, SHIELD_NAME, CUTTER_NAME, CUTTER_BACK_NAME, OBJ_NAME_PUPIL, BACK_SHIELD_NAME, BACK_CUTTER_NAME, BACK_CUTTER_BACK_NAME, OBJ_NAME_CAM_PATH, OBJ_NAME_TGT_PATH]:
            o = bpy.data.objects.get(n)
            if o: bpy.data.objects.remove(o, do_unlink=True)
            
        main_col = bpy.data.collections.get(COLLECTION_NAME_CAM) or bpy.data.collections.new(COLLECTION_NAME_CAM)
        if main_col.name not in context.scene.collection.children: context.scene.collection.children.link(main_col)
        
        sub_col = bpy.data.collections.get(COLLECTION_SUB_NAME_CAM) or bpy.data.collections.new(COLLECTION_SUB_NAME_CAM)
        if sub_col.name not in main_col.children: main_col.children.link(sub_col)

        m_tgt = bpy.data.meshes.new(f"{OBJ_NAME_TARGET}_Mesh")
        bm_t = bmesh.new()
        bmesh.ops.create_cube(bm_t, size=1.0)
        bm_t.to_mesh(m_tgt)
        bm_t.free()
        
        tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, m_tgt)
        sub_col.objects.link(tgt_obj)
        
        update_target_visuals(props, context)

        root_obj = bpy.data.objects.new(OBJ_NAME_ROOT, None)
        root_obj.empty_display_type = 'PLAIN_AXES'
        root_obj.location = props.cam_fixed_loc
        root_obj.rotation_mode = 'XYZ'
        root_obj.rotation_euler = props.cam_rot_start
        sub_col.objects.link(root_obj)

        cam_data = bpy.data.cameras.new(OBJ_NAME_CAMERA)
        cam_obj = bpy.data.objects.new(OBJ_NAME_CAMERA, cam_data)
        cam_obj.parent = root_obj
        cam_obj.location = (0, 0, 0)
        cam_obj.rotation_euler = props.cam_local_offset_rot
        sub_col.objects.link(cam_obj)
        context.scene.camera = cam_obj
        
        cam_obj.data.dof.use_dof = True
        cam_obj.data.dof.focus_object = tgt_obj
        cam_obj.data.dof.aperture_fstop = 1.8
        cam_obj.data.show_passepartout = True
        cam_obj.data.passepartout_alpha = 0.8
        
        cam_back_data = bpy.data.cameras.new(OBJ_NAME_CAMERA_BACK)
        cam_back_data.show_passepartout = True
        cam_back_data.passepartout_alpha = 0.8
        cam_back_obj = bpy.data.objects.new(OBJ_NAME_CAMERA_BACK, cam_back_data)
        cam_back_obj.parent = cam_obj
        cam_back_obj.location = (0.0, 0.0, props.cam_back_dist)
        cam_back_obj.rotation_mode = 'XYZ'
        cam_back_obj.rotation_euler = (0.0, 0.0, 0.0) 
        sub_col.objects.link(cam_back_obj)

        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=32, v_segments=16, radius=1.0)
        m = bpy.data.meshes.new(SHIELD_NAME)
        bm.to_mesh(m)
        bm.free()
        s_obj = bpy.data.objects.new(SHIELD_NAME, m)
        s_obj.parent = cam_obj
        sub_col.objects.link(s_obj)

        bm_c = bmesh.new()
        bmesh.ops.create_cone(bm_c, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_c.verts: v.co.z -= 0.5
        m_c = bpy.data.meshes.new(CUTTER_NAME)
        bm_c.to_mesh(m_c)
        bm_c.free()
        c_obj = bpy.data.objects.new(CUTTER_NAME, m_c)
        c_obj.parent = cam_obj
        c_obj.hide_viewport = not props.show_target_empty
        sub_col.objects.link(c_obj)

        bm_b = bmesh.new()
        bmesh.ops.create_cone(bm_b, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_b.verts: v.co.z += 0.5
        m_b = bpy.data.meshes.new(CUTTER_BACK_NAME)
        bm_b.to_mesh(m_b)
        bm_b.free()
        cb_obj = bpy.data.objects.new(CUTTER_BACK_NAME, m_b)
        cb_obj.parent = cam_obj
        cb_obj.hide_viewport = not props.show_target_empty
        sub_col.objects.link(cb_obj)

        mod_front = s_obj.modifiers.new("Hole_Front", 'BOOLEAN')
        mod_front.object = c_obj
        mod_front.solver = 'FLOAT'
        
        mod_back = s_obj.modifiers.new("Hole_Back", 'BOOLEAN')
        mod_back.object = cb_obj
        mod_back.solver = 'FLOAT'

        mat = bpy.data.materials.new(MAT_NAME_SHIELD)
        mat.use_nodes = True
        mat.blend_method = 'BLEND'
        s_obj.data.materials.append(mat)
        s_obj.show_transparent = True

        bm_bs = bmesh.new()
        bmesh.ops.create_uvsphere(bm_bs, u_segments=32, v_segments=16, radius=1.0)
        m_bs = bpy.data.meshes.new(BACK_SHIELD_NAME)
        bm_bs.to_mesh(m_bs)
        bm_bs.free()
        s_obj_b = bpy.data.objects.new(BACK_SHIELD_NAME, m_bs)
        s_obj_b.parent = cam_back_obj
        sub_col.objects.link(s_obj_b)

        bm_bc = bmesh.new()
        bmesh.ops.create_cone(bm_bc, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_bc.verts: v.co.z -= 0.5
        m_bc = bpy.data.meshes.new(BACK_CUTTER_NAME)
        bm_bc.to_mesh(m_bc)
        bm_bc.free()
        c_obj_b = bpy.data.objects.new(BACK_CUTTER_NAME, m_bc)
        c_obj_b.parent = cam_back_obj
        c_obj_b.hide_viewport = not props.show_target_empty
        sub_col.objects.link(c_obj_b)

        bm_bb = bmesh.new()
        bmesh.ops.create_cone(bm_bb, cap_ends=False, segments=32, radius1=1.0, radius2=1.0, depth=1.0)
        for v in bm_bb.verts: v.co.z += 0.5
        m_bb = bpy.data.meshes.new(BACK_CUTTER_BACK_NAME)
        bm_bb.to_mesh(m_bb)
        bm_bb.free()
        cb_obj_b = bpy.data.objects.new(BACK_CUTTER_BACK_NAME, m_bb)
        cb_obj_b.parent = cam_back_obj
        cb_obj_b.hide_viewport = not props.show_target_empty
        sub_col.objects.link(cb_obj_b)

        mod_front_b = s_obj_b.modifiers.new("Hole_Front", 'BOOLEAN')
        mod_front_b.object = c_obj_b
        mod_front_b.solver = 'FLOAT'
        
        mod_back_b = s_obj_b.modifiers.new("Hole_Back", 'BOOLEAN')
        mod_back_b.object = cb_obj_b
        mod_back_b.solver = 'FLOAT'

        mat_b = bpy.data.materials.new(MAT_NAME_BACK_SHIELD)
        mat_b.use_nodes = True
        mat_b.blend_method = 'BLEND'
        s_obj_b.data.materials.append(mat_b)
        s_obj_b.show_transparent = True

        m_pupil = bpy.data.meshes.new(f"{OBJ_NAME_PUPIL}_Mesh")
        o_pupil = bpy.data.objects.new(OBJ_NAME_PUPIL, m_pupil)
        o_pupil.parent = cam_obj
        o_pupil.rotation_mode = 'XYZ'
        sub_col.objects.link(o_pupil)
        
        mat_pupil = bpy.data.materials.new(MAT_NAME_PUPIL)
        mat_pupil.use_nodes = True
        mat_pupil.blend_method = 'BLEND'
        o_pupil.data.materials.append(mat_pupil)
        
        update_pupil_window(props, context)
        update_pupil_window_rot(props, context)
        update_pupil_window_mat(props, context)

        update_cam_root_constraint(props, context)
        update_visibility(props, context)
        update_cam_shield(props, context)
        update_back_cam_shield(props, context)
        update_cam_anim(props, context)
        
        update_camera_color(props, context)
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
                        space.region_3d.view_perspective = 'CAMERA'
                        space.lock_camera = False
        
        self.report({'INFO'}, "カメラを作成し、カメラビュー&マテリアルプレビューに切り替えました")
        return {'FINISHED'}

class OBJECT_OT_sync_from_scene(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_sync_from_scene"
    bl_label = "シーンから取得"
    bl_description = "Blender本体のタイムライン(開始/終了フレーム)をこのアドオンの設定にコピーします"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.anim_start_frame = context.scene.frame_start
        props.anim_end_frame = context.scene.frame_end
        self.report({'INFO'}, f"シーンから取得しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
        return {'FINISHED'}

class OBJECT_OT_sync_to_scene(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_sync_to_scene"
    bl_label = "シーンへ適用"
    bl_description = "このアドオンで設定した開始/終了フレームを、Blender本体のタイムラインに適用します"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        context.scene.frame_start = props.anim_start_frame
        context.scene.frame_end = props.anim_end_frame
        self.report({'INFO'}, f"タイムラインのフレーム範囲を更新しました (開始:{context.scene.frame_start} - 終了:{context.scene.frame_end})")
        return {'FINISHED'}

class OBJECT_OT_apply_cam_anim(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_apply_cam_anim"
    bl_label = "アニメーションを適用(更新)"
    bl_description = "現在の設定で回転・移動アニメーションのキーフレームを打ち直します"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        update_cam_anim(props, context)
        self.report({'INFO'}, "アニメーションキーフレームを更新しました")
        return {'FINISHED'}

class OBJECT_OT_switch_cam_main(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_switch_cam_main"
    bl_label = "包み内 カメラ視点"
    bl_description = "本来の固定カメラをアクティブにしてカメラビューに切り替えます"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "包み内カメラ視点に切り替えました")
        else:
            self.report({'ERROR'}, "包み内カメラが見つかりません")
        return {'FINISHED'}

class OBJECT_OT_switch_cam_back(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_switch_cam_back"
    bl_label = "背後 観察視点"
    bl_description = "全体を見渡す背後カメラをアクティブにしてカメラビューに切り替えます"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA_BACK)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "背後からの観察視点に切り替えました")
        else:
            self.report({'ERROR'}, "背後カメラが見つかりません。リグを再作成してください")
        return {'FINISHED'}

class OBJECT_OT_set_active_camera(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_set_active_camera"
    bl_label = "専用カメラをアクティブにする"
    def execute(self, context):
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if cam:
            context.scene.camera = cam
            cam.data.show_passepartout = True
            cam.data.passepartout_alpha = 0.8
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.spaces.active.region_3d.view_perspective = 'CAMERA'
            self.report({'INFO'}, "専用カメラをアクティブに設定しました")
        else:
            self.report({'ERROR'}, "専用カメラが見つかりません。リグを作成してください")
        return {'FINISHED'}

class OBJECT_OT_update_camera_info(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_update_camera_info"
    bl_label = "情報更新"
    bl_description = "現在のビューから画面の情報を取得・更新します"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        rv3d = get_rv3d(context)
        
        if not rv3d:
            self.report({'WARNING'}, "3Dビューが見つかりません")
            return {'CANCELLED'}

        if rv3d.view_perspective == 'CAMERA': props.info_proj_mode = "カメラビュー"
        elif rv3d.view_perspective == 'PERSP': props.info_proj_mode = "透視投影"
        elif rv3d.view_perspective == 'ORTHO': props.info_proj_mode = "平行投影"
        else: props.info_proj_mode = rv3d.view_perspective
            
        dist = rv3d.view_distance
        props.info_view_dist = dist
        
        res_x = context.scene.render.resolution_x
        res_y = context.scene.render.resolution_y
        aspect = (res_x / res_y) if res_y != 0 else 1.0

        if rv3d.view_perspective == 'CAMERA' and context.scene.camera:
            cam = context.scene.camera
            
            if cam.name == OBJ_NAME_CAMERA:
                props.info_active_camera_name = "メインカメラ"
            elif cam.name == OBJ_NAME_CAMERA_BACK:
                props.info_active_camera_name = "背後霊カメラ"
            else:
                props.info_active_camera_name = f"その他 ({cam.name})"
                
            view_loc = cam.matrix_world.translation
            view_dir = cam.matrix_world.to_quaternion() @ mathutils.Vector((0, 0, -1))
            center_loc = view_loc + view_dir * dist
            fov = cam.data.angle
            width = 2.0 * dist * math.tan(fov / 2.0)
            height = width / aspect
        else:
            props.info_active_camera_name = "なし (ビューポート)"
            center_loc = rv3d.view_location
            view_rot = rv3d.view_rotation
            view_dir = view_rot @ mathutils.Vector((0, 0, -1))
            view_loc = center_loc - view_dir * dist
            space = None
            for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    space = area.spaces.active
                    break
            lens = space.lens if space and hasattr(space, "lens") else 50.0
            fov = 2.0 * math.atan(36.0 / (2.0 * lens))
            width = 2.0 * dist * math.tan(fov / 2.0)
            height = width / aspect
            
        props.info_center_loc = center_loc
        props.info_view_loc = view_loc
        props.info_view_dir = view_dir
        props.info_view_width = width
        props.info_view_height = height
        
        props.info_target_path = "ターゲット軌道: なし"
        if props.cam_target_mode == 'LINE':
            s = props.target_start_loc
            e = props.target_end_loc
            props.info_target_path = f"ターゲット線分(往復 {props.target_anim_speed_mult:.1f}倍速): Start({s[0]:.1f}, {s[1]:.1f}, {s[2]:.1f}) -> End({e[0]:.1f}, {e[1]:.1f}, {e[2]:.1f})"
        elif props.cam_target_mode == 'CIRCLE':
            rot_mat = mathutils.Euler(props.target_circle_rot, 'XYZ').to_matrix()
            normal = rot_mat @ mathutils.Vector((0, 0, 1))
            c = props.target_circle_center
            d = -(normal.x * c[0] + normal.y * c[1] + normal.z * c[2])
            props.info_target_path = f"ターゲット平面({props.target_anim_speed_mult:.1f}倍速): {normal.x:.2f}x + {normal.y:.2f}y + {normal.z:.2f}z + ({d:.2f}) = 0"
            
        if props.cam_move_mode == 'LINE':
            cs = props.cam_start_loc
            ce = props.cam_end_loc
            props.info_target_path += f"\nカメラ移動(往復 {props.cam_anim_speed_mult:.1f}倍速): Start({cs[0]:.1f}, {cs[1]:.1f}, {cs[2]:.1f}) -> End({ce[0]:.1f}, {ce[1]:.1f}, {ce[2]:.1f})"
        elif props.cam_move_mode == 'CIRCLE':
            rot_mat_c = mathutils.Euler(props.cam_circle_rot, 'XYZ').to_matrix()
            normal_c = rot_mat_c @ mathutils.Vector((0, 0, 1))
            cc = props.cam_circle_center
            dc = -(normal_c.x * cc[0] + normal_c.y * cc[1] + normal_c.z * cc[2])
            props.info_target_path += f"\nカメラ平面({props.cam_anim_speed_mult:.1f}倍速): {normal_c.x:.2f}x + {normal_c.y:.2f}y + {normal_c.z:.2f}z + ({dc:.2f}) = 0"

        
        self.report({'INFO'}, "画面情報を更新しました")
        return {'FINISHED'}

class OBJECT_OT_copy_camera_info(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_copy_camera_info"
    bl_label = "情報をコピー"
    bl_description = "更新された画面情報をクリップボードにコピーします"

    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        
        info = (
            f"投影モード: {props.info_proj_mode}\n"
            f"表示カメラ: {props.info_active_camera_name}\n"
            f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}\n"
            f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}\n"
            f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}\n"
            f"画面水平の大きさ: {props.info_view_width:.1f}\n"
            f"画面上下の大きさ: {props.info_view_height:.1f}\n"
            f"視座からの距離: {props.info_view_dist:.1f}\n"
            f"{props.info_target_path}"
        )
        
        context.window_manager.clipboard = info
        self.report({'INFO'}, "画面情報をクリップボードにコピーしました")
        return {'FINISHED'}

class OBJECT_OT_export_addon(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_export_addon"
    bl_label = "現在の設定でアドオンを書き出す"
    
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        source = get_addon_source()
        if not source:
            self.report({'ERROR'}, "ソースコードを取得できませんでした")
            return {'CANCELLED'}
            
        lines = source.split('\n')
        for i, line in enumerate(lines):
            if line.startswith("CAM_LOC_U_DEFAULT = "): lines[i] = f"CAM_LOC_U_DEFAULT = {props.cam_fixed_loc[0]:.4f}"
            elif line.startswith("CAM_LOC_V_DEFAULT = "): lines[i] = f"CAM_LOC_V_DEFAULT = {props.cam_fixed_loc[1]:.4f}"
            elif line.startswith("CAM_LOC_W_DEFAULT = "): lines[i] = f"CAM_LOC_W_DEFAULT = {props.cam_fixed_loc[2]:.4f}"
            
            elif line.startswith("CAM_START_LOC_DEFAULT = "): lines[i] = f"CAM_START_LOC_DEFAULT = ({props.cam_start_loc[0]:.4f}, {props.cam_start_loc[1]:.4f}, {props.cam_start_loc[2]:.4f})"
            elif line.startswith("CAM_END_LOC_DEFAULT = "): lines[i] = f"CAM_END_LOC_DEFAULT = ({props.cam_end_loc[0]:.4f}, {props.cam_end_loc[1]:.4f}, {props.cam_end_loc[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"CAM_CIRCLE_CENTER_DEFAULT = ({props.cam_circle_center[0]:.4f}, {props.cam_circle_center[1]:.4f}, {props.cam_circle_center[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"CAM_CIRCLE_RADIUS_DEFAULT = {props.cam_circle_radius:.4f}"
            elif line.startswith("CAM_CIRCLE_ROT_DEFAULT = "): lines[i] = f"CAM_CIRCLE_ROT_DEFAULT = ({props.cam_circle_rot[0]:.4f}, {props.cam_circle_rot[1]:.4f}, {props.cam_circle_rot[2]:.4f})"
            elif line.startswith("CAM_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_START_ANGLE_DEFAULT = {props.cam_circle_start_angle:.4f}"
            elif line.startswith("CAM_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"CAM_CIRCLE_END_ANGLE_DEFAULT = {props.cam_circle_end_angle:.4f}"
            elif line.startswith("CAM_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"CAM_ANIM_SPEED_MULT_DEFAULT = {props.cam_anim_speed_mult:.4f}"
            
            elif line.startswith("CAM_TGT_U_DEFAULT = "): lines[i] = f"CAM_TGT_U_DEFAULT = {props.cam_target_loc[0]:.4f}"
            elif line.startswith("CAM_TGT_V_DEFAULT = "): lines[i] = f"CAM_TGT_V_DEFAULT = {props.cam_target_loc[1]:.4f}"
            elif line.startswith("CAM_TGT_W_DEFAULT = "): lines[i] = f"CAM_TGT_W_DEFAULT = {props.cam_target_loc[2]:.4f}"
            
            elif line.startswith("TARGET_START_DEFAULT = "): lines[i] = f"TARGET_START_DEFAULT = ({props.target_start_loc[0]:.4f}, {props.target_start_loc[1]:.4f}, {props.target_start_loc[2]:.4f})"
            elif line.startswith("TARGET_END_DEFAULT = "): lines[i] = f"TARGET_END_DEFAULT = ({props.target_end_loc[0]:.4f}, {props.target_end_loc[1]:.4f}, {props.target_end_loc[2]:.4f})"
            
            elif line.startswith("TARGET_CIRCLE_CENTER_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_CENTER_DEFAULT = ({props.target_circle_center[0]:.4f}, {props.target_circle_center[1]:.4f}, {props.target_circle_center[2]:.4f})"
            elif line.startswith("TARGET_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_RADIUS_DEFAULT = {props.target_circle_radius:.4f}"
            elif line.startswith("TARGET_CIRCLE_ROT_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_ROT_DEFAULT = ({props.target_circle_rot[0]:.4f}, {props.target_circle_rot[1]:.4f}, {props.target_circle_rot[2]:.4f})"
            elif line.startswith("TARGET_CIRCLE_START_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_START_ANGLE_DEFAULT = {props.target_circle_start_angle:.4f}"
            elif line.startswith("TARGET_CIRCLE_END_ANGLE_DEFAULT = "): lines[i] = f"TARGET_CIRCLE_END_ANGLE_DEFAULT = {props.target_circle_end_angle:.4f}"

            elif line.startswith("TARGET_SIZE_DEFAULT = "): lines[i] = f"TARGET_SIZE_DEFAULT = {props.target_size:.4f}"
            elif line.startswith("TARGET_COLOR_DEFAULT = "): lines[i] = f"TARGET_COLOR_DEFAULT = ({props.target_color[0]:.4f}, {props.target_color[1]:.4f}, {props.target_color[2]:.4f}, 1.0)"
            elif line.startswith("TARGET_RANDOM_COLOR_DEFAULT = "): lines[i] = f"TARGET_RANDOM_COLOR_DEFAULT = {props.target_random_color}"
            elif line.startswith("TARGET_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"

            elif line.startswith("SHOW_CAM_PATH_DEFAULT = "): lines[i] = f"SHOW_CAM_PATH_DEFAULT = {props.show_cam_path}"
            elif line.startswith("CAM_PATH_THICKNESS_DEFAULT = "): lines[i] = f"CAM_PATH_THICKNESS_DEFAULT = {props.cam_path_thickness:.4f}"
            elif line.startswith("CAM_PATH_COLOR_DEFAULT = "): lines[i] = f"CAM_PATH_COLOR_DEFAULT = ({props.cam_path_color[0]:.4f}, {props.cam_path_color[1]:.4f}, {props.cam_path_color[2]:.4f}, 1.0)"

            elif line.startswith("SHOW_TARGET_PATH_DEFAULT = "): lines[i] = f"SHOW_TARGET_PATH_DEFAULT = {props.show_target_path}"
            elif line.startswith("TARGET_PATH_THICKNESS_DEFAULT = "): lines[i] = f"TARGET_PATH_THICKNESS_DEFAULT = {props.target_path_thickness:.4f}"
            elif line.startswith("TARGET_PATH_COLOR_DEFAULT = "): lines[i] = f"TARGET_PATH_COLOR_DEFAULT = ({props.target_path_color[0]:.4f}, {props.target_path_color[1]:.4f}, {props.target_path_color[2]:.4f}, 1.0)"

            elif line.startswith("CAM_BACK_DIST_DEFAULT = "): lines[i] = f"CAM_BACK_DIST_DEFAULT = {props.cam_back_dist:.4f}"
            
            elif line.startswith("CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"CAM_SHIELD_RADIUS_DEFAULT = {props.cam_shield_radius:.4f}"
            elif line.startswith("CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.cam_shield_hole_ratio_front:.4f}"
            elif line.startswith("CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.cam_shield_hole_ratio_back:.4f}"
            elif line.startswith("ALPHA_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_SHIELD_DEFAULT = {props.cam_shield_alpha:.4f}"
            elif line.startswith("COLOR_SHIELD = "): lines[i] = f"COLOR_SHIELD = ({props.cam_shield_color[0]:.4f}, {props.cam_shield_color[1]:.4f}, {props.cam_shield_color[2]:.4f}, 1.0)"

            elif line.startswith("BACK_CAM_SHIELD_RADIUS_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_RADIUS_DEFAULT = {props.back_cam_shield_radius:.4f}"
            elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_FRONT_DEFAULT = {props.back_cam_shield_hole_ratio_front:.4f}"
            elif line.startswith("BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = "): lines[i] = f"BACK_CAM_SHIELD_HOLE_RATIO_BACK_DEFAULT = {props.back_cam_shield_hole_ratio_back:.4f}"
            elif line.startswith("ALPHA_BACK_SHIELD_DEFAULT = "): lines[i] = f"ALPHA_BACK_SHIELD_DEFAULT = {props.back_cam_shield_alpha:.4f}"
            elif line.startswith("COLOR_BACK_SHIELD = "): lines[i] = f"COLOR_BACK_SHIELD = ({props.back_cam_shield_color[0]:.4f}, {props.back_cam_shield_color[1]:.4f}, {props.back_cam_shield_color[2]:.4f}, 1.0)"

            elif line.startswith("CAMERA_COLOR_DEFAULT = "): lines[i] = f"CAMERA_COLOR_DEFAULT = ({props.camera_color[0]:.4f}, {props.camera_color[1]:.4f}, {props.camera_color[2]:.4f})"

            elif line.startswith("ANIM_START_FRAME_DEFAULT = "): lines[i] = f"ANIM_START_FRAME_DEFAULT = {props.anim_start_frame}"
            elif line.startswith("ANIM_END_FRAME_DEFAULT = "): lines[i] = f"ANIM_END_FRAME_DEFAULT = {props.anim_end_frame}"
            
            elif line.startswith("CAM_ROT_U_START = "): lines[i] = f"CAM_ROT_U_START = {props.cam_rot_start[0]:.4f}"
            elif line.startswith("CAM_ROT_V_START = "): lines[i] = f"CAM_ROT_V_START = {props.cam_rot_start[1]:.4f}"
            elif line.startswith("CAM_ROT_W_START = "): lines[i] = f"CAM_ROT_W_START = {props.cam_rot_start[2]:.4f}"
            
            elif line.startswith("CAM_ROT_SPEED_U_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_U_DEFAULT = {props.cam_rot_speed[0]:.4f}"
            elif line.startswith("CAM_ROT_SPEED_V_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_V_DEFAULT = {props.cam_rot_speed[1]:.4f}"
            elif line.startswith("CAM_ROT_SPEED_W_DEFAULT = "): lines[i] = f"CAM_ROT_SPEED_W_DEFAULT = {props.cam_rot_speed[2]:.4f}"
            
            elif line.startswith("CAM_OFFSET_U_DEFAULT = "): lines[i] = f"CAM_OFFSET_U_DEFAULT = {props.cam_local_offset_rot[0]:.4f}"
            elif line.startswith("CAM_OFFSET_V_DEFAULT = "): lines[i] = f"CAM_OFFSET_V_DEFAULT = {props.cam_local_offset_rot[1]:.4f}"
            elif line.startswith("CAM_OFFSET_W_DEFAULT = "): lines[i] = f"CAM_OFFSET_W_DEFAULT = {props.cam_local_offset_rot[2]:.4f}"
            
            elif line.startswith("PUPIL_SHOW_DEFAULT = "): lines[i] = f"PUPIL_SHOW_DEFAULT = {props.pupil_window_show}"
            elif line.startswith("PUPIL_DIST_DEFAULT = "): lines[i] = f"PUPIL_DIST_DEFAULT = {props.pupil_window_dist:.4f}"
            elif line.startswith("PUPIL_RADIUS_DEFAULT = "): lines[i] = f"PUPIL_RADIUS_DEFAULT = {props.pupil_window_radius:.4f}"
            elif line.startswith("PUPIL_THICKNESS_DEFAULT = "): lines[i] = f"PUPIL_THICKNESS_DEFAULT = {props.pupil_window_thickness:.4f}"
            elif line.startswith("PUPIL_SEGMENTS_DEFAULT = "): lines[i] = f"PUPIL_SEGMENTS_DEFAULT = {props.pupil_window_segments}"
            elif line.startswith("COLOR_PUPIL = "): lines[i] = f"COLOR_PUPIL = ({props.pupil_window_color[0]:.4f}, {props.pupil_window_color[1]:.4f}, {props.pupil_window_color[2]:.4f}, 1.0)"
            elif line.startswith("PUPIL_ROT_X_DEFAULT = "): lines[i] = f"PUPIL_ROT_X_DEFAULT = {props.pupil_window_rot[0]:.4f}"
            elif line.startswith("PUPIL_ROT_Y_DEFAULT = "): lines[i] = f"PUPIL_ROT_Y_DEFAULT = {props.pupil_window_rot[1]:.4f}"
            elif line.startswith("PUPIL_ROT_Z_DEFAULT = "): lines[i] = f"PUPIL_ROT_Z_DEFAULT = {props.pupil_window_rot[2]:.4f}"

        new_text = '\n'.join(lines)
        
        import time
        t_name = f"Custom_Camera_Addon_{int(time.time())}.py"
        text_data = bpy.data.texts.new(name=t_name)
        text_data.write(new_text)
        
        for area in context.screen.areas:
            if area.type == 'TEXT_EDITOR':
                area.spaces.active.text = text_data
                break
                
        self.report({'INFO'}, f"現在の設定を {t_name} に書き出しました")
        return {'FINISHED'}

# ===================================
# 9. パネル構成
# ===================================

class VIEW3D_PT_main(bpy.types.Panel):
    bl_label = "Viewport & Grid"
    bl_idname = f"{PREFIX.upper()}_PT_main"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    
    def draw(self, context):
        layout = self.layout
        layout.operator(f"view3d.{PREFIX}_reset_view", icon='ZOOM_ALL')

class VIEW3D_PT_camera(bpy.types.Panel):
    bl_label = "Fixed Camera Rig (一体化)"
    bl_idname = f"{PREFIX.upper()}_PT_camera"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX}_props")
        
        box = layout.box()
        box.label(text="1. カメラリグの配置 (移動)", icon='CAMERA_DATA')
        box.prop(props, "cam_move_mode", expand=True)
        col_loc = box.column(align=True)
        
        if props.cam_move_mode == 'FIXED':
            col_loc.prop(props, "cam_fixed_loc", index=0, text=f"{LABEL_LOC} {LABEL_U}")
            col_loc.prop(props, "cam_fixed_loc", index=1, text=f"{LABEL_LOC} {LABEL_V}")
            col_loc.prop(props, "cam_fixed_loc", index=2, text=f"{LABEL_LOC} {LABEL_W}")
            
        elif props.cam_move_mode == 'LINE':
            col_loc.label(text="出発点:")
            col_loc.prop(props, "cam_start_loc", index=0, text="X")
            col_loc.prop(props, "cam_start_loc", index=1, text="Y")
            col_loc.prop(props, "cam_start_loc", index=2, text="Z")
            col_loc.separator()
            col_loc.label(text="到達点:")
            col_loc.prop(props, "cam_end_loc", index=0, text="X")
            col_loc.prop(props, "cam_end_loc", index=1, text="Y")
            col_loc.prop(props, "cam_end_loc", index=2, text="Z")
            col_loc.separator()
            col_loc.prop(props, "cam_anim_speed_mult", slider=True)
            
        elif props.cam_move_mode == 'CIRCLE':
            col_loc.label(text="中心位置:")
            col_loc.prop(props, "cam_circle_center", index=0, text="X")
            col_loc.prop(props, "cam_circle_center", index=1, text="Y")
            col_loc.prop(props, "cam_circle_center", index=2, text="Z")
            col_loc.separator()
            col_loc.prop(props, "cam_circle_radius")
            col_loc.separator()
            col_loc.label(text="平面の傾き (XYZ):")
            col_loc.prop(props, "cam_circle_rot", index=0, text="X (Pitch)")
            col_loc.prop(props, "cam_circle_rot", index=1, text="Y (Roll)")
            col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Yaw)")
            col_loc.separator()
            col_loc.prop(props, "cam_circle_start_angle")
            col_loc.prop(props, "cam_circle_end_angle")
            col_loc.separator()
            col_loc.prop(props, "cam_anim_speed_mult", slider=True)
            
        if props.cam_move_mode in ['LINE', 'CIRCLE']:
            col_loc.separator()
            row_path = col_loc.row(align=True)
            row_path.prop(props, "show_cam_path")
            if props.show_cam_path:
                col_loc.prop(props, "cam_path_thickness")
                col_loc.prop(props, "cam_path_color")
            
        box.separator()
        row_cam = box.row(align=True)
        row_cam.operator(f"object.{PREFIX}_create_cam_rig", icon='ADD')
        row_cam.operator(f"object.{PREFIX}_set_active_camera", text="", icon='RESTRICT_RENDER_OFF')
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if root and cam:
            box_tgt = layout.box()
            box_tgt.label(text="2. 注視点 (Track To) 設定", icon='TRACKING')
            box_tgt.prop(props, "cam_target_mode", expand=False)
            
            if props.cam_target_mode == 'POINT':
                col_tgt = box_tgt.column(align=True)
                col_tgt.prop(props, "cam_target_loc", index=0, text=f"注視点 {LABEL_U}")
                col_tgt.prop(props, "cam_target_loc", index=1, text=f"注視点 {LABEL_V}")
                col_tgt.prop(props, "cam_target_loc", index=2, text=f"注視点 {LABEL_W}")
                
            elif props.cam_target_mode == 'LINE':
                col_tgt = box_tgt.column(align=True)
                col_tgt.label(text="出発点:")
                col_tgt.prop(props, "target_start_loc", index=0, text="X")
                col_tgt.prop(props, "target_start_loc", index=1, text="Y")
                col_tgt.prop(props, "target_start_loc", index=2, text="Z")
                col_tgt.separator()
                col_tgt.label(text="到達点:")
                col_tgt.prop(props, "target_end_loc", index=0, text="X")
                col_tgt.prop(props, "target_end_loc", index=1, text="Y")
                col_tgt.prop(props, "target_end_loc", index=2, text="Z")
                
            elif props.cam_target_mode == 'CIRCLE':
                col_tgt = box_tgt.column(align=True)
                col_tgt.label(text="中心位置:")
                col_tgt.prop(props, "target_circle_center", index=0, text="X")
                col_tgt.prop(props, "target_circle_center", index=1, text="Y")
                col_tgt.prop(props, "target_circle_center", index=2, text="Z")
                col_tgt.separator()
                col_tgt.prop(props, "target_circle_radius")
                col_tgt.separator()
                col_tgt.label(text="平面の傾き (XYZ):")
                col_tgt.prop(props, "target_circle_rot", index=0, text="X (Pitch)")
                col_tgt.prop(props, "target_circle_rot", index=1, text="Y (Roll)")
                col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Yaw)")
                col_tgt.separator()
                col_tgt.prop(props, "target_circle_start_angle")
                col_tgt.prop(props, "target_circle_end_angle")
                
            elif props.cam_target_mode == 'OBJECT':
                box_tgt.prop(props, "cam_target_obj")

            if props.cam_target_mode in ['POINT', 'LINE', 'CIRCLE']:
                col_tgt = box_tgt.column(align=True)
                col_tgt.separator()
                col_tgt.prop(props, "target_size")
                col_tgt.prop(props, "target_random_color")
                if not props.target_random_color:
                    col_tgt.prop(props, "target_color")
                    
            if props.cam_target_mode in ['LINE', 'CIRCLE']:
                col_tgt = box_tgt.column(align=True)
                col_tgt.separator()
                col_tgt.prop(props, "target_anim_speed_mult", slider=True)
                col_tgt.separator()
                row_path_tgt = col_tgt.row(align=True)
                row_path_tgt.prop(props, "show_target_path")
                if props.show_target_path:
                    col_tgt.prop(props, "target_path_thickness")
                    col_tgt.prop(props, "target_path_color")

            box_offset = layout.box()
            box_offset.label(text="補助: 注視点ベースの角度ズレ", icon='ORIENTATION_LOCAL')
            col_off = box_offset.column(align=True)
            col_off.prop(props, "cam_local_offset_rot", index=0, text=f"角度ズレ {LABEL_U} (Pitch)")
            col_off.prop(props, "cam_local_offset_rot", index=1, text=f"角度ズレ {LABEL_V} (Roll)")
            col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Yaw)")

            box_rot = layout.box()
            box_rot.label(text="3. アニメーション設定", icon='ANIM_DATA')
            if props.cam_target_mode != 'NONE':
                box_rot.label(text="※速度ベースのRoot回転は無効化されています", icon='INFO')
            
            if props.cam_target_mode == 'NONE':
                col_rot = box_rot.column(align=True)
                col_rot.label(text="Rootの速度ベース回転:")
                col_rot.prop(props, "cam_rot_start", index=0, text=f"初期 {LABEL_U} (Pitch)")
                col_rot.prop(props, "cam_rot_start", index=1, text=f"初期 {LABEL_V} (Roll)")
                col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Yaw)")
                col_rot.separator()
                col_rot.prop(props, "cam_rot_speed", index=0, text=f"速度 {LABEL_U} (度/F)")
                col_rot.prop(props, "cam_rot_speed", index=1, text=f"速度 {LABEL_V} (度/F)")
                col_rot.prop(props, "cam_rot_speed", index=2, text=f"速度 {LABEL_W} (度/F)")
                row_hint = box_rot.row()
                row_hint.label(text="方向ガイド (UVW):", icon='QUESTION')
                col_hint = box_rot.column(align=True)
                col_hint.scale_y = 0.8
                col_hint.label(text=f"{LABEL_U}+: 下向 / {LABEL_U}-: 上向")
                col_hint.label(text=f"{LABEL_V}+: 左傾 / {LABEL_V}-: 右傾")
                col_hint.label(text=f"{LABEL_W}+: 左向 / {LABEL_W}-: 右向")
                box_rot.separator()
            
            col_f = box_rot.column(align=True)
            row_f = col_f.row(align=True)
            row_f.prop(props, "anim_start_frame")
            row_f.prop(props, "anim_end_frame")
            
            row_sync = col_f.row(align=True)
            row_sync.operator(f"object.{PREFIX}_sync_from_scene", icon='IMPORT')
            row_sync.operator(f"object.{PREFIX}_sync_to_scene", icon='EXPORT')
            
            box_rot.separator()
            
            col_btn = box_rot.column(align=True)
            col_btn.operator(f"object.{PREFIX}_apply_cam_anim", icon='KEYINGSET')
            if not context.screen.is_animation_playing:
                col_btn.operator("screen.animation_play", text="アニメーションを再生", icon='PLAY')
            else:
                col_btn.operator("screen.animation_play", text="アニメーションを停止", icon='PAUSE')
                
            box_rot.separator()

            # --- 視点切り替えボタン & 背後カメラの距離 ---
            col_switch = box_rot.column(align=True)
            row_switch = col_switch.row(align=True)
            row_switch.prop(props, "active_camera", expand=True)
            col_switch.prop(props, "cam_back_dist", text="背後カメラの距離", slider=True)
            box_rot.separator()

            # --- 画面情報表示と更新・コピー機能 ---
            box_info = box_rot.box()
            row_info_head = box_info.row(align=True)
            row_info_head.label(text="画面情報", icon='INFO')
            row_info_head.operator(f"object.{PREFIX}_update_camera_info", text="情報更新", icon='FILE_REFRESH')
            
            box_info.label(text=f"投影モード: {props.info_proj_mode}")
            box_info.label(text=f"表示カメラ: {props.info_active_camera_name}")
            box_info.label(text=f"画面中央位置: X: {props.info_center_loc[0]:.1f}, Y: {props.info_center_loc[1]:.1f}, Z: {props.info_center_loc[2]:.1f}")
            box_info.label(text=f"視座位置: X: {props.info_view_loc[0]:.1f}, Y: {props.info_view_loc[1]:.1f}, Z: {props.info_view_loc[2]:.1f}")
            box_info.label(text=f"視線方向: X: {props.info_view_dir[0]:.1f}, Y: {props.info_view_dir[1]:.1f}, Z: {props.info_view_dir[2]:.1f}")
            box_info.label(text=f"画面水平の大きさ: {props.info_view_width:.1f}")
            box_info.label(text=f"画面上下の大きさ: {props.info_view_height:.1f}")
            box_info.label(text=f"視座からの距離: {props.info_view_dist:.1f}")
            box_info.separator()
            
            for line in props.info_target_path.split('\n'):
                box_info.label(text=line, icon='EDITMODE_HLT')
            
            box_info.operator(f"object.{PREFIX}_copy_camera_info", icon='COPYDOWN')

            # --- 4. メインカメラ 包み・線画設定 ---
            box_vis = layout.box()
            box_vis.label(text="4. カメラ色(共通テーマ) & メイン包み", icon='RESTRICT_VIEW_OFF')
            
            row_disp = box_vis.row(align=True)
            row_disp.prop(props, "show_cam_obj", text="カメラ")
            row_disp.prop(props, "show_target_empty", text="注視点/カッター") 
            row_disp.prop(props, "show_cam_shield", text="包み表示")
            
            col_vis = box_vis.column(align=True)
            row_cam_col = col_vis.row(align=True)
            row_cam_col.prop(props, "camera_preset", text="")
            row_cam_col.prop(props, "camera_color", text="")
            col_vis.separator()

            if props.show_cam_shield:
                col_vis.label(text="メインカメラ 包み設定", icon='MESH_UVSPHERE')
                col_vis.prop(props, "cam_shield_radius")
                col_vis.prop(props, "cam_shield_hole_ratio_front")
                col_vis.prop(props, "cam_shield_hole_ratio_back")
                col_vis.separator()
                col_vis.prop(props, "cam_shield_color")
                col_vis.prop(props, "cam_shield_alpha", slider=True)

            # --- 5. 背後霊カメラ 包み設定 ---
            box_back = layout.box()
            box_back.label(text="5. 背後霊カメラ 包み設定", icon='OUTLINER_OB_CAMERA')
            
            row_back_disp = box_back.row(align=True)
            row_back_disp.prop(props, "show_back_cam_shield", text="背後包み表示")
            
            col_back = box_back.column(align=True)

            if props.show_back_cam_shield:
                col_back.separator()
                col_back.prop(props, "back_cam_shield_radius")
                col_back.prop(props, "back_cam_shield_hole_ratio_front")
                col_back.prop(props, "back_cam_shield_hole_ratio_back")
                col_back.separator()
                col_back.prop(props, "back_cam_shield_color")
                col_back.prop(props, "back_cam_shield_alpha", slider=True)
                
            # --- 6. 瞳孔窓面 設定 ---
            box_pupil = layout.box()
            box_pupil.label(text="6. 瞳孔窓面 (前方の枠)", icon='MESH_TORUS')
            box_pupil.prop(props, "pupil_window_show", text="表示する")
            if props.pupil_window_show:
                col_p = box_pupil.column(align=True)
                col_p.prop(props, "pupil_window_dist", text="距離 (1-100)", slider=True)
                col_p.prop(props, "pupil_window_segments", text="角数 (3-32)")
                
                if props.pupil_window_segments == 4:
                    col_p.prop(props, "pupil_window_radius", text="1辺の長さ")
                else:
                    col_p.prop(props, "pupil_window_radius", text="半径")
                    
                col_p.prop(props, "pupil_window_thickness")
                col_p.prop(props, "pupil_window_color")
                col_p.separator()
                
                col_p.label(text="ローカル回転 (XYZ):")
                col_p.prop(props, "pupil_window_rot", index=0, text="X (Pitch)")
                col_p.prop(props, "pupil_window_rot", index=1, text="Y (Roll)")
                col_p.prop(props, "pupil_window_rot", index=2, text="Z (Yaw)")
                
                
            # --- 7. レンズ・カメラ設定 (統合) ---
            box_lens = layout.box()
            box_lens.label(text="7. レンズ・カメラ設定", icon='VIEW_CAMERA')
            col_lens = box_lens.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="クリップ開始")
            col_lens.prop(cam.data, "clip_end", text="クリップ終了")
            col_lens.separator()
            col_lens.prop(cam.data, "show_passepartout", text="枠外を暗くする")
            if cam.data.show_passepartout:
                col_lens.prop(cam.data, "passepartout_alpha", text="枠外の暗さ", slider=True)
            col_lens.separator()
            col_lens.prop(cam.data.dof, "use_dof", text="被写界深度 (ボケ)")
            if cam.data.dof.use_dof:
                if cam.data.dof.focus_object:
                    col_lens.prop(cam.data.dof, "focus_object", text="ピント対象")
                col_lens.prop(cam.data.dof, "aperture_fstop", text="F値 (小さいとボケる)")
                
            box_exp = layout.box()
            box_exp.label(text="8. 設定値の保存 (自己書き出し)", icon='TEXT')
            box_exp.operator(f"object.{PREFIX}_export_addon", icon='EXPORT')

class VIEW3D_PT_links(bpy.types.Panel):
    bl_label = "Links"
    bl_idname = f"{PREFIX.upper()}_PT_links"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        if LINK_NAME_1 and LINK_URL_1: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_1, icon='URL').url = LINK_URL_1
        if LINK_NAME_2 and LINK_URL_2: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_2, icon='URL').url = LINK_URL_2
        if LINK_NAME_3 and LINK_URL_3: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_3, icon='URL').url = LINK_URL_3
        if LINK_NAME_4 and LINK_URL_4: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_4, icon='URL').url = LINK_URL_4
        if LINK_NAME_5 and LINK_URL_5: layout.operator(f"wm.{PREFIX}_open_url", text=LINK_NAME_5, icon='URL').url = LINK_URL_5

class VIEW3D_PT_remove(bpy.types.Panel):
    bl_label = "Addon Remove"
    bl_idname = f"{PREFIX.upper()}_PT_remove"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY

    def draw(self, context):
        layout = self.layout
        layout.operator(f"wm.{PREFIX}_remove_addon", icon='CANCEL')

# ===================================
# 10. register()
# ===================================
classes = [
    CP_OT_open_url,
    CP_OT_remove_addon,
    ADDON_Properties,
    VIEW3D_OT_reset_view,
    OBJECT_OT_create_cam_rig,
    OBJECT_OT_sync_from_scene,
    OBJECT_OT_sync_to_scene,
    OBJECT_OT_apply_cam_anim,
    OBJECT_OT_switch_cam_main,
    OBJECT_OT_switch_cam_back,
    OBJECT_OT_set_active_camera,
    OBJECT_OT_update_camera_info,
    OBJECT_OT_copy_camera_info,
    OBJECT_OT_export_addon,
    VIEW3D_PT_main,
    VIEW3D_PT_camera,
    VIEW3D_PT_links,
    VIEW3D_PT_remove
]

def register():
    for c in classes:
        bpy.utils.register_class(c)
    setattr(bpy.types.Scene, f"{PREFIX}_props", bpy.props.PointerProperty(type=ADDON_Properties))
    
    def apply_initial_settings():
        if bpy.context.scene and hasattr(bpy.context.scene, f"{PREFIX}_props"):
            props = getattr(bpy.context.scene, f"{PREFIX}_props")
            try:
                bpy.context.preferences.themes[0].view_3d.camera = props.camera_color
            except Exception:
                pass

        if bpy.context.window_manager:
            for window in bpy.context.window_manager.windows:
                for area in window.screen.areas:
                    if area.type == 'VIEW_3D':
                        for space in area.spaces:
                            if space.type == 'VIEW_3D':
                                space.show_region_ui = True
                                space.shading.type = 'MATERIAL'
                                
        return None

    bpy.app.timers.register(apply_initial_settings, first_interval=0.1)

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

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
カメラと
立方体 それぞれの 初期値20秒間の移動軌跡を描く兄身
色 別々で 太さ調整あり
それぞれ 表示非表示ボタン 移動軌跡