blender Million 2026

カメラ3台 原型 20260328










import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty

# ======================================================================
# --- アドオン情報 / Addon Info ---
# ======================================================================

PREFIX = "unit_circle_cam"

bl_info = {
    "name": "zionad 521 [Unit Circle Cam]",
    "author": "zionadchat",
    "version": (37, 0, 7),
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "3つの専用カメラ初期値変更と一括リセット、ビューポートカメラの一括リセット機能(完全安定版)",
    "category": "Cam three",  # UI崩れ防止のため余分なスペースを削除
}

# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================

ADDON_CATEGORY_NAME = bl_info["category"]

# ※ハードコードパスですが、内部で os.path.exists により存在チェックを行うため
# 他環境で実行してもクラッシュはしません(ファイルが見つからないだけになります)
HDRI_PATHS = [
    r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
    r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
    r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
    r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]
WIRE_PRESETS = [("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)), ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)), ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)), ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),]
GRID_PRESETS = [("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)), ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)), ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),]

# ★ 親コレクションとサブコレクション名
MASTER_COLLECTION_NAME = "Cam three"
CAMERA_COLLECTION_NAME = "Cam"

SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]

# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================

NEW_DOC_LINKS = [
    {"label": "THIS_ADDON [ カメラ3台 原型 20260328 ]", "url": "<https://www.notion.so/3-20260328-330f5dacaf4380a4b9b5eef6e98a276f>"},
]

SOCIAL_LINKS = [
    {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]

# ======================================================================
# --- パネル管理 ---
# ======================================================================

PANEL_IDS = {
    "SETUP": f"{PREFIX}_PT_setup", 
    "AIMING": f"{PREFIX}_PT_aiming", "VIEWPORT_CAM": f"{PREFIX}_PT_viewport_cam",
    "LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
    "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {
    PANEL_IDS["SETUP"]: 0, PANEL_IDS["AIMING"]: 1, PANEL_IDS["VIEWPORT_CAM"]: 2, PANEL_IDS["LENS"]: 3, 
    PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, 
    PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 190, PANEL_IDS["REMOVE"]: 200,
}

# ======================================================================
# --- ロック機構 & タイマー管理 (プロフェッショナル設計) ---
# ======================================================================

def set_update_lock(scene, state: bool):
    """ 更新フラグをSceneに持たせ、競合を防ぐ """
    if scene:
        scene["_sfc_updating"] = state

def is_updating(scene):
    if scene:
        return scene.get("_sfc_updating", False)
    return False

def schedule_update_lock_reset():
    # タイマーから呼ばれるためcontextから安全に取得
    if bpy.context and hasattr(bpy.context, 'scene'):
        bpy.context.scene["_sfc_updating"] = False
    return None

def trigger_delayed_unlock():
    """ 提言: 多重登録リスクを完全に回避する堅牢なタイマー登録 """
    if bpy.app.timers.is_registered(schedule_update_lock_reset):
        bpy.app.timers.unregister(schedule_update_lock_reset)
    bpy.app.timers.register(schedule_update_lock_reset, first_interval=0.01)

# ======================================================================
# --- 汎用ヘルパー関数 ---
# ======================================================================

def get_or_create_collection(context, name, parent_col=None):
    col = bpy.data.collections.get(name)
    if not col:
        col = bpy.data.collections.new(name)
        if parent_col:
            if col.name not in parent_col.children:
                parent_col.children.link(col)
        else:
            if col.name not in context.scene.collection.children:
                context.scene.collection.children.link(col)
    return col

def get_master_collection(context):
    return get_or_create_collection(context, MASTER_COLLECTION_NAME)

def find_node(nodes, node_type, name):
    if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
    return nodes.get(name)

def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
    node = find_node(nodes, node_type, name)
    if node: return node
    new_node = nodes.new(type=node_type)
    new_node.name = name
    new_node.label = name.replace("_", " ")
    output_node = find_node(nodes, 'OUTPUT_WORLD', '')
    if output_node: 
        new_node.location = output_node.location + mathutils.Vector(location_offset)
    return new_node

def get_world_nodes(context, create=True):
    world = context.scene.world
    if not world and create: 
        world = bpy.data.worlds.new("World")
        context.scene.world = world
    if not world: return None, None, None
    if create: world.use_nodes = True
    if not world.use_nodes: return world, None, None
    return world, world.node_tree.nodes, world.node_tree.links

def load_hdri_from_path(filepath, context):
    _, nodes, _ = get_world_nodes(context)
    if not nodes: return False
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
    if os.path.exists(filepath):
        try: 
            env_node.image = bpy.data.images.load(filepath, check_existing=True)
            return True
        except Exception as e: 
            print(f"[HDRI Load Error] {filepath} -> {e}")
            return False
    return False

def update_viewport(context):
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D': 
                        space.shading.type = 'MATERIAL'
                return

def update_background_mode(self, context):
    mode = context.scene.zionad_swt_props.background_mode
    world, nodes, links = get_world_nodes(context)
    if not nodes: return
    output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
    background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
    sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
    mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
    tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
    
    if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
    if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
    
    links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
    
    if mode == 'SKY': 
        links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        links.new(env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(HDRI_PATHS): 
            load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
            
    update_viewport(context)

# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================

def update_cam_color(self, context):
    if self.camera_obj:
        context.preferences.themes[0].view_3d.camera = self.camera_color

def update_grid_color_cb(self, context):
    context.preferences.themes[0].view_3d.grid = self.grid_color

def update_wire_color_cb(self, context):
    context.preferences.themes[0].view_3d.wire = self.wire_color
    context.preferences.themes[0].view_3d.object_active = self.wire_color

class ThemeGridProperties(PropertyGroup):
    grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0), update=update_grid_color_cb)
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))

class ThemeWireProperties(PropertyGroup):
    wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75), update=update_wire_color_cb)
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))

class TargetProperty(PropertyGroup): name: StringProperty()

# ----------------------------------------------------------------------
# Property update の過密呼び出しを防ぐ Debounce(遅延)処理
# ----------------------------------------------------------------------

def _do_update_viewport_cam():
    context = bpy.context
    if not context or not hasattr(context, 'scene'): return None
    scene = context.scene
    props = scene.surface_camera_properties
    
    vp_loc = mathutils.Vector(props.viewport_location)
    vp_tgt = mathutils.Vector(props.viewport_target)
    direction = vp_tgt - vp_loc
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        rv3d = space.region_3d
                        if rv3d:
                            set_update_lock(scene, True)
                            try:
                                if rv3d.view_perspective == 'CAMERA':
                                    rv3d.view_perspective = 'PERSP'
                                rv3d.view_location = vp_tgt
                                rv3d.view_rotation = rot_quat
                                rv3d.view_distance = direction.length
                            finally:
                                trigger_delayed_unlock()
                            break
    return None

def safe_update_viewport_cam(self, context):
    if is_updating(context.scene): return
    if bpy.app.timers.is_registered(_do_update_viewport_cam):
        bpy.app.timers.unregister(_do_update_viewport_cam)
    bpy.app.timers.register(_do_update_viewport_cam, first_interval=0.01)

def _do_update_surface_camera():
    context = bpy.context
    if not context or not hasattr(context, 'scene'): return None
    scene = context.scene
    props = scene.surface_camera_properties
    camera_obj = props.camera_obj
    
    set_update_lock(scene, True)
    try:
        if props.is_updating_settings or not camera_obj: 
            update_info_panel_text(props, scene)
            return None
        cam_data = camera_obj.data
        if cam_data: 
            cam_data.sensor_fit = 'HORIZONTAL'
            cam_data.lens_unit = 'MILLIMETERS'
            cam_data.lens = props.lens_focal_length
            cam_data.clip_start = props.clip_start
            cam_data.clip_end = props.clip_end
        update_object_transform(camera_obj, props)
        update_info_panel_text(props, scene)
    finally: 
        trigger_delayed_unlock()
    return None

def safe_update_surface_camera(self, context):
    if is_updating(context.scene): return
    if bpy.app.timers.is_registered(_do_update_surface_camera):
        bpy.app.timers.unregister(_do_update_surface_camera)
    bpy.app.timers.register(_do_update_surface_camera, first_interval=0.01)

# ----------------------------------------------------------------------

class SurfaceCameraProperties(PropertyGroup):
    camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=safe_update_surface_camera)
    
    show_init_settings: BoolProperty(name="初期値設定を表示", default=False)
    
    cam1_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam1_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 100.0, 0.0), subtype='XYZ')
    cam2_init_loc: FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), subtype='XYZ')
    cam2_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam3_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 100.0), subtype='XYZ')
    cam3_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    
    target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=safe_update_surface_camera)
    offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=safe_update_surface_camera)
    offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=safe_update_surface_camera)
    offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=safe_update_surface_camera)
    
    viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=safe_update_viewport_cam)
    viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=safe_update_viewport_cam)
    
    is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
    lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=safe_update_surface_camera)
    clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=safe_update_surface_camera)
    clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=safe_update_surface_camera)
    
    info_horizontal_fov: StringProperty(name="水平視野角")
    
    camera_color: FloatVectorProperty(
        name="カメラ枠線 色", 
        subtype='COLOR', size=3, min=0.0, max=1.0, 
        default=(0.0, 1.0, 1.0), 
        update=lambda self, context: update_cam_color(self, context)
    )

class ZIONAD_SWT_Properties(PropertyGroup):
    background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
    hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)

def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
    try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
    except: return 0.0

def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except: return 50.0

def get_target_location(props):
    return mathutils.Vector(props.target_location)

def update_object_transform(obj, props):
    location = obj.location
    target_location = get_target_location(props)
    direction = target_location - location
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    base_track_quat = direction.to_track_quat('-Z', 'Y')
    offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
    final_quat = base_track_quat @ offset_euler.to_quaternion()
    obj.rotation_euler = final_quat.to_euler('XYZ')

def update_info_panel_text(props, scene):
    if not props: return
    camera_obj = props.camera_obj
    if not camera_obj: return
    current_fov = calculate_horizontal_fov(props.lens_focal_length)
    props.info_horizontal_fov = f"{current_fov:.1f} °"

def sync_ui_from_manual_transform(props, obj, scene):
    if is_updating(scene): return
    set_update_lock(scene, True)
    try:
        target_location = get_target_location(props)
        direction = target_location - obj.location
        if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
        base_track_quat = direction.to_track_quat('-Z', 'Y')
        final_quat = obj.matrix_world.to_quaternion()
        offset_quat = base_track_quat.inverted() @ final_quat
        offset_euler = offset_quat.to_euler('XYZ')
        props.offset_pitch = offset_euler.x
        props.offset_yaw = offset_euler.y
        props.offset_roll = offset_euler.z
    finally: 
        trigger_delayed_unlock()
    update_info_panel_text(props, scene)

@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
    if is_updating(scene): return
    
    sfc_props = scene.surface_camera_properties
    cam_obj = sfc_props.camera_obj
    if not cam_obj: return 
    
    for update in depsgraph.updates:
        if not update.is_updated_transform: continue
        # 提言: 監視対象を特定のものだけに完全に限定し、無駄な同期をなくす
        if update.id.original == cam_obj: 
            sync_ui_from_manual_transform(sfc_props, cam_obj, scene)
            return

# ======================================================================
# --- オペレーター ---
# ======================================================================

def set_initial_camera_transform(obj, loc, tgt):
    loc_vec = mathutils.Vector(loc)
    tgt_vec = mathutils.Vector(tgt)
    direction = tgt_vec - loc_vec
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    obj.location = loc_vec
    obj.rotation_euler = rot_quat.to_euler('XYZ')

