blender Million 2026

元型 時空図 光の予算配分 20260329








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

# ======================================================================
# --- 定数管理 / CONFIG ---
# ======================================================================

class CONFIG:
    PREFIX = "unit_circle_cam"
    MASTER_COLLECTION = "Cam three"
    CAMERA_COLLECTION = "Cam"
    VP_COLLECTION = "VP_Objects"
    SAVED_COLLECTION = "Saved_Objects"
    
    SENSOR_WIDTH = 36.0
    FOV_PRESETS =[1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
    
    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))]
    
    NEW_DOC_LINKS =[
        {"label": "時空図 光の予算配分 20260329", "url": "<https://www.notion.so/20260329-332f5dacaf438016b8f9cff480994ec1>"},
        {"label": "カメラ3台 ジグザク 20260328b", "url": "<https://www.notion.so/20260328b-331f5dacaf4380b9abeed323cd5621a4>"},
        {"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
    ]
    SOCIAL_LINKS =[
        {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
    ]

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

bl_info = {
    "name": "zionad 521[Unit Circle Cam]",
    "author": "zionadchat",
    "version": (40, 0, 0),
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "【V40】3つの専用カメラ、透明球体と交差円生成 (表裏・色分離機能搭載、UI縦並び)",
    "category": "Cam three 元型", 
}
ADDON_CATEGORY_NAME = bl_info["category"]

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

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

# ======================================================================
# --- タイマー管理 & ロック機構 (安定化) ---
# ======================================================================

TIMER_REGISTRY = {}

def safe_register_timer(func, delay=0.01):
    if func in TIMER_REGISTRY: return
    def wrapper():
        try: func()
        finally: TIMER_REGISTRY.pop(func, None)
        return None
    TIMER_REGISTRY[func] = wrapper
    bpy.app.timers.register(wrapper, first_interval=delay)

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

def set_update_lock(scene, state: bool):
    if scene: scene["_sfc_updating"] = bool(state)

def schedule_update_lock_reset():
    for scene in bpy.data.scenes:
        if "_sfc_updating" in scene: scene["_sfc_updating"] = False

# ======================================================================
# --- 汎用ヘルパー関数 (安全化) ---
# ======================================================================

def safe_remove_object(obj):
    if not obj: return
    if obj.users > 1:
        for col in list(obj.users_collection): col.objects.unlink(obj)
    else: bpy.data.objects.remove(obj, do_unlink=True)

def safe_link(links, out_socket, in_socket):
    if in_socket.is_linked: links.remove(in_socket.links[0])
    links.new(out_socket, in_socket)

def get_or_copy_material(mat, suffix):
    name = f"{mat.name}_{suffix}"
    existing = bpy.data.materials.get(name)
    if existing: return existing
    new_mat = mat.copy()
    new_mat.name = name
    return new_mat

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, CONFIG.MASTER_COLLECTION)

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_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))
    
    safe_link(links, background_node.outputs['Background'], output_node.inputs['Surface'])
    
    if mode == 'SKY': safe_link(links, sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        safe_link(links, tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        safe_link(links, mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        safe_link(links, env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(CONFIG.HDRI_PATHS): load_hdri_from_path(CONFIG.HDRI_PATHS[props.hdri_list_index], context)
            
    wm = bpy.context.window_manager
    if wm:
        for window in wm.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'

# ======================================================================
# --- オブジェクト生成関数 (球体・リング) ---
# ======================================================================

def get_or_create_color_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
    bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None) if mat.use_nodes else None
    if bsdf:
        if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
    mat.blend_method = 'BLEND'
    return mat

def get_or_create_front_back_material(name, color_front, color_back):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
        
    if mat.use_nodes:
        nodes, links = mat.node_tree.nodes, mat.node_tree.links
        bsdf = next((n for n in nodes if n.type == 'BSDF_PRINCIPLED'), None)
        
        if bsdf:
            geom = nodes.get("Geometry") or nodes.new('ShaderNodeNewGeometry')
            geom.name = "Geometry"
            geom.location = (bsdf.location.x - 400, bsdf.location.y + 200)
            
            mix_rgb = nodes.get("Mix_Color") or nodes.new('ShaderNodeMix')
            mix_rgb.name = "Mix_Color"
            mix_rgb.data_type = 'RGBA'
            mix_rgb.blend_type = 'MIX'
            mix_rgb.location = (bsdf.location.x - 200, bsdf.location.y + 200)
            
            mix_alpha = nodes.get("Mix_Alpha") or nodes.new('ShaderNodeMix')
            mix_alpha.name = "Mix_Alpha"
            mix_alpha.data_type = 'FLOAT'
            mix_alpha.location = (bsdf.location.x - 200, bsdf.location.y - 100)
            
            safe_link(links, geom.outputs['Backfacing'], mix_rgb.inputs['Factor'])
            safe_link(links, mix_rgb.outputs['Result'], bsdf.inputs['Base Color'])
            
            safe_link(links, geom.outputs['Backfacing'], mix_alpha.inputs['Factor'])
            safe_link(links, mix_alpha.outputs['Result'], bsdf.inputs['Alpha'])
            
            mix_rgb.inputs['A'].default_value = color_front
            mix_rgb.inputs['B'].default_value = color_back
            
            mix_alpha.inputs['A'].default_value = color_front[3]
            mix_alpha.inputs['B'].default_value = color_back[3]
            
    mat.blend_method = 'BLEND'
    return mat

def create_sphere_object(name, collection, loc, radius, mat):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
    bm.to_mesh(me); bm.free()
    for poly in me.polygons: poly.use_smooth = True
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    if mat: obj.data.materials.append(mat)
    collection.objects.link(obj)
    return obj

def create_split_spheres(name_prefix, collection, loc, radius, mat1, mat2, plane):
    if plane == 'XY': norm, co = mathutils.Vector((0, 0, 1)), mathutils.Vector((0, 0, -loc.z))
    elif plane == 'YZ': norm, co = mathutils.Vector((1, 0, 0)), mathutils.Vector((-loc.x, 0, 0))
    else: norm, co = mathutils.Vector((0, 1, 0)), mathutils.Vector((0, -loc.y, 0))
        
    objs, mats = [],[mat1, mat2]
    
    for i, clear_in in enumerate([True, False]):
        me = bpy.data.meshes.new(f"{name_prefix}_{i+1}"); bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
        bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=norm, clear_inner=clear_in, clear_outer=not clear_in)
        bm.to_mesh(me); bm.free()
        
        if len(me.vertices) > 0:
            for poly in me.polygons: poly.use_smooth = True
            obj = bpy.data.objects.new(f"{name_prefix}_{i+1}", me)
            obj.location = loc
            if mats[i]: obj.data.materials.append(mats[i])
            collection.objects.link(obj); objs.append(obj)
        else:
            bpy.data.meshes.remove(me)
    return objs

def create_ring_object(name, collection, loc, rot, major_radius, minor_radius, mat):
    me = bpy.data.meshes.new(name); bm = bmesh.new()
    r_outer, r_inner, segments = major_radius + minor_radius, max(0.001, major_radius - minor_radius), 64
    verts_outer, verts_inner = [],[]
    
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        c, s = math.cos(angle), math.sin(angle)
        verts_outer.append(bm.verts.new((r_outer * c, r_outer * s, 0.0)))
        verts_inner.append(bm.verts.new((r_inner * c, r_inner * s, 0.0)))
        
    for i in range(segments):
        ni = (i + 1) % segments
        bm.faces.new((verts_outer[i], verts_outer[ni], verts_inner[ni], verts_inner[i]))
        
    bm.to_mesh(me); bm.free()
    for poly in me.polygons: poly.use_smooth = True
        
    obj = bpy.data.objects.new(name, me)
    obj.location, obj.rotation_euler = loc, rot
    if mat: obj.data.materials.append(mat)
    
    mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
    mod.thickness, mod.offset = minor_radius * 2, 0.0
    collection.objects.link(obj)
    return obj

def update_group_visibility(prefix, hide):
    col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
    if not col: return
    for obj in col.objects:
        if obj.name.startswith(prefix): obj.hide_viewport = obj.hide_render = hide

def update_group_visibility_exact(name, hide):
    col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
    if not col: return
    obj = col.objects.get(name)
    if obj: obj.hide_viewport = obj.hide_render = hide

def update_vis_vp_sphere_1(self, context): update_group_visibility_exact("VP_Sphere_1", not self.vis_vp_sphere_1)
def update_vis_vp_sphere_2(self, context): update_group_visibility_exact("VP_Sphere_2", not self.vis_vp_sphere_2)
def update_vis_vp_circles(self, context): update_group_visibility("VP_Circle", not self.vis_vp_circles)

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

def update_cam_color(self, context):
    if self.camera_obj: context.preferences.themes[0].view_3d.camera = self.camera_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=lambda self, context: setattr(context.preferences.themes[0].view_3d, 'grid', self.grid_color))
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.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=lambda self, context:[setattr(context.preferences.themes[0].view_3d, 'wire', self.wire_color), setattr(context.preferences.themes[0].view_3d, 'object_active', self.wire_color)])
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))

class TargetProperty(PropertyGroup): name: StringProperty()

def _do_update_viewport_cam():
    wm = bpy.context.window_manager
    if not wm: return
    for window in wm.windows:
        scene = window.scene
        if not scene: continue
        props = scene.surface_camera_properties
        vp_loc, vp_tgt = mathutils.Vector(props.viewport_location), 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 area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D' and space.region_3d:
                        set_update_lock(scene, True)
                        try:
                            if space.region_3d.view_perspective == 'CAMERA': space.region_3d.view_perspective = 'PERSP'
                            space.region_3d.view_location = vp_tgt
                            space.region_3d.view_rotation = rot_quat
                            space.region_3d.view_distance = direction.length
                        finally: safe_register_timer(schedule_update_lock_reset, 0.01)

def safe_update_viewport_cam(self, context):
    if not is_updating(context.scene): safe_register_timer(_do_update_viewport_cam, 0.01)

def _do_update_surface_camera():
    wm = bpy.context.window_manager
    if not wm: return
    for window in wm.windows:
        scene = window.scene
        if not scene: continue
        props, camera_obj = scene.surface_camera_properties, scene.surface_camera_properties.camera_obj
        set_update_lock(scene, True)
        try:
            if props.is_updating_settings or not camera_obj: 
                update_info_panel_text(props); continue
            if camera_obj.data: 
                camera_obj.data.sensor_fit = 'HORIZONTAL'
                camera_obj.data.lens_unit = 'MILLIMETERS'
                camera_obj.data.lens, camera_obj.data.clip_start, camera_obj.data.clip_end = props.lens_focal_length, props.clip_start, props.clip_end
            update_object_transform(camera_obj, props)
            update_info_panel_text(props)
        finally: safe_register_timer(schedule_update_lock_reset, 0.01)

def safe_update_surface_camera(self, context):
    if not is_updating(context.scene): safe_register_timer(_do_update_surface_camera, 0.01)

def update_sphere_colors(self, context):
    if getattr(self, "sync_sphere_colors", False):
        c = self.sphere1_color_front
        self.sphere1_color_back = c
        self.sphere2_color_front = c
        self.sphere2_color_back = c

