331 Camera simple












bl_info = {
    "name": "Fixed Camera & Automation Master (単一カメラ版)",
    "author": "Your Name",
    "version": (4, 33),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "固定位置カメラ(カメラ円周移動・軌道設定、枠付き、000方向矢印、窓面平面 追随強化)",
    "category": "3D View",
}

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

# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed "
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_TARGET = "FixedCamera_Target"

# 瞳孔平面 (6番)
OBJ_NAME_PUPIL = "瞳孔平面"
MAT_NAME_PUPIL = "Pupil_Plane_Mat"

# 窓面平面 (7番)
OBJ_NAME_WINDOW = "窓面平面"
MAT_NAME_WINDOW = "Window_Plane_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'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = False

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_ANIM_SPEED_MULT_DEFAULT = 1.0

# 瞳孔平面
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

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

# ==========================================
# 6. 瞳孔平面用 000方向矢印 設定
# ==========================================
ARROW_SHOW_DEFAULT = True
ARROW_LENGTH_DEFAULT = 5.0
ARROW_THICKNESS_DEFAULT = 0.08
ARROW_HEAD_LENGTH_DEFAULT = 0.5
ARROW_HEAD_WIDTH_DEFAULT = 0.25
ARROW_COLOR_DEFAULT = (1.0, 0.0, 0.0, 1.0)
ARROW_ALPHA_DEFAULT = 1.0
ARROW_OFFSET_DEFAULT = 0.0

OBJ_NAME_ARROW = "ARROW_000_DIRECTION"
MAT_NAME_ARROW = "Arrow_000_Mat"

# ==========================================
# 7. 窓面平面 & 000方向矢印 設定
# ==========================================
WINDOW_SHOW_DEFAULT = False
WINDOW_POS_MODE_DEFAULT = 'CAMERA'
WINDOW_CAM_DIST_DEFAULT = 5.0
WINDOW_LOC_DEFAULT = (0.0, 0.0, 0.0)
WINDOW_CIRCLE_Z_DEFAULT = 0.0
WINDOW_CIRCLE_RADIUS_DEFAULT = 10.0
WINDOW_CIRCLE_ANGLE_DEFAULT = 0.0
WINDOW_RADIUS_DEFAULT = 2.0
WINDOW_THICKNESS_DEFAULT = 0.05
WINDOW_SEGMENTS_DEFAULT = 4
COLOR_WINDOW = (0.0, 1.0, 0.0, 1.0)
WINDOW_ROT_X_DEFAULT = 0.0
WINDOW_ROT_Y_DEFAULT = 0.0
WINDOW_ROT_Z_DEFAULT = 0.7854

W_ARROW_SHOW_DEFAULT = True
W_ARROW_LENGTH_DEFAULT = 5.0
W_ARROW_THICKNESS_DEFAULT = 0.08
W_ARROW_HEAD_LENGTH_DEFAULT = 0.5
W_ARROW_HEAD_WIDTH_DEFAULT = 0.25
W_ARROW_COLOR_DEFAULT = (0.0, 1.0, 0.0, 1.0)
W_ARROW_ALPHA_DEFAULT = 1.0
W_ARROW_OFFSET_DEFAULT = 0.0

OBJ_NAME_WINDOW_ARROW = "ARROW_000_DIRECTION_WINDOW"
MAT_NAME_WINDOW_ARROW = "Arrow_000_Window_Mat"

# ===================================
# 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]:
        obj = bpy.data.objects.get(name)
        if obj:
            obj.hide_viewport = not self.show_target_empty

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)

    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_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

# ==========================================
# 6. 瞳孔平面 関連関数
# ==========================================
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

def update_arrow_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_ARROW)
    if not obj: return
    
    L = self.arrow_length
    R = self.arrow_thickness
    HL = self.arrow_head_length
    HW = self.arrow_head_width
    
    mesh = obj.data
    mesh.clear_geometry()
    
    if not self.arrow_show:
        return
        
    verts = []
    faces = []
    
    segments = 16
    z_start = -L / 2.0
    z_end = L / 2.0 - HL
    
    verts.append((0, 0, z_start))
    v_bottom_center = 0
    bottom_start = 1
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_start))
        
    verts.append((0, 0, z_end))
    v_top_center = segments + 1
    top_start = segments + 2
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_end))
        
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((bottom_start + i, bottom_start + next_i, top_start + next_i, top_start + i))
        faces.append((bottom_start + next_i, bottom_start + i, v_bottom_center))
        
    z_head_tip = L / 2.0
    verts.append((0, 0, z_end))
    v_head_center = len(verts) - 1
    
    head_ring_start = len(verts)
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((HW * math.cos(angle), HW * math.sin(angle), z_end))
        
    verts.append((0, 0, z_head_tip))
    v_tip = len(verts) - 1
    
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((head_ring_start + i, head_ring_start + next_i, v_tip))
        faces.append((head_ring_start + next_i, head_ring_start + i, v_head_center))
        
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_arrow_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_ARROW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = (self.arrow_color[0], self.arrow_color[1], self.arrow_color[2], self.arrow_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.arrow_color
                if 'Alpha' in bsdf.inputs:
                    bsdf.inputs['Alpha'].default_value = self.arrow_alpha
        mat.blend_method = 'BLEND'

def update_arrow_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        arrow = bpy.data.objects.get(OBJ_NAME_ARROW)
        pupil = bpy.data.objects.get(OBJ_NAME_PUPIL)
        if not arrow: return
        if not props.arrow_show or not pupil:
            arrow.hide_viewport = True
            arrow.hide_render = True
            return
            
        arrow.hide_viewport = False
        arrow.hide_render = False
        
        P = pupil.matrix_world.translation
        obj_000 = bpy.data.objects.get("000")
        if obj_000:
            T = obj_000.matrix_world.translation
        else:
            T = mathutils.Vector((0.0, 0.0, 0.0))
            
        V = T - P
        if V.length > 0.0001:
            N = V.normalized()
        else:
            N = (pupil.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))).normalized()
                    
        rot_quat = N.to_track_quat('Z', 'Y')
        offset_vec = N * props.arrow_offset
        mat_loc = mathutils.Matrix.Translation(P + offset_vec)
        mat_rot = rot_quat.to_matrix().to_4x4()
        arrow.matrix_world = mat_loc @ mat_rot
    except Exception:
        pass

def update_arrow_transform_cb(self, context):
    update_arrow_transform(context)

def update_arrow_all(self, context):
    update_arrow_mesh(self, context)
    update_arrow_mat(self, context)
    update_arrow_transform(context)

# ==========================================
# 7. 窓面平面 関連関数
# ==========================================
def update_window_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        if not obj: return
        
        if not props.window_plane_show:
            obj.hide_viewport = True
            obj.hide_render = True
            return
            
        obj.hide_viewport = False
        obj.hide_render = False

        if props.window_plane_pos_mode == 'CAMERA':
            # カメラ追随モード: カメラを親に設定し、完全に連動させる
            if cam:
                if obj.parent != cam:
                    obj.parent = cam
                    obj.matrix_parent_inverse = mathutils.Matrix.Identity(4)
                
                # ローカル座標で位置を設定
                obj.location = (0.0, 0.0, -props.window_plane_cam_dist)
                obj.rotation_euler = props.window_plane_rot
        else:
            # それ以外のモード: 親を解除し、ワールド座標で固定させる
            if obj.parent is not None:
                old_mat = obj.matrix_world.copy()
                obj.parent = None
                obj.matrix_world = old_mat

            if props.window_plane_pos_mode == 'OFFSET':
                obj.location = props.window_plane_loc
                obj.rotation_euler = props.window_plane_rot
            elif props.window_plane_pos_mode == 'CIRCLE':
                obj_000 = bpy.data.objects.get("000")
                if obj_000:
                    cx, cy = obj_000.location.x, obj_000.location.y
                else:
                    cx, cy = 0.0, 0.0
                    
                rad = math.radians(props.window_plane_circle_angle)
                x = cx + props.window_plane_circle_radius * math.cos(rad)
                y = cy + props.window_plane_circle_radius * math.sin(rad)
                z = props.window_plane_circle_z
                
                obj.location = (x, y, z)
                obj.rotation_euler = props.window_plane_rot
                
    except Exception:
        pass

def update_window_transform_cb(self, context):
    update_window_transform(context)
    if bpy.context: bpy.context.view_layer.update()
    update_window_arrow_transform(context)

def update_window_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
    if not obj: return
    
    if not props_check(context).window_plane_show: return
        
    mesh = obj.data
    mesh.clear_geometry()
    
    maj_s = self.window_plane_segments
    min_s = 16
    
    val = self.window_plane_radius
    if maj_s == 4: R = val / math.sqrt(2.0)
    else: R = val
        
    r = self.window_plane_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_window_segments(self, context):
    if self.window_plane_segments == 4:
        self.window_plane_rot = (0.0, 0.0, math.radians(45.0))
    else:
        self.window_plane_rot = (0.0, 0.0, math.radians(90.0))
    update_window_mesh(self, context)
    update_window_transform_cb(self, context)

def update_window_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = self.window_plane_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.window_plane_color

def props_check(context):
    return getattr(context.scene, f"{PREFIX}_props", None)

def update_window_all(self, context):
    update_window_mesh(self, context)
    update_window_mat(self, context)
    update_window_transform_cb(self, context)

# --- 7. 窓面平面用矢印 ---
def update_window_arrow_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
    if not obj: return
    if not self.window_arrow_show: return
    
    L = self.window_arrow_length
    R = self.window_arrow_thickness
    HL = self.window_arrow_head_length
    HW = self.window_arrow_head_width
    
    mesh = obj.data
    mesh.clear_geometry()
    
    verts, faces = [], []
    segments = 16
    z_start = -L / 2.0
    z_end = L / 2.0 - HL
    
    verts.append((0, 0, z_start))
    v_bottom_center = 0
    bottom_start = 1
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_start))
        
    verts.append((0, 0, z_end))
    v_top_center = segments + 1
    top_start = segments + 2
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_end))
        
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((bottom_start + i, bottom_start + next_i, top_start + next_i, top_start + i))
        faces.append((bottom_start + next_i, bottom_start + i, v_bottom_center))
        
    z_head_tip = L / 2.0
    verts.append((0, 0, z_end))
    v_head_center = len(verts) - 1
    
    head_ring_start = len(verts)
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((HW * math.cos(angle), HW * math.sin(angle), z_end))
        
    verts.append((0, 0, z_head_tip))
    v_tip = len(verts) - 1
    
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((head_ring_start + i, head_ring_start + next_i, v_tip))
        faces.append((head_ring_start + next_i, head_ring_start + i, v_head_center))
        
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_window_arrow_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = (self.window_arrow_color[0], self.window_arrow_color[1], self.window_arrow_color[2], self.window_arrow_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.window_arrow_color
                if 'Alpha' in bsdf.inputs: bsdf.inputs['Alpha'].default_value = self.window_arrow_alpha
        mat.blend_method = 'BLEND'

def update_window_arrow_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        arrow = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
        window = bpy.data.objects.get(OBJ_NAME_WINDOW)
        if not arrow: return
        if not props.window_arrow_show or not window:
            arrow.hide_viewport = True
            arrow.hide_render = True
            return
            
        arrow.hide_viewport = False
        arrow.hide_render = False
        
        P = window.matrix_world.translation
        obj_000 = bpy.data.objects.get("000")
        if obj_000:
            T = obj_000.matrix_world.translation
        else:
            T = mathutils.Vector((0.0, 0.0, 0.0))
            
        V = T - P
        if V.length > 0.0001:
            N = V.normalized()
        else:
            N = (window.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))).normalized()
                    
        rot_quat = N.to_track_quat('Z', 'Y')
        offset_vec = N * props.window_arrow_offset
        mat_loc = mathutils.Matrix.Translation(P + offset_vec)
        mat_rot = rot_quat.to_matrix().to_4x4()
        arrow.matrix_world = mat_loc @ mat_rot
    except Exception:
        pass

def update_window_arrow_transform_cb(self, context):
    update_window_arrow_transform(context)

def update_window_arrow_all(self, context):
    update_window_arrow_mesh(self, context)
    update_window_arrow_mat(self, context)
    update_window_arrow_transform(context)

# --- ハンドラ ---
@bpy.app.handlers.persistent
def arrow_depsgraph_update(scene, depsgraph):
    # Depsgraph変更時に瞳孔用・窓面用、カメラ追従の更新を行う
    if bpy.context:
        update_arrow_transform(bpy.context)
        update_window_transform(bpy.context)
        update_window_arrow_transform(bpy.context)