class SFC_OT_CreateThreeCameras(Operator):
    bl_idname = f"{PREFIX}.create_three_cameras"
    bl_label = "3つのカメラを生成・初期化"
    
    def execute(self, context):
        master_col = get_master_collection(context)
        col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, master_col)
            
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            name = f"Fixed_Cam_{idx}"
            # 提言1: O(1)アクセスによるカメラの高速・安全取得
            cam_obj = bpy.data.objects.get(name)
            if cam_obj and cam_obj.type != 'CAMERA':
                cam_obj = None
            
            if not cam_obj:
                cam_data = bpy.data.cameras.new(name=name)
                cam_obj = bpy.data.objects.new(name, cam_data)
                col.objects.link(cam_obj)
                if cam_obj.name in context.scene.collection.objects.keys():
                    context.scene.collection.objects.unlink(cam_obj)
                    
            set_initial_camera_transform(cam_obj, loc, tgt)
            
        op_func = getattr(getattr(bpy.ops, PREFIX), "switch_camera")
        op_func(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました")
        return {'FINISHED'}

class SFC_OT_ResetThreeCameras(Operator):
    bl_idname = f"{PREFIX}.reset_three_cameras"
    bl_label = "カメラを初期値に一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            cam_obj = bpy.data.objects.get(f"Fixed_Cam_{idx}")
            if cam_obj and cam_obj.type == 'CAMERA':
                set_initial_camera_transform(cam_obj, loc, tgt)
                if props.camera_obj == cam_obj:
                    props.is_updating_settings = True
                    props.target_location = tgt
                    props.offset_yaw = 0.0
                    props.offset_pitch = 0.0
                    props.offset_roll = 0.0
                    props.is_updating_settings = False
                    
        self.report({'INFO'}, "カメラを初期値にリセットしました")
        return {'FINISHED'}

class SFC_OT_ResetViewportCam(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_cam"
    bl_label = "架空カメラを一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        props.viewport_location = (0.0, -10.0, 5.0)
        props.viewport_target = (0.0, 0.0, 0.0)
        self.report({'INFO'}, "架空カメラをリセットしました")
        return {'FINISHED'}

class SFC_OT_CopyViewportInfo(Operator):
    bl_idname = f"{PREFIX}.copy_viewport_info"
    bl_label = "視座・注視点情報をコピー"
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc = props.viewport_location
        tgt = props.viewport_target
        
        fmt = ".2f"
        loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
        tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
        
        text_to_copy = f"視座位置: {loc_str}\n注視点: {tgt_str}"
        context.window_manager.clipboard = text_to_copy
        self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました")
        return {'FINISHED'}

class SFC_OT_SwitchCamera(Operator):
    bl_idname = f"{PREFIX}.switch_camera"
    bl_label = "カメラを切り替え"
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        name = f"Fixed_Cam_{self.cam_index}"
        
        # 提言1: O(1)アクセスによるカメラの高速・安全取得
        cam_obj = bpy.data.objects.get(name)
        if cam_obj and cam_obj.type != 'CAMERA':
            cam_obj = None
            
        if not cam_obj:
            self.report({'WARNING'}, f"{name} が見つかりません。先に「生成」ボタンを押してください。")
            return {'CANCELLED'}
            
        props.is_updating_settings = True
        props.camera_obj = cam_obj
        context.scene.camera = cam_obj
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.region_3d.view_perspective = 'CAMERA'
        
        context.preferences.themes[0].view_3d.camera = props.camera_color
        
        cam_data = cam_obj.data
        props.lens_focal_length = cam_data.lens
        props.clip_start = cam_data.clip_start
        props.clip_end = cam_data.clip_end
        
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        props.target_location = cam_obj.location + forward_vec
        props.offset_yaw = 0.0
        props.offset_pitch = 0.0
        props.offset_roll = 0.0
        
        props.is_updating_settings = False
        sync_ui_from_manual_transform(props, cam_obj, context.scene)
        return {'FINISHED'}

class SFC_OT_GridApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_grid_properties
        props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()

class SFC_OT_GridCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
    def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, "コピーしました"); return {'FINISHED'}

class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        prop_groups = {"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip": ["clip_start", "clip_end", "lens_focal_length"],}
        target_names, props_to_reset = {t.name for t in self.targets}, set()
        if "all" in target_names:
            for g in prop_groups.values(): props_to_reset.update(g)
        else:
            for name in target_names: props_to_reset.update(prop_groups.get(name, []))
        props.is_updating_settings = True
        for p in props_to_reset:
            if hasattr(props, p): props.property_unset(p)
        props.is_updating_settings = False
        safe_update_surface_camera(props, context)
        return {'FINISHED'}

class SFC_OT_SetFOV(Operator):
    bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}

class SFC_OT_OpenURL(Operator):
    bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class SFC_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); unregister(); return {'FINISHED'}

class SFC_OT_WireApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
    def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_wire_properties
        props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
        getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()

class SFC_OT_WireCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
    def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; return {'FINISHED'}

class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
    def execute(self, context):
        props = context.scene.zionad_swt_props
        if 0 <= self.hdri_index < len(HDRI_PATHS):
            props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
        return {'FINISHED'}

class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
    def execute(self, context):
        _, nodes, _ = get_world_nodes(context)
        if not nodes: return {'CANCELLED'}
        mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
        if not mapping_node: return {'CANCELLED'}
        if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
        return {'FINISHED'}

# ======================================================================
# --- UIパネル ---
# ======================================================================

class SFC_PT_CameraSetupPanel(Panel):
    bl_label = "1. カメラ作成・切り替え"
    bl_idname = PANEL_IDS["SETUP"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        layout.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
        
        box_init = layout.box()
        box_init.prop(props, "show_init_settings", icon="TRIA_DOWN" if props.show_init_settings else "TRIA_RIGHT")
        if props.show_init_settings:
            col_init = box_init.column(align=True)
            col_init.prop(props, "cam1_init_loc", text="1: 位置"); col_init.prop(props, "cam1_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam2_init_loc", text="2: 位置"); col_init.prop(props, "cam2_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam3_init_loc", text="3: 位置"); col_init.prop(props, "cam3_init_tgt", text="  注視")
            
            box_init.separator()
            box_init.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
            
        layout.separator()
        
        box = layout.box()
        box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA')
        row = box.row(align=True)
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 1", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_1")).cam_index = "1"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 2", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_2")).cam_index = "2"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 3", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_3")).cam_index = "3"
        
        if props.camera_obj:
            box.label(text=f"操作・描画中: {props.camera_obj.name}", icon='CAMERA_DATA')
        else:
            box.label(text="操作カメラ未選択", icon='ERROR')
            
        box.separator()
        box_color = box.box()
        box_color.prop(props, "camera_color")

class SFC_PT_CameraAimingPanel(Panel):
    bl_label = "2. 専用カメラ視線制御 (位置固定)"
    bl_idname = PANEL_IDS["AIMING"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties

        box_manual = layout.box()
        box_manual.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
        
        if props.camera_obj:
            box_manual.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
        
        col_aim = box_manual.column(align=True)
        row_aim = col_aim.row(align=True)
        row_aim.label(text="注視点")
        op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_aim.targets.add().name = "aim"
        op_aim.prop_group_name = "camera"
        col_aim.prop(props, "target_location", text="")
        
        box_manual.separator()
        
        col_offset = box_manual.column(align=True)
        row_offset = col_offset.row(align=True)
        row_offset.label(text="視線オフセット (YPR)")
        op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_offset.targets.add().name = "ypr"
        op_offset.prop_group_name = "camera"
        col_offset.prop(props, "offset_yaw")
        col_offset.prop(props, "offset_pitch")
        col_offset.prop(props, "offset_roll")

class SFC_PT_ViewportCamPanel(Panel):
    bl_label = "3. ビューポート視座コントロール (架空カメラ)"
    bl_idname = PANEL_IDS["VIEWPORT_CAM"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["VIEWPORT_CAM"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        box = layout.box()
        box.label(text="透視投影ビューの操作", icon='VIEW3D')
        
        col = box.column(align=True)
        col.prop(props, "viewport_location")
        col.prop(props, "viewport_target")
        
        box.separator()
        box.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="視座位置・注視点をコピー")
        box.operator(SFC_OT_ResetViewportCam.bl_idname, icon='LOOP_BACK', text="視座・注視点を一括リセット")

class SFC_PT_LensPanel(Panel):
    bl_label = "4. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        if props.camera_obj and props.camera_obj.data:
            cam_data = props.camera_obj.data
            box_type = layout.box()
            box_type.prop(cam_data, "type", text="投影タイプ (透視/平行)")
            
        box = layout.box()
        col = box.column(align=True)
        row = col.row(align=True)
        row.label(text="レンズとクリップ")
        op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op.targets.add().name = "clip"
        op.prop_group_name = "camera"
        
        col.prop(props, "lens_focal_length")
        row = col.row(align=True)
        row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov)
        col.label(text="FOVプリセット:")
        row = col.row(align=True)
        col1, col2 = row.column(align=True), row.column(align=True)
        for i, fov in enumerate(FOV_PRESETS):
            op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°")
            op.fov = fov
        col.separator()
        row = col.row(align=True)
        row.prop(props, "clip_start")
        row.prop(props, "clip_end")

class SFC_PT_CameraDisplayPanel(Panel):
    bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
    def draw(self, context):
        layout, scene, cam = self.layout, context.scene, context.scene.camera
        box_render = layout.box(); box_render.label(text="Render Engine", icon='SCENE'); box_render.prop(scene.render, "engine", expand=True); layout.separator()
        if not cam or not isinstance(cam.data, bpy.types.Camera): layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR'); return
        cam_data = cam.data; overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
        box_passepartout = layout.box(); box_passepartout.label(text="Passepartout", icon='MOD_MASK'); col_passepartout = box_passepartout.column(align=True); col_passepartout.prop(cam_data, "show_passepartout", text="Enable"); row_passepartout = col_passepartout.row(); row_passepartout.enabled = cam_data.show_passepartout; row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
        layout.separator(); box_display = layout.box(); box_display.label(text="Viewport Display", icon='OVERLAY')
        if not overlay: return
        box_display.prop(overlay, "show_overlays", text="Viewport Overlays"); col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays; col_overlay_options.prop(overlay, "show_extras", text="Extras")
        col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras; col_details.prop(overlay, "show_text", text="Text Info"); col_details.prop(cam_data, "show_name", text="Name"); col_details.prop(cam_data, "show_limits", text="Limits")

class ZIONAD_SWT_PT_WorldControlPanel(Panel):
    bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
        if not world or not world.use_nodes or not nodes: return
        box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True)
            for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
            box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if env_node: box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI")
        elif props.background_mode == 'SKY':
            box_sky = layout.box(); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sky_node: box_sky.prop(sky_node, "sky_type", text="Sky Type")

class SFC_PT_GridPanel(Panel):
    bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_grid_properties; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")

class SFC_PT_WirePanel(Panel):
    bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_wire_properties; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")

class SFC_PT_LinksPanel(Panel):
    bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        
        box1 = layout.box()
        box1.label(text="ドキュメント", icon='HELP')
        for link in NEW_DOC_LINKS:
            op = box1.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]
            
        box2 = layout.box()
        box2.label(text="ソーシャル", icon='WORLD_DATA')
        for link in SOCIAL_LINKS:
            op = box2.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]

class SFC_PT_RemovePanel(Panel):
    bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')

# ======================================================================
# --- World Tools 初期化 ---
# ======================================================================

def initial_setup():
    context = bpy.context
    if not context.window_manager:
        return 0.1
    
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                area.show_region_ui = True
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
    
    if context.scene.world and context.scene.world.use_nodes:
        props = context.scene.zionad_swt_props
        nodes = context.scene.world.node_tree.nodes
        background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
        if background_node and background_node.inputs['Color'].is_linked:
            source_node = background_node.inputs['Color'].links[0].from_node
            if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
            else: props.background_mode = 'HDRI'
        update_background_mode(props, context)
    return None

# ======================================================================
# --- 登録/解除 ---
# ======================================================================

classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, 
    SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon,
    ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
    SFC_PT_CameraSetupPanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
    ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_GridPanel, SFC_PT_WirePanel, SFC_PT_LinksPanel,
    SFC_PT_RemovePanel,
)

_registered_classes = []

def register():
    global _registered_classes
    _registered_classes.clear()
    
    for cls in classes:
        try: 
            bpy.utils.register_class(cls)
            _registered_classes.append(cls)
        except Exception as e: 
            print(f"[REGISTER ERROR] {cls.__name__}: {e}")
            
    bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
    bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
    bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
    bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
    
    if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: 
        bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
        
    if not bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.register(initial_setup, first_interval=0.1)