def update_sync_checkbox(self, context):
    if self.sync_sphere_colors: update_sphere_colors(self, context)

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, 1.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, 20.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)
    
    vis_vp_sphere_1: BoolProperty(name="球体1", default=True, update=update_vis_vp_sphere_1)
    vis_vp_sphere_2: BoolProperty(name="球体2", default=True, update=update_vis_vp_sphere_2)
    vis_vp_circles: BoolProperty(name="交差円", default=True, update=update_vis_vp_circles)
    
    intersect_plane: EnumProperty(name="交差平面", items=[('XY', "XY平面 (Z=0)", ""), ('YZ', "YZ平面 (X=0)", ""), ('ZX', "ZX平面 (Y=0)", "")], default='ZX')
    sphere_mode: EnumProperty(name="サイズ指定モード", items=[('RADIUS', "球の半径を指定", ""), ('CIRCLE', "交差円の半径を指定", "")], default='RADIUS')
    sphere_radius: FloatProperty(name="球の半径", default=10.0, min=0.001)
    intersect_circle_radius: FloatProperty(name="交差円の半径", default=10.0, min=0.001)
    
    sync_sphere_colors: BoolProperty(name="球体1の表色で全て統一", default=False, update=update_sync_checkbox)
    sphere1_color_front: FloatVectorProperty(name="球体1 表色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.5, 0.8, 0.2), update=update_sphere_colors)
    sphere1_color_back: FloatVectorProperty(name="球体1 裏色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.6, 0.9, 0.2))
    sphere2_color_front: FloatVectorProperty(name="球体2 表色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.5, 0.1, 0.2))
    sphere2_color_back: FloatVectorProperty(name="球体2 裏色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.9, 0.6, 0.2, 0.2))
    
    circle_color: FloatVectorProperty(name="交差円 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.2, 0.1, 0.8))
    circle_thickness: FloatProperty(name="交差円 太さ", default=0.05, min=0.001)
    
    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=update_cam_color)

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=CONFIG.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=CONFIG.SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except: return 50.0

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

def update_info_panel_text(props):
    if props and props.camera_obj: props.info_horizontal_fov = f"{calculate_horizontal_fov(props.lens_focal_length):.1f} °"

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

@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
    if is_updating(scene): return
    sfc_props = scene.surface_camera_properties
    if not sfc_props.camera_obj: return 
    
    if any(update.is_updated_transform and update.id.original == sfc_props.camera_obj for update in depsgraph.updates):
        sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, scene)

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

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

class SFC_OT_CreateThreeCameras(Operator):
    bl_idname = f"{CONFIG.PREFIX}.create_three_cameras"; bl_label = "3つのカメラを生成・初期化"
    def execute(self, context):
        col, props = get_or_create_collection(context, CONFIG.CAMERA_COLLECTION, get_master_collection(context)), context.scene.surface_camera_properties
        for idx, loc, tgt in[(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)]:
            name = f"Fixed_Cam_{idx}"; cam_obj = bpy.data.objects.get(name)
            if cam_obj and cam_obj.type != 'CAMERA': cam_obj.name += "_old"; cam_obj = None
            if not cam_obj: cam_obj = bpy.data.objects.new(name, bpy.data.cameras.new(name=name))
            if cam_obj.name not in col.objects: col.objects.link(cam_obj)
            for c in list(cam_obj.users_collection):
                if c != col: c.objects.unlink(cam_obj)
            set_initial_camera_transform(cam_obj, loc, tgt)
        getattr(getattr(bpy.ops, CONFIG.PREFIX), "switch_camera")(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました"); return {'FINISHED'}

class SFC_OT_GetCameraInitInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.get_camera_init_info"; bl_label = "カメラの現在位置・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj: return {'CANCELLED'}
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        setattr(props, f"cam{self.cam_index}_init_loc", cam_obj.location.copy())
        setattr(props, f"cam{self.cam_index}_init_tgt", cam_obj.location.copy() + forward_vec)
        return {'FINISHED'}

class SFC_OT_ResetCameraInit(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_camera_init"; bl_label = "カメラを初期値にリセット"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc, tgt = getattr(props, f"cam{self.cam_index}_init_loc"), getattr(props, f"cam{self.cam_index}_init_tgt")
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        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, props.offset_yaw, props.offset_pitch, props.offset_roll = tgt, 0.0, 0.0, 0.0
                props.is_updating_settings = False
        return {'FINISHED'}

class SFC_OT_CopyCameraInitInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_camera_init_info"; bl_label = "初期値情報をコピー"; cam_index: StringProperty()
    def execute(self, context):
        loc, tgt = getattr(context.scene.surface_camera_properties, f"cam{self.cam_index}_init_loc"), getattr(context.scene.surface_camera_properties, f"cam{self.cam_index}_init_tgt")
        context.window_manager.clipboard = f"Cam {self.cam_index}: 位置 ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f}) / 注視 ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        return {'FINISHED'}

class SFC_OT_SetViewportToCamera(Operator):
    bl_idname = f"{CONFIG.PREFIX}.set_viewport_to_camera"; bl_label = "指定カメラの視座を透視投影に適用"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj: return {'CANCELLED'}
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        props.viewport_location, props.viewport_target = cam_obj.location.copy(), cam_obj.location + forward_vec
        return {'FINISHED'}

class SFC_OT_ResetViewportLocation(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_viewport_location"; bl_label = "視座位置をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context): context.scene.surface_camera_properties.viewport_location = (0.0, -10.0, 5.0); return {'FINISHED'}

class SFC_OT_ResetViewportTarget(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_viewport_target"; bl_label = "注視点をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context): context.scene.surface_camera_properties.viewport_target = (0.0, 0.0, 0.0); return {'FINISHED'}

class SFC_OT_CopyViewportInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_viewport_info"; bl_label = "視座・注視点情報をコピー"
    def execute(self, context):
        loc, tgt = context.scene.surface_camera_properties.viewport_location, context.scene.surface_camera_properties.viewport_target
        context.window_manager.clipboard = f"視座位置: ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f})\n注視点: ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        return {'FINISHED'}

class SFC_OT_GetViewportInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.get_viewport_info"; bl_label = "現在の視座・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = context.scene.surface_camera_properties
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D' and space.region_3d:
                        set_update_lock(context.scene, True)
                        try: props.viewport_location, props.viewport_target = space.region_3d.view_matrix.inverted().translation, space.region_3d.view_location
                        finally: safe_register_timer(schedule_update_lock_reset, 0.01)
                        return {'FINISHED'}
        return {'CANCELLED'}

class SFC_OT_CopySphereInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_sphere_info"; bl_label = "球体・円情報をコピー"
    def execute(self, context):
        props = context.scene.surface_camera_properties; vp_loc, plane = props.viewport_location, props.intersect_plane
        d, plane_str = (abs(vp_loc.z), "XY平面 (Z=0)") if plane == 'XY' else (abs(vp_loc.x), "YZ平面 (X=0)") if plane == 'YZ' else (abs(vp_loc.y), "ZX平面 (Y=0)")
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        context.window_manager.clipboard = f"視座位置: ({vp_loc.x:.3f}, {vp_loc.y:.3f}, {vp_loc.z:.3f})\n交差平面: {plane_str}\n平面までの距離: {d:.3f}\n球の半径: {R:.3f}\n交差円の半径: {r_circ:.3f}"
        return {'FINISHED'}

class SFC_OT_GenerateViewportSphere(Operator):
    bl_idname = f"{CONFIG.PREFIX}.generate_viewport_sphere"; bl_label = "透明球体と交差円を生成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = context.scene.surface_camera_properties
        col = get_or_create_collection(context, CONFIG.VP_COLLECTION, get_master_collection(context))
        
        for obj in[o for o in col.objects if o.name.startswith("VP_Sphere") or o.name.startswith("VP_Circle")]: safe_remove_object(obj)
            
        vp_loc, plane = mathutils.Vector(props.viewport_location), props.intersect_plane
        d, circle_loc, circle_rot = (abs(vp_loc.z), mathutils.Vector((vp_loc.x, vp_loc.y, 0.0)), mathutils.Euler((0.0, 0.0, 0.0), 'XYZ')) if plane == 'XY' else (abs(vp_loc.x), mathutils.Vector((0.0, vp_loc.y, vp_loc.z)), mathutils.Euler((0.0, math.pi/2, 0.0), 'XYZ')) if plane == 'YZ' else (abs(vp_loc.y), mathutils.Vector((vp_loc.x, 0.0, vp_loc.z)), mathutils.Euler((math.pi/2, 0.0, 0.0), 'XYZ'))
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        
        mat1 = get_or_create_front_back_material("Mat_VP_Sphere_1", props.sphere1_color_front, props.sphere1_color_back)
        mat2 = get_or_create_front_back_material("Mat_VP_Sphere_2", props.sphere2_color_front, props.sphere2_color_back)
        mat_c = get_or_create_color_material("Mat_VP_Circle", props.circle_color)
        
        if r_circ > 0.001:
            create_split_spheres("VP_Sphere", col, vp_loc, R, mat1, mat2, plane)
            create_ring_object("VP_Circle", col, circle_loc, circle_rot, r_circ, props.circle_thickness, mat_c)
        else:
            create_sphere_object("VP_Sphere_1", col, vp_loc, R, mat1)
            
        update_group_visibility_exact("VP_Sphere_1", not props.vis_vp_sphere_1)
        update_group_visibility_exact("VP_Sphere_2", not props.vis_vp_sphere_2)
        update_group_visibility("VP_Circle", not props.vis_vp_circles)
        return {'FINISHED'}

class SFC_OT_DetachSpheres(Operator):
    bl_idname = f"{CONFIG.PREFIX}.detach_spheres"; bl_label = "アドオンから切り離して残す"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
        if not col: return {'CANCELLED'}
        saved_col = get_or_create_collection(context, CONFIG.SAVED_COLLECTION, get_master_collection(context))
        objs_to_detach =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        if not objs_to_detach: return {'CANCELLED'}
        
        suffix = str(uuid.uuid4())[:6]
        for obj in objs_to_detach:
            obj.name = obj.name.replace("VP_", f"Saved_{suffix}_")
            if obj.data.materials:
                new_mats =[get_or_copy_material(mat, suffix) for mat in obj.data.materials if mat]
                obj.data.materials.clear()
                for nm in new_mats: obj.data.materials.append(nm)
            saved_col.objects.link(obj)
            col.objects.unlink(obj)
        self.report({'INFO'}, f"{len(objs_to_detach)} 個のオブジェクトを保存しました")
        return {'FINISHED'}

class SFC_OT_SwitchCamera(Operator):
    bl_idname = f"{CONFIG.PREFIX}.switch_camera"; bl_label = "カメラを切り替え"; cam_index: StringProperty()
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj or cam_obj.type != 'CAMERA': return {'CANCELLED'}
            
        props.is_updating_settings, props.camera_obj, context.scene.camera = True, cam_obj, 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
        props.lens_focal_length, props.clip_start, props.clip_end = cam_obj.data.lens, cam_obj.data.clip_start, cam_obj.data.clip_end
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        props.target_location, props.offset_yaw, props.offset_pitch, props.offset_roll = cam_obj.location + forward_vec, 0.0, 0.0, 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"{CONFIG.PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): bpy.context.preferences.themes[0].view_3d.grid = context.scene.theme_grid_properties.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 CONFIG.GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{CONFIG.PREFIX}.apply_grid_color")()

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

class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        groups = {"ypr":["offset_yaw", "offset_pitch", "offset_roll"], "aim":["target_location"], "clip":["clip_start", "clip_end", "lens_focal_length"]}
        to_reset = set(p for t in self.targets for p in (groups.get(t.name,[]) if t.name != "all" else sum(groups.values(),[])))
        props.is_updating_settings = True
        for p in 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"{CONFIG.PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): context.scene.surface_camera_properties.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}

class SFC_OT_OpenURL(Operator):
    bl_idname = f"{CONFIG.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"{CONFIG.PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): bpy.ops.preferences.addon_disable(module=__name__.split('.')[0]); unregister(); return {'FINISHED'}

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

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

class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{CONFIG.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(CONFIG.HDRI_PATHS):
            props.hdri_list_index, props.background_mode = self.hdri_index, 'HDRI'
            load_hdri_from_path(CONFIG.HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
        return {'FINISHED'}

class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{CONFIG.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 nodes and (mn := find_node(nodes, 'ShaderNodeMapping', 'Mapping')):
            mn.inputs[self.property_to_reset].default_value = (1, 1, 1) if self.property_to_reset == 'Scale' else (0, 0, 0)
        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, props = self.layout, 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:
            for idx in ["1", "2", "3"]:
                b = box_init.box(); b.label(text=f"Cam {idx} 初期値"); col = b.column(align=True); col.prop(props, f"cam{idx}_init_loc", text="位置"); col.prop(props, f"cam{idx}_init_tgt", text="注視")
                row_ops = b.row(align=True); row_ops.operator(SFC_OT_GetCameraInitInfo.bl_idname, text="取得", icon='RESTRICT_VIEW_OFF').cam_index = idx; row_ops.operator(SFC_OT_ResetCameraInit.bl_idname, text="リセット", icon='LOOP_BACK').cam_index = idx; row_ops.operator(SFC_OT_CopyCameraInitInfo.bl_idname, text="コピー", icon='COPYDOWN').cam_index = idx
        layout.separator(); box = layout.box(); box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA'); row = box.row(align=True)
        for i in["1", "2", "3"]: row.operator(SFC_OT_SwitchCamera.bl_idname, text=f"Cam {i}", depress=(props.camera_obj and props.camera_obj.name==f"Fixed_Cam_{i}")).cam_index = i
        box.label(text=f"操作・描画中: {props.camera_obj.name}" if props.camera_obj else "操作カメラ未選択", icon='CAMERA_DATA' if props.camera_obj else 'ERROR')
        box.separator(); box.box().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, props = self.layout, context.scene.surface_camera_properties
        box = layout.box(); box.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
        if props.camera_obj: box.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
        col1 = box.column(align=True); r1 = col1.row(align=True); r1.label(text="注視点"); r1.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "aim"; col1.prop(props, "target_location", text="")
        box.separator(); col2 = box.column(align=True); r2 = col2.row(align=True); r2.label(text="視線オフセット (YPR)"); r2.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "ypr"
        for p in ["offset_yaw", "offset_pitch", "offset_roll"]: col2.prop(props, p)

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, props = self.layout, context.scene.surface_camera_properties
        box = layout.box(); box.label(text="透視投影ビューの操作", icon='VIEW3D'); box.operator(SFC_OT_GetViewportInfo.bl_idname, icon='RESTRICT_VIEW_OFF', text="現在の視座・注視点を取得")
        row = box.row(align=True)
        for i in ["1", "2", "3"]: row.operator(SFC_OT_SetViewportToCamera.bl_idname, text=f"Cam{i} 視座へ", icon='CAMERA_DATA').cam_index = i
        col = box.column(align=True); col.prop(props, "viewport_location"); col.prop(props, "viewport_target")
        box.separator(); row_vp = box.row(align=True); row_vp.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="コピー"); row_vp.operator(SFC_OT_ResetViewportLocation.bl_idname, icon='LOOP_BACK', text="位置リセット"); row_vp.operator(SFC_OT_ResetViewportTarget.bl_idname, icon='LOOP_BACK', text="注視リセット")
        
        layout.separator(); box_vis = layout.box(); box_vis.label(text="生成オブジェクト 表示 / 非表示", icon='RESTRICT_VIEW_OFF'); row_vis = box_vis.row(align=True)
        for p, t in[("vis_vp_sphere_1", "球体1"), ("vis_vp_sphere_2", "球体2"), ("vis_vp_circles", "交差円")]: row_vis.prop(props, p, text=t, toggle=True)
        
        layout.separator(); box_sp = layout.box(); box_sp.label(text="透明球体 & 交差平面の円 生成・情報", icon='SPHERE'); col_sp = box_sp.column(align=True)
        col_sp.prop(props, "intersect_plane"); col_sp.prop(props, "sphere_mode"); col_sp.prop(props, "sphere_radius" if props.sphere_mode == 'RADIUS' else "intersect_circle_radius")
        
        col_sp.separator()
        col_sp.prop(props, "sync_sphere_colors")
        
        col_sp.prop(props, "sphere1_color_front", text="球1 表色")
        
        col_b1 = col_sp.column(align=True)
        col_b1.enabled = not props.sync_sphere_colors
        col_b1.prop(props, "sphere1_color_back", text="球1 裏色")
        
        col_f2 = col_sp.column(align=True)
        col_f2.enabled = not props.sync_sphere_colors
        col_f2.prop(props, "sphere2_color_front", text="球2 表色")
        
        col_b2 = col_sp.column(align=True)
        col_b2.enabled = not props.sync_sphere_colors
        col_b2.prop(props, "sphere2_color_back", text="球2 裏色")
        
        col_sp.separator()
        r_circ = col_sp.row(align=True)
        r_circ.prop(props, "circle_thickness")
        r_circ.prop(props, "circle_color", text="")
        
        vp_loc, plane = props.viewport_location, props.intersect_plane
        d = abs(vp_loc.z) if plane == 'XY' else abs(vp_loc.x) if plane == 'YZ' else abs(vp_loc.y)
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        col_i = box_sp.column(align=True); col_i.label(text=f"平面までの距離: {d:.2f}"); col_i.label(text=f"球の半径: {R:.2f}"); col_i.label(text=f"交差円 半径: {r_circ:.2f}")
        
        box_sp.separator(); col_g = box_sp.column(align=True); col_g.operator(SFC_OT_GenerateViewportSphere.bl_idname, icon='MESH_UVSPHERE'); col_g.operator(SFC_OT_CopySphereInfo.bl_idname, icon='COPYDOWN'); col_g.operator(SFC_OT_DetachSpheres.bl_idname, icon='UNLINKED')

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, props = self.layout, context.scene.surface_camera_properties
        if props.camera_obj and props.camera_obj.data: layout.box().prop(props.camera_obj.data, "type", text="投影タイプ")
        box = layout.box(); col = box.column(align=True); r = col.row(align=True); r.label(text="レンズとクリップ"); r.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "clip"
        col.prop(props, "lens_focal_length"); r2 = col.row(align=True); r2.label(text="水平視野角:"); r2.label(text=props.info_horizontal_fov); col.label(text="FOVプリセット:")
        r3 = col.row(align=True); c1, c2 = r3.column(align=True), r3.column(align=True)
        for i, fov in enumerate(CONFIG.FOV_PRESETS): (c1 if i % 2 == 0 else c2).operator(f"{CONFIG.PREFIX}.set_fov", text=f"{fov}°").fov = fov
        col.separator(); r4 = col.row(align=True); r4.prop(props, "clip_start"); r4.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
        layout.box().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, overlay = cam.data, getattr(context.space_data, 'overlay', None)
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA'); bp = layout.box(); bp.label(text="Passepartout", icon='MOD_MASK'); cp = bp.column(align=True); cp.prop(cam_data, "show_passepartout"); rp = cp.row(); rp.enabled = cam_data.show_passepartout; rp.prop(cam_data, "passepartout_alpha")
        if not overlay: return
        layout.separator(); bd = layout.box(); bd.label(text="Viewport Display", icon='OVERLAY'); bd.prop(overlay, "show_overlays"); co = bd.column(); co.enabled = overlay.show_overlays; co.prop(overlay, "show_extras"); cd = co.column(); cd.enabled = overlay.show_extras; cd.prop(overlay, "show_text"); cd.prop(cam_data, "show_name"); cd.prop(cam_data, "show_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, props = self.layout, 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
        bm = layout.box(); bm.label(text="Background Mode", icon='WORLD'); bm.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            be = layout.box(); be.label(text="Environment Texture", icon='IMAGE_DATA'); cl = be.column(align=True)
            for i, path in enumerate(CONFIG.HDRI_PATHS): cl.operator(f"{CONFIG.PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)).hdri_index = i
            be.separator(); en = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if en: be.template_ID(en, "image", open="image.open", text="Select HDRI")
        elif props.background_mode == 'SKY':
            bs = layout.box(); sn = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sn: bs.prop(sn, "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"{CONFIG.PREFIX}.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"{CONFIG.PREFIX}.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; b1 = layout.box(); b1.label(text="ドキュメント", icon='HELP')
        for link in CONFIG.NEW_DOC_LINKS: b1.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').url = link["url"]
        b2 = layout.box(); b2.label(text="ソーシャル", icon='WORLD_DATA')
        for link in CONFIG.SOCIAL_LINKS: b2.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').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"{CONFIG.PREFIX}.remove_addon", icon='CANCEL')

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

def initial_setup():
    wm = bpy.context.window_manager
    if not wm: return 0.1
    for window in wm.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 bpy.context.scene.world and bpy.context.scene.world.use_nodes:
        props = bpy.context.scene.zionad_swt_props
        nodes = bpy.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
            props.background_mode = 'SKY' if source_node.type == 'TEX_SKY' else 'HDRI'
        update_background_mode(props, bpy.context)
    return None

classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, SFC_OT_CreateThreeCameras, SFC_OT_GetCameraInitInfo, SFC_OT_ResetCameraInit, SFC_OT_CopyCameraInitInfo, 
    SFC_OT_SetViewportToCamera, SFC_OT_ResetViewportLocation, SFC_OT_ResetViewportTarget, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_GetViewportInfo, SFC_OT_CopySphereInfo, SFC_OT_GenerateViewportSphere, SFC_OT_DetachSpheres,
    SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor, 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():
    _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)
    safe_register_timer(initial_setup, 0.1)

def unregister():
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
    
    for wrapper in list(TIMER_REGISTRY.values()):
        if bpy.app.timers.is_registered(wrapper): bpy.app.timers.unregister(wrapper)
    TIMER_REGISTRY.clear()
        
    for prop_name in['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if hasattr(bpy.types.Scene, prop_name): delattr(bpy.types.Scene, prop_name)
            
    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
import time
import uuid
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty

# ======================================================================
# --- 定数管理 / CONFIG ---
# ======================================================================

class CONFIG:
    PREFIX = "unit_circle_cam"
    MASTER_COLLECTION = "Cam three"
    CAMERA_COLLECTION = "Cam"
    VP_COLLECTION = "VP_Objects"
    SAVED_COLLECTION = "Saved_Objects"
    
    SENSOR_WIDTH = 36.0
    FOV_PRESETS =[1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
    
    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))]
    
    NEW_DOC_LINKS =[
        {"label": "時空図 光の予算配分 20260329", "url": "<https://www.notion.so/20260329-332f5dacaf438016b8f9cff480994ec1>"},
        {"label": "カメラ3台 ジグザク 20260328b", "url": "<https://www.notion.so/20260328b-331f5dacaf4380b9abeed323cd5621a4>"},
        {"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
    ]
    SOCIAL_LINKS =[
        {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
    ]

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

bl_info = {
    "name": "zionad 521[Unit Circle Cam]",
    "author": "zionadchat",
    "version": (39, 0, 0),
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "【V39】3つの専用カメラ、透明球体と交差円生成 (表裏・色分離機能搭載)",
    "category": "Cam three 元型", 
}
ADDON_CATEGORY_NAME = bl_info["category"]

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

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

# ======================================================================
# --- タイマー管理 & ロック機構 (安定化) ---
# ======================================================================

TIMER_REGISTRY = {}

def safe_register_timer(func, delay=0.01):
    if func in TIMER_REGISTRY: return
    def wrapper():
        try: func()
        finally: TIMER_REGISTRY.pop(func, None)
        return None
    TIMER_REGISTRY[func] = wrapper
    bpy.app.timers.register(wrapper, first_interval=delay)

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

def set_update_lock(scene, state: bool):
    if scene: scene["_sfc_updating"] = bool(state)

def schedule_update_lock_reset():
    for scene in bpy.data.scenes:
        if "_sfc_updating" in scene: scene["_sfc_updating"] = False

# ======================================================================
# --- 汎用ヘルパー関数 (安全化) ---
# ======================================================================

def safe_remove_object(obj):
    if not obj: return
    if obj.users > 1:
        for col in list(obj.users_collection): col.objects.unlink(obj)
    else: bpy.data.objects.remove(obj, do_unlink=True)

def safe_link(links, out_socket, in_socket):
    if in_socket.is_linked: links.remove(in_socket.links[0])
    links.new(out_socket, in_socket)

def get_or_copy_material(mat, suffix):
    name = f"{mat.name}_{suffix}"
    existing = bpy.data.materials.get(name)
    if existing: return existing
    new_mat = mat.copy()
    new_mat.name = name
    return new_mat

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, CONFIG.MASTER_COLLECTION)

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_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))
    
    safe_link(links, background_node.outputs['Background'], output_node.inputs['Surface'])
    
    if mode == 'SKY': safe_link(links, sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        safe_link(links, tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        safe_link(links, mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        safe_link(links, env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(CONFIG.HDRI_PATHS): load_hdri_from_path(CONFIG.HDRI_PATHS[props.hdri_list_index], context)
            
    wm = bpy.context.window_manager
    if wm:
        for window in wm.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'

# ======================================================================
# --- オブジェクト生成関数 (球体・リング) ---
# ======================================================================

def get_or_create_color_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
    bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None) if mat.use_nodes else None
    if bsdf:
        if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
    mat.blend_method = 'BLEND'
    return mat

def get_or_create_front_back_material(name, color_front, color_back):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
        
    if mat.use_nodes:
        nodes, links = mat.node_tree.nodes, mat.node_tree.links
        bsdf = next((n for n in nodes if n.type == 'BSDF_PRINCIPLED'), None)
        
        if bsdf:
            geom = nodes.get("Geometry") or nodes.new('ShaderNodeNewGeometry')
            geom.name = "Geometry"
            geom.location = (bsdf.location.x - 400, bsdf.location.y + 200)
            
            mix_rgb = nodes.get("Mix_Color") or nodes.new('ShaderNodeMix')
            mix_rgb.name = "Mix_Color"
            mix_rgb.data_type = 'RGBA'
            mix_rgb.blend_type = 'MIX'
            mix_rgb.location = (bsdf.location.x - 200, bsdf.location.y + 200)
            
            mix_alpha = nodes.get("Mix_Alpha") or nodes.new('ShaderNodeMix')
            mix_alpha.name = "Mix_Alpha"
            mix_alpha.data_type = 'FLOAT'
            mix_alpha.location = (bsdf.location.x - 200, bsdf.location.y - 100)
            
            safe_link(links, geom.outputs['Backfacing'], mix_rgb.inputs['Factor'])
            safe_link(links, mix_rgb.outputs['Result'], bsdf.inputs['Base Color'])
            
            safe_link(links, geom.outputs['Backfacing'], mix_alpha.inputs['Factor'])
            safe_link(links, mix_alpha.outputs['Result'], bsdf.inputs['Alpha'])
            
            mix_rgb.inputs['A'].default_value = color_front
            mix_rgb.inputs['B'].default_value = color_back
            
            mix_alpha.inputs['A'].default_value = color_front[3]
            mix_alpha.inputs['B'].default_value = color_back[3]
            
    mat.blend_method = 'BLEND'
    return mat

def create_sphere_object(name, collection, loc, radius, mat):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
    bm.to_mesh(me); bm.free()
    for poly in me.polygons: poly.use_smooth = True
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    if mat: obj.data.materials.append(mat)
    collection.objects.link(obj)
    return obj

def create_split_spheres(name_prefix, collection, loc, radius, mat1, mat2, plane):
    if plane == 'XY': norm, co = mathutils.Vector((0, 0, 1)), mathutils.Vector((0, 0, -loc.z))
    elif plane == 'YZ': norm, co = mathutils.Vector((1, 0, 0)), mathutils.Vector((-loc.x, 0, 0))
    else: norm, co = mathutils.Vector((0, 1, 0)), mathutils.Vector((0, -loc.y, 0))
        
    objs, mats = [],[mat1, mat2]
    
    for i, clear_in in enumerate([True, False]):
        me = bpy.data.meshes.new(f"{name_prefix}_{i+1}"); bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
        bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=norm, clear_inner=clear_in, clear_outer=not clear_in)
        bm.to_mesh(me); bm.free()
        
        if len(me.vertices) > 0:
            for poly in me.polygons: poly.use_smooth = True
            obj = bpy.data.objects.new(f"{name_prefix}_{i+1}", me)
            obj.location = loc
            if mats[i]: obj.data.materials.append(mats[i])
            collection.objects.link(obj); objs.append(obj)
        else:
            bpy.data.meshes.remove(me)
    return objs

def create_ring_object(name, collection, loc, rot, major_radius, minor_radius, mat):
    me = bpy.data.meshes.new(name); bm = bmesh.new()
    r_outer, r_inner, segments = major_radius + minor_radius, max(0.001, major_radius - minor_radius), 64
    verts_outer, verts_inner = [],[]
    
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        c, s = math.cos(angle), math.sin(angle)
        verts_outer.append(bm.verts.new((r_outer * c, r_outer * s, 0.0)))
        verts_inner.append(bm.verts.new((r_inner * c, r_inner * s, 0.0)))
        
    for i in range(segments):
        ni = (i + 1) % segments
        bm.faces.new((verts_outer[i], verts_outer[ni], verts_inner[ni], verts_inner[i]))
        
    bm.to_mesh(me); bm.free()
    for poly in me.polygons: poly.use_smooth = True
        
    obj = bpy.data.objects.new(name, me)
    obj.location, obj.rotation_euler = loc, rot
    if mat: obj.data.materials.append(mat)
    
    mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
    mod.thickness, mod.offset = minor_radius * 2, 0.0
    collection.objects.link(obj)
    return obj

def update_group_visibility(prefix, hide):
    col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
    if not col: return
    for obj in col.objects:
        if obj.name.startswith(prefix): obj.hide_viewport = obj.hide_render = hide

def update_group_visibility_exact(name, hide):
    col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
    if not col: return
    obj = col.objects.get(name)
    if obj: obj.hide_viewport = obj.hide_render = hide

def update_vis_vp_sphere_1(self, context): update_group_visibility_exact("VP_Sphere_1", not self.vis_vp_sphere_1)
def update_vis_vp_sphere_2(self, context): update_group_visibility_exact("VP_Sphere_2", not self.vis_vp_sphere_2)
def update_vis_vp_circles(self, context): update_group_visibility("VP_Circle", not self.vis_vp_circles)

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

def update_cam_color(self, context):
    if self.camera_obj: context.preferences.themes[0].view_3d.camera = self.camera_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=lambda self, context: setattr(context.preferences.themes[0].view_3d, 'grid', self.grid_color))
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.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=lambda self, context: [setattr(context.preferences.themes[0].view_3d, 'wire', self.wire_color), setattr(context.preferences.themes[0].view_3d, 'object_active', self.wire_color)])
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))

class TargetProperty(PropertyGroup): name: StringProperty()

def _do_update_viewport_cam():
    wm = bpy.context.window_manager
    if not wm: return
    for window in wm.windows:
        scene = window.scene
        if not scene: continue
        props = scene.surface_camera_properties
        vp_loc, vp_tgt = mathutils.Vector(props.viewport_location), 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 area in window.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D' and space.region_3d:
                        set_update_lock(scene, True)
                        try:
                            if space.region_3d.view_perspective == 'CAMERA': space.region_3d.view_perspective = 'PERSP'
                            space.region_3d.view_location = vp_tgt
                            space.region_3d.view_rotation = rot_quat
                            space.region_3d.view_distance = direction.length
                        finally: safe_register_timer(schedule_update_lock_reset, 0.01)

def safe_update_viewport_cam(self, context):
    if not is_updating(context.scene): safe_register_timer(_do_update_viewport_cam, 0.01)

def _do_update_surface_camera():
    wm = bpy.context.window_manager
    if not wm: return
    for window in wm.windows:
        scene = window.scene
        if not scene: continue
        props, camera_obj = scene.surface_camera_properties, scene.surface_camera_properties.camera_obj
        set_update_lock(scene, True)
        try:
            if props.is_updating_settings or not camera_obj: 
                update_info_panel_text(props); continue
            if camera_obj.data: 
                camera_obj.data.sensor_fit = 'HORIZONTAL'
                camera_obj.data.lens_unit = 'MILLIMETERS'
                camera_obj.data.lens, camera_obj.data.clip_start, camera_obj.data.clip_end = props.lens_focal_length, props.clip_start, props.clip_end
            update_object_transform(camera_obj, props)
            update_info_panel_text(props)
        finally: safe_register_timer(schedule_update_lock_reset, 0.01)

def safe_update_surface_camera(self, context):
    if not is_updating(context.scene): safe_register_timer(_do_update_surface_camera, 0.01)

def update_sphere_colors(self, context):
    if getattr(self, "sync_sphere_colors", False):
        c = self.sphere1_color_front
        self.sphere1_color_back = c
        self.sphere2_color_front = c
        self.sphere2_color_back = c

def update_sync_checkbox(self, context):
    if self.sync_sphere_colors: update_sphere_colors(self, context)

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, 1.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, 20.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)
    
    vis_vp_sphere_1: BoolProperty(name="球体1", default=True, update=update_vis_vp_sphere_1)
    vis_vp_sphere_2: BoolProperty(name="球体2", default=True, update=update_vis_vp_sphere_2)
    vis_vp_circles: BoolProperty(name="交差円", default=True, update=update_vis_vp_circles)
    
    intersect_plane: EnumProperty(name="交差平面", items=[('XY', "XY平面 (Z=0)", ""), ('YZ', "YZ平面 (X=0)", ""), ('ZX', "ZX平面 (Y=0)", "")], default='ZX')
    sphere_mode: EnumProperty(name="サイズ指定モード", items=[('RADIUS', "球の半径を指定", ""), ('CIRCLE', "交差円の半径を指定", "")], default='RADIUS')
    sphere_radius: FloatProperty(name="球の半径", default=10.0, min=0.001)
    intersect_circle_radius: FloatProperty(name="交差円の半径", default=10.0, min=0.001)
    
    sync_sphere_colors: BoolProperty(name="球体1の表色で全て統一", default=False, update=update_sync_checkbox)
    sphere1_color_front: FloatVectorProperty(name="球体1 表色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.5, 0.8, 0.2), update=update_sphere_colors)
    sphere1_color_back: FloatVectorProperty(name="球体1 裏色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.2, 0.6, 0.9, 0.2))
    sphere2_color_front: FloatVectorProperty(name="球体2 表色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.5, 0.1, 0.2))
    sphere2_color_back: FloatVectorProperty(name="球体2 裏色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.9, 0.6, 0.2, 0.2))
    
    circle_color: FloatVectorProperty(name="交差円 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.2, 0.1, 0.8))
    circle_thickness: FloatProperty(name="交差円 太さ", default=0.05, min=0.001)
    
    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=update_cam_color)

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=CONFIG.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=CONFIG.SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except: return 50.0

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

def update_info_panel_text(props):
    if props and props.camera_obj: props.info_horizontal_fov = f"{calculate_horizontal_fov(props.lens_focal_length):.1f} °"

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

@bpy.app.handlers.persistent
def on_depsgraph_update(scene, depsgraph):
    if is_updating(scene): return
    sfc_props = scene.surface_camera_properties
    if not sfc_props.camera_obj: return 
    
    if any(update.is_updated_transform and update.id.original == sfc_props.camera_obj for update in depsgraph.updates):
        sync_ui_from_manual_transform(sfc_props, sfc_props.camera_obj, scene)

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

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

class SFC_OT_CreateThreeCameras(Operator):
    bl_idname = f"{CONFIG.PREFIX}.create_three_cameras"; bl_label = "3つのカメラを生成・初期化"
    def execute(self, context):
        col, props = get_or_create_collection(context, CONFIG.CAMERA_COLLECTION, get_master_collection(context)), context.scene.surface_camera_properties
        for idx, loc, tgt in[(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)]:
            name = f"Fixed_Cam_{idx}"; cam_obj = bpy.data.objects.get(name)
            if cam_obj and cam_obj.type != 'CAMERA': cam_obj.name += "_old"; cam_obj = None
            if not cam_obj: cam_obj = bpy.data.objects.new(name, bpy.data.cameras.new(name=name))
            if cam_obj.name not in col.objects: col.objects.link(cam_obj)
            for c in list(cam_obj.users_collection):
                if c != col: c.objects.unlink(cam_obj)
            set_initial_camera_transform(cam_obj, loc, tgt)
        getattr(getattr(bpy.ops, CONFIG.PREFIX), "switch_camera")(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました"); return {'FINISHED'}

class SFC_OT_GetCameraInitInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.get_camera_init_info"; bl_label = "カメラの現在位置・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj: return {'CANCELLED'}
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        setattr(props, f"cam{self.cam_index}_init_loc", cam_obj.location.copy())
        setattr(props, f"cam{self.cam_index}_init_tgt", cam_obj.location.copy() + forward_vec)
        return {'FINISHED'}

class SFC_OT_ResetCameraInit(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_camera_init"; bl_label = "カメラを初期値にリセット"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc, tgt = getattr(props, f"cam{self.cam_index}_init_loc"), getattr(props, f"cam{self.cam_index}_init_tgt")
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        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, props.offset_yaw, props.offset_pitch, props.offset_roll = tgt, 0.0, 0.0, 0.0
                props.is_updating_settings = False
        return {'FINISHED'}

class SFC_OT_CopyCameraInitInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_camera_init_info"; bl_label = "初期値情報をコピー"; cam_index: StringProperty()
    def execute(self, context):
        loc, tgt = getattr(context.scene.surface_camera_properties, f"cam{self.cam_index}_init_loc"), getattr(context.scene.surface_camera_properties, f"cam{self.cam_index}_init_tgt")
        context.window_manager.clipboard = f"Cam {self.cam_index}: 位置 ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f}) / 注視 ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        return {'FINISHED'}

class SFC_OT_SetViewportToCamera(Operator):
    bl_idname = f"{CONFIG.PREFIX}.set_viewport_to_camera"; bl_label = "指定カメラの視座を透視投影に適用"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj: return {'CANCELLED'}
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        props.viewport_location, props.viewport_target = cam_obj.location.copy(), cam_obj.location + forward_vec
        return {'FINISHED'}

class SFC_OT_ResetViewportLocation(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_viewport_location"; bl_label = "視座位置をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context): context.scene.surface_camera_properties.viewport_location = (0.0, -10.0, 5.0); return {'FINISHED'}

class SFC_OT_ResetViewportTarget(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_viewport_target"; bl_label = "注視点をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context): context.scene.surface_camera_properties.viewport_target = (0.0, 0.0, 0.0); return {'FINISHED'}

class SFC_OT_CopyViewportInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_viewport_info"; bl_label = "視座・注視点情報をコピー"
    def execute(self, context):
        loc, tgt = context.scene.surface_camera_properties.viewport_location, context.scene.surface_camera_properties.viewport_target
        context.window_manager.clipboard = f"視座位置: ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f})\n注視点: ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        return {'FINISHED'}

class SFC_OT_GetViewportInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.get_viewport_info"; bl_label = "現在の視座・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = context.scene.surface_camera_properties
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D' and space.region_3d:
                        set_update_lock(context.scene, True)
                        try: props.viewport_location, props.viewport_target = space.region_3d.view_matrix.inverted().translation, space.region_3d.view_location
                        finally: safe_register_timer(schedule_update_lock_reset, 0.01)
                        return {'FINISHED'}
        return {'CANCELLED'}

class SFC_OT_CopySphereInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_sphere_info"; bl_label = "球体・円情報をコピー"
    def execute(self, context):
        props = context.scene.surface_camera_properties; vp_loc, plane = props.viewport_location, props.intersect_plane
        d, plane_str = (abs(vp_loc.z), "XY平面 (Z=0)") if plane == 'XY' else (abs(vp_loc.x), "YZ平面 (X=0)") if plane == 'YZ' else (abs(vp_loc.y), "ZX平面 (Y=0)")
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        context.window_manager.clipboard = f"視座位置: ({vp_loc.x:.3f}, {vp_loc.y:.3f}, {vp_loc.z:.3f})\n交差平面: {plane_str}\n平面までの距離: {d:.3f}\n球の半径: {R:.3f}\n交差円の半径: {r_circ:.3f}"
        return {'FINISHED'}

class SFC_OT_GenerateViewportSphere(Operator):
    bl_idname = f"{CONFIG.PREFIX}.generate_viewport_sphere"; bl_label = "透明球体と交差円を生成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = context.scene.surface_camera_properties
        col = get_or_create_collection(context, CONFIG.VP_COLLECTION, get_master_collection(context))
        
        for obj in[o for o in col.objects if o.name.startswith("VP_Sphere") or o.name.startswith("VP_Circle")]: safe_remove_object(obj)
            
        vp_loc, plane = mathutils.Vector(props.viewport_location), props.intersect_plane
        d, circle_loc, circle_rot = (abs(vp_loc.z), mathutils.Vector((vp_loc.x, vp_loc.y, 0.0)), mathutils.Euler((0.0, 0.0, 0.0), 'XYZ')) if plane == 'XY' else (abs(vp_loc.x), mathutils.Vector((0.0, vp_loc.y, vp_loc.z)), mathutils.Euler((0.0, math.pi/2, 0.0), 'XYZ')) if plane == 'YZ' else (abs(vp_loc.y), mathutils.Vector((vp_loc.x, 0.0, vp_loc.z)), mathutils.Euler((math.pi/2, 0.0, 0.0), 'XYZ'))
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        
        mat1 = get_or_create_front_back_material("Mat_VP_Sphere_1", props.sphere1_color_front, props.sphere1_color_back)
        mat2 = get_or_create_front_back_material("Mat_VP_Sphere_2", props.sphere2_color_front, props.sphere2_color_back)
        mat_c = get_or_create_color_material("Mat_VP_Circle", props.circle_color)
        
        if r_circ > 0.001:
            create_split_spheres("VP_Sphere", col, vp_loc, R, mat1, mat2, plane)
            create_ring_object("VP_Circle", col, circle_loc, circle_rot, r_circ, props.circle_thickness, mat_c)
        else:
            create_sphere_object("VP_Sphere_1", col, vp_loc, R, mat1)
            
        update_group_visibility_exact("VP_Sphere_1", not props.vis_vp_sphere_1)
        update_group_visibility_exact("VP_Sphere_2", not props.vis_vp_sphere_2)
        update_group_visibility("VP_Circle", not props.vis_vp_circles)
        return {'FINISHED'}

class SFC_OT_DetachSpheres(Operator):
    bl_idname = f"{CONFIG.PREFIX}.detach_spheres"; bl_label = "アドオンから切り離して残す"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
        if not col: return {'CANCELLED'}
        saved_col = get_or_create_collection(context, CONFIG.SAVED_COLLECTION, get_master_collection(context))
        objs_to_detach =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        if not objs_to_detach: return {'CANCELLED'}
        
        suffix = str(uuid.uuid4())[:6]
        for obj in objs_to_detach:
            obj.name = obj.name.replace("VP_", f"Saved_{suffix}_")
            if obj.data.materials:
                new_mats =[get_or_copy_material(mat, suffix) for mat in obj.data.materials if mat]
                obj.data.materials.clear()
                for nm in new_mats: obj.data.materials.append(nm)
            saved_col.objects.link(obj)
            col.objects.unlink(obj)
        self.report({'INFO'}, f"{len(objs_to_detach)} 個のオブジェクトを保存しました")
        return {'FINISHED'}

class SFC_OT_SwitchCamera(Operator):
    bl_idname = f"{CONFIG.PREFIX}.switch_camera"; bl_label = "カメラを切り替え"; cam_index: StringProperty()
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj or cam_obj.type != 'CAMERA': return {'CANCELLED'}
            
        props.is_updating_settings, props.camera_obj, context.scene.camera = True, cam_obj, 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
        props.lens_focal_length, props.clip_start, props.clip_end = cam_obj.data.lens, cam_obj.data.clip_start, cam_obj.data.clip_end
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        props.target_location, props.offset_yaw, props.offset_pitch, props.offset_roll = cam_obj.location + forward_vec, 0.0, 0.0, 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"{CONFIG.PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): bpy.context.preferences.themes[0].view_3d.grid = context.scene.theme_grid_properties.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 CONFIG.GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{CONFIG.PREFIX}.apply_grid_color")()

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

class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        groups = {"ypr":["offset_yaw", "offset_pitch", "offset_roll"], "aim":["target_location"], "clip":["clip_start", "clip_end", "lens_focal_length"]}
        to_reset = set(p for t in self.targets for p in (groups.get(t.name,[]) if t.name != "all" else sum(groups.values(),[])))
        props.is_updating_settings = True
        for p in 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"{CONFIG.PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): context.scene.surface_camera_properties.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}

class SFC_OT_OpenURL(Operator):
    bl_idname = f"{CONFIG.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"{CONFIG.PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): bpy.ops.preferences.addon_disable(module=__name__.split('.')[0]); unregister(); return {'FINISHED'}

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

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

class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{CONFIG.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(CONFIG.HDRI_PATHS):
            props.hdri_list_index, props.background_mode = self.hdri_index, 'HDRI'
            load_hdri_from_path(CONFIG.HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
        return {'FINISHED'}

class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{CONFIG.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 nodes and (mn := find_node(nodes, 'ShaderNodeMapping', 'Mapping')):
            mn.inputs[self.property_to_reset].default_value = (1, 1, 1) if self.property_to_reset == 'Scale' else (0, 0, 0)
        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, props = self.layout, 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:
            for idx in ["1", "2", "3"]:
                b = box_init.box(); b.label(text=f"Cam {idx} 初期値"); col = b.column(align=True); col.prop(props, f"cam{idx}_init_loc", text="位置"); col.prop(props, f"cam{idx}_init_tgt", text="注視")
                row_ops = b.row(align=True); row_ops.operator(SFC_OT_GetCameraInitInfo.bl_idname, text="取得", icon='RESTRICT_VIEW_OFF').cam_index = idx; row_ops.operator(SFC_OT_ResetCameraInit.bl_idname, text="リセット", icon='LOOP_BACK').cam_index = idx; row_ops.operator(SFC_OT_CopyCameraInitInfo.bl_idname, text="コピー", icon='COPYDOWN').cam_index = idx
        layout.separator(); box = layout.box(); box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA'); row = box.row(align=True)
        for i in["1", "2", "3"]: row.operator(SFC_OT_SwitchCamera.bl_idname, text=f"Cam {i}", depress=(props.camera_obj and props.camera_obj.name==f"Fixed_Cam_{i}")).cam_index = i
        box.label(text=f"操作・描画中: {props.camera_obj.name}" if props.camera_obj else "操作カメラ未選択", icon='CAMERA_DATA' if props.camera_obj else 'ERROR')
        box.separator(); box.box().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, props = self.layout, context.scene.surface_camera_properties
        box = layout.box(); box.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
        if props.camera_obj: box.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
        col1 = box.column(align=True); r1 = col1.row(align=True); r1.label(text="注視点"); r1.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "aim"; col1.prop(props, "target_location", text="")
        box.separator(); col2 = box.column(align=True); r2 = col2.row(align=True); r2.label(text="視線オフセット (YPR)"); r2.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "ypr"
        for p in ["offset_yaw", "offset_pitch", "offset_roll"]: col2.prop(props, p)

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, props = self.layout, context.scene.surface_camera_properties
        box = layout.box(); box.label(text="透視投影ビューの操作", icon='VIEW3D'); box.operator(SFC_OT_GetViewportInfo.bl_idname, icon='RESTRICT_VIEW_OFF', text="現在の視座・注視点を取得")
        row = box.row(align=True)
        for i in ["1", "2", "3"]: row.operator(SFC_OT_SetViewportToCamera.bl_idname, text=f"Cam{i} 視座へ", icon='CAMERA_DATA').cam_index = i
        col = box.column(align=True); col.prop(props, "viewport_location"); col.prop(props, "viewport_target")
        box.separator(); row_vp = box.row(align=True); row_vp.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="コピー"); row_vp.operator(SFC_OT_ResetViewportLocation.bl_idname, icon='LOOP_BACK', text="位置リセット"); row_vp.operator(SFC_OT_ResetViewportTarget.bl_idname, icon='LOOP_BACK', text="注視リセット")
        
        layout.separator(); box_vis = layout.box(); box_vis.label(text="生成オブジェクト 表示 / 非表示", icon='RESTRICT_VIEW_OFF'); row_vis = box_vis.row(align=True)
        for p, t in[("vis_vp_sphere_1", "球体1"), ("vis_vp_sphere_2", "球体2"), ("vis_vp_circles", "交差円")]: row_vis.prop(props, p, text=t, toggle=True)
        
        layout.separator(); box_sp = layout.box(); box_sp.label(text="透明球体 & 交差平面の円 生成・情報", icon='SPHERE'); col_sp = box_sp.column(align=True)
        col_sp.prop(props, "intersect_plane"); col_sp.prop(props, "sphere_mode"); col_sp.prop(props, "sphere_radius" if props.sphere_mode == 'RADIUS' else "intersect_circle_radius")
        
        col_sp.separator()
        col_sp.prop(props, "sync_sphere_colors")
        
        r_col1 = col_sp.row(align=True)
        r_col1.prop(props, "sphere1_color_front", text="球1 表")
        col_b1 = r_col1.column(align=True)
        col_b1.enabled = not props.sync_sphere_colors
        col_b1.prop(props, "sphere1_color_back", text="球1 裏")
        
        r_col2 = col_sp.row(align=True)
        col_f2 = r_col2.column(align=True)
        col_f2.enabled = not props.sync_sphere_colors
        col_f2.prop(props, "sphere2_color_front", text="球2 表")
        
        col_b2 = r_col2.column(align=True)
        col_b2.enabled = not props.sync_sphere_colors
        col_b2.prop(props, "sphere2_color_back", text="球2 裏")
        
        col_sp.separator()
        r_circ = col_sp.row(align=True)
        r_circ.prop(props, "circle_thickness")
        r_circ.prop(props, "circle_color", text="")
        
        vp_loc, plane = props.viewport_location, props.intersect_plane
        d = abs(vp_loc.z) if plane == 'XY' else abs(vp_loc.x) if plane == 'YZ' else abs(vp_loc.y)
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        col_i = box_sp.column(align=True); col_i.label(text=f"平面までの距離: {d:.2f}"); col_i.label(text=f"球の半径: {R:.2f}"); col_i.label(text=f"交差円 半径: {r_circ:.2f}")
        
        box_sp.separator(); col_g = box_sp.column(align=True); col_g.operator(SFC_OT_GenerateViewportSphere.bl_idname, icon='MESH_UVSPHERE'); col_g.operator(SFC_OT_CopySphereInfo.bl_idname, icon='COPYDOWN'); col_g.operator(SFC_OT_DetachSpheres.bl_idname, icon='UNLINKED')

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, props = self.layout, context.scene.surface_camera_properties
        if props.camera_obj and props.camera_obj.data: layout.box().prop(props.camera_obj.data, "type", text="投影タイプ")
        box = layout.box(); col = box.column(align=True); r = col.row(align=True); r.label(text="レンズとクリップ"); r.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "clip"
        col.prop(props, "lens_focal_length"); r2 = col.row(align=True); r2.label(text="水平視野角:"); r2.label(text=props.info_horizontal_fov); col.label(text="FOVプリセット:")
        r3 = col.row(align=True); c1, c2 = r3.column(align=True), r3.column(align=True)
        for i, fov in enumerate(CONFIG.FOV_PRESETS): (c1 if i % 2 == 0 else c2).operator(f"{CONFIG.PREFIX}.set_fov", text=f"{fov}°").fov = fov
        col.separator(); r4 = col.row(align=True); r4.prop(props, "clip_start"); r4.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
        layout.box().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, overlay = cam.data, getattr(context.space_data, 'overlay', None)
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA'); bp = layout.box(); bp.label(text="Passepartout", icon='MOD_MASK'); cp = bp.column(align=True); cp.prop(cam_data, "show_passepartout"); rp = cp.row(); rp.enabled = cam_data.show_passepartout; rp.prop(cam_data, "passepartout_alpha")
        if not overlay: return
        layout.separator(); bd = layout.box(); bd.label(text="Viewport Display", icon='OVERLAY'); bd.prop(overlay, "show_overlays"); co = bd.column(); co.enabled = overlay.show_overlays; co.prop(overlay, "show_extras"); cd = co.column(); cd.enabled = overlay.show_extras; cd.prop(overlay, "show_text"); cd.prop(cam_data, "show_name"); cd.prop(cam_data, "show_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, props = self.layout, 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
        bm = layout.box(); bm.label(text="Background Mode", icon='WORLD'); bm.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            be = layout.box(); be.label(text="Environment Texture", icon='IMAGE_DATA'); cl = be.column(align=True)
            for i, path in enumerate(CONFIG.HDRI_PATHS): cl.operator(f"{CONFIG.PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)).hdri_index = i
            be.separator(); en = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if en: be.template_ID(en, "image", open="image.open", text="Select HDRI")
        elif props.background_mode == 'SKY':
            bs = layout.box(); sn = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sn: bs.prop(sn, "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"{CONFIG.PREFIX}.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"{CONFIG.PREFIX}.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; b1 = layout.box(); b1.label(text="ドキュメント", icon='HELP')
        for link in CONFIG.NEW_DOC_LINKS: b1.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').url = link["url"]
        b2 = layout.box(); b2.label(text="ソーシャル", icon='WORLD_DATA')
        for link in CONFIG.SOCIAL_LINKS: b2.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').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"{CONFIG.PREFIX}.remove_addon", icon='CANCEL')

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

def initial_setup():
    wm = bpy.context.window_manager
    if not wm: return 0.1
    for window in wm.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 bpy.context.scene.world and bpy.context.scene.world.use_nodes:
        props = bpy.context.scene.zionad_swt_props
        nodes = bpy.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
            props.background_mode = 'SKY' if source_node.type == 'TEX_SKY' else 'HDRI'
        update_background_mode(props, bpy.context)
    return None

classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, SFC_OT_CreateThreeCameras, SFC_OT_GetCameraInitInfo, SFC_OT_ResetCameraInit, SFC_OT_CopyCameraInitInfo, 
    SFC_OT_SetViewportToCamera, SFC_OT_ResetViewportLocation, SFC_OT_ResetViewportTarget, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_GetViewportInfo, SFC_OT_CopySphereInfo, SFC_OT_GenerateViewportSphere, SFC_OT_DetachSpheres,
    SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor, 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():
    _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)
    safe_register_timer(initial_setup, 0.1)

def unregister():
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
    
    for wrapper in list(TIMER_REGISTRY.values()):
        if bpy.app.timers.is_registered(wrapper): bpy.app.timers.unregister(wrapper)
    TIMER_REGISTRY.clear()
        
    for prop_name in['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if hasattr(bpy.types.Scene, prop_name): delattr(bpy.types.Scene, prop_name)
            
    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
import bpy
import bmesh
import math
import mathutils
import webbrowser
import os
import time
import uuid
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, PointerProperty, EnumProperty, FloatVectorProperty, FloatProperty, CollectionProperty, BoolProperty, IntProperty

# ======================================================================
# --- 定数管理 / CONFIG ---
# ======================================================================

class CONFIG:
    PREFIX = "unit_circle_cam"
    MASTER_COLLECTION = "Cam three"
    CAMERA_COLLECTION = "Cam"
    VP_COLLECTION = "VP_Objects"
    SAVED_COLLECTION = "Saved_Objects"
    
    SENSOR_WIDTH = 36.0
    FOV_PRESETS =[1, 5, 10, 30, 45, 60, 90, 120, 135, 150, 179]
    
    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))]
    
    NEW_DOC_LINKS =[
        {"label": "時空図 光の予算配分 20260329", "url": "<https://www.notion.so/20260329-332f5dacaf438016b8f9cff480994ec1>"},
        {"label": "カメラ3台 ジグザク 20260328b", "url": "<https://www.notion.so/20260328b-331f5dacaf4380b9abeed323cd5621a4>"},
        {"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
    ]
    SOCIAL_LINKS =[
        {"label": "単純トリック", "url": "<https://posfie.com/@timekagura?sort=0>"},
    ]

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

bl_info = {
    "name": "zionad 521[Unit Circle Cam]",
    "author": "zionadchat",
    "version": (38, 0, 0),
    "blender": (4, 1, 0),
    "location": "View3D > Sidebar > zionad Control",
    "description": "【完全安定化版】3つの専用カメラ、ビューポートカメラ制御、透明球体と交差円の生成",
    "category": "Cam three 元型", 
}
ADDON_CATEGORY_NAME = bl_info["category"]

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

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

# ======================================================================
# --- タイマー管理 & ロック機構 (安定化) ---
# ======================================================================

TIMER_REGISTRY = {}

def safe_register_timer(func, delay=0.01):
    if func in TIMER_REGISTRY:
        return
    def wrapper():
        try:
            func()
        finally:
            TIMER_REGISTRY.pop(func, None)
        return None
    TIMER_REGISTRY[func] = wrapper
    bpy.app.timers.register(wrapper, first_interval=delay)

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

def set_update_lock(scene, state: bool):
    if scene: scene["_sfc_updating"] = bool(state)

def schedule_update_lock_reset():
    for scene in bpy.data.scenes:
        if "_sfc_updating" in scene:
            scene["_sfc_updating"] = False

# ======================================================================
# --- 汎用ヘルパー関数 (安全化) ---
# ======================================================================

def safe_remove_object(obj):
    if not obj: return
    if obj.users > 1:
        for col in list(obj.users_collection):
            col.objects.unlink(obj)
    else:
        bpy.data.objects.remove(obj, do_unlink=True)

def safe_link(links, out_socket, in_socket):
    if in_socket.is_linked:
        links.remove(in_socket.links[0])
    links.new(out_socket, in_socket)

def get_or_copy_material(mat, suffix):
    name = f"{mat.name}_{suffix}"
    existing = bpy.data.materials.get(name)
    if existing: return existing
    new_mat = mat.copy()
    new_mat.name = name
    return new_mat

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, CONFIG.MASTER_COLLECTION)

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_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))
    
    safe_link(links, background_node.outputs['Background'], output_node.inputs['Surface'])
    
    if mode == 'SKY': 
        safe_link(links, sky_node.outputs['Color'], background_node.inputs['Color'])
    elif mode == 'HDRI':
        safe_link(links, tex_coord_node.outputs['Generated'], mapping_node.inputs['Vector'])
        safe_link(links, mapping_node.outputs['Vector'], env_node.inputs['Vector'])
        safe_link(links, env_node.outputs['Color'], background_node.inputs['Color'])
        props = context.scene.zionad_swt_props
        if 0 <= props.hdri_list_index < len(CONFIG.HDRI_PATHS): 
            load_hdri_from_path(CONFIG.HDRI_PATHS[props.hdri_list_index], context)
            
    # Viewport shading update
    wm = bpy.context.window_manager
    if wm:
        for window in wm.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'

# ======================================================================
# --- オブジェクト生成関数 (球体・リング) ---
# ======================================================================

def get_or_create_color_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
    bsdf = None
    if mat.use_nodes:
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED':
                bsdf = node
                break
    if bsdf:
        if "Base Color" in bsdf.inputs: bsdf.inputs["Base Color"].default_value = color
        if "Alpha" in bsdf.inputs: bsdf.inputs["Alpha"].default_value = color[3]
    mat.blend_method = 'BLEND'
    return mat

def create_sphere_object(name, collection, loc, radius, mat):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
    bm.to_mesh(me)
    bm.free()
    for poly in me.polygons: poly.use_smooth = True
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    if mat: obj.data.materials.append(mat)
    collection.objects.link(obj)
    return obj

def create_split_spheres(name_prefix, collection, loc, radius, mat1, mat2, plane):
    if plane == 'XY':
        norm, co = mathutils.Vector((0, 0, 1)), mathutils.Vector((0, 0, -loc.z))
    elif plane == 'YZ':
        norm, co = mathutils.Vector((1, 0, 0)), mathutils.Vector((-loc.x, 0, 0))
    else: 
        norm, co = mathutils.Vector((0, 1, 0)), mathutils.Vector((0, -loc.y, 0))
        
    objs = []
    mats =[mat1, mat2]
    
    for i, clear_in in enumerate([True, False]):
        me = bpy.data.meshes.new(f"{name_prefix}_{i+1}")
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
        bmesh.ops.bisect_plane(bm, geom=bm.verts[:] + bm.edges[:] + bm.faces[:], dist=0.0001, plane_co=co, plane_no=norm, clear_inner=clear_in, clear_outer=not clear_in)
        bm.to_mesh(me)
        bm.free()
        
        if len(me.vertices) > 0:
            for poly in me.polygons: poly.use_smooth = True
            obj = bpy.data.objects.new(f"{name_prefix}_{i+1}", me)
            obj.location = loc
            if mats[i]: obj.data.materials.append(mats[i])
            collection.objects.link(obj)
            objs.append(obj)
        else:
            bpy.data.meshes.remove(me)
            
    return objs

def create_ring_object(name, collection, loc, rot, major_radius, minor_radius, mat):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    r_outer = major_radius + minor_radius
    r_inner = max(0.001, major_radius - minor_radius)
    segments = 64
    verts_outer = []
    verts_inner =[]
    
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        c, s = math.cos(angle), math.sin(angle)
        verts_outer.append(bm.verts.new((r_outer * c, r_outer * s, 0.0)))
        verts_inner.append(bm.verts.new((r_inner * c, r_inner * s, 0.0)))
        
    for i in range(segments):
        ni = (i + 1) % segments
        bm.faces.new((verts_outer[i], verts_outer[ni], verts_inner[ni], verts_inner[i]))
        
    bm.to_mesh(me)
    bm.free()
    for poly in me.polygons: poly.use_smooth = True
        
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    obj.rotation_euler = rot
    if mat: obj.data.materials.append(mat)
    
    mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
    mod.thickness = minor_radius * 2
    mod.offset = 0.0
    
    collection.objects.link(obj)
    return obj

def update_group_visibility(prefix, hide):
    col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
    if not col: return
    for obj in col.objects:
        if obj.name.startswith(prefix):
            obj.hide_viewport = obj.hide_render = hide

def update_group_visibility_exact(name, hide):
    col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
    if not col: return
    obj = col.objects.get(name)
    if obj:
        obj.hide_viewport = obj.hide_render = hide

def update_vis_vp_sphere_1(self, context): update_group_visibility_exact("VP_Sphere_1", not self.vis_vp_sphere_1)
def update_vis_vp_sphere_2(self, context): update_group_visibility_exact("VP_Sphere_2", not self.vis_vp_sphere_2)
def update_vis_vp_circles(self, context): update_group_visibility("VP_Circle", not self.vis_vp_circles)

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

def update_cam_color(self, context):
    if self.camera_obj: context.preferences.themes[0].view_3d.camera = self.camera_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=lambda self, context: setattr(context.preferences.themes[0].view_3d, 'grid', self.grid_color))
    grid_preset: EnumProperty(name="Grid Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.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=lambda self, context: [setattr(context.preferences.themes[0].view_3d, 'wire', self.wire_color), setattr(context.preferences.themes[0].view_3d, 'object_active', self.wire_color)])
    wire_preset: EnumProperty(name="Wire Preset", items=[(p[0], p[1], p[2]) for p in CONFIG.WIRE_PRESETS], update=lambda self, context: SFC_OT_WireApplyColor.update_preset(self, context))

class TargetProperty(PropertyGroup): name: StringProperty()

def _do_update_viewport_cam():
    wm = bpy.context.window_manager
    if not wm: return
    
    for window in wm.windows:
        scene = window.scene
        if not scene: continue
        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 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:
                                safe_register_timer(schedule_update_lock_reset, 0.01)

def safe_update_viewport_cam(self, context):
    if is_updating(context.scene): return
    safe_register_timer(_do_update_viewport_cam, 0.01)

def _do_update_surface_camera():
    wm = bpy.context.window_manager
    if not wm: return
    
    for window in wm.windows:
        scene = window.scene
        if not scene: continue
        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)
                continue
            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)
        finally:
            safe_register_timer(schedule_update_lock_reset, 0.01)

def safe_update_surface_camera(self, context):
    if is_updating(context.scene): return
    safe_register_timer(_do_update_surface_camera, 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, 1.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, 20.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)
    
    vis_vp_sphere_1: BoolProperty(name="球体1", default=True, update=update_vis_vp_sphere_1)
    vis_vp_sphere_2: BoolProperty(name="球体2", default=True, update=update_vis_vp_sphere_2)
    vis_vp_circles: BoolProperty(name="交差円", default=True, update=update_vis_vp_circles)
    
    intersect_plane: EnumProperty(name="交差平面", items=[('XY', "XY平面 (Z=0)", ""), ('YZ', "YZ平面 (X=0)", ""), ('ZX', "ZX平面 (Y=0)", "")], default='ZX')
    sphere_mode: EnumProperty(name="サイズ指定モード", items=[('RADIUS', "球の半径を指定", ""), ('CIRCLE', "交差円の半径を指定", "")], default='RADIUS')
    sphere_radius: FloatProperty(name="球の半径", default=10.0, min=0.001)
    intersect_circle_radius: FloatProperty(name="交差円の半径", default=10.0, min=0.001)
    
    sphere_color: FloatVectorProperty(name="球体1 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.5, 0.8, 0.2))
    sphere_color_2: FloatVectorProperty(name="球体2 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.5, 0.1, 0.2))
    circle_color: FloatVectorProperty(name="交差円 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.2, 0.1, 0.8))
    circle_thickness: FloatProperty(name="交差円 太さ", default=0.05, min=0.001)
    
    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=update_cam_color)

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=CONFIG.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=CONFIG.SENSOR_WIDTH):
    try: return sensor_width / (2 * math.tan(math.radians(fov_degrees) / 2))
    except: return 50.0

def update_object_transform(obj, props):
    location = obj.location
    target_location = mathutils.Vector(props.target_location)
    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')
    obj.rotation_euler = (base_track_quat @ offset_euler.to_quaternion()).to_euler('XYZ')

def update_info_panel_text(props):
    if not props or not props.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 = mathutils.Vector(props.target_location)
        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')
        offset_quat = base_track_quat.inverted() @ obj.matrix_world.to_quaternion()
        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: 
        safe_register_timer(schedule_update_lock_reset, 0.01)
    update_info_panel_text(props)

@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 
    
    # 負荷軽減: 監視対象カメラが更新されていない場合はスキップ
    has_cam_update = False
    for update in depsgraph.updates:
        if update.is_updated_transform and update.id.original == cam_obj:
            has_cam_update = True; break
    if not has_cam_update: return
    
    sync_ui_from_manual_transform(sfc_props, cam_obj, scene)

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

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))
    obj.location = loc_vec; obj.rotation_euler = direction.to_track_quat('-Z', 'Y').to_euler('XYZ')

class SFC_OT_CreateThreeCameras(Operator):
    bl_idname = f"{CONFIG.PREFIX}.create_three_cameras"; bl_label = "3つのカメラを生成・初期化"
    def execute(self, context):
        col = get_or_create_collection(context, CONFIG.CAMERA_COLLECTION, get_master_collection(context))
        props = context.scene.surface_camera_properties
        for idx, loc, tgt in[(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)]:
            name = f"Fixed_Cam_{idx}"; cam_obj = bpy.data.objects.get(name)
            if cam_obj and cam_obj.type != 'CAMERA':
                cam_obj.name += "_old"; cam_obj = None
            if not cam_obj:
                cam_obj = bpy.data.objects.new(name, bpy.data.cameras.new(name=name))
            if cam_obj.name not in col.objects: col.objects.link(cam_obj)
            for c in cam_obj.users_collection:
                if c != col: c.objects.unlink(cam_obj)
            set_initial_camera_transform(cam_obj, loc, tgt)
        getattr(getattr(bpy.ops, CONFIG.PREFIX), "switch_camera")(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました"); return {'FINISHED'}

class SFC_OT_GetCameraInitInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.get_camera_init_info"; bl_label = "カメラの現在位置・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj: return {'CANCELLED'}
        loc = cam_obj.location.copy()
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        setattr(props, f"cam{self.cam_index}_init_loc", loc)
        setattr(props, f"cam{self.cam_index}_init_tgt", loc + forward_vec)
        self.report({'INFO'}, f"Cam {self.cam_index} の現在位置・注視点を取得しました"); return {'FINISHED'}

class SFC_OT_ResetCameraInit(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_camera_init"; bl_label = "カメラを初期値にリセット"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc, tgt = getattr(props, f"cam{self.cam_index}_init_loc"), getattr(props, f"cam{self.cam_index}_init_tgt")
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        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 = props.offset_pitch = props.offset_roll = 0.0
                props.is_updating_settings = False
        return {'FINISHED'}

class SFC_OT_CopyCameraInitInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_camera_init_info"; bl_label = "初期値情報をコピー"; cam_index: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        loc, tgt = getattr(props, f"cam{self.cam_index}_init_loc"), getattr(props, f"cam{self.cam_index}_init_tgt")
        context.window_manager.clipboard = f"Cam {self.cam_index}: 位置 ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f}) / 注視 ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        return {'FINISHED'}

class SFC_OT_SetViewportToCamera(Operator):
    bl_idname = f"{CONFIG.PREFIX}.set_viewport_to_camera"; bl_label = "指定カメラの視座を透視投影に適用"; bl_options = {'REGISTER', 'UNDO'}; cam_index: StringProperty()
    def execute(self, context):
        props, cam_obj = context.scene.surface_camera_properties, bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj: return {'CANCELLED'}
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0)); forward_vec.rotate(cam_obj.rotation_euler)
        props.viewport_location = cam_obj.location.copy()
        props.viewport_target = cam_obj.location + forward_vec
        return {'FINISHED'}

class SFC_OT_ResetViewportLocation(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_viewport_location"; bl_label = "視座位置をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context): context.scene.surface_camera_properties.viewport_location = (0.0, -10.0, 5.0); return {'FINISHED'}

class SFC_OT_ResetViewportTarget(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_viewport_target"; bl_label = "注視点をリセット"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context): context.scene.surface_camera_properties.viewport_target = (0.0, 0.0, 0.0); return {'FINISHED'}

class SFC_OT_CopyViewportInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_viewport_info"; bl_label = "視座・注視点情報をコピー"
    def execute(self, context):
        props = context.scene.surface_camera_properties; loc, tgt = props.viewport_location, props.viewport_target
        context.window_manager.clipboard = f"視座位置: ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f})\n注視点: ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        return {'FINISHED'}

class SFC_OT_GetViewportInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.get_viewport_info"; bl_label = "現在の視座・注視点を取得"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = context.scene.surface_camera_properties
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                for space in area.spaces:
                    if space.type == 'VIEW_3D' and space.region_3d:
                        rv3d = space.region_3d
                        set_update_lock(context.scene, True)
                        try:
                            props.viewport_location = rv3d.view_matrix.inverted().translation
                            props.viewport_target = rv3d.view_location
                        finally: safe_register_timer(schedule_update_lock_reset, 0.01)
                        return {'FINISHED'}
        return {'CANCELLED'}

class SFC_OT_CopySphereInfo(Operator):
    bl_idname = f"{CONFIG.PREFIX}.copy_sphere_info"; bl_label = "球体・円情報をコピー"
    def execute(self, context):
        props = context.scene.surface_camera_properties; vp_loc = props.viewport_location; plane = props.intersect_plane
        d, plane_str = (abs(vp_loc.z), "XY平面 (Z=0)") if plane == 'XY' else (abs(vp_loc.x), "YZ平面 (X=0)") if plane == 'YZ' else (abs(vp_loc.y), "ZX平面 (Y=0)")
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        context.window_manager.clipboard = f"視座位置: ({vp_loc.x:.3f}, {vp_loc.y:.3f}, {vp_loc.z:.3f})\n交差平面: {plane_str}\n平面までの距離: {d:.3f}\n球の半径: {R:.3f}\n交差円の半径: {r_circ:.3f}"
        return {'FINISHED'}

class SFC_OT_GenerateViewportSphere(Operator):
    bl_idname = f"{CONFIG.PREFIX}.generate_viewport_sphere"; bl_label = "透明球体と交差円を生成"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        props = context.scene.surface_camera_properties
        col = get_or_create_collection(context, CONFIG.VP_COLLECTION, get_master_collection(context))
        
        for obj in[o for o in col.objects if o.name.startswith("VP_Sphere") or o.name.startswith("VP_Circle")]:
            safe_remove_object(obj)
            
        vp_loc, plane = mathutils.Vector(props.viewport_location), props.intersect_plane
        d, circle_loc, circle_rot = (abs(vp_loc.z), mathutils.Vector((vp_loc.x, vp_loc.y, 0.0)), mathutils.Euler((0.0, 0.0, 0.0), 'XYZ')) if plane == 'XY' else (abs(vp_loc.x), mathutils.Vector((0.0, vp_loc.y, vp_loc.z)), mathutils.Euler((0.0, math.pi/2, 0.0), 'XYZ')) if plane == 'YZ' else (abs(vp_loc.y), mathutils.Vector((vp_loc.x, 0.0, vp_loc.z)), mathutils.Euler((math.pi/2, 0.0, 0.0), 'XYZ'))
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        mat1, mat2, mat_c = get_or_create_color_material("Mat_VP_Sphere_1", props.sphere_color), get_or_create_color_material("Mat_VP_Sphere_2", props.sphere_color_2), get_or_create_color_material("Mat_VP_Circle", props.circle_color)
        
        if r_circ > 0.001:
            create_split_spheres("VP_Sphere", col, vp_loc, R, mat1, mat2, plane)
            create_ring_object("VP_Circle", col, circle_loc, circle_rot, r_circ, props.circle_thickness, mat_c)
        else:
            create_sphere_object("VP_Sphere_1", col, vp_loc, R, mat1)
            
        update_group_visibility_exact("VP_Sphere_1", not props.vis_vp_sphere_1)
        update_group_visibility_exact("VP_Sphere_2", not props.vis_vp_sphere_2)
        update_group_visibility("VP_Circle", not props.vis_vp_circles)
        return {'FINISHED'}

class SFC_OT_DetachSpheres(Operator):
    bl_idname = f"{CONFIG.PREFIX}.detach_spheres"; bl_label = "アドオンから切り離して残す"; bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        col = bpy.data.collections.get(CONFIG.VP_COLLECTION)
        if not col: return {'CANCELLED'}
        saved_col = get_or_create_collection(context, CONFIG.SAVED_COLLECTION, get_master_collection(context))
        objs_to_detach =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        if not objs_to_detach: return {'CANCELLED'}
        
        suffix = str(uuid.uuid4())[:6] # ランダムサフィックスで名前衝突を完全回避
        for obj in objs_to_detach:
            obj.name = obj.name.replace("VP_", f"Saved_{suffix}_")
            if obj.data.materials:
                new_mats =[get_or_copy_material(mat, suffix) for mat in obj.data.materials if mat]
                obj.data.materials.clear()
                for nm in new_mats: obj.data.materials.append(nm)
            saved_col.objects.link(obj)
            col.objects.unlink(obj)
        self.report({'INFO'}, f"{len(objs_to_detach)} 個のオブジェクトを保存しました")
        return {'FINISHED'}

class SFC_OT_SwitchCamera(Operator):
    bl_idname = f"{CONFIG.PREFIX}.switch_camera"; bl_label = "カメラを切り替え"; cam_index: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj or cam_obj.type != 'CAMERA': 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
        props.lens_focal_length = cam_obj.data.lens; props.clip_start = cam_obj.data.clip_start; props.clip_end = cam_obj.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 = props.offset_pitch = 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"{CONFIG.PREFIX}.apply_grid_color"; bl_label = "Apply Grid Color"
    def execute(self, context): bpy.context.preferences.themes[0].view_3d.grid = context.scene.theme_grid_properties.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 CONFIG.GRID_PRESETS if p[0] == props.grid_preset), props.grid_color)
        getattr(bpy.ops, f"{CONFIG.PREFIX}.apply_grid_color")()

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

class SFC_OT_ResetProperty(Operator):
    bl_idname = f"{CONFIG.PREFIX}.reset_property"; bl_label = "プロパティリセット"; targets: CollectionProperty(type=TargetProperty); prop_group_name: StringProperty()
    def execute(self, context):
        props = context.scene.surface_camera_properties
        groups = {"ypr":["offset_yaw", "offset_pitch", "offset_roll"], "aim":["target_location"], "clip":["clip_start", "clip_end", "lens_focal_length"]}
        to_reset = set(p for t in self.targets for p in (groups.get(t.name,[]) if t.name != "all" else sum(groups.values(),[])))
        props.is_updating_settings = True
        for p in 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"{CONFIG.PREFIX}.set_fov"; bl_label = "FOV設定"; fov: FloatProperty(default=0.0)
    def execute(self, context): context.scene.surface_camera_properties.lens_focal_length = calculate_focal_length(self.fov); return {'FINISHED'}

class SFC_OT_OpenURL(Operator):
    bl_idname = f"{CONFIG.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"{CONFIG.PREFIX}.remove_addon"; bl_label = "アドオン解除"
    def execute(self, context): bpy.ops.preferences.addon_disable(module=__name__.split('.')[0]); unregister(); return {'FINISHED'}

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

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

class ZIONAD_SWT_OT_LoadHdriFromList(Operator):
    bl_idname = f"{CONFIG.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(CONFIG.HDRI_PATHS):
            props.hdri_list_index = self.hdri_index; props.background_mode = 'HDRI'; load_hdri_from_path(CONFIG.HDRI_PATHS[self.hdri_index], context); update_background_mode(props, context)
        return {'FINISHED'}

class ZIONAD_SWT_OT_ResetTransform(Operator):
    bl_idname = f"{CONFIG.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 nodes and (mn := find_node(nodes, 'ShaderNodeMapping', 'Mapping')):
            mn.inputs[self.property_to_reset].default_value = (1, 1, 1) if self.property_to_reset == 'Scale' else (0, 0, 0)
        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, props = self.layout, 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:
            for idx in ["1", "2", "3"]:
                b = box_init.box(); b.label(text=f"Cam {idx} 初期値")
                col = b.column(align=True); col.prop(props, f"cam{idx}_init_loc", text="位置"); col.prop(props, f"cam{idx}_init_tgt", text="注視")
                row_ops = b.row(align=True)
                row_ops.operator(SFC_OT_GetCameraInitInfo.bl_idname, text="取得", icon='RESTRICT_VIEW_OFF').cam_index = idx
                row_ops.operator(SFC_OT_ResetCameraInit.bl_idname, text="リセット", icon='LOOP_BACK').cam_index = idx
                row_ops.operator(SFC_OT_CopyCameraInitInfo.bl_idname, text="コピー", icon='COPYDOWN').cam_index = idx
        layout.separator(); box = layout.box(); box.label(text="操作するカメラを選択:", icon='VIEW_CAMERA'); row = box.row(align=True)
        for i in["1", "2", "3"]: row.operator(SFC_OT_SwitchCamera.bl_idname, text=f"Cam {i}", depress=(props.camera_obj and props.camera_obj.name==f"Fixed_Cam_{i}")).cam_index = i
        box.label(text=f"操作・描画中: {props.camera_obj.name}" if props.camera_obj else "操作カメラ未選択", icon='CAMERA_DATA' if props.camera_obj else 'ERROR')
        box.separator(); box.box().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, props = self.layout, context.scene.surface_camera_properties
        box = layout.box(); box.label(text="回転・注視点のコントロール", icon='MOUSE_LMB')
        if props.camera_obj: box.label(text=f"現在の位置: {tuple(round(v, 2) for v in props.camera_obj.location)} (固定)")
        col1 = box.column(align=True); r1 = col1.row(align=True); r1.label(text="注視点"); r1.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "aim"; col1.prop(props, "target_location", text="")
        box.separator(); col2 = box.column(align=True); r2 = col2.row(align=True); r2.label(text="視線オフセット (YPR)"); r2.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "ypr"
        for p in ["offset_yaw", "offset_pitch", "offset_roll"]: col2.prop(props, p)

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, props = self.layout, context.scene.surface_camera_properties
        box = layout.box(); box.label(text="透視投影ビューの操作", icon='VIEW3D'); box.operator(SFC_OT_GetViewportInfo.bl_idname, icon='RESTRICT_VIEW_OFF', text="現在の視座・注視点を取得")
        row = box.row(align=True)
        for i in ["1", "2", "3"]: row.operator(SFC_OT_SetViewportToCamera.bl_idname, text=f"Cam{i} 視座へ", icon='CAMERA_DATA').cam_index = i
        col = box.column(align=True); col.prop(props, "viewport_location"); col.prop(props, "viewport_target")
        box.separator(); row_vp = box.row(align=True); row_vp.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="コピー"); row_vp.operator(SFC_OT_ResetViewportLocation.bl_idname, icon='LOOP_BACK', text="位置リセット"); row_vp.operator(SFC_OT_ResetViewportTarget.bl_idname, icon='LOOP_BACK', text="注視リセット")
        layout.separator(); box_vis = layout.box(); box_vis.label(text="生成オブジェクト 表示 / 非表示", icon='RESTRICT_VIEW_OFF'); row_vis = box_vis.row(align=True)
        for p, t in[("vis_vp_sphere_1", "球体1"), ("vis_vp_sphere_2", "球体2"), ("vis_vp_circles", "交差円")]: row_vis.prop(props, p, text=t, toggle=True)
        layout.separator(); box_sp = layout.box(); box_sp.label(text="透明球体 & 交差平面の円 生成・情報", icon='SPHERE'); col_sp = box_sp.column(align=True)
        col_sp.prop(props, "intersect_plane"); col_sp.prop(props, "sphere_mode"); col_sp.prop(props, "sphere_radius" if props.sphere_mode == 'RADIUS' else "intersect_circle_radius")
        col_sp.separator(); r_col = col_sp.row(align=True); r_col.prop(props, "sphere_color", text="球体1 色"); r_col.prop(props, "sphere_color_2", text="球体2 色")
        r_circ = col_sp.row(align=True); r_circ.prop(props, "circle_thickness"); r_circ.prop(props, "circle_color", text="")
        
        vp_loc, plane = props.viewport_location, props.intersect_plane
        d = abs(vp_loc.z) if plane == 'XY' else abs(vp_loc.x) if plane == 'YZ' else abs(vp_loc.y)
        R, r_circ = (props.sphere_radius, math.sqrt(max(0, props.sphere_radius**2 - d**2))) if props.sphere_mode == 'RADIUS' else (math.sqrt(d**2 + props.intersect_circle_radius**2), props.intersect_circle_radius)
        col_i = box_sp.column(align=True); col_i.label(text=f"平面までの距離: {d:.2f}"); col_i.label(text=f"球の半径: {R:.2f}"); col_i.label(text=f"交差円 半径: {r_circ:.2f}")
        box_sp.separator(); col_g = box_sp.column(align=True); col_g.operator(SFC_OT_GenerateViewportSphere.bl_idname, icon='MESH_UVSPHERE'); col_g.operator(SFC_OT_CopySphereInfo.bl_idname, icon='COPYDOWN'); col_g.operator(SFC_OT_DetachSpheres.bl_idname, icon='UNLINKED')

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, props = self.layout, context.scene.surface_camera_properties
        if props.camera_obj and props.camera_obj.data: layout.box().prop(props.camera_obj.data, "type", text="投影タイプ")
        box = layout.box(); col = box.column(align=True); r = col.row(align=True); r.label(text="レンズとクリップ"); r.operator(f"{CONFIG.PREFIX}.reset_property", text="", icon='LOOP_BACK').targets.add().name = "clip"
        col.prop(props, "lens_focal_length"); r2 = col.row(align=True); r2.label(text="水平視野角:"); r2.label(text=props.info_horizontal_fov); col.label(text="FOVプリセット:")
        r3 = col.row(align=True); c1, c2 = r3.column(align=True), r3.column(align=True)
        for i, fov in enumerate(CONFIG.FOV_PRESETS): (c1 if i % 2 == 0 else c2).operator(f"{CONFIG.PREFIX}.set_fov", text=f"{fov}°").fov = fov
        col.separator(); r4 = col.row(align=True); r4.prop(props, "clip_start"); r4.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
        layout.box().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, overlay = cam.data, getattr(context.space_data, 'overlay', None)
        layout.label(text="Active Camera: " + cam.name, icon='CAMERA_DATA'); bp = layout.box(); bp.label(text="Passepartout", icon='MOD_MASK'); cp = bp.column(align=True); cp.prop(cam_data, "show_passepartout"); rp = cp.row(); rp.enabled = cam_data.show_passepartout; rp.prop(cam_data, "passepartout_alpha")
        if not overlay: return
        layout.separator(); bd = layout.box(); bd.label(text="Viewport Display", icon='OVERLAY'); bd.prop(overlay, "show_overlays"); co = bd.column(); co.enabled = overlay.show_overlays; co.prop(overlay, "show_extras"); cd = co.column(); cd.enabled = overlay.show_extras; cd.prop(overlay, "show_text"); cd.prop(cam_data, "show_name"); cd.prop(cam_data, "show_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, props = self.layout, 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
        bm = layout.box(); bm.label(text="Background Mode", icon='WORLD'); bm.prop(props, "background_mode", expand=True); layout.separator()
        if props.background_mode == 'HDRI':
            be = layout.box(); be.label(text="Environment Texture", icon='IMAGE_DATA'); cl = be.column(align=True)
            for i, path in enumerate(CONFIG.HDRI_PATHS): cl.operator(f"{CONFIG.PREFIX}.load_hdri_from_list", text=os.path.basename(path), depress=(props.hdri_list_index == i)).hdri_index = i
            be.separator(); en = find_node(nodes, 'ShaderNodeTexEnvironment', 'Environment_Texture')
            if en: be.template_ID(en, "image", open="image.open", text="Select HDRI")
        elif props.background_mode == 'SKY':
            bs = layout.box(); sn = find_node(nodes, 'ShaderNodeTexSky', 'Sky_Texture')
            if sn: bs.prop(sn, "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"{CONFIG.PREFIX}.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"{CONFIG.PREFIX}.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; b1 = layout.box(); b1.label(text="ドキュメント", icon='HELP')
        for link in CONFIG.NEW_DOC_LINKS: b1.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').url = link["url"]
        b2 = layout.box(); b2.label(text="ソーシャル", icon='WORLD_DATA')
        for link in CONFIG.SOCIAL_LINKS: b2.operator(f"{CONFIG.PREFIX}.open_url", text=link["label"], icon='URL').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"{CONFIG.PREFIX}.remove_addon", icon='CANCEL')

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

def initial_setup():
    wm = bpy.context.window_manager
    if not wm: return 0.1
    for window in wm.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 bpy.context.scene.world and bpy.context.scene.world.use_nodes:
        props = bpy.context.scene.zionad_swt_props
        nodes = bpy.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
            props.background_mode = 'SKY' if source_node.type == 'TEX_SKY' else 'HDRI'
        update_background_mode(props, bpy.context)
    return None

classes = (
    ThemeGridProperties, ThemeWireProperties, TargetProperty, SurfaceCameraProperties, ZIONAD_SWT_Properties,
    SFC_OT_GridApplyColor, SFC_OT_GridCopyColor, SFC_OT_CreateThreeCameras, SFC_OT_GetCameraInitInfo, SFC_OT_ResetCameraInit, SFC_OT_CopyCameraInitInfo, 
    SFC_OT_SetViewportToCamera, SFC_OT_ResetViewportLocation, SFC_OT_ResetViewportTarget, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_GetViewportInfo, SFC_OT_CopySphereInfo, SFC_OT_GenerateViewportSphere, SFC_OT_DetachSpheres,
    SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor, 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():
    _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)
    safe_register_timer(initial_setup, 0.1)

def unregister():
    if on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: bpy.app.handlers.depsgraph_update_post.remove(on_depsgraph_update)
    
    for wrapper in list(TIMER_REGISTRY.values()):
        if bpy.app.timers.is_registered(wrapper):
            bpy.app.timers.unregister(wrapper)
    TIMER_REGISTRY.clear()
        
    for prop_name in['surface_camera_properties', 'theme_grid_properties', 'theme_wire_properties', 'zionad_swt_props']:
        if hasattr(bpy.types.Scene, prop_name): delattr(bpy.types.Scene, prop_name)
            
    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
import time
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, 30),
    "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": "時空図 光の予算配分 20260329", "url": "<https://www.notion.so/20260329-332f5dacaf438016b8f9cff480994ec1>"},
    {"label": "カメラ3台 ジグザク 20260328b", "url": "<https://www.notion.so/20260328b-331f5dacaf4380b9abeed323cd5621a4>"},
    {"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
]

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"]: 2, 
    PANEL_IDS["VIEWPORT_CAM"]: 3, 
    PANEL_IDS["LENS"]: 4, 
    PANEL_IDS["CAMERA_DISPLAY"]: 5, 
    PANEL_IDS["WORLD_CONTROL"]: 6, 
    PANEL_IDS["GRID"]: 89, 
    PANEL_IDS["WIRE"]: 90, 
    PANEL_IDS["LINKS"]: 190, 
    PANEL_IDS["REMOVE"]: 200,
}

# ======================================================================
# --- ロック機構 & タイマー管理 ---
# ======================================================================

def set_update_lock(scene, state: bool):
    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():
    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 get_or_create_color_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
    bsdf = None
    if mat.use_nodes:
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED':
                bsdf = node
                break
    if bsdf:
        if "Base Color" in bsdf.inputs:
            bsdf.inputs["Base Color"].default_value = color
        if "Alpha" in bsdf.inputs:
            bsdf.inputs["Alpha"].default_value = color[3]
    mat.blend_method = 'BLEND'
    return mat

def create_sphere_object(name, collection, loc, radius, mat):
    """ 交差しない場合に使用する、分割されていない1つの球体を生成 """
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
    bm.to_mesh(me)
    bm.free()
    for poly in me.polygons:
        poly.use_smooth = True
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    if mat: obj.data.materials.append(mat)
    collection.objects.link(obj)
    return obj

def create_split_spheres(name_prefix, collection, loc, radius, mat1, mat2, plane):
    """ 指定平面で切断された2つの球体オブジェクトを生成する """
    if plane == 'XY':
        norm = mathutils.Vector((0, 0, 1))
        co = mathutils.Vector((0, 0, -loc.z))
    elif plane == 'YZ':
        norm = mathutils.Vector((1, 0, 0))
        co = mathutils.Vector((-loc.x, 0, 0))
    else: # 'ZX'
        norm = mathutils.Vector((0, 1, 0))
        co = mathutils.Vector((0, -loc.y, 0))
        
    objs = []
    mats =[mat1, mat2]
    
    for i, clear_in in enumerate([True, False]):
        me = bpy.data.meshes.new(f"{name_prefix}_{i+1}")
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
        
        # 平面でメッシュを切断
        bmesh.ops.bisect_plane(
            bm,
            geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
            dist=0.0001,
            plane_co=co,
            plane_no=norm,
            clear_inner=clear_in,
            clear_outer=not clear_in
        )
        bm.to_mesh(me)
        bm.free()
        
        if len(me.vertices) > 0:
            for poly in me.polygons:
                poly.use_smooth = True
            obj = bpy.data.objects.new(f"{name_prefix}_{i+1}", me)
            obj.location = loc
            if mats[i]: obj.data.materials.append(mats[i])
            collection.objects.link(obj)
            objs.append(obj)
        else:
            bpy.data.meshes.remove(me)
            
    return objs

def create_ring_object(name, collection, loc, rot, major_radius, minor_radius, mat):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    
    r_outer = major_radius + minor_radius
    r_inner = max(0.001, major_radius - minor_radius)
    
    segments = 64
    verts_outer =[]
    verts_inner =[]
    
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        c, s = math.cos(angle), math.sin(angle)
        verts_outer.append(bm.verts.new((r_outer * c, r_outer * s, 0.0)))
        verts_inner.append(bm.verts.new((r_inner * c, r_inner * s, 0.0)))
        
    for i in range(segments):
        ni = (i + 1) % segments
        bm.faces.new((verts_outer[i], verts_outer[ni], verts_inner[ni], verts_inner[i]))
        
    bm.to_mesh(me)
    bm.free()
    
    for poly in me.polygons:
        poly.use_smooth = True
        
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    obj.rotation_euler = rot
    if mat: obj.data.materials.append(mat)
    
    mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
    mod.thickness = minor_radius * 2
    mod.offset = 0.0
    
    collection.objects.link(obj)
    return obj

def update_group_visibility(prefix, hide):
    col = bpy.data.collections.get("VP_Objects")
    if not col: return
    for obj in col.objects:
        if obj.name.startswith(prefix):
            obj.hide_viewport = hide
            obj.hide_render = hide

def update_group_visibility_exact(name, hide):
    col = bpy.data.collections.get("VP_Objects")
    if not col: return
    obj = col.objects.get(name)
    if obj:
        obj.hide_viewport = hide
        obj.hide_render = hide

def update_vis_vp_sphere_1(self, context): update_group_visibility_exact("VP_Sphere_1", not self.vis_vp_sphere_1)
def update_vis_vp_sphere_2(self, context): update_group_visibility_exact("VP_Sphere_2", not self.vis_vp_sphere_2)
def update_vis_vp_circles(self, context): update_group_visibility("VP_Circle", not self.vis_vp_circles)

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

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.object_active = self.wire_color
    context.preferences.themes[0].view_3d.wire = 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 _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, 1.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, 20.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)
    
    # オブジェクト表示トグル
    vis_vp_sphere_1: BoolProperty(name="球体1", default=True, update=update_vis_vp_sphere_1)
    vis_vp_sphere_2: BoolProperty(name="球体2", default=True, update=update_vis_vp_sphere_2)
    vis_vp_circles: BoolProperty(name="交差円", default=True, update=update_vis_vp_circles)
    
    # 透明球体・交差円 の設定プロパティ
    intersect_plane: EnumProperty(
        name="交差平面",
        items=[('XY', "XY平面 (Z=0)", ""), ('YZ', "YZ平面 (X=0)", ""), ('ZX', "ZX平面 (Y=0)", "")],
        default='ZX'
    )
    sphere_mode: EnumProperty(
        name="サイズ指定モード",
        items=[('RADIUS', "球の半径を指定", ""), ('CIRCLE', "交差円の半径を指定", "")],
        default='RADIUS'
    )
    sphere_radius: FloatProperty(name="球の半径", default=10.0, min=0.001)
    intersect_circle_radius: FloatProperty(name="交差円の半径", default=10.0, min=0.001)
    
    sphere_color: FloatVectorProperty(name="球体1 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.5, 0.8, 0.2))
    sphere_color_2: FloatVectorProperty(name="球体2 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.5, 0.1, 0.2))
    circle_color: FloatVectorProperty(name="交差円 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.2, 0.1, 0.8))
    circle_thickness: FloatProperty(name="交差円 太さ", default=0.05, min=0.001)
    
    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 or not props.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):
        col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, get_master_collection(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:
            name = f"Fixed_Cam_{idx}"; cam_obj = bpy.data.objects.get(name)
            
            if cam_obj and cam_obj.type != 'CAMERA':
                cam_obj.name = cam_obj.name + "_old"
                cam_obj = None
                
            if not cam_obj:
                cam_obj = bpy.data.objects.new(name, bpy.data.cameras.new(name=name))
                
            if cam_obj.name not in col.objects:
                col.objects.link(cam_obj)
                
            for c in cam_obj.users_collection:
                if c != col: c.objects.unlink(cam_obj)
                
            set_initial_camera_transform(cam_obj, loc, tgt)
            
        getattr(getattr(bpy.ops, PREFIX), "switch_camera")(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました")
        return {'FINISHED'}

class SFC_OT_GetCameraInitInfo(Operator):
    bl_idname = f"{PREFIX}.get_camera_init_info"
    bl_label = "カメラの現在位置・注視点を取得"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj:
            self.report({'WARNING'}, f"Fixed_Cam_{self.cam_index} が見つかりません。先に生成してください。")
            return {'CANCELLED'}
            
        loc = cam_obj.location.copy()
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        tgt = loc + forward_vec
        
        if self.cam_index == "1":
            props.cam1_init_loc = loc
            props.cam1_init_tgt = tgt
        elif self.cam_index == "2":
            props.cam2_init_loc = loc
            props.cam2_init_tgt = tgt
        elif self.cam_index == "3":
            props.cam3_init_loc = loc
            props.cam3_init_tgt = tgt
            
        self.report({'INFO'}, f"Cam {self.cam_index} の現在位置・注視点を取得しました")
        return {'FINISHED'}

class SFC_OT_ResetCameraInit(Operator):
    bl_idname = f"{PREFIX}.reset_camera_init"
    bl_label = "カメラを初期値にリセット"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        idx = self.cam_index
        loc = getattr(props, f"cam{idx}_init_loc")
        tgt = getattr(props, f"cam{idx}_init_tgt")
        
        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'}, f"Cam {idx} を初期値にリセットしました")
        return {'FINISHED'}

class SFC_OT_CopyCameraInitInfo(Operator):
    bl_idname = f"{PREFIX}.copy_camera_init_info"
    bl_label = "初期値情報をコピー"
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        idx = self.cam_index
        loc = getattr(props, f"cam{idx}_init_loc")
        tgt = getattr(props, f"cam{idx}_init_tgt")
        
        text = f"Cam {idx}: 位置 ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f}) / 注視 ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        context.window_manager.clipboard = text
        self.report({'INFO'}, f"Cam {idx} の初期値をコピーしました")
        return {'FINISHED'}

class SFC_OT_SetViewportToCamera(Operator):
    bl_idname = f"{PREFIX}.set_viewport_to_camera"
    bl_label = "指定カメラの視座を透視投影に適用"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj:
            self.report({'WARNING'}, f"Fixed_Cam_{self.cam_index} が見つかりません。")
            return {'CANCELLED'}
        
        loc = cam_obj.location.copy()
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        tgt = loc + forward_vec
        
        props.viewport_location = loc
        props.viewport_target = tgt
        
        self.report({'INFO'}, f"透視投影ビューを Cam {self.cam_index} の視座に合わせました")
        return {'FINISHED'}

class SFC_OT_ResetViewportLocation(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_location"
    bl_label = "視座位置をリセット"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        context.scene.surface_camera_properties.viewport_location = (0.0, -10.0, 5.0)
        self.report({'INFO'}, "視座位置をリセットしました")
        return {'FINISHED'}

class SFC_OT_ResetViewportTarget(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_target"
    bl_label = "注視点をリセット"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        context.scene.surface_camera_properties.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, tgt = props.viewport_location, props.viewport_target
        context.window_manager.clipboard = f"視座位置: ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f})\n注視点: ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました"); return {'FINISHED'}

class SFC_OT_GetViewportInfo(Operator):
    bl_idname = f"{PREFIX}.get_viewport_info"
    bl_label = "現在の視座・注視点を取得"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        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:
                            loc = rv3d.view_matrix.inverted().translation
                            tgt = rv3d.view_location
                            set_update_lock(context.scene, True)
                            try:
                                props.viewport_location = loc
                                props.viewport_target = tgt
                            finally: trigger_delayed_unlock()
                            self.report({'INFO'}, "現在の透視投影ビューの視座・注視点を取得しました")
                            return {'FINISHED'}
        self.report({'WARNING'}, "3Dビューが見つかりませんでした")
        return {'CANCELLED'}

class SFC_OT_CopySphereInfo(Operator):
    bl_idname = f"{PREFIX}.copy_sphere_info"
    bl_label = "球体・円情報をコピー"
    def execute(self, context):
        props = context.scene.surface_camera_properties
        vp_loc = props.viewport_location
        plane = props.intersect_plane
        
        if plane == 'XY':
            d = abs(vp_loc.z)
            plane_str = "XY平面 (Z=0)"
        elif plane == 'YZ':
            d = abs(vp_loc.x)
            plane_str = "YZ平面 (X=0)"
        else:
            d = abs(vp_loc.y)
            plane_str = "ZX平面 (Y=0)"
            
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        lines =[
            f"視座位置: ({vp_loc.x:.3f}, {vp_loc.y:.3f}, {vp_loc.z:.3f})",
            f"交差平面: {plane_str}",
            f"平面までの距離: {d:.3f}",
            f"球の半径: {R:.3f}",
            f"交差円の半径: {r_circ:.3f}"
        ]
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "透明球体と円の情報をコピーしました")
        return {'FINISHED'}

class SFC_OT_GenerateViewportSphere(Operator):
    bl_idname = f"{PREFIX}.generate_viewport_sphere"
    bl_label = "透明球体と交差円を生成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        master_col = get_master_collection(context)
        col = get_or_create_collection(context, "VP_Objects", master_col)
        
        objs_to_remove =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        for obj in objs_to_remove:
            bpy.data.objects.remove(obj, do_unlink=True)
            
        vp_loc = mathutils.Vector(props.viewport_location)
        plane = props.intersect_plane
        
        if plane == 'XY':
            d = abs(vp_loc.z)
            circle_loc = mathutils.Vector((vp_loc.x, vp_loc.y, 0.0))
            circle_rot = mathutils.Euler((0.0, 0.0, 0.0), 'XYZ')
        elif plane == 'YZ':
            d = abs(vp_loc.x)
            circle_loc = mathutils.Vector((0.0, vp_loc.y, vp_loc.z))
            circle_rot = mathutils.Euler((0.0, math.pi/2, 0.0), 'XYZ')
        else: # ZX
            d = abs(vp_loc.y)
            circle_loc = mathutils.Vector((vp_loc.x, 0.0, vp_loc.z))
            circle_rot = mathutils.Euler((math.pi/2, 0.0, 0.0), 'XYZ')
            
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        mat_sphere_1 = get_or_create_color_material("Mat_VP_Sphere_1", props.sphere_color)
        mat_sphere_2 = get_or_create_color_material("Mat_VP_Sphere_2", props.sphere_color_2)
        mat_circle = get_or_create_color_material("Mat_VP_Circle", props.circle_color)
        
        if r_circ > 0.001:
            # 平面で分割された2つの球体を別々のマテリアルで生成
            create_split_spheres("VP_Sphere", col, vp_loc, R, mat_sphere_1, mat_sphere_2, plane)
            create_ring_object("VP_Circle", col, circle_loc, circle_rot, r_circ, props.circle_thickness, mat_circle)
            self.report({'INFO'}, "透明球体(2分割)と交差円を生成しました")
        else:
            # 球体が平面に届かず交差しない場合は、分割せずに1つの球体として生成し、色は球体1を使う
            create_sphere_object("VP_Sphere_1", col, vp_loc, R, mat_sphere_1)
            self.report({'INFO'}, "球体が交差平面に届かないため、分割せずに1つの球体を生成しました")
            
        update_group_visibility_exact("VP_Sphere_1", not props.vis_vp_sphere_1)
        update_group_visibility_exact("VP_Sphere_2", not props.vis_vp_sphere_2)
        update_group_visibility("VP_Circle", not props.vis_vp_circles)
            
        return {'FINISHED'}

class SFC_OT_DetachSpheres(Operator):
    bl_idname = f"{PREFIX}.detach_spheres"
    bl_label = "アドオンから切り離して残す"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        master_col = get_master_collection(context)
        col = bpy.data.collections.get("VP_Objects")
        if not col:
            self.report({'WARNING'}, "切り離すオブジェクトがありません。")
            return {'CANCELLED'}
            
        saved_col = get_or_create_collection(context, "Saved_Objects", master_col)
        
        count = 0
        objs_to_detach =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        
        if not objs_to_detach:
            self.report({'WARNING'}, "切り離す対象が見つかりませんでした。先に生成してください。")
            return {'CANCELLED'}
            
        suffix = str(int(time.time() * 1000))[-4:]
        
        for obj in objs_to_detach:
            obj.name = obj.name.replace("VP_", f"Saved_{suffix}_")
            
            # マテリアルを複製して独立化
            if obj.data.materials:
                new_mats =[]
                for mat in obj.data.materials:
                    if mat:
                        new_mat = mat.copy()
                        new_mat.name = mat.name.replace("Mat_VP_", f"Mat_Saved_{suffix}_")
                        new_mats.append(new_mat)
                obj.data.materials.clear()
                for new_mat in new_mats:
                    obj.data.materials.append(new_mat)
            
            saved_col.objects.link(obj)
            col.objects.unlink(obj)
            count += 1
            
        self.report({'INFO'}, f"{count} 個のオブジェクトを切り離して保存しました")
        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 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:
            for idx in["1", "2", "3"]:
                b = box_init.box()
                b.label(text=f"Cam {idx} 初期値")
                col = b.column(align=True)
                col.prop(props, f"cam{idx}_init_loc", text="位置")
                col.prop(props, f"cam{idx}_init_tgt", text="注視")
                
                row_ops = b.row(align=True)
                op_get = row_ops.operator(SFC_OT_GetCameraInitInfo.bl_idname, text="取得", icon='RESTRICT_VIEW_OFF')
                op_get.cam_index = idx
                op_reset = row_ops.operator(SFC_OT_ResetCameraInit.bl_idname, text="リセット", icon='LOOP_BACK')
                op_reset.cam_index = idx
                op_copy = row_ops.operator(SFC_OT_CopyCameraInitInfo.bl_idname, text="コピー", icon='COPYDOWN')
                op_copy.cam_index = idx
            
        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')
        box.operator(SFC_OT_GetViewportInfo.bl_idname, icon='RESTRICT_VIEW_OFF', text="現在の視座・注視点を取得")
        
        row_apply = box.row(align=True)
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam1 視座へ", icon='CAMERA_DATA').cam_index = "1"
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam2 視座へ", icon='CAMERA_DATA').cam_index = "2"
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam3 視座へ", icon='CAMERA_DATA').cam_index = "3"
        
        col = box.column(align=True)
        col.prop(props, "viewport_location")
        col.prop(props, "viewport_target")
        
        box.separator()
        row_vp_ops = box.row(align=True)
        row_vp_ops.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="コピー")
        row_vp_ops.operator(SFC_OT_ResetViewportLocation.bl_idname, icon='LOOP_BACK', text="視座位置リセット")
        row_vp_ops.operator(SFC_OT_ResetViewportTarget.bl_idname, icon='LOOP_BACK', text="注視点リセット")

        layout.separator()
        
        box_vis = layout.box()
        box_vis.label(text="生成オブジェクト 表示 / 非表示", icon='RESTRICT_VIEW_OFF')
        row_vis = box_vis.row(align=True)
        row_vis.prop(props, "vis_vp_sphere_1", text="球体1", toggle=True)
        row_vis.prop(props, "vis_vp_sphere_2", text="球体2", toggle=True)
        row_vis.prop(props, "vis_vp_circles", text="交差円", toggle=True)

        layout.separator()
        
        box_sphere = layout.box()
        box_sphere.label(text="透明球体 & 交差平面の円 生成・情報", icon='SPHERE')
        col_sphere = box_sphere.column(align=True)
        col_sphere.prop(props, "intersect_plane")
        col_sphere.prop(props, "sphere_mode")
        
        if props.sphere_mode == 'RADIUS':
            col_sphere.prop(props, "sphere_radius")
        else:
            col_sphere.prop(props, "intersect_circle_radius")
            
        col_sphere.separator()
        row_colors = col_sphere.row(align=True)
        row_colors.prop(props, "sphere_color", text="球体1 色")
        row_colors.prop(props, "sphere_color_2", text="球体2 色")
        
        row_circ = col_sphere.row(align=True)
        row_circ.prop(props, "circle_thickness")
        row_circ.prop(props, "circle_color", text="")
        
        # --- 情報計算 ---
        vp_loc = props.viewport_location
        plane = props.intersect_plane
        
        if plane == 'XY': d = abs(vp_loc.z)
        elif plane == 'YZ': d = abs(vp_loc.x)
        else: d = abs(vp_loc.y)
        
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        col_info = box_sphere.column(align=True)
        col_info.label(text=f"平面までの距離: {d:.2f}")
        col_info.label(text=f"球の半径: {R:.2f}")
        col_info.label(text=f"交差円 半径: {r_circ:.2f}")
        
        box_sphere.separator()
        col_gen = box_sphere.column(align=True)
        col_gen.operator(SFC_OT_GenerateViewportSphere.bl_idname, text="透明球体と交差円を生成", icon='MESH_UVSPHERE')
        col_gen.operator(SFC_OT_CopySphereInfo.bl_idname, text="球体・円情報をコピー", icon='COPYDOWN')
        col_gen.operator(SFC_OT_DetachSpheres.bl_idname, text="アドオンから切り離して残す", icon='UNLINKED')

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_GetCameraInitInfo, SFC_OT_ResetCameraInit, SFC_OT_CopyCameraInitInfo, 
    SFC_OT_SetViewportToCamera, SFC_OT_ResetViewportLocation, SFC_OT_ResetViewportTarget, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_GetViewportInfo, SFC_OT_CopySphereInfo, SFC_OT_GenerateViewportSphere, SFC_OT_DetachSpheres,
    SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor,
    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
import time
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, 28),
    "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": "時空図 光の予算配分 20260329", "url": "<https://www.notion.so/20260329-332f5dacaf438016b8f9cff480994ec1>"},
    {"label": "カメラ3台 ジグザク 20260328b", "url": "<https://www.notion.so/20260328b-331f5dacaf4380b9abeed323cd5621a4>"},
    {"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
]

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"]: 2, 
    PANEL_IDS["VIEWPORT_CAM"]: 3, 
    PANEL_IDS["LENS"]: 4, 
    PANEL_IDS["CAMERA_DISPLAY"]: 5, 
    PANEL_IDS["WORLD_CONTROL"]: 6, 
    PANEL_IDS["GRID"]: 89, 
    PANEL_IDS["WIRE"]: 90, 
    PANEL_IDS["LINKS"]: 190, 
    PANEL_IDS["REMOVE"]: 200,
}