# --- 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_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)
    
    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
    )
    
    # 6. 瞳孔平面 関連プロパティ
    pupil_window_show: bpy.props.BoolProperty(name="瞳孔平面を表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
    pupil_window_dist: bpy.props.FloatProperty(name="距離 (-100から100)", default=PUPIL_DIST_DEFAULT, min=-100.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)

    arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=ARROW_SHOW_DEFAULT, update=update_arrow_all)
    arrow_length: bpy.props.FloatProperty(name="矢印全長", default=ARROW_LENGTH_DEFAULT, min=0.1, update=update_arrow_mesh)
    arrow_thickness: bpy.props.FloatProperty(name="矢印太さ", default=ARROW_THICKNESS_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_head_length: bpy.props.FloatProperty(name="矢印先端長さ", default=ARROW_HEAD_LENGTH_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_head_width: bpy.props.FloatProperty(name="矢印先端幅", default=ARROW_HEAD_WIDTH_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_color: bpy.props.FloatVectorProperty(name="矢印色", subtype='COLOR', size=4, default=ARROW_COLOR_DEFAULT, min=0.0, max=1.0, update=update_arrow_mat)
    arrow_alpha: bpy.props.FloatProperty(name="透明度", default=ARROW_ALPHA_DEFAULT, min=0.0, max=1.0, update=update_arrow_mat)
    arrow_offset: bpy.props.FloatProperty(name="矢印中央の移動", default=ARROW_OFFSET_DEFAULT, update=update_arrow_transform_cb)

    # 7. 窓面平面 関連プロパティ
    window_plane_show: bpy.props.BoolProperty(name="窓面平面を表示", default=WINDOW_SHOW_DEFAULT, update=update_window_all)
    window_plane_pos_mode: bpy.props.EnumProperty(
        name="配置モード",
        items=[
            ('CAMERA', "カメラ追随", "カメラの前方に追随します"),
            ('OFFSET', "平行移動", "固定位置に配置します"), 
            ('CIRCLE', "円周移動", "000を中心に回転します")
        ],
        default=WINDOW_POS_MODE_DEFAULT,
        update=update_window_transform_cb
    )
    window_plane_cam_dist: bpy.props.FloatProperty(name="カメラからの距離", default=WINDOW_CAM_DIST_DEFAULT, min=-100.0, max=100.0, update=update_window_transform_cb)
    window_plane_loc: bpy.props.FloatVectorProperty(name="XYZ平行移動", default=WINDOW_LOC_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_z: bpy.props.FloatProperty(name="Z軸高さ", default=WINDOW_CIRCLE_Z_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_radius: bpy.props.FloatProperty(name="半径", default=WINDOW_CIRCLE_RADIUS_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_angle: bpy.props.FloatProperty(name="角度 (円周上の位置)", default=WINDOW_CIRCLE_ANGLE_DEFAULT, update=update_window_transform_cb)
    
    window_plane_radius: bpy.props.FloatProperty(name="サイズ", default=WINDOW_RADIUS_DEFAULT, min=0.1, update=update_window_mesh)
    window_plane_thickness: bpy.props.FloatProperty(name="枠の太さ", default=WINDOW_THICKNESS_DEFAULT, min=0.01, update=update_window_mesh)
    window_plane_segments: bpy.props.IntProperty(name="角数 (3-32)", default=WINDOW_SEGMENTS_DEFAULT, min=3, max=32, update=update_window_segments)
    window_plane_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_WINDOW, min=0.0, max=1.0, update=update_window_mat)
    window_plane_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(WINDOW_ROT_X_DEFAULT, WINDOW_ROT_Y_DEFAULT, WINDOW_ROT_Z_DEFAULT), update=update_window_transform_cb)

    window_arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=W_ARROW_SHOW_DEFAULT, update=update_window_arrow_all)
    window_arrow_length: bpy.props.FloatProperty(name="矢印全長", default=W_ARROW_LENGTH_DEFAULT, min=0.1, update=update_window_arrow_mesh)
    window_arrow_thickness: bpy.props.FloatProperty(name="矢印太さ", default=W_ARROW_THICKNESS_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_head_length: bpy.props.FloatProperty(name="矢印先端長さ", default=W_ARROW_HEAD_LENGTH_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_head_width: bpy.props.FloatProperty(name="矢印先端幅", default=W_ARROW_HEAD_WIDTH_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_color: bpy.props.FloatVectorProperty(name="矢印色", subtype='COLOR', size=4, default=W_ARROW_COLOR_DEFAULT, min=0.0, max=1.0, update=update_window_arrow_mat)
    window_arrow_alpha: bpy.props.FloatProperty(name="透明度", default=W_ARROW_ALPHA_DEFAULT, min=0.0, max=1.0, update=update_window_arrow_mat)
    window_arrow_offset: bpy.props.FloatProperty(name="矢印中央の移動", default=W_ARROW_OFFSET_DEFAULT, update=update_window_arrow_transform_cb)

    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_TARGET, OBJ_NAME_PUPIL, OBJ_NAME_ARROW, OBJ_NAME_WINDOW, OBJ_NAME_WINDOW_ARROW]:
            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)

        tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, None)
        tgt_obj.empty_display_type = 'PLAIN_AXES'
        sub_col.objects.link(tgt_obj)
        
        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 = False
        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
        
        # 6. 瞳孔平面の作成 (カメラの子)
        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)

        # 瞳孔平面用矢印の作成
        m_arrow = bpy.data.meshes.new(f"{OBJ_NAME_ARROW}_Mesh")
        o_arrow = bpy.data.objects.new(OBJ_NAME_ARROW, m_arrow)
        sub_col.objects.link(o_arrow)
        mat_arrow = bpy.data.materials.new(MAT_NAME_ARROW)
        mat_arrow.use_nodes = True
        mat_arrow.blend_method = 'BLEND'
        o_arrow.data.materials.append(mat_arrow)
        
        # 7. 窓面平面の作成 (ワールド直下 または カメラの子)
        m_window = bpy.data.meshes.new(f"{OBJ_NAME_WINDOW}_Mesh")
        o_window = bpy.data.objects.new(OBJ_NAME_WINDOW, m_window)
        o_window.rotation_mode = 'XYZ'
        sub_col.objects.link(o_window)
        mat_window = bpy.data.materials.new(MAT_NAME_WINDOW)
        mat_window.use_nodes = True
        mat_window.blend_method = 'BLEND'
        o_window.data.materials.append(mat_window)

        # 窓面平面用矢印の作成
        m_w_arrow = bpy.data.meshes.new(f"{OBJ_NAME_WINDOW_ARROW}_Mesh")
        o_w_arrow = bpy.data.objects.new(OBJ_NAME_WINDOW_ARROW, m_w_arrow)
        sub_col.objects.link(o_w_arrow)
        mat_w_arrow = bpy.data.materials.new(MAT_NAME_WINDOW_ARROW)
        mat_w_arrow.use_nodes = True
        mat_w_arrow.blend_method = 'BLEND'
        o_w_arrow.data.materials.append(mat_w_arrow)
        
        
        # 初期化更新の実行
        update_pupil_window(props, context)
        update_pupil_window_rot(props, context)
        update_pupil_window_mat(props, context)

        update_window_all(props, context)

        update_cam_root_constraint(props, context)
        update_visibility(props, context)
        update_cam_anim(props, context)
        
        context.view_layer.update()
        
        update_arrow_all(props, context)
        update_window_arrow_all(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_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 = "メインカメラ"
            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_reset_cam_offset_rot(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_reset_cam_offset_rot"
    bl_label = "角度ズレをリセット (0, 0, 0)"
    bl_description = "カメラの角度ズレ(オフセット)をすべて0にします"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.cam_local_offset_rot = (0.0, 0.0, 0.0)
        return {'FINISHED'}

class OBJECT_OT_reset_cam_root_rot(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_reset_cam_root_rot"
    bl_label = "初期回転をリセット"
    bl_description = "Rootの初期回転を元のデフォルト状態に戻します"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.cam_rot_start = (CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START)
        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 = "): 
                val = str(getattr(props, 'cam_move_mode', 'FIXED'))
                lines[i] = f"CAM_MOVE_MODE_DEFAULT = '{val}'"
            elif line.startswith("CAM_TARGET_MODE_DEFAULT = "): 
                val = str(getattr(props, 'cam_target_mode', 'NONE'))
                lines[i] = f"CAM_TARGET_MODE_DEFAULT = '{val}'"
            elif line.startswith("CAMERA_PRESET_DEFAULT = "): 
                val = str(getattr(props, 'camera_preset', 'Cam'))
                lines[i] = f"CAMERA_PRESET_DEFAULT = '{val}'"
            elif line.startswith("WINDOW_POS_MODE_DEFAULT = "): 
                val = str(getattr(props, 'window_plane_pos_mode', 'CAMERA'))
                if val not in ['CAMERA', 'OFFSET', 'CIRCLE']: val = 'CAMERA'
                lines[i] = f"WINDOW_POS_MODE_DEFAULT = '{val}'"

            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("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_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"

            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}"
            
            # 6. Pupil
            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}"
            elif line.startswith("ARROW_SHOW_DEFAULT = "): lines[i] = f"ARROW_SHOW_DEFAULT = {props.arrow_show}"
            elif line.startswith("ARROW_LENGTH_DEFAULT = "): lines[i] = f"ARROW_LENGTH_DEFAULT = {props.arrow_length:.4f}"
            elif line.startswith("ARROW_THICKNESS_DEFAULT = "): lines[i] = f"ARROW_THICKNESS_DEFAULT = {props.arrow_thickness:.4f}"
            elif line.startswith("ARROW_HEAD_LENGTH_DEFAULT = "): lines[i] = f"ARROW_HEAD_LENGTH_DEFAULT = {props.arrow_head_length:.4f}"
            elif line.startswith("ARROW_HEAD_WIDTH_DEFAULT = "): lines[i] = f"ARROW_HEAD_WIDTH_DEFAULT = {props.arrow_head_width:.4f}"
            elif line.startswith("ARROW_COLOR_DEFAULT = "): lines[i] = f"ARROW_COLOR_DEFAULT = ({props.arrow_color[0]:.4f}, {props.arrow_color[1]:.4f}, {props.arrow_color[2]:.4f}, {props.arrow_color[3]:.4f})"
            elif line.startswith("ARROW_ALPHA_DEFAULT = "): lines[i] = f"ARROW_ALPHA_DEFAULT = {props.arrow_alpha:.4f}"
            elif line.startswith("ARROW_OFFSET_DEFAULT = "): lines[i] = f"ARROW_OFFSET_DEFAULT = {props.arrow_offset:.4f}"

            # 7. Window Plane
            elif line.startswith("WINDOW_SHOW_DEFAULT = "): lines[i] = f"WINDOW_SHOW_DEFAULT = {props.window_plane_show}"
            elif line.startswith("WINDOW_CAM_DIST_DEFAULT = "): lines[i] = f"WINDOW_CAM_DIST_DEFAULT = {props.window_plane_cam_dist:.4f}"
            elif line.startswith("WINDOW_LOC_DEFAULT = "): lines[i] = f"WINDOW_LOC_DEFAULT = ({props.window_plane_loc[0]:.4f}, {props.window_plane_loc[1]:.4f}, {props.window_plane_loc[2]:.4f})"
            elif line.startswith("WINDOW_CIRCLE_Z_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_Z_DEFAULT = {props.window_plane_circle_z:.4f}"
            elif line.startswith("WINDOW_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_RADIUS_DEFAULT = {props.window_plane_circle_radius:.4f}"
            elif line.startswith("WINDOW_CIRCLE_ANGLE_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_ANGLE_DEFAULT = {props.window_plane_circle_angle:.4f}"
            elif line.startswith("WINDOW_RADIUS_DEFAULT = "): lines[i] = f"WINDOW_RADIUS_DEFAULT = {props.window_plane_radius:.4f}"
            elif line.startswith("WINDOW_THICKNESS_DEFAULT = "): lines[i] = f"WINDOW_THICKNESS_DEFAULT = {props.window_plane_thickness:.4f}"
            elif line.startswith("WINDOW_SEGMENTS_DEFAULT = "): lines[i] = f"WINDOW_SEGMENTS_DEFAULT = {props.window_plane_segments}"
            elif line.startswith("COLOR_WINDOW = "): lines[i] = f"COLOR_WINDOW = ({props.window_plane_color[0]:.4f}, {props.window_plane_color[1]:.4f}, {props.window_plane_color[2]:.4f}, 1.0)"
            elif line.startswith("WINDOW_ROT_X_DEFAULT = "): lines[i] = f"WINDOW_ROT_X_DEFAULT = {props.window_plane_rot[0]:.4f}"
            elif line.startswith("WINDOW_ROT_Y_DEFAULT = "): lines[i] = f"WINDOW_ROT_Y_DEFAULT = {props.window_plane_rot[1]:.4f}"
            elif line.startswith("WINDOW_ROT_Z_DEFAULT = "): lines[i] = f"WINDOW_ROT_Z_DEFAULT = {props.window_plane_rot[2]:.4f}"
            
            elif line.startswith("W_ARROW_SHOW_DEFAULT = "): lines[i] = f"W_ARROW_SHOW_DEFAULT = {props.window_arrow_show}"
            elif line.startswith("W_ARROW_LENGTH_DEFAULT = "): lines[i] = f"W_ARROW_LENGTH_DEFAULT = {props.window_arrow_length:.4f}"
            elif line.startswith("W_ARROW_THICKNESS_DEFAULT = "): lines[i] = f"W_ARROW_THICKNESS_DEFAULT = {props.window_arrow_thickness:.4f}"
            elif line.startswith("W_ARROW_HEAD_LENGTH_DEFAULT = "): lines[i] = f"W_ARROW_HEAD_LENGTH_DEFAULT = {props.window_arrow_head_length:.4f}"
            elif line.startswith("W_ARROW_HEAD_WIDTH_DEFAULT = "): lines[i] = f"W_ARROW_HEAD_WIDTH_DEFAULT = {props.window_arrow_head_width:.4f}"
            elif line.startswith("W_ARROW_COLOR_DEFAULT = "): lines[i] = f"W_ARROW_COLOR_DEFAULT = ({props.window_arrow_color[0]:.4f}, {props.window_arrow_color[1]:.4f}, {props.window_arrow_color[2]:.4f}, {props.window_arrow_color[3]:.4f})"
            elif line.startswith("W_ARROW_ALPHA_DEFAULT = "): lines[i] = f"W_ARROW_ALPHA_DEFAULT = {props.window_arrow_alpha:.4f}"
            elif line.startswith("W_ARROW_OFFSET_DEFAULT = "): lines[i] = f"W_ARROW_OFFSET_DEFAULT = {props.window_arrow_offset:.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")
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        
        # --- 1. 注視点 (Track To) 設定 ---
        box_tgt = layout.box()
        box_tgt.label(text="1. 注視点 (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 (Yaw)")
            col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Roll)")
            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 ['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} (Yaw)")
        col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Roll)")
        col_off.operator(f"object.{PREFIX}_reset_cam_offset_rot", icon='FILE_REFRESH')

        # --- 2. レンズ・カメラ設定 ---
        box_lens = layout.box()
        box_lens.label(text="2. レンズ・カメラ設定", icon='VIEW_CAMERA')
        if cam and cam.type == '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値 (小さいとボケる)")
        else:
            box_lens.label(text="※リグ作成後に設定できます", icon='INFO')

        # --- 3. カメラリグの配置 (移動) ---
        box = layout.box()
        box.label(text="3. カメラリグの配置 (移動)", 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 (Yaw)")
            col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Roll)")
            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')
        

        if root and cam:
            # --- 4. アニメーション設定 ---
            box_rot = layout.box()
            box_rot.label(text="4. アニメーション設定", 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} (Yaw)")
                col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Roll)")
                col_rot.operator(f"object.{PREFIX}_reset_cam_root_rot", icon='FILE_REFRESH')
                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()

            # --- 画面情報表示 ---
            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')

            # --- 5. カメラ色 (共通テーマ) & 表示 ---
            box_vis = layout.box()
            box_vis.label(text="5. カメラ色 (共通テーマ) & 表示", 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") 
            
            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="")

            # --- 6. 瞳孔平面 & 000方向矢印 ---
            box_pupil_arrow = layout.box()
            box_pupil_arrow.label(text="6. 瞳孔平面 & 000方向矢印", icon='MESH_TORUS')
            
            row_pa = box_pupil_arrow.row(align=True)
            row_pa.prop(props, "pupil_window_show", text="瞳孔平面を表示")
            row_pa.prop(props, "arrow_show", text="矢印を表示")

            if props.pupil_window_show:
                col_p = box_pupil_arrow.column(align=True)
                col_p.label(text="[瞳孔平面] ※カメラ前方に配置", icon='MESH_PLANE')
                col_p.prop(props, "pupil_window_dist", text="距離 (-100から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 (Yaw)")
                col_p.prop(props, "pupil_window_rot", index=2, text="Z (Roll)")
                box_pupil_arrow.separator()

            if props.arrow_show:
                col_a = box_pupil_arrow.column(align=True)
                col_a.label(text="[000方向矢印]", icon='EMPTY_AXIS')
                obj_000 = bpy.data.objects.get("000")
                if not obj_000: col_a.label(text="「000」が見つからないため、原点(0,0,0)を向いています", icon='INFO')
                col_a.prop(props, "arrow_offset", text="矢印中央の移動")
                col_a.prop(props, "arrow_length")
                col_a.prop(props, "arrow_thickness")
                col_a.prop(props, "arrow_head_length")
                col_a.prop(props, "arrow_head_width")
                col_a.separator()
                col_a.prop(props, "arrow_color")
                col_a.prop(props, "arrow_alpha", slider=True)

            # --- 7. 窓面平面 & 000方向矢印 ---
            box_window = layout.box()
            box_window.label(text="7. 窓面平面 & 000方向矢印", icon='MESH_PLANE')
            
            row_wa = box_window.row(align=True)
            row_wa.prop(props, "window_plane_show", text="窓面平面を表示")
            row_wa.prop(props, "window_arrow_show", text="矢印を表示")

            if props.window_plane_show:
                col_w = box_window.column(align=True)
                col_w.label(text="[窓面平面の位置]", icon='ORIENTATION_GLOBAL')
                col_w.prop(props, "window_plane_pos_mode", expand=True)
                
                if props.window_plane_pos_mode == 'OFFSET':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_loc", index=0, text="X")
                    col_pos.prop(props, "window_plane_loc", index=1, text="Y")
                    col_pos.prop(props, "window_plane_loc", index=2, text="Z")
                elif props.window_plane_pos_mode == 'CIRCLE':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_circle_z")
                    col_pos.prop(props, "window_plane_circle_radius")
                    col_pos.prop(props, "window_plane_circle_angle")
                elif props.window_plane_pos_mode == 'CAMERA':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_cam_dist", text="距離 (-100から100)", slider=True)
                
                col_w.separator()
                col_w.label(text="[形状設定]", icon='MESH_DATA')
                col_w.prop(props, "window_plane_segments", text="角数 (3-32)")
                if props.window_plane_segments == 4: col_w.prop(props, "window_plane_radius", text="1辺の長さ")
                else: col_w.prop(props, "window_plane_radius", text="半径")
                col_w.prop(props, "window_plane_thickness")
                col_w.prop(props, "window_plane_color")
                col_w.separator()
                col_w.label(text="ローカル回転 (XYZ):")
                col_w.prop(props, "window_plane_rot", index=0, text="X (Pitch)")
                col_w.prop(props, "window_plane_rot", index=1, text="Y (Yaw)")
                col_w.prop(props, "window_plane_rot", index=2, text="Z (Roll)")

                box_window.separator()

            if props.window_arrow_show:
                col_wa = box_window.column(align=True)
                col_wa.label(text="[000方向矢印]", icon='EMPTY_AXIS')
                obj_000 = bpy.data.objects.get("000")
                if not obj_000: col_wa.label(text="「000」が見つからないため、原点(0,0,0)を向いています", icon='INFO')
                col_wa.prop(props, "window_arrow_offset", text="矢印中央の移動")
                col_wa.prop(props, "window_arrow_length")
                col_wa.prop(props, "window_arrow_thickness")
                col_wa.prop(props, "window_arrow_head_length")
                col_wa.prop(props, "window_arrow_head_width")
                col_wa.separator()
                col_wa.prop(props, "window_arrow_color")
                col_wa.prop(props, "window_arrow_alpha", slider=True)

                
            # --- 8. 設定値の保存 ---
            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_set_active_camera,
    OBJECT_OT_update_camera_info,
    OBJECT_OT_copy_camera_info,
    OBJECT_OT_reset_cam_offset_rot,
    OBJECT_OT_reset_cam_root_rot,
    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))
    
    if arrow_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.append(arrow_depsgraph_update)
    
    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:
            try:
                bpy.context.view_layer.update()
                update_arrow_transform(bpy.context)
                update_window_arrow_transform(bpy.context)
            except:
                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():
    if arrow_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.remove(arrow_depsgraph_update)

    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, 32),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "固定位置カメラ(カメラ円周移動・軌道設定、枠付き、000方向矢印、窓面平面 追随追加)",
    "category": "3D View",
}

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

# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed "
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_TARGET = "FixedCamera_Target"

# 瞳孔平面 (6番)
OBJ_NAME_PUPIL = "瞳孔平面"
MAT_NAME_PUPIL = "Pupil_Plane_Mat"

# 窓面平面 (7番)
OBJ_NAME_WINDOW = "窓面平面"
MAT_NAME_WINDOW = "Window_Plane_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'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = False

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_ANIM_SPEED_MULT_DEFAULT = 1.0

# 瞳孔平面
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

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

# ==========================================
# 6. 瞳孔平面用 000方向矢印 設定
# ==========================================
ARROW_SHOW_DEFAULT = True
ARROW_LENGTH_DEFAULT = 5.0
ARROW_THICKNESS_DEFAULT = 0.08
ARROW_HEAD_LENGTH_DEFAULT = 0.5
ARROW_HEAD_WIDTH_DEFAULT = 0.25
ARROW_COLOR_DEFAULT = (1.0, 0.0, 0.0, 1.0)
ARROW_ALPHA_DEFAULT = 1.0
ARROW_OFFSET_DEFAULT = 0.0

OBJ_NAME_ARROW = "ARROW_000_DIRECTION"
MAT_NAME_ARROW = "Arrow_000_Mat"

# ==========================================
# 7. 窓面平面 & 000方向矢印 設定
# ==========================================
WINDOW_SHOW_DEFAULT = False
WINDOW_POS_MODE_DEFAULT = 'CAMERA'
WINDOW_CAM_DIST_DEFAULT = 5.0
WINDOW_LOC_DEFAULT = (0.0, 0.0, 0.0)
WINDOW_CIRCLE_Z_DEFAULT = 0.0
WINDOW_CIRCLE_RADIUS_DEFAULT = 10.0
WINDOW_CIRCLE_ANGLE_DEFAULT = 0.0
WINDOW_RADIUS_DEFAULT = 2.0
WINDOW_THICKNESS_DEFAULT = 0.05
WINDOW_SEGMENTS_DEFAULT = 4
COLOR_WINDOW = (0.0, 1.0, 0.0, 1.0)
WINDOW_ROT_X_DEFAULT = 0.0
WINDOW_ROT_Y_DEFAULT = 0.0
WINDOW_ROT_Z_DEFAULT = 0.7854

W_ARROW_SHOW_DEFAULT = True
W_ARROW_LENGTH_DEFAULT = 5.0
W_ARROW_THICKNESS_DEFAULT = 0.08
W_ARROW_HEAD_LENGTH_DEFAULT = 0.5
W_ARROW_HEAD_WIDTH_DEFAULT = 0.25
W_ARROW_COLOR_DEFAULT = (0.0, 1.0, 0.0, 1.0)
W_ARROW_ALPHA_DEFAULT = 1.0
W_ARROW_OFFSET_DEFAULT = 0.0

OBJ_NAME_WINDOW_ARROW = "ARROW_000_DIRECTION_WINDOW"
MAT_NAME_WINDOW_ARROW = "Arrow_000_Window_Mat"

# ===================================
# 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]:
        obj = bpy.data.objects.get(name)
        if obj:
            obj.hide_viewport = not self.show_target_empty

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)

    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_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

# ==========================================
# 6. 瞳孔平面 関連関数
# ==========================================
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

