進化版 平面2つ通過光線

シュウセイチュウ

bl_info = {
    "name": "Polygon Torus Generator Pro (Realtime & Detach)",
    "author": "Your Name",
    "version": (5, 2),
    "blender": (4, 2, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
    "category": "Object",
}

import bpy
import webbrowser
import math
import bmesh

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "多角形ツール"
PREFIX_NAME = "poly_torus"

ADDON_LINKS = [
    {
        "label": "トーラス作成",
        "url": "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>",
        "icon": "URL"
    }
]

INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False

# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ生成】
# =========================================================================
def get_or_create_mat(name, color, alpha=1.0):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
        mat.use_nodes = True
        
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = (*color, 1.0)
        bsdf.inputs["Alpha"].default_value = alpha
        
    mat.diffuse_color = (*color, alpha)
        
    try: mat.surface_render_method = 'BLENDED' if alpha < 1.0 else 'DITHERED'
    except: pass
    try: mat.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
    except: pass
    try: mat.shadow_method = 'NONE'
    except: pass
    
    return mat

def update_skirt_material(mat, color, alpha):
    if not mat.use_nodes:
        mat.use_nodes = True
        
    # 透明レンダリング・裏面表示の基本設定
    try: mat.blend_method = 'BLEND'
    except: pass
    try: mat.surface_render_method = 'BLENDED'
    except: pass
    try: mat.show_transparent_back = True
    except: pass
    try: mat.use_transparency_overlap = True
    except: pass
    try: mat.shadow_method = 'NONE'
    except: pass
    try: mat.use_backface_culling = False
    except: pass
    try: mat.use_backface_culling_shadow = False
    except: pass
    
    tree = mat.node_tree
    
    # 既存の複雑なノードを全クリアしてシンプルな構成に戻す
    tree.nodes.clear()
    
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    
    bsdf.inputs["Base Color"].default_value = (*color, 1.0)
    bsdf.inputs["Alpha"].default_value = alpha
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = (*color, alpha)

def get_polygon_verts(n, r, y_pos):
    verts = []
    radius = r * math.sqrt(2) if n == 4 else r
    phase = math.pi / 4 if n == 4 else 0.0
    for i in range(n):
        angle = phase + 2 * math.pi * i / n
        verts.append((radius * math.cos(angle), y_pos, radius * math.sin(angle)))
    return verts

def create_convex_mesh(verts, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 3: return mesh
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try: bmesh.ops.convex_hull(bm, input=bm.verts)
    except: pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def create_skirt_mesh(verts, y_c, fill_bottom, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 4: return mesh
        
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try: bmesh.ops.convex_hull(bm, input=bm.verts)
    except: pass
        
    if not fill_bottom:
        faces_to_delete = []
        for face in bm.faces:
            is_bottom = True
            for v in face.verts:
                if abs(v.co.y - y_c) > 0.001:
                    is_bottom = False
                    break
            if is_bottom:
                faces_to_delete.append(face)
                
        if faces_to_delete:
            bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
            
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def get_torus_mesh_by_ops(n, r, thick, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    
    radius = r * math.sqrt(2) if n == 4 else r
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_torus_add(
                major_segments=n, minor_segments=12,
                major_radius=radius, minor_radius=thick,
                location=(0,0,0), rotation=(0,0,0)
            )
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackTorus")

def get_sphere_mesh_by_ops(r, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackSphere")

# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
    if abs(y_b - y_a) < 0.0001: return []
    t = (y_c - y_a) / (y_b - y_a)
    pts = []
    for pa in verts_a:
        for pb in verts_b:
            x = pa[0] + t * (pb[0] - pa[0])
            z = pa[2] + t * (pb[2] - pa[2])
            pts.append((x, y_c, z))
    return pts

def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
    px, py, pz = p_loc
    def project_polygon(verts):
        poly2d = []
        for vx, vy, vz in verts:
            if abs(vy - py) < 1e-6: return [] 
            t = (y_c - py) / (vy - py)
            if t < 0: return [] 
            x = px + t * (vx - px)
            z = pz + t * (vz - pz)
            poly2d.append((x, z))
        return poly2d

    def make_ccw(poly):
        if len(poly) < 3: return poly
        area = sum(poly[i][0]*poly[(i+1)%len(poly)][1] - poly[(i+1)%len(poly)][0]*poly[i][1] for i in range(len(poly)))
        return poly[::-1] if area < 0 else poly

    def sutherland_hodgman(subject, clip):
        def inside(p, cp1, cp2):
            return (cp2[0] - cp1[0]) * (p[1] - cp1[1]) - (cp2[1] - cp1[1]) * (p[0] - cp1[0]) >= -1e-8
        def compute_intersection(cp1, cp2, s, e):
            dc, dp = [cp1[0] - cp2[0], cp1[1] - cp2[1]], [s[0] - e[0], s[1] - e[1]]
            n1, n2 = cp1[0] * cp2[1] - cp1[1] * cp2[0], s[0] * e[1] - s[1] * e[0]
            det = dc[0] * dp[1] - dc[1] * dp[0]
            if abs(det) < 1e-8: return s
            n3 = 1.0 / det
            return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3)

        out_poly = subject
        if not out_poly or not clip: return []
        cp1 = clip[-1]
        for cp2 in clip:
            in_poly = out_poly
            out_poly = []
            if not in_poly: break
            s = in_poly[-1]
            for e in in_poly:
                if inside(e, cp1, cp2):
                    if not inside(s, cp1, cp2):
                        out_poly.append(compute_intersection(cp1, cp2, s, e))
                    out_poly.append(e)
                elif inside(s, cp1, cp2):
                    out_poly.append(compute_intersection(cp1, cp2, s, e))
                s = e
            cp1 = cp2
        return out_poly

    poly_a = project_polygon(verts_a)
    poly_b = project_polygon(verts_b)
    if not poly_a or not poly_b: return []
    intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
    return [(x, y_c, z) for (x, z) in intersect_poly2d]

# =========================================================================
# 【リアルタイム更新関数 (投影シミュレーション用)】
# =========================================================================
def update_proj(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    
    props = context.scene.poly_torus_props
    obj_a = bpy.data.objects.get("Proj_Plane_A")
    obj_b = bpy.data.objects.get("Proj_Plane_B")
    obj_c = bpy.data.objects.get("Proj_Plane_C")
    obj_p = bpy.data.objects.get("Proj_Point_P")
    obj_skirt = bpy.data.objects.get("Proj_Skirt")
    
    if not (obj_a and obj_b and obj_c and obj_p and obj_skirt): return
    
    IS_UPDATING = True
    try:
        n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
        n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
        y_c = props.proj_y_c
        
        def update_torus(obj, n, r, thick, y_pos, color):
            mesh = get_torus_mesh_by_ops(n, r, thick, context)
            old_m, old_mat = obj.data, obj.active_material
            obj.data = mesh
            if old_mat: 
                obj.data.materials.append(old_mat)
                if old_mat.use_nodes:
                    bsdf = old_mat.node_tree.nodes.get("Principled BSDF")
                    if bsdf: bsdf.inputs["Base Color"].default_value = (*color, 1.0)
                old_mat.diffuse_color = (*color, 1.0)
            obj.location = (0, y_pos, 0)
            obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
            if old_m: bpy.data.meshes.remove(old_m)

        update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
        update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)

        if props.proj_mode == 'MODE2':
            obj_p.hide_viewport = False
            mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
            old_m_p = obj_p.data
            mat_p = obj_p.active_material
            obj_p.data = mesh_p
            if mat_p:
                obj_p.data.materials.append(mat_p)
                if mat_p.use_nodes:
                    bsdf = mat_p.node_tree.nodes.get("Principled BSDF")
                    if bsdf:
                        bsdf.inputs["Base Color"].default_value = (*props.proj_color_p, 1.0)
                        bsdf.inputs["Alpha"].default_value = props.proj_alpha_p
                mat_p.diffuse_color = (*props.proj_color_p, props.proj_alpha_p)
                
                try: mat_p.surface_render_method = 'BLENDED' if props.proj_alpha_p < 1.0 else 'DITHERED'
                except: pass
                try: mat_p.blend_method = 'BLEND' if props.proj_alpha_p < 1.0 else 'OPAQUE'
                except: pass
                
            if old_m_p: bpy.data.meshes.remove(old_m_p)
            obj_p.location = props.proj_p_loc
        else:
            obj_p.hide_viewport = True

        va = get_polygon_verts(n_a, r_a, y_a)
        vb = get_polygon_verts(n_b, r_b, y_b)
        
        if props.proj_mode == 'MODE1':
            vc = get_projected_verts_mode1(va, vb, y_a, y_b, y_c)
            color_c = props.proj_color_c_m1
        else:
            vc = get_projected_verts_mode2(props.proj_p_loc, va, vb, y_c)
            color_c = props.proj_color_c_m2
        
        mesh_c = create_convex_mesh(vc, "Mesh_C")
        old_mc, old_mat_c = obj_c.data, obj_c.active_material
        obj_c.data = mesh_c
        
        obj_c.hide_viewport = not props.proj_show_c
        obj_c.hide_render = not props.proj_show_c
        
        if old_mat_c: obj_c.data.materials.append(old_mat_c)
        obj_c.location = (0, 0, 0)
        obj_c.rotation_euler = (0, 0, 0)
        if old_mc: bpy.data.meshes.remove(old_mc)
        
        if old_mat_c and old_mat_c.use_nodes:
            bsdf = old_mat_c.node_tree.nodes.get("Principled BSDF")
            if bsdf: bsdf.inputs["Base Color"].default_value = (*color_c, 1.0)
            old_mat_c.diffuse_color = (*color_c, 0.8)

        # スカート更新
        if props.proj_mode == 'MODE2' and props.proj_show_skirt and len(vc) >= 3:
            obj_skirt.hide_viewport = False
            
            verts_skirt = [tuple(props.proj_p_loc)] + vc
            mesh_skirt = create_skirt_mesh(verts_skirt, y_c, props.proj_skirt_fill_bottom, "Mesh_Skirt")
            
            old_m_skirt = obj_skirt.data
            mat_skirt = obj_skirt.active_material
            obj_skirt.data = mesh_skirt
            
            if mat_skirt:
                obj_skirt.data.materials.append(mat_skirt)
                update_skirt_material(
                    mat_skirt, 
                    props.proj_color_skirt, props.proj_alpha_skirt
                )
                
            if old_m_skirt: bpy.data.meshes.remove(old_m_skirt)
            obj_skirt.location = (0, 0, 0)
            obj_skirt.rotation_euler = (0, 0, 0)
        else:
            obj_skirt.hide_viewport = True

    finally:
        IS_UPDATING = False

# =========================================================================
# 【連動制御用アップデート関数】
# =========================================================================
def update_proj_link_p_a(self, context):
    props = context.scene.poly_torus_props
    # 連動開始時の、平面A(Y軸)と球体P(Y座標)の差分をオフセットとして記録
    props.proj_p_a_offset = props.proj_y_a - props.proj_p_loc[1]

def update_proj_p_loc(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if props.proj_link_p_a:
        IS_UPDATING = True
        try:
            # 球体Pの位置変更に合わせて平面AのY座標も連動
            props.proj_y_a = props.proj_p_loc[1] + props.proj_p_a_offset
        finally:
            IS_UPDATING = False
    update_proj(self, context)

def update_proj_y_a(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if props.proj_link_p_a:
        # 平面Aが手動で直接動かされた場合はオフセットの方を再計算
        props.proj_p_a_offset = props.proj_y_a - props.proj_p_loc[1]
    update_proj(self, context)

# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
def update_poly_torus(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if not props.active_obj_name: return
    obj = bpy.data.objects.get(props.active_obj_name)
    if not obj or not obj.get("pt_live"): return
        
    IS_UPDATING = True
    try:
        obj.location = (props.loc_x, props.loc_y, props.loc_z)
        obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
        
        mat = obj.active_material
        if mat and mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                bsdf.inputs["Base Color"].default_value = (*props.color, 1.0)
                bsdf.inputs["Alpha"].default_value = props.alpha
            mat.diffuse_color = (*props.color, props.alpha)
            
            try: mat.surface_render_method = 'BLENDED' if props.alpha < 1.0 else 'DITHERED'
            except: pass
            try: mat.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
            except: pass
        
        mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
        old_mesh = obj.data
        obj.data = mesh_new
        if mat: obj.data.materials.append(mat)
        if old_mesh: bpy.data.meshes.remove(old_mesh)
    finally:
        IS_UPDATING = False

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
    active_obj_name: bpy.props.StringProperty(name="編集中のオブジェクト", default="")
    segments: bpy.props.IntProperty(name="角数", default=3, min=3, max=256, update=update_poly_torus)
    radius: bpy.props.FloatProperty(name="半径 (R)", default=1.0, min=0.0001, update=update_poly_torus)
    thick: bpy.props.FloatProperty(name="太さ (断面)", default=0.05, min=0.001, update=update_poly_torus)
    multiplier_n: bpy.props.IntProperty(name="n =", default=2, min=1)
    color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_COLOR, min=0.0, max=1.0, update=update_poly_torus)
    alpha: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_poly_torus)
    loc_x: bpy.props.FloatProperty(name="位置 X", default=0.0, update=update_poly_torus)
    loc_y: bpy.props.FloatProperty(name="位置 Y", default=0.0, update=update_poly_torus)
    loc_z: bpy.props.FloatProperty(name="位置 Z", default=0.0, update=update_poly_torus)
    rot_x: bpy.props.FloatProperty(name="X軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_y: bpy.props.FloatProperty(name="Y軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_z: bpy.props.FloatProperty(name="Z軸", default=0.0, subtype='ANGLE', update=update_poly_torus)

    proj_mode: bpy.props.EnumProperty(
        name="シミュレーションの種類",
        items=[
            ('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
            ('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
        ],
        default='MODE1',
        update=update_proj
    )
    
    # 指定点P
    proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), update=update_proj_p_loc)
    proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)
    proj_color_p: bpy.props.FloatVectorProperty(name="P 色", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, update=update_proj)
    proj_alpha_p: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_proj)

    # 連動設定用プロパティ
    proj_link_p_a: bpy.props.BoolProperty(name="球体Pと平面AのY座標を連動", default=False, update=update_proj_link_p_a)
    proj_p_a_offset: bpy.props.FloatProperty(name="連動オフセット", default=0.0)

    # スカート機能 (シンプル化: 色と透明度は1つだけ)
    proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
    proj_skirt_fill_bottom: bpy.props.BoolProperty(name="スカートの底面 (フタ) を作成する", default=True, update=update_proj)
    
    proj_color_skirt: bpy.props.FloatVectorProperty(name="スカート色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt: bpy.props.FloatProperty(name="透明度", default=0.5, min=0.0, max=1.0, update=update_proj)

    # 枠A・B
    proj_n_a: bpy.props.IntProperty(name="A 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_a: bpy.props.FloatProperty(name="A サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_a: bpy.props.FloatProperty(name="A 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj_y_a)
    proj_color_a: bpy.props.FloatVectorProperty(name="A 色", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    
    proj_n_b: bpy.props.IntProperty(name="B 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_b: bpy.props.FloatProperty(name="B サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_b: bpy.props.FloatProperty(name="B 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_b: bpy.props.FloatProperty(name="B Y座標", default=0.0, update=update_proj)
    proj_color_b: bpy.props.FloatVectorProperty(name="B 色", subtype='COLOR', default=(0.0, 1.0, 0.5), min=0.0, max=1.0, update=update_proj)
    
    # 平面C (到達範囲)
    proj_y_c: bpy.props.FloatProperty(name="C (投影先) Y座標", default=10.0, update=update_proj)
    proj_show_c: bpy.props.BoolProperty(name="平面C (壁・到達範囲) を表示する", default=True, update=update_proj)
    proj_color_c_m1: bpy.props.FloatVectorProperty(name="C 色(モード1)", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    proj_color_c_m2: bpy.props.FloatVectorProperty(name="C 色(モード2)", subtype='COLOR', default=(1.0, 0.2, 0.8), min=0.0, max=1.0, update=update_proj)

# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_create_projection(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create_proj"
    bl_label = "投影平面を生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.poly_torus_props
        col = context.collection
        
        mat_a = get_or_create_mat("Proj_Mat_A", props.proj_color_a, 1.0)
        mat_b = get_or_create_mat("Proj_Mat_B", props.proj_color_b, 1.0)
        mat_c = get_or_create_mat("Proj_Mat_C", props.proj_color_c_m1, 0.8)
        mat_p = get_or_create_mat("Proj_Mat_P", props.proj_color_p, props.proj_alpha_p)
        mat_skirt = get_or_create_mat("Proj_Mat_Skirt", (1,1,1), 1.0) 

        def create_or_get(name, mat):
            obj = bpy.data.objects.get(name)
            if not obj:
                obj = bpy.data.objects.new(name, bpy.data.meshes.new(f"{name}_Mesh"))
                col.objects.link(obj)
            if len(obj.data.materials) == 0:
                obj.data.materials.append(mat)
            return obj

        create_or_get("Proj_Plane_A", mat_a)
        create_or_get("Proj_Plane_B", mat_b)
        create_or_get("Proj_Plane_C", mat_c)
        create_or_get("Proj_Point_P", mat_p)
        create_or_get("Proj_Skirt", mat_skirt)
        
        update_proj(self, context)
        return {'FINISHED'}

class POLY_OT_create_torus(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create"
    bl_label = "単体多角形を生成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name:
            old_obj = bpy.data.objects.get(props.active_obj_name)
            if old_obj and "pt_live" in old_obj: del old_obj["pt_live"]
        mat = get_or_create_mat(f"{PREFIX_NAME}_Mat", props.color, props.alpha)
        IS_UPDATING = True
        try:
            mesh = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
            obj = bpy.data.objects.new("PolyTorus", mesh)
            context.collection.objects.link(obj)
            obj.data.materials.append(mat)
            obj["pt_live"] = True
            obj.location = (props.loc_x, props.loc_y, props.loc_z)
            obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
            props.active_obj_name = obj.name
            context.view_layer.objects.active = obj
            obj.select_set(True)
        finally: IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_detach_torus(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_detach"
    bl_label = "アドオンから切り離し"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        obj = bpy.data.objects.get(props.active_obj_name)
        if obj and "pt_live" in obj: del obj["pt_live"]
        IS_UPDATING = True; props.active_obj_name = ""; IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_modify_radius(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_modify_radius"
    bl_label = "半径を数値修飾"
    bl_options = {'REGISTER', 'UNDO'}
    action: bpy.props.EnumProperty(items=[('MULTIPLY', "Multiply", ""), ('DIVIDE', "Divide", "")])
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.multiplier_n <= 0: return {'CANCELLED'}
        factor = math.sqrt(props.multiplier_n)
        if self.action == 'MULTIPLY': props.radius *= factor
        elif self.action == 'DIVIDE': props.radius /= factor
        return {'FINISHED'}

class POLY_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル群】
# =========================================================================
class POLY_PT_projection_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_projection_panel"
    bl_label = "直線通過範囲 (シミュレーション)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        layout.operator(f"mesh.{PREFIX_NAME.lower()}_create_proj", text="シミュレーションを生成", icon='LIGHT_SUN')
        layout.separator()
        
        layout.prop(props, "proj_mode", expand=False)
        
        if props.proj_mode == 'MODE2':
            box_p = layout.box()
            box_p.label(text="指定点 P (球体・光源)", icon='LIGHT')
            col_p = box_p.column(align=True)
            col_p.prop(props, "proj_p_loc")
            
            # 連動オプションのUI表示を追加
            col_p.prop(props, "proj_link_p_a", text="球体Pと平面AのY座標を連動", icon='LINKED')
            
            col_p.prop(props, "proj_p_radius")
            
            row_p = col_p.row(align=True)
            row_p.prop(props, "proj_color_p", text="")
            row_p.prop(props, "proj_alpha_p", text="透明度")
            layout.separator()
            
            if props.proj_show_skirt:
                box_s = layout.box()
                box_s.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT', text="光の道筋 (スカート) 設定")
                box_s.prop(props, "proj_skirt_fill_bottom", icon='SNAP_FACE')
                
                row_skirt = box_s.row(align=True)
                row_skirt.prop(props, "proj_color_skirt", text="色")
                row_skirt.prop(props, "proj_alpha_skirt", text="透明度")
            else:
                layout.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT')
            
            layout.separator()
        
        box_a = layout.box()
        box_a.label(text="平面A (窓枠1)", icon='MESH_TORUS')
        col_a = box_a.column(align=True)
        col_a.prop(props, "proj_n_a")
        col_a.prop(props, "proj_r_a")
        col_a.prop(props, "proj_thick_a")
        col_a.prop(props, "proj_y_a")
        col_a.prop(props, "proj_color_a", text="A 色")
        
        box_b = layout.box()
        box_b.label(text="平面B (窓枠2)", icon='MESH_TORUS')
        col_b = box_b.column(align=True)
        col_b.prop(props, "proj_n_b")
        col_b.prop(props, "proj_r_b")
        col_b.prop(props, "proj_thick_b")
        col_b.prop(props, "proj_y_b")
        col_b.prop(props, "proj_color_b", text="B 色")
        
        box_c = layout.box()
        box_c.prop(props, "proj_show_c", icon='RESTRICT_VIEW_OFF' if props.proj_show_c else 'RESTRICT_VIEW_ON')
        if props.proj_show_c:
            col = box_c.column(align=True)
            col.prop(props, "proj_y_c", text="Cの Y座標")
            if props.proj_mode == 'MODE1':
                col.prop(props, "proj_color_c_m1", text="C 色 (青系)")
            else:
                col.prop(props, "proj_color_c_m2", text="C 色 (ピンク系)")

class POLY_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
    bl_label = "単体多角形トーラス (リアルタイム生成)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name and bpy.data.objects.get(props.active_obj_name):
            box_live = layout.box()
            box_live.label(text=f"🔄 編集中: {props.active_obj_name}", icon='EDITMODE_HLT')
            box_live.scale_y = 1.3
            box_live.operator(f"object.{PREFIX_NAME.lower()}_detach", text="アドオンから切り離し", icon='UNLINKED')
            layout.separator()
        else:
            layout.scale_y = 1.3
            layout.operator(f"mesh.{PREFIX_NAME.lower()}_create", text="新しく単体多角形を生成", icon='ADD')
            layout.scale_y = 1.0
            layout.separator()
        box_s = layout.box()
        col_s = box_s.column(align=True)
        col_s.prop(props, "segments")
        col_s.separator()
        col_s.prop(props, "radius")
        row_n = col_s.row(align=True)
        row_n.prop(props, "multiplier_n")
        op_mult = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× √n")
        op_mult.action = 'MULTIPLY'
        op_div = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× 1/√n")
        op_div.action = 'DIVIDE'
        col_s.separator()
        col_s.prop(props, "thick")
        col_s.separator()
        row_c = col_s.row(align=True)
        row_c.prop(props, "color", text="")
        row_c.prop(props, "alpha", text="透明")

class POLY_PT_footer_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_footer_panel"
    bl_label = "ドキュメント / 管理"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_order = 100
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        box_links = layout.box()
        box_links.label(text="関連リンク:", icon='BOOKMARKS')
        for link in ADDON_LINKS:
            box_links.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text=link["label"], icon='URL').url = link["url"]
        layout.separator(factor=2.0)
        layout.operator(f"wm.{PREFIX_NAME.lower()}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    PolyTorusProperties, 
    POLY_OT_create_torus, 
    POLY_OT_create_projection,
    POLY_OT_detach_torus,
    POLY_OT_modify_radius,
    POLY_OT_open_url, 
    POLY_OT_remove_addon,
    POLY_PT_projection_panel,
    POLY_PT_main_panel, 
    POLY_PT_footer_panel
]

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

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

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
bl_info = {
    "name": "Polygon Torus Generator Pro (Realtime & Detach)",
    "author": "Your Name",
    "version": (5, 1),
    "blender": (4, 2, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
    "category": "Object",
}

import bpy
import webbrowser
import math
import bmesh

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "多角形ツール"
PREFIX_NAME = "poly_torus"

ADDON_LINKS = [
    {
        "label": "トーラス作成",
        "url": "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>",
        "icon": "URL"
    }
]

INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False

# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ生成】
# =========================================================================
def get_or_create_mat(name, color, alpha=1.0):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
        mat.use_nodes = True
        
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = (*color, 1.0)
        bsdf.inputs["Alpha"].default_value = alpha
        
    mat.diffuse_color = (*color, alpha)
        
    try: mat.surface_render_method = 'BLENDED' if alpha < 1.0 else 'DITHERED'
    except: pass
    try: mat.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
    except: pass
    try: mat.shadow_method = 'NONE'
    except: pass
    
    return mat

def update_skirt_material(mat, color, alpha):
    if not mat.use_nodes:
        mat.use_nodes = True
        
    # 透明レンダリング・裏面表示の基本設定
    try: mat.blend_method = 'BLEND'
    except: pass
    try: mat.surface_render_method = 'BLENDED'
    except: pass
    try: mat.show_transparent_back = True
    except: pass
    try: mat.use_transparency_overlap = True
    except: pass
    try: mat.shadow_method = 'NONE'
    except: pass
    try: mat.use_backface_culling = False
    except: pass
    try: mat.use_backface_culling_shadow = False
    except: pass
    
    tree = mat.node_tree
    
    # 既存の複雑なノードを全クリアしてシンプルな構成に戻す
    tree.nodes.clear()
    
    bsdf = tree.nodes.new('ShaderNodeBsdfPrincipled')
    out  = tree.nodes.new('ShaderNodeOutputMaterial')
    
    bsdf.inputs["Base Color"].default_value = (*color, 1.0)
    bsdf.inputs["Alpha"].default_value = alpha
    
    tree.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"])
    mat.diffuse_color = (*color, alpha)

def get_polygon_verts(n, r, y_pos):
    verts = []
    radius = r * math.sqrt(2) if n == 4 else r
    phase = math.pi / 4 if n == 4 else 0.0
    for i in range(n):
        angle = phase + 2 * math.pi * i / n
        verts.append((radius * math.cos(angle), y_pos, radius * math.sin(angle)))
    return verts

def create_convex_mesh(verts, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 3: return mesh
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try: bmesh.ops.convex_hull(bm, input=bm.verts)
    except: pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def create_skirt_mesh(verts, y_c, fill_bottom, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 4: return mesh
        
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try: bmesh.ops.convex_hull(bm, input=bm.verts)
    except: pass
        
    if not fill_bottom:
        faces_to_delete = []
        for face in bm.faces:
            is_bottom = True
            for v in face.verts:
                if abs(v.co.y - y_c) > 0.001:
                    is_bottom = False
                    break
            if is_bottom:
                faces_to_delete.append(face)
                
        if faces_to_delete:
            bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
            
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def get_torus_mesh_by_ops(n, r, thick, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    
    radius = r * math.sqrt(2) if n == 4 else r
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_torus_add(
                major_segments=n, minor_segments=12,
                major_radius=radius, minor_radius=thick,
                location=(0,0,0), rotation=(0,0,0)
            )
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackTorus")

def get_sphere_mesh_by_ops(r, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackSphere")

# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
    if abs(y_b - y_a) < 0.0001: return []
    t = (y_c - y_a) / (y_b - y_a)
    pts = []
    for pa in verts_a:
        for pb in verts_b:
            x = pa[0] + t * (pb[0] - pa[0])
            z = pa[2] + t * (pb[2] - pa[2])
            pts.append((x, y_c, z))
    return pts

def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
    px, py, pz = p_loc
    def project_polygon(verts):
        poly2d = []
        for vx, vy, vz in verts:
            if abs(vy - py) < 1e-6: return [] 
            t = (y_c - py) / (vy - py)
            if t < 0: return [] 
            x = px + t * (vx - px)
            z = pz + t * (vz - pz)
            poly2d.append((x, z))
        return poly2d

    def make_ccw(poly):
        if len(poly) < 3: return poly
        area = sum(poly[i][0]*poly[(i+1)%len(poly)][1] - poly[(i+1)%len(poly)][0]*poly[i][1] for i in range(len(poly)))
        return poly[::-1] if area < 0 else poly

    def sutherland_hodgman(subject, clip):
        def inside(p, cp1, cp2):
            return (cp2[0] - cp1[0]) * (p[1] - cp1[1]) - (cp2[1] - cp1[1]) * (p[0] - cp1[0]) >= -1e-8
        def compute_intersection(cp1, cp2, s, e):
            dc, dp = [cp1[0] - cp2[0], cp1[1] - cp2[1]], [s[0] - e[0], s[1] - e[1]]
            n1, n2 = cp1[0] * cp2[1] - cp1[1] * cp2[0], s[0] * e[1] - s[1] * e[0]
            det = dc[0] * dp[1] - dc[1] * dp[0]
            if abs(det) < 1e-8: return s
            n3 = 1.0 / det
            return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3)

        out_poly = subject
        if not out_poly or not clip: return []
        cp1 = clip[-1]
        for cp2 in clip:
            in_poly = out_poly
            out_poly = []
            if not in_poly: break
            s = in_poly[-1]
            for e in in_poly:
                if inside(e, cp1, cp2):
                    if not inside(s, cp1, cp2):
                        out_poly.append(compute_intersection(cp1, cp2, s, e))
                    out_poly.append(e)
                elif inside(s, cp1, cp2):
                    out_poly.append(compute_intersection(cp1, cp2, s, e))
                s = e
            cp1 = cp2
        return out_poly

    poly_a = project_polygon(verts_a)
    poly_b = project_polygon(verts_b)
    if not poly_a or not poly_b: return []
    intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
    return [(x, y_c, z) for (x, z) in intersect_poly2d]

# =========================================================================
# 【リアルタイム更新関数 (投影シミュレーション用)】
# =========================================================================
def update_proj(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    
    props = context.scene.poly_torus_props
    obj_a = bpy.data.objects.get("Proj_Plane_A")
    obj_b = bpy.data.objects.get("Proj_Plane_B")
    obj_c = bpy.data.objects.get("Proj_Plane_C")
    obj_p = bpy.data.objects.get("Proj_Point_P")
    obj_skirt = bpy.data.objects.get("Proj_Skirt")
    
    if not (obj_a and obj_b and obj_c and obj_p and obj_skirt): return
    
    IS_UPDATING = True
    try:
        n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
        n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
        y_c = props.proj_y_c
        
        def update_torus(obj, n, r, thick, y_pos, color):
            mesh = get_torus_mesh_by_ops(n, r, thick, context)
            old_m, old_mat = obj.data, obj.active_material
            obj.data = mesh
            if old_mat: 
                obj.data.materials.append(old_mat)
                if old_mat.use_nodes:
                    bsdf = old_mat.node_tree.nodes.get("Principled BSDF")
                    if bsdf: bsdf.inputs["Base Color"].default_value = (*color, 1.0)
                old_mat.diffuse_color = (*color, 1.0)
            obj.location = (0, y_pos, 0)
            obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
            if old_m: bpy.data.meshes.remove(old_m)

        update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
        update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)

        if props.proj_mode == 'MODE2':
            obj_p.hide_viewport = False
            mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
            old_m_p = obj_p.data
            mat_p = obj_p.active_material
            obj_p.data = mesh_p
            if mat_p:
                obj_p.data.materials.append(mat_p)
                if mat_p.use_nodes:
                    bsdf = mat_p.node_tree.nodes.get("Principled BSDF")
                    if bsdf:
                        bsdf.inputs["Base Color"].default_value = (*props.proj_color_p, 1.0)
                        bsdf.inputs["Alpha"].default_value = props.proj_alpha_p
                mat_p.diffuse_color = (*props.proj_color_p, props.proj_alpha_p)
                
                try: mat_p.surface_render_method = 'BLENDED' if props.proj_alpha_p < 1.0 else 'DITHERED'
                except: pass
                try: mat_p.blend_method = 'BLEND' if props.proj_alpha_p < 1.0 else 'OPAQUE'
                except: pass
                
            if old_m_p: bpy.data.meshes.remove(old_m_p)
            obj_p.location = props.proj_p_loc
        else:
            obj_p.hide_viewport = True

        va = get_polygon_verts(n_a, r_a, y_a)
        vb = get_polygon_verts(n_b, r_b, y_b)
        
        if props.proj_mode == 'MODE1':
            vc = get_projected_verts_mode1(va, vb, y_a, y_b, y_c)
            color_c = props.proj_color_c_m1
        else:
            vc = get_projected_verts_mode2(props.proj_p_loc, va, vb, y_c)
            color_c = props.proj_color_c_m2
        
        mesh_c = create_convex_mesh(vc, "Mesh_C")
        old_mc, old_mat_c = obj_c.data, obj_c.active_material
        obj_c.data = mesh_c
        
        obj_c.hide_viewport = not props.proj_show_c
        obj_c.hide_render = not props.proj_show_c
        
        if old_mat_c: obj_c.data.materials.append(old_mat_c)
        obj_c.location = (0, 0, 0)
        obj_c.rotation_euler = (0, 0, 0)
        if old_mc: bpy.data.meshes.remove(old_mc)
        
        if old_mat_c and old_mat_c.use_nodes:
            bsdf = old_mat_c.node_tree.nodes.get("Principled BSDF")
            if bsdf: bsdf.inputs["Base Color"].default_value = (*color_c, 1.0)
            old_mat_c.diffuse_color = (*color_c, 0.8)

        # スカート更新
        if props.proj_mode == 'MODE2' and props.proj_show_skirt and len(vc) >= 3:
            obj_skirt.hide_viewport = False
            
            verts_skirt = [tuple(props.proj_p_loc)] + vc
            mesh_skirt = create_skirt_mesh(verts_skirt, y_c, props.proj_skirt_fill_bottom, "Mesh_Skirt")
            
            old_m_skirt = obj_skirt.data
            mat_skirt = obj_skirt.active_material
            obj_skirt.data = mesh_skirt
            
            if mat_skirt:
                obj_skirt.data.materials.append(mat_skirt)
                update_skirt_material(
                    mat_skirt, 
                    props.proj_color_skirt, props.proj_alpha_skirt
                )
                
            if old_m_skirt: bpy.data.meshes.remove(old_m_skirt)
            obj_skirt.location = (0, 0, 0)
            obj_skirt.rotation_euler = (0, 0, 0)
        else:
            obj_skirt.hide_viewport = True

    finally:
        IS_UPDATING = False

# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
def update_poly_torus(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if not props.active_obj_name: return
    obj = bpy.data.objects.get(props.active_obj_name)
    if not obj or not obj.get("pt_live"): return
        
    IS_UPDATING = True
    try:
        obj.location = (props.loc_x, props.loc_y, props.loc_z)
        obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
        
        mat = obj.active_material
        if mat and mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                bsdf.inputs["Base Color"].default_value = (*props.color, 1.0)
                bsdf.inputs["Alpha"].default_value = props.alpha
            mat.diffuse_color = (*props.color, props.alpha)
            
            try: mat.surface_render_method = 'BLENDED' if props.alpha < 1.0 else 'DITHERED'
            except: pass
            try: mat.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
            except: pass
        
        mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
        old_mesh = obj.data
        obj.data = mesh_new
        if mat: obj.data.materials.append(mat)
        if old_mesh: bpy.data.meshes.remove(old_mesh)
    finally:
        IS_UPDATING = False

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
    active_obj_name: bpy.props.StringProperty(name="編集中のオブジェクト", default="")
    segments: bpy.props.IntProperty(name="角数", default=3, min=3, max=256, update=update_poly_torus)
    radius: bpy.props.FloatProperty(name="半径 (R)", default=1.0, min=0.0001, update=update_poly_torus)
    thick: bpy.props.FloatProperty(name="太さ (断面)", default=0.05, min=0.001, update=update_poly_torus)
    multiplier_n: bpy.props.IntProperty(name="n =", default=2, min=1)
    color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_COLOR, min=0.0, max=1.0, update=update_poly_torus)
    alpha: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_poly_torus)
    loc_x: bpy.props.FloatProperty(name="位置 X", default=0.0, update=update_poly_torus)
    loc_y: bpy.props.FloatProperty(name="位置 Y", default=0.0, update=update_poly_torus)
    loc_z: bpy.props.FloatProperty(name="位置 Z", default=0.0, update=update_poly_torus)
    rot_x: bpy.props.FloatProperty(name="X軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_y: bpy.props.FloatProperty(name="Y軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_z: bpy.props.FloatProperty(name="Z軸", default=0.0, subtype='ANGLE', update=update_poly_torus)

    proj_mode: bpy.props.EnumProperty(
        name="シミュレーションの種類",
        items=[
            ('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
            ('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
        ],
        default='MODE1',
        update=update_proj
    )
    
    # 指定点P
    proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -10.0, 0.0), update=update_proj)
    proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)
    proj_color_p: bpy.props.FloatVectorProperty(name="P 色", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, update=update_proj)
    proj_alpha_p: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_proj)

    # スカート機能 (シンプル化: 色と透明度は1つだけ)
    proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
    proj_skirt_fill_bottom: bpy.props.BoolProperty(name="スカートの底面 (フタ) を作成する", default=True, update=update_proj)
    
    proj_color_skirt: bpy.props.FloatVectorProperty(name="スカート色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt: bpy.props.FloatProperty(name="透明度", default=0.5, min=0.0, max=1.0, update=update_proj)

    # 枠A・B
    proj_n_a: bpy.props.IntProperty(name="A 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_a: bpy.props.FloatProperty(name="A サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_a: bpy.props.FloatProperty(name="A 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
    proj_color_a: bpy.props.FloatVectorProperty(name="A 色", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    
    proj_n_b: bpy.props.IntProperty(name="B 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_b: bpy.props.FloatProperty(name="B サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_b: bpy.props.FloatProperty(name="B 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_b: bpy.props.FloatProperty(name="B Y座標", default=0.0, update=update_proj)
    proj_color_b: bpy.props.FloatVectorProperty(name="B 色", subtype='COLOR', default=(0.0, 1.0, 0.5), min=0.0, max=1.0, update=update_proj)
    
    # 平面C (到達範囲)
    proj_y_c: bpy.props.FloatProperty(name="C (投影先) Y座標", default=10.0, update=update_proj)
    proj_show_c: bpy.props.BoolProperty(name="平面C (壁・到達範囲) を表示する", default=True, update=update_proj)
    proj_color_c_m1: bpy.props.FloatVectorProperty(name="C 色(モード1)", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    proj_color_c_m2: bpy.props.FloatVectorProperty(name="C 色(モード2)", subtype='COLOR', default=(1.0, 0.2, 0.8), min=0.0, max=1.0, update=update_proj)

# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_create_projection(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create_proj"
    bl_label = "投影平面を生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.poly_torus_props
        col = context.collection
        
        mat_a = get_or_create_mat("Proj_Mat_A", props.proj_color_a, 1.0)
        mat_b = get_or_create_mat("Proj_Mat_B", props.proj_color_b, 1.0)
        mat_c = get_or_create_mat("Proj_Mat_C", props.proj_color_c_m1, 0.8)
        mat_p = get_or_create_mat("Proj_Mat_P", props.proj_color_p, props.proj_alpha_p)
        mat_skirt = get_or_create_mat("Proj_Mat_Skirt", (1,1,1), 1.0) 

        def create_or_get(name, mat):
            obj = bpy.data.objects.get(name)
            if not obj:
                obj = bpy.data.objects.new(name, bpy.data.meshes.new(f"{name}_Mesh"))
                col.objects.link(obj)
            if len(obj.data.materials) == 0:
                obj.data.materials.append(mat)
            return obj

        create_or_get("Proj_Plane_A", mat_a)
        create_or_get("Proj_Plane_B", mat_b)
        create_or_get("Proj_Plane_C", mat_c)
        create_or_get("Proj_Point_P", mat_p)
        create_or_get("Proj_Skirt", mat_skirt)
        
        update_proj(self, context)
        return {'FINISHED'}

class POLY_OT_create_torus(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create"
    bl_label = "単体多角形を生成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name:
            old_obj = bpy.data.objects.get(props.active_obj_name)
            if old_obj and "pt_live" in old_obj: del old_obj["pt_live"]
        mat = get_or_create_mat(f"{PREFIX_NAME}_Mat", props.color, props.alpha)
        IS_UPDATING = True
        try:
            mesh = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
            obj = bpy.data.objects.new("PolyTorus", mesh)
            context.collection.objects.link(obj)
            obj.data.materials.append(mat)
            obj["pt_live"] = True
            obj.location = (props.loc_x, props.loc_y, props.loc_z)
            obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
            props.active_obj_name = obj.name
            context.view_layer.objects.active = obj
            obj.select_set(True)
        finally: IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_detach_torus(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_detach"
    bl_label = "アドオンから切り離し"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        obj = bpy.data.objects.get(props.active_obj_name)
        if obj and "pt_live" in obj: del obj["pt_live"]
        IS_UPDATING = True; props.active_obj_name = ""; IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_modify_radius(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_modify_radius"
    bl_label = "半径を数値修飾"
    bl_options = {'REGISTER', 'UNDO'}
    action: bpy.props.EnumProperty(items=[('MULTIPLY', "Multiply", ""), ('DIVIDE', "Divide", "")])
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.multiplier_n <= 0: return {'CANCELLED'}
        factor = math.sqrt(props.multiplier_n)
        if self.action == 'MULTIPLY': props.radius *= factor
        elif self.action == 'DIVIDE': props.radius /= factor
        return {'FINISHED'}

class POLY_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル群】
# =========================================================================
class POLY_PT_projection_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_projection_panel"
    bl_label = "直線通過範囲 (シミュレーション)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        layout.operator(f"mesh.{PREFIX_NAME.lower()}_create_proj", text="シミュレーションを生成", icon='LIGHT_SUN')
        layout.separator()
        
        layout.prop(props, "proj_mode", expand=False)
        
        if props.proj_mode == 'MODE2':
            box_p = layout.box()
            box_p.label(text="指定点 P (球体・光源)", icon='LIGHT')
            col_p = box_p.column(align=True)
            col_p.prop(props, "proj_p_loc")
            col_p.prop(props, "proj_p_radius")
            
            row_p = col_p.row(align=True)
            row_p.prop(props, "proj_color_p", text="")
            row_p.prop(props, "proj_alpha_p", text="透明度")
            layout.separator()
            
            if props.proj_show_skirt:
                box_s = layout.box()
                box_s.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT', text="光の道筋 (スカート) 設定")
                box_s.prop(props, "proj_skirt_fill_bottom", icon='SNAP_FACE')
                
                row_skirt = box_s.row(align=True)
                row_skirt.prop(props, "proj_color_skirt", text="色")
                row_skirt.prop(props, "proj_alpha_skirt", text="透明度")
            else:
                layout.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT')
            
            layout.separator()
        
        box_a = layout.box()
        box_a.label(text="平面A (窓枠1)", icon='MESH_TORUS')
        col_a = box_a.column(align=True)
        col_a.prop(props, "proj_n_a")
        col_a.prop(props, "proj_r_a")
        col_a.prop(props, "proj_thick_a")
        col_a.prop(props, "proj_y_a")
        col_a.prop(props, "proj_color_a", text="A 色")
        
        box_b = layout.box()
        box_b.label(text="平面B (窓枠2)", icon='MESH_TORUS')
        col_b = box_b.column(align=True)
        col_b.prop(props, "proj_n_b")
        col_b.prop(props, "proj_r_b")
        col_b.prop(props, "proj_thick_b")
        col_b.prop(props, "proj_y_b")
        col_b.prop(props, "proj_color_b", text="B 色")
        
        box_c = layout.box()
        box_c.prop(props, "proj_show_c", icon='RESTRICT_VIEW_OFF' if props.proj_show_c else 'RESTRICT_VIEW_ON')
        if props.proj_show_c:
            col = box_c.column(align=True)
            col.prop(props, "proj_y_c", text="Cの Y座標")
            if props.proj_mode == 'MODE1':
                col.prop(props, "proj_color_c_m1", text="C 色 (青系)")
            else:
                col.prop(props, "proj_color_c_m2", text="C 色 (ピンク系)")

class POLY_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
    bl_label = "単体多角形トーラス (リアルタイム生成)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name and bpy.data.objects.get(props.active_obj_name):
            box_live = layout.box()
            box_live.label(text=f"🔄 編集中: {props.active_obj_name}", icon='EDITMODE_HLT')
            box_live.scale_y = 1.3
            box_live.operator(f"object.{PREFIX_NAME.lower()}_detach", text="アドオンから切り離し", icon='UNLINKED')
            layout.separator()
        else:
            layout.scale_y = 1.3
            layout.operator(f"mesh.{PREFIX_NAME.lower()}_create", text="新しく単体多角形を生成", icon='ADD')
            layout.scale_y = 1.0
            layout.separator()
        box_s = layout.box()
        col_s = box_s.column(align=True)
        col_s.prop(props, "segments")
        col_s.separator()
        col_s.prop(props, "radius")
        row_n = col_s.row(align=True)
        row_n.prop(props, "multiplier_n")
        op_mult = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× √n")
        op_mult.action = 'MULTIPLY'
        op_div = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× 1/√n")
        op_div.action = 'DIVIDE'
        col_s.separator()
        col_s.prop(props, "thick")
        col_s.separator()
        row_c = col_s.row(align=True)
        row_c.prop(props, "color", text="")
        row_c.prop(props, "alpha", text="透明")

class POLY_PT_footer_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_footer_panel"
    bl_label = "ドキュメント / 管理"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_order = 100
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        box_links = layout.box()
        box_links.label(text="関連リンク:", icon='BOOKMARKS')
        for link in ADDON_LINKS:
            box_links.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text=link["label"], icon='URL').url = link["url"]
        layout.separator(factor=2.0)
        layout.operator(f"wm.{PREFIX_NAME.lower()}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    PolyTorusProperties, 
    POLY_OT_create_torus, 
    POLY_OT_create_projection,
    POLY_OT_detach_torus,
    POLY_OT_modify_radius,
    POLY_OT_open_url, 
    POLY_OT_remove_addon,
    POLY_PT_projection_panel,
    POLY_PT_main_panel, 
    POLY_PT_footer_panel
]

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

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

if __name__ == "__main__":
    try: unregister()
    except: pass
    register()
bl_info = {
    "name": "Polygon Torus Generator Pro (Realtime & Detach)",
    "author": "Your Name",
    "version": (3, 5),
    "blender": (3, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
    "category": "Object",
}

import bpy
import webbrowser
import math
import bmesh

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "多角形ツール"
PREFIX_NAME = "poly_torus"

ADDON_LINKS = [
    {
        "label": "トーラス作成",
        "url": "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>",
        "icon": "URL"
    }
]

INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False

# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ生成】
# =========================================================================
def get_or_create_mat(name, color, alpha=1.0):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
        mat.use_nodes = True
        
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = (*color, 1.0)
        bsdf.inputs["Alpha"].default_value = alpha
        
    mat.diffuse_color = (*color, alpha)
        
    try:
        mat.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
        mat.shadow_method = 'NONE'
    except:
        pass
    return mat

# 【修正】スカート専用:Mix Shaderを使って表裏のBSDFを完全に分離
def update_skirt_material(mat, cf, af, cb, ab):
    if not mat.use_nodes:
        mat.use_nodes = True
        
    # 透明レンダリング用の設定
    try: mat.blend_method = 'BLEND'
    except: pass
    try: mat.show_transparent_back = True
    except: pass
    try: mat.shadow_method = 'NONE'
    except: pass
    try: mat.use_backface_culling = False
    except: pass
    
    tree = mat.node_tree
    
    # ノード取得・作成ヘルパー
    def get_node(name, idname):
        n = tree.nodes.get(name)
        if not n:
            n = tree.nodes.new(idname)
            n.name = name
        return n
        
    # 2つのBSDFとMixShaderを用意する
    bsdf_front = get_node("Skirt_Front_BSDF", 'ShaderNodeBsdfPrincipled')
    bsdf_back  = get_node("Skirt_Back_BSDF", 'ShaderNodeBsdfPrincipled')
    mix_shader = get_node("Skirt_MixShader", 'ShaderNodeMixShader')
    geom       = get_node("Skirt_Geom", 'ShaderNodeNewGeometry')
    out        = get_node("Skirt_Out", 'ShaderNodeOutputMaterial')
    
    links = tree.links
    def ensure_link(out_sock, in_sock):
        if not in_sock.is_linked or in_sock.links[0].from_socket != out_sock:
            links.new(out_sock, in_sock)
            
    # Front設定 (表面)
    bsdf_front.inputs["Base Color"].default_value = (*cf, 1.0)
    bsdf_front.inputs["Alpha"].default_value = af
    
    # Back設定 (裏面)
    bsdf_back.inputs["Base Color"].default_value = (*cb, 1.0)
    bsdf_back.inputs["Alpha"].default_value = ab
    
    # ノード接続: Geometry(Backfacing) -> MixShader(Fac)
    ensure_link(geom.outputs["Backfacing"], mix_shader.inputs["Fac"])
    ensure_link(bsdf_front.outputs["BSDF"], mix_shader.inputs[1])
    ensure_link(bsdf_back.outputs["BSDF"], mix_shader.inputs[2])
    ensure_link(mix_shader.outputs["Shader"], out.inputs["Surface"])
    
    mat.diffuse_color = (*cf, af)

def get_polygon_verts(n, r, y_pos):
    verts = []
    radius = r * math.sqrt(2) if n == 4 else r
    phase = math.pi / 4 if n == 4 else 0.0
    for i in range(n):
        angle = phase + 2 * math.pi * i / n
        verts.append((radius * math.cos(angle), y_pos, radius * math.sin(angle)))
    return verts

def create_convex_mesh(verts, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 3: return mesh
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try:
        bmesh.ops.convex_hull(bm, input=bm.verts)
    except:
        pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def create_skirt_mesh(verts, y_c, fill_bottom, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 4: return mesh
        
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try:
        bmesh.ops.convex_hull(bm, input=bm.verts)
    except:
        pass
        
    if not fill_bottom:
        faces_to_delete = []
        for face in bm.faces:
            is_bottom = True
            for v in face.verts:
                if abs(v.co.y - y_c) > 0.001:
                    is_bottom = False
                    break
            if is_bottom:
                faces_to_delete.append(face)
                
        if faces_to_delete:
            bmesh.ops.delete(bm, geom=faces_to_delete, context='FACES')
            
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def get_torus_mesh_by_ops(n, r, thick, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    
    radius = r * math.sqrt(2) if n == 4 else r
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_torus_add(
                major_segments=n, minor_segments=12,
                major_radius=radius, minor_radius=thick,
                location=(0,0,0), rotation=(0,0,0)
            )
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackTorus")

def get_sphere_mesh_by_ops(r, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackSphere")

# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
    if abs(y_b - y_a) < 0.0001: return []
    t = (y_c - y_a) / (y_b - y_a)
    pts = []
    for pa in verts_a:
        for pb in verts_b:
            x = pa[0] + t * (pb[0] - pa[0])
            z = pa[2] + t * (pb[2] - pa[2])
            pts.append((x, y_c, z))
    return pts

def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
    px, py, pz = p_loc
    def project_polygon(verts):
        poly2d = []
        for vx, vy, vz in verts:
            if abs(vy - py) < 1e-6: return [] 
            t = (y_c - py) / (vy - py)
            if t < 0: return [] 
            x = px + t * (vx - px)
            z = pz + t * (vz - pz)
            poly2d.append((x, z))
        return poly2d

    def make_ccw(poly):
        if len(poly) < 3: return poly
        area = sum(poly[i][0]*poly[(i+1)%len(poly)][1] - poly[(i+1)%len(poly)][0]*poly[i][1] for i in range(len(poly)))
        return poly[::-1] if area < 0 else poly

    def sutherland_hodgman(subject, clip):
        def inside(p, cp1, cp2):
            return (cp2[0] - cp1[0]) * (p[1] - cp1[1]) - (cp2[1] - cp1[1]) * (p[0] - cp1[0]) >= -1e-8
        def compute_intersection(cp1, cp2, s, e):
            dc, dp = [cp1[0] - cp2[0], cp1[1] - cp2[1]], [s[0] - e[0], s[1] - e[1]]
            n1, n2 = cp1[0] * cp2[1] - cp1[1] * cp2[0], s[0] * e[1] - s[1] * e[0]
            det = dc[0] * dp[1] - dc[1] * dp[0]
            if abs(det) < 1e-8: return s
            n3 = 1.0 / det
            return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3)

        out_poly = subject
        if not out_poly or not clip: return []
        cp1 = clip[-1]
        for cp2 in clip:
            in_poly = out_poly
            out_poly = []
            if not in_poly: break
            s = in_poly[-1]
            for e in in_poly:
                if inside(e, cp1, cp2):
                    if not inside(s, cp1, cp2):
                        out_poly.append(compute_intersection(cp1, cp2, s, e))
                    out_poly.append(e)
                elif inside(s, cp1, cp2):
                    out_poly.append(compute_intersection(cp1, cp2, s, e))
                s = e
            cp1 = cp2
        return out_poly

    poly_a = project_polygon(verts_a)
    poly_b = project_polygon(verts_b)
    if not poly_a or not poly_b: return []
    intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
    return [(x, y_c, z) for (x, z) in intersect_poly2d]

# =========================================================================
# 【リアルタイム更新関数 (投影シミュレーション用)】
# =========================================================================
def update_proj(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    
    props = context.scene.poly_torus_props
    obj_a = bpy.data.objects.get("Proj_Plane_A")
    obj_b = bpy.data.objects.get("Proj_Plane_B")
    obj_c = bpy.data.objects.get("Proj_Plane_C")
    obj_p = bpy.data.objects.get("Proj_Point_P")
    obj_skirt = bpy.data.objects.get("Proj_Skirt")
    
    if not (obj_a and obj_b and obj_c and obj_p and obj_skirt): return
    
    IS_UPDATING = True
    try:
        n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
        n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
        y_c = props.proj_y_c
        
        # 1. 枠A、枠Bの更新
        def update_torus(obj, n, r, thick, y_pos, color):
            mesh = get_torus_mesh_by_ops(n, r, thick, context)
            old_m, old_mat = obj.data, obj.active_material
            obj.data = mesh
            if old_mat: 
                obj.data.materials.append(old_mat)
                if old_mat.use_nodes:
                    bsdf = old_mat.node_tree.nodes.get("Principled BSDF")
                    if bsdf: bsdf.inputs["Base Color"].default_value = (*color, 1.0)
                old_mat.diffuse_color = (*color, 1.0)
            obj.location = (0, y_pos, 0)
            obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
            if old_m: bpy.data.meshes.remove(old_m)

        update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
        update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)

        # ---------------------------------------------------
        # 2. P (球体) の更新
        if props.proj_mode == 'MODE2':
            obj_p.hide_viewport = False
            mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
            old_m_p = obj_p.data
            mat_p = obj_p.active_material
            obj_p.data = mesh_p
            if mat_p:
                obj_p.data.materials.append(mat_p)
                if mat_p.use_nodes:
                    bsdf = mat_p.node_tree.nodes.get("Principled BSDF")
                    if bsdf:
                        bsdf.inputs["Base Color"].default_value = (*props.proj_color_p, 1.0)
                        bsdf.inputs["Alpha"].default_value = props.proj_alpha_p
                mat_p.diffuse_color = (*props.proj_color_p, props.proj_alpha_p)
                try: mat_p.blend_method = 'BLEND' if props.proj_alpha_p < 1.0 else 'OPAQUE'
                except: pass
            if old_m_p: bpy.data.meshes.remove(old_m_p)
            obj_p.location = props.proj_p_loc
        else:
            obj_p.hide_viewport = True

        # ---------------------------------------------------
        # 3. C (到達範囲) の計算
        va = get_polygon_verts(n_a, r_a, y_a)
        vb = get_polygon_verts(n_b, r_b, y_b)
        
        if props.proj_mode == 'MODE1':
            vc = get_projected_verts_mode1(va, vb, y_a, y_b, y_c)
            color_c = props.proj_color_c_m1
        else:
            vc = get_projected_verts_mode2(props.proj_p_loc, va, vb, y_c)
            color_c = props.proj_color_c_m2
        
        mesh_c = create_convex_mesh(vc, "Mesh_C")
        old_mc, old_mat_c = obj_c.data, obj_c.active_material
        obj_c.data = mesh_c
        
        # Cの表示切替
        obj_c.hide_viewport = not props.proj_show_c
        obj_c.hide_render = not props.proj_show_c
        
        if old_mat_c: obj_c.data.materials.append(old_mat_c)
        obj_c.location = (0, 0, 0)
        obj_c.rotation_euler = (0, 0, 0)
        if old_mc: bpy.data.meshes.remove(old_mc)
        
        if old_mat_c and old_mat_c.use_nodes:
            bsdf = old_mat_c.node_tree.nodes.get("Principled BSDF")
            if bsdf: bsdf.inputs["Base Color"].default_value = (*color_c, 1.0)
            old_mat_c.diffuse_color = (*color_c, 0.8)

        # ---------------------------------------------------
        # 4. スカート (光の道筋) の更新
        if props.proj_mode == 'MODE2' and props.proj_show_skirt and len(vc) >= 3:
            obj_skirt.hide_viewport = False
            
            verts_skirt = [tuple(props.proj_p_loc)] + vc
            mesh_skirt = create_skirt_mesh(verts_skirt, y_c, props.proj_skirt_fill_bottom, "Mesh_Skirt")
            
            old_m_skirt = obj_skirt.data
            mat_skirt = obj_skirt.active_material
            obj_skirt.data = mesh_skirt
            
            if mat_skirt:
                obj_skirt.data.materials.append(mat_skirt)
                update_skirt_material(
                    mat_skirt, 
                    props.proj_color_skirt_front, props.proj_alpha_skirt_front,
                    props.proj_color_skirt_back, props.proj_alpha_skirt_back
                )
                
            if old_m_skirt: bpy.data.meshes.remove(old_m_skirt)
            obj_skirt.location = (0, 0, 0)
            obj_skirt.rotation_euler = (0, 0, 0)
        else:
            obj_skirt.hide_viewport = True

    finally:
        IS_UPDATING = False

# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
def update_poly_torus(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if not props.active_obj_name: return
    obj = bpy.data.objects.get(props.active_obj_name)
    if not obj or not obj.get("pt_live"): return
        
    IS_UPDATING = True
    try:
        obj.location = (props.loc_x, props.loc_y, props.loc_z)
        obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
        
        mat = obj.active_material
        if mat and mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                bsdf.inputs["Base Color"].default_value = (*props.color, 1.0)
                bsdf.inputs["Alpha"].default_value = props.alpha
            mat.diffuse_color = (*props.color, props.alpha)
            try: mat.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
            except: pass
        
        mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
        old_mesh = obj.data
        obj.data = mesh_new
        if mat: obj.data.materials.append(mat)
        if old_mesh: bpy.data.meshes.remove(old_mesh)
    finally:
        IS_UPDATING = False

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
    active_obj_name: bpy.props.StringProperty(name="編集中のオブジェクト", default="")
    segments: bpy.props.IntProperty(name="角数", default=3, min=3, max=256, update=update_poly_torus)
    radius: bpy.props.FloatProperty(name="半径 (R)", default=1.0, min=0.0001, update=update_poly_torus)
    thick: bpy.props.FloatProperty(name="太さ (断面)", default=0.05, min=0.001, update=update_poly_torus)
    multiplier_n: bpy.props.IntProperty(name="n =", default=2, min=1)
    color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_COLOR, min=0.0, max=1.0, update=update_poly_torus)
    alpha: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_poly_torus)
    loc_x: bpy.props.FloatProperty(name="位置 X", default=0.0, update=update_poly_torus)
    loc_y: bpy.props.FloatProperty(name="位置 Y", default=0.0, update=update_poly_torus)
    loc_z: bpy.props.FloatProperty(name="位置 Z", default=0.0, update=update_poly_torus)
    rot_x: bpy.props.FloatProperty(name="X軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_y: bpy.props.FloatProperty(name="Y軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_z: bpy.props.FloatProperty(name="Z軸", default=0.0, subtype='ANGLE', update=update_poly_torus)

    proj_mode: bpy.props.EnumProperty(
        name="シミュレーションの種類",
        items=[
            ('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
            ('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
        ],
        default='MODE1',
        update=update_proj
    )
    
    # 指定点P
    proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -100.0, 0.0), update=update_proj)
    proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)
    proj_color_p: bpy.props.FloatVectorProperty(name="P 色", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, update=update_proj)
    proj_alpha_p: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_proj)

    # 【初期値修正】スカート機能 (表裏独立)
    proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
    proj_skirt_fill_bottom: bpy.props.BoolProperty(name="スカートの底面 (フタ) を作成する", default=True, update=update_proj)
    
    proj_color_skirt_front: bpy.props.FloatVectorProperty(name="表面色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt_front: bpy.props.FloatProperty(name="表面 透明度", default=1.0, min=0.0, max=1.0, update=update_proj) # 1.0 に変更
    
    proj_color_skirt_back: bpy.props.FloatVectorProperty(name="裏面色", subtype='COLOR', default=(0.2, 0.8, 1.0), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt_back: bpy.props.FloatProperty(name="裏面 透明度", default=0.5, min=0.0, max=1.0, update=update_proj) # 0.5 に変更

    # 枠A・B
    proj_n_a: bpy.props.IntProperty(name="A 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_a: bpy.props.FloatProperty(name="A サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_a: bpy.props.FloatProperty(name="A 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
    proj_color_a: bpy.props.FloatVectorProperty(name="A 色", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    
    proj_n_b: bpy.props.IntProperty(name="B 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_b: bpy.props.FloatProperty(name="B サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_b: bpy.props.FloatProperty(name="B 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_b: bpy.props.FloatProperty(name="B Y座標", default=0.0, update=update_proj)
    proj_color_b: bpy.props.FloatVectorProperty(name="B 色", subtype='COLOR', default=(0.0, 1.0, 0.5), min=0.0, max=1.0, update=update_proj)
    
    # 平面C (到達範囲)
    proj_y_c: bpy.props.FloatProperty(name="C (投影先) Y座標", default=10.0, update=update_proj)
    proj_show_c: bpy.props.BoolProperty(name="平面C (壁・到達範囲) を表示する", default=True, update=update_proj)
    proj_color_c_m1: bpy.props.FloatVectorProperty(name="C 色(モード1)", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    proj_color_c_m2: bpy.props.FloatVectorProperty(name="C 色(モード2)", subtype='COLOR', default=(1.0, 0.2, 0.8), min=0.0, max=1.0, update=update_proj)

# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_create_projection(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create_proj"
    bl_label = "投影平面を生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.poly_torus_props
        col = context.collection
        
        mat_a = get_or_create_mat("Proj_Mat_A", props.proj_color_a, 1.0)
        mat_b = get_or_create_mat("Proj_Mat_B", props.proj_color_b, 1.0)
        mat_c = get_or_create_mat("Proj_Mat_C", props.proj_color_c_m1, 0.8)
        mat_p = get_or_create_mat("Proj_Mat_P", props.proj_color_p, props.proj_alpha_p)
        mat_skirt = get_or_create_mat("Proj_Mat_Skirt", (1,1,1), 1.0) 

        def create_or_get(name, mat):
            obj = bpy.data.objects.get(name)
            if not obj:
                obj = bpy.data.objects.new(name, bpy.data.meshes.new(f"{name}_Mesh"))
                col.objects.link(obj)
            if len(obj.data.materials) == 0:
                obj.data.materials.append(mat)
            return obj

        create_or_get("Proj_Plane_A", mat_a)
        create_or_get("Proj_Plane_B", mat_b)
        create_or_get("Proj_Plane_C", mat_c)
        create_or_get("Proj_Point_P", mat_p)
        create_or_get("Proj_Skirt", mat_skirt)
        
        update_proj(self, context)
        return {'FINISHED'}

class POLY_OT_create_torus(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create"
    bl_label = "単体多角形を生成"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name:
            old_obj = bpy.data.objects.get(props.active_obj_name)
            if old_obj and "pt_live" in old_obj: del old_obj["pt_live"]
        mat = get_or_create_mat(f"{PREFIX_NAME}_Mat", props.color, props.alpha)
        IS_UPDATING = True
        try:
            mesh = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
            obj = bpy.data.objects.new("PolyTorus", mesh)
            context.collection.objects.link(obj)
            obj.data.materials.append(mat)
            obj["pt_live"] = True
            obj.location = (props.loc_x, props.loc_y, props.loc_z)
            obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
            props.active_obj_name = obj.name
            context.view_layer.objects.active = obj
            obj.select_set(True)
        finally: IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_detach_torus(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_detach"
    bl_label = "アドオンから切り離し"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        obj = bpy.data.objects.get(props.active_obj_name)
        if obj and "pt_live" in obj: del obj["pt_live"]
        IS_UPDATING = True; props.active_obj_name = ""; IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_modify_radius(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_modify_radius"
    bl_label = "半径を数値修飾"
    bl_options = {'REGISTER', 'UNDO'}
    action: bpy.props.EnumProperty(items=[('MULTIPLY', "Multiply", ""), ('DIVIDE', "Divide", "")])
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.multiplier_n <= 0: return {'CANCELLED'}
        factor = math.sqrt(props.multiplier_n)
        if self.action == 'MULTIPLY': props.radius *= factor
        elif self.action == 'DIVIDE': props.radius /= factor
        return {'FINISHED'}

class POLY_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル群】
# =========================================================================
class POLY_PT_projection_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_projection_panel"
    bl_label = "直線通過範囲 (シミュレーション)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        layout.operator(f"mesh.{PREFIX_NAME.lower()}_create_proj", text="シミュレーションを生成", icon='LIGHT_SUN')
        layout.separator()
        
        layout.prop(props, "proj_mode", expand=False)
        
        if props.proj_mode == 'MODE2':
            box_p = layout.box()
            box_p.label(text="指定点 P (球体・光源)", icon='LIGHT')
            col_p = box_p.column(align=True)
            col_p.prop(props, "proj_p_loc")
            col_p.prop(props, "proj_p_radius")
            
            row_p = col_p.row(align=True)
            row_p.prop(props, "proj_color_p", text="")
            row_p.prop(props, "proj_alpha_p", text="透明度")
            layout.separator()
            
            if props.proj_show_skirt:
                box_s = layout.box()
                box_s.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT', text="光の道筋 (スカート) 設定")
                box_s.prop(props, "proj_skirt_fill_bottom", icon='SNAP_FACE')
                
                row_front = box_s.row(align=True)
                row_front.prop(props, "proj_color_skirt_front", text="表面色")
                row_front.prop(props, "proj_alpha_skirt_front", text="表 透明度")
                
                row_back = box_s.row(align=True)
                row_back.prop(props, "proj_color_skirt_back", text="裏面色")
                row_back.prop(props, "proj_alpha_skirt_back", text="裏 透明度")
            else:
                layout.prop(props, "proj_show_skirt", icon='OUTLINER_OB_LIGHT')
            
            layout.separator()
        
        box_a = layout.box()
        box_a.label(text="平面A (窓枠1)", icon='MESH_TORUS')
        col_a = box_a.column(align=True)
        col_a.prop(props, "proj_n_a")
        col_a.prop(props, "proj_r_a")
        col_a.prop(props, "proj_thick_a")
        col_a.prop(props, "proj_y_a")
        col_a.prop(props, "proj_color_a", text="A 色")
        
        box_b = layout.box()
        box_b.label(text="平面B (窓枠2)", icon='MESH_TORUS')
        col_b = box_b.column(align=True)
        col_b.prop(props, "proj_n_b")
        col_b.prop(props, "proj_r_b")
        col_b.prop(props, "proj_thick_b")
        col_b.prop(props, "proj_y_b")
        col_b.prop(props, "proj_color_b", text="B 色")
        
        box_c = layout.box()
        box_c.prop(props, "proj_show_c", icon='RESTRICT_VIEW_OFF' if props.proj_show_c else 'RESTRICT_VIEW_ON')
        if props.proj_show_c:
            col = box_c.column(align=True)
            col.prop(props, "proj_y_c", text="Cの Y座標")
            if props.proj_mode == 'MODE1':
                col.prop(props, "proj_color_c_m1", text="C 色 (青系)")
            else:
                col.prop(props, "proj_color_c_m2", text="C 色 (ピンク系)")

class POLY_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
    bl_label = "単体多角形トーラス (リアルタイム生成)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name and bpy.data.objects.get(props.active_obj_name):
            box_live = layout.box()
            box_live.label(text=f"🔄 編集中: {props.active_obj_name}", icon='EDITMODE_HLT')
            box_live.scale_y = 1.3
            box_live.operator(f"object.{PREFIX_NAME.lower()}_detach", text="アドオンから切り離し", icon='UNLINKED')
            layout.separator()
        else:
            layout.scale_y = 1.3
            layout.operator(f"mesh.{PREFIX_NAME.lower()}_create", text="新しく単体多角形を生成", icon='ADD')
            layout.scale_y = 1.0
            layout.separator()
        box_s = layout.box()
        col_s = box_s.column(align=True)
        col_s.prop(props, "segments")
        col_s.separator()
        col_s.prop(props, "radius")
        row_n = col_s.row(align=True)
        row_n.prop(props, "multiplier_n")
        op_mult = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× √n")
        op_mult.action = 'MULTIPLY'
        op_div = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× 1/√n")
        op_div.action = 'DIVIDE'
        col_s.separator()
        col_s.prop(props, "thick")
        col_s.separator()
        row_c = col_s.row(align=True)
        row_c.prop(props, "color", text="")
        row_c.prop(props, "alpha", text="透明")

class POLY_PT_footer_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_footer_panel"
    bl_label = "ドキュメント / 管理"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_order = 100
    bl_options = {'DEFAULT_CLOSED'}
    def draw(self, context):
        layout = self.layout
        box_links = layout.box()
        box_links.label(text="関連リンク:", icon='BOOKMARKS')
        for link in ADDON_LINKS:
            box_links.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text=link["label"], icon='URL').url = link["url"]
        layout.separator(factor=2.0)
        layout.operator(f"wm.{PREFIX_NAME.lower()}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    PolyTorusProperties, 
    POLY_OT_create_torus, 
    POLY_OT_create_projection,
    POLY_OT_detach_torus,
    POLY_OT_modify_radius,
    POLY_OT_open_url, 
    POLY_OT_remove_addon,
    POLY_PT_projection_panel,
    POLY_PT_main_panel, 
    POLY_PT_footer_panel
]

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

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

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

bl_info = {
    "name": "Polygon Torus Generator Pro (Realtime & Detach)",
    "author": "Your Name",
    "version": (3, 2),
    "blender": (3, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
    "category": "Object",
}

import bpy
import webbrowser
import math
import bmesh

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "多角形ツール"
PREFIX_NAME = "poly_torus"

ADDON_LINKS = [
    {
        "label": "トーラス作成",
        "url": "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>",
        "icon": "URL"
    }
]

INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False

# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ生成】
# =========================================================================
def get_or_create_mat(name, color, alpha=1.0):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
        mat.use_nodes = True
        
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = (*color, 1.0)
        bsdf.inputs["Alpha"].default_value = alpha
        
    mat.diffuse_color = (*color, alpha)
        
    try:
        mat.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
    except:
        pass
    return mat

def get_polygon_verts(n, r, y_pos):
    verts = []
    radius = r * math.sqrt(2) if n == 4 else r
    phase = math.pi / 4 if n == 4 else 0.0
    for i in range(n):
        angle = phase + 2 * math.pi * i / n
        verts.append((radius * math.cos(angle), y_pos, radius * math.sin(angle)))
    return verts

def create_convex_mesh(verts, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 3: return mesh
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try:
        bmesh.ops.convex_hull(bm, input=bm.verts)
    except:
        pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def get_torus_mesh_by_ops(n, r, thick, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    
    radius = r * math.sqrt(2) if n == 4 else r
    temp_obj = None
    
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_torus_add(
                major_segments=n, minor_segments=12,
                major_radius=radius, minor_radius=thick,
                location=(0,0,0), rotation=(0,0,0)
            )
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackTorus")

def get_sphere_mesh_by_ops(r, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackSphere")

# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
    if abs(y_b - y_a) < 0.0001: return []
    t = (y_c - y_a) / (y_b - y_a)
    pts = []
    for pa in verts_a:
        for pb in verts_b:
            x = pa[0] + t * (pb[0] - pa[0])
            z = pa[2] + t * (pb[2] - pa[2])
            pts.append((x, y_c, z))
    return pts

def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
    px, py, pz = p_loc
    
    def project_polygon(verts):
        poly2d = []
        for vx, vy, vz in verts:
            if abs(vy - py) < 1e-6: return [] 
            t = (y_c - py) / (vy - py)
            if t < 0: return [] 
            x = px + t * (vx - px)
            z = pz + t * (vz - pz)
            poly2d.append((x, z))
        return poly2d

    def make_ccw(poly):
        if len(poly) < 3: return poly
        area = sum(poly[i][0]*poly[(i+1)%len(poly)][1] - poly[(i+1)%len(poly)][0]*poly[i][1] for i in range(len(poly)))
        return poly[::-1] if area < 0 else poly

    def sutherland_hodgman(subject, clip):
        def inside(p, cp1, cp2):
            return (cp2[0] - cp1[0]) * (p[1] - cp1[1]) - (cp2[1] - cp1[1]) * (p[0] - cp1[0]) >= -1e-8
        def compute_intersection(cp1, cp2, s, e):
            dc, dp = [cp1[0] - cp2[0], cp1[1] - cp2[1]], [s[0] - e[0], s[1] - e[1]]
            n1, n2 = cp1[0] * cp2[1] - cp1[1] * cp2[0], s[0] * e[1] - s[1] * e[0]
            det = dc[0] * dp[1] - dc[1] * dp[0]
            if abs(det) < 1e-8: return s
            n3 = 1.0 / det
            return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3)

        out_poly = subject
        if not out_poly or not clip: return []
        cp1 = clip[-1]
        for cp2 in clip:
            in_poly = out_poly
            out_poly = []
            if not in_poly: break
            s = in_poly[-1]
            for e in in_poly:
                if inside(e, cp1, cp2):
                    if not inside(s, cp1, cp2):
                        out_poly.append(compute_intersection(cp1, cp2, s, e))
                    out_poly.append(e)
                elif inside(s, cp1, cp2):
                    out_poly.append(compute_intersection(cp1, cp2, s, e))
                s = e
            cp1 = cp2
        return out_poly

    poly_a = project_polygon(verts_a)
    poly_b = project_polygon(verts_b)
    if not poly_a or not poly_b: return []
    intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
    return [(x, y_c, z) for (x, z) in intersect_poly2d]

# =========================================================================
# 【リアルタイム更新関数 (投影シミュレーション用)】
# =========================================================================
def update_proj(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    
    props = context.scene.poly_torus_props
    obj_a = bpy.data.objects.get("Proj_Plane_A")
    obj_b = bpy.data.objects.get("Proj_Plane_B")
    obj_c = bpy.data.objects.get("Proj_Plane_C")
    obj_p = bpy.data.objects.get("Proj_Point_P")
    
    if not (obj_a and obj_b and obj_c and obj_p): return
    
    IS_UPDATING = True
    try:
        n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
        n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
        y_c = props.proj_y_c
        
        # 1. 枠A、枠Bの更新
        def update_torus(obj, n, r, thick, y_pos, color):
            mesh = get_torus_mesh_by_ops(n, r, thick, context)
            old_m, old_mat = obj.data, obj.active_material
            obj.data = mesh
            if old_mat: 
                obj.data.materials.append(old_mat)
                if old_mat.use_nodes:
                    bsdf = old_mat.node_tree.nodes.get("Principled BSDF")
                    if bsdf: bsdf.inputs["Base Color"].default_value = (*color, 1.0)
                old_mat.diffuse_color = (*color, 1.0)
            obj.location = (0, y_pos, 0)
            obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
            if old_m: bpy.data.meshes.remove(old_m)

        update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
        update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)

        # ---------------------------------------------------
        # 2. P (球体) の更新とマテリアル変更(※ここを修正しました)
        if props.proj_mode == 'MODE2':
            obj_p.hide_viewport = False
            mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
            
            # 先に現在のマテリアルを確保しておく
            old_m_p = obj_p.data
            mat_p = obj_p.active_material
            
            # メッシュを入れ替える
            obj_p.data = mesh_p
            
            # マテリアルを新しいメッシュに再設定
            if mat_p:
                obj_p.data.materials.append(mat_p)
                if mat_p.use_nodes:
                    bsdf = mat_p.node_tree.nodes.get("Principled BSDF")
                    if bsdf:
                        bsdf.inputs["Base Color"].default_value = (*props.proj_color_p, 1.0)
                        bsdf.inputs["Alpha"].default_value = props.proj_alpha_p
                mat_p.diffuse_color = (*props.proj_color_p, props.proj_alpha_p)
                try: mat_p.blend_method = 'BLEND' if props.proj_alpha_p < 1.0 else 'OPAQUE'
                except: pass
                
            # 古いメッシュを消去
            if old_m_p: bpy.data.meshes.remove(old_m_p)
                
            obj_p.location = props.proj_p_loc
        else:
            obj_p.hide_viewport = True

        # ---------------------------------------------------
        # 3. C (到達範囲) の計算と更新
        va = get_polygon_verts(n_a, r_a, y_a)
        vb = get_polygon_verts(n_b, r_b, y_b)
        
        if props.proj_mode == 'MODE1':
            vc = get_projected_verts_mode1(va, vb, y_a, y_b, y_c)
            color_c = props.proj_color_c_m1
        else:
            vc = get_projected_verts_mode2(props.proj_p_loc, va, vb, y_c)
            color_c = props.proj_color_c_m2
        
        mesh_c = create_convex_mesh(vc, "Mesh_C")
        
        old_mc, old_mat_c = obj_c.data, obj_c.active_material
        obj_c.data = mesh_c
        if old_mat_c: obj_c.data.materials.append(old_mat_c)
        obj_c.location = (0, 0, 0)
        obj_c.rotation_euler = (0, 0, 0)
        if old_mc: bpy.data.meshes.remove(old_mc)
        
        if old_mat_c and old_mat_c.use_nodes:
            bsdf = old_mat_c.node_tree.nodes.get("Principled BSDF")
            if bsdf: bsdf.inputs["Base Color"].default_value = (*color_c, 1.0)
            old_mat_c.diffuse_color = (*color_c, 0.8)
            
    finally:
        IS_UPDATING = False

# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
def update_poly_torus(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if not props.active_obj_name: return
    obj = bpy.data.objects.get(props.active_obj_name)
    if not obj or not obj.get("pt_live"): return
        
    IS_UPDATING = True
    try:
        obj.location = (props.loc_x, props.loc_y, props.loc_z)
        obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
        
        mat = obj.active_material
        if mat and mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                bsdf.inputs["Base Color"].default_value = (*props.color, 1.0)
                bsdf.inputs["Alpha"].default_value = props.alpha
            mat.diffuse_color = (*props.color, props.alpha)
            try: mat.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
            except: pass
        
        mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
        old_mesh = obj.data
        obj.data = mesh_new
        if mat: obj.data.materials.append(mat)
        if old_mesh: bpy.data.meshes.remove(old_mesh)
    finally:
        IS_UPDATING = False

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
    active_obj_name: bpy.props.StringProperty(name="編集中のオブジェクト", default="")
    segments: bpy.props.IntProperty(name="角数", default=3, min=3, max=256, update=update_poly_torus)
    radius: bpy.props.FloatProperty(name="半径 (R)", default=1.0, min=0.0001, update=update_poly_torus)
    thick: bpy.props.FloatProperty(name="太さ (断面)", default=0.05, min=0.001, update=update_poly_torus)
    multiplier_n: bpy.props.IntProperty(name="n =", default=2, min=1)
    color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_COLOR, min=0.0, max=1.0, update=update_poly_torus)
    alpha: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_poly_torus)
    loc_x: bpy.props.FloatProperty(name="位置 X", default=0.0, update=update_poly_torus)
    loc_y: bpy.props.FloatProperty(name="位置 Y", default=0.0, update=update_poly_torus)
    loc_z: bpy.props.FloatProperty(name="位置 Z", default=0.0, update=update_poly_torus)
    rot_x: bpy.props.FloatProperty(name="X軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_y: bpy.props.FloatProperty(name="Y軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_z: bpy.props.FloatProperty(name="Z軸", default=0.0, subtype='ANGLE', update=update_poly_torus)

    proj_mode: bpy.props.EnumProperty(
        name="シミュレーションの種類",
        items=[
            ('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
            ('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
        ],
        default='MODE1',
        update=update_proj
    )
    
    proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -100.0, 0.0), update=update_proj)
    proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)
    proj_color_p: bpy.props.FloatVectorProperty(name="P 色", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, update=update_proj)
    proj_alpha_p: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_proj)

    proj_n_a: bpy.props.IntProperty(name="A 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_a: bpy.props.FloatProperty(name="A サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_a: bpy.props.FloatProperty(name="A 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
    proj_color_a: bpy.props.FloatVectorProperty(name="A 色", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    
    proj_n_b: bpy.props.IntProperty(name="B 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_b: bpy.props.FloatProperty(name="B サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_b: bpy.props.FloatProperty(name="B 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_b: bpy.props.FloatProperty(name="B Y座標", default=0.0, update=update_proj)
    proj_color_b: bpy.props.FloatVectorProperty(name="B 色", subtype='COLOR', default=(0.0, 1.0, 0.5), min=0.0, max=1.0, update=update_proj)
    
    proj_y_c: bpy.props.FloatProperty(name="C (投影先) Y座標", default=10.0, update=update_proj)
    proj_color_c_m1: bpy.props.FloatVectorProperty(name="C 色(モード1)", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    proj_color_c_m2: bpy.props.FloatVectorProperty(name="C 色(モード2)", subtype='COLOR', default=(1.0, 0.2, 0.8), min=0.0, max=1.0, update=update_proj)

# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_create_projection(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create_proj"
    bl_label = "投影平面を生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.poly_torus_props
        col = context.collection
        
        mat_a = get_or_create_mat("Proj_Mat_A", props.proj_color_a, 1.0)
        mat_b = get_or_create_mat("Proj_Mat_B", props.proj_color_b, 1.0)
        mat_c = get_or_create_mat("Proj_Mat_C", props.proj_color_c_m1, 0.8)
        mat_p = get_or_create_mat("Proj_Mat_P", props.proj_color_p, props.proj_alpha_p)

        def create_or_get(name, mat):
            obj = bpy.data.objects.get(name)
            if not obj:
                obj = bpy.data.objects.new(name, bpy.data.meshes.new(f"{name}_Mesh"))
                col.objects.link(obj)
            if len(obj.data.materials) == 0:
                obj.data.materials.append(mat)
            return obj

        create_or_get("Proj_Plane_A", mat_a)
        create_or_get("Proj_Plane_B", mat_b)
        create_or_get("Proj_Plane_C", mat_c)
        create_or_get("Proj_Point_P", mat_p)
        
        update_proj(self, context)
        return {'FINISHED'}

class POLY_OT_create_torus(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create"
    bl_label = "単体多角形を生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name:
            old_obj = bpy.data.objects.get(props.active_obj_name)
            if old_obj and "pt_live" in old_obj: del old_obj["pt_live"]
        mat = get_or_create_mat(f"{PREFIX_NAME}_Mat", props.color, props.alpha)
        
        IS_UPDATING = True
        try:
            mesh = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
            obj = bpy.data.objects.new("PolyTorus", mesh)
            context.collection.objects.link(obj)
            obj.data.materials.append(mat)
            obj["pt_live"] = True
            obj.location = (props.loc_x, props.loc_y, props.loc_z)
            obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
            props.active_obj_name = obj.name
            context.view_layer.objects.active = obj
            obj.select_set(True)
        finally:
            IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_detach_torus(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_detach"
    bl_label = "アドオンから切り離し"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        obj = bpy.data.objects.get(props.active_obj_name)
        if obj and "pt_live" in obj: del obj["pt_live"]
        IS_UPDATING = True
        props.active_obj_name = ""
        IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_modify_radius(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_modify_radius"
    bl_label = "半径を数値修飾"
    bl_options = {'REGISTER', 'UNDO'}
    action: bpy.props.EnumProperty(items=[('MULTIPLY', "Multiply", ""), ('DIVIDE', "Divide", "")])
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.multiplier_n <= 0: return {'CANCELLED'}
        factor = math.sqrt(props.multiplier_n)
        if self.action == 'MULTIPLY': props.radius *= factor
        elif self.action == 'DIVIDE': props.radius /= factor
        return {'FINISHED'}

class POLY_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル群】
# =========================================================================
class POLY_PT_projection_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_projection_panel"
    bl_label = "直線通過範囲 (シミュレーション)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        layout.operator(f"mesh.{PREFIX_NAME.lower()}_create_proj", text="シミュレーションを生成", icon='LIGHT_SUN')
        layout.separator()
        
        layout.prop(props, "proj_mode", expand=False)
        
        if props.proj_mode == 'MODE2':
            box_p = layout.box()
            box_p.label(text="指定点 P (球体・光源)", icon='LIGHT')
            col_p = box_p.column(align=True)
            col_p.prop(props, "proj_p_loc")
            col_p.prop(props, "proj_p_radius")
            
            row_p = col_p.row(align=True)
            row_p.prop(props, "proj_color_p", text="")
            row_p.prop(props, "proj_alpha_p", text="透明度")
            layout.separator()
        
        box_a = layout.box()
        box_a.label(text="平面A (窓枠1)", icon='MESH_TORUS')
        col_a = box_a.column(align=True)
        col_a.prop(props, "proj_n_a")
        col_a.prop(props, "proj_r_a")
        col_a.prop(props, "proj_thick_a")
        col_a.prop(props, "proj_y_a")
        col_a.prop(props, "proj_color_a", text="A 色")
        
        box_b = layout.box()
        box_b.label(text="平面B (窓枠2)", icon='MESH_TORUS')
        col_b = box_b.column(align=True)
        col_b.prop(props, "proj_n_b")
        col_b.prop(props, "proj_r_b")
        col_b.prop(props, "proj_thick_b")
        col_b.prop(props, "proj_y_b")
        col_b.prop(props, "proj_color_b", text="B 色")
        
        box_c = layout.box()
        box_c.label(text="平面C (壁・到達範囲)", icon='MESH_PLANE')
        col = box_c.column(align=True)
        col.prop(props, "proj_y_c", text="Cの Y座標")
        
        if props.proj_mode == 'MODE1':
            col.prop(props, "proj_color_c_m1", text="C 色 (青系)")
        else:
            col.prop(props, "proj_color_c_m2", text="C 色 (ピンク系)")

class POLY_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
    bl_label = "単体多角形トーラス (リアルタイム生成)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        if props.active_obj_name and bpy.data.objects.get(props.active_obj_name):
            box_live = layout.box()
            box_live.label(text=f"🔄 編集中: {props.active_obj_name}", icon='EDITMODE_HLT')
            box_live.scale_y = 1.3
            box_live.operator(f"object.{PREFIX_NAME.lower()}_detach", text="アドオンから切り離し", icon='UNLINKED')
            layout.separator()
        else:
            layout.scale_y = 1.3
            layout.operator(f"mesh.{PREFIX_NAME.lower()}_create", text="新しく単体多角形を生成", icon='ADD')
            layout.scale_y = 1.0
            layout.separator()
        
        box_s = layout.box()
        col_s = box_s.column(align=True)
        col_s.prop(props, "segments")
        col_s.separator()
        col_s.prop(props, "radius")
        
        row_n = col_s.row(align=True)
        row_n.prop(props, "multiplier_n")
        op_mult = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× √n")
        op_mult.action = 'MULTIPLY'
        op_div = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× 1/√n")
        op_div.action = 'DIVIDE'
        
        col_s.separator()
        col_s.prop(props, "thick")
        col_s.separator()
        row_c = col_s.row(align=True)
        row_c.prop(props, "color", text="")
        row_c.prop(props, "alpha", text="透明")

class POLY_PT_footer_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_footer_panel"
    bl_label = "ドキュメント / 管理"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_order = 100
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        box_links = layout.box()
        box_links.label(text="関連リンク:", icon='BOOKMARKS')
        for link in ADDON_LINKS:
            box_links.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text=link["label"], icon='URL').url = link["url"]
        layout.separator(factor=2.0)
        layout.operator(f"wm.{PREFIX_NAME.lower()}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    PolyTorusProperties, 
    POLY_OT_create_torus, 
    POLY_OT_create_projection,
    POLY_OT_detach_torus,
    POLY_OT_modify_radius,
    POLY_OT_open_url, 
    POLY_OT_remove_addon,
    POLY_PT_projection_panel,
    POLY_PT_main_panel, 
    POLY_PT_footer_panel
]

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

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

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

bl_info = {
    "name": "Polygon Torus Generator Pro (Realtime & Detach)",
    "author": "Your Name",
    "version": (3, 0),
    "blender": (3, 0, 0),
    "location": "View3D > Sidebar (Nパネル)",
    "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション",
    "category": "Object",
}

import bpy
import webbrowser
import math
import bmesh

# =========================================================================
# 【基本設定】
# =========================================================================
TAB_NAME    = "多角形ツール"
PREFIX_NAME = "poly_torus"

ADDON_LINKS = [
    {
        "label": "トーラス作成",
        "url": "<https://app.notion.com/p/390f5dacaf43801d8f08c695979e60e1>",
        "icon": "URL"
    }
]

INIT_COLOR = (0.0, 1.0, 0.5)
IS_UPDATING = False

# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ生成】
# =========================================================================
def get_or_create_mat(name, color, alpha=1.0):
    mat = bpy.data.materials.get(name)
    if not mat:
        mat = bpy.data.materials.new(name)
        mat.use_nodes = True
        
    bsdf = mat.node_tree.nodes.get("Principled BSDF")
    if bsdf:
        bsdf.inputs["Base Color"].default_value = (*color, 1.0)
        bsdf.inputs["Alpha"].default_value = alpha
        
    mat.diffuse_color = (*color, alpha)
        
    try:
        mat.blend_method = 'BLEND' if alpha < 1.0 else 'OPAQUE'
    except:
        pass
    return mat

def get_polygon_verts(n, r, y_pos):
    verts = []
    radius = r * math.sqrt(2) if n == 4 else r
    phase = math.pi / 4 if n == 4 else 0.0
    for i in range(n):
        angle = phase + 2 * math.pi * i / n
        verts.append((radius * math.cos(angle), y_pos, radius * math.sin(angle)))
    return verts

def create_convex_mesh(verts, mesh_name):
    mesh = bpy.data.meshes.new(mesh_name)
    if len(verts) < 3: return mesh
    bm = bmesh.new()
    for v in verts:
        bm.verts.new(v)
    try:
        bmesh.ops.convex_hull(bm, input=bm.verts)
    except:
        pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def get_torus_mesh_by_ops(n, r, thick, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    
    radius = r * math.sqrt(2) if n == 4 else r
    temp_obj = None
    
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_torus_add(
                major_segments=n, minor_segments=12,
                major_radius=radius, minor_radius=thick,
                location=(0,0,0), rotation=(0,0,0)
            )
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackTorus")

def get_sphere_mesh_by_ops(r, context):
    area = next((a for a in context.screen.areas if a.type == 'VIEW_3D'), None)
    region = next((r_ for r_ in area.regions if r_.type == 'WINDOW'), None) if area else None
    old_active = context.view_layer.objects.active
    temp_obj = None
    override_kwargs = {}
    if area: override_kwargs['area'] = area
    if region: override_kwargs['region'] = region

    with context.temp_override(**override_kwargs):
        try:
            bpy.ops.mesh.primitive_ico_sphere_add(radius=r, subdivisions=3, location=(0,0,0))
            temp_obj = context.active_object
        except: pass
            
    if temp_obj:
        new_mesh = temp_obj.data.copy()
        bpy.data.objects.remove(temp_obj)
        if old_active: context.view_layer.objects.active = old_active
        return new_mesh
    return bpy.data.meshes.new("FallbackSphere")

# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
# モード1:2つの多角形領域間の全直線の束 (従来型)
def get_projected_verts_mode1(verts_a, verts_b, y_a, y_b, y_c):
    if abs(y_b - y_a) < 0.0001: return []
    t = (y_c - y_a) / (y_b - y_a)
    pts = []
    for pa in verts_a:
        for pb in verts_b:
            x = pa[0] + t * (pb[0] - pa[0])
            z = pa[2] + t * (pb[2] - pa[2])
            pts.append((x, y_c, z))
    return pts

# モード2:1点から発射され、2つの多角形領域をすり抜けた範囲 (新型)
def get_projected_verts_mode2(p_loc, verts_a, verts_b, y_c):
    px, py, pz = p_loc
    
    # 1. 任意の多角形を点Pから平面y_cに投影する関数
    def project_polygon(verts):
        poly2d = []
        for vx, vy, vz in verts:
            if abs(vy - py) < 1e-6: return [] # 平行・計算不能
            t = (y_c - py) / (vy - py)
            if t < 0: return [] # Pから見て後ろにある場合は到達しない
            x = px + t * (vx - px)
            z = pz + t * (vz - pz)
            poly2d.append((x, z))
        return poly2d

    # 2. 頂点群を反時計回り(CCW)に整列する関数
    def make_ccw(poly):
        if len(poly) < 3: return poly
        area = sum(poly[i][0]*poly[(i+1)%len(poly)][1] - poly[(i+1)%len(poly)][0]*poly[i][1] for i in range(len(poly)))
        return poly[::-1] if area < 0 else poly

    # 3. 2つの凸多角形の交差(共通部分)を求める Sutherland-Hodgman アルゴリズム
    def sutherland_hodgman(subject, clip):
        def inside(p, cp1, cp2):
            return (cp2[0] - cp1[0]) * (p[1] - cp1[1]) - (cp2[1] - cp1[1]) * (p[0] - cp1[0]) >= -1e-8
        def compute_intersection(cp1, cp2, s, e):
            dc, dp = [cp1[0] - cp2[0], cp1[1] - cp2[1]], [s[0] - e[0], s[1] - e[1]]
            n1, n2 = cp1[0] * cp2[1] - cp1[1] * cp2[0], s[0] * e[1] - s[1] * e[0]
            det = dc[0] * dp[1] - dc[1] * dp[0]
            if abs(det) < 1e-8: return s
            n3 = 1.0 / det
            return ((n1*dp[0] - n2*dc[0]) * n3, (n1*dp[1] - n2*dc[1]) * n3)

        out_poly = subject
        if not out_poly or not clip: return []
        cp1 = clip[-1]
        for cp2 in clip:
            in_poly = out_poly
            out_poly = []
            if not in_poly: break
            s = in_poly[-1]
            for e in in_poly:
                if inside(e, cp1, cp2):
                    if not inside(s, cp1, cp2):
                        out_poly.append(compute_intersection(cp1, cp2, s, e))
                    out_poly.append(e)
                elif inside(s, cp1, cp2):
                    out_poly.append(compute_intersection(cp1, cp2, s, e))
                s = e
            cp1 = cp2
        return out_poly

    # AとBそれぞれの投影形状(2D)を取得
    poly_a = project_polygon(verts_a)
    poly_b = project_polygon(verts_b)
    if not poly_a or not poly_b: return []
    
    # 共通部分(Intersection)を計算
    intersect_poly2d = sutherland_hodgman(make_ccw(poly_a), make_ccw(poly_b))
    
    # 3D空間(y = y_c)の座標に戻す
    return [(x, y_c, z) for (x, z) in intersect_poly2d]

# =========================================================================
# 【リアルタイム更新関数 (投影シミュレーション用)】
# =========================================================================
def update_proj(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    
    props = context.scene.poly_torus_props
    obj_a = bpy.data.objects.get("Proj_Plane_A")
    obj_b = bpy.data.objects.get("Proj_Plane_B")
    obj_c = bpy.data.objects.get("Proj_Plane_C")
    obj_p = bpy.data.objects.get("Proj_Point_P")
    
    if not (obj_a and obj_b and obj_c and obj_p): return
    
    IS_UPDATING = True
    try:
        # Aの設定
        n_a, r_a, thick_a, y_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a, props.proj_y_a
        # Bの設定
        n_b, r_b, thick_b, y_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b, props.proj_y_b
        # Cの設定
        y_c = props.proj_y_c
        
        # ---------------------------------------------------
        # 1. 枠A、枠Bのメッシュ更新
        def update_torus(obj, n, r, thick, y_pos, color):
            mesh = get_torus_mesh_by_ops(n, r, thick, context)
            old_m, old_mat = obj.data, obj.active_material
            obj.data = mesh
            if old_mat: 
                obj.data.materials.append(old_mat)
                if old_mat.use_nodes:
                    bsdf = old_mat.node_tree.nodes.get("Principled BSDF")
                    if bsdf: bsdf.inputs["Base Color"].default_value = (*color, 1.0)
                old_mat.diffuse_color = (*color, 1.0)
            obj.location = (0, y_pos, 0)
            obj.rotation_euler = (math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)
            if old_m: bpy.data.meshes.remove(old_m)

        update_torus(obj_a, n_a, r_a, thick_a, y_a, props.proj_color_a)
        update_torus(obj_b, n_b, r_b, thick_b, y_b, props.proj_color_b)

        # ---------------------------------------------------
        # 2. P (指定点・赤い球体) の更新と表示制御
        if props.proj_mode == 'MODE2':
            obj_p.hide_viewport = False
            mesh_p = get_sphere_mesh_by_ops(props.proj_p_radius, context)
            old_m_p = obj_p.data
            obj_p.data = mesh_p
            if old_m_p: bpy.data.meshes.remove(old_m_p)
            # マテリアルは初期化時に赤色固定でセット済み
            if obj_p.active_material:
                obj_p.data.materials.append(obj_p.active_material)
            obj_p.location = props.proj_p_loc
        else:
            obj_p.hide_viewport = True # モード1の時は隠す

        # ---------------------------------------------------
        # 3. C (到達範囲) の計算と更新
        va = get_polygon_verts(n_a, r_a, y_a)
        vb = get_polygon_verts(n_b, r_b, y_b)
        
        if props.proj_mode == 'MODE1':
            vc = get_projected_verts_mode1(va, vb, y_a, y_b, y_c)
            color_c = props.proj_color_c_m1
        else:
            vc = get_projected_verts_mode2(props.proj_p_loc, va, vb, y_c)
            color_c = props.proj_color_c_m2
        
        mesh_c = create_convex_mesh(vc, "Mesh_C")
        
        old_mc, old_mat_c = obj_c.data, obj_c.active_material
        obj_c.data = mesh_c
        if old_mat_c: obj_c.data.materials.append(old_mat_c)
        obj_c.location = (0, 0, 0)
        obj_c.rotation_euler = (0, 0, 0)
        if old_mc: bpy.data.meshes.remove(old_mc)
        
        # Cの色更新
        if old_mat_c and old_mat_c.use_nodes:
            bsdf = old_mat_c.node_tree.nodes.get("Principled BSDF")
            if bsdf: bsdf.inputs["Base Color"].default_value = (*color_c, 1.0)
            old_mat_c.diffuse_color = (*color_c, 0.8)
            
    finally:
        IS_UPDATING = False

# =========================================================================
# 【リアルタイム更新関数 (単体多角形トーラス用)】
# =========================================================================
def update_poly_torus(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if not props.active_obj_name: return
    obj = bpy.data.objects.get(props.active_obj_name)
    if not obj or not obj.get("pt_live"): return
        
    IS_UPDATING = True
    try:
        obj.location = (props.loc_x, props.loc_y, props.loc_z)
        obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
        
        mat = obj.active_material
        if mat and mat.use_nodes:
            bsdf = mat.node_tree.nodes.get("Principled BSDF")
            if bsdf:
                bsdf.inputs["Base Color"].default_value = (*props.color, 1.0)
                bsdf.inputs["Alpha"].default_value = props.alpha
            mat.diffuse_color = (*props.color, props.alpha)
            try: mat.blend_method = 'BLEND' if props.alpha < 1.0 else 'OPAQUE'
            except: pass
        
        mesh_new = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
        old_mesh = obj.data
        obj.data = mesh_new
        if mat: obj.data.materials.append(mat)
        if old_mesh: bpy.data.meshes.remove(old_mesh)
    finally:
        IS_UPDATING = False

# =========================================================================
# 【プロパティ定義】
# =========================================================================
class PolyTorusProperties(bpy.types.PropertyGroup):
    # トーラス用
    active_obj_name: bpy.props.StringProperty(name="編集中のオブジェクト", default="")
    segments: bpy.props.IntProperty(name="角数", default=3, min=3, max=256, update=update_poly_torus)
    radius: bpy.props.FloatProperty(name="半径 (R)", default=1.0, min=0.0001, update=update_poly_torus)
    thick: bpy.props.FloatProperty(name="太さ (断面)", default=0.05, min=0.001, update=update_poly_torus)
    multiplier_n: bpy.props.IntProperty(name="n =", default=2, min=1)
    color: bpy.props.FloatVectorProperty(name="色", subtype='COLOR', default=INIT_COLOR, min=0.0, max=1.0, update=update_poly_torus)
    alpha: bpy.props.FloatProperty(name="透明度", default=1.0, min=0.0, max=1.0, update=update_poly_torus)
    loc_x: bpy.props.FloatProperty(name="位置 X", default=0.0, update=update_poly_torus)
    loc_y: bpy.props.FloatProperty(name="位置 Y", default=0.0, update=update_poly_torus)
    loc_z: bpy.props.FloatProperty(name="位置 Z", default=0.0, update=update_poly_torus)
    rot_x: bpy.props.FloatProperty(name="X軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_y: bpy.props.FloatProperty(name="Y軸", default=0.0, subtype='ANGLE', update=update_poly_torus)
    rot_z: bpy.props.FloatProperty(name="Z軸", default=0.0, subtype='ANGLE', update=update_poly_torus)

    # ==== 投影シミュレーション用 ====
    proj_mode: bpy.props.EnumProperty(
        name="シミュレーションの種類",
        items=[
            ('MODE1', "モード1 (広域・青)", "枠Aと枠Bを繋ぐすべての直線の範囲"),
            ('MODE2', "モード2 (点光源・ピンク)", "指定点PからA・Bを通り抜ける視界・範囲")
        ],
        default='MODE1',
        update=update_proj
    )
    
    # 【新規】指定点 P
    proj_p_loc: bpy.props.FloatVectorProperty(name="位置", default=(0.0, -100.0, 0.0), update=update_proj)
    proj_p_radius: bpy.props.FloatProperty(name="球の半径", default=1.0, min=0.1, update=update_proj)

    # 枠A
    proj_n_a: bpy.props.IntProperty(name="A 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_a: bpy.props.FloatProperty(name="A サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_a: bpy.props.FloatProperty(name="A 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_a: bpy.props.FloatProperty(name="A Y座標", default=-10.0, update=update_proj)
    proj_color_a: bpy.props.FloatVectorProperty(name="A 色", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    
    # 枠B
    proj_n_b: bpy.props.IntProperty(name="B 角数 (4=正)", default=4, min=3, max=256, update=update_proj)
    proj_r_b: bpy.props.FloatProperty(name="B サイズ", default=10.0, min=0.1, update=update_proj)
    proj_thick_b: bpy.props.FloatProperty(name="B 枠の太さ", default=0.2, min=0.01, update=update_proj)
    proj_y_b: bpy.props.FloatProperty(name="B Y座標", default=0.0, update=update_proj)
    proj_color_b: bpy.props.FloatVectorProperty(name="B 色", subtype='COLOR', default=(0.0, 1.0, 0.5), min=0.0, max=1.0, update=update_proj)
    
    # 平面C (到達範囲)
    proj_y_c: bpy.props.FloatProperty(name="C (投影先) Y座標", default=10.0, update=update_proj)
    proj_color_c_m1: bpy.props.FloatVectorProperty(name="C 色(モード1)", subtype='COLOR', default=(0.0, 0.5, 1.0), min=0.0, max=1.0, update=update_proj)
    proj_color_c_m2: bpy.props.FloatVectorProperty(name="C 色(モード2)", subtype='COLOR', default=(1.0, 0.2, 0.8), min=0.0, max=1.0, update=update_proj)

# =========================================================================
# 【オペレーター】
# =========================================================================
class POLY_OT_create_projection(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create_proj"
    bl_label = "投影平面を生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.poly_torus_props
        col = context.collection
        
        mat_a = get_or_create_mat("Proj_Mat_A", props.proj_color_a, 1.0)
        mat_b = get_or_create_mat("Proj_Mat_B", props.proj_color_b, 1.0)
        mat_c = get_or_create_mat("Proj_Mat_C", props.proj_color_c_m1, 0.8)
        mat_p = get_or_create_mat("Proj_Mat_P", (1.0, 0.0, 0.0), 1.0) # Pは赤固定

        def create_or_get(name, mat):
            obj = bpy.data.objects.get(name)
            if not obj:
                obj = bpy.data.objects.new(name, bpy.data.meshes.new(f"{name}_Mesh"))
                col.objects.link(obj)
            if len(obj.data.materials) == 0:
                obj.data.materials.append(mat)
            return obj

        create_or_get("Proj_Plane_A", mat_a)
        create_or_get("Proj_Plane_B", mat_b)
        create_or_get("Proj_Plane_C", mat_c)
        create_or_get("Proj_Point_P", mat_p) # 球体P
        
        update_proj(self, context)
        return {'FINISHED'}

class POLY_OT_create_torus(bpy.types.Operator):
    bl_idname = f"mesh.{PREFIX_NAME.lower()}_create"
    bl_label = "単体多角形を生成"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.active_obj_name:
            old_obj = bpy.data.objects.get(props.active_obj_name)
            if old_obj and "pt_live" in old_obj: del old_obj["pt_live"]
        mat = get_or_create_mat(f"{PREFIX_NAME}_Mat", props.color, props.alpha)
        
        IS_UPDATING = True
        try:
            mesh = get_torus_mesh_by_ops(props.segments, props.radius, props.thick, context)
            obj = bpy.data.objects.new("PolyTorus", mesh)
            context.collection.objects.link(obj)
            obj.data.materials.append(mat)
            obj["pt_live"] = True
            obj.location = (props.loc_x, props.loc_y, props.loc_z)
            obj.rotation_euler = (props.rot_x, props.rot_y, props.rot_z)
            props.active_obj_name = obj.name
            context.view_layer.objects.active = obj
            obj.select_set(True)
        finally:
            IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_detach_torus(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_detach"
    bl_label = "アドオンから切り離し"
    bl_options = {'REGISTER', 'UNDO'}
    def execute(self, context):
        global IS_UPDATING
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        obj = bpy.data.objects.get(props.active_obj_name)
        if obj and "pt_live" in obj: del obj["pt_live"]
        IS_UPDATING = True
        props.active_obj_name = ""
        IS_UPDATING = False
        return {'FINISHED'}

class POLY_OT_modify_radius(bpy.types.Operator):
    bl_idname = f"object.{PREFIX_NAME.lower()}_modify_radius"
    bl_label = "半径を数値修飾"
    bl_options = {'REGISTER', 'UNDO'}
    action: bpy.props.EnumProperty(items=[('MULTIPLY', "Multiply", ""), ('DIVIDE', "Divide", "")])
    def execute(self, context):
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        if props.multiplier_n <= 0: return {'CANCELLED'}
        factor = math.sqrt(props.multiplier_n)
        if self.action == 'MULTIPLY': props.radius *= factor
        elif self.action == 'DIVIDE': props.radius /= factor
        return {'FINISHED'}

class POLY_OT_open_url(bpy.types.Operator):
    bl_idname = f"wm.{PREFIX_NAME.lower()}_open_url"
    bl_label = "URL"
    url: bpy.props.StringProperty()
    def execute(self, context): webbrowser.open(self.url); return {'FINISHED'}

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

# =========================================================================
# 【UIパネル群】
# =========================================================================
class POLY_PT_projection_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_projection_panel"
    bl_label = "直線通過範囲 (シミュレーション)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    
    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        layout.operator(f"mesh.{PREFIX_NAME.lower()}_create_proj", text="シミュレーションを生成", icon='LIGHT_SUN')
        layout.separator()
        
        # モード選択
        layout.prop(props, "proj_mode", expand=False)
        
        # モード2のみ表示する「指定点P」の設定
        if props.proj_mode == 'MODE2':
            box_p = layout.box()
            box_p.label(text="指定点 P (赤い球体・光源)", icon='LIGHT')
            col_p = box_p.column(align=True)
            col_p.prop(props, "proj_p_loc")
            col_p.prop(props, "proj_p_radius")
            layout.separator()
        
        box_a = layout.box()
        box_a.label(text="平面A (窓枠1)", icon='MESH_TORUS')
        col_a = box_a.column(align=True)
        col_a.prop(props, "proj_n_a")
        col_a.prop(props, "proj_r_a")
        col_a.prop(props, "proj_thick_a")
        col_a.prop(props, "proj_y_a")
        col_a.prop(props, "proj_color_a", text="A 色")
        
        box_b = layout.box()
        box_b.label(text="平面B (窓枠2)", icon='MESH_TORUS')
        col_b = box_b.column(align=True)
        col_b.prop(props, "proj_n_b")
        col_b.prop(props, "proj_r_b")
        col_b.prop(props, "proj_thick_b")
        col_b.prop(props, "proj_y_b")
        col_b.prop(props, "proj_color_b", text="B 色")
        
        box_c = layout.box()
        box_c.label(text="平面C (壁・到達範囲)", icon='MESH_PLANE')
        col = box_c.column(align=True)
        col.prop(props, "proj_y_c", text="Cの Y座標")
        
        # モードによって色のプロパティを変える
        if props.proj_mode == 'MODE1':
            col.prop(props, "proj_color_c_m1", text="C 色 (青系)")
        else:
            col.prop(props, "proj_color_c_m2", text="C 色 (ピンク系)")

class POLY_PT_main_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_main_panel"
    bl_label = "単体多角形トーラス (リアルタイム生成)"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        props = getattr(context.scene, f"{PREFIX_NAME.lower()}_props")
        
        if props.active_obj_name and bpy.data.objects.get(props.active_obj_name):
            box_live = layout.box()
            box_live.label(text=f"🔄 編集中: {props.active_obj_name}", icon='EDITMODE_HLT')
            box_live.scale_y = 1.3
            box_live.operator(f"object.{PREFIX_NAME.lower()}_detach", text="アドオンから切り離し", icon='UNLINKED')
            layout.separator()
        else:
            layout.scale_y = 1.3
            layout.operator(f"mesh.{PREFIX_NAME.lower()}_create", text="新しく単体多角形を生成", icon='ADD')
            layout.scale_y = 1.0
            layout.separator()
        
        box_s = layout.box()
        col_s = box_s.column(align=True)
        col_s.prop(props, "segments")
        col_s.separator()
        col_s.prop(props, "radius")
        
        row_n = col_s.row(align=True)
        row_n.prop(props, "multiplier_n")
        op_mult = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× √n")
        op_mult.action = 'MULTIPLY'
        op_div = row_n.operator(f"object.{PREFIX_NAME.lower()}_modify_radius", text="× 1/√n")
        op_div.action = 'DIVIDE'
        
        col_s.separator()
        col_s.prop(props, "thick")
        col_s.separator()
        row_c = col_s.row(align=True)
        row_c.prop(props, "color", text="")
        row_c.prop(props, "alpha", text="透明")

class POLY_PT_footer_panel(bpy.types.Panel):
    bl_idname = f"{PREFIX_NAME.upper()}_PT_footer_panel"
    bl_label = "ドキュメント / 管理"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = TAB_NAME
    bl_order = 100
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        box_links = layout.box()
        box_links.label(text="関連リンク:", icon='BOOKMARKS')
        for link in ADDON_LINKS:
            box_links.operator(f"wm.{PREFIX_NAME.lower()}_open_url", text=link["label"], icon='URL').url = link["url"]
        layout.separator(factor=2.0)
        layout.operator(f"wm.{PREFIX_NAME.lower()}_remove_addon", text="アドオンを無効化して閉じる", icon='CANCEL')

# =========================================================================
# 【登録処理】
# =========================================================================
classes = [
    PolyTorusProperties, 
    POLY_OT_create_torus, 
    POLY_OT_create_projection,
    POLY_OT_detach_torus,
    POLY_OT_modify_radius,
    POLY_OT_open_url, 
    POLY_OT_remove_addon,
    POLY_PT_projection_panel,
    POLY_PT_main_panel, 
    POLY_PT_footer_panel
]

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

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

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