def unregister():
    global _registered_classes
    
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: 
        bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
        
    if bpy.app.timers.is_registered(schedule_update_lock_reset): 
        bpy.app.timers.unregister(schedule_update_lock_reset)
    if bpy.app.timers.is_registered(_do_update_surface_camera): 
        bpy.app.timers.unregister(_do_update_surface_camera)
    if bpy.app.timers.is_registered(_do_update_viewport_cam): 
        bpy.app.timers.unregister(_do_update_viewport_cam)
        
    if bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.unregister(initial_setup)
        
    for prop_name in ['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if prop_name in bpy.types.Scene.__dict__:
            try: delattr(bpy.types.Scene, prop_name)
            except Exception as e: print(f"[UNREGISTER ERROR] delattr {prop_name}: {e}")
            
    for cls in reversed(_registered_classes):
        try: 
            bpy.utils.unregister_class(cls)
        except Exception as e: 
            print(f"[UNREGISTER ERROR] {cls.__name__}: {e}")
            
    _registered_classes.clear()

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty

# ======================================================================
# --- アドオン情報 / Addon Info ---
# ======================================================================

# UI増殖バグを防ぐため、PREFIXは固定化(提言10)
PREFIX = "unit_circle_cam"

bl_info = {
    "name": "zionad 521 [Unit Circle Cam]",
    "author": "zionadchat",
    "version": (37, 0, 6),
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "3つの専用カメラ初期値変更と一括リセット、ビューポートカメラの一括リセット機能(完全安定版)",
    "category": "   [ Cam three ]   ",
}

# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================

ADDON_CATEGORY_NAME = bl_info["category"]
HDRI_PATHS = [
    r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
    r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
    r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
    r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]
WIRE_PRESETS = [("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)), ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)), ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)), ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),]
GRID_PRESETS = [("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)), ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)), ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),]

# ★ 親コレクションとサブコレクション名
MASTER_COLLECTION_NAME = " Cam three "
CAMERA_COLLECTION_NAME = "Cam"

SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]

# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================

NEW_DOC_LINKS = [
    {"label": "THIS_ADDON [ カメラ3台 原型 20260328 ]", "url": "<https://www.notion.so/3-20260328-330f5dacaf4380a4b9b5eef6e98a276f>"},
]

SOCIAL_LINKS = [
    {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]

# ======================================================================
# --- パネル管理 ---
# ======================================================================

PANEL_IDS = {
    "SETUP": f"{PREFIX}_PT_setup", 
    "AIMING": f"{PREFIX}_PT_aiming", "VIEWPORT_CAM": f"{PREFIX}_PT_viewport_cam",
    "LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
    "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {
    PANEL_IDS["SETUP"]: 0, PANEL_IDS["AIMING"]: 1, PANEL_IDS["VIEWPORT_CAM"]: 2, PANEL_IDS["LENS"]: 3, 
    PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, 
    PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 190, PANEL_IDS["REMOVE"]: 200,
}

# ======================================================================
# --- ロック機構 (UIの無限ループ防止) ---
# ======================================================================

def set_update_lock(context, state: bool):
    """ 更新フラグをSceneに持たせ、競合を防ぐ(提言2) """
    if context and hasattr(context, 'scene'):
        context.scene["_sfc_updating"] = state

def is_updating(context):
    if context and hasattr(context, 'scene'):
        return context.scene.get("_sfc_updating", False)
    return False

# 遅延ロック解除用タイマー関数
def schedule_update_lock_reset():
    if bpy.context and hasattr(bpy.context, 'scene'):
        bpy.context.scene["_sfc_updating"] = False
    return None

def trigger_delayed_unlock():
    if not bpy.app.timers.is_registered(schedule_update_lock_reset):
        bpy.app.timers.register(schedule_update_lock_reset, first_interval=0.01)

# ======================================================================
# --- 汎用ヘルパー関数 ---
# ======================================================================

def get_or_create_collection(context, name, parent_col=None):
    col = bpy.data.collections.get(name)
    if not col:
        col = bpy.data.collections.new(name)
        if parent_col:
            if col.name not in parent_col.children:
                parent_col.children.link(col)
        else:
            if col.name not in context.scene.collection.children:
                context.scene.collection.children.link(col)
    return col

def get_master_collection(context):
    return get_or_create_collection(context, MASTER_COLLECTION_NAME)

def find_node(nodes, node_type, name):
    if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
    return nodes.get(name)

def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
    node = find_node(nodes, node_type, name)
    if node: return node
    new_node = nodes.new(type=node_type)
    new_node.name = name
    new_node.label = name.replace("_", " ")
    output_node = find_node(nodes, 'OUTPUT_WORLD', '')
    if output_node: 
        new_node.location = output_node.location + mathutils.Vector(location_offset)
    return new_node

def get_world_nodes(context, create=True):
    world = context.scene.world
    if not world and create: 
        world = bpy.data.worlds.new("World")
        context.scene.world = world
    if not world: return None, None, None
    if create: world.use_nodes = True
    if not world.use_nodes: return world, None, None
    return world, world.node_tree.nodes, world.node_tree.links

def load_hdri_from_path(filepath, context):
    _, nodes, _ = get_world_nodes(context)
    if not nodes: return False
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
    if os.path.exists(filepath):
        try: 
            env_node.image = bpy.data.images.load(filepath, check_existing=True)
            return True
        except Exception as e: 
            print(f"[HDRI Load Error] {filepath} -> {e}")  # 提言6: ファイル破損時のクラッシュ回避とログ出力
            return False
    return False

def update_viewport(context):
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D': 
                        space.shading.type = 'MATERIAL'
                return

def update_background_mode(self, context):
    mode = context.scene.zionad_swt_props.background_mode
    world, nodes, links = get_world_nodes(context)
    if not nodes: return
    output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
    background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
    sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
    mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
    tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
    
    if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
    if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
    
    links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
    
    if mode == 'SKY': 
        links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        links.new(env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(HDRI_PATHS): 
            load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
            
    update_viewport(context)

# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================

def update_cam_color(self, context):
    if self.camera_obj:
        context.preferences.themes[0].view_3d.camera = self.camera_color

def update_grid_color_cb(self, context):
    context.preferences.themes[0].view_3d.grid = self.grid_color

def update_wire_color_cb(self, context):
    context.preferences.themes[0].view_3d.wire = self.wire_color
    context.preferences.themes[0].view_3d.object_active = self.wire_color

class ThemeGridProperties(PropertyGroup):
    grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0), update=update_grid_color_cb)
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))

class ThemeWireProperties(PropertyGroup):
    wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75), update=update_wire_color_cb)
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))

class TargetProperty(PropertyGroup): name: StringProperty()

def update_viewport_cam(self, context):
    if is_updating(context): return
    
    vp_loc = mathutils.Vector(self.viewport_location)
    vp_tgt = mathutils.Vector(self.viewport_target)
    direction = vp_tgt - vp_loc
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    
    for area in context.screen.areas:
        if area.type == 'VIEW_3D':
            for space in area.spaces:
                if space.type == 'VIEW_3D':
                    rv3d = space.region_3d
                    if rv3d:
                        set_update_lock(context, True)
                        try:
                            if rv3d.view_perspective == 'CAMERA':
                                rv3d.view_perspective = 'PERSP'
                            rv3d.view_location = vp_tgt
                            rv3d.view_rotation = rot_quat
                            rv3d.view_distance = direction.length
                        finally:
                            trigger_delayed_unlock()
                        break

class SurfaceCameraProperties(PropertyGroup):
    camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
    
    show_init_settings: BoolProperty(name="初期値設定を表示", default=False)
    
    cam1_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam1_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 100.0, 0.0), subtype='XYZ')
    cam2_init_loc: FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), subtype='XYZ')
    cam2_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam3_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 100.0), subtype='XYZ')
    cam3_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    
    target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
    offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    
    viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
    viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
    
    is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
    lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=lambda s,c: update_surface_camera(s,c))
    clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
    clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
    
    info_horizontal_fov: StringProperty(name="水平視野角")
    
    camera_color: FloatVectorProperty(
        name="カメラ枠線 色", 
        subtype='COLOR', size=3, min=0.0, max=1.0, 
        default=(0.0, 1.0, 1.0), 
        update=lambda self, context: update_cam_color(self, context)
    )

class ZIONAD_SWT_Properties(PropertyGroup):
    background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
    hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)

def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
    try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
    except: return 0.0

def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except: return 50.0

def get_target_location(props):
    return mathutils.Vector(props.target_location)

def update_object_transform(obj, props):
    location = obj.location
    target_location = get_target_location(props)
    direction = target_location - location
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    base_track_quat = direction.to_track_quat('-Z', 'Y')
    offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
    final_quat = base_track_quat @ offset_euler.to_quaternion()
    obj.rotation_euler = final_quat.to_euler('XYZ')

def update_surface_camera(self, context):
    if is_updating(context): return
    set_update_lock(context, True)
    try:
        props = context.scene.surface_camera_properties
        camera_obj = props.camera_obj
        if props.is_updating_settings or not camera_obj: 
            update_info_panel_text(props, context)
            return
        
        cam_data = camera_obj.data
        if cam_data: 
            cam_data.sensor_fit = 'HORIZONTAL'
            cam_data.lens_unit = 'MILLIMETERS'
            cam_data.lens = props.lens_focal_length
            cam_data.clip_start = props.clip_start
            cam_data.clip_end = props.clip_end
            
        update_object_transform(camera_obj, props)
        update_info_panel_text(props, context)
    finally: 
        trigger_delayed_unlock()

def update_info_panel_text(props, context):
    if not hasattr(context, 'scene') or not props: return
    camera_obj = props.camera_obj
    if not camera_obj: return
    current_fov = calculate_horizontal_fov(props.lens_focal_length)
    props.info_horizontal_fov = f"{current_fov:.1f} °"

def sync_ui_from_manual_transform(props, obj, context):
    if is_updating(context): return
    set_update_lock(context, True)
    try:
        target_location = get_target_location(props)
        direction = target_location - obj.location
        if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
        base_track_quat = direction.to_track_quat('-Z', 'Y')
        final_quat = obj.matrix_world.to_quaternion()
        offset_quat = base_track_quat.inverted() @ final_quat
        offset_euler = offset_quat.to_euler('XYZ')
        props.offset_pitch = offset_euler.x
        props.offset_yaw = offset_euler.y
        props.offset_roll = offset_euler.z
    finally: 
        trigger_delayed_unlock()
    update_info_panel_text(props, context)

@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
    context = bpy.context
    if is_updating(context): return
    if not (hasattr(context, 'scene') and context.scene): return
    
    sfc_props = context.scene.surface_camera_properties
    cam_obj = sfc_props.camera_obj
    if not cam_obj: return # 提言3: 対象がない場合は即終了
    
    for update in depsgraph.updates:
        if not update.is_updated_transform: continue
        if update.id.original == cam_obj: # 提言3: 監視対象を特定のものだけに限定
            sync_ui_from_manual_transform(sfc_props, cam_obj, context)
            return

# ======================================================================
# --- オペレーター ---
# ======================================================================

def set_initial_camera_transform(obj, loc, tgt):
    loc_vec = mathutils.Vector(loc)
    tgt_vec = mathutils.Vector(tgt)
    direction = tgt_vec - loc_vec
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    obj.location = loc_vec
    obj.rotation_euler = rot_quat.to_euler('XYZ')

class SFC_OT_CreateThreeCameras(Operator):
    bl_idname = f"{PREFIX}.create_three_cameras"
    bl_label = "3つのカメラを生成・初期化"
    
    def execute(self, context):
        master_col = get_master_collection(context)
        col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, master_col)
            
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            name = f"Fixed_Cam_{idx}"
            # 提言4: カメラとして存在するか安全に取得
            cam_obj = next((obj for obj in bpy.data.objects if obj.name == name and obj.type == 'CAMERA'), None)
            
            if not cam_obj:
                cam_data = bpy.data.cameras.new(name=name)
                cam_obj = bpy.data.objects.new(name, cam_data)
                col.objects.link(cam_obj)
                # 提言5: リンク解除の安全性確保
                if cam_obj.name in context.scene.collection.objects.keys():
                    context.scene.collection.objects.unlink(cam_obj)
                    
            set_initial_camera_transform(cam_obj, loc, tgt)
            
        op_func = getattr(getattr(bpy.ops, PREFIX), "switch_camera")
        op_func(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました")
        return {'FINISHED'}

class SFC_OT_ResetThreeCameras(Operator):
    bl_idname = f"{PREFIX}.reset_three_cameras"
    bl_label = "カメラを初期値に一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            cam_obj = bpy.data.objects.get(f"Fixed_Cam_{idx}")
            if cam_obj and cam_obj.type == 'CAMERA':
                set_initial_camera_transform(cam_obj, loc, tgt)
                if props.camera_obj == cam_obj:
                    props.is_updating_settings = True
                    props.target_location = tgt
                    props.offset_yaw = 0.0
                    props.offset_pitch = 0.0
                    props.offset_roll = 0.0
                    props.is_updating_settings = False
                    
        self.report({'INFO'}, "カメラを初期値にリセットしました")
        return {'FINISHED'}

class SFC_OT_ResetViewportCam(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_cam"
    bl_label = "架空カメラを一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        props.viewport_location = (0.0, -10.0, 5.0)
        props.viewport_target = (0.0, 0.0, 0.0)
        self.report({'INFO'}, "架空カメラをリセットしました")
        return {'FINISHED'}

class SFC_OT_CopyViewportInfo(Operator):
    bl_idname = f"{PREFIX}.copy_viewport_info"
    bl_label = "視座・注視点情報をコピー"
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc = props.viewport_location
        tgt = props.viewport_target
        
        fmt = ".2f"
        loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
        tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
        
        text_to_copy = f"視座位置: {loc_str}\n注視点: {tgt_str}"
        context.window_manager.clipboard = text_to_copy
        self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました")
        return {'FINISHED'}

class SFC_OT_SwitchCamera(Operator):
    bl_idname = f"{PREFIX}.switch_camera"
    bl_label = "カメラを切り替え"
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        name = f"Fixed_Cam_{self.cam_index}"
        
        # 提言4: 安全なカメラ取得
        cam_obj = next((obj for obj in bpy.data.objects if obj.name == name and obj.type == 'CAMERA'), None)
        
        if not cam_obj:
            self.report({'WARNING'}, f"{name} が見つかりません。先に「生成」ボタンを押してください。")
            return {'CANCELLED'}
            
        props.is_updating_settings = True
        props.camera_obj = cam_obj
        context.scene.camera = cam_obj
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.region_3d.view_perspective = 'CAMERA'
        
        context.preferences.themes[0].view_3d.camera = props.camera_color
        
        cam_data = cam_obj.data
        props.lens_focal_length = cam_data.lens
        props.clip_start = cam_data.clip_start
        props.clip_end = cam_data.clip_end
        
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        props.target_location = cam_obj.location + forward_vec
        props.offset_yaw = 0.0
        props.offset_pitch = 0.0
        props.offset_roll = 0.0
        
        props.is_updating_settings = False
        sync_ui_from_manual_transform(props, cam_obj, context)
        return {'FINISHED'}

class SFC_OT_GridApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_grid_properties
        props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()

class SFC_OT_GridCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
    def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, "コピーしました"); return {'FINISHED'}

class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        prop_groups = {"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip": ["clip_start", "clip_end", "lens_focal_length"],}
        target_names, props_to_reset = {t.name for t in self.targets}, set()
        if "all" in target_names:
            for g in prop_groups.values(): props_to_reset.update(g)
        else:
            for name in target_names: props_to_reset.update(prop_groups.get(name, []))
        props.is_updating_settings = True
        for p in props_to_reset:
            if hasattr(props, p): props.property_unset(p)
        props.is_updating_settings = False; update_surface_camera(props, context); return {'FINISHED'}

class SFC_OT_SetFOV(Operator):
    bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}

class SFC_OT_OpenURL(Operator):
    bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class SFC_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); unregister(); return {'FINISHED'}

class SFC_OT_WireApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
    def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_wire_properties
        props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
        getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()

class SFC_OT_WireCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
    def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; return {'FINISHED'}

class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
    def execute(self, context):
        props = context.scene.zionad_swt_props
        if 0 <= self.hdri_index < len(HDRI_PATHS):
            props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
        return {'FINISHED'}

class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
    def execute(self, context):
        _, nodes, _ = get_world_nodes(context)
        if not nodes: return {'CANCELLED'}
        mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
        if not mapping_node: return {'CANCELLED'}
        if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
        return {'FINISHED'}

# ======================================================================
# --- UIパネル ---
# ======================================================================