def update_arrow_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_ARROW)
    if not obj: return
    
    L = self.arrow_length
    R = self.arrow_thickness
    HL = self.arrow_head_length
    HW = self.arrow_head_width
    
    mesh = obj.data
    mesh.clear_geometry()
    
    if not self.arrow_show:
        return
        
    verts = []
    faces = []
    
    segments = 16
    z_start = -L / 2.0
    z_end = L / 2.0 - HL
    
    verts.append((0, 0, z_start))
    v_bottom_center = 0
    bottom_start = 1
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_start))
        
    verts.append((0, 0, z_end))
    v_top_center = segments + 1
    top_start = segments + 2
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_end))
        
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((bottom_start + i, bottom_start + next_i, top_start + next_i, top_start + i))
        faces.append((bottom_start + next_i, bottom_start + i, v_bottom_center))
        
    z_head_tip = L / 2.0
    verts.append((0, 0, z_end))
    v_head_center = len(verts) - 1
    
    head_ring_start = len(verts)
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((HW * math.cos(angle), HW * math.sin(angle), z_end))
        
    verts.append((0, 0, z_head_tip))
    v_tip = len(verts) - 1
    
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((head_ring_start + i, head_ring_start + next_i, v_tip))
        faces.append((head_ring_start + next_i, head_ring_start + i, v_head_center))
        
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_arrow_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_ARROW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = (self.arrow_color[0], self.arrow_color[1], self.arrow_color[2], self.arrow_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.arrow_color
                if 'Alpha' in bsdf.inputs:
                    bsdf.inputs['Alpha'].default_value = self.arrow_alpha
        mat.blend_method = 'BLEND'

def update_arrow_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        arrow = bpy.data.objects.get(OBJ_NAME_ARROW)
        pupil = bpy.data.objects.get(OBJ_NAME_PUPIL)
        if not arrow: return
        if not props.arrow_show or not pupil:
            arrow.hide_viewport = True
            arrow.hide_render = True
            return
            
        arrow.hide_viewport = False
        arrow.hide_render = False
        
        P = pupil.matrix_world.translation
        obj_000 = bpy.data.objects.get("000")
        if obj_000:
            T = obj_000.matrix_world.translation
        else:
            T = mathutils.Vector((0.0, 0.0, 0.0))
            
        V = T - P
        if V.length > 0.0001:
            N = V.normalized()
        else:
            N = (pupil.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))).normalized()
                    
        rot_quat = N.to_track_quat('Z', 'Y')
        offset_vec = N * props.arrow_offset
        mat_loc = mathutils.Matrix.Translation(P + offset_vec)
        mat_rot = rot_quat.to_matrix().to_4x4()
        arrow.matrix_world = mat_loc @ mat_rot
    except Exception:
        pass

def update_arrow_transform_cb(self, context):
    update_arrow_transform(context)

def update_arrow_all(self, context):
    update_arrow_mesh(self, context)
    update_arrow_mat(self, context)
    update_arrow_transform(context)

# ==========================================
# 7. 窓面平面 関連関数
# ==========================================
def update_window_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
        if not obj: return
        
        if not props.window_plane_show:
            obj.hide_viewport = True
            obj.hide_render = True
            return
            
        obj.hide_viewport = False
        obj.hide_render = False

        if props.window_plane_pos_mode == 'OFFSET':
            obj.location = props.window_plane_loc
            obj.rotation_euler = props.window_plane_rot
        elif props.window_plane_pos_mode == 'CIRCLE':
            obj_000 = bpy.data.objects.get("000")
            if obj_000:
                cx, cy = obj_000.location.x, obj_000.location.y
            else:
                cx, cy = 0.0, 0.0
                
            rad = math.radians(props.window_plane_circle_angle)
            x = cx + props.window_plane_circle_radius * math.cos(rad)
            y = cy + props.window_plane_circle_radius * math.sin(rad)
            z = props.window_plane_circle_z
            
            obj.location = (x, y, z)
            obj.rotation_euler = props.window_plane_rot
        elif props.window_plane_pos_mode == 'CAMERA':
            cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
            if cam:
                mat_cam = cam.matrix_world
                local_offset = mathutils.Vector((0.0, 0.0, -props.window_plane_cam_dist))
                world_pos = mat_cam @ local_offset
                
                mat_rot = mathutils.Euler(props.window_plane_rot, 'XYZ').to_matrix().to_4x4()
                mat_final = mat_cam.to_3x3().to_4x4() @ mat_rot
                
                obj.location = world_pos
                obj.rotation_euler = mat_final.to_euler('XYZ')
                
    except Exception:
        pass

def update_window_transform_cb(self, context):
    update_window_transform(context)
    if bpy.context: bpy.context.view_layer.update()
    update_window_arrow_transform(context)

def update_window_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
    if not obj: return
    
    if not props_check(context).window_plane_show: return
        
    mesh = obj.data
    mesh.clear_geometry()
    
    maj_s = self.window_plane_segments
    min_s = 16
    
    val = self.window_plane_radius
    if maj_s == 4: R = val / math.sqrt(2.0)
    else: R = val
        
    r = self.window_plane_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_window_segments(self, context):
    if self.window_plane_segments == 4:
        self.window_plane_rot = (0.0, 0.0, math.radians(45.0))
    else:
        self.window_plane_rot = (0.0, 0.0, math.radians(90.0))
    update_window_mesh(self, context)
    update_window_transform_cb(self, context)

def update_window_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = self.window_plane_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.window_plane_color

def props_check(context):
    return getattr(context.scene, f"{PREFIX}_props", None)

def update_window_all(self, context):
    update_window_mesh(self, context)
    update_window_mat(self, context)
    update_window_transform_cb(self, context)

# --- 7. 窓面平面用矢印 ---
def update_window_arrow_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
    if not obj: return
    if not self.window_arrow_show: return
    
    L = self.window_arrow_length
    R = self.window_arrow_thickness
    HL = self.window_arrow_head_length
    HW = self.window_arrow_head_width
    
    mesh = obj.data
    mesh.clear_geometry()
    
    verts, faces = [], []
    segments = 16
    z_start = -L / 2.0
    z_end = L / 2.0 - HL
    
    verts.append((0, 0, z_start))
    v_bottom_center = 0
    bottom_start = 1
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_start))
        
    verts.append((0, 0, z_end))
    v_top_center = segments + 1
    top_start = segments + 2
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_end))
        
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((bottom_start + i, bottom_start + next_i, top_start + next_i, top_start + i))
        faces.append((bottom_start + next_i, bottom_start + i, v_bottom_center))
        
    z_head_tip = L / 2.0
    verts.append((0, 0, z_end))
    v_head_center = len(verts) - 1
    
    head_ring_start = len(verts)
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((HW * math.cos(angle), HW * math.sin(angle), z_end))
        
    verts.append((0, 0, z_head_tip))
    v_tip = len(verts) - 1
    
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((head_ring_start + i, head_ring_start + next_i, v_tip))
        faces.append((head_ring_start + next_i, head_ring_start + i, v_head_center))
        
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_window_arrow_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = (self.window_arrow_color[0], self.window_arrow_color[1], self.window_arrow_color[2], self.window_arrow_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.window_arrow_color
                if 'Alpha' in bsdf.inputs: bsdf.inputs['Alpha'].default_value = self.window_arrow_alpha
        mat.blend_method = 'BLEND'

def update_window_arrow_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        arrow = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
        window = bpy.data.objects.get(OBJ_NAME_WINDOW)
        if not arrow: return
        if not props.window_arrow_show or not window:
            arrow.hide_viewport = True
            arrow.hide_render = True
            return
            
        arrow.hide_viewport = False
        arrow.hide_render = False
        
        P = window.matrix_world.translation
        obj_000 = bpy.data.objects.get("000")
        if obj_000:
            T = obj_000.matrix_world.translation
        else:
            T = mathutils.Vector((0.0, 0.0, 0.0))
            
        V = T - P
        if V.length > 0.0001:
            N = V.normalized()
        else:
            N = (window.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))).normalized()
                    
        rot_quat = N.to_track_quat('Z', 'Y')
        offset_vec = N * props.window_arrow_offset
        mat_loc = mathutils.Matrix.Translation(P + offset_vec)
        mat_rot = rot_quat.to_matrix().to_4x4()
        arrow.matrix_world = mat_loc @ mat_rot
    except Exception:
        pass

def update_window_arrow_transform_cb(self, context):
    update_window_arrow_transform(context)

def update_window_arrow_all(self, context):
    update_window_arrow_mesh(self, context)
    update_window_arrow_mat(self, context)
    update_window_arrow_transform(context)

# --- ハンドラ ---
@bpy.app.handlers.persistent
def arrow_depsgraph_update(scene, depsgraph):
    # Depsgraph変更時に瞳孔用・窓面用、カメラ追従の更新を行う
    update_arrow_transform(bpy.context)
    update_window_transform(bpy.context)
    update_window_arrow_transform(bpy.context)

