Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= 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_matrix(loc, rot): """位置とオイラー回転(ラジアン)から4x4"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= 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_matrix(loc, rot): """位置とオイラー回転(ラジアン)から4x4"> Sidebar (Nパネル)", "description": "多角形トーラス生成と、独立した2領域間/点光源からの直線通過範囲シミュレーション", "category": "Object", } import bpy import webbrowser import math import bmesh import mathutils # ========================================================================= # 【基本設定】 # ========================================================================= 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_matrix(loc, rot): """位置とオイラー回転(ラジアン)から4x4">

rapture_20260704070559.png

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

import bpy
import webbrowser
import math
import bmesh
import mathutils

# =========================================================================
# 【基本設定】
# =========================================================================
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_matrix(loc, rot):
    """位置とオイラー回転(ラジアン)から4x4行列を作成"""
    euler = mathutils.Euler(rot, 'XYZ')
    mat_rot = euler.to_matrix().to_4x4()
    mat_loc = mathutils.Matrix.Translation(loc)
    return mat_loc @ mat_rot

def decompose_matrix(matrix):
    """4x4行列から位置とオイラー回転を取得"""
    loc = matrix.to_translation()
    rot = matrix.to_euler('XYZ')
    return loc, rot

def get_world_verts(verts_local, loc, rot):
    """ローカル頂点をワールド座標系に変換"""
    mat = get_matrix(loc, rot)
    verts_world = []
    for v in verts_local:
        v_4d = mathutils.Vector((v[0], v[1], v[2], 1.0))
        v_world = (mat @ v_4d).to_3d()
        verts_world.append(tuple(v_world))
    return verts_world

# =========================================================================
# 【純粋数学メッシュ生成:オペレータ不要で安定動作】
# =========================================================================
def get_torus_mesh_pure(n, r, thick):
    """数学的なパラメータから多角形トーラスを直接BMeshで生成"""
    radius = r * math.sqrt(2) if n == 4 else r
    mesh = bpy.data.meshes.new("TorusMesh")
    bm = bmesh.new()
    
    major_segments = n
    minor_segments = 12
    
    verts = []
    for i in range(major_segments):
        theta = 2.0 * math.pi * i / major_segments
        cos_theta = math.cos(theta)
        sin_theta = math.sin(theta)
        
        ring = []
        for j in range(minor_segments):
            phi = 2.0 * math.pi * j / minor_segments
            cos_phi = math.cos(phi)
            sin_phi = math.sin(phi)
            
            x = (radius + thick * cos_phi) * cos_theta
            y = (radius + thick * cos_phi) * sin_theta
            z = thick * sin_phi
            
            ring.append(bm.verts.new((x, y, z)))
        verts.append(ring)
        
    for i in range(major_segments):
        next_i = (i + 1) % major_segments
        for j in range(minor_segments):
            next_j = (j + 1) % minor_segments
            
            v1 = verts[i][j]
            v2 = verts[next_i][j]
            v3 = verts[next_i][next_j]
            v4 = verts[i][next_j]
            
            try:
                bm.faces.new((v1, v2, v3, v4))
            except ValueError:
                pass 
                
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def get_sphere_mesh_pure(r):
    """球体(光源P)をBMeshでオペレータ不使用で安全生成"""
    mesh = bpy.data.meshes.new("SphereMesh")
    bm = bmesh.new()
    bmesh.ops.create_icosphere(bm, subdivisions=3, radius=r)
    bm.to_mesh(mesh)
    bm.free()
    return mesh

# =========================================================================
# 【ヘルパー関数:マテリアル・メッシュ生成】
# =========================================================================
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_local(n, r):
    """平面上(Y=0)のローカルポリゴン頂点座標を取得"""
    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), 0.0, 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_cone_skirt_mesh(p_loc, va, mesh_name):
    """Pから平面Aへのスカート (底面なしコーン)"""
    mesh = bpy.data.meshes.new(mesh_name)
    if len(va) < 3: return mesh
    bm = bmesh.new()
    v_p = bm.verts.new(p_loc)
    v_as = [bm.verts.new(v) for v in va]
    for k in range(len(v_as)):
        v1 = v_as[k]
        v2 = v_as[(k + 1) % len(v_as)]
        try:
            bm.faces.new((v_p, v2, v1))
        except ValueError:
            pass
    bm.to_mesh(mesh)
    bm.free()
    return mesh