# ======================================================================
# --- ロック機構 & タイマー管理 ---
# ======================================================================

def set_update_lock(scene, state: bool):
    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():
    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 get_or_create_color_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
    bsdf = None
    if mat.use_nodes:
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED':
                bsdf = node
                break
    if bsdf:
        if "Base Color" in bsdf.inputs:
            bsdf.inputs["Base Color"].default_value = color
        if "Alpha" in bsdf.inputs:
            bsdf.inputs["Alpha"].default_value = color[3]
    mat.blend_method = 'BLEND'
    return mat

def create_split_spheres(name_prefix, collection, loc, radius, mat1, mat2, plane):
    """ 指定平面で切断された2つの球体オブジェクトを生成する """
    if plane == 'XY':
        norm = mathutils.Vector((0, 0, 1))
        co = mathutils.Vector((0, 0, -loc.z))
    elif plane == 'YZ':
        norm = mathutils.Vector((1, 0, 0))
        co = mathutils.Vector((-loc.x, 0, 0))
    else: # 'ZX'
        norm = mathutils.Vector((0, 1, 0))
        co = mathutils.Vector((0, -loc.y, 0))
        
    objs = []
    mats =[mat1, mat2]
    
    for i, clear_in in enumerate([True, False]):
        me = bpy.data.meshes.new(f"{name_prefix}_{i+1}")
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
        
        # 平面でメッシュを切断
        bmesh.ops.bisect_plane(
            bm,
            geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
            dist=0.0001,
            plane_co=co,
            plane_no=norm,
            clear_inner=clear_in,
            clear_outer=not clear_in
        )
        bm.to_mesh(me)
        bm.free()
        
        if len(me.vertices) > 0:
            for poly in me.polygons:
                poly.use_smooth = True
            obj = bpy.data.objects.new(f"{name_prefix}_{i+1}", me)
            obj.location = loc
            if mats[i]: obj.data.materials.append(mats[i])
            collection.objects.link(obj)
            objs.append(obj)
        else:
            bpy.data.meshes.remove(me)
            
    return objs