# --- 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_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)
    
    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
    )
    
    # 6. 瞳孔平面 関連プロパティ
    pupil_window_show: bpy.props.BoolProperty(name="瞳孔平面を表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
    pupil_window_dist: bpy.props.FloatProperty(name="距離 (-100から100)", default=PUPIL_DIST_DEFAULT, min=-100.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)

    arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=ARROW_SHOW_DEFAULT, update=update_arrow_all)
    arrow_length: bpy.props.FloatProperty(name="矢印全長", default=ARROW_LENGTH_DEFAULT, min=0.1, update=update_arrow_mesh)
    arrow_thickness: bpy.props.FloatProperty(name="矢印太さ", default=ARROW_THICKNESS_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_head_length: bpy.props.FloatProperty(name="矢印先端長さ", default=ARROW_HEAD_LENGTH_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_head_width: bpy.props.FloatProperty(name="矢印先端幅", default=ARROW_HEAD_WIDTH_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_color: bpy.props.FloatVectorProperty(name="矢印色", subtype='COLOR', size=4, default=ARROW_COLOR_DEFAULT, min=0.0, max=1.0, update=update_arrow_mat)
    arrow_alpha: bpy.props.FloatProperty(name="透明度", default=ARROW_ALPHA_DEFAULT, min=0.0, max=1.0, update=update_arrow_mat)
    arrow_offset: bpy.props.FloatProperty(name="矢印中央の移動", default=ARROW_OFFSET_DEFAULT, update=update_arrow_transform_cb)

    # 7. 窓面平面 関連プロパティ
    window_plane_show: bpy.props.BoolProperty(name="窓面平面を表示", default=WINDOW_SHOW_DEFAULT, update=update_window_all)
    window_plane_pos_mode: bpy.props.EnumProperty(
        name="配置モード",
        items=[
            ('CAMERA', "カメラ前方 追随", "カメラの前方に追随します"),
            ('OFFSET', "XYZ 平行移動 (固定)", "固定位置に配置します"), 
            ('CIRCLE', "000中心 円周回転 (固定)", "000を中心に回転します")
        ],
        default=WINDOW_POS_MODE_DEFAULT,
        update=update_window_transform_cb
    )
    window_plane_cam_dist: bpy.props.FloatProperty(name="カメラからの距離", default=WINDOW_CAM_DIST_DEFAULT, min=-100.0, max=100.0, update=update_window_transform_cb)
    window_plane_loc: bpy.props.FloatVectorProperty(name="XYZ平行移動", default=WINDOW_LOC_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_z: bpy.props.FloatProperty(name="Z軸高さ", default=WINDOW_CIRCLE_Z_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_radius: bpy.props.FloatProperty(name="半径", default=WINDOW_CIRCLE_RADIUS_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_angle: bpy.props.FloatProperty(name="角度 (円周上の位置)", default=WINDOW_CIRCLE_ANGLE_DEFAULT, update=update_window_transform_cb)
    
    window_plane_radius: bpy.props.FloatProperty(name="サイズ", default=WINDOW_RADIUS_DEFAULT, min=0.1, update=update_window_mesh)
    window_plane_thickness: bpy.props.FloatProperty(name="枠の太さ", default=WINDOW_THICKNESS_DEFAULT, min=0.01, update=update_window_mesh)
    window_plane_segments: bpy.props.IntProperty(name="角数 (3-32)", default=WINDOW_SEGMENTS_DEFAULT, min=3, max=32, update=update_window_segments)
    window_plane_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_WINDOW, min=0.0, max=1.0, update=update_window_mat)
    window_plane_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(WINDOW_ROT_X_DEFAULT, WINDOW_ROT_Y_DEFAULT, WINDOW_ROT_Z_DEFAULT), update=update_window_transform_cb)

    window_arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=W_ARROW_SHOW_DEFAULT, update=update_window_arrow_all)
    window_arrow_length: bpy.props.FloatProperty(name="矢印全長", default=W_ARROW_LENGTH_DEFAULT, min=0.1, update=update_window_arrow_mesh)
    window_arrow_thickness: bpy.props.FloatProperty(name="矢印太さ", default=W_ARROW_THICKNESS_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_head_length: bpy.props.FloatProperty(name="矢印先端長さ", default=W_ARROW_HEAD_LENGTH_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_head_width: bpy.props.FloatProperty(name="矢印先端幅", default=W_ARROW_HEAD_WIDTH_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_color: bpy.props.FloatVectorProperty(name="矢印色", subtype='COLOR', size=4, default=W_ARROW_COLOR_DEFAULT, min=0.0, max=1.0, update=update_window_arrow_mat)
    window_arrow_alpha: bpy.props.FloatProperty(name="透明度", default=W_ARROW_ALPHA_DEFAULT, min=0.0, max=1.0, update=update_window_arrow_mat)
    window_arrow_offset: bpy.props.FloatProperty(name="矢印中央の移動", default=W_ARROW_OFFSET_DEFAULT, update=update_window_arrow_transform_cb)

    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_TARGET, OBJ_NAME_PUPIL, OBJ_NAME_ARROW, OBJ_NAME_WINDOW, OBJ_NAME_WINDOW_ARROW]:
            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)

        tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, None)
        tgt_obj.empty_display_type = 'PLAIN_AXES'
        sub_col.objects.link(tgt_obj)
        
        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 = False
        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
        
        # 6. 瞳孔平面の作成 (カメラの子)
        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)

        # 瞳孔平面用矢印の作成
        m_arrow = bpy.data.meshes.new(f"{OBJ_NAME_ARROW}_Mesh")
        o_arrow = bpy.data.objects.new(OBJ_NAME_ARROW, m_arrow)
        sub_col.objects.link(o_arrow)
        mat_arrow = bpy.data.materials.new(MAT_NAME_ARROW)
        mat_arrow.use_nodes = True
        mat_arrow.blend_method = 'BLEND'
        o_arrow.data.materials.append(mat_arrow)
        
        # 7. 窓面平面の作成 (ワールド直下)
        m_window = bpy.data.meshes.new(f"{OBJ_NAME_WINDOW}_Mesh")
        o_window = bpy.data.objects.new(OBJ_NAME_WINDOW, m_window)
        o_window.rotation_mode = 'XYZ'
        sub_col.objects.link(o_window)
        mat_window = bpy.data.materials.new(MAT_NAME_WINDOW)
        mat_window.use_nodes = True
        mat_window.blend_method = 'BLEND'
        o_window.data.materials.append(mat_window)

        # 窓面平面用矢印の作成
        m_w_arrow = bpy.data.meshes.new(f"{OBJ_NAME_WINDOW_ARROW}_Mesh")
        o_w_arrow = bpy.data.objects.new(OBJ_NAME_WINDOW_ARROW, m_w_arrow)
        sub_col.objects.link(o_w_arrow)
        mat_w_arrow = bpy.data.materials.new(MAT_NAME_WINDOW_ARROW)
        mat_w_arrow.use_nodes = True
        mat_w_arrow.blend_method = 'BLEND'
        o_w_arrow.data.materials.append(mat_w_arrow)
        
        
        # 初期化更新の実行
        update_pupil_window(props, context)
        update_pupil_window_rot(props, context)
        update_pupil_window_mat(props, context)

        update_window_all(props, context)

        update_cam_root_constraint(props, context)
        update_visibility(props, context)
        update_cam_anim(props, context)
        
        context.view_layer.update()
        
        update_arrow_all(props, context)
        update_window_arrow_all(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_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 = "メインカメラ"
            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_reset_cam_offset_rot(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_reset_cam_offset_rot"
    bl_label = "角度ズレをリセット (0, 0, 0)"
    bl_description = "カメラの角度ズレ(オフセット)をすべて0にします"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.cam_local_offset_rot = (0.0, 0.0, 0.0)
        return {'FINISHED'}

class OBJECT_OT_reset_cam_root_rot(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_reset_cam_root_rot"
    bl_label = "初期回転をリセット"
    bl_description = "Rootの初期回転を元のデフォルト状態に戻します"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.cam_rot_start = (CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START)
        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 = "): 
                val = str(getattr(props, 'cam_move_mode', 'FIXED'))
                lines[i] = f"CAM_MOVE_MODE_DEFAULT = '{val}'"
            elif line.startswith("CAM_TARGET_MODE_DEFAULT = "): 
                val = str(getattr(props, 'cam_target_mode', 'NONE'))
                lines[i] = f"CAM_TARGET_MODE_DEFAULT = '{val}'"
            elif line.startswith("CAMERA_PRESET_DEFAULT = "): 
                val = str(getattr(props, 'camera_preset', 'Cam'))
                lines[i] = f"CAMERA_PRESET_DEFAULT = '{val}'"
            elif line.startswith("WINDOW_POS_MODE_DEFAULT = "): 
                val = str(getattr(props, 'window_plane_pos_mode', 'CAMERA'))
                if val not in ['CAMERA', 'OFFSET', 'CIRCLE']: val = 'CAMERA'
                lines[i] = f"WINDOW_POS_MODE_DEFAULT = '{val}'"

            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("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_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"

            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}"
            
            # 6. Pupil
            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}"
            elif line.startswith("ARROW_SHOW_DEFAULT = "): lines[i] = f"ARROW_SHOW_DEFAULT = {props.arrow_show}"
            elif line.startswith("ARROW_LENGTH_DEFAULT = "): lines[i] = f"ARROW_LENGTH_DEFAULT = {props.arrow_length:.4f}"
            elif line.startswith("ARROW_THICKNESS_DEFAULT = "): lines[i] = f"ARROW_THICKNESS_DEFAULT = {props.arrow_thickness:.4f}"
            elif line.startswith("ARROW_HEAD_LENGTH_DEFAULT = "): lines[i] = f"ARROW_HEAD_LENGTH_DEFAULT = {props.arrow_head_length:.4f}"
            elif line.startswith("ARROW_HEAD_WIDTH_DEFAULT = "): lines[i] = f"ARROW_HEAD_WIDTH_DEFAULT = {props.arrow_head_width:.4f}"
            elif line.startswith("ARROW_COLOR_DEFAULT = "): lines[i] = f"ARROW_COLOR_DEFAULT = ({props.arrow_color[0]:.4f}, {props.arrow_color[1]:.4f}, {props.arrow_color[2]:.4f}, {props.arrow_color[3]:.4f})"
            elif line.startswith("ARROW_ALPHA_DEFAULT = "): lines[i] = f"ARROW_ALPHA_DEFAULT = {props.arrow_alpha:.4f}"
            elif line.startswith("ARROW_OFFSET_DEFAULT = "): lines[i] = f"ARROW_OFFSET_DEFAULT = {props.arrow_offset:.4f}"

            # 7. Window Plane
            elif line.startswith("WINDOW_SHOW_DEFAULT = "): lines[i] = f"WINDOW_SHOW_DEFAULT = {props.window_plane_show}"
            elif line.startswith("WINDOW_CAM_DIST_DEFAULT = "): lines[i] = f"WINDOW_CAM_DIST_DEFAULT = {props.window_plane_cam_dist:.4f}"
            elif line.startswith("WINDOW_LOC_DEFAULT = "): lines[i] = f"WINDOW_LOC_DEFAULT = ({props.window_plane_loc[0]:.4f}, {props.window_plane_loc[1]:.4f}, {props.window_plane_loc[2]:.4f})"
            elif line.startswith("WINDOW_CIRCLE_Z_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_Z_DEFAULT = {props.window_plane_circle_z:.4f}"
            elif line.startswith("WINDOW_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_RADIUS_DEFAULT = {props.window_plane_circle_radius:.4f}"
            elif line.startswith("WINDOW_CIRCLE_ANGLE_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_ANGLE_DEFAULT = {props.window_plane_circle_angle:.4f}"
            elif line.startswith("WINDOW_RADIUS_DEFAULT = "): lines[i] = f"WINDOW_RADIUS_DEFAULT = {props.window_plane_radius:.4f}"
            elif line.startswith("WINDOW_THICKNESS_DEFAULT = "): lines[i] = f"WINDOW_THICKNESS_DEFAULT = {props.window_plane_thickness:.4f}"
            elif line.startswith("WINDOW_SEGMENTS_DEFAULT = "): lines[i] = f"WINDOW_SEGMENTS_DEFAULT = {props.window_plane_segments}"
            elif line.startswith("COLOR_WINDOW = "): lines[i] = f"COLOR_WINDOW = ({props.window_plane_color[0]:.4f}, {props.window_plane_color[1]:.4f}, {props.window_plane_color[2]:.4f}, 1.0)"
            elif line.startswith("WINDOW_ROT_X_DEFAULT = "): lines[i] = f"WINDOW_ROT_X_DEFAULT = {props.window_plane_rot[0]:.4f}"
            elif line.startswith("WINDOW_ROT_Y_DEFAULT = "): lines[i] = f"WINDOW_ROT_Y_DEFAULT = {props.window_plane_rot[1]:.4f}"
            elif line.startswith("WINDOW_ROT_Z_DEFAULT = "): lines[i] = f"WINDOW_ROT_Z_DEFAULT = {props.window_plane_rot[2]:.4f}"
            
            elif line.startswith("W_ARROW_SHOW_DEFAULT = "): lines[i] = f"W_ARROW_SHOW_DEFAULT = {props.window_arrow_show}"
            elif line.startswith("W_ARROW_LENGTH_DEFAULT = "): lines[i] = f"W_ARROW_LENGTH_DEFAULT = {props.window_arrow_length:.4f}"
            elif line.startswith("W_ARROW_THICKNESS_DEFAULT = "): lines[i] = f"W_ARROW_THICKNESS_DEFAULT = {props.window_arrow_thickness:.4f}"
            elif line.startswith("W_ARROW_HEAD_LENGTH_DEFAULT = "): lines[i] = f"W_ARROW_HEAD_LENGTH_DEFAULT = {props.window_arrow_head_length:.4f}"
            elif line.startswith("W_ARROW_HEAD_WIDTH_DEFAULT = "): lines[i] = f"W_ARROW_HEAD_WIDTH_DEFAULT = {props.window_arrow_head_width:.4f}"
            elif line.startswith("W_ARROW_COLOR_DEFAULT = "): lines[i] = f"W_ARROW_COLOR_DEFAULT = ({props.window_arrow_color[0]:.4f}, {props.window_arrow_color[1]:.4f}, {props.window_arrow_color[2]:.4f}, {props.window_arrow_color[3]:.4f})"
            elif line.startswith("W_ARROW_ALPHA_DEFAULT = "): lines[i] = f"W_ARROW_ALPHA_DEFAULT = {props.window_arrow_alpha:.4f}"
            elif line.startswith("W_ARROW_OFFSET_DEFAULT = "): lines[i] = f"W_ARROW_OFFSET_DEFAULT = {props.window_arrow_offset:.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")
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        
        # --- 1. 注視点 (Track To) 設定 ---
        box_tgt = layout.box()
        box_tgt.label(text="1. 注視点 (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 (Yaw)")
            col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Roll)")
            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 ['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} (Yaw)")
        col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Roll)")
        col_off.operator(f"object.{PREFIX}_reset_cam_offset_rot", icon='FILE_REFRESH')

        # --- 2. レンズ・カメラ設定 ---
        box_lens = layout.box()
        box_lens.label(text="2. レンズ・カメラ設定", icon='VIEW_CAMERA')
        if cam and cam.type == '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値 (小さいとボケる)")
        else:
            box_lens.label(text="※リグ作成後に設定できます", icon='INFO')

        # --- 3. カメラリグの配置 (移動) ---
        box = layout.box()
        box.label(text="3. カメラリグの配置 (移動)", 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 (Yaw)")
            col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Roll)")
            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')
        

        if root and cam:
            # --- 4. アニメーション設定 ---
            box_rot = layout.box()
            box_rot.label(text="4. アニメーション設定", 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} (Yaw)")
                col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Roll)")
                col_rot.operator(f"object.{PREFIX}_reset_cam_root_rot", icon='FILE_REFRESH')
                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()

            # --- 画面情報表示 ---
            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')

            # --- 5. カメラ色 (共通テーマ) & 表示 ---
            box_vis = layout.box()
            box_vis.label(text="5. カメラ色 (共通テーマ) & 表示", 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") 
            
            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="")

            # --- 6. 瞳孔平面 & 000方向矢印 ---
            box_pupil_arrow = layout.box()
            box_pupil_arrow.label(text="6. 瞳孔平面 & 000方向矢印", icon='MESH_TORUS')
            
            row_pa = box_pupil_arrow.row(align=True)
            row_pa.prop(props, "pupil_window_show", text="瞳孔平面を表示")
            row_pa.prop(props, "arrow_show", text="矢印を表示")

            if props.pupil_window_show:
                col_p = box_pupil_arrow.column(align=True)
                col_p.label(text="[瞳孔平面] ※カメラ前方に配置", icon='MESH_PLANE')
                col_p.prop(props, "pupil_window_dist", text="距離 (-100から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 (Yaw)")
                col_p.prop(props, "pupil_window_rot", index=2, text="Z (Roll)")
                box_pupil_arrow.separator()

            if props.arrow_show:
                col_a = box_pupil_arrow.column(align=True)
                col_a.label(text="[000方向矢印]", icon='EMPTY_AXIS')
                obj_000 = bpy.data.objects.get("000")
                if not obj_000: col_a.label(text="「000」が見つからないため、原点(0,0,0)を向いています", icon='INFO')
                col_a.prop(props, "arrow_offset", text="矢印中央の移動")
                col_a.prop(props, "arrow_length")
                col_a.prop(props, "arrow_thickness")
                col_a.prop(props, "arrow_head_length")
                col_a.prop(props, "arrow_head_width")
                col_a.separator()
                col_a.prop(props, "arrow_color")
                col_a.prop(props, "arrow_alpha", slider=True)

            # --- 7. 窓面平面 & 000方向矢印 ---
            box_window = layout.box()
            box_window.label(text="7. 窓面平面 & 000方向矢印", icon='MESH_PLANE')
            
            row_wa = box_window.row(align=True)
            row_wa.prop(props, "window_plane_show", text="窓面平面を表示")
            row_wa.prop(props, "window_arrow_show", text="矢印を表示")

            if props.window_plane_show:
                col_w = box_window.column(align=True)
                col_w.label(text="[窓面平面の位置]", icon='ORIENTATION_GLOBAL')
                col_w.prop(props, "window_plane_pos_mode", expand=True)
                
                if props.window_plane_pos_mode == 'OFFSET':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_loc", index=0, text="X")
                    col_pos.prop(props, "window_plane_loc", index=1, text="Y")
                    col_pos.prop(props, "window_plane_loc", index=2, text="Z")
                elif props.window_plane_pos_mode == 'CIRCLE':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_circle_z")
                    col_pos.prop(props, "window_plane_circle_radius")
                    col_pos.prop(props, "window_plane_circle_angle")
                elif props.window_plane_pos_mode == 'CAMERA':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_cam_dist", text="距離 (-100から100)", slider=True)
                
                col_w.separator()
                col_w.label(text="[形状設定]", icon='MESH_DATA')
                col_w.prop(props, "window_plane_segments", text="角数 (3-32)")
                if props.window_plane_segments == 4: col_w.prop(props, "window_plane_radius", text="1辺の長さ")
                else: col_w.prop(props, "window_plane_radius", text="半径")
                col_w.prop(props, "window_plane_thickness")
                col_w.prop(props, "window_plane_color")
                col_w.separator()
                col_w.label(text="ローカル回転 (XYZ):")
                col_w.prop(props, "window_plane_rot", index=0, text="X (Pitch)")
                col_w.prop(props, "window_plane_rot", index=1, text="Y (Yaw)")
                col_w.prop(props, "window_plane_rot", index=2, text="Z (Roll)")

                box_window.separator()

            if props.window_arrow_show:
                col_wa = box_window.column(align=True)
                col_wa.label(text="[000方向矢印]", icon='EMPTY_AXIS')
                obj_000 = bpy.data.objects.get("000")
                if not obj_000: col_wa.label(text="「000」が見つからないため、原点(0,0,0)を向いています", icon='INFO')
                col_wa.prop(props, "window_arrow_offset", text="矢印中央の移動")
                col_wa.prop(props, "window_arrow_length")
                col_wa.prop(props, "window_arrow_thickness")
                col_wa.prop(props, "window_arrow_head_length")
                col_wa.prop(props, "window_arrow_head_width")
                col_wa.separator()
                col_wa.prop(props, "window_arrow_color")
                col_wa.prop(props, "window_arrow_alpha", slider=True)

                
            # --- 8. 設定値の保存 ---
            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_set_active_camera,
    OBJECT_OT_update_camera_info,
    OBJECT_OT_copy_camera_info,
    OBJECT_OT_reset_cam_offset_rot,
    OBJECT_OT_reset_cam_root_rot,
    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))
    
    if arrow_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.append(arrow_depsgraph_update)
    
    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:
            try:
                bpy.context.view_layer.update()
                update_arrow_transform(bpy.context)
                update_window_arrow_transform(bpy.context)
            except:
                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():
    if arrow_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.remove(arrow_depsgraph_update)

    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, 31),
    "blender": (5, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "固定位置カメラ(カメラ円周移動・軌道設定、枠付き、000方向矢印、窓面平面 追随追加)",
    "category": "3D View",
}

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

# ===================================
# 4. 共通定義
# ===================================
ADDON_TITLE = "Cam Fixed "
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_TARGET = "FixedCamera_Target"

# 瞳孔平面 (6番)
OBJ_NAME_PUPIL = "瞳孔平面"
MAT_NAME_PUPIL = "Pupil_Plane_Mat"

# 窓面平面 (7番)
OBJ_NAME_WINDOW = "窓面平面"
MAT_NAME_WINDOW = "Window_Plane_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'
CAMERA_PRESET_DEFAULT = 'Cam'
SHOW_CAM_OBJ_DEFAULT = True
SHOW_TARGET_EMPTY_DEFAULT = False

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_ANIM_SPEED_MULT_DEFAULT = 1.0

# 瞳孔平面
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

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

# ==========================================
# 6. 瞳孔平面用 000方向矢印 設定
# ==========================================
ARROW_SHOW_DEFAULT = True
ARROW_LENGTH_DEFAULT = 5.0
ARROW_THICKNESS_DEFAULT = 0.08
ARROW_HEAD_LENGTH_DEFAULT = 0.5
ARROW_HEAD_WIDTH_DEFAULT = 0.25
ARROW_COLOR_DEFAULT = (1.0, 0.0, 0.0, 1.0)
ARROW_ALPHA_DEFAULT = 1.0
ARROW_OFFSET_DEFAULT = 0.0

OBJ_NAME_ARROW = "ARROW_000_DIRECTION"
MAT_NAME_ARROW = "Arrow_000_Mat"

# ==========================================
# 7. 窓面平面 & 000方向矢印 設定
# ==========================================
WINDOW_SHOW_DEFAULT = False
WINDOW_POS_MODE_DEFAULT = 'CAMERA'
WINDOW_CAM_DIST_DEFAULT = 5.0
WINDOW_LOC_DEFAULT = (0.0, 0.0, 0.0)
WINDOW_CIRCLE_Z_DEFAULT = 0.0
WINDOW_CIRCLE_RADIUS_DEFAULT = 10.0
WINDOW_CIRCLE_ANGLE_DEFAULT = 0.0
WINDOW_RADIUS_DEFAULT = 2.0
WINDOW_THICKNESS_DEFAULT = 0.05
WINDOW_SEGMENTS_DEFAULT = 4
COLOR_WINDOW = (0.0, 1.0, 0.0, 1.0)
WINDOW_ROT_X_DEFAULT = 0.0
WINDOW_ROT_Y_DEFAULT = 0.0
WINDOW_ROT_Z_DEFAULT = 0.7854

W_ARROW_SHOW_DEFAULT = True
W_ARROW_LENGTH_DEFAULT = 5.0
W_ARROW_THICKNESS_DEFAULT = 0.08
W_ARROW_HEAD_LENGTH_DEFAULT = 0.5
W_ARROW_HEAD_WIDTH_DEFAULT = 0.25
W_ARROW_COLOR_DEFAULT = (0.0, 1.0, 0.0, 1.0)
W_ARROW_ALPHA_DEFAULT = 1.0
W_ARROW_OFFSET_DEFAULT = 0.0

OBJ_NAME_WINDOW_ARROW = "ARROW_000_DIRECTION_WINDOW"
MAT_NAME_WINDOW_ARROW = "Arrow_000_Window_Mat"

# ===================================
# 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]:
        obj = bpy.data.objects.get(name)
        if obj:
            obj.hide_viewport = not self.show_target_empty

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)

    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_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