class SFC_PT_CameraSetupPanel(Panel):
    bl_label = "1. カメラ作成・切り替え"
    bl_idname = PANEL_IDS["SETUP"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        layout.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
        
        box_init = layout.box()
        box_init.prop(props, "show_init_settings", icon="TRIA_DOWN" if props.show_init_settings else "TRIA_RIGHT")
        if props.show_init_settings:
            col_init = box_init.column(align=True)
            col_init.prop(props, "cam1_init_loc", text="1: 位置"); col_init.prop(props, "cam1_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam2_init_loc", text="2: 位置"); col_init.prop(props, "cam2_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam3_init_loc", text="3: 位置"); col_init.prop(props, "cam3_init_tgt", text="  注視")
            
            box_init.separator()
            box_init.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
            
        layout.separator()
        
        box = layout.box()
        box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA')
        row = box.row(align=True)
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 1", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_1")).cam_index = "1"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 2", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_2")).cam_index = "2"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 3", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_3")).cam_index = "3"
        
        if props.camera_obj:
            box.label(text=f"操作・描画中: {props.camera_obj.name}", icon='CAMERA_DATA')
        else:
            box.label(text="操作カメラ未選択", icon='ERROR')
            
        box.separator()
        box_color = box.box()
        box_color.prop(props, "camera_color")

class SFC_PT_CameraAimingPanel(Panel):
    bl_label = "2. 専用カメラ視線制御 (位置固定)"
    bl_idname = PANEL_IDS["AIMING"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties

        box_manual = layout.box()
        box_manual.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
        
        if props.camera_obj:
            box_manual.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
        
        col_aim = box_manual.column(align=True)
        row_aim = col_aim.row(align=True)
        row_aim.label(text="注視点")
        op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_aim.targets.add().name = "aim"
        op_aim.prop_group_name = "camera"
        col_aim.prop(props, "target_location", text="")
        
        box_manual.separator()
        
        col_offset = box_manual.column(align=True)
        row_offset = col_offset.row(align=True)
        row_offset.label(text="視線オフセット (YPR)")
        op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_offset.targets.add().name = "ypr"
        op_offset.prop_group_name = "camera"
        col_offset.prop(props, "offset_yaw")
        col_offset.prop(props, "offset_pitch")
        col_offset.prop(props, "offset_roll")

class SFC_PT_ViewportCamPanel(Panel):
    bl_label = "3. ビューポート視座コントロール (架空カメラ)"
    bl_idname = PANEL_IDS["VIEWPORT_CAM"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["VIEWPORT_CAM"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        box = layout.box()
        box.label(text="透視投影ビューの操作", icon='VIEW3D')
        
        col = box.column(align=True)
        col.prop(props, "viewport_location")
        col.prop(props, "viewport_target")
        
        box.separator()
        box.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="視座位置・注視点をコピー")
        box.operator(SFC_OT_ResetViewportCam.bl_idname, icon='LOOP_BACK', text="視座・注視点を一括リセット")

class SFC_PT_LensPanel(Panel):
    bl_label = "4. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        if props.camera_obj and props.camera_obj.data:
            cam_data = props.camera_obj.data
            box_type = layout.box()
            box_type.prop(cam_data, "type", text="投影タイプ (透視/平行)")
            
        box = layout.box()
        col = box.column(align=True)
        row = col.row(align=True)
        row.label(text="レンズとクリップ")
        op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op.targets.add().name = "clip"
        op.prop_group_name = "camera"
        
        col.prop(props, "lens_focal_length")
        row = col.row(align=True)
        row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov)
        col.label(text="FOVプリセット:")
        row = col.row(align=True)
        col1, col2 = row.column(align=True), row.column(align=True)
        for i, fov in enumerate(FOV_PRESETS):
            op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°")
            op.fov = fov
        col.separator()
        row = col.row(align=True)
        row.prop(props, "clip_start")
        row.prop(props, "clip_end")

class SFC_PT_CameraDisplayPanel(Panel):
    bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
    def draw(self, context):
        layout, scene, cam = self.layout, context.scene, context.scene.camera
        box_render = layout.box(); box_render.label(text="Render Engine", icon='SCENE'); box_render.prop(scene.render, "engine", expand=True); layout.separator()
        if not cam or not isinstance(cam.data, bpy.types.Camera): layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR'); return
        cam_data = cam.data; overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
        box_passepartout = layout.box(); box_passepartout.label(text="Passepartout", icon='MOD_MASK'); col_passepartout = box_passepartout.column(align=True); col_passepartout.prop(cam_data, "show_passepartout", text="Enable"); row_passepartout = col_passepartout.row(); row_passepartout.enabled = cam_data.show_passepartout; row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
        layout.separator(); box_display = layout.box(); box_display.label(text="Viewport Display", icon='OVERLAY')
        if not overlay: return
        box_display.prop(overlay, "show_overlays", text="Viewport Overlays"); col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays; col_overlay_options.prop(overlay, "show_extras", text="Extras")
        col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras; col_details.prop(overlay, "show_text", text="Text Info"); col_details.prop(cam_data, "show_name", text="Name"); col_details.prop(cam_data, "show_limits", text="Limits")

class ZIONAD_SWT_PT_WorldControlPanel(Panel):
    bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
        if not world or not world.use_nodes or not nodes: return
        box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True)
            for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
            box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if env_node: box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI")
        elif props.background_mode == 'SKY':
            box_sky = layout.box(); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sky_node: box_sky.prop(sky_node, "sky_type", text="Sky Type")

class SFC_PT_GridPanel(Panel):
    bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_grid_properties; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")

class SFC_PT_WirePanel(Panel):
    bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_wire_properties; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")

class SFC_PT_LinksPanel(Panel):
    bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        
        box1 = layout.box()
        box1.label(text="ドキュメント", icon='HELP')
        for link in NEW_DOC_LINKS:
            op = box1.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]
            
        box2 = layout.box()
        box2.label(text="ソーシャル", icon='WORLD_DATA')
        for link in SOCIAL_LINKS:
            op = box2.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]

class SFC_PT_RemovePanel(Panel):
    bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')

# ======================================================================
# --- World Tools 初期化 ---
# ======================================================================

def initial_setup():
    context = bpy.context
    
    # 提言8: UIの準備ができていない場合はリトライさせる
    if not context.window_manager:
        return 0.1
    
    # 全ての3Dビューポートでサイドパネル(Nパネル)を開き、ShadingをMATERIALに変更
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                area.show_region_ui = True
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
    
    if context.scene.world and context.scene.world.use_nodes:
        props = context.scene.zionad_swt_props
        nodes = context.scene.world.node_tree.nodes
        background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
        if background_node and background_node.inputs['Color'].is_linked:
            source_node = background_node.inputs['Color'].links[0].from_node
            if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
            else: props.background_mode = 'HDRI'
        update_background_mode(props, context)
    return None

# ======================================================================
# --- 登録/解除 ---
# ======================================================================

classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, 
    SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon,
    ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
    SFC_PT_CameraSetupPanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
    ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_GridPanel, SFC_PT_WirePanel, SFC_PT_LinksPanel,
    SFC_PT_RemovePanel,
)

_registered_classes = []

def register():
    global _registered_classes
    _registered_classes.clear()
    
    for cls in classes:
        try: 
            bpy.utils.register_class(cls)
            _registered_classes.append(cls)
        except Exception as e: 
            print(f"[REGISTER ERROR] {cls.__name__}: {e}") # 提言1: 例外の握り潰し解消
            
    bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
    bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
    bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
    bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
    
    if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: 
        bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
        
    if not bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.register(initial_setup, first_interval=0.1)

def unregister():
    global _registered_classes
    
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: 
        bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
        
    if bpy.app.timers.is_registered(schedule_update_lock_reset): 
        bpy.app.timers.unregister(schedule_update_lock_reset)
        
    if bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.unregister(initial_setup)
        
    for prop_name in ['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if hasattr(bpy.types.Scene, prop_name):
            try: delattr(bpy.types.Scene, prop_name)
            except: pass
            
    # 提言9: 登録に成功したクラスだけを正確に逆順で解除する
    for cls in reversed(_registered_classes):
        try: 
            bpy.utils.unregister_class(cls)
        except Exception as e: 
            print(f"[UNREGISTER ERROR] {cls.__name__}: {e}")
            
    _registered_classes.clear()

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty

# ======================================================================
# --- アドオン情報 / Addon Info ---
# ======================================================================

# UI増殖バグを防ぐため、PREFIXは固定化(提言10)
PREFIX = "unit_circle_cam"

bl_info = {
    "name": "zionad 521 [Unit Circle Cam]",
    "author": "zionadchat",
    "version": (37, 0, 6),
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "3つの専用カメラ初期値変更と一括リセット、ビューポートカメラの一括リセット機能(完全安定版)",
    "category": "   [ Cam three ]   ",
}

# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================

ADDON_CATEGORY_NAME = bl_info["category"]
HDRI_PATHS = [
    r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
    r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
    r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
    r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]
WIRE_PRESETS = [("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)), ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)), ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)), ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),]
GRID_PRESETS = [("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)), ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)), ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),]

# ★ 親コレクションとサブコレクション名
MASTER_COLLECTION_NAME = " Cam three "
CAMERA_COLLECTION_NAME = "Cam"

SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]

# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================

NEW_DOC_LINKS = [
    {"label": "THIS_ADDON [ カメラ3台 原型 20260328 ]", "url": "<https://www.notion.so/3-20260328-330f5dacaf4380a4b9b5eef6e98a276f>"},
]

SOCIAL_LINKS = [
    {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]

# ======================================================================
# --- パネル管理 ---
# ======================================================================

PANEL_IDS = {
    "SETUP": f"{PREFIX}_PT_setup", 
    "AIMING": f"{PREFIX}_PT_aiming", "VIEWPORT_CAM": f"{PREFIX}_PT_viewport_cam",
    "LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
    "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {
    PANEL_IDS["SETUP"]: 0, PANEL_IDS["AIMING"]: 1, PANEL_IDS["VIEWPORT_CAM"]: 2, PANEL_IDS["LENS"]: 3, 
    PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, 
    PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 190, PANEL_IDS["REMOVE"]: 200,
}

# ======================================================================
# --- ロック機構 (UIの無限ループ防止) ---
# ======================================================================

def set_update_lock(context, state: bool):
    """ 更新フラグをSceneに持たせ、競合を防ぐ(提言2) """
    if context and hasattr(context, 'scene'):
        context.scene["_sfc_updating"] = state

def is_updating(context):
    if context and hasattr(context, 'scene'):
        return context.scene.get("_sfc_updating", False)
    return False

# 遅延ロック解除用タイマー関数
def schedule_update_lock_reset():
    if bpy.context and hasattr(bpy.context, 'scene'):
        bpy.context.scene["_sfc_updating"] = False
    return None

def trigger_delayed_unlock():
    if not bpy.app.timers.is_registered(schedule_update_lock_reset):
        bpy.app.timers.register(schedule_update_lock_reset, first_interval=0.01)

# ======================================================================
# --- 汎用ヘルパー関数 ---
# ======================================================================

def get_or_create_collection(context, name, parent_col=None):
    col = bpy.data.collections.get(name)
    if not col:
        col = bpy.data.collections.new(name)
        if parent_col:
            if col.name not in parent_col.children:
                parent_col.children.link(col)
        else:
            if col.name not in context.scene.collection.children:
                context.scene.collection.children.link(col)
    return col

def get_master_collection(context):
    return get_or_create_collection(context, MASTER_COLLECTION_NAME)

def find_node(nodes, node_type, name):
    if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
    return nodes.get(name)

def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
    node = find_node(nodes, node_type, name)
    if node: return node
    new_node = nodes.new(type=node_type)
    new_node.name = name
    new_node.label = name.replace("_", " ")
    output_node = find_node(nodes, 'OUTPUT_WORLD', '')
    if output_node: 
        new_node.location = output_node.location + mathutils.Vector(location_offset)
    return new_node

def get_world_nodes(context, create=True):
    world = context.scene.world
    if not world and create: 
        world = bpy.data.worlds.new("World")
        context.scene.world = world
    if not world: return None, None, None
    if create: world.use_nodes = True
    if not world.use_nodes: return world, None, None
    return world, world.node_tree.nodes, world.node_tree.links

def load_hdri_from_path(filepath, context):
    _, nodes, _ = get_world_nodes(context)
    if not nodes: return False
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
    if os.path.exists(filepath):
        try: 
            env_node.image = bpy.data.images.load(filepath, check_existing=True)
            return True
        except Exception as e: 
            print(f"[HDRI Load Error] {filepath} -> {e}")  # 提言6: ファイル破損時のクラッシュ回避とログ出力
            return False
    return False

def update_viewport(context):
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D': 
                        space.shading.type = 'MATERIAL'
                return

def update_background_mode(self, context):
    mode = context.scene.zionad_swt_props.background_mode
    world, nodes, links = get_world_nodes(context)
    if not nodes: return
    output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
    background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
    sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
    mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
    tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
    
    if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
    if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
    
    links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
    
    if mode == 'SKY': 
        links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        links.new(env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(HDRI_PATHS): 
            load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
            
    update_viewport(context)

# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================

def update_cam_color(self, context):
    if self.camera_obj:
        context.preferences.themes[0].view_3d.camera = self.camera_color

def update_grid_color_cb(self, context):
    context.preferences.themes[0].view_3d.grid = self.grid_color

def update_wire_color_cb(self, context):
    context.preferences.themes[0].view_3d.wire = self.wire_color
    context.preferences.themes[0].view_3d.object_active = self.wire_color

class ThemeGridProperties(PropertyGroup):
    grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0), update=update_grid_color_cb)
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))

class ThemeWireProperties(PropertyGroup):
    wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75), update=update_wire_color_cb)
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))