def create_ring_object(name, collection, loc, rot, major_radius, minor_radius, mat):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    
    r_outer = major_radius + minor_radius
    r_inner = max(0.001, major_radius - minor_radius)
    
    segments = 64
    verts_outer =[]
    verts_inner =[]
    
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        c, s = math.cos(angle), math.sin(angle)
        verts_outer.append(bm.verts.new((r_outer * c, r_outer * s, 0.0)))
        verts_inner.append(bm.verts.new((r_inner * c, r_inner * s, 0.0)))
        
    for i in range(segments):
        ni = (i + 1) % segments
        bm.faces.new((verts_outer[i], verts_outer[ni], verts_inner[ni], verts_inner[i]))
        
    bm.to_mesh(me)
    bm.free()
    
    for poly in me.polygons:
        poly.use_smooth = True
        
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    obj.rotation_euler = rot
    if mat: obj.data.materials.append(mat)
    
    mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
    mod.thickness = minor_radius * 2
    mod.offset = 0.0
    
    collection.objects.link(obj)
    return obj

def update_group_visibility(prefix, hide):
    col = bpy.data.collections.get("VP_Objects")
    if not col: return
    for obj in col.objects:
        if obj.name.startswith(prefix):
            obj.hide_viewport = hide
            obj.hide_render = hide