# ==========================================
# 6. 瞳孔平面 関連関数
# ==========================================
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

def update_arrow_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_ARROW)
    if not obj: return
    
    L = self.arrow_length
    R = self.arrow_thickness
    HL = self.arrow_head_length
    HW = self.arrow_head_width
    
    mesh = obj.data
    mesh.clear_geometry()
    
    if not self.arrow_show:
        return
        
    verts = []
    faces = []
    
    segments = 16
    z_start = -L / 2.0
    z_end = L / 2.0 - HL
    
    verts.append((0, 0, z_start))
    v_bottom_center = 0
    bottom_start = 1
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_start))
        
    verts.append((0, 0, z_end))
    v_top_center = segments + 1
    top_start = segments + 2
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_end))
        
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((bottom_start + i, bottom_start + next_i, top_start + next_i, top_start + i))
        faces.append((bottom_start + next_i, bottom_start + i, v_bottom_center))
        
    z_head_tip = L / 2.0
    verts.append((0, 0, z_end))
    v_head_center = len(verts) - 1
    
    head_ring_start = len(verts)
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((HW * math.cos(angle), HW * math.sin(angle), z_end))
        
    verts.append((0, 0, z_head_tip))
    v_tip = len(verts) - 1
    
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((head_ring_start + i, head_ring_start + next_i, v_tip))
        faces.append((head_ring_start + next_i, head_ring_start + i, v_head_center))
        
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_arrow_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_ARROW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = (self.arrow_color[0], self.arrow_color[1], self.arrow_color[2], self.arrow_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.arrow_color
                if 'Alpha' in bsdf.inputs:
                    bsdf.inputs['Alpha'].default_value = self.arrow_alpha
        mat.blend_method = 'BLEND'

def update_arrow_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        arrow = bpy.data.objects.get(OBJ_NAME_ARROW)
        pupil = bpy.data.objects.get(OBJ_NAME_PUPIL)
        if not arrow: return
        if not props.arrow_show or not pupil:
            arrow.hide_viewport = True
            arrow.hide_render = True
            return
            
        arrow.hide_viewport = False
        arrow.hide_render = False
        
        P = pupil.matrix_world.translation
        obj_000 = bpy.data.objects.get("000")
        if obj_000:
            T = obj_000.matrix_world.translation
        else:
            T = mathutils.Vector((0.0, 0.0, 0.0))
            
        V = T - P
        if V.length > 0.0001:
            N = V.normalized()
        else:
            N = (pupil.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))).normalized()
                    
        rot_quat = N.to_track_quat('Z', 'Y')
        offset_vec = N * props.arrow_offset
        mat_loc = mathutils.Matrix.Translation(P + offset_vec)
        mat_rot = rot_quat.to_matrix().to_4x4()
        arrow.matrix_world = mat_loc @ mat_rot
    except Exception:
        pass

def update_arrow_transform_cb(self, context):
    update_arrow_transform(context)

def update_arrow_all(self, context):
    update_arrow_mesh(self, context)
    update_arrow_mat(self, context)
    update_arrow_transform(context)

# ==========================================
# 7. 窓面平面 関連関数
# ==========================================
def update_window_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
        if not obj: return
        
        if not props.window_plane_show:
            obj.hide_viewport = True
            obj.hide_render = True
            return
            
        obj.hide_viewport = False
        obj.hide_render = False

        if props.window_plane_pos_mode == 'OFFSET':
            obj.location = props.window_plane_loc
            obj.rotation_euler = props.window_plane_rot
        elif props.window_plane_pos_mode == 'CIRCLE':
            obj_000 = bpy.data.objects.get("000")
            if obj_000:
                cx, cy = obj_000.location.x, obj_000.location.y
            else:
                cx, cy = 0.0, 0.0
                
            rad = math.radians(props.window_plane_circle_angle)
            x = cx + props.window_plane_circle_radius * math.cos(rad)
            y = cy + props.window_plane_circle_radius * math.sin(rad)
            z = props.window_plane_circle_z
            
            obj.location = (x, y, z)
            obj.rotation_euler = props.window_plane_rot
        elif props.window_plane_pos_mode == 'CAMERA':
            cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
            if cam:
                mat_cam = cam.matrix_world
                local_offset = mathutils.Vector((0.0, 0.0, -props.window_plane_cam_dist))
                world_pos = mat_cam @ local_offset
                
                mat_rot = mathutils.Euler(props.window_plane_rot, 'XYZ').to_matrix().to_4x4()
                mat_final = mat_cam.to_3x3().to_4x4() @ mat_rot
                
                obj.location = world_pos
                obj.rotation_euler = mat_final.to_euler('XYZ')
                
    except Exception:
        pass

def update_window_transform_cb(self, context):
    update_window_transform(context)
    if bpy.context: bpy.context.view_layer.update()
    update_window_arrow_transform(context)

def update_window_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
    if not obj: return
    
    if not props_check(context).window_plane_show: return
        
    mesh = obj.data
    mesh.clear_geometry()
    
    maj_s = self.window_plane_segments
    min_s = 16
    
    val = self.window_plane_radius
    if maj_s == 4: R = val / math.sqrt(2.0)
    else: R = val
        
    r = self.window_plane_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_window_segments(self, context):
    if self.window_plane_segments == 4:
        self.window_plane_rot = (0.0, 0.0, math.radians(45.0))
    else:
        self.window_plane_rot = (0.0, 0.0, math.radians(90.0))
    update_window_mesh(self, context)
    update_window_transform_cb(self, context)

def update_window_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = self.window_plane_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.window_plane_color

def props_check(context):
    return getattr(context.scene, f"{PREFIX}_props", None)

def update_window_all(self, context):
    update_window_mesh(self, context)
    update_window_mat(self, context)
    update_window_transform_cb(self, context)