def create_frustum_skirt_mesh(p_loc, vc, normal_a, a_loc, mesh_name):
    """平面Aから平面Cへのスカート (底面・上面なし錐台 / 円錐台)"""
    mesh = bpy.data.meshes.new(mesh_name)
    if len(vc) < 3: return mesh
    bm = bmesh.new()
    p = mathutils.Vector(p_loc)
    a_l = mathutils.Vector(a_loc)
    
    va_projected = []
    for v in vc:
        c_k = mathutils.Vector(v)
        dir_v = c_k - p
        denom = dir_v.dot(normal_a)
        if abs(denom) > 1e-6:
            t = (a_l - p).dot(normal_a) / denom
            a_k = p + t * dir_v
        else:
            a_k = c_k
        va_projected.append(tuple(a_k))
        
    v_cs = [bm.verts.new(v) for v in vc]
    v_aps = [bm.verts.new(v) for v in va_projected]
    
    for k in range(len(vc)):
        next_k = (k + 1) % len(vc)
        try:
            bm.faces.new((v_aps[k], v_cs[k], v_cs[next_k], v_aps[next_k]))
        except ValueError:
            pass
            
    bm.to_mesh(mesh)
    bm.free()
    return mesh

# =========================================================================
# 【数学・幾何学ロジック (シミュレーション計算)】
# =========================================================================
def get_projected_verts_mode1(verts_a, verts_b, y_c):
    """任意の3D点集合A、Bを結ぶ直線をY=y_c平面に投影"""
    pts = []
    for pa in verts_a:
        for pb in verts_b:
            dy = pb[1] - pa[1]
            if abs(dy) < 0.0001: continue
            t = (y_c - pa[1]) / dy
            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_a = bpy.data.objects.get("Proj_Skirt_A")
    obj_skirt_c = bpy.data.objects.get("Proj_Skirt_C")
    
    if not (obj_a and obj_b and obj_c and obj_p and obj_skirt_a and obj_skirt_c): return
    
    IS_UPDATING = True
    try:
        n_a, r_a, thick_a = props.proj_n_a, props.proj_r_a, props.proj_thick_a
        n_b, r_b, thick_b = props.proj_n_b, props.proj_r_b, props.proj_thick_b
        y_c = props.proj_y_c
        
        def update_torus(obj, n, r, thick, loc, rot, color):
            mesh = get_torus_mesh_pure(n, r, thick)
            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)
            
            mat_base = mathutils.Euler((math.pi / 2, math.pi / 4 if n == 4 else 0.0, 0.0)).to_matrix().to_4x4()
            mat_user = get_matrix(loc, rot)
            mat_final = mat_user @ mat_base
            
            obj.location = mat_final.to_translation()
            obj.rotation_euler = mat_final.to_euler('XYZ')
            if old_m: bpy.data.meshes.remove(old_m)

        update_torus(obj_a, n_a, r_a, thick_a, props.proj_a_loc, props.proj_a_rot, props.proj_color_a)
        update_torus(obj_b, n_b, r_b, thick_b, props.proj_b_loc, props.proj_b_rot, props.proj_color_b)

        if props.proj_mode == 'MODE2':
            obj_p.hide_viewport = False
            mesh_p = get_sphere_mesh_pure(props.proj_p_radius)
            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)
            
            mat_p_user = get_matrix(props.proj_p_loc, props.proj_p_rot)
            obj_p.location = mat_p_user.to_translation()
            obj_p.rotation_euler = mat_p_user.to_euler('XYZ')
        else:
            obj_p.hide_viewport = True

        # ワールド空間上の頂点座標配列を算出
        va = get_world_verts(get_polygon_verts_local(n_a, r_a), props.proj_a_loc, props.proj_a_rot)
        vb = get_world_verts(get_polygon_verts_local(n_b, r_b), props.proj_b_loc, props.proj_b_rot)
        
        if props.proj_mode == 'MODE1':
            vc = get_projected_verts_mode1(va, vb, 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:
            obj_skirt_a.hide_viewport = False
            obj_skirt_c.hide_viewport = False
            
            # 平面Aのワールド法線
            mat_a_rot = mathutils.Euler(props.proj_a_rot, 'XYZ').to_matrix()
            normal_a = mat_a_rot @ mathutils.Vector((0.0, 1.0, 0.0))
            normal_a.normalize()
            
            # 1. Pから平面Aへのスカート (コーン)
            mesh_skirt_a = create_cone_skirt_mesh(props.proj_p_loc, va, "Mesh_Skirt_A")
            old_m_skirt_a = obj_skirt_a.data
            mat_skirt_a = obj_skirt_a.active_material
            obj_skirt_a.data = mesh_skirt_a
            if mat_skirt_a:
                obj_skirt_a.data.materials.append(mat_skirt_a)
                update_skirt_material(mat_skirt_a, props.proj_color_skirt_a, props.proj_alpha_skirt_a)
            if old_m_skirt_a: bpy.data.meshes.remove(old_m_skirt_a)
            obj_skirt_a.location = (0, 0, 0)
            obj_skirt_a.rotation_euler = (0, 0, 0)
            
            # 2. 平面Aから平面Cへのスカート (錐台)
            mesh_skirt_c = create_frustum_skirt_mesh(props.proj_p_loc, vc, normal_a, props.proj_a_loc, "Mesh_Skirt_C")
            old_m_skirt_c = obj_skirt_c.data
            mat_skirt_c = obj_skirt_c.active_material
            obj_skirt_c.data = mesh_skirt_c
            if mat_skirt_c:
                obj_skirt_c.data.materials.append(mat_skirt_c)
                update_skirt_material(mat_skirt_c, props.proj_color_skirt_c, props.proj_alpha_skirt_c)
            if old_m_skirt_c: bpy.data.meshes.remove(old_m_skirt_c)
            obj_skirt_c.location = (0, 0, 0)
            obj_skirt_c.rotation_euler = (0, 0, 0)
        else:
            obj_skirt_a.hide_viewport = True
            obj_skirt_c.hide_viewport = True

    finally:
        IS_UPDATING = False

# =========================================================================
# 【連動制御用アップデート関数】
# =========================================================================
def update_proj_link_p_a(self, context):
    props = context.scene.poly_torus_props
    if props.proj_link_p_a:
        mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
        mat_a = get_matrix(props.proj_a_loc, props.proj_a_rot)
        mat_rel = mat_p.inverted() @ mat_a
        props.proj_p_a_rel_matrix = [val for row in mat_rel for val in row]

def update_proj_p_transform(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:
            mat_rel = mathutils.Matrix([props.proj_p_a_rel_matrix[i:i+4] for i in range(0, 16, 4)])
            mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
            mat_a_new = mat_p @ mat_rel
            loc_a, rot_a = decompose_matrix(mat_a_new)
            props.proj_a_loc = loc_a
            props.proj_a_rot = rot_a
        finally:
            IS_UPDATING = False
    update_proj(self, context)

def update_proj_a_transform(self, context):
    global IS_UPDATING
    if IS_UPDATING: return
    props = context.scene.poly_torus_props
    if props.proj_link_p_a:
        mat_p = get_matrix(props.proj_p_loc, props.proj_p_rot)
        mat_a = get_matrix(props.proj_a_loc, props.proj_a_rot)
        mat_rel = mat_p.inverted() @ mat_a
        props.proj_p_a_rel_matrix = [val for row in mat_rel for val in row]
    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_pure(props.segments, props.radius, props.thick)
        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 (位置と回転 - 初期位置 Y=-50.0 に変更)
    proj_p_loc: bpy.props.FloatVectorProperty(name="P 位置", default=(0.0, -50.0, 0.0), update=update_proj_p_transform)
    proj_p_rot: bpy.props.FloatVectorProperty(name="P 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj_p_transform)
    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を完全連動 (位置・回転)", default=False, update=update_proj_link_p_a)
    proj_p_a_rel_matrix: bpy.props.FloatVectorProperty(name="相対トランスフォーム行列", size=16, default=[0.0]*16)

    # スカート機能
    proj_show_skirt: bpy.props.BoolProperty(name="光の道筋 (スカート) を表示", default=True, update=update_proj)
    
    # スカート1 (P-A 軌跡/コーン) 設定
    proj_color_skirt_a: bpy.props.FloatVectorProperty(name="P-A色", subtype='COLOR', default=(1.0, 0.4, 0.1), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt_a: bpy.props.FloatProperty(name="P-A透明度", default=0.4, min=0.0, max=1.0, update=update_proj)

    # スカート2 (A-C 軌跡/錐台) 設定
    proj_color_skirt_c: bpy.props.FloatVectorProperty(name="A-C色", subtype='COLOR', default=(1.0, 0.8, 0.2), min=0.0, max=1.0, update=update_proj)
    proj_alpha_skirt_c: bpy.props.FloatProperty(name="A-C透明度", default=0.5, min=0.0, max=1.0, update=update_proj)

    # 枠A (位置・回転を3D化)
    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_a_loc: bpy.props.FloatVectorProperty(name="A 位置", default=(0.0, -5.0, 0.0), update=update_proj_a_transform)
    proj_a_rot: bpy.props.FloatVectorProperty(name="A 回転", subtype='EULER', default=(0.0, 0.0, 0.0), update=update_proj_a_transform)
    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 (位置・回転を3D化)
    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_b_loc: bpy.props.FloatVectorProperty(name="B 位置", default=(0.0, 0.0, 0.0), update=update_proj)
    proj_b_rot: bpy.props.FloatVectorProperty(name="B 回転", subtype='EULER', default=(0.0, 0.0, 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_reset_view_and_p(bpy.types.Operator):
    """球体Pを現在の画面注視点にリセットし、フロント・透視投影ビューにする"""
    bl_idname = f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p"
    bl_label = "視線と球体Pを初期化"
    bl_options = {'REGISTER', 'UNDO'}

    def execute(self, context):
        props = context.scene.poly_torus_props
        
        # 3Dビューポートの領域情報(region_3d)を取得
        rv3d = None
        for area in context.screen.areas:
            if area.type == 'VIEW_3D':
                rv3d = area.spaces.active.region_3d
                break
                
        if not rv3d:
            self.report({'WARNING'}, "3Dビューポートが見つかりません。")
            return {'CANCELLED'}
            
        # 1. 球体Pを画面の真ん中(現在の注視点の座標)に移動
        props.proj_p_loc = rv3d.view_location
        
        # 2. 視線角度を「Y軸プラスが奥行き」のフロント方向に切り替え (オイラー角: 90, 0, 0 の回転)
        rv3d.view_rotation = mathutils.Quaternion((0.7071068, 0.7071068, 0.0, 0.0))
        
        # 3. 投影モードを透視投影(Perspective)に設定
        rv3d.view_perspective = 'PERSP'
        
        # 4. スカート等の表示シミュレーションを更新
        update_proj(self, context)
        
        self.report({'INFO'}, "ビューと球体Pをリセットしました。")
        return {'FINISHED'}

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_a = get_or_create_mat("Proj_Mat_Skirt_A", props.proj_color_skirt_a, props.proj_alpha_skirt_a) 
        mat_skirt_c = get_or_create_mat("Proj_Mat_Skirt_C", props.proj_color_skirt_c, props.proj_alpha_skirt_c) 

        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_A", mat_skirt_a)
        create_or_get("Proj_Skirt_C", mat_skirt_c)
        
        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_pure(props.segments, props.radius, props.thick)
            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")
        
        # 一番上に、ビューリセット&球体Pの初期化ボタンを配置
        layout.operator(f"view3d.{PREFIX_NAME.lower()}_reset_view_and_p", text="視点と球体Pの位置をリセット", icon='VIEW_CAMERA')
        layout.separator()
        
        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", text="位置")
            col_p.prop(props, "proj_p_rot", text="回転")
            
            col_p.prop(props, "proj_link_p_a", text="球体Pと平面Aを完全連動 (位置・回転)", 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.label(text="光の道筋 (スカート) 設定", icon='OUTLINER_OB_LIGHT')
                box_s.prop(props, "proj_show_skirt", text="スカートを表示する")
                
                # スカートAの設定項目
                col_sa = box_s.column(align=True)
                col_sa.label(text="P-A 軌跡 (コーン):")
                row_sa = col_sa.row(align=True)
                row_sa.prop(props, "proj_color_skirt_a", text="")
                row_sa.prop(props, "proj_alpha_skirt_a", text="透明度")
                
                # スカートCの設定項目
                col_sc = box_s.column(align=True)
                col_sc.label(text="A-C 軌跡 (円錐台):")
                row_sc = col_sc.row(align=True)
                row_sc.prop(props, "proj_color_skirt_c", text="")
                row_sc.prop(props, "proj_alpha_skirt_c", 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.separator()
        col_a.prop(props, "proj_a_loc", text="位置")
        col_a.prop(props, "proj_a_rot", text="回転")
        col_a.separator()
        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.separator()
        col_b.prop(props, "proj_b_loc", text="位置")
        col_b.prop(props, "proj_b_rot", text="回転")
        col_b.separator()
        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_reset_view_and_p,
    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()