def update_group_visibility_exact(name, hide):
    col = bpy.data.collections.get("VP_Objects")
    if not col: return
    obj = col.objects.get(name)
    if obj:
        obj.hide_viewport = hide
        obj.hide_render = hide

def update_vis_vp_sphere_1(self, context): update_group_visibility_exact("VP_Sphere_1", not self.vis_vp_sphere_1)
def update_vis_vp_sphere_2(self, context): update_group_visibility_exact("VP_Sphere_2", not self.vis_vp_sphere_2)
def update_vis_vp_circles(self, context): update_group_visibility("VP_Circle", not self.vis_vp_circles)

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

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.object_active = self.wire_color
    context.preferences.themes[0].view_3d.wire = 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 _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, 1.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, 20.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)
    
    # オブジェクト表示トグル
    vis_vp_sphere_1: BoolProperty(name="球体1", default=True, update=update_vis_vp_sphere_1)
    vis_vp_sphere_2: BoolProperty(name="球体2", default=True, update=update_vis_vp_sphere_2)
    vis_vp_circles: BoolProperty(name="交差円", default=True, update=update_vis_vp_circles)
    
    # 透明球体・交差円 の設定プロパティ
    intersect_plane: EnumProperty(
        name="交差平面",
        items=[('XY', "XY平面 (Z=0)", ""), ('YZ', "YZ平面 (X=0)", ""), ('ZX', "ZX平面 (Y=0)", "")],
        default='ZX'
    )
    sphere_mode: EnumProperty(
        name="サイズ指定モード",
        items=[('RADIUS', "球の半径を指定", ""), ('CIRCLE', "交差円の半径を指定", "")],
        default='RADIUS'
    )
    sphere_radius: FloatProperty(name="球の半径", default=10.0, min=0.001)
    intersect_circle_radius: FloatProperty(name="交差円の半径", default=10.0, min=0.001)
    
    sphere_color: FloatVectorProperty(name="球体1 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.5, 0.8, 0.2))
    sphere_color_2: FloatVectorProperty(name="球体2 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.5, 0.1, 0.2))
    circle_color: FloatVectorProperty(name="交差円 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.2, 0.1, 0.8))
    circle_thickness: FloatProperty(name="交差円 太さ", default=0.05, min=0.001)
    
    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 or not props.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):
        col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, get_master_collection(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:
            name = f"Fixed_Cam_{idx}"; cam_obj = bpy.data.objects.get(name)
            if cam_obj and cam_obj.type != 'CAMERA': cam_obj = None
            if not cam_obj:
                cam_obj = bpy.data.objects.new(name, bpy.data.cameras.new(name=name))
                col.objects.link(cam_obj)
                if context.scene.collection.objects.get(cam_obj.name): context.scene.collection.objects.unlink(cam_obj)
            set_initial_camera_transform(cam_obj, loc, tgt)
        getattr(getattr(bpy.ops, PREFIX), "switch_camera")(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました")
        return {'FINISHED'}

class SFC_OT_GetCameraInitInfo(Operator):
    bl_idname = f"{PREFIX}.get_camera_init_info"
    bl_label = "カメラの現在位置・注視点を取得"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj:
            self.report({'WARNING'}, f"Fixed_Cam_{self.cam_index} が見つかりません。先に生成してください。")
            return {'CANCELLED'}
            
        loc = cam_obj.location.copy()
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        tgt = loc + forward_vec
        
        if self.cam_index == "1":
            props.cam1_init_loc = loc
            props.cam1_init_tgt = tgt
        elif self.cam_index == "2":
            props.cam2_init_loc = loc
            props.cam2_init_tgt = tgt
        elif self.cam_index == "3":
            props.cam3_init_loc = loc
            props.cam3_init_tgt = tgt
            
        self.report({'INFO'}, f"Cam {self.cam_index} の現在位置・注視点を取得しました")
        return {'FINISHED'}

class SFC_OT_ResetCameraInit(Operator):
    bl_idname = f"{PREFIX}.reset_camera_init"
    bl_label = "カメラを初期値にリセット"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        idx = self.cam_index
        loc = getattr(props, f"cam{idx}_init_loc")
        tgt = getattr(props, f"cam{idx}_init_tgt")
        
        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'}, f"Cam {idx} を初期値にリセットしました")
        return {'FINISHED'}

class SFC_OT_CopyCameraInitInfo(Operator):
    bl_idname = f"{PREFIX}.copy_camera_init_info"
    bl_label = "初期値情報をコピー"
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        idx = self.cam_index
        loc = getattr(props, f"cam{idx}_init_loc")
        tgt = getattr(props, f"cam{idx}_init_tgt")
        
        text = f"Cam {idx}: 位置 ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f}) / 注視 ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        context.window_manager.clipboard = text
        self.report({'INFO'}, f"Cam {idx} の初期値をコピーしました")
        return {'FINISHED'}

class SFC_OT_SetViewportToCamera(Operator):
    bl_idname = f"{PREFIX}.set_viewport_to_camera"
    bl_label = "指定カメラの視座を透視投影に適用"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj:
            self.report({'WARNING'}, f"Fixed_Cam_{self.cam_index} が見つかりません。")
            return {'CANCELLED'}
        
        loc = cam_obj.location.copy()
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        tgt = loc + forward_vec
        
        props.viewport_location = loc
        props.viewport_target = tgt
        
        self.report({'INFO'}, f"透視投影ビューを Cam {self.cam_index} の視座に合わせました")
        return {'FINISHED'}

class SFC_OT_ResetViewportLocation(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_location"
    bl_label = "視座位置をリセット"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        context.scene.surface_camera_properties.viewport_location = (0.0, -10.0, 5.0)
        self.report({'INFO'}, "視座位置をリセットしました")
        return {'FINISHED'}

class SFC_OT_ResetViewportTarget(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_target"
    bl_label = "注視点をリセット"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        context.scene.surface_camera_properties.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, tgt = props.viewport_location, props.viewport_target
        context.window_manager.clipboard = f"視座位置: ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f})\n注視点: ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました"); return {'FINISHED'}

class SFC_OT_GetViewportInfo(Operator):
    bl_idname = f"{PREFIX}.get_viewport_info"
    bl_label = "現在の視座・注視点を取得"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        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:
                            loc = rv3d.view_matrix.inverted().translation
                            tgt = rv3d.view_location
                            set_update_lock(context.scene, True)
                            try:
                                props.viewport_location = loc
                                props.viewport_target = tgt
                            finally: trigger_delayed_unlock()
                            self.report({'INFO'}, "現在の透視投影ビューの視座・注視点を取得しました")
                            return {'FINISHED'}
        self.report({'WARNING'}, "3Dビューが見つかりませんでした")
        return {'CANCELLED'}

class SFC_OT_CopySphereInfo(Operator):
    bl_idname = f"{PREFIX}.copy_sphere_info"
    bl_label = "球体・円情報をコピー"
    def execute(self, context):
        props = context.scene.surface_camera_properties
        vp_loc = props.viewport_location
        plane = props.intersect_plane
        
        if plane == 'XY':
            d = abs(vp_loc.z)
            plane_str = "XY平面 (Z=0)"
        elif plane == 'YZ':
            d = abs(vp_loc.x)
            plane_str = "YZ平面 (X=0)"
        else:
            d = abs(vp_loc.y)
            plane_str = "ZX平面 (Y=0)"
            
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        lines =[
            f"視座位置: ({vp_loc.x:.3f}, {vp_loc.y:.3f}, {vp_loc.z:.3f})",
            f"交差平面: {plane_str}",
            f"平面までの距離: {d:.3f}",
            f"球の半径: {R:.3f}",
            f"交差円の半径: {r_circ:.3f}"
        ]
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "透明球体と円の情報をコピーしました")
        return {'FINISHED'}

class SFC_OT_GenerateViewportSphere(Operator):
    bl_idname = f"{PREFIX}.generate_viewport_sphere"
    bl_label = "透明球体と交差円を生成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        master_col = get_master_collection(context)
        col = get_or_create_collection(context, "VP_Objects", master_col)
        
        objs_to_remove =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        for obj in objs_to_remove:
            bpy.data.objects.remove(obj, do_unlink=True)
            
        vp_loc = mathutils.Vector(props.viewport_location)
        plane = props.intersect_plane
        
        if plane == 'XY':
            d = abs(vp_loc.z)
            circle_loc = mathutils.Vector((vp_loc.x, vp_loc.y, 0.0))
            circle_rot = mathutils.Euler((0.0, 0.0, 0.0), 'XYZ')
        elif plane == 'YZ':
            d = abs(vp_loc.x)
            circle_loc = mathutils.Vector((0.0, vp_loc.y, vp_loc.z))
            circle_rot = mathutils.Euler((0.0, math.pi/2, 0.0), 'XYZ')
        else: # ZX
            d = abs(vp_loc.y)
            circle_loc = mathutils.Vector((vp_loc.x, 0.0, vp_loc.z))
            circle_rot = mathutils.Euler((math.pi/2, 0.0, 0.0), 'XYZ')
            
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        mat_sphere_1 = get_or_create_color_material("Mat_VP_Sphere_1", props.sphere_color)
        mat_sphere_2 = get_or_create_color_material("Mat_VP_Sphere_2", props.sphere_color_2)
        mat_circle = get_or_create_color_material("Mat_VP_Circle", props.circle_color)
        
        # 平面で分割された2つの球体を別々のマテリアルで生成
        create_split_spheres("VP_Sphere", col, vp_loc, R, mat_sphere_1, mat_sphere_2, plane)
        
        if r_circ > 0.001:
            create_ring_object("VP_Circle", col, circle_loc, circle_rot, r_circ, props.circle_thickness, mat_circle)
            
        update_group_visibility_exact("VP_Sphere_1", not props.vis_vp_sphere_1)
        update_group_visibility_exact("VP_Sphere_2", not props.vis_vp_sphere_2)
        update_group_visibility("VP_Circle", not props.vis_vp_circles)
            
        self.report({'INFO'}, "透明球体(2分割)と交差円を生成しました")
        return {'FINISHED'}

class SFC_OT_DetachSpheres(Operator):
    bl_idname = f"{PREFIX}.detach_spheres"
    bl_label = "アドオンから切り離して残す"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        master_col = get_master_collection(context)
        col = bpy.data.collections.get("VP_Objects")
        if not col:
            self.report({'WARNING'}, "切り離すオブジェクトがありません。")
            return {'CANCELLED'}
            
        saved_col = get_or_create_collection(context, "Saved_Objects", master_col)
        
        count = 0
        objs_to_detach =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        
        if not objs_to_detach:
            self.report({'WARNING'}, "切り離す対象が見つかりませんでした。先に生成してください。")
            return {'CANCELLED'}
            
        suffix = str(int(time.time() * 1000))[-4:]
        
        for obj in objs_to_detach:
            obj.name = obj.name.replace("VP_", f"Saved_{suffix}_")
            
            # マテリアルを複製して独立化
            if obj.data.materials:
                new_mats =[]
                for mat in obj.data.materials:
                    if mat:
                        new_mat = mat.copy()
                        new_mat.name = mat.name.replace("Mat_VP_", f"Mat_Saved_{suffix}_")
                        new_mats.append(new_mat)
                obj.data.materials.clear()
                for new_mat in new_mats:
                    obj.data.materials.append(new_mat)
            
            saved_col.objects.link(obj)
            col.objects.unlink(obj)
            count += 1
            
        self.report({'INFO'}, f"{count} 個のオブジェクトを切り離して保存しました")
        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 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:
            for idx in["1", "2", "3"]:
                b = box_init.box()
                b.label(text=f"Cam {idx} 初期値")
                col = b.column(align=True)
                col.prop(props, f"cam{idx}_init_loc", text="位置")
                col.prop(props, f"cam{idx}_init_tgt", text="注視")
                
                row_ops = b.row(align=True)
                op_get = row_ops.operator(SFC_OT_GetCameraInitInfo.bl_idname, text="取得", icon='RESTRICT_VIEW_OFF')
                op_get.cam_index = idx
                op_reset = row_ops.operator(SFC_OT_ResetCameraInit.bl_idname, text="リセット", icon='LOOP_BACK')
                op_reset.cam_index = idx
                op_copy = row_ops.operator(SFC_OT_CopyCameraInitInfo.bl_idname, text="コピー", icon='COPYDOWN')
                op_copy.cam_index = idx
            
        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')
        box.operator(SFC_OT_GetViewportInfo.bl_idname, icon='RESTRICT_VIEW_OFF', text="現在の視座・注視点を取得")
        
        row_apply = box.row(align=True)
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam1 視座へ", icon='CAMERA_DATA').cam_index = "1"
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam2 視座へ", icon='CAMERA_DATA').cam_index = "2"
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam3 視座へ", icon='CAMERA_DATA').cam_index = "3"
        
        col = box.column(align=True)
        col.prop(props, "viewport_location")
        col.prop(props, "viewport_target")
        
        box.separator()
        row_vp_ops = box.row(align=True)
        row_vp_ops.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="コピー")
        row_vp_ops.operator(SFC_OT_ResetViewportLocation.bl_idname, icon='LOOP_BACK', text="視座位置リセット")
        row_vp_ops.operator(SFC_OT_ResetViewportTarget.bl_idname, icon='LOOP_BACK', text="注視点リセット")

        layout.separator()
        
        box_vis = layout.box()
        box_vis.label(text="生成オブジェクト 表示 / 非表示", icon='RESTRICT_VIEW_OFF')
        row_vis = box_vis.row(align=True)
        row_vis.prop(props, "vis_vp_sphere_1", text="球体1", toggle=True)
        row_vis.prop(props, "vis_vp_sphere_2", text="球体2", toggle=True)
        row_vis.prop(props, "vis_vp_circles", text="交差円", toggle=True)

        layout.separator()
        
        box_sphere = layout.box()
        box_sphere.label(text="透明球体 & 交差平面の円 生成・情報", icon='SPHERE')
        col_sphere = box_sphere.column(align=True)
        col_sphere.prop(props, "intersect_plane")
        col_sphere.prop(props, "sphere_mode")
        
        if props.sphere_mode == 'RADIUS':
            col_sphere.prop(props, "sphere_radius")
        else:
            col_sphere.prop(props, "intersect_circle_radius")
            
        col_sphere.separator()
        row_colors = col_sphere.row(align=True)
        row_colors.prop(props, "sphere_color", text="球体1 色")
        row_colors.prop(props, "sphere_color_2", text="球体2 色")
        
        row_circ = col_sphere.row(align=True)
        row_circ.prop(props, "circle_thickness")
        row_circ.prop(props, "circle_color", text="")
        
        # --- 情報計算 ---
        vp_loc = props.viewport_location
        plane = props.intersect_plane
        
        if plane == 'XY': d = abs(vp_loc.z)
        elif plane == 'YZ': d = abs(vp_loc.x)
        else: d = abs(vp_loc.y)
        
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        col_info = box_sphere.column(align=True)
        col_info.label(text=f"平面までの距離: {d:.2f}")
        col_info.label(text=f"球の半径: {R:.2f}")
        col_info.label(text=f"交差円 半径: {r_circ:.2f}")
        
        box_sphere.separator()
        box_sphere.operator(SFC_OT_GenerateViewportSphere.bl_idname, text="透明球体と交差円を生成", icon='MESH_UVSPHERE')
        box_sphere.operator(SFC_OT_CopySphereInfo.bl_idname, text="球体・円情報をコピー", icon='COPYDOWN')
        box_sphere.operator(SFC_OT_DetachSpheres.bl_idname, text="アドオンから切り離して残す", icon='UNLINKED')

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_GetCameraInitInfo, SFC_OT_ResetCameraInit, SFC_OT_CopyCameraInitInfo, 
    SFC_OT_SetViewportToCamera, SFC_OT_ResetViewportLocation, SFC_OT_ResetViewportTarget, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_GetViewportInfo, SFC_OT_CopySphereInfo, SFC_OT_GenerateViewportSphere, SFC_OT_DetachSpheres,
    SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor,
    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
import time
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, 27),
    "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": "時空図 光の予算配分 20260329", "url": "<https://www.notion.so/20260329-332f5dacaf438016b8f9cff480994ec1>"},
    {"label": "カメラ3台 ジグザク 20260328b", "url": "<https://www.notion.so/20260328b-331f5dacaf4380b9abeed323cd5621a4>"},
    {"label": "THIS_ADDON[ カメラ3台 ジグザク 20260328 ]", "url": "<https://www.notion.so/20260328-330f5dacaf43808eae2dcc7e31f14bec>"},
]

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"]: 2, 
    PANEL_IDS["VIEWPORT_CAM"]: 3, 
    PANEL_IDS["LENS"]: 4, 
    PANEL_IDS["CAMERA_DISPLAY"]: 5, 
    PANEL_IDS["WORLD_CONTROL"]: 6, 
    PANEL_IDS["GRID"]: 89, 
    PANEL_IDS["WIRE"]: 90, 
    PANEL_IDS["LINKS"]: 190, 
    PANEL_IDS["REMOVE"]: 200,
}