# --- 7. 窓面平面用矢印 ---
def update_window_arrow_mesh(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
    if not obj: return
    if not self.window_arrow_show: return
    
    L = self.window_arrow_length
    R = self.window_arrow_thickness
    HL = self.window_arrow_head_length
    HW = self.window_arrow_head_width
    
    mesh = obj.data
    mesh.clear_geometry()
    
    verts, faces = [], []
    segments = 16
    z_start = -L / 2.0
    z_end = L / 2.0 - HL
    
    verts.append((0, 0, z_start))
    v_bottom_center = 0
    bottom_start = 1
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_start))
        
    verts.append((0, 0, z_end))
    v_top_center = segments + 1
    top_start = segments + 2
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((R * math.cos(angle), R * math.sin(angle), z_end))
        
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((bottom_start + i, bottom_start + next_i, top_start + next_i, top_start + i))
        faces.append((bottom_start + next_i, bottom_start + i, v_bottom_center))
        
    z_head_tip = L / 2.0
    verts.append((0, 0, z_end))
    v_head_center = len(verts) - 1
    
    head_ring_start = len(verts)
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        verts.append((HW * math.cos(angle), HW * math.sin(angle), z_end))
        
    verts.append((0, 0, z_head_tip))
    v_tip = len(verts) - 1
    
    for i in range(segments):
        next_i = (i + 1) % segments
        faces.append((head_ring_start + i, head_ring_start + next_i, v_tip))
        faces.append((head_ring_start + next_i, head_ring_start + i, v_head_center))
        
    mesh.from_pydata(verts, [], faces)
    mesh.update()