class TargetProperty(PropertyGroup): name: StringProperty()

def update_viewport_cam(self, context):
    if is_updating(context): return
    
    vp_loc = mathutils.Vector(self.viewport_location)
    vp_tgt = mathutils.Vector(self.viewport_target)
    direction = vp_tgt - vp_loc
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    
    for area in context.screen.areas:
        if area.type == 'VIEW_3D':
            for space in area.spaces:
                if space.type == 'VIEW_3D':
                    rv3d = space.region_3d
                    if rv3d:
                        set_update_lock(context, True)
                        try:
                            if rv3d.view_perspective == 'CAMERA':
                                rv3d.view_perspective = 'PERSP'
                            rv3d.view_location = vp_tgt
                            rv3d.view_rotation = rot_quat
                            rv3d.view_distance = direction.length
                        finally:
                            trigger_delayed_unlock()
                        break

class SurfaceCameraProperties(PropertyGroup):
    camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
    
    show_init_settings: BoolProperty(name="初期値設定を表示", default=False)
    
    cam1_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam1_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 100.0, 0.0), subtype='XYZ')
    cam2_init_loc: FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), subtype='XYZ')
    cam2_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam3_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 100.0), subtype='XYZ')
    cam3_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    
    target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
    offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    
    viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
    viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
    
    is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
    lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=lambda s,c: update_surface_camera(s,c))
    clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
    clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
    
    info_horizontal_fov: StringProperty(name="水平視野角")
    
    camera_color: FloatVectorProperty(
        name="カメラ枠線 色", 
        subtype='COLOR', size=3, min=0.0, max=1.0, 
        default=(0.0, 1.0, 1.0), 
        update=lambda self, context: update_cam_color(self, context)
    )

class ZIONAD_SWT_Properties(PropertyGroup):
    background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
    hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)

def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
    try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
    except: return 0.0

def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except: return 50.0

def get_target_location(props):
    return mathutils.Vector(props.target_location)

def update_object_transform(obj, props):
    location = obj.location
    target_location = get_target_location(props)
    direction = target_location - location
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    base_track_quat = direction.to_track_quat('-Z', 'Y')
    offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
    final_quat = base_track_quat @ offset_euler.to_quaternion()
    obj.rotation_euler = final_quat.to_euler('XYZ')

def update_surface_camera(self, context):
    if is_updating(context): return
    set_update_lock(context, True)
    try:
        props = context.scene.surface_camera_properties
        camera_obj = props.camera_obj
        if props.is_updating_settings or not camera_obj: 
            update_info_panel_text(props, context)
            return
        
        cam_data = camera_obj.data
        if cam_data: 
            cam_data.sensor_fit = 'HORIZONTAL'
            cam_data.lens_unit = 'MILLIMETERS'
            cam_data.lens = props.lens_focal_length
            cam_data.clip_start = props.clip_start
            cam_data.clip_end = props.clip_end
            
        update_object_transform(camera_obj, props)
        update_info_panel_text(props, context)
    finally: 
        trigger_delayed_unlock()

def update_info_panel_text(props, context):
    if not hasattr(context, 'scene') or not props: return
    camera_obj = props.camera_obj
    if not camera_obj: return
    current_fov = calculate_horizontal_fov(props.lens_focal_length)
    props.info_horizontal_fov = f"{current_fov:.1f} °"

def sync_ui_from_manual_transform(props, obj, context):
    if is_updating(context): return
    set_update_lock(context, True)
    try:
        target_location = get_target_location(props)
        direction = target_location - obj.location
        if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
        base_track_quat = direction.to_track_quat('-Z', 'Y')
        final_quat = obj.matrix_world.to_quaternion()
        offset_quat = base_track_quat.inverted() @ final_quat
        offset_euler = offset_quat.to_euler('XYZ')
        props.offset_pitch = offset_euler.x
        props.offset_yaw = offset_euler.y
        props.offset_roll = offset_euler.z
    finally: 
        trigger_delayed_unlock()
    update_info_panel_text(props, context)

@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
    context = bpy.context
    if is_updating(context): return
    if not (hasattr(context, 'scene') and context.scene): return
    
    sfc_props = context.scene.surface_camera_properties
    cam_obj = sfc_props.camera_obj
    if not cam_obj: return # 提言3: 対象がない場合は即終了
    
    for update in depsgraph.updates:
        if not update.is_updated_transform: continue
        if update.id.original == cam_obj: # 提言3: 監視対象を特定のものだけに限定
            sync_ui_from_manual_transform(sfc_props, cam_obj, context)
            return

# ======================================================================
# --- オペレーター ---
# ======================================================================

def set_initial_camera_transform(obj, loc, tgt):
    loc_vec = mathutils.Vector(loc)
    tgt_vec = mathutils.Vector(tgt)
    direction = tgt_vec - loc_vec
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    obj.location = loc_vec
    obj.rotation_euler = rot_quat.to_euler('XYZ')

class SFC_OT_CreateThreeCameras(Operator):
    bl_idname = f"{PREFIX}.create_three_cameras"
    bl_label = "3つのカメラを生成・初期化"
    
    def execute(self, context):
        master_col = get_master_collection(context)
        col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, master_col)
            
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            name = f"Fixed_Cam_{idx}"
            # 提言4: カメラとして存在するか安全に取得
            cam_obj = next((obj for obj in bpy.data.objects if obj.name == name and obj.type == 'CAMERA'), None)
            
            if not cam_obj:
                cam_data = bpy.data.cameras.new(name=name)
                cam_obj = bpy.data.objects.new(name, cam_data)
                col.objects.link(cam_obj)
                # 提言5: リンク解除の安全性確保
                if cam_obj.name in context.scene.collection.objects.keys():
                    context.scene.collection.objects.unlink(cam_obj)
                    
            set_initial_camera_transform(cam_obj, loc, tgt)
            
        op_func = getattr(getattr(bpy.ops, PREFIX), "switch_camera")
        op_func(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました")
        return {'FINISHED'}

class SFC_OT_ResetThreeCameras(Operator):
    bl_idname = f"{PREFIX}.reset_three_cameras"
    bl_label = "カメラを初期値に一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            cam_obj = bpy.data.objects.get(f"Fixed_Cam_{idx}")
            if cam_obj and cam_obj.type == 'CAMERA':
                set_initial_camera_transform(cam_obj, loc, tgt)
                if props.camera_obj == cam_obj:
                    props.is_updating_settings = True
                    props.target_location = tgt
                    props.offset_yaw = 0.0
                    props.offset_pitch = 0.0
                    props.offset_roll = 0.0
                    props.is_updating_settings = False
                    
        self.report({'INFO'}, "カメラを初期値にリセットしました")
        return {'FINISHED'}

class SFC_OT_ResetViewportCam(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_cam"
    bl_label = "架空カメラを一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        props.viewport_location = (0.0, -10.0, 5.0)
        props.viewport_target = (0.0, 0.0, 0.0)
        self.report({'INFO'}, "架空カメラをリセットしました")
        return {'FINISHED'}

class SFC_OT_CopyViewportInfo(Operator):
    bl_idname = f"{PREFIX}.copy_viewport_info"
    bl_label = "視座・注視点情報をコピー"
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc = props.viewport_location
        tgt = props.viewport_target
        
        fmt = ".2f"
        loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
        tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
        
        text_to_copy = f"視座位置: {loc_str}\n注視点: {tgt_str}"
        context.window_manager.clipboard = text_to_copy
        self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました")
        return {'FINISHED'}

class SFC_OT_SwitchCamera(Operator):
    bl_idname = f"{PREFIX}.switch_camera"
    bl_label = "カメラを切り替え"
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        name = f"Fixed_Cam_{self.cam_index}"
        
        # 提言4: 安全なカメラ取得
        cam_obj = next((obj for obj in bpy.data.objects if obj.name == name and obj.type == 'CAMERA'), None)
        
        if not cam_obj:
            self.report({'WARNING'}, f"{name} が見つかりません。先に「生成」ボタンを押してください。")
            return {'CANCELLED'}
            
        props.is_updating_settings = True
        props.camera_obj = cam_obj
        context.scene.camera = cam_obj
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.region_3d.view_perspective = 'CAMERA'
        
        context.preferences.themes[0].view_3d.camera = props.camera_color
        
        cam_data = cam_obj.data
        props.lens_focal_length = cam_data.lens
        props.clip_start = cam_data.clip_start
        props.clip_end = cam_data.clip_end
        
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        props.target_location = cam_obj.location + forward_vec
        props.offset_yaw = 0.0
        props.offset_pitch = 0.0
        props.offset_roll = 0.0
        
        props.is_updating_settings = False
        sync_ui_from_manual_transform(props, cam_obj, context)
        return {'FINISHED'}

class SFC_OT_GridApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_grid_properties
        props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()

class SFC_OT_GridCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
    def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, "コピーしました"); return {'FINISHED'}

class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        prop_groups = {"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip": ["clip_start", "clip_end", "lens_focal_length"],}
        target_names, props_to_reset = {t.name for t in self.targets}, set()
        if "all" in target_names:
            for g in prop_groups.values(): props_to_reset.update(g)
        else:
            for name in target_names: props_to_reset.update(prop_groups.get(name, []))
        props.is_updating_settings = True
        for p in props_to_reset:
            if hasattr(props, p): props.property_unset(p)
        props.is_updating_settings = False; update_surface_camera(props, context); return {'FINISHED'}

class SFC_OT_SetFOV(Operator):
    bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}

class SFC_OT_OpenURL(Operator):
    bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class SFC_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); unregister(); return {'FINISHED'}

class SFC_OT_WireApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
    def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_wire_properties
        props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
        getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()

class SFC_OT_WireCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
    def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; return {'FINISHED'}

class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
    def execute(self, context):
        props = context.scene.zionad_swt_props
        if 0 <= self.hdri_index < len(HDRI_PATHS):
            props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
        return {'FINISHED'}

class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
    def execute(self, context):
        _, nodes, _ = get_world_nodes(context)
        if not nodes: return {'CANCELLED'}
        mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
        if not mapping_node: return {'CANCELLED'}
        if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
        return {'FINISHED'}

# ======================================================================
# --- UIパネル ---
# ======================================================================

class SFC_PT_CameraSetupPanel(Panel):
    bl_label = "1. カメラ作成・切り替え"
    bl_idname = PANEL_IDS["SETUP"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        layout.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
        
        box_init = layout.box()
        box_init.prop(props, "show_init_settings", icon="TRIA_DOWN" if props.show_init_settings else "TRIA_RIGHT")
        if props.show_init_settings:
            col_init = box_init.column(align=True)
            col_init.prop(props, "cam1_init_loc", text="1: 位置"); col_init.prop(props, "cam1_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam2_init_loc", text="2: 位置"); col_init.prop(props, "cam2_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam3_init_loc", text="3: 位置"); col_init.prop(props, "cam3_init_tgt", text="  注視")
            
            box_init.separator()
            box_init.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
            
        layout.separator()
        
        box = layout.box()
        box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA')
        row = box.row(align=True)
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 1", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_1")).cam_index = "1"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 2", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_2")).cam_index = "2"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 3", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_3")).cam_index = "3"
        
        if props.camera_obj:
            box.label(text=f"操作・描画中: {props.camera_obj.name}", icon='CAMERA_DATA')
        else:
            box.label(text="操作カメラ未選択", icon='ERROR')
            
        box.separator()
        box_color = box.box()
        box_color.prop(props, "camera_color")

class SFC_PT_CameraAimingPanel(Panel):
    bl_label = "2. 専用カメラ視線制御 (位置固定)"
    bl_idname = PANEL_IDS["AIMING"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties

        box_manual = layout.box()
        box_manual.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
        
        if props.camera_obj:
            box_manual.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
        
        col_aim = box_manual.column(align=True)
        row_aim = col_aim.row(align=True)
        row_aim.label(text="注視点")
        op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_aim.targets.add().name = "aim"
        op_aim.prop_group_name = "camera"
        col_aim.prop(props, "target_location", text="")
        
        box_manual.separator()
        
        col_offset = box_manual.column(align=True)
        row_offset = col_offset.row(align=True)
        row_offset.label(text="視線オフセット (YPR)")
        op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_offset.targets.add().name = "ypr"
        op_offset.prop_group_name = "camera"
        col_offset.prop(props, "offset_yaw")
        col_offset.prop(props, "offset_pitch")
        col_offset.prop(props, "offset_roll")

class SFC_PT_ViewportCamPanel(Panel):
    bl_label = "3. ビューポート視座コントロール (架空カメラ)"
    bl_idname = PANEL_IDS["VIEWPORT_CAM"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["VIEWPORT_CAM"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        box = layout.box()
        box.label(text="透視投影ビューの操作", icon='VIEW3D')
        
        col = box.column(align=True)
        col.prop(props, "viewport_location")
        col.prop(props, "viewport_target")
        
        box.separator()
        box.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="視座位置・注視点をコピー")
        box.operator(SFC_OT_ResetViewportCam.bl_idname, icon='LOOP_BACK', text="視座・注視点を一括リセット")

class SFC_PT_LensPanel(Panel):
    bl_label = "4. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        if props.camera_obj and props.camera_obj.data:
            cam_data = props.camera_obj.data
            box_type = layout.box()
            box_type.prop(cam_data, "type", text="投影タイプ (透視/平行)")
            
        box = layout.box()
        col = box.column(align=True)
        row = col.row(align=True)
        row.label(text="レンズとクリップ")
        op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op.targets.add().name = "clip"
        op.prop_group_name = "camera"
        
        col.prop(props, "lens_focal_length")
        row = col.row(align=True)
        row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov)
        col.label(text="FOVプリセット:")
        row = col.row(align=True)
        col1, col2 = row.column(align=True), row.column(align=True)
        for i, fov in enumerate(FOV_PRESETS):
            op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°")
            op.fov = fov
        col.separator()
        row = col.row(align=True)
        row.prop(props, "clip_start")
        row.prop(props, "clip_end")

class SFC_PT_CameraDisplayPanel(Panel):
    bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
    def draw(self, context):
        layout, scene, cam = self.layout, context.scene, context.scene.camera
        box_render = layout.box(); box_render.label(text="Render Engine", icon='SCENE'); box_render.prop(scene.render, "engine", expand=True); layout.separator()
        if not cam or not isinstance(cam.data, bpy.types.Camera): layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR'); return
        cam_data = cam.data; overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
        box_passepartout = layout.box(); box_passepartout.label(text="Passepartout", icon='MOD_MASK'); col_passepartout = box_passepartout.column(align=True); col_passepartout.prop(cam_data, "show_passepartout", text="Enable"); row_passepartout = col_passepartout.row(); row_passepartout.enabled = cam_data.show_passepartout; row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
        layout.separator(); box_display = layout.box(); box_display.label(text="Viewport Display", icon='OVERLAY')
        if not overlay: return
        box_display.prop(overlay, "show_overlays", text="Viewport Overlays"); col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays; col_overlay_options.prop(overlay, "show_extras", text="Extras")
        col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras; col_details.prop(overlay, "show_text", text="Text Info"); col_details.prop(cam_data, "show_name", text="Name"); col_details.prop(cam_data, "show_limits", text="Limits")

class ZIONAD_SWT_PT_WorldControlPanel(Panel):
    bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
        if not world or not world.use_nodes or not nodes: return
        box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True)
            for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
            box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if env_node: box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI")
        elif props.background_mode == 'SKY':
            box_sky = layout.box(); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sky_node: box_sky.prop(sky_node, "sky_type", text="Sky Type")