# ======================================================================
# --- ロック機構 & タイマー管理 ---
# ======================================================================

def set_update_lock(scene, state: bool):
    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():
    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 get_or_create_color_material(name, color):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name=name)
        mat.use_nodes = True
    bsdf = None
    if mat.use_nodes:
        for node in mat.node_tree.nodes:
            if node.type == 'BSDF_PRINCIPLED':
                bsdf = node
                break
    if bsdf:
        if "Base Color" in bsdf.inputs:
            bsdf.inputs["Base Color"].default_value = color
        if "Alpha" in bsdf.inputs:
            bsdf.inputs["Alpha"].default_value = color[3]
    mat.blend_method = 'BLEND'
    return mat

def create_split_spheres(name_prefix, collection, loc, radius, mat, plane):
    """ 指定平面で切断された2つの球体オブジェクトを生成する """
    if plane == 'XY':
        norm = mathutils.Vector((0, 0, 1))
        co = mathutils.Vector((0, 0, -loc.z))
    elif plane == 'YZ':
        norm = mathutils.Vector((1, 0, 0))
        co = mathutils.Vector((-loc.x, 0, 0))
    else: # 'ZX'
        norm = mathutils.Vector((0, 1, 0))
        co = mathutils.Vector((0, -loc.y, 0))
        
    objs = []
    
    for i, clear_in in enumerate([True, False]):
        me = bpy.data.meshes.new(f"{name_prefix}_{i+1}")
        bm = bmesh.new()
        bmesh.ops.create_uvsphere(bm, u_segments=64, v_segments=32, radius=radius)
        
        # 平面でメッシュを切断
        bmesh.ops.bisect_plane(
            bm,
            geom=bm.verts[:] + bm.edges[:] + bm.faces[:],
            dist=0.0001,
            plane_co=co,
            plane_no=norm,
            clear_inner=clear_in,
            clear_outer=not clear_in
        )
        bm.to_mesh(me)
        bm.free()
        
        if len(me.vertices) > 0:
            for poly in me.polygons:
                poly.use_smooth = True
            obj = bpy.data.objects.new(f"{name_prefix}_{i+1}", me)
            obj.location = loc
            if mat: obj.data.materials.append(mat)
            collection.objects.link(obj)
            objs.append(obj)
        else:
            bpy.data.meshes.remove(me)
            
    return objs