def update_window_arrow_mat(self, context):
    obj = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
    if obj and obj.data.materials:
        mat = obj.data.materials[0]
        mat.diffuse_color = (self.window_arrow_color[0], self.window_arrow_color[1], self.window_arrow_color[2], self.window_arrow_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.window_arrow_color
                if 'Alpha' in bsdf.inputs: bsdf.inputs['Alpha'].default_value = self.window_arrow_alpha
        mat.blend_method = 'BLEND'

def update_window_arrow_transform(context):
    try:
        props = getattr(context.scene, f"{PREFIX}_props", None)
        if not props: return
        arrow = bpy.data.objects.get(OBJ_NAME_WINDOW_ARROW)
        window = bpy.data.objects.get(OBJ_NAME_WINDOW)
        if not arrow: return
        if not props.window_arrow_show or not window:
            arrow.hide_viewport = True
            arrow.hide_render = True
            return
            
        arrow.hide_viewport = False
        arrow.hide_render = False
        
        P = window.matrix_world.translation
        obj_000 = bpy.data.objects.get("000")
        if obj_000:
            T = obj_000.matrix_world.translation
        else:
            T = mathutils.Vector((0.0, 0.0, 0.0))
            
        V = T - P
        if V.length > 0.0001:
            N = V.normalized()
        else:
            N = (window.matrix_world.to_3x3() @ mathutils.Vector((0, 0, 1))).normalized()
                    
        rot_quat = N.to_track_quat('Z', 'Y')
        offset_vec = N * props.window_arrow_offset
        mat_loc = mathutils.Matrix.Translation(P + offset_vec)
        mat_rot = rot_quat.to_matrix().to_4x4()
        arrow.matrix_world = mat_loc @ mat_rot
    except Exception:
        pass

def update_window_arrow_transform_cb(self, context):
    update_window_arrow_transform(context)

def update_window_arrow_all(self, context):
    update_window_arrow_mesh(self, context)
    update_window_arrow_mat(self, context)
    update_window_arrow_transform(context)

# --- ハンドラ ---
@bpy.app.handlers.persistent
def arrow_depsgraph_update(scene, depsgraph):
    # Depsgraph変更時に瞳孔用・窓面用、カメラ追従の更新を行う
    update_arrow_transform(bpy.context)
    update_window_transform(bpy.context)
    update_window_arrow_transform(bpy.context)

# --- 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_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)
    
    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
    )
    
    # 6. 瞳孔平面 関連プロパティ
    pupil_window_show: bpy.props.BoolProperty(name="瞳孔平面を表示", default=PUPIL_SHOW_DEFAULT, update=update_pupil_window)
    pupil_window_dist: bpy.props.FloatProperty(name="距離 (-100から100)", default=PUPIL_DIST_DEFAULT, min=-100.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)

    arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=ARROW_SHOW_DEFAULT, update=update_arrow_all)
    arrow_length: bpy.props.FloatProperty(name="矢印全長", default=ARROW_LENGTH_DEFAULT, min=0.1, update=update_arrow_mesh)
    arrow_thickness: bpy.props.FloatProperty(name="矢印太さ", default=ARROW_THICKNESS_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_head_length: bpy.props.FloatProperty(name="矢印先端長さ", default=ARROW_HEAD_LENGTH_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_head_width: bpy.props.FloatProperty(name="矢印先端幅", default=ARROW_HEAD_WIDTH_DEFAULT, min=0.01, update=update_arrow_mesh)
    arrow_color: bpy.props.FloatVectorProperty(name="矢印色", subtype='COLOR', size=4, default=ARROW_COLOR_DEFAULT, min=0.0, max=1.0, update=update_arrow_mat)
    arrow_alpha: bpy.props.FloatProperty(name="透明度", default=ARROW_ALPHA_DEFAULT, min=0.0, max=1.0, update=update_arrow_mat)
    arrow_offset: bpy.props.FloatProperty(name="矢印中央の移動", default=ARROW_OFFSET_DEFAULT, update=update_arrow_transform_cb)

    # 7. 窓面平面 関連プロパティ
    window_plane_show: bpy.props.BoolProperty(name="窓面平面を表示", default=WINDOW_SHOW_DEFAULT, update=update_window_all)
    window_plane_pos_mode: bpy.props.EnumProperty(
        name="配置モード",
        items=[
            ('CAMERA', "カメラ前方 追随", ""),
            ('OFFSET', "XYZ 平行移動 (固定)", ""), 
            ('CIRCLE', "000中心 円周回転 (固定)", "")
        ],
        default=WINDOW_POS_MODE_DEFAULT,
        update=update_window_transform_cb
    )
    window_plane_cam_dist: bpy.props.FloatProperty(name="カメラからの距離", default=WINDOW_CAM_DIST_DEFAULT, min=-100.0, max=100.0, update=update_window_transform_cb)
    window_plane_loc: bpy.props.FloatVectorProperty(name="XYZ平行移動", default=WINDOW_LOC_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_z: bpy.props.FloatProperty(name="Z軸高さ", default=WINDOW_CIRCLE_Z_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_radius: bpy.props.FloatProperty(name="半径", default=WINDOW_CIRCLE_RADIUS_DEFAULT, update=update_window_transform_cb)
    window_plane_circle_angle: bpy.props.FloatProperty(name="角度 (円周上の位置)", default=WINDOW_CIRCLE_ANGLE_DEFAULT, update=update_window_transform_cb)
    
    window_plane_radius: bpy.props.FloatProperty(name="サイズ", default=WINDOW_RADIUS_DEFAULT, min=0.1, update=update_window_mesh)
    window_plane_thickness: bpy.props.FloatProperty(name="枠の太さ", default=WINDOW_THICKNESS_DEFAULT, min=0.01, update=update_window_mesh)
    window_plane_segments: bpy.props.IntProperty(name="角数 (3-32)", default=WINDOW_SEGMENTS_DEFAULT, min=3, max=32, update=update_window_segments)
    window_plane_color: bpy.props.FloatVectorProperty(name="枠の色", subtype='COLOR', size=4, default=COLOR_WINDOW, min=0.0, max=1.0, update=update_window_mat)
    window_plane_rot: bpy.props.FloatVectorProperty(name="ローカル回転", subtype='EULER', default=(WINDOW_ROT_X_DEFAULT, WINDOW_ROT_Y_DEFAULT, WINDOW_ROT_Z_DEFAULT), update=update_window_transform_cb)

    window_arrow_show: bpy.props.BoolProperty(name="矢印を表示", default=W_ARROW_SHOW_DEFAULT, update=update_window_arrow_all)
    window_arrow_length: bpy.props.FloatProperty(name="矢印全長", default=W_ARROW_LENGTH_DEFAULT, min=0.1, update=update_window_arrow_mesh)
    window_arrow_thickness: bpy.props.FloatProperty(name="矢印太さ", default=W_ARROW_THICKNESS_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_head_length: bpy.props.FloatProperty(name="矢印先端長さ", default=W_ARROW_HEAD_LENGTH_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_head_width: bpy.props.FloatProperty(name="矢印先端幅", default=W_ARROW_HEAD_WIDTH_DEFAULT, min=0.01, update=update_window_arrow_mesh)
    window_arrow_color: bpy.props.FloatVectorProperty(name="矢印色", subtype='COLOR', size=4, default=W_ARROW_COLOR_DEFAULT, min=0.0, max=1.0, update=update_window_arrow_mat)
    window_arrow_alpha: bpy.props.FloatProperty(name="透明度", default=W_ARROW_ALPHA_DEFAULT, min=0.0, max=1.0, update=update_window_arrow_mat)
    window_arrow_offset: bpy.props.FloatProperty(name="矢印中央の移動", default=W_ARROW_OFFSET_DEFAULT, update=update_window_arrow_transform_cb)

    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_TARGET, OBJ_NAME_PUPIL, OBJ_NAME_ARROW, OBJ_NAME_WINDOW, OBJ_NAME_WINDOW_ARROW]:
            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)

        tgt_obj = bpy.data.objects.new(OBJ_NAME_TARGET, None)
        tgt_obj.empty_display_type = 'PLAIN_AXES'
        sub_col.objects.link(tgt_obj)
        
        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 = False
        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
        
        # 6. 瞳孔平面の作成 (カメラの子)
        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)

        # 瞳孔平面用矢印の作成
        m_arrow = bpy.data.meshes.new(f"{OBJ_NAME_ARROW}_Mesh")
        o_arrow = bpy.data.objects.new(OBJ_NAME_ARROW, m_arrow)
        sub_col.objects.link(o_arrow)
        mat_arrow = bpy.data.materials.new(MAT_NAME_ARROW)
        mat_arrow.use_nodes = True
        mat_arrow.blend_method = 'BLEND'
        o_arrow.data.materials.append(mat_arrow)
        
        # 7. 窓面平面の作成 (ワールド直下)
        m_window = bpy.data.meshes.new(f"{OBJ_NAME_WINDOW}_Mesh")
        o_window = bpy.data.objects.new(OBJ_NAME_WINDOW, m_window)
        o_window.rotation_mode = 'XYZ'
        sub_col.objects.link(o_window)
        mat_window = bpy.data.materials.new(MAT_NAME_WINDOW)
        mat_window.use_nodes = True
        mat_window.blend_method = 'BLEND'
        o_window.data.materials.append(mat_window)

        # 窓面平面用矢印の作成
        m_w_arrow = bpy.data.meshes.new(f"{OBJ_NAME_WINDOW_ARROW}_Mesh")
        o_w_arrow = bpy.data.objects.new(OBJ_NAME_WINDOW_ARROW, m_w_arrow)
        sub_col.objects.link(o_w_arrow)
        mat_w_arrow = bpy.data.materials.new(MAT_NAME_WINDOW_ARROW)
        mat_w_arrow.use_nodes = True
        mat_w_arrow.blend_method = 'BLEND'
        o_w_arrow.data.materials.append(mat_w_arrow)
        
        
        # 初期化更新の実行
        update_pupil_window(props, context)
        update_pupil_window_rot(props, context)
        update_pupil_window_mat(props, context)

        update_window_all(props, context)

        update_cam_root_constraint(props, context)
        update_visibility(props, context)
        update_cam_anim(props, context)
        
        context.view_layer.update()
        
        update_arrow_all(props, context)
        update_window_arrow_all(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_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 = "メインカメラ"
            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_reset_cam_offset_rot(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_reset_cam_offset_rot"
    bl_label = "角度ズレをリセット (0, 0, 0)"
    bl_description = "カメラの角度ズレ(オフセット)をすべて0にします"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.cam_local_offset_rot = (0.0, 0.0, 0.0)
        return {'FINISHED'}

class OBJECT_OT_reset_cam_root_rot(bpy.types.Operator):
    bl_idname = f"object.{PREFIX}_reset_cam_root_rot"
    bl_label = "初期回転をリセット"
    bl_description = "Rootの初期回転を元のデフォルト状態に戻します"
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX}_props")
        props.cam_rot_start = (CAM_ROT_U_START, CAM_ROT_V_START, CAM_ROT_W_START)
        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("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("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_ANIM_SPEED_MULT_DEFAULT = "): lines[i] = f"TARGET_ANIM_SPEED_MULT_DEFAULT = {props.target_anim_speed_mult:.4f}"

            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}"
            
            # 6. Pupil
            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}"
            elif line.startswith("ARROW_SHOW_DEFAULT = "): lines[i] = f"ARROW_SHOW_DEFAULT = {props.arrow_show}"
            elif line.startswith("ARROW_LENGTH_DEFAULT = "): lines[i] = f"ARROW_LENGTH_DEFAULT = {props.arrow_length:.4f}"
            elif line.startswith("ARROW_THICKNESS_DEFAULT = "): lines[i] = f"ARROW_THICKNESS_DEFAULT = {props.arrow_thickness:.4f}"
            elif line.startswith("ARROW_HEAD_LENGTH_DEFAULT = "): lines[i] = f"ARROW_HEAD_LENGTH_DEFAULT = {props.arrow_head_length:.4f}"
            elif line.startswith("ARROW_HEAD_WIDTH_DEFAULT = "): lines[i] = f"ARROW_HEAD_WIDTH_DEFAULT = {props.arrow_head_width:.4f}"
            elif line.startswith("ARROW_COLOR_DEFAULT = "): lines[i] = f"ARROW_COLOR_DEFAULT = ({props.arrow_color[0]:.4f}, {props.arrow_color[1]:.4f}, {props.arrow_color[2]:.4f}, {props.arrow_color[3]:.4f})"
            elif line.startswith("ARROW_ALPHA_DEFAULT = "): lines[i] = f"ARROW_ALPHA_DEFAULT = {props.arrow_alpha:.4f}"
            elif line.startswith("ARROW_OFFSET_DEFAULT = "): lines[i] = f"ARROW_OFFSET_DEFAULT = {props.arrow_offset:.4f}"

            # 7. Window Plane
            elif line.startswith("WINDOW_SHOW_DEFAULT = "): lines[i] = f"WINDOW_SHOW_DEFAULT = {props.window_plane_show}"
            elif line.startswith("WINDOW_POS_MODE_DEFAULT = "): lines[i] = f"WINDOW_POS_MODE_DEFAULT = '{props.window_plane_pos_mode}'"
            elif line.startswith("WINDOW_CAM_DIST_DEFAULT = "): lines[i] = f"WINDOW_CAM_DIST_DEFAULT = {props.window_plane_cam_dist:.4f}"
            elif line.startswith("WINDOW_LOC_DEFAULT = "): lines[i] = f"WINDOW_LOC_DEFAULT = ({props.window_plane_loc[0]:.4f}, {props.window_plane_loc[1]:.4f}, {props.window_plane_loc[2]:.4f})"
            elif line.startswith("WINDOW_CIRCLE_Z_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_Z_DEFAULT = {props.window_plane_circle_z:.4f}"
            elif line.startswith("WINDOW_CIRCLE_RADIUS_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_RADIUS_DEFAULT = {props.window_plane_circle_radius:.4f}"
            elif line.startswith("WINDOW_CIRCLE_ANGLE_DEFAULT = "): lines[i] = f"WINDOW_CIRCLE_ANGLE_DEFAULT = {props.window_plane_circle_angle:.4f}"
            elif line.startswith("WINDOW_RADIUS_DEFAULT = "): lines[i] = f"WINDOW_RADIUS_DEFAULT = {props.window_plane_radius:.4f}"
            elif line.startswith("WINDOW_THICKNESS_DEFAULT = "): lines[i] = f"WINDOW_THICKNESS_DEFAULT = {props.window_plane_thickness:.4f}"
            elif line.startswith("WINDOW_SEGMENTS_DEFAULT = "): lines[i] = f"WINDOW_SEGMENTS_DEFAULT = {props.window_plane_segments}"
            elif line.startswith("COLOR_WINDOW = "): lines[i] = f"COLOR_WINDOW = ({props.window_plane_color[0]:.4f}, {props.window_plane_color[1]:.4f}, {props.window_plane_color[2]:.4f}, 1.0)"
            elif line.startswith("WINDOW_ROT_X_DEFAULT = "): lines[i] = f"WINDOW_ROT_X_DEFAULT = {props.window_plane_rot[0]:.4f}"
            elif line.startswith("WINDOW_ROT_Y_DEFAULT = "): lines[i] = f"WINDOW_ROT_Y_DEFAULT = {props.window_plane_rot[1]:.4f}"
            elif line.startswith("WINDOW_ROT_Z_DEFAULT = "): lines[i] = f"WINDOW_ROT_Z_DEFAULT = {props.window_plane_rot[2]:.4f}"
            
            elif line.startswith("W_ARROW_SHOW_DEFAULT = "): lines[i] = f"W_ARROW_SHOW_DEFAULT = {props.window_arrow_show}"
            elif line.startswith("W_ARROW_LENGTH_DEFAULT = "): lines[i] = f"W_ARROW_LENGTH_DEFAULT = {props.window_arrow_length:.4f}"
            elif line.startswith("W_ARROW_THICKNESS_DEFAULT = "): lines[i] = f"W_ARROW_THICKNESS_DEFAULT = {props.window_arrow_thickness:.4f}"
            elif line.startswith("W_ARROW_HEAD_LENGTH_DEFAULT = "): lines[i] = f"W_ARROW_HEAD_LENGTH_DEFAULT = {props.window_arrow_head_length:.4f}"
            elif line.startswith("W_ARROW_HEAD_WIDTH_DEFAULT = "): lines[i] = f"W_ARROW_HEAD_WIDTH_DEFAULT = {props.window_arrow_head_width:.4f}"
            elif line.startswith("W_ARROW_COLOR_DEFAULT = "): lines[i] = f"W_ARROW_COLOR_DEFAULT = ({props.window_arrow_color[0]:.4f}, {props.window_arrow_color[1]:.4f}, {props.window_arrow_color[2]:.4f}, {props.window_arrow_color[3]:.4f})"
            elif line.startswith("W_ARROW_ALPHA_DEFAULT = "): lines[i] = f"W_ARROW_ALPHA_DEFAULT = {props.window_arrow_alpha:.4f}"
            elif line.startswith("W_ARROW_OFFSET_DEFAULT = "): lines[i] = f"W_ARROW_OFFSET_DEFAULT = {props.window_arrow_offset:.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")
        
        root = bpy.data.objects.get(OBJ_NAME_ROOT)
        cam = bpy.data.objects.get(OBJ_NAME_CAMERA)
        
        # --- 1. 注視点 (Track To) 設定 ---
        box_tgt = layout.box()
        box_tgt.label(text="1. 注視点 (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 (Yaw)")
            col_tgt.prop(props, "target_circle_rot", index=2, text="Z (Roll)")
            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 ['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} (Yaw)")
        col_off.prop(props, "cam_local_offset_rot", index=2, text=f"角度ズレ {LABEL_W} (Roll)")
        col_off.operator(f"object.{PREFIX}_reset_cam_offset_rot", icon='FILE_REFRESH')

        # --- 2. レンズ・カメラ設定 ---
        box_lens = layout.box()
        box_lens.label(text="2. レンズ・カメラ設定", icon='VIEW_CAMERA')
        if cam and cam.type == '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値 (小さいとボケる)")
        else:
            box_lens.label(text="※リグ作成後に設定できます", icon='INFO')

        # --- 3. カメラリグの配置 (移動) ---
        box = layout.box()
        box.label(text="3. カメラリグの配置 (移動)", 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 (Yaw)")
            col_loc.prop(props, "cam_circle_rot", index=2, text="Z (Roll)")
            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')
        

        if root and cam:
            # --- 4. アニメーション設定 ---
            box_rot = layout.box()
            box_rot.label(text="4. アニメーション設定", 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} (Yaw)")
                col_rot.prop(props, "cam_rot_start", index=2, text=f"初期 {LABEL_W} (Roll)")
                col_rot.operator(f"object.{PREFIX}_reset_cam_root_rot", icon='FILE_REFRESH')
                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()

            # --- 画面情報表示 ---
            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')

            # --- 5. カメラ色 (共通テーマ) & 表示 ---
            box_vis = layout.box()
            box_vis.label(text="5. カメラ色 (共通テーマ) & 表示", 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") 
            
            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="")

            # --- 6. 瞳孔平面 & 000方向矢印 ---
            box_pupil_arrow = layout.box()
            box_pupil_arrow.label(text="6. 瞳孔平面 & 000方向矢印", icon='MESH_TORUS')
            
            row_pa = box_pupil_arrow.row(align=True)
            row_pa.prop(props, "pupil_window_show", text="瞳孔平面を表示")
            row_pa.prop(props, "arrow_show", text="矢印を表示")

            if props.pupil_window_show:
                col_p = box_pupil_arrow.column(align=True)
                col_p.label(text="[瞳孔平面] ※カメラ前方に配置", icon='MESH_PLANE')
                col_p.prop(props, "pupil_window_dist", text="距離 (-100から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 (Yaw)")
                col_p.prop(props, "pupil_window_rot", index=2, text="Z (Roll)")
                box_pupil_arrow.separator()

            if props.arrow_show:
                col_a = box_pupil_arrow.column(align=True)
                col_a.label(text="[000方向矢印]", icon='EMPTY_AXIS')
                obj_000 = bpy.data.objects.get("000")
                if not obj_000: col_a.label(text="「000」が見つからないため、原点(0,0,0)を向いています", icon='INFO')
                col_a.prop(props, "arrow_offset", text="矢印中央の移動")
                col_a.prop(props, "arrow_length")
                col_a.prop(props, "arrow_thickness")
                col_a.prop(props, "arrow_head_length")
                col_a.prop(props, "arrow_head_width")
                col_a.separator()
                col_a.prop(props, "arrow_color")
                col_a.prop(props, "arrow_alpha", slider=True)

            # --- 7. 窓面平面 & 000方向矢印 ---
            box_window = layout.box()
            box_window.label(text="7. 窓面平面 & 000方向矢印", icon='MESH_PLANE')
            
            row_wa = box_window.row(align=True)
            row_wa.prop(props, "window_plane_show", text="窓面平面を表示")
            row_wa.prop(props, "window_arrow_show", text="矢印を表示")

            if props.window_plane_show:
                col_w = box_window.column(align=True)
                col_w.label(text="[窓面平面の位置]", icon='ORIENTATION_GLOBAL')
                col_w.prop(props, "window_plane_pos_mode", expand=True)
                
                if props.window_plane_pos_mode == 'OFFSET':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_loc", index=0, text="X")
                    col_pos.prop(props, "window_plane_loc", index=1, text="Y")
                    col_pos.prop(props, "window_plane_loc", index=2, text="Z")
                elif props.window_plane_pos_mode == 'CIRCLE':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_circle_z")
                    col_pos.prop(props, "window_plane_circle_radius")
                    col_pos.prop(props, "window_plane_circle_angle")
                elif props.window_plane_pos_mode == 'CAMERA':
                    col_pos = col_w.column(align=True)
                    col_pos.prop(props, "window_plane_cam_dist", text="距離 (-100から100)", slider=True)
                
                col_w.separator()
                col_w.label(text="[形状設定]", icon='MESH_DATA')
                col_w.prop(props, "window_plane_segments", text="角数 (3-32)")
                if props.window_plane_segments == 4: col_w.prop(props, "window_plane_radius", text="1辺の長さ")
                else: col_w.prop(props, "window_plane_radius", text="半径")
                col_w.prop(props, "window_plane_thickness")
                col_w.prop(props, "window_plane_color")
                col_w.separator()
                col_w.label(text="ローカル回転 (XYZ):")
                col_w.prop(props, "window_plane_rot", index=0, text="X (Pitch)")
                col_w.prop(props, "window_plane_rot", index=1, text="Y (Yaw)")
                col_w.prop(props, "window_plane_rot", index=2, text="Z (Roll)")

                box_window.separator()

            if props.window_arrow_show:
                col_wa = box_window.column(align=True)
                col_wa.label(text="[000方向矢印]", icon='EMPTY_AXIS')
                obj_000 = bpy.data.objects.get("000")
                if not obj_000: col_wa.label(text="「000」が見つからないため、原点(0,0,0)を向いています", icon='INFO')
                col_wa.prop(props, "window_arrow_offset", text="矢印中央の移動")
                col_wa.prop(props, "window_arrow_length")
                col_wa.prop(props, "window_arrow_thickness")
                col_wa.prop(props, "window_arrow_head_length")
                col_wa.prop(props, "window_arrow_head_width")
                col_wa.separator()
                col_wa.prop(props, "window_arrow_color")
                col_wa.prop(props, "window_arrow_alpha", slider=True)

                
            # --- 8. 設定値の保存 ---
            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_set_active_camera,
    OBJECT_OT_update_camera_info,
    OBJECT_OT_copy_camera_info,
    OBJECT_OT_reset_cam_offset_rot,
    OBJECT_OT_reset_cam_root_rot,
    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))
    
    if arrow_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.append(arrow_depsgraph_update)
    
    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:
            try:
                bpy.context.view_layer.update()
                update_arrow_transform(bpy.context)
                update_window_arrow_transform(bpy.context)
            except:
                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():
    if arrow_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
        bpy.app.handlers.depsgraph_update_post.remove(arrow_depsgraph_update)

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