class SFC_PT_GridPanel(Panel):
    bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_grid_properties; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")

class SFC_PT_WirePanel(Panel):
    bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_wire_properties; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")

class SFC_PT_LinksPanel(Panel):
    bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        
        box1 = layout.box()
        box1.label(text="ドキュメント", icon='HELP')
        for link in NEW_DOC_LINKS:
            op = box1.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]
            
        box2 = layout.box()
        box2.label(text="ソーシャル", icon='WORLD_DATA')
        for link in SOCIAL_LINKS:
            op = box2.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]

class SFC_PT_RemovePanel(Panel):
    bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')

# ======================================================================
# --- World Tools 初期化 ---
# ======================================================================

def initial_setup():
    context = bpy.context
    
    # 提言8: UIの準備ができていない場合はリトライさせる
    if not context.window_manager:
        return 0.1
    
    # 全ての3Dビューポートでサイドパネル(Nパネル)を開き、ShadingをMATERIALに変更
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                area.show_region_ui = True
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
    
    if context.scene.world and context.scene.world.use_nodes:
        props = context.scene.zionad_swt_props
        nodes = context.scene.world.node_tree.nodes
        background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
        if background_node and background_node.inputs['Color'].is_linked:
            source_node = background_node.inputs['Color'].links[0].from_node
            if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
            else: props.background_mode = 'HDRI'
        update_background_mode(props, context)
    return None

# ======================================================================
# --- 登録/解除 ---
# ======================================================================

classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, 
    SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon,
    ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
    SFC_PT_CameraSetupPanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
    ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_GridPanel, SFC_PT_WirePanel, SFC_PT_LinksPanel,
    SFC_PT_RemovePanel,
)

_registered_classes = []

def register():
    global _registered_classes
    _registered_classes.clear()
    
    for cls in classes:
        try: 
            bpy.utils.register_class(cls)
            _registered_classes.append(cls)
        except Exception as e: 
            print(f"[REGISTER ERROR] {cls.__name__}: {e}") # 提言1: 例外の握り潰し解消
            
    bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
    bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
    bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
    bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
    
    if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: 
        bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
        
    if not bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.register(initial_setup, first_interval=0.1)

def unregister():
    global _registered_classes
    
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: 
        bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
        
    if bpy.app.timers.is_registered(schedule_update_lock_reset): 
        bpy.app.timers.unregister(schedule_update_lock_reset)
        
    if bpy.app.timers.is_registered(initial_setup): 
        bpy.app.timers.unregister(initial_setup)
        
    for prop_name in ['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if hasattr(bpy.types.Scene, prop_name):
            try: delattr(bpy.types.Scene, prop_name)
            except: pass
            
    # 提言9: 登録に成功したクラスだけを正確に逆順で解除する
    for cls in reversed(_registered_classes):
        try: 
            bpy.utils.unregister_class(cls)
        except Exception as e: 
            print(f"[UNREGISTER ERROR] {cls.__name__}: {e}")
            
    _registered_classes.clear()

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
from bpy.types import Operator, Panel, Scene, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty
from datetime import datetime

# --- ユニークID生成 ---
START_TIMESTAMP = datetime.now().strftime("%Y%m%d%H%M%S")
PREFIX = f"unit_circle_cam_{START_TIMESTAMP}"

# --- bl_info ---
bl_info = {
    "name": "zionad 521 [Unit Circle Cam]",
    "author": "zionadchat",
    "version": (37, 0, 5),
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "3つの専用カメラ初期値変更と一括リセット、ビューポートカメラの一括リセット機能",
    "category": "   [ Unit Circle Cam ]   ",
}

# ======================================================================
# --- ユーザー設定 / Parameters to Customize ---
# ======================================================================

ADDON_CATEGORY_NAME = bl_info["category"]
HDRI_PATHS = [
    r"C:\a111\HDRi_pic\qwantani_afternoon_puresky_4k.exr",
    r"C:\a111\HDRi_pic\rogland_moonlit_night_4k.hdr",
    r"C:\a111\HDRi_pic\rogland_clear_night_4k.hdr",
    r"C:\a111\HDRi_pic\golden_bay_4k.hdr",
]
WIRE_PRESETS = [("CUSTOM_GREENISH", "Custom Greenish", "Custom greenish wire color", (0.51, 1.0, 0.75)), ("WHITE", "White", "White wire", (1.0, 1.0, 1.0)), ("RED", "Red", "Red wire", (1.0, 0.0, 0.0)), ("GREEN", "Green", "Green wire", (0.0, 1.0, 0.0)),]
GRID_PRESETS = [("CUSTOM_REDDISH", "Custom Reddish", "Custom reddish color", (0.545, 0.322, 0.322, 1.0)), ("DEEP_GREEN", "Deep Green", "A deep green color", (0.098, 0.314, 0.271, 1.0)), ("MINT_GREEN", "Mint Green", "A mint green color", (0.165, 0.557, 0.475, 1.0)),]

# ★ 親コレクションとサブコレクション名
MASTER_COLLECTION_NAME = "Unit Circle Cam"
CAMERA_COLLECTION_NAME = "Cam"

SENSOR_WIDTH = 36.0
FOV_PRESETS = [1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]

# ======================================================================
# --- リンク設定 / Links ---
# ======================================================================

NEW_DOC_LINKS = [
    {"label": "THIS_ADDON [ カメラ3台 原型 20260328 ]", "url": "<https://www.notion.so/3-20260328-330f5dacaf4380a4b9b5eef6e98a276f>"},
]

SOCIAL_LINKS = [
    {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
]

# ======================================================================

PANEL_IDS = {
    "SETUP": f"{PREFIX}_PT_setup", 
    "AIMING": f"{PREFIX}_PT_aiming", "VIEWPORT_CAM": f"{PREFIX}_PT_viewport_cam",
    "LENS": f"{PREFIX}_PT_lens", "CAMERA_DISPLAY": f"{PREFIX}_PT_camera_display", "WORLD_CONTROL": f"{PREFIX}_PT_world_control",
    "GRID": f"{PREFIX}_PT_grid_panel", "WIRE": f"{PREFIX}_PT_wire_panel", "LINKS": f"{PREFIX}_PT_links", "REMOVE": f"{PREFIX}_PT_remove",
}
PANEL_ORDER = {
    PANEL_IDS["SETUP"]: 0, PANEL_IDS["AIMING"]: 1, PANEL_IDS["VIEWPORT_CAM"]: 2, PANEL_IDS["LENS"]: 3, 
    PANEL_IDS["CAMERA_DISPLAY"]: 4, PANEL_IDS["WORLD_CONTROL"]: 5, 
    PANEL_IDS["GRID"]: 89, PANEL_IDS["WIRE"]: 90, PANEL_IDS["LINKS"]: 190, PANEL_IDS["REMOVE"]: 200,
}

_is_updating_by_addon = False; _update_timer = None
def reset_update_flag(): global _is_updating_by_addon, _update_timer; _is_updating_by_addon = False; _update_timer = None; return None
def schedule_update_flag_reset():
    global _update_timer
    if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
    bpy.app.timers.register(reset_update_flag, first_interval=0.01)

# --- Collection Helper ---
def get_or_create_collection(context, name, parent_col=None):
    """ コレクションを取得、なければ作成して親にリンクする関数 """
    col = bpy.data.collections.get(name)
    if not col:
        col = bpy.data.collections.new(name)
        if parent_col:
            if col.name not in parent_col.children:
                parent_col.children.link(col)
        else:
            if col.name not in context.scene.collection.children:
                context.scene.collection.children.link(col)
    return col

def get_master_collection(context):
    return get_or_create_collection(context, MASTER_COLLECTION_NAME)

# --- World Tools ヘルパー関数 ---
def find_node(nodes, node_type, name):
    if node_type == 'OUTPUT_WORLD': return next((n for n in nodes if n.type == 'OUTPUT_WORLD'), None)
    return nodes.get(name)
def find_or_create_node(nodes, node_type, name, location_offset=(0, 0)):
    node = find_node(nodes, node_type, name)
    if node: return node
    new_node = nodes.new(type=node_type); new_node.name = name; new_node.label = name.replace("_", " ")
    output_node = find_node(nodes, 'OUTPUT_WORLD', '');
    if output_node: new_node.location = output_node.location + mathutils.Vector(location_offset)
    return new_node
def get_world_nodes(context, create=True):
    world = context.scene.world
    if not world and create: world = bpy.data.worlds.new("World"); context.scene.world = world
    if not world: return None, None, None
    if create: world.use_nodes = True
    if not world.use_nodes: return world, None, None
    return world, world.node_tree.nodes, world.node_tree.links
def load_hdri_from_path(filepath, context):
    _, nodes, _ = get_world_nodes(context)
    if not nodes: return False
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
    if os.path.exists(filepath):
        try: env_node.image = bpy.data.images.load(filepath, check_existing=True); return True
        except RuntimeError as e: print(f"Error loading image: {e}"); return False
    return False
def update_viewport(context):
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D': 
                        space.shading.type = 'MATERIAL'
                return

def update_background_mode(self, context):
    mode = context.scene.zionad_swt_props.background_mode; world, nodes, links = get_world_nodes(context)
    if not nodes: return
    output_node = find_or_create_node(nodes, 'OUTPUT_WORLD', 'World_Output')
    background_node = find_or_create_node(nodes, 'ShaderNodeBackground', 'Background', (-250, 0))
    sky_node = find_or_create_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture', (-550, 0))
    env_node = find_or_create_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture', (-550, 0))
    mapping_node = find_or_create_node(nodes, 'ShaderNodeMapping', 'Mapping', (-800, 0))
    tex_coord_node = find_or_create_node(nodes, 'ShaderNodeTexCoord', 'Texture_Coordinate', (-1050, 0))
    if background_node.inputs['Color'].is_linked: links.remove(background_node.inputs['Color'].links[0])
    if output_node.inputs['Surface'].is_linked: links.remove(output_node.inputs['Surface'].links[0])
    links.new(background_node.outputs['Background'], output_node.inputs['Surface'])
    if mode == 'SKY': links.new(sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        if not mapping_node.inputs['Vector'].is_linked: links.new(tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        if not env_node.inputs['Vector'].is_linked: links.new(mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        links.new(env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(HDRI_PATHS): load_hdri_from_path(HDRI_PATHS[props.hdri_list_index], context)
    update_viewport(context)

# ======================================================================
# --- カメラ コアロジック・プロパティ ---
# ======================================================================

def update_cam_color(self, context):
    """ 現在のテーマのカメラ枠線色を更新する """
    if self.camera_obj:
        context.preferences.themes[0].view_3d.camera = self.camera_color

def update_grid_color_cb(self, context):
    """ 現在のテーマのグリッド色を即時更新する """
    context.preferences.themes[0].view_3d.grid = self.grid_color

def update_wire_color_cb(self, context):
    """ 現在のテーマのワイヤー色を即時更新する """
    context.preferences.themes[0].view_3d.wire = self.wire_color
    context.preferences.themes[0].view_3d.object_active = self.wire_color

class ThemeGridProperties(PropertyGroup):
    grid_color: FloatVectorProperty(name="Grid Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.545, 0.322, 0.322, 1.0), update=update_grid_color_cb)
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in GRID_PRESETS], update=lambda self, context: SFC_OT_GridApplyColor.update_preset(self, context))

class ThemeWireProperties(PropertyGroup):
    wire_color: FloatVectorProperty(name="Wire Color", subtype='COLOR', size=3, min=0.0, max=1.0, default=(0.51, 1.0, 0.75), update=update_wire_color_cb)
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))

class TargetProperty(PropertyGroup): name: StringProperty()

def update_viewport_cam(self, context):
    global _is_updating_by_addon
    if _is_updating_by_addon: return
    vp_loc = mathutils.Vector(self.viewport_location)
    vp_tgt = mathutils.Vector(self.viewport_target)
    direction = vp_tgt - vp_loc
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    
    for area in context.screen.areas:
        if area.type == 'VIEW_3D':
            for space in area.spaces:
                if space.type == 'VIEW_3D':
                    rv3d = space.region_3d
                    if rv3d:
                        _is_updating_by_addon = True
                        if rv3d.view_perspective == 'CAMERA':
                            rv3d.view_perspective = 'PERSP'
                        rv3d.view_location = vp_tgt
                        rv3d.view_rotation = rot_quat
                        rv3d.view_distance = direction.length
                        _is_updating_by_addon = False
                        break

class SurfaceCameraProperties(PropertyGroup):
    camera_obj: PointerProperty(name="操作カメラ", type=bpy.types.Object, poll=lambda self, obj: obj.type == 'CAMERA', update=lambda s,c: update_surface_camera(s,c))
    
    show_init_settings: BoolProperty(name="初期値設定を表示", default=False)
    
    cam1_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam1_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 100.0, 0.0), subtype='XYZ')
    cam2_init_loc: FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), subtype='XYZ')
    cam2_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    cam3_init_loc: FloatVectorProperty(name="位置", default=(0.0, 0.0, 100.0), subtype='XYZ')
    cam3_init_tgt: FloatVectorProperty(name="注視", default=(0.0, 0.0, 0.0), subtype='XYZ')
    
    target_location: FloatVectorProperty(name="固定注視点", default=(0.0, 100.0, 0.0), subtype='XYZ', update=lambda s,c: update_surface_camera(s,c))
    offset_yaw: FloatProperty(name="Yaw", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    offset_pitch: FloatProperty(name="Pitch", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    offset_roll: FloatProperty(name="Roll", subtype='ANGLE', default=0, update=lambda s,c: update_surface_camera(s,c))
    
    viewport_location: FloatVectorProperty(name="視座位置", default=(0.0, -10.0, 5.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
    viewport_target: FloatVectorProperty(name="注視点", default=(0.0, 0.0, 0.0), subtype='XYZ', update=lambda s,c: update_viewport_cam(s,c))
    
    is_updating_settings: BoolProperty(default=False, options={'HIDDEN'})
    lens_focal_length: FloatProperty(name="焦点距離 (mm)", default=50.0, min=1.0, max=1000.0, unit='LENGTH', update=lambda s,c: update_surface_camera(s,c))
    clip_start: FloatProperty(name="クリップ開始", default=0.1, min=0.001, update=lambda s,c: update_surface_camera(s,c))
    clip_end: FloatProperty(name="クリップ終了", default=1000.0, min=1.0, update=lambda s,c: update_surface_camera(s,c))
    
    info_horizontal_fov: StringProperty(name="水平視野角")
    
    camera_color: FloatVectorProperty(
        name="カメラ枠線 色", 
        subtype='COLOR', size=3, min=0.0, max=1.0, 
        default=(0.0, 1.0, 1.0), 
        update=lambda self, context: update_cam_color(self, context)
    )

class ZIONAD_SWT_Properties(PropertyGroup):
    background_mode: EnumProperty(name="Background Mode", items=[('HDRI', "HDRI", ""), ('SKY', "Sky", "")], default='HDRI', update=update_background_mode)
    hdri_list_index: IntProperty(name="Active HDRI Index", default=0, update=update_background_mode)

def calculate_horizontal_fov(focal_length, sensor_width=SENSOR_WIDTH):
    try: return 2 * math.atan(sensor_width / (2 * focal_length)) * (180 / math.pi)
    except: return 0.0
def calculate_focal_length(fov_degrees, sensor_width=SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except: return 50.0
def get_target_location(props):
    return mathutils.Vector(props.target_location)
def update_object_transform(obj, props):
    location = obj.location
    target_location = get_target_location(props)
    direction = target_location - location
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    base_track_quat = direction.to_track_quat('-Z', 'Y')
    offset_euler = mathutils.Euler((props.offset_pitch, props.offset_yaw, props.offset_roll), 'XYZ')
    final_quat = base_track_quat @ offset_euler.to_quaternion()
    obj.rotation_euler = final_quat.to_euler('XYZ')
def update_surface_camera(self, context):
    global _is_updating_by_addon
    if _is_updating_by_addon: return
    _is_updating_by_addon = True
    try:
        props, camera_obj = context.scene.surface_camera_properties, context.scene.surface_camera_properties.camera_obj
        if props.is_updating_settings or not camera_obj: update_info_panel_text(props, context); return
        cam_data = camera_obj.data
        if cam_data: cam_data.sensor_fit, cam_data.lens_unit, cam_data.lens, cam_data.clip_start, cam_data.clip_end = 'HORIZONTAL', 'MILLIMETERS', props.lens_focal_length, props.clip_start, props.clip_end
        update_object_transform(camera_obj, props); update_info_panel_text(props, context)
    finally: schedule_update_flag_reset()

def update_info_panel_text(props, context):
    if not hasattr(context, 'scene') or not props: return
    camera_obj = props.camera_obj
    if not camera_obj: return
    current_fov = calculate_horizontal_fov(props.lens_focal_length)
    props.info_horizontal_fov = f"{current_fov:.1f} °"

def sync_ui_from_manual_transform(props, obj, context):
    global _is_updating_by_addon
    if _is_updating_by_addon: return
    _is_updating_by_addon = True
    try:
        target_location = get_target_location(props); direction = target_location - obj.location
        if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
        base_track_quat, final_quat = direction.to_track_quat('-Z', 'Y'), obj.matrix_world.to_quaternion()
        offset_quat = base_track_quat.inverted() @ final_quat; offset_euler = offset_quat.to_euler('XYZ')
        props.offset_pitch, props.offset_yaw, props.offset_roll = offset_euler.x, offset_euler.y, offset_euler.z
    finally: _is_updating_by_addon = False
    update_info_panel_text(props, context)

@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
    if _is_updating_by_addon: return
    context = bpy.context
    if not (hasattr(context, 'scene') and context.scene): return
    sfc_props = context.scene.surface_camera_properties
    for update in depsgraph.updates:
        if not update.is_updated_transform: continue
        obj_id = update.id.original
        if sfc_props.camera_obj and obj_id == sfc_props.camera_obj: sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, context); return

# --- オペレーター ---

def set_initial_camera_transform(obj, loc, tgt):
    loc_vec = mathutils.Vector(loc)
    tgt_vec = mathutils.Vector(tgt)
    direction = tgt_vec - loc_vec
    if direction.length < 0.0001: direction = mathutils.Vector((0, -1, 0))
    rot_quat = direction.to_track_quat('-Z', 'Y')
    obj.location = loc_vec
    obj.rotation_euler = rot_quat.to_euler('XYZ')

class SFC_OT_CreateThreeCameras(Operator):
    bl_idname = f"{PREFIX}.create_three_cameras"
    bl_label = "3つのカメラを生成・初期化"
    
    def execute(self, context):
        master_col = get_master_collection(context)
        col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, master_col)
            
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            name = f"Fixed_Cam_{idx}"
            cam_obj = bpy.data.objects.get(name)
            if not cam_obj:
                cam_data = bpy.data.cameras.new(name=name)
                cam_obj = bpy.data.objects.new(name, cam_data)
                col.objects.link(cam_obj)
                if cam_obj.name in context.scene.collection.objects:
                    context.scene.collection.objects.unlink(cam_obj)
            set_initial_camera_transform(cam_obj, loc, tgt)
            
        op_func = getattr(getattr(bpy.ops, PREFIX), "switch_camera")
        op_func(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました")
        return {'FINISHED'}

class SFC_OT_ResetThreeCameras(Operator):
    bl_idname = f"{PREFIX}.reset_three_cameras"
    bl_label = "カメラを初期値に一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        configs = [
            (1, props.cam1_init_loc, props.cam1_init_tgt),
            (2, props.cam2_init_loc, props.cam2_init_tgt),
            (3, props.cam3_init_loc, props.cam3_init_tgt),
        ]
        
        for idx, loc, tgt in configs:
            cam_obj = bpy.data.objects.get(f"Fixed_Cam_{idx}")
            if cam_obj:
                set_initial_camera_transform(cam_obj, loc, tgt)
                if props.camera_obj == cam_obj:
                    props.is_updating_settings = True
                    props.target_location = tgt
                    props.offset_yaw = 0.0
                    props.offset_pitch = 0.0
                    props.offset_roll = 0.0
                    props.is_updating_settings = False
                    
        self.report({'INFO'}, "カメラを初期値にリセットしました")
        return {'FINISHED'}

class SFC_OT_ResetViewportCam(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_cam"
    bl_label = "架空カメラを一括リセット"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        props.viewport_location = (0.0, -10.0, 5.0)
        props.viewport_target = (0.0, 0.0, 0.0)
        self.report({'INFO'}, "架空カメラをリセットしました")
        return {'FINISHED'}

class SFC_OT_CopyViewportInfo(Operator):
    bl_idname = f"{PREFIX}.copy_viewport_info"
    bl_label = "視座・注視点情報をコピー"
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc = props.viewport_location
        tgt = props.viewport_target
        
        fmt = ".2f"
        loc_str = f"({loc.x:{fmt}}, {loc.y:{fmt}}, {loc.z:{fmt}})"
        tgt_str = f"({tgt.x:{fmt}}, {tgt.y:{fmt}}, {tgt.z:{fmt}})"
        
        text_to_copy = f"視座位置: {loc_str}\n注視点: {tgt_str}"
        context.window_manager.clipboard = text_to_copy
        self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました")
        return {'FINISHED'}

class SFC_OT_SwitchCamera(Operator):
    bl_idname = f"{PREFIX}.switch_camera"
    bl_label = "カメラを切り替え"
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        name = f"Fixed_Cam_{self.cam_index}"
        cam_obj = bpy.data.objects.get(name)
        
        if not cam_obj:
            self.report({'WARNING'}, f"{name} が見つかりません。先に「生成」ボタンを押してください。")
            return {'CANCELLED'}
            
        props.is_updating_settings = True
        props.camera_obj = cam_obj
        context.scene.camera = cam_obj
        
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.region_3d.view_perspective = 'CAMERA'
        
        context.preferences.themes[0].view_3d.camera = props.camera_color
        
        cam_data = cam_obj.data
        props.lens_focal_length = cam_data.lens
        props.clip_start = cam_data.clip_start
        props.clip_end = cam_data.clip_end
        
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        props.target_location = cam_obj.location + forward_vec
        props.offset_yaw = 0.0
        props.offset_pitch = 0.0
        props.offset_roll = 0.0
        
        props.is_updating_settings = False
        sync_ui_from_manual_transform(props, cam_obj, context)
        return {'FINISHED'}

class SFC_OT_GridApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): props = context.scene.theme_grid_properties; theme = bpy.context.preferences.themes[0]; theme.view_3d.grid = props.grid_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_grid_properties
        props.grid_color = next((p[3] for p in GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{PREFIX}.apply_grid_color")()

class SFC_OT_GridCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_grid_color"; bl_label = "Copy Grid Color"
    def execute(self, context): theme = bpy.context.preferences.themes[0]; color_tuple = tuple(round(c, 3) for c in theme.view_3d.grid); context.window_manager.clipboard = f'("CUSTOM", "Custom", "Custom grid color", {color_tuple}),'; self.report({'INFO'}, "コピーしました"); return {'FINISHED'}

class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        prop_groups = {"ypr": ["offset_yaw", "offset_pitch", "offset_roll"],"aim": ["target_location"],"clip": ["clip_start", "clip_end", "lens_focal_length"],}
        target_names, props_to_reset = {t.name for t in self.targets}, set()
        if "all" in target_names:
            for g in prop_groups.values(): props_to_reset.update(g)
        else:
            for name in target_names: props_to_reset.update(prop_groups.get(name, []))
        props.is_updating_settings = True
        for p in props_to_reset:
            if hasattr(props, p): props.property_unset(p)
        props.is_updating_settings = False; update_surface_camera(props, context); return {'FINISHED'}

class SFC_OT_SetFOV(Operator):
    bl_idname = f"{PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): props = context.scene.surface_camera_properties; props.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}

class SFC_OT_OpenURL(Operator):
    bl_idname = f"{PREFIX}.open_url"; bl_label = "URLを開く"; url: StringProperty(default="")
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

class SFC_OT_RemoveAddon(Operator):
    bl_idname = f"{PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): module_name = __name__.split('.')[0]; bpy.ops.preferences.addon_disable(module=module_name); unregister(); return {'FINISHED'}

class SFC_OT_WireApplyColor(Operator):
    bl_idname = f"{PREFIX}.apply_wire_color"; bl_label = "Apply Wire Color"
    def execute(self, context): props=context.scene.theme_wire_properties; theme=bpy.context.preferences.themes[0]; theme.view_3d.wire=props.wire_color; theme.view_3d.object_active=props.wire_color; return {'FINISHED'}
    @staticmethod
    def update_preset(self, context):
        props = context.scene.theme_wire_properties
        props.wire_color = next((p[3] for p in WIRE_PRESETS if p[0] == props.wire_preset), props.wire_color)
        getattr(bpy.ops, f"{PREFIX}.apply_wire_color")()

class SFC_OT_WireCopyColor(Operator):
    bl_idname = f"{PREFIX}.copy_wire_color"; bl_label = "Copy Wire Color"
    def execute(self, context): theme=bpy.context.preferences.themes[0]; color_tuple=tuple(round(c, 2) for c in theme.view_3d.wire); context.window_manager.clipboard=f'("CUSTOM", "Custom", "Custom wire color", {color_tuple}),'; return {'FINISHED'}

class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{PREFIX}.load_hdri_from_list"; bl_label = "Load HDRI from List"; bl_options = {'REGISTER', 'UNDO'}; hdri_index: IntProperty()
    def execute(self, context):
        props = context.scene.zionad_swt_props
        if 0 <= self.hdri_index < len(HDRI_PATHS):
            props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
        return {'FINISHED'}

class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{PREFIX}.reset_transform"; bl_label = "Reset Transform Value"; bl_options = {'REGISTER', 'UNDO'}; property_to_reset: StringProperty()
    def execute(self, context):
        _, nodes, _ = get_world_nodes(context)
        if not nodes: return {'CANCELLED'}
        mapping_node = find_node(nodes, 'ShaderNodeMapping', 'Mapping')
        if not mapping_node: return {'CANCELLED'}
        if self.property_to_reset == 'Location': mapping_node.inputs['Location'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Rotation': mapping_node.inputs['Rotation'].default_value = (0, 0, 0)
        elif self.property_to_reset == 'Scale': mapping_node.inputs['Scale'].default_value = (1, 1, 1)
        return {'FINISHED'}

# --- UIパネル ---
class SFC_PT_CameraSetupPanel(Panel):
    bl_label = "1. カメラ作成・切り替え"
    bl_idname = PANEL_IDS["SETUP"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["SETUP"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        layout.operator(SFC_OT_CreateThreeCameras.bl_idname, icon='OUTLINER_OB_CAMERA', text="3つのカメラを生成・初期化")
        
        box_init = layout.box()
        box_init.prop(props, "show_init_settings", icon="TRIA_DOWN" if props.show_init_settings else "TRIA_RIGHT")
        if props.show_init_settings:
            col_init = box_init.column(align=True)
            col_init.prop(props, "cam1_init_loc", text="1: 位置"); col_init.prop(props, "cam1_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam2_init_loc", text="2: 位置"); col_init.prop(props, "cam2_init_tgt", text="  注視")
            col_init.separator()
            col_init.prop(props, "cam3_init_loc", text="3: 位置"); col_init.prop(props, "cam3_init_tgt", text="  注視")
            
            box_init.separator()
            box_init.operator(SFC_OT_ResetThreeCameras.bl_idname, icon='LOOP_BACK', text="初期値にリセット")
            
        layout.separator()
        
        box = layout.box()
        box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA')
        row = box.row(align=True)
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 1", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_1")).cam_index = "1"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 2", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_2")).cam_index = "2"
        row.operator(SFC_OT_SwitchCamera.bl_idname, text="Cam 3", depress=(props.camera_obj and props.camera_obj.name=="Fixed_Cam_3")).cam_index = "3"
        
        if props.camera_obj:
            box.label(text=f"操作・描画中: {props.camera_obj.name}", icon='CAMERA_DATA')
        else:
            box.label(text="操作カメラ未選択", icon='ERROR')
            
        box.separator()
        box_color = box.box()
        box_color.prop(props, "camera_color")

class SFC_PT_CameraAimingPanel(Panel):
    bl_label = "2. 専用カメラ視線制御 (位置固定)"
    bl_idname = PANEL_IDS["AIMING"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["AIMING"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties

        box_manual = layout.box()
        box_manual.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
        
        if props.camera_obj:
            box_manual.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
        
        col_aim = box_manual.column(align=True)
        row_aim = col_aim.row(align=True)
        row_aim.label(text="注視点")
        op_aim = row_aim.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_aim.targets.add().name = "aim"
        op_aim.prop_group_name = "camera"
        col_aim.prop(props, "target_location", text="")
        
        box_manual.separator()
        
        col_offset = box_manual.column(align=True)
        row_offset = col_offset.row(align=True)
        row_offset.label(text="視線オフセット (YPR)")
        op_offset = row_offset.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op_offset.targets.add().name = "ypr"
        op_offset.prop_group_name = "camera"
        col_offset.prop(props, "offset_yaw")
        col_offset.prop(props, "offset_pitch")
        col_offset.prop(props, "offset_roll")

class SFC_PT_ViewportCamPanel(Panel):
    bl_label = "3. ビューポート視座コントロール (架空カメラ)"
    bl_idname = PANEL_IDS["VIEWPORT_CAM"]
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = ADDON_CATEGORY_NAME
    bl_order = PANEL_ORDER[PANEL_IDS["VIEWPORT_CAM"]]

    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        box = layout.box()
        box.label(text="透視投影ビューの操作", icon='VIEW3D')
        
        col = box.column(align=True)
        col.prop(props, "viewport_location")
        col.prop(props, "viewport_target")
        
        box.separator()
        box.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="視座位置・注視点をコピー")
        box.operator(SFC_OT_ResetViewportCam.bl_idname, icon='LOOP_BACK', text="視座・注視点を一括リセット")

class SFC_PT_LensPanel(Panel):
    bl_label = "4. レンズ設定"; bl_idname = PANEL_IDS["LENS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LENS"]]
    def draw(self, context):
        layout = self.layout
        props = context.scene.surface_camera_properties
        
        if props.camera_obj and props.camera_obj.data:
            cam_data = props.camera_obj.data
            box_type = layout.box()
            box_type.prop(cam_data, "type", text="投影タイプ (透視/平行)")
            
        box = layout.box()
        col = box.column(align=True)
        row = col.row(align=True)
        row.label(text="レンズとクリップ")
        op = row.operator(f"{PREFIX}.reset_property", text="", icon='LOOP_BACK')
        op.targets.add().name = "clip"
        op.prop_group_name = "camera"
        
        col.prop(props, "lens_focal_length")
        row = col.row(align=True)
        row.label(text="水平視野角:"); row.label(text=props.info_horizontal_fov)
        col.label(text="FOVプリセット:")
        row = col.row(align=True)
        col1, col2 = row.column(align=True), row.column(align=True)
        for i, fov in enumerate(FOV_PRESETS):
            op = (col1 if i % 2 == 0 else col2).operator(f"{PREFIX}.set_fov", text=f"{fov}°")
            op.fov = fov
        col.separator()
        row = col.row(align=True)
        row.prop(props, "clip_start")
        row.prop(props, "clip_end")

class SFC_PT_CameraDisplayPanel(Panel):
    bl_label = "Camera Display & Render"; bl_idname = PANEL_IDS["CAMERA_DISPLAY"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["CAMERA_DISPLAY"]]
    def draw(self, context):
        layout, scene, cam = self.layout, context.scene, context.scene.camera
        box_render = layout.box(); box_render.label(text="Render Engine", icon='SCENE'); box_render.prop(scene.render, "engine", expand=True); layout.separator()
        if not cam or not isinstance(cam.data, bpy.types.Camera): layout.box().label(text="シーンにアクティブなカメラがありません", icon='ERROR'); return
        cam_data = cam.data; overlay = context.space_data.overlay if context.space_data and hasattr(context.space_data, 'overlay') else None
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA')
        box_passepartout = layout.box(); box_passepartout.label(text="Passepartout", icon='MOD_MASK'); col_passepartout = box_passepartout.column(align=True); col_passepartout.prop(cam_data, "show_passepartout", text="Enable"); row_passepartout = col_passepartout.row(); row_passepartout.enabled = cam_data.show_passepartout; row_passepartout.prop(cam_data, "passepartout_alpha", text="Opacity")
        layout.separator(); box_display = layout.box(); box_display.label(text="Viewport Display", icon='OVERLAY')
        if not overlay: return
        box_display.prop(overlay, "show_overlays", text="Viewport Overlays"); col_overlay_options = box_display.column(); col_overlay_options.enabled = overlay.show_overlays; col_overlay_options.prop(overlay, "show_extras", text="Extras")
        col_details = col_overlay_options.column(); col_details.enabled = overlay.show_extras; col_details.prop(overlay, "show_text", text="Text Info"); col_details.prop(cam_data, "show_name", text="Name"); col_details.prop(cam_data, "show_limits", text="Limits")

class ZIONAD_SWT_PT_WorldControlPanel(Panel):
    bl_label = "World Control"; bl_idname = PANEL_IDS["WORLD_CONTROL"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WORLD_CONTROL"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout, scene, props = self.layout, context.scene, context.scene.zionad_swt_props; world, nodes, _ = get_world_nodes(context, create=False)
        if not world or not world.use_nodes or not nodes: return
        box_mode = layout.box(); box_mode.label(text="Background Mode", icon='WORLD'); box_mode.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            box_env = layout.box(); box_env.label(text="Environment Texture (HDRI)", icon='IMAGE_DATA'); col_list = box_env.column(align=True)
            for i, path in enumerate(HDRI_PATHS): op = col_list.operator(f"{PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)); op.hdri_index = i
            box_env.separator(); env_node = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if env_node: box_env.template_ID(env_node, "image", open="image.open", text="Select HDRI")
        elif props.background_mode == 'SKY':
            box_sky = layout.box(); sky_node = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sky_node: box_sky.prop(sky_node, "sky_type", text="Sky Type")

class SFC_PT_GridPanel(Panel):
    bl_label = "Grid Color"; bl_idname = PANEL_IDS["GRID"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["GRID"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_grid_properties; layout.prop(props, "grid_preset"); layout.prop(props, "grid_color"); layout.operator(f"{PREFIX}.apply_grid_color", text="Apply Grid Color")

class SFC_PT_WirePanel(Panel):
    bl_label = "Wire Color"; bl_idname = PANEL_IDS["WIRE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["WIRE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): layout, props = self.layout, context.scene.theme_wire_properties; layout.prop(props, "wire_preset"); layout.prop(props, "wire_color"); layout.operator(f"{PREFIX}.apply_wire_color", text="Apply Wire Color")

class SFC_PT_LinksPanel(Panel):
    bl_label = "リンク"; bl_idname = PANEL_IDS["LINKS"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["LINKS"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        
        box1 = layout.box()
        box1.label(text="ドキュメント", icon='HELP')
        for link in NEW_DOC_LINKS:
            op = box1.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]
            
        box2 = layout.box()
        box2.label(text="ソーシャル", icon='WORLD_DATA')
        for link in SOCIAL_LINKS:
            op = box2.operator(f"{PREFIX}.open_url", text=link["label"], icon='URL')
            op.url = link["url"]

class SFC_PT_RemovePanel(Panel):
    bl_label = "アドオン削除"; bl_idname = PANEL_IDS["REMOVE"]; bl_space_type = 'VIEW_3D'; bl_region_type = 'UI'; bl_category = ADDON_CATEGORY_NAME; bl_order = PANEL_ORDER[PANEL_IDS["REMOVE"]]; bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context): self.layout.operator(f"{PREFIX}.remove_addon", text="このアドオンを解除", icon='CANCEL')

# --- World Tools 初期化 ---
def initial_setup():
    context = bpy.context
    
    # 全ての3Dビューポートでサイドパネル(Nパネル)を開き、ShadingをMATERIALに変更
    for window in context.window_manager.windows:
        for area in window.screen.areas:
            if area.type == 'VIEW_3D':
                area.show_region_ui = True
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        space.shading.type = 'MATERIAL'
    
    if context.scene.world and context.scene.world.use_nodes:
        props = context.scene.zionad_swt_props
        nodes = context.scene.world.node_tree.nodes
        background_node = find_node(nodes, 'ShaderNodeBackground', 'Background')
        if background_node and background_node.inputs['Color'].is_linked:
            source_node = background_node.inputs['Color'].links[0].from_node
            if source_node.type == 'TEX_SKY': props.background_mode = 'SKY'
            else: props.background_mode = 'HDRI'
        update_background_mode(props, context)
    return None

# --- 登録/解除 ---
classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, 
    SFC_OT_CreateThreeCameras, SFC_OT_ResetThreeCameras, SFC_OT_ResetViewportCam, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_OpenURL, SFC_OT_RemoveAddon,
    ZIONAD_SWT_OT_LoadHdriFromList, ZIONAD_SWT_OT_ResetTransform,
    SFC_PT_CameraSetupPanel, SFC_PT_CameraAimingPanel, SFC_PT_ViewportCamPanel, SFC_PT_LensPanel, SFC_PT_CameraDisplayPanel,
    ZIONAD_SWT_PT_WorldControlPanel, SFC_PT_GridPanel, SFC_PT_WirePanel, SFC_PT_LinksPanel,
    SFC_PT_RemovePanel,
)

_registered_classes = []
def register():
    global _registered_classes; _registered_classes.clear()
    for cls in classes:
        try: bpy.utils.register_class(cls); _registered_classes.append(cls)
        except Exception as e: pass
    bpy.types.Scene.surface_camera_properties = PointerProperty(type=SurfaceCameraProperties)
    bpy.types.Scene.theme_grid_properties = PointerProperty(type=ThemeGridProperties)
    bpy.types.Scene.theme_wire_properties = PointerProperty(type=ThemeWireProperties)
    bpy.types.Scene.zionad_swt_props = PointerProperty(type=ZIONAD_SWT_Properties)
    if on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.append(on_depsgraph_update)
    if not bpy.app.timers.is_registered(initial_setup): bpy.app.timers.register(initial_setup, first_interval=0.1)

def unregister():
    global _registered_classes
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
    if _update_timer and bpy.app.timers.is_registered(reset_update_flag): bpy.app.timers.unregister(reset_update_flag)
    if bpy.app.timers.is_registered(initial_setup): bpy.app.timers.unregister(initial_setup)
    for prop_name in ['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if hasattr(bpy.types.Scene, prop_name):
            try: delattr(bpy.types.Scene, prop_name)
            except: pass
    for cls in reversed(classes):
        if hasattr(bpy.utils, 'unregister_class') and cls in _registered_classes:
            try: bpy.utils.unregister_class(cls)
            except: pass
    _registered_classes.clear()

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