def create_ring_object(name, collection, loc, rot, major_radius, minor_radius, mat):
    me = bpy.data.meshes.new(name)
    bm = bmesh.new()
    
    r_outer = major_radius + minor_radius
    r_inner = max(0.001, major_radius - minor_radius)
    
    segments = 64
    verts_outer =[]
    verts_inner =[]
    
    for i in range(segments):
        angle = 2.0 * math.pi * i / segments
        c, s = math.cos(angle), math.sin(angle)
        verts_outer.append(bm.verts.new((r_outer * c, r_outer * s, 0.0)))
        verts_inner.append(bm.verts.new((r_inner * c, r_inner * s, 0.0)))
        
    for i in range(segments):
        ni = (i + 1) % segments
        bm.faces.new((verts_outer[i], verts_outer[ni], verts_inner[ni], verts_inner[i]))
        
    bm.to_mesh(me)
    bm.free()
    
    for poly in me.polygons:
        poly.use_smooth = True
        
    obj = bpy.data.objects.new(name, me)
    obj.location = loc
    obj.rotation_euler = rot
    if mat: obj.data.materials.append(mat)
    
    mod = obj.modifiers.new(name="Solidify", type='SOLIDIFY')
    mod.thickness = minor_radius * 2
    mod.offset = 0.0
    
    collection.objects.link(obj)
    return obj

def update_group_visibility(prefix, hide):
    col = bpy.data.collections.get("VP_Objects")
    if not col: return
    for obj in col.objects:
        if obj.name.startswith(prefix):
            obj.hide_viewport = hide
            obj.hide_render = hide

def update_group_visibility_exact(name, hide):
    col = bpy.data.collections.get("VP_Objects")
    if not col: return
    obj = col.objects.get(name)
    if obj:
        obj.hide_viewport = hide
        obj.hide_render = hide

def update_vis_vp_sphere_1(self, context): update_group_visibility_exact("VP_Sphere_1", not self.vis_vp_sphere_1)
def update_vis_vp_sphere_2(self, context): update_group_visibility_exact("VP_Sphere_2", not self.vis_vp_sphere_2)
def update_vis_vp_circles(self, context): update_group_visibility("VP_Circle", not self.vis_vp_circles)

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

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.object_active = self.wire_color
    context.preferences.themes[0].view_3d.wire = 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 _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, 1.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, 20.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)
    
    # オブジェクト表示トグル
    vis_vp_sphere_1: BoolProperty(name="球体1", default=True, update=update_vis_vp_sphere_1)
    vis_vp_sphere_2: BoolProperty(name="球体2", default=True, update=update_vis_vp_sphere_2)
    vis_vp_circles: BoolProperty(name="交差円", default=True, update=update_vis_vp_circles)
    
    # 透明球体・交差円 の設定プロパティ
    intersect_plane: EnumProperty(
        name="交差平面",
        items=[('XY', "XY平面 (Z=0)", ""), ('YZ', "YZ平面 (X=0)", ""), ('ZX', "ZX平面 (Y=0)", "")],
        default='ZX'
    )
    sphere_mode: EnumProperty(
        name="サイズ指定モード",
        items=[('RADIUS', "球の半径を指定", ""), ('CIRCLE', "交差円の半径を指定", "")],
        default='RADIUS'
    )
    sphere_radius: FloatProperty(name="球の半径", default=10.0, min=0.001)
    intersect_circle_radius: FloatProperty(name="交差円の半径", default=10.0, min=0.001)
    
    sphere_color: FloatVectorProperty(name="透明球体 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.1, 0.5, 0.8, 0.2))
    circle_color: FloatVectorProperty(name="交差円 色", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.8, 0.2, 0.1, 0.8))
    circle_thickness: FloatProperty(name="交差円 太さ", default=0.05, min=0.001)
    
    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 or not props.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):
        col = get_or_create_collection(context, CAMERA_COLLECTION_NAME, get_master_collection(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:
            name = f"Fixed_Cam_{idx}"; cam_obj = bpy.data.objects.get(name)
            if cam_obj and cam_obj.type != 'CAMERA': cam_obj = None
            if not cam_obj:
                cam_obj = bpy.data.objects.new(name, bpy.data.cameras.new(name=name))
                col.objects.link(cam_obj)
                if context.scene.collection.objects.get(cam_obj.name): context.scene.collection.objects.unlink(cam_obj)
            set_initial_camera_transform(cam_obj, loc, tgt)
        getattr(getattr(bpy.ops, PREFIX), "switch_camera")(cam_index="1")
        self.report({'INFO'}, "3つのカメラを生成しました")
        return {'FINISHED'}

class SFC_OT_GetCameraInitInfo(Operator):
    bl_idname = f"{PREFIX}.get_camera_init_info"
    bl_label = "カメラの現在位置・注視点を取得"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj:
            self.report({'WARNING'}, f"Fixed_Cam_{self.cam_index} が見つかりません。先に生成してください。")
            return {'CANCELLED'}
            
        loc = cam_obj.location.copy()
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        tgt = loc + forward_vec
        
        if self.cam_index == "1":
            props.cam1_init_loc = loc
            props.cam1_init_tgt = tgt
        elif self.cam_index == "2":
            props.cam2_init_loc = loc
            props.cam2_init_tgt = tgt
        elif self.cam_index == "3":
            props.cam3_init_loc = loc
            props.cam3_init_tgt = tgt
            
        self.report({'INFO'}, f"Cam {self.cam_index} の現在位置・注視点を取得しました")
        return {'FINISHED'}

class SFC_OT_ResetCameraInit(Operator):
    bl_idname = f"{PREFIX}.reset_camera_init"
    bl_label = "カメラを初期値にリセット"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        idx = self.cam_index
        loc = getattr(props, f"cam{idx}_init_loc")
        tgt = getattr(props, f"cam{idx}_init_tgt")
        
        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'}, f"Cam {idx} を初期値にリセットしました")
        return {'FINISHED'}

class SFC_OT_CopyCameraInitInfo(Operator):
    bl_idname = f"{PREFIX}.copy_camera_init_info"
    bl_label = "初期値情報をコピー"
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        idx = self.cam_index
        loc = getattr(props, f"cam{idx}_init_loc")
        tgt = getattr(props, f"cam{idx}_init_tgt")
        
        text = f"Cam {idx}: 位置 ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f}) / 注視 ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        context.window_manager.clipboard = text
        self.report({'INFO'}, f"Cam {idx} の初期値をコピーしました")
        return {'FINISHED'}

class SFC_OT_SetViewportToCamera(Operator):
    bl_idname = f"{PREFIX}.set_viewport_to_camera"
    bl_label = "指定カメラの視座を透視投影に適用"
    bl_options = {'REGISTER', 'UNDO'}
    cam_index: StringProperty()
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        cam_obj = bpy.data.objects.get(f"Fixed_Cam_{self.cam_index}")
        if not cam_obj:
            self.report({'WARNING'}, f"Fixed_Cam_{self.cam_index} が見つかりません。")
            return {'CANCELLED'}
        
        loc = cam_obj.location.copy()
        forward_vec = mathutils.Vector((0.0, 0.0, -100.0))
        forward_vec.rotate(cam_obj.rotation_euler)
        tgt = loc + forward_vec
        
        props.viewport_location = loc
        props.viewport_target = tgt
        
        self.report({'INFO'}, f"透視投影ビューを Cam {self.cam_index} の視座に合わせました")
        return {'FINISHED'}

class SFC_OT_ResetViewportLocation(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_location"
    bl_label = "視座位置をリセット"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        context.scene.surface_camera_properties.viewport_location = (0.0, -10.0, 5.0)
        self.report({'INFO'}, "視座位置をリセットしました")
        return {'FINISHED'}

class SFC_OT_ResetViewportTarget(Operator):
    bl_idname = f"{PREFIX}.reset_viewport_target"
    bl_label = "注視点をリセット"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        context.scene.surface_camera_properties.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, tgt = props.viewport_location, props.viewport_target
        context.window_manager.clipboard = f"視座位置: ({loc.x:.2f}, {loc.y:.2f}, {loc.z:.2f})\n注視点: ({tgt.x:.2f}, {tgt.y:.2f}, {tgt.z:.2f})"
        self.report({'INFO'}, "ビューポートの視座位置・注視点をコピーしました"); return {'FINISHED'}

class SFC_OT_GetViewportInfo(Operator):
    bl_idname = f"{PREFIX}.get_viewport_info"
    bl_label = "現在の視座・注視点を取得"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        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:
                            loc = rv3d.view_matrix.inverted().translation
                            tgt = rv3d.view_location
                            set_update_lock(context.scene, True)
                            try:
                                props.viewport_location = loc
                                props.viewport_target = tgt
                            finally: trigger_delayed_unlock()
                            self.report({'INFO'}, "現在の透視投影ビューの視座・注視点を取得しました")
                            return {'FINISHED'}
        self.report({'WARNING'}, "3Dビューが見つかりませんでした")
        return {'CANCELLED'}

class SFC_OT_CopySphereInfo(Operator):
    bl_idname = f"{PREFIX}.copy_sphere_info"
    bl_label = "球体・円情報をコピー"
    def execute(self, context):
        props = context.scene.surface_camera_properties
        vp_loc = props.viewport_location
        plane = props.intersect_plane
        
        if plane == 'XY':
            d = abs(vp_loc.z)
            plane_str = "XY平面 (Z=0)"
        elif plane == 'YZ':
            d = abs(vp_loc.x)
            plane_str = "YZ平面 (X=0)"
        else:
            d = abs(vp_loc.y)
            plane_str = "ZX平面 (Y=0)"
            
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        lines =[
            f"視座位置: ({vp_loc.x:.3f}, {vp_loc.y:.3f}, {vp_loc.z:.3f})",
            f"交差平面: {plane_str}",
            f"平面までの距離: {d:.3f}",
            f"球の半径: {R:.3f}",
            f"交差円の半径: {r_circ:.3f}"
        ]
        context.window_manager.clipboard = "\n".join(lines)
        self.report({'INFO'}, "透明球体と円の情報をコピーしました")
        return {'FINISHED'}

class SFC_OT_GenerateViewportSphere(Operator):
    bl_idname = f"{PREFIX}.generate_viewport_sphere"
    bl_label = "透明球体と交差円を生成"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        props = context.scene.surface_camera_properties
        master_col = get_master_collection(context)
        col = get_or_create_collection(context, "VP_Objects", master_col)
        
        objs_to_remove =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        for obj in objs_to_remove:
            bpy.data.objects.remove(obj, do_unlink=True)
            
        vp_loc = mathutils.Vector(props.viewport_location)
        plane = props.intersect_plane
        
        if plane == 'XY':
            d = abs(vp_loc.z)
            circle_loc = mathutils.Vector((vp_loc.x, vp_loc.y, 0.0))
            circle_rot = mathutils.Euler((0.0, 0.0, 0.0), 'XYZ')
        elif plane == 'YZ':
            d = abs(vp_loc.x)
            circle_loc = mathutils.Vector((0.0, vp_loc.y, vp_loc.z))
            circle_rot = mathutils.Euler((0.0, math.pi/2, 0.0), 'XYZ')
        else: # ZX
            d = abs(vp_loc.y)
            circle_loc = mathutils.Vector((vp_loc.x, 0.0, vp_loc.z))
            circle_rot = mathutils.Euler((math.pi/2, 0.0, 0.0), 'XYZ')
            
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        mat_sphere = get_or_create_color_material("Mat_VP_Sphere", props.sphere_color)
        mat_circle = get_or_create_color_material("Mat_VP_Circle", props.circle_color)
        
        # 平面で分割された2つの球体を生成
        create_split_spheres("VP_Sphere", col, vp_loc, R, mat_sphere, plane)
        
        if r_circ > 0.001:
            create_ring_object("VP_Circle", col, circle_loc, circle_rot, r_circ, props.circle_thickness, mat_circle)
            
        update_group_visibility_exact("VP_Sphere_1", not props.vis_vp_sphere_1)
        update_group_visibility_exact("VP_Sphere_2", not props.vis_vp_sphere_2)
        update_group_visibility("VP_Circle", not props.vis_vp_circles)
            
        self.report({'INFO'}, "透明球体(2分割)と交差円を生成しました")
        return {'FINISHED'}

class SFC_OT_DetachSpheres(Operator):
    bl_idname = f"{PREFIX}.detach_spheres"
    bl_label = "アドオンから切り離して残す"
    bl_options = {'REGISTER', 'UNDO'}
    
    def execute(self, context):
        master_col = get_master_collection(context)
        col = bpy.data.collections.get("VP_Objects")
        if not col:
            self.report({'WARNING'}, "切り離すオブジェクトがありません。")
            return {'CANCELLED'}
            
        saved_col = get_or_create_collection(context, "Saved_Objects", master_col)
        
        count = 0
        objs_to_detach =[obj for obj in col.objects if obj.name.startswith("VP_Sphere") or obj.name.startswith("VP_Circle")]
        
        if not objs_to_detach:
            self.report({'WARNING'}, "切り離す対象が見つかりませんでした。先に生成してください。")
            return {'CANCELLED'}
            
        suffix = str(int(time.time() * 1000))[-4:]
        
        for obj in objs_to_detach:
            obj.name = obj.name.replace("VP_", f"Saved_{suffix}_")
            
            # マテリアルを複製して独立化
            if obj.data.materials:
                new_mats =[]
                for mat in obj.data.materials:
                    if mat:
                        new_mat = mat.copy()
                        new_mat.name = mat.name.replace("Mat_VP_", "Mat_Saved_")
                        new_mats.append(new_mat)
                obj.data.materials.clear()
                for new_mat in new_mats:
                    obj.data.materials.append(new_mat)
            
            saved_col.objects.link(obj)
            col.objects.unlink(obj)
            count += 1
            
        self.report({'INFO'}, f"{count} 個のオブジェクトを切り離して保存しました")
        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 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:
            for idx in["1", "2", "3"]:
                b = box_init.box()
                b.label(text=f"Cam {idx} 初期値")
                col = b.column(align=True)
                col.prop(props, f"cam{idx}_init_loc", text="位置")
                col.prop(props, f"cam{idx}_init_tgt", text="注視")
                
                row_ops = b.row(align=True)
                op_get = row_ops.operator(SFC_OT_GetCameraInitInfo.bl_idname, text="取得", icon='RESTRICT_VIEW_OFF')
                op_get.cam_index = idx
                op_reset = row_ops.operator(SFC_OT_ResetCameraInit.bl_idname, text="リセット", icon='LOOP_BACK')
                op_reset.cam_index = idx
                op_copy = row_ops.operator(SFC_OT_CopyCameraInitInfo.bl_idname, text="コピー", icon='COPYDOWN')
                op_copy.cam_index = idx
            
        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')
        box.operator(SFC_OT_GetViewportInfo.bl_idname, icon='RESTRICT_VIEW_OFF', text="現在の視座・注視点を取得")
        
        row_apply = box.row(align=True)
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam1 視座へ", icon='CAMERA_DATA').cam_index = "1"
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam2 視座へ", icon='CAMERA_DATA').cam_index = "2"
        row_apply.operator(SFC_OT_SetViewportToCamera.bl_idname, text="Cam3 視座へ", icon='CAMERA_DATA').cam_index = "3"
        
        col = box.column(align=True)
        col.prop(props, "viewport_location")
        col.prop(props, "viewport_target")
        
        box.separator()
        row_vp_ops = box.row(align=True)
        row_vp_ops.operator(SFC_OT_CopyViewportInfo.bl_idname, icon='COPYDOWN', text="コピー")
        row_vp_ops.operator(SFC_OT_ResetViewportLocation.bl_idname, icon='LOOP_BACK', text="視座位置リセット")
        row_vp_ops.operator(SFC_OT_ResetViewportTarget.bl_idname, icon='LOOP_BACK', text="注視点リセット")

        layout.separator()
        
        box_vis = layout.box()
        box_vis.label(text="生成オブジェクト 表示 / 非表示", icon='RESTRICT_VIEW_OFF')
        row_vis = box_vis.row(align=True)
        row_vis.prop(props, "vis_vp_sphere_1", text="球体1", toggle=True)
        row_vis.prop(props, "vis_vp_sphere_2", text="球体2", toggle=True)
        row_vis.prop(props, "vis_vp_circles", text="交差円", toggle=True)

        layout.separator()
        
        box_sphere = layout.box()
        box_sphere.label(text="透明球体 & 交差平面の円 生成・情報", icon='SPHERE')
        col_sphere = box_sphere.column(align=True)
        col_sphere.prop(props, "intersect_plane")
        col_sphere.prop(props, "sphere_mode")
        
        if props.sphere_mode == 'RADIUS':
            col_sphere.prop(props, "sphere_radius")
        else:
            col_sphere.prop(props, "intersect_circle_radius")
            
        col_sphere.separator()
        col_sphere.prop(props, "sphere_color")
        
        row_circ = col_sphere.row(align=True)
        row_circ.prop(props, "circle_thickness")
        row_circ.prop(props, "circle_color", text="")
        
        # --- 情報計算 ---
        vp_loc = props.viewport_location
        plane = props.intersect_plane
        
        if plane == 'XY': d = abs(vp_loc.z)
        elif plane == 'YZ': d = abs(vp_loc.x)
        else: d = abs(vp_loc.y)
        
        if props.sphere_mode == 'RADIUS':
            R = props.sphere_radius
            r_circ = math.sqrt(max(0, R**2 - d**2))
        else:
            r_circ = props.intersect_circle_radius
            R = math.sqrt(d**2 + r_circ**2)
            
        col_info = box_sphere.column(align=True)
        col_info.label(text=f"平面までの距離: {d:.2f}")
        col_info.label(text=f"球の半径: {R:.2f}")
        col_info.label(text=f"交差円 半径: {r_circ:.2f}")
        
        box_sphere.separator()
        box_sphere.operator(SFC_OT_GenerateViewportSphere.bl_idname, text="透明球体と交差円を生成", icon='MESH_UVSPHERE')
        box_sphere.operator(SFC_OT_CopySphereInfo.bl_idname, text="球体・円情報をコピー", icon='COPYDOWN')
        
        box_sphere.separator()
        box_sphere.operator(SFC_OT_DetachSpheres.bl_idname, text="アドオンから切り離して残す", icon='UNLINKED')

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_GetCameraInitInfo, SFC_OT_ResetCameraInit, SFC_OT_CopyCameraInitInfo, 
    SFC_OT_SetViewportToCamera, SFC_OT_ResetViewportLocation, SFC_OT_ResetViewportTarget, SFC_OT_SwitchCamera, SFC_OT_ResetProperty, SFC_OT_SetFOV, 
    SFC_OT_CopyViewportInfo, SFC_OT_GetViewportInfo, SFC_OT_CopySphereInfo, SFC_OT_GenerateViewportSphere, SFC_OT_DetachSpheres,
    SFC_OT_OpenURL, SFC_OT_RemoveAddon, SFC_OT_WireApplyColor, SFC_OT_WireCopyColor,